diff --git a/.buildkite/scripts/common/env.sh b/.buildkite/scripts/common/env.sh index 901fa9f50a61..b1701603ec08 100755 --- a/.buildkite/scripts/common/env.sh +++ b/.buildkite/scripts/common/env.sh @@ -70,3 +70,4 @@ export TEST_ES_URL="http://elastic:changeme@localhost:6102" export TEST_ES_TRANSPORT_PORT=6301-6309 export TEST_CORS_SERVER_PORT=6106 export ALERTING_PROXY_PORT=6105 +export TEST_PROXY_SERVER_PORT=6107 diff --git a/.ci/Dockerfile b/.ci/Dockerfile index 947242ecc0ec..d3ea74ca3896 100644 --- a/.ci/Dockerfile +++ b/.ci/Dockerfile @@ -1,7 +1,7 @@ # NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable. # If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts -ARG NODE_VERSION=14.17.5 +ARG NODE_VERSION=14.17.6 FROM node:${NODE_VERSION} AS base diff --git a/.ci/Jenkinsfile_baseline_trigger b/.ci/Jenkinsfile_baseline_trigger index 221b7a44e30d..fd1c267fb330 100644 --- a/.ci/Jenkinsfile_baseline_trigger +++ b/.ci/Jenkinsfile_baseline_trigger @@ -1,5 +1,7 @@ #!/bin/groovy +return + def MAXIMUM_COMMITS_TO_CHECK = 10 def MAXIMUM_COMMITS_TO_BUILD = 5 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 74a206ea98e0..381fad404ca7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -12,31 +12,34 @@ # Virtual teams /x-pack/plugins/rule_registry/ @elastic/rac -# App -/x-pack/plugins/discover_enhanced/ @elastic/kibana-app -/x-pack/plugins/lens/ @elastic/kibana-app -/x-pack/plugins/graph/ @elastic/kibana-app -/src/plugins/advanced_settings/ @elastic/kibana-app -/src/plugins/charts/ @elastic/kibana-app -/src/plugins/discover/ @elastic/kibana-app -/src/plugins/management/ @elastic/kibana-app -/src/plugins/kibana_legacy/ @elastic/kibana-app -/src/plugins/timelion/ @elastic/kibana-app -/src/plugins/vis_default_editor/ @elastic/kibana-app -/src/plugins/vis_type_metric/ @elastic/kibana-app -/src/plugins/vis_type_table/ @elastic/kibana-app -/src/plugins/vis_type_tagcloud/ @elastic/kibana-app -/src/plugins/vis_type_timelion/ @elastic/kibana-app -/src/plugins/vis_type_timeseries/ @elastic/kibana-app -/src/plugins/vis_type_vega/ @elastic/kibana-app -/src/plugins/vis_types/vislib/ @elastic/kibana-app -/src/plugins/vis_types/xy/ @elastic/kibana-app -/src/plugins/vis_types/pie/ @elastic/kibana-app -/src/plugins/visualize/ @elastic/kibana-app -/src/plugins/visualizations/ @elastic/kibana-app -/src/plugins/chart_expressions/expression_tagcloud/ @elastic/kibana-app -/src/plugins/url_forwarding/ @elastic/kibana-app -/packages/kbn-tinymath/ @elastic/kibana-app +# Data Discovery +/src/plugins/discover/ @elastic/kibana-data-discovery +/x-pack/plugins/discover_enhanced/ @elastic/kibana-data-discovery +/test/functional/apps/discover/ @elastic/kibana-data-discovery + +# Vis Editors +/x-pack/plugins/lens/ @elastic/kibana-vis-editors +/x-pack/plugins/graph/ @elastic/kibana-vis-editors +/src/plugins/advanced_settings/ @elastic/kibana-vis-editors +/src/plugins/charts/ @elastic/kibana-vis-editors +/src/plugins/management/ @elastic/kibana-vis-editors +/src/plugins/kibana_legacy/ @elastic/kibana-vis-editors +/src/plugins/timelion/ @elastic/kibana-vis-editors +/src/plugins/vis_default_editor/ @elastic/kibana-vis-editors +/src/plugins/vis_type_metric/ @elastic/kibana-vis-editors +/src/plugins/vis_type_table/ @elastic/kibana-vis-editors +/src/plugins/vis_type_tagcloud/ @elastic/kibana-vis-editors +/src/plugins/vis_type_timelion/ @elastic/kibana-vis-editors +/src/plugins/vis_type_timeseries/ @elastic/kibana-vis-editors +/src/plugins/vis_type_vega/ @elastic/kibana-vis-editors +/src/plugins/vis_types/vislib/ @elastic/kibana-vis-editors +/src/plugins/vis_types/xy/ @elastic/kibana-vis-editors +/src/plugins/vis_types/pie/ @elastic/kibana-vis-editors +/src/plugins/visualize/ @elastic/kibana-vis-editors +/src/plugins/visualizations/ @elastic/kibana-vis-editors +/src/plugins/chart_expressions/expression_tagcloud/ @elastic/kibana-vis-editors +/src/plugins/url_forwarding/ @elastic/kibana-vis-editors +/packages/kbn-tinymath/ @elastic/kibana-vis-editors # Application Services /examples/bfetch_explorer/ @elastic/kibana-app-services @@ -436,6 +439,9 @@ /x-pack/test/reporting_api_integration/ @elastic/kibana-reporting-services @elastic/kibana-app-services /x-pack/test/reporting_functional/ @elastic/kibana-reporting-services @elastic/kibana-app-services /x-pack/test/stack_functional_integration/apps/reporting/ @elastic/kibana-reporting-services @elastic/kibana-app-services +/docs/user/reporting @elastic/kibana-reporting-services @elastic/kibana-app-services +/docs/settings/reporting-settings.asciidoc @elastic/kibana-reporting-services @elastic/kibana-app-services +/docs/setup/configuring-reporting.asciidoc @elastic/kibana-reporting-services @elastic/kibana-app-services #CC# /x-pack/plugins/reporting/ @elastic/kibana-reporting-services diff --git a/.github/workflows/project-assigner.yml b/.github/workflows/project-assigner.yml index 7e658197c7a8..8b32b7d699c7 100644 --- a/.github/workflows/project-assigner.yml +++ b/.github/workflows/project-assigner.yml @@ -14,7 +14,7 @@ jobs: issue-mappings: | [ {"label": "Feature:Lens", "projectNumber": 32, "columnName": "Long-term goals"}, - {"label": "Feature:Discover", "projectNumber": 44, "columnName": "Inbox"}, + {"label": "Team:DataDiscovery", "projectNumber": 44, "columnName": "Inbox"}, {"label": "Feature:Canvas", "projectNumber": 38, "columnName": "Inbox"}, {"label": "Feature:Dashboard", "projectNumber": 68, "columnName": "Inbox"}, {"label": "Feature:Drilldowns", "projectNumber": 68, "columnName": "Inbox"}, diff --git a/.i18nrc.json b/.i18nrc.json index 3301cd04ad06..f38d6b8faae7 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -2,6 +2,7 @@ "paths": { "alerts": "packages/kbn-alerts/src", "autocomplete": "packages/kbn-securitysolution-autocomplete/src", + "kbnConfig": "packages/kbn-config/src", "console": "src/plugins/console", "core": "src/core", "discover": "src/plugins/discover", diff --git a/.node-version b/.node-version index 18711d290eac..5595ae1aa9e4 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -14.17.5 +14.17.6 diff --git a/.nvmrc b/.nvmrc index 18711d290eac..5595ae1aa9e4 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14.17.5 +14.17.6 diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 384277822709..3ae3f202a3bf 100644 --- a/WORKSPACE.bazel +++ b/WORKSPACE.bazel @@ -27,13 +27,13 @@ check_rules_nodejs_version(minimum_version_string = "3.8.0") # we can update that rule. node_repositories( node_repositories = { - "14.17.5-darwin_amd64": ("node-v14.17.5-darwin-x64.tar.gz", "node-v14.17.5-darwin-x64", "2e40ab625b45b9bdfcb963ddd4d65d87ddf1dd37a86b6f8b075cf3d77fe9dc09"), - "14.17.5-linux_arm64": ("node-v14.17.5-linux-arm64.tar.xz", "node-v14.17.5-linux-arm64", "3a2e674b6db50dfde767c427e8f077235bbf6f9236e1b12a4cc3496b12f94bae"), - "14.17.5-linux_s390x": ("node-v14.17.5-linux-s390x.tar.xz", "node-v14.17.5-linux-s390x", "7d40eee3d54241403db12fb3bc420cd776e2b02e89100c45cf5e74a73942e7f6"), - "14.17.5-linux_amd64": ("node-v14.17.5-linux-x64.tar.xz", "node-v14.17.5-linux-x64", "2d759de07a50cd7f75bd73d67e97b0d0e095ee3c413efac7d1b3d1e84ed76fff"), - "14.17.5-windows_amd64": ("node-v14.17.5-win-x64.zip", "node-v14.17.5-win-x64", "a99b7ee08e846e5d1f4e70c4396265542819d79ed9cebcc27760b89571f03cbf"), + "14.17.6-darwin_amd64": ("node-v14.17.6-darwin-x64.tar.gz", "node-v14.17.6-darwin-x64", "e3e4c02240d74fb1dc8a514daa62e5de04f7eaee0bcbca06a366ece73a52ad88"), + "14.17.6-linux_arm64": ("node-v14.17.6-linux-arm64.tar.xz", "node-v14.17.6-linux-arm64", "9c4f3a651e03cd9b5bddd33a80e8be6a6eb15e518513e410bb0852a658699156"), + "14.17.6-linux_s390x": ("node-v14.17.6-linux-s390x.tar.xz", "node-v14.17.6-linux-s390x", "3677f35b97608056013b5368f86eecdb044bdccc1b3976c1d4448736c37b6a0c"), + "14.17.6-linux_amd64": ("node-v14.17.6-linux-x64.tar.xz", "node-v14.17.6-linux-x64", "3bbe4faf356738d88b45be222bf5e858330541ff16bd0d4cfad36540c331461b"), + "14.17.6-windows_amd64": ("node-v14.17.6-win-x64.zip", "node-v14.17.6-win-x64", "b83e9ce542fda7fc519cec6eb24a2575a84862ea4227dedc171a8e0b5b614ac0"), }, - node_version = "14.17.5", + node_version = "14.17.6", node_urls = [ "https://nodejs.org/dist/v{version}/{filename}", ], diff --git a/dev_docs/building_blocks.mdx b/dev_docs/building_blocks.mdx index 327492a20d5b..6320a7db4558 100644 --- a/dev_docs/building_blocks.mdx +++ b/dev_docs/building_blocks.mdx @@ -62,7 +62,7 @@ Check out the Lens Embeddable if you wish to show users visualizations based on and . Using the same configuration, it's also possible to link to a prefilled Lens editor, allowing the user to drill deeper and explore their data. -**Github labels**: `Team:KibanaApp`, `Feature:Lens` +**Github labels**: `Team:VisEditors`, `Feature:Lens` ### Map Embeddable diff --git a/docs/api/spaces-management.asciidoc b/docs/api/spaces-management.asciidoc index 2e3b9abec912..333a06cf3754 100644 --- a/docs/api/spaces-management.asciidoc +++ b/docs/api/spaces-management.asciidoc @@ -20,6 +20,8 @@ The following {kib} spaces APIs are available: * <> to overwrite saved objects returned as errors from the copy saved objects to space API +* <> to disable legacy URL aliases if an error is encountered + include::spaces-management/post.asciidoc[] include::spaces-management/put.asciidoc[] include::spaces-management/get.asciidoc[] @@ -27,3 +29,4 @@ include::spaces-management/get_all.asciidoc[] include::spaces-management/delete.asciidoc[] include::spaces-management/copy_saved_objects.asciidoc[] include::spaces-management/resolve_copy_saved_objects_conflicts.asciidoc[] +include::spaces-management/disable_legacy_url_aliases.asciidoc[] diff --git a/docs/api/spaces-management/disable_legacy_url_aliases.asciidoc b/docs/api/spaces-management/disable_legacy_url_aliases.asciidoc new file mode 100644 index 000000000000..3f713d9d0c25 --- /dev/null +++ b/docs/api/spaces-management/disable_legacy_url_aliases.asciidoc @@ -0,0 +1,59 @@ +[[spaces-api-disable-legacy-url-aliases]] +=== Disable legacy URL aliases API +++++ +Disable legacy URL aliases +++++ + +experimental[] Disable a <> in {kib}. + +[[spaces-api-disable-legacy-url-aliases-request]] +==== {api-request-title} + +`POST :/api/spaces/_disable_legacy_url_aliases` + +[role="child_attributes"] +[[spaces-api-disable-legacy-url-aliases-request-body]] +==== {api-request-body-title} + +`aliases`:: + (Required, object array) The aliases to disable. ++ +.Properties of `aliases` +[%collapsible%open] +===== + `targetSpace`::: + (Required, string) The space where the alias target object exists. + + `targetType`::: + (Required, string) The type of the alias target object. + + `sourceId`::: + (Required, string) The ID of the alias source object. This is the "legacy" object ID. +===== + +[[spaces-api-disable-legacy-url-aliases-response-codes]] +==== {api-response-codes-title} + +`204`:: + Indicates a successful call. + +[[spaces-api-disable-legacy-url-aliases-example]] +==== {api-examples-title} + +[source,sh] +-------------------------------------------------- +$ curl -X POST api/spaces/_disable_legacy_url_aliases +{ + "aliases": [ + { + "targetSpace": "bills-space", + "targetType": "dashboard", + "sourceId": "123" + } + ] +} +-------------------------------------------------- +// KIBANA + +This example leaves the alias intact, but the legacy URL for this alias, http://localhost:5601/s/bills-space/app/dashboards#/view/123, will +no longer function. The dashboard still exists, and you can access it with the new URL. \ No newline at end of file diff --git a/docs/developer/advanced/index.asciidoc b/docs/developer/advanced/index.asciidoc index 289b88cddd7a..27072d85b650 100644 --- a/docs/developer/advanced/index.asciidoc +++ b/docs/developer/advanced/index.asciidoc @@ -6,6 +6,7 @@ * <> * <> * <> +* <> include::development-es-snapshots.asciidoc[leveloffset=+1] @@ -15,4 +16,6 @@ include::development-basepath.asciidoc[leveloffset=+1] include::upgrading-nodejs.asciidoc[leveloffset=+1] -include::sharing-saved-objects.asciidoc[leveloffset=+1] \ No newline at end of file +include::sharing-saved-objects.asciidoc[leveloffset=+1] + +include::legacy-url-aliases.asciidoc[leveloffset=+1] \ No newline at end of file diff --git a/docs/developer/advanced/legacy-url-aliases.asciidoc b/docs/developer/advanced/legacy-url-aliases.asciidoc new file mode 100644 index 000000000000..3e441dd58212 --- /dev/null +++ b/docs/developer/advanced/legacy-url-aliases.asciidoc @@ -0,0 +1,45 @@ +[[legacy-url-aliases]] +== Legacy URL Aliases + +This page describes legacy URL aliases: what they are, where they come from, and how to disable them. + +[[legacy-url-aliases-overview]] +=== Overview + +Many saved object types were converted in {kib} 8.0, so they can eventually be shared across <>. Before 8.0, you could +have two objects with the same type and same ID in two different spaces. Part of this conversion is to make sure all object IDs of a given +type are *globally unique across all spaces*. + +{kib} creates a special entity called a **legacy URL alias** for each saved object that requires a new ID. This legacy URL alias allows +{kib} to preserve any deep link URLs that exist for these objects. + +[[legacy-url-aliases-example]] +=== Example + +Consider the following scenario: + +You have {kib} 7.16, and you create a new dashboard.The ID of this dashboard is "123". You create a new space called "Bill's space" and +<> your dashboard to the other space. Now you have two different dashboards that can be accessed +at the following URLs: + +* *Default space*: `http://localhost:5601/app/dashboards#/view/123` +* *Bill's space*: `http://localhost:5601/s/bills-space/app/dashboards#/view/123` + +You use these two dashboards frequently, so you bookmark them in your web browser. After some time, you decide to upgrade to {kib} 8.0. When +these two dashboards go through the conversion process, the one in "Bill's space" will have its ID changed to "456". The URL to access that +dashboard is different -- not to worry though, there is a legacy URL alias for that dashboard. + +If you use your bookmark to access that dashboard using its old URL, {kib} detects that you are using a legacy URL, and finds the new object +ID. If you navigate to `http://localhost:5601/s/bills-space/app/dashboards#/view/123`, you'll see a message indicating that the dashboard +has a new URL, and you're automatically redirected to `http://localhost:5601/s/bills-space/app/dashboards#/view/456`. + +[[legacy-url-aliases-handling-errors]] +=== Handling errors + +Legacy URL aliases are intended to be fully transparent, but there are rare situations where this can lead to an error. For example, you +might have a dashboard and one of the visualizations fails to load, directing you to this page. If you encounter an error in this situation, +you might want to disable the legacy URL alias completely. This leaves the saved object intact, and you will not lose any data -- you just +won't be able to use the old URL to access that saved object. + +To disable a legacy URL alias, you need three pieces of information: the `targetSpace`, the `targetType`, and the `sourceId`. Then use the +<> API to disable the problematic legacy URL alias. diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index cadb34ae63b8..26d0c38f72fd 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -128,6 +128,7 @@ readonly links: { readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { + readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index aded69733b58..aa3f95801804 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
readonly ecs: {
readonly guide: string;
};
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly canvas: {
readonly guide: string;
};
readonly dashboard: {
readonly guide: string;
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly discover: Record<string, string>;
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly elasticsearchModule: string;
readonly startup: string;
readonly exportedFields: string;
readonly suricataModule: string;
readonly zeekModule: string;
};
readonly auditbeat: {
readonly base: string;
readonly auditdModule: string;
readonly systemModule: string;
};
readonly metricbeat: {
readonly base: string;
readonly configure: string;
readonly httpEndpoint: string;
readonly install: string;
readonly start: string;
};
readonly enterpriseSearch: {
readonly base: string;
readonly appSearchBase: string;
readonly workplaceSearchBase: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly libbeat: {
readonly getStarted: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly composite: string;
readonly composite_missing_bucket: string;
readonly date_histogram: string;
readonly date_range: string;
readonly date_format_pattern: string;
readonly filter: string;
readonly filters: string;
readonly geohash_grid: string;
readonly histogram: string;
readonly ip_range: string;
readonly range: string;
readonly significant_terms: string;
readonly terms: string;
readonly avg: string;
readonly avg_bucket: string;
readonly max_bucket: string;
readonly min_bucket: string;
readonly sum_bucket: string;
readonly cardinality: string;
readonly count: string;
readonly cumulative_sum: string;
readonly derivative: string;
readonly geo_bounds: string;
readonly geo_centroid: string;
readonly max: string;
readonly median: string;
readonly min: string;
readonly moving_avg: string;
readonly percentile_ranks: string;
readonly serial_diff: string;
readonly std_dev: string;
readonly sum: string;
readonly top_hits: string;
};
readonly runtimeFields: {
readonly overview: string;
readonly mapping: string;
};
readonly scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessLangSpec: string;
readonly painlessSyntax: string;
readonly painlessWalkthrough: string;
readonly luceneExpressions: string;
};
readonly search: {
readonly sessions: string;
readonly sessionLimits: string;
};
readonly indexPatterns: {
readonly introduction: string;
readonly fieldFormattersNumber: string;
readonly fieldFormattersString: string;
readonly runtimeFields: string;
};
readonly addData: string;
readonly kibana: string;
readonly upgradeAssistant: string;
readonly rollupJobs: string;
readonly elasticsearch: Record<string, string>;
readonly siem: {
readonly privileges: string;
readonly guide: string;
readonly gettingStarted: string;
readonly ml: string;
readonly ruleChangeLog: string;
readonly detectionsReq: string;
readonly networkMap: string;
};
readonly query: {
readonly eql: string;
readonly kueryQuerySyntax: string;
readonly luceneQuerySyntax: string;
readonly percolate: string;
readonly queryDsl: string;
readonly autocompleteChanges: string;
};
readonly date: {
readonly dateMath: string;
readonly dateMathIndexNames: string;
};
readonly management: Record<string, string>;
readonly ml: Record<string, string>;
readonly transforms: Record<string, string>;
readonly visualize: Record<string, string>;
readonly apis: Readonly<{
bulkIndexAlias: string;
byteSizeUnits: string;
createAutoFollowPattern: string;
createFollower: string;
createIndex: string;
createSnapshotLifecyclePolicy: string;
createRoleMapping: string;
createRoleMappingTemplates: string;
createRollupJobsRequest: string;
createApiKey: string;
createPipeline: string;
createTransformRequest: string;
cronExpressions: string;
executeWatchActionModes: string;
indexExists: string;
openIndex: string;
putComponentTemplate: string;
painlessExecute: string;
painlessExecuteAPIContexts: string;
putComponentTemplateMetadata: string;
putSnapshotLifecyclePolicy: string;
putIndexTemplateV1: string;
putWatch: string;
simulatePipeline: string;
timeUnits: string;
updateTransform: string;
}>;
readonly observability: Record<string, string>;
readonly alerting: Record<string, string>;
readonly maps: Record<string, string>;
readonly monitoring: Record<string, string>;
readonly security: Readonly<{
apiKeyServiceSettings: string;
clusterPrivileges: string;
elasticsearchSettings: string;
elasticsearchEnableSecurity: string;
indicesPrivileges: string;
kibanaTLS: string;
kibanaPrivileges: string;
mappingRoles: string;
mappingRolesFieldRules: string;
runAsPrivilege: string;
}>;
readonly watcher: Record<string, string>;
readonly ccs: Record<string, string>;
readonly plugins: Record<string, string>;
readonly snapshotRestore: Record<string, string>;
readonly ingest: Record<string, string>;
readonly fleet: Readonly<{
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
upgradeElasticAgent: string;
upgradeElasticAgent712lower: string;
}>;
readonly ecs: {
readonly guide: string;
};
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md index e5f08213da51..bd0fc1e5b371 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md @@ -18,6 +18,7 @@ export interface DeprecationsDetails | [deprecationType](./kibana-plugin-core-server.deprecationsdetails.deprecationtype.md) | 'config' | 'feature' | (optional) Used to identify between different deprecation types. Example use case: in Upgrade Assistant, we may want to allow the user to sort by deprecation type or show each type in a separate tab.Feel free to add new types if necessary. Predefined types are necessary to reduce having similar definitions with different keywords across kibana deprecations. | | [documentationUrl](./kibana-plugin-core-server.deprecationsdetails.documentationurl.md) | string | | | [level](./kibana-plugin-core-server.deprecationsdetails.level.md) | 'warning' | 'critical' | 'fetch_error' | levels: - warning: will not break deployment upon upgrade - critical: needs to be addressed before upgrade. - fetch\_error: Deprecations service failed to grab the deprecation details for the domain. | -| [message](./kibana-plugin-core-server.deprecationsdetails.message.md) | string | | +| [message](./kibana-plugin-core-server.deprecationsdetails.message.md) | string | The description message to be displayed for the deprecation. Check the README for writing deprecations in src/core/server/deprecations/README.mdx | | [requireRestart](./kibana-plugin-core-server.deprecationsdetails.requirerestart.md) | boolean | | +| [title](./kibana-plugin-core-server.deprecationsdetails.title.md) | string | The title of the deprecation. Check the README for writing deprecations in src/core/server/deprecations/README.mdx | diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.message.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.message.md index d79a4c9bd799..906ce8118f95 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.message.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.message.md @@ -4,6 +4,8 @@ ## DeprecationsDetails.message property +The description message to be displayed for the deprecation. Check the README for writing deprecations in `src/core/server/deprecations/README.mdx` + Signature: ```typescript diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.title.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.title.md new file mode 100644 index 000000000000..e8907688f6e5 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.title.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [title](./kibana-plugin-core-server.deprecationsdetails.title.md) + +## DeprecationsDetails.title property + +The title of the deprecation. Check the README for writing deprecations in `src/core/server/deprecations/README.mdx` + +Signature: + +```typescript +title: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md index 7d9d3dcdda4d..75732f59f1b3 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsservicesetup.md @@ -21,6 +21,7 @@ export interface DeprecationsServiceSetup ```ts import { DeprecationsDetails, GetDeprecationsContext, CoreSetup } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecationsContext): Promise { const deprecations: DeprecationsDetails[] = []; @@ -29,52 +30,44 @@ async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecations if (count > 0) { // Example of a manual correctiveAction deprecations.push({ - message: `You have ${count} Timelion worksheets. The Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard.`, + title: i18n.translate('xpack.timelion.deprecations.worksheetsTitle', { + defaultMessage: 'Found Timelion worksheets.' + }), + message: i18n.translate('xpack.timelion.deprecations.worksheetsMessage', { + defaultMessage: 'You have {count} Timelion worksheets. The Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard.', + values: { count }, + }), documentationUrl: 'https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html', level: 'warning', correctiveActions: { manualSteps: [ - 'Navigate to the Kibana Dashboard and click "Create dashboard".', - 'Select Timelion from the "New Visualization" window.', - 'Open a new tab, open the Timelion app, select the chart you want to copy, then copy the chart expression.', - 'Go to Timelion, paste the chart expression in the Timelion expression field, then click Update.', - 'In the toolbar, click Save.', - 'On the Save visualization window, enter the visualization Title, then click Save and return.', + i18n.translate('xpack.timelion.deprecations.worksheets.manualStepOneMessage', { + defaultMessage: 'Navigate to the Kibana Dashboard and click "Create dashboard".', + }), + i18n.translate('xpack.timelion.deprecations.worksheets.manualStepTwoMessage', { + defaultMessage: 'Select Timelion from the "New Visualization" window.', + }), ], + api: { + path: '/internal/security/users/test_dashboard_user', + method: 'POST', + body: { + username: 'test_dashboard_user', + roles: [ + "machine_learning_user", + "enrich_user", + "kibana_admin" + ], + full_name: "Alison Goryachev", + email: "alisongoryachev@gmail.com", + metadata: {}, + enabled: true + } + }, }, }); } - - // Example of an api correctiveAction - deprecations.push({ - "message": "User 'test_dashboard_user' is using a deprecated role: 'kibana_user'", - "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-user.html", - "level": "critical", - "correctiveActions": { - "api": { - "path": "/internal/security/users/test_dashboard_user", - "method": "POST", - "body": { - "username": "test_dashboard_user", - "roles": [ - "machine_learning_user", - "enrich_user", - "kibana_admin" - ], - "full_name": "Alison Goryachev", - "email": "alisongoryachev@gmail.com", - "metadata": {}, - "enabled": true - } - }, - "manualSteps": [ - "Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.", - "Using Kibana role-mapping management, change all role-mappings which assing the kibana_user role to the kibana_admin role." - ] - }, - }); - return deprecations; } diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index 49adc72bbe34..a4863bd60089 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -186,7 +186,7 @@ Set to `true` to enable a dark mode for the {kib} UI. You must refresh the page to apply the setting. [[theme-version]]`theme:version`:: -Specifies the {kib} theme. If you change the setting, refresh the page to apply the setting. +Specifies the {kib} theme. If you change the setting, refresh the page to apply the setting. [[timepicker-quickranges]]`timepicker:quickRanges`:: The list of ranges to show in the Quick section of the time filter. This should @@ -214,7 +214,7 @@ truncation. When enabled, provides access to the experimental *Labs* features for *Canvas*. [[labs-dashboard-defer-below-fold]]`labs:dashboard:deferBelowFold`:: -When enabled, the panels that appear below the fold are loaded when they become visible on the dashboard. +When enabled, the panels that appear below the fold are loaded when they become visible on the dashboard. _Below the fold_ refers to panels that are not immediately visible when you open a dashboard, but become visible as you scroll. For additional information, refer to <>. [[labs-dashboard-enable-ui]]`labs:dashboard:enable_ui`:: @@ -240,7 +240,7 @@ Banners are a https://www.elastic.co/subscriptions[subscription feature]. [horizontal] [[banners-placement]]`banners:placement`:: -Set to `Top` to display a banner above the Elastic header for this space. Defaults to the value of +Set to `Top` to display a banner above the Elastic header for this space. Defaults to the value of the `xpack.banners.placement` configuration property. [[banners-textcontent]]`banners:textContent`:: @@ -443,6 +443,9 @@ The threshold above which {ml} job anomalies are displayed in the {security-app} A comma-delimited list of {es} indices from which the {security-app} collects events. +[[securitysolution-threatindices]]`securitySolution:defaultThreatIndex`:: +A comma-delimited list of Threat Intelligence indices from which the {security-app} collects indicators. + [[securitysolution-enablenewsfeed]]`securitySolution:enableNewsFeed`:: Enables the security news feed on the Security *Overview* page. @@ -544,4 +547,4 @@ only production-ready visualizations are available to users. [horizontal] [[telemetry-enabled-advanced-setting]]`telemetry:enabled`:: When enabled, helps improve the Elastic Stack by providing usage statistics for -basic features. This data will not be shared outside of Elastic. \ No newline at end of file +basic features. This data will not be shared outside of Elastic. diff --git a/docs/settings/reporting-settings.asciidoc b/docs/settings/reporting-settings.asciidoc index b339daf3d36f..f215655f7f36 100644 --- a/docs/settings/reporting-settings.asciidoc +++ b/docs/settings/reporting-settings.asciidoc @@ -281,16 +281,15 @@ NOTE: This setting exists for backwards compatibility, but is unused and hardcod [[reporting-advanced-settings]] ==== Security settings -[[xpack-reporting-roles-enabled]] `xpack.reporting.roles.enabled`:: -deprecated:[7.14.0,This setting must be set to `false` in 8.0.] When `true`, grants users access to the {report-features} by assigning reporting roles, specified by `xpack.reporting.roles.allow`. Granting access to users this way is deprecated. Set to `false` and use {kibana-ref}/kibana-privileges.html[{kib} privileges] instead. Defaults to `true`. +With Security enabled, Reporting has two forms of access control: each user can only access their own reports, and custom roles determine who has privilege to generate reports. When Reporting is configured with <>, you can control the spaces and applications where users are allowed to generate reports. [NOTE] ============================================================================ -In 7.x, the default value of `xpack.reporting.roles.enabled` is `true`. To migrate users to the -new method of securing access to *Reporting*, you must set `xpack.reporting.roles.enabled: false`. In the next major version of {kib}, `false` will be the only valid configuration. +The `xpack.reporting.roles` settings are for a deprecated system of access control in Reporting. It does not allow API Keys to generate reports, and it doesn't allow {kib} application privileges. We recommend you explicitly turn off reporting's deprecated access control feature by adding `xpack.reporting.roles.enabled: false` in kibana.yml. This will enable application privileges for reporting, as described in <>. ============================================================================ -`xpack.reporting.roles.allow`:: -deprecated:[7.14.0,This setting will be removed in 8.0.] Specifies the roles, in addition to superusers, that can generate reports, using the {ref}/security-api.html#security-role-apis[{es} role management APIs]. Requires `xpack.reporting.roles.enabled` to be `true`. Granting access to users this way is deprecated. Use {kibana-ref}/kibana-privileges.html[{kib} privileges] instead. Defaults to `[ "reporting_user" ]`. +[[xpack-reporting-roles-enabled]] `xpack.reporting.roles.enabled`:: +deprecated:[7.14.0,The default for this setting will be `false` in an upcoming version of {kib}.] Sets access control to a set of assigned reporting roles, specified by `xpack.reporting.roles.allow`. Defaults to `true`. -NOTE: Each user has access to only their own reports. +`xpack.reporting.roles.allow`:: +deprecated:[7.14.0] In addition to superusers, specifies the roles that can generate reports using the {ref}/security-api.html#security-role-apis[{es} role management APIs]. Requires `xpack.reporting.roles.enabled` to be `true`. Defaults to `[ "reporting_user" ]`. diff --git a/docs/setup/configuring-reporting.asciidoc b/docs/setup/configuring-reporting.asciidoc index 0dba7befa293..6d209092d333 100644 --- a/docs/setup/configuring-reporting.asciidoc +++ b/docs/setup/configuring-reporting.asciidoc @@ -41,11 +41,16 @@ To troubleshoot the problem, start the {kib} server with environment variables t [float] [[grant-user-access]] === Grant users access to reporting +When security is enabled, you grant users access to generate reports with <>, which allow you to create custom roles that control the spaces and applications where users generate reports. -When security is enabled, access to the {report-features} is controlled by roles and <>. With privileges, you can define custom roles that grant *Reporting* privileges as sub-features of {kib} applications. To grant users permission to generate reports and view their reports in *Reporting*, create and assign the reporting role. - -[[reporting-app-users]] -NOTE: In 7.12.0 and earlier, you grant access to the {report-features} by assigning users the `reporting_user` role in {es}. +. Enable application privileges in Reporting. To enable, turn off the default user access control features in `kibana.yml`: ++ +[source,yaml] +------------------------------------ +xpack.reporting.roles.enabled: false +------------------------------------ ++ +NOTE: If you use the default settings, you can still create a custom role that grants reporting privileges. The default role is `reporting_user`. This behavior is being deprecated and does not allow application-level access controls for {report-features}, and does not allow API keys or authentication tokens to authorize report generation. Refer to <> for information and caveats about the deprecated access control features. . Create the reporting role. @@ -90,10 +95,12 @@ If the *Reporting* option is unavailable, contact your administrator, or < Reporting*. Users can only access their own reports. + [float] [[reporting-roles-user-api]] ==== Grant access with the role API -You can also use the {ref}/security-api-put-role.html[role API] to grant access to the reporting features. Grant the reporting role to users in combination with other roles that grant read access to the data in {es}, and at least read access in the applications where users can generate reports. +With <> enabled in Reporting, you can also use the {ref}/security-api-put-role.html[role API] to grant access to the {report-features}. Grant custom reporting roles to users in combination with other roles that grant read access to the data in {es}, and at least read access in the applications where users can generate reports. [source, sh] --------------------------------------------------------------- diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index a0611b79aae4..203339be638a 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -371,7 +371,7 @@ The size limit policy will perform a rollover when the log file reaches a maximu The time interval policy will rotate the log file every given interval of time. *Default 24h* -| [[regionmap-ES-map]] `map.includeElasticMapsService:` {ess-icon} +| `map.includeElasticMapsService:` {ess-icon} | Set to `false` to disable connections to Elastic Maps Service. When `includeElasticMapsService` is turned off, only tile layer configured by <> is available in <>. *Default: `true`* @@ -383,88 +383,6 @@ When `includeElasticMapsService` is turned off, only tile layer configured by << Set to `true` to proxy all <> Elastic Maps Service requests through the {kib} server. *Default: `false`* -| [[regionmap-settings]] `map.regionmap:` {ess-icon} - | deprecated:[7.14.0,"In 8.0 and later, this setting will no longer be supported."] - Specifies additional vector layers for -use in <> visualizations. Each layer -object points to an external vector file that contains a geojson -FeatureCollection. The file must use the -https://en.wikipedia.org/wiki/World_Geodetic_System[WGS84 coordinate reference system (ESPG:4326)] -and only include polygons. If the file is hosted on a separate domain from -{kib}, the server needs to be CORS-enabled so {kib} can download the file. -The following example shows a valid region map configuration. - -|=== - -[source,text] --- -map.regionmap: - layers: - - name: "Departments of France" - url: "http://my.cors.enabled.server.org/france_departements.geojson" - attribution: "INRAP" - fields: - - name: "department" - description: "Full department name" - - name: "INSEE" - description: "INSEE numeric identifier" --- - -[cols="2*<"] -|=== - -| [[regionmap-attribution]] `map.regionmap.layers[].attribution:` {ess-icon} - | deprecated:[7.14.0,"In 8.0 and later, this setting will no longer be supported."] - Optional. References the originating source of the geojson file. - -| [[regionmap-fields]] `map.regionmap.layers[].fields[]:` {ess-icon} - | deprecated:[7.14.0,"In 8.0 and later, this setting will no longer be supported."] - Mandatory. Each layer -can contain multiple fields to indicate what properties from the geojson -features you wish to expose. The following shows how to define multiple -properties: - -|=== - -[source,text] --- -map.regionmap: - layers: - - name: "Departments of France" - url: "http://my.cors.enabled.server.org/france_departements.geojson" - attribution: "INRAP" - fields: - - name: "department" - description: "Full department name" - - name: "INSEE" - description: "INSEE numeric identifier" --- - -[cols="2*<"] -|=== - -| [[regionmap-field-description]] `map.regionmap.layers[].fields[].description:` {ess-icon} - | deprecated:[7.14.0,"In 8.0 and later, this setting will no longer be supported."] - Mandatory. The human readable text that is shown under the Options tab when -building the Region Map visualization. - -| [[regionmap-field-name]] `map.regionmap.layers[].fields[].name:` {ess-icon} - | deprecated:[7.14.0,"In 8.0 and later, this setting will no longer be supported."] - Mandatory. -This value is used to do an inner-join between the document stored in -{es} and the geojson file. For example, if the field in the geojson is -called `Location` and has city names, there must be a field in {es} -that holds the same values that {kib} can then use to lookup for the geoshape -data. - -| [[regionmap-name]] `map.regionmap.layers[].name:` {ess-icon} - | deprecated:[7.14.0,"In 8.0 and later, this setting will no longer be supported."] - Mandatory. A description of the map being provided. - -| [[regionmap-url]] `map.regionmap.layers[].url:` {ess-icon} - | deprecated:[7.14.0,"In 8.0 and later, this setting will no longer be supported."] - Mandatory. The location of the geojson file as provided by a webserver. - | [[tilemap-settings]] `map.tilemap.options.attribution:` {ess-icon} | The map attribution string. *Default: `"© [Elastic Maps Service](https://www.elastic.co/elastic-maps-service)"`* @@ -488,7 +406,10 @@ override this parameter to use their own Tile Map Service. For example: `"https://tiles.elastic.co/v2/default/{z}/{x}/{y}.png?elastic_tile_service_tos=agree&my_app_name=kibana"` | `migrations.batchSize:` - | Defines the number of documents migrated at a time. The higher the value, the faster the Saved Objects migration process performs at the cost of higher memory consumption. If the migration fails due to a `circuit_breaking_exception`, set a smaller `batchSize` value. *Default: `1000`* + | Defines the number of documents migrated at a time. The higher the value, the faster the Saved Objects migration process performs at the cost of higher memory consumption. If upgrade migrations results in {kib} crashing with an out of memory exception or fails due to an Elasticsearch `circuit_breaking_exception`, use a smaller `batchSize` value to reduce the memory pressure. *Default: `1000`* + + | `migrations.maxBatchSizeBytes:` + | Defines the maximum payload size for indexing batches of upgraded saved objects to avoid migrations failing due to a 413 Request Entity Too Large response from Elasticsearch. This value should be lower than or equal to your Elasticsearch cluster's `http.max_content_length` configuration option. *Default: `100mb`* | `migrations.enableV2:` | experimental[]. Enables the new Saved Objects migration algorithm. For information about the migration algorithm, refer to <>. When `migrations v2` is stable, the setting will be removed in an upcoming release without any further notice. Setting the value to `false` causes {kib} to use the legacy migration algorithm, which shipped in 7.11 and earlier versions. *Default: `true`* diff --git a/package.json b/package.json index 54c9039213ef..20d20d13fa12 100644 --- a/package.json +++ b/package.json @@ -89,7 +89,7 @@ "**/underscore": "^1.13.1" }, "engines": { - "node": "14.17.5", + "node": "14.17.6", "yarn": "^1.21.1" }, "dependencies": { @@ -98,7 +98,7 @@ "@elastic/apm-rum-react": "^1.3.1", "@elastic/charts": "34.2.1", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", - "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.18", + "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.19", "@elastic/ems-client": "7.15.0", "@elastic/eui": "37.3.0", "@elastic/filesaver": "1.1.2", @@ -287,7 +287,7 @@ "lodash": "^4.17.21", "lru-cache": "^4.1.5", "lz-string": "^1.4.4", - "mapbox-gl": "1.13.1", + "maplibre-gl": "1.15.2", "mapbox-gl-draw-rectangle-mode": "1.0.4", "markdown-it": "^10.0.0", "md5": "^2.1.0", @@ -332,7 +332,7 @@ "raw-loader": "^3.1.0", "rbush": "^3.0.1", "re-resizable": "^6.1.1", - "re2": "^1.15.4", + "re2": "^1.16.0", "react": "^16.12.0", "react-ace": "^7.0.5", "react-beautiful-dnd": "^13.0.0", @@ -400,7 +400,6 @@ "tar": "4.4.13", "tinycolor2": "1.4.1", "tinygradient": "0.4.3", - "topojson-client": "3.1.0", "tree-kill": "^1.2.2", "ts-easing": "^0.2.0", "tslib": "^2.0.0", @@ -430,6 +429,7 @@ "devDependencies": { "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", + "@babel/generator": "^7.12.11", "@babel/parser": "^7.12.11", "@babel/plugin-proposal-class-properties": "^7.12.1", "@babel/plugin-proposal-export-namespace-from": "^7.12.1", @@ -572,7 +572,6 @@ "@types/lodash": "^4.14.159", "@types/lru-cache": "^5.1.0", "@types/lz-string": "^1.3.34", - "@types/mapbox-gl": "1.13.1", "@types/markdown-it": "^0.0.7", "@types/md5": "^2.2.0", "@types/memoize-one": "^4.1.0", @@ -656,6 +655,7 @@ "@types/yauzl": "^2.9.1", "@types/zen-observable": "^0.8.0", "@typescript-eslint/eslint-plugin": "^4.14.1", + "@typescript-eslint/typescript-estree": "^4.14.1", "@typescript-eslint/parser": "^4.14.1", "@yarnpkg/lockfile": "^1.1.0", "abab": "^2.0.4", @@ -726,6 +726,7 @@ "eslint-plugin-react": "^7.20.3", "eslint-plugin-react-hooks": "^4.2.0", "eslint-plugin-react-perf": "^3.2.3", + "eslint-traverse": "^1.0.0", "expose-loader": "^0.7.5", "faker": "^5.1.0", "fancy-log": "^1.3.2", @@ -790,7 +791,7 @@ "multimatch": "^4.0.0", "mutation-observer": "^1.0.3", "ncp": "^2.0.0", - "node-sass": "^4.14.1", + "node-sass": "^6.0.1", "null-loader": "^3.0.0", "nyc": "^15.0.1", "oboe": "^2.1.4", @@ -810,7 +811,7 @@ "regenerate": "^1.4.0", "resolve": "^1.7.1", "rxjs-marbles": "^5.0.6", - "sass-loader": "^8.0.2", + "sass-loader": "^10.2.0", "sass-resources-loader": "^2.0.1", "selenium-webdriver": "^4.0.0-alpha.7", "serve-static": "1.14.1", diff --git a/packages/elastic-eslint-config-kibana/.eslintrc.js b/packages/elastic-eslint-config-kibana/.eslintrc.js index 1b3e852e5a50..38c0c4313256 100644 --- a/packages/elastic-eslint-config-kibana/.eslintrc.js +++ b/packages/elastic-eslint-config-kibana/.eslintrc.js @@ -90,5 +90,7 @@ module.exports = { }, ], ], + + '@kbn/eslint/no_async_promise_body': 'error', }, }; diff --git a/packages/kbn-config/BUILD.bazel b/packages/kbn-config/BUILD.bazel index 75e4428ed2d7..e0cf4d2205d6 100644 --- a/packages/kbn-config/BUILD.bazel +++ b/packages/kbn-config/BUILD.bazel @@ -35,6 +35,7 @@ RUNTIME_DEPS = [ "//packages/kbn-logging", "//packages/kbn-std", "//packages/kbn-utility-types", + "//packages/kbn-i18n", "@npm//js-yaml", "@npm//load-json-file", "@npm//lodash", @@ -48,6 +49,7 @@ TYPES_DEPS = [ "//packages/kbn-logging", "//packages/kbn-std", "//packages/kbn-utility-types", + "//packages/kbn-i18n", "@npm//load-json-file", "@npm//rxjs", "@npm//@types/jest", diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts index 563d4017f5ed..0a605cbc1c53 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts @@ -48,7 +48,8 @@ describe('DeprecationFactory', () => { "Replace \\"myplugin.deprecated\\" with \\"myplugin.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", ], }, - "message": "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"", + "message": "Setting \\"myplugin.deprecated\\" has been replaced by \\"myplugin.renamed\\"", + "title": "Setting \\"myplugin.deprecated\\" is deprecated", }, ], ] @@ -103,7 +104,8 @@ describe('DeprecationFactory', () => { "Replace \\"myplugin.oldsection.deprecated\\" with \\"myplugin.newsection.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", ], }, - "message": "\\"myplugin.oldsection.deprecated\\" is deprecated and has been replaced by \\"myplugin.newsection.renamed\\"", + "message": "Setting \\"myplugin.oldsection.deprecated\\" has been replaced by \\"myplugin.newsection.renamed\\"", + "title": "Setting \\"myplugin.oldsection.deprecated\\" is deprecated", }, ], ] @@ -130,7 +132,8 @@ describe('DeprecationFactory', () => { "Remove \\"myplugin.deprecated\\" from the config.", ], }, - "message": "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"", + "message": "Setting \\"$myplugin.deprecated\\" has been replaced by \\"$myplugin.renamed\\". However, both keys are present. Ignoring \\"$myplugin.deprecated\\"", + "title": "Setting \\"myplugin.deprecated\\" is deprecated", }, ], ] @@ -172,7 +175,8 @@ describe('DeprecationFactory', () => { "Replace \\"myplugin.deprecated\\" with \\"myplugin.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", ], }, - "message": "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\"", + "message": "Setting \\"myplugin.deprecated\\" has been replaced by \\"myplugin.renamed\\"", + "title": "Setting \\"myplugin.deprecated\\" is deprecated", }, ], ] @@ -212,7 +216,8 @@ describe('DeprecationFactory', () => { "Replace \\"oldplugin.deprecated\\" with \\"newplugin.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", ], }, - "message": "\\"oldplugin.deprecated\\" is deprecated and has been replaced by \\"newplugin.renamed\\"", + "message": "Setting \\"oldplugin.deprecated\\" has been replaced by \\"newplugin.renamed\\"", + "title": "Setting \\"oldplugin.deprecated\\" is deprecated", }, ], ] @@ -264,7 +269,8 @@ describe('DeprecationFactory', () => { "Remove \\"myplugin.deprecated\\" from the config.", ], }, - "message": "\\"myplugin.deprecated\\" is deprecated and has been replaced by \\"myplugin.renamed\\". However both key are present, ignoring \\"myplugin.deprecated\\"", + "message": "Setting \\"$myplugin.deprecated\\" has been replaced by \\"$myplugin.renamed\\". However, both keys are present. Ignoring \\"$myplugin.deprecated\\"", + "title": "Setting \\"myplugin.deprecated\\" is deprecated", }, ], ] @@ -293,10 +299,11 @@ describe('DeprecationFactory', () => { Object { "correctiveActions": Object { "manualSteps": Array [ - "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only)", + "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only).", ], }, - "message": "myplugin.deprecated is deprecated and is no longer used", + "message": "You no longer need to configure \\"myplugin.deprecated\\".", + "title": "Setting \\"myplugin.deprecated\\" is deprecated", }, ], ] @@ -325,10 +332,11 @@ describe('DeprecationFactory', () => { Object { "correctiveActions": Object { "manualSteps": Array [ - "Remove \\"myplugin.section.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only)", + "Remove \\"myplugin.section.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only).", ], }, - "message": "myplugin.section.deprecated is deprecated and is no longer used", + "message": "You no longer need to configure \\"myplugin.section.deprecated\\".", + "title": "Setting \\"myplugin.section.deprecated\\" is deprecated", }, ], ] @@ -375,10 +383,11 @@ describe('DeprecationFactory', () => { Object { "correctiveActions": Object { "manualSteps": Array [ - "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only)", + "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only).", ], }, - "message": "myplugin.deprecated is deprecated and is no longer used", + "message": "You no longer need to configure \\"myplugin.deprecated\\".", + "title": "Setting \\"myplugin.deprecated\\" is deprecated", }, ], ] diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.ts b/packages/kbn-config/src/deprecation/deprecation_factory.ts index 76bcc1958d0d..6d7669cef04f 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.ts @@ -7,6 +7,8 @@ */ import { get } from 'lodash'; +import { i18n } from '@kbn/i18n'; + import { ConfigDeprecation, AddConfigDeprecation, @@ -15,6 +17,13 @@ import { ConfigDeprecationCommand, } from './types'; +const getDeprecationTitle = (deprecationPath: string) => { + return i18n.translate('kbnConfig.deprecations.deprecatedSettingTitle', { + defaultMessage: 'Setting "{deprecationPath}" is deprecated', + values: { deprecationPath }, + }); +}; + const _rename = ( config: Record, rootPath: string, @@ -33,10 +42,18 @@ const _rename = ( const newValue = get(config, fullNewPath); if (newValue === undefined) { addDeprecation({ - message: `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}"`, + title: getDeprecationTitle(fullOldPath), + message: i18n.translate('kbnConfig.deprecations.replacedSettingMessage', { + defaultMessage: `Setting "{fullOldPath}" has been replaced by "{fullNewPath}"`, + values: { fullOldPath, fullNewPath }, + }), correctiveActions: { manualSteps: [ - `Replace "${fullOldPath}" with "${fullNewPath}" in the Kibana config file, CLI flag, or environment variable (in Docker only).`, + i18n.translate('kbnConfig.deprecations.replacedSetting.manualStepOneMessage', { + defaultMessage: + 'Replace "{fullOldPath}" with "{fullNewPath}" in the Kibana config file, CLI flag, or environment variable (in Docker only).', + values: { fullOldPath, fullNewPath }, + }), ], }, ...details, @@ -47,11 +64,23 @@ const _rename = ( }; } else { addDeprecation({ - message: `"${fullOldPath}" is deprecated and has been replaced by "${fullNewPath}". However both key are present, ignoring "${fullOldPath}"`, + title: getDeprecationTitle(fullOldPath), + message: i18n.translate('kbnConfig.deprecations.conflictSettingMessage', { + defaultMessage: + 'Setting "${fullOldPath}" has been replaced by "${fullNewPath}". However, both keys are present. Ignoring "${fullOldPath}"', + values: { fullOldPath, fullNewPath }, + }), correctiveActions: { manualSteps: [ - `Make sure "${fullNewPath}" contains the correct value in the config file, CLI flag, or environment variable (in Docker only).`, - `Remove "${fullOldPath}" from the config.`, + i18n.translate('kbnConfig.deprecations.conflictSetting.manualStepOneMessage', { + defaultMessage: + 'Make sure "{fullNewPath}" contains the correct value in the config file, CLI flag, or environment variable (in Docker only).', + values: { fullNewPath }, + }), + i18n.translate('kbnConfig.deprecations.conflictSetting.manualStepTwoMessage', { + defaultMessage: 'Remove "{fullOldPath}" from the config.', + values: { fullOldPath }, + }), ], }, ...details, @@ -75,10 +104,18 @@ const _unused = ( return; } addDeprecation({ - message: `${fullPath} is deprecated and is no longer used`, + title: getDeprecationTitle(fullPath), + message: i18n.translate('kbnConfig.deprecations.unusedSettingMessage', { + defaultMessage: 'You no longer need to configure "{fullPath}".', + values: { fullPath }, + }), correctiveActions: { manualSteps: [ - `Remove "${fullPath}" from the Kibana config file, CLI flag, or environment variable (in Docker only)`, + i18n.translate('kbnConfig.deprecations.unusedSetting.manualStepOneMessage', { + defaultMessage: + 'Remove "{fullPath}" from the Kibana config file, CLI flag, or environment variable (in Docker only).', + values: { fullPath }, + }), ], }, ...details, diff --git a/packages/kbn-config/src/deprecation/types.ts b/packages/kbn-config/src/deprecation/types.ts index 1791dac060e2..007c3ec54113 100644 --- a/packages/kbn-config/src/deprecation/types.ts +++ b/packages/kbn-config/src/deprecation/types.ts @@ -19,6 +19,8 @@ export type AddConfigDeprecation = (details: DeprecatedConfigDetails) => void; * @public */ export interface DeprecatedConfigDetails { + /* The title to be displayed for the deprecation. */ + title?: string; /* The message to be displayed for the deprecation. */ message: string; /* (optional) set false to prevent the config service from logging the deprecation message. */ diff --git a/packages/kbn-dev-utils/BUILD.bazel b/packages/kbn-dev-utils/BUILD.bazel index 502bcd05b74f..90b4d91b6669 100644 --- a/packages/kbn-dev-utils/BUILD.bazel +++ b/packages/kbn-dev-utils/BUILD.bazel @@ -57,6 +57,7 @@ RUNTIME_DEPS = [ "@npm//load-json-file", "@npm//markdown-it", "@npm//normalize-path", + "@npm//prettier", "@npm//rxjs", "@npm//tar", "@npm//tree-kill", @@ -81,6 +82,7 @@ TYPES_DEPS = [ "@npm//@types/markdown-it", "@npm//@types/node", "@npm//@types/normalize-path", + "@npm//@types/prettier", "@npm//@types/react", "@npm//@types/tar", "@npm//@types/testing-library__jest-dom", diff --git a/packages/kbn-dev-utils/src/index.ts b/packages/kbn-dev-utils/src/index.ts index 9dc9d1723945..381e99ac677f 100644 --- a/packages/kbn-dev-utils/src/index.ts +++ b/packages/kbn-dev-utils/src/index.ts @@ -32,3 +32,4 @@ export * from './plugins'; export * from './streams'; export * from './babel'; export * from './extract'; +export * from './vscode_config'; diff --git a/src/plugins/discover/public/application/angular/create_discover_directive.ts b/packages/kbn-dev-utils/src/vscode_config/index.ts similarity index 52% rename from src/plugins/discover/public/application/angular/create_discover_directive.ts rename to packages/kbn-dev-utils/src/vscode_config/index.ts index ae0d978322bc..1b08881273ed 100644 --- a/src/plugins/discover/public/application/angular/create_discover_directive.ts +++ b/packages/kbn-dev-utils/src/vscode_config/index.ts @@ -5,12 +5,5 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { DiscoverMainApp } from '../apps/main'; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function createDiscoverDirective(reactDirective: any) { - return reactDirective(DiscoverMainApp, [ - ['indexPattern', { watchDepth: 'reference' }], - ['opts', { watchDepth: 'reference' }], - ]); -} +export * from './update_vscode_config_cli'; diff --git a/packages/kbn-dev-utils/src/vscode_config/managed_config_keys.ts b/packages/kbn-dev-utils/src/vscode_config/managed_config_keys.ts new file mode 100644 index 000000000000..288bfad02d7f --- /dev/null +++ b/packages/kbn-dev-utils/src/vscode_config/managed_config_keys.ts @@ -0,0 +1,40 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export interface ManagedConfigKey { + key: string; + value: Record; +} + +/** + * Defines the keys which we overrite in user's vscode config for the workspace. We currently + * only support object values because that's all we needed to support, but support for non object + * values should be easy to add. + */ +export const MANAGED_CONFIG_KEYS: ManagedConfigKey[] = [ + { + key: 'files.watcherExclude', + value: { + ['**/.eslintcache']: true, + ['**/.es']: true, + ['**/.yarn-local-mirror']: true, + ['**/.chromium']: true, + ['**/packages/kbn-pm/dist/index.js']: true, + ['**/bazel-*']: true, + ['**/node_modules']: true, + ['**/target']: true, + ['**/*.log']: true, + }, + }, + { + key: 'search.exclude', + value: { + ['**/packages/kbn-pm/dist/index.js']: true, + }, + }, +]; diff --git a/packages/kbn-dev-utils/src/vscode_config/update_vscode_config.test.ts b/packages/kbn-dev-utils/src/vscode_config/update_vscode_config.test.ts new file mode 100644 index 000000000000..dd57449c21da --- /dev/null +++ b/packages/kbn-dev-utils/src/vscode_config/update_vscode_config.test.ts @@ -0,0 +1,340 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import dedent from 'dedent'; + +import { updateVscodeConfig } from './update_vscode_config'; +import { ManagedConfigKey } from './managed_config_keys'; + +// avoid excessive escaping in snapshots +expect.addSnapshotSerializer({ test: (v) => typeof v === 'string', print: (v) => `${v}` }); + +const TEST_KEYS: ManagedConfigKey[] = [ + { + key: 'key', + value: { + hello: true, + world: [1, 2, 3], + }, + }, +]; + +const run = (json?: string) => updateVscodeConfig(TEST_KEYS, '', json); + +it('updates the passed JSON with the managed settings', () => { + expect(run(`{}`)).toMatchInlineSnapshot(` + // @managed + { + "key": { + // @managed + "hello": true, + // @managed + "world": [1, 2, 3] + } + } + + `); +}); + +it('initialized empty or undefined json values', () => { + expect(run('')).toMatchInlineSnapshot(` + // @managed + { + "key": { + // @managed + "hello": true, + // @managed + "world": [1, 2, 3] + } + } + + `); + + expect(run()).toMatchInlineSnapshot(` + // @managed + { + "key": { + // @managed + "hello": true, + // @managed + "world": [1, 2, 3] + } + } + + `); +}); + +it('replaces conflicting managed keys which do not have object values', () => { + expect(run(`{ "key": false }`)).toMatchInlineSnapshot(` + // @managed + { + "key": { + // @managed + "hello": true, + // @managed + "world": [1, 2, 3] + } + } + + `); +}); + +it(`throws if the JSON file doesn't contain an object`, () => { + expect(() => run('[]')).toThrowErrorMatchingInlineSnapshot( + `expected VSCode config to contain a JSON object` + ); + expect(() => run('1')).toThrowErrorMatchingInlineSnapshot( + `expected VSCode config to contain a JSON object` + ); + expect(() => run('"foo"')).toThrowErrorMatchingInlineSnapshot( + `expected VSCode config to contain a JSON object` + ); +}); + +it('persists comments in the original file', () => { + const newJson = run(` + /** + * This is a top level comment + */ + { + "a": "bar", + // this is just test data + "b": "box" + } + `); + expect(newJson).toMatchInlineSnapshot(` + // @managed + + /** + * This is a top level comment + */ + { + "a": "bar", + // this is just test data + "b": "box", + "key": { + // @managed + "hello": true, + // @managed + "world": [1, 2, 3] + } + } + + `); +}); + +it('overrides old values for managed keys', () => { + const newJson = run(` + { + "foo": 0, + "bar": "some other config", + "complex": "some other config", + } + `); + + expect(newJson).toMatchInlineSnapshot(` + // @managed + { + "foo": 0, + "bar": "some other config", + "complex": "some other config", + "key": { + // @managed + "hello": true, + // @managed + "world": [1, 2, 3] + } + } + + `); +}); + +it('does not modify files starting with // SELF MANAGED', () => { + const newJson = run(dedent` + // self managed + { + "invalid": "I know what I am doing", + } + `); + + expect(newJson).toMatchInlineSnapshot(` + // self managed + { + "invalid": "I know what I am doing", + } + `); +}); + +it('does not modify properties with leading `// self managed` comment', () => { + const newJson = run(dedent` + { + // self managed + "key": { + "world": [5] + } + } + `); + + expect(newJson).toMatchInlineSnapshot(` + // @managed + { + // self managed + "key": { + "world": [5] + } + } + + `); +}); + +it('does not modify child properties with leading `// self managed` comment', () => { + const newJson = run(dedent` + { + "key": { + // self managed + "world": [5] + } + } + `); + + expect(newJson).toMatchInlineSnapshot(` + // @managed + { + "key": { + // self managed + "world": [5], + // @managed + "hello": true + } + } + + `); +}); + +it('does not modify unknown child properties', () => { + const newJson = run(dedent` + { + "key": { + "foo": "bar", + // self managed + "world": [5], + } + } + `); + + expect(newJson).toMatchInlineSnapshot(` + // @managed + { + "key": { + "foo": "bar", + // self managed + "world": [5], + // @managed + "hello": true + } + } + + `); +}); + +it('removes managed properties which are no longer managed', () => { + const newJson = run(dedent` + { + "key": { + // @managed + "foo": "bar", + // self managed + "world": [5], + } + } + `); + + expect(newJson).toMatchInlineSnapshot(` + // @managed + { + "key": { + // self managed + "world": [5], + // @managed + "hello": true + } + } + + `); +}); + +it('wipes out child keys which conflict with newly managed child keys', () => { + const newJson = run(dedent` + { + "key": { + // some user specified comment + "world": [5], + } + } + `); + + expect(newJson).toMatchInlineSnapshot(` + // @managed + { + "key": { + // @managed + "hello": true, + // @managed + "world": [1, 2, 3] + } + } + + `); +}); + +it('correctly formats info text when specified', () => { + const newJson = updateVscodeConfig(TEST_KEYS, 'info users\nshould know', `{}`); + + expect(newJson).toMatchInlineSnapshot(` + /** + * @managed + * + * info users + * should know + */ + { + "key": { + // @managed + "hello": true, + // @managed + "world": [1, 2, 3] + } + } + + `); +}); + +it('allows "// self managed" comments conflicting with "// @managed" comments to win', () => { + const newJson = run(dedent` + { + "key": { + // @managed + // self managed + "hello": ["world"] + } + } + `); + + expect(newJson).toMatchInlineSnapshot(` + // @managed + { + "key": { + // self managed + "hello": ["world"], + // @managed + "world": [1, 2, 3] + } + } + + `); +}); diff --git a/packages/kbn-dev-utils/src/vscode_config/update_vscode_config.ts b/packages/kbn-dev-utils/src/vscode_config/update_vscode_config.ts new file mode 100644 index 000000000000..ac1b5515252d --- /dev/null +++ b/packages/kbn-dev-utils/src/vscode_config/update_vscode_config.ts @@ -0,0 +1,210 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { parseExpression } from '@babel/parser'; +import * as t from '@babel/types'; +import generate from '@babel/generator'; +import Prettier from 'prettier'; + +import { ManagedConfigKey } from './managed_config_keys'; + +type BasicObjectProp = t.ObjectProperty & { + key: t.StringLiteral; +}; + +const isBasicObjectProp = (n: t.Node): n is BasicObjectProp => + n.type === 'ObjectProperty' && n.key.type === 'StringLiteral'; + +const isManaged = (node?: t.Node) => + !!node?.leadingComments?.some( + (c) => c.type === 'CommentLine' && c.value.trim().toLocaleLowerCase() === '@managed' + ); + +const isSelfManaged = (node?: t.Node) => + !!node?.leadingComments?.some( + (c) => c.type === 'CommentLine' && c.value.trim().toLocaleLowerCase() === 'self managed' + ); + +const remove = (arr: T[], value: T) => { + const index = arr.indexOf(value); + if (index > -1) { + arr.splice(index, 1); + } +}; + +const createManagedChildProp = (key: string, value: any) => { + const childProp = t.objectProperty(t.stringLiteral(key), parseExpression(JSON.stringify(value))); + t.addComment(childProp, 'leading', ' @managed', true); + return childProp; +}; + +const createManagedProp = (key: string, value: Record) => { + return t.objectProperty( + t.stringLiteral(key), + t.objectExpression(Object.entries(value).map(([k, v]) => createManagedChildProp(k, v))) + ); +}; + +/** + * Adds a new setting to the settings.json file. Used when there is no existing key + * + * @param ast AST of the entire settings.json file + * @param key the key name to add + * @param value managed value which should be set at `key` + */ +const addManagedProp = (ast: t.ObjectExpression, key: string, value: Record) => { + ast.properties.push(createManagedProp(key, value)); +}; + +/** + * Replace an existing setting in the settings.json file with the `managedValue`, ignoring its + * type, used when the value of the existing setting is not an ObjectExpression + * + * @param ast AST of the entire settings.json file + * @param existing node which should be replaced + * @param value managed value which should replace the current value, regardless of its type + */ +const replaceManagedProp = ( + ast: t.ObjectExpression, + existing: BasicObjectProp, + value: Record +) => { + remove(ast.properties, existing); + addManagedProp(ast, existing.key.value, value); +}; + +/** + * Merge the managed value in to the value already in the settings.json file. Any property which is + * labeled with a `// self managed` comment is untouched, any property which is `// @managed` but + * no longer in the `managedValue` is removed, and any properties in the `managedValue` are either + * added or updated based on their existence in the AST. + * + * @param properties Object expression properties list which we will merge with ("key": ) + * @param managedValue the managed value that should be merged into the existing values + */ +const mergeManagedProperties = ( + properties: t.ObjectExpression['properties'], + managedValue: Record +) => { + // iterate through all the keys in the managed `value` and either add them to the + // prop, update their value, or ignore them because they are "// self managed" + for (const [key, value] of Object.entries(managedValue)) { + const existing = properties.filter(isBasicObjectProp).find((p) => p.key.value === key); + + if (!existing) { + // add the new managed prop + properties.push(createManagedChildProp(key, value)); + continue; + } + + if (isSelfManaged(existing)) { + // strip "// @managed" comment if conflicting with "// self managed" + existing.leadingComments = (existing.leadingComments ?? []).filter( + (c) => c.value.trim() !== '@managed' + ); + continue; + } + + if (isManaged(existing)) { + // the prop already exists and is still managed, so update it's value + existing.value = parseExpression(JSON.stringify(value)); + continue; + } + + // take over the unmanaged child prop by deleting the previous prop and replacing it + // with a brand new one + remove(properties, existing); + properties.push(createManagedChildProp(key, value)); + } + + // iterate through the props to find "// @managed" props which are no longer in + // the `managedValue` and remove them + for (const prop of properties) { + if ( + isBasicObjectProp(prop) && + isManaged(prop) && + !Object.prototype.hasOwnProperty.call(managedValue, prop.key.value) + ) { + remove(properties, prop); + } + } +}; + +/** + * Update the settings.json file used by VSCode in the Kibana repository. If the file starts + * with the comment "// self managed" then it is not touched. If a top-level keys is prefixed with + * `// self managed` then all the properties of that setting are left untouched. And finally, if + * a specific child property of a setting like `search.exclude` is prefixed with `// self managed` + * then it is left untouched. + * + * We don't just use `JSON.parse()` and `JSON.stringify()` in order to support this customization and + * also to support users using comments in this file, which is very useful for temporarily disabling settings. + * + * After the config file is updated it is formatted with prettier. + * + * @param keys The config keys which are managed + * @param infoText The text which should be written to the top of the file to educate users how to customize the settings + * @param json The settings file as a string + */ +export function updateVscodeConfig(keys: ManagedConfigKey[], infoText: string, json?: string) { + json = json || '{}'; + const ast = parseExpression(json); + + if (ast.type !== 'ObjectExpression') { + throw new Error(`expected VSCode config to contain a JSON object`); + } + + if (isSelfManaged(ast)) { + return json; + } + + for (const { key, value } of keys) { + const existingProp = ast.properties.filter(isBasicObjectProp).find((p) => p.key.value === key); + + if (isSelfManaged(existingProp)) { + continue; + } + + if (existingProp && existingProp.value.type === 'ObjectExpression') { + // setting exists and is an object so merge properties of `value` with it + mergeManagedProperties(existingProp.value.properties, value); + continue; + } + + if (existingProp) { + // setting exists but its value is not an object expression so replace it + replaceManagedProp(ast, existingProp, value); + continue; + } + + // setting isn't in config file so create it + addManagedProp(ast, key, value); + } + + ast.leadingComments = [ + (infoText + ? { + type: 'CommentBlock', + value: `* + * @managed + * + * ${infoText.split(/\r?\n/).join('\n * ')} +`, + } + : { + type: 'CommentLine', + value: ' @managed', + }) as t.CommentBlock, + ...(ast.leadingComments ?? [])?.filter((c) => !c.value.includes('@managed')), + ]; + + return Prettier.format(generate(ast).code, { + endOfLine: 'auto', + filepath: 'settings.json', + }); +} diff --git a/packages/kbn-dev-utils/src/vscode_config/update_vscode_config_cli.ts b/packages/kbn-dev-utils/src/vscode_config/update_vscode_config_cli.ts new file mode 100644 index 000000000000..273aed958557 --- /dev/null +++ b/packages/kbn-dev-utils/src/vscode_config/update_vscode_config_cli.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import Fs from 'fs/promises'; + +import { REPO_ROOT } from '@kbn/utils'; +import dedent from 'dedent'; + +import { run } from '../run'; + +import { MANAGED_CONFIG_KEYS } from './managed_config_keys'; +import { updateVscodeConfig } from './update_vscode_config'; + +export function runUpdateVscodeConfigCli() { + run(async ({ log }) => { + const path = Path.resolve(REPO_ROOT, '.vscode/settings.json'); + + let json; + try { + json = await Fs.readFile(path, 'utf-8'); + } catch (error) { + if (error.code !== 'ENOENT') { + throw error; + } + } + + const updatedJson = updateVscodeConfig( + MANAGED_CONFIG_KEYS, + dedent` + Some settings in this file are managed by @kbn/dev-utils. When a setting is managed it is preceeded + with a comment "// @managed" comment. Replace that with "// self managed" and the scripts will not + touch that value. Put a "// self managed" comment at the top of the file, or above a group of settings + to disable management of that entire section. + `, + json + ); + await Fs.mkdir(Path.dirname(path), { recursive: true }); + await Fs.writeFile(path, updatedJson); + + log.success('updated', path); + }); +} diff --git a/packages/kbn-es-query/src/es_query/index.ts b/packages/kbn-es-query/src/es_query/index.ts index 6e4a58fbe96c..0690e6ab9843 100644 --- a/packages/kbn-es-query/src/es_query/index.ts +++ b/packages/kbn-es-query/src/es_query/index.ts @@ -10,4 +10,11 @@ export { buildEsQuery, EsQueryConfig } from './build_es_query'; export { buildQueryFromFilters } from './from_filters'; export { luceneStringToDsl } from './lucene_string_to_dsl'; export { decorateQuery } from './decorate_query'; -export { IndexPatternBase, IndexPatternFieldBase, IFieldSubType, BoolQuery } from './types'; +export { + IndexPatternBase, + IndexPatternFieldBase, + IFieldSubType, + BoolQuery, + DataViewBase, + DataViewFieldBase, +} from './types'; diff --git a/packages/kbn-es-query/src/es_query/types.ts b/packages/kbn-es-query/src/es_query/types.ts index 333536a5f3ec..0d443366626a 100644 --- a/packages/kbn-es-query/src/es_query/types.ts +++ b/packages/kbn-es-query/src/es_query/types.ts @@ -21,7 +21,7 @@ export interface IFieldSubType { * A base interface for an index pattern field * @public */ -export interface IndexPatternFieldBase { +export interface DataViewFieldBase { name: string; /** * Kibana field type @@ -40,16 +40,26 @@ export interface IndexPatternFieldBase { scripted?: boolean; } +/** + * @deprecated Use DataViewField instead. All index pattern interfaces were renamed. + */ +export type IndexPatternFieldBase = DataViewFieldBase; + /** * A base interface for an index pattern * @public */ -export interface IndexPatternBase { - fields: IndexPatternFieldBase[]; +export interface DataViewBase { + fields: DataViewFieldBase[]; id?: string; title?: string; } +/** + * @deprecated Use DataViewBase instead. All index pattern interfaces were renamed. + */ +export type IndexPatternBase = DataViewBase; + export interface BoolQuery { must: estypes.QueryDslQueryContainer[]; must_not: estypes.QueryDslQueryContainer[]; diff --git a/packages/kbn-eslint-plugin-eslint/index.js b/packages/kbn-eslint-plugin-eslint/index.js index e5a38e5f0952..a7a9c6b5bebd 100644 --- a/packages/kbn-eslint-plugin-eslint/index.js +++ b/packages/kbn-eslint-plugin-eslint/index.js @@ -12,5 +12,6 @@ module.exports = { 'disallow-license-headers': require('./rules/disallow_license_headers'), 'no-restricted-paths': require('./rules/no_restricted_paths'), module_migration: require('./rules/module_migration'), + no_async_promise_body: require('./rules/no_async_promise_body'), }, }; diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.js b/packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.js new file mode 100644 index 000000000000..317758fd3629 --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.js @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const { parseExpression } = require('@babel/parser'); +const { default: generate } = require('@babel/generator'); +const tsEstree = require('@typescript-eslint/typescript-estree'); +const traverse = require('eslint-traverse'); +const esTypes = tsEstree.AST_NODE_TYPES; +const babelTypes = require('@babel/types'); + +/** @typedef {import("eslint").Rule.RuleModule} Rule */ +/** @typedef {import("@typescript-eslint/parser").ParserServices} ParserServices */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.Expression} Expression */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.ArrowFunctionExpression} ArrowFunctionExpression */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.FunctionExpression} FunctionExpression */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.TryStatement} TryStatement */ +/** @typedef {import("@typescript-eslint/typescript-estree").TSESTree.NewExpression} NewExpression */ +/** @typedef {import("typescript").ExportDeclaration} ExportDeclaration */ +/** @typedef {import("eslint").Rule.RuleFixer} Fixer */ + +const ERROR_MSG = + 'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections'; + +/** + * @param {Expression} node + */ +const isPromise = (node) => node.type === esTypes.Identifier && node.name === 'Promise'; + +/** + * @param {Expression} node + * @returns {node is ArrowFunctionExpression | FunctionExpression} + */ +const isFunc = (node) => + node.type === esTypes.ArrowFunctionExpression || node.type === esTypes.FunctionExpression; + +/** + * @param {any} context + * @param {ArrowFunctionExpression | FunctionExpression} node + */ +const isFuncBodySafe = (context, node) => { + // if the body isn't wrapped in a blockStatement it can't have a try/catch at the root + if (node.body.type !== esTypes.BlockStatement) { + return false; + } + + // when the entire body is wrapped in a try/catch it is the only node + if (node.body.body.length !== 1) { + return false; + } + + const tryNode = node.body.body[0]; + // ensure we have a try node with a handler + if (tryNode.type !== esTypes.TryStatement || !tryNode.handler) { + return false; + } + + // ensure the handler doesn't throw + let hasThrow = false; + traverse(context, tryNode.handler, (path) => { + if (path.node.type === esTypes.ThrowStatement) { + hasThrow = true; + return traverse.STOP; + } + }); + return !hasThrow; +}; + +/** + * @param {string} code + */ +const wrapFunctionInTryCatch = (code) => { + // parse the code with babel so we can mutate the AST + const ast = parseExpression(code, { + plugins: ['typescript', 'jsx'], + }); + + // validate that the code reperesents an arrow or function expression + if (!babelTypes.isArrowFunctionExpression(ast) && !babelTypes.isFunctionExpression(ast)) { + throw new Error('expected function to be an arrow or function expression'); + } + + // ensure that the function receives the second argument, and capture its name if already defined + let rejectName = 'reject'; + if (ast.params.length === 0) { + ast.params.push(babelTypes.identifier('resolve'), babelTypes.identifier(rejectName)); + } else if (ast.params.length === 1) { + ast.params.push(babelTypes.identifier(rejectName)); + } else if (ast.params.length === 2) { + if (babelTypes.isIdentifier(ast.params[1])) { + rejectName = ast.params[1].name; + } else { + throw new Error('expected second param of promise definition function to be an identifier'); + } + } + + // ensure that the body of the function is a blockStatement + let block = ast.body; + if (!babelTypes.isBlockStatement(block)) { + block = babelTypes.blockStatement([babelTypes.returnStatement(block)]); + } + + // redefine the body of the function as a new blockStatement containing a tryStatement + // which catches errors and forwards them to reject() when caught + ast.body = babelTypes.blockStatement([ + // try { + babelTypes.tryStatement( + block, + // catch (error) { + babelTypes.catchClause( + babelTypes.identifier('error'), + babelTypes.blockStatement([ + // reject(error) + babelTypes.expressionStatement( + babelTypes.callExpression(babelTypes.identifier(rejectName), [ + babelTypes.identifier('error'), + ]) + ), + ]) + ) + ), + ]); + + return generate(ast).code; +}; + +/** @type {Rule} */ +module.exports = { + meta: { + fixable: 'code', + schema: [], + }, + create: (context) => ({ + NewExpression(_) { + const node = /** @type {NewExpression} */ (_); + + // ensure we are newing up a promise with a single argument + if (!isPromise(node.callee) || node.arguments.length !== 1) { + return; + } + + const func = node.arguments[0]; + // ensure the argument is an arrow or function expression and is async + if (!isFunc(func) || !func.async) { + return; + } + + // body must be a blockStatement, try/catch can't exist outside of a block + if (!isFuncBodySafe(context, func)) { + context.report({ + message: ERROR_MSG, + loc: func.loc, + fix(fixer) { + const source = context.getSourceCode(); + return fixer.replaceText(func, wrapFunctionInTryCatch(source.getText(func))); + }, + }); + } + }, + }), +}; diff --git a/packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.test.js b/packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.test.js new file mode 100644 index 000000000000..f5929b1b3966 --- /dev/null +++ b/packages/kbn-eslint-plugin-eslint/rules/no_async_promise_body.test.js @@ -0,0 +1,254 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +const { RuleTester } = require('eslint'); +const rule = require('./no_async_promise_body'); +const dedent = require('dedent'); + +const ruleTester = new RuleTester({ + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { + sourceType: 'module', + ecmaVersion: 2018, + ecmaFeatures: { + jsx: true, + }, + }, +}); + +ruleTester.run('@kbn/eslint/no_async_promise_body', rule, { + valid: [ + // caught but no resolve + { + code: dedent` + new Promise(async function (resolve) { + try { + await asyncOperation(); + } catch (error) { + // noop + } + }) + `, + }, + // arrow caught but no resolve + { + code: dedent` + new Promise(async (resolve) => { + try { + await asyncOperation(); + } catch (error) { + // noop + } + }) + `, + }, + // caught with reject + { + code: dedent` + new Promise(async function (resolve, reject) { + try { + await asyncOperation(); + } catch (error) { + reject(error) + } + }) + `, + }, + // arrow caught with reject + { + code: dedent` + new Promise(async (resolve, reject) => { + try { + await asyncOperation(); + } catch (error) { + reject(error) + } + }) + `, + }, + // non async + { + code: dedent` + new Promise(function (resolve) { + setTimeout(resolve, 10); + }) + `, + }, + // arrow non async + { + code: dedent` + new Promise((resolve) => setTimeout(resolve, 10)) + `, + }, + ], + + invalid: [ + // no catch + { + code: dedent` + new Promise(async function (resolve) { + const result = await asyncOperation(); + resolve(result); + }) + `, + errors: [ + { + line: 1, + message: + 'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections', + }, + ], + output: dedent` + new Promise(async function (resolve, reject) { + try { + const result = await asyncOperation(); + resolve(result); + } catch (error) { + reject(error); + } + }) + `, + }, + // arrow no catch + { + code: dedent` + new Promise(async (resolve) => { + const result = await asyncOperation(); + resolve(result); + }) + `, + errors: [ + { + line: 1, + message: + 'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections', + }, + ], + output: dedent` + new Promise(async (resolve, reject) => { + try { + const result = await asyncOperation(); + resolve(result); + } catch (error) { + reject(error); + } + }) + `, + }, + // catch, but it throws + { + code: dedent` + new Promise(async function (resolve) { + try { + const result = await asyncOperation(); + resolve(result); + } catch (error) { + if (error.code === 'foo') { + throw error; + } + } + }) + `, + errors: [ + { + line: 1, + message: + 'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections', + }, + ], + output: dedent` + new Promise(async function (resolve, reject) { + try { + try { + const result = await asyncOperation(); + resolve(result); + } catch (error) { + if (error.code === 'foo') { + throw error; + } + } + } catch (error) { + reject(error); + } + }) + `, + }, + // no catch without block + { + code: dedent` + new Promise(async (resolve) => resolve(await asyncOperation())); + `, + errors: [ + { + line: 1, + message: + 'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections', + }, + ], + output: dedent` + new Promise(async (resolve, reject) => { + try { + return resolve(await asyncOperation()); + } catch (error) { + reject(error); + } + }); + `, + }, + // no catch with named reject + { + code: dedent` + new Promise(async (resolve, rej) => { + const result = await asyncOperation(); + result ? resolve(true) : rej() + }); + `, + errors: [ + { + line: 1, + message: + 'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections', + }, + ], + output: dedent` + new Promise(async (resolve, rej) => { + try { + const result = await asyncOperation(); + result ? resolve(true) : rej(); + } catch (error) { + rej(error); + } + }); + `, + }, + // no catch with no args + { + code: dedent` + new Promise(async () => { + await asyncOperation(); + }); + `, + errors: [ + { + line: 1, + message: + 'Passing an async function to the Promise constructor leads to a hidden promise being created and prevents handling rejections', + }, + ], + output: dedent` + new Promise(async (resolve, reject) => { + try { + await asyncOperation(); + } catch (error) { + reject(error); + } + }); + `, + }, + ], +}); diff --git a/packages/kbn-mapbox-gl/BUILD.bazel b/packages/kbn-mapbox-gl/BUILD.bazel index e0de6848c228..00b3213c4dea 100644 --- a/packages/kbn-mapbox-gl/BUILD.bazel +++ b/packages/kbn-mapbox-gl/BUILD.bazel @@ -30,13 +30,13 @@ NPM_MODULE_EXTRA_FILES = [ RUNTIME_DEPS = [ "@npm//@mapbox/mapbox-gl-rtl-text", "@npm//file-loader", - "@npm//mapbox-gl", + "@npm//maplibre-gl", ] TYPES_DEPS = [ "@npm//@mapbox/mapbox-gl-rtl-text", "@npm//file-loader", - "@npm//@types/mapbox-gl", + "@npm//maplibre-gl", ] jsts_transpiler( diff --git a/packages/kbn-mapbox-gl/src/index.ts b/packages/kbn-mapbox-gl/src/index.ts index 87b85002d759..97c4238f1e56 100644 --- a/packages/kbn-mapbox-gl/src/index.ts +++ b/packages/kbn-mapbox-gl/src/index.ts @@ -24,13 +24,13 @@ import type { MapboxGeoJSONFeature, Point, CustomLayerInterface, -} from 'mapbox-gl'; -import mapboxgl from 'mapbox-gl/dist/mapbox-gl-csp'; +} from 'maplibre-gl'; +import mapboxgl from 'maplibre-gl/dist/maplibre-gl-csp'; // @ts-expect-error import mbRtlPlugin from '!!file-loader!@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js'; // @ts-expect-error -import mbWorkerUrl from '!!file-loader!mapbox-gl/dist/mapbox-gl-csp-worker'; -import 'mapbox-gl/dist/mapbox-gl.css'; +import mbWorkerUrl from '!!file-loader!maplibre-gl/dist/maplibre-gl-csp-worker'; +import 'maplibre-gl/dist/maplibre-gl.css'; mapboxgl.workerUrl = mbWorkerUrl; mapboxgl.setRTLTextPlugin(mbRtlPlugin); diff --git a/packages/kbn-mapbox-gl/src/typings.ts b/packages/kbn-mapbox-gl/src/typings.ts index 0cc6908aca42..76d8850cc1d6 100644 --- a/packages/kbn-mapbox-gl/src/typings.ts +++ b/packages/kbn-mapbox-gl/src/typings.ts @@ -7,4 +7,4 @@ */ // Mapbox-gl doesn't declare this type. -declare module 'mapbox-gl/dist/mapbox-gl-csp'; +declare module 'maplibre-gl/dist/maplibre-gl-csp'; diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 445fa6bff280..77bbeabb7f73 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -112,7 +112,7 @@ pageLoadAssetSize: expressionImage: 19288 expressionMetric: 22238 expressionShape: 34008 - interactiveSetup: 18532 + interactiveSetup: 70000 expressionTagcloud: 27505 expressions: 239290 securitySolution: 231753 diff --git a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts index 55d267b72951..39a7e0ab0e62 100644 --- a/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts +++ b/packages/kbn-optimizer/src/optimizer/optimizer_config.test.ts @@ -446,78 +446,33 @@ describe('OptimizerConfig::create()', () => { } `); - expect(findKibanaPlatformPlugins.mock).toMatchInlineSnapshot(` - Object { - "calls": Array [ - Array [ - Symbol(parsed plugin scan dirs), - Symbol(parsed plugin paths), - ], - ], - "instances": Array [ - [Window], - ], - "invocationCallOrder": Array [ - 25, - ], - "results": Array [ - Object { - "type": "return", - "value": Symbol(new platform plugins), - }, + expect(findKibanaPlatformPlugins.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Symbol(parsed plugin scan dirs), + Symbol(parsed plugin paths), ], - } + ] `); - expect(filterById.mock).toMatchInlineSnapshot(` - Object { - "calls": Array [ - Array [ - Array [], - Symbol(focused bundles), - ], - ], - "instances": Array [ - [Window], - ], - "invocationCallOrder": Array [ - 28, + expect(filterById.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Array [], + Symbol(focused bundles), ], - "results": Array [ - Object { - "type": "return", - "value": Symbol(filtered bundles), - }, - ], - } + ] `); - expect(getPluginBundles.mock).toMatchInlineSnapshot(` - Object { - "calls": Array [ - Array [ - Symbol(new platform plugins), - Symbol(parsed repo root), - Symbol(parsed output root), - Symbol(limits), - ], - ], - "instances": Array [ - [Window], + expect(getPluginBundles.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + Symbol(new platform plugins), + Symbol(parsed repo root), + Symbol(parsed output root), + Symbol(limits), ], - "invocationCallOrder": Array [ - 26, - ], - "results": Array [ - Object { - "type": "return", - "value": Array [ - Symbol(bundle1), - Symbol(bundle2), - ], - }, - ], - } + ] `); }); }); diff --git a/packages/kbn-optimizer/src/worker/webpack.config.ts b/packages/kbn-optimizer/src/worker/webpack.config.ts index 47d34cd8b29f..1ea2849ac098 100644 --- a/packages/kbn-optimizer/src/worker/webpack.config.ts +++ b/packages/kbn-optimizer/src/worker/webpack.config.ts @@ -159,14 +159,14 @@ export function getWebpackConfig(bundle: Bundle, bundleRefs: BundleRefs, worker: { loader: 'sass-loader', options: { - prependData(loaderContext: webpack.loader.LoaderContext) { + additionalData(content: string, loaderContext: webpack.loader.LoaderContext) { return `@import ${stringifyRequest( loaderContext, Path.resolve( worker.repoRoot, `src/core/public/core_app/styles/_globals_${theme}.scss` ) - )};\n`; + )};\n${content}`; }, webpackImporter: false, implementation: require('node-sass'), diff --git a/packages/kbn-pm/dist/index.js b/packages/kbn-pm/dist/index.js index 10b0230a91ff..f0a95a612f02 100644 --- a/packages/kbn-pm/dist/index.js +++ b/packages/kbn-pm/dist/index.js @@ -8968,9 +8968,17 @@ const BootstrapCommand = { // NOTE: We don't probably need this anymore, is actually not being used - await Object(_utils_link_project_executables__WEBPACK_IMPORTED_MODULE_2__["linkProjectExecutables"])(projects, projectGraph); // Build typescript references + await Object(_utils_link_project_executables__WEBPACK_IMPORTED_MODULE_2__["linkProjectExecutables"])(projects, projectGraph); // Update vscode settings - await Object(_utils_child_process__WEBPACK_IMPORTED_MODULE_1__["spawnStreaming"])('node', ['scripts/build_ts_refs', '--ignore-type-failures', '--info'], { + await Object(_utils_child_process__WEBPACK_IMPORTED_MODULE_1__["spawnStreaming"])(process.execPath, ['scripts/update_vscode_config'], { + cwd: kbn.getAbsolute(), + env: process.env + }, { + prefix: '[vscode]', + debug: false + }); // Build typescript references + + await Object(_utils_child_process__WEBPACK_IMPORTED_MODULE_1__["spawnStreaming"])(process.execPath, ['scripts/build_ts_refs', '--ignore-type-failures', '--info'], { cwd: kbn.getAbsolute(), env: process.env }, { @@ -63468,7 +63476,7 @@ __webpack_require__.r(__webpack_exports__); /* harmony import */ var _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(565); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return _build_bazel_production_projects__WEBPACK_IMPORTED_MODULE_0__["buildBazelProductionProjects"]; }); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(813); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(814); /* harmony reexport (safe) */ __webpack_require__.d(__webpack_exports__, "buildNonBazelProductionProjects", function() { return _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_1__["buildNonBazelProductionProjects"]; }); /* @@ -63490,11 +63498,11 @@ __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "buildBazelProductionProjects", function() { return buildBazelProductionProjects; }); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(566); /* harmony import */ var cpy__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(cpy__WEBPACK_IMPORTED_MODULE_0__); -/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(775); +/* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(776); /* harmony import */ var globby__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(globby__WEBPACK_IMPORTED_MODULE_1__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_2__); -/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(813); +/* harmony import */ var _build_non_bazel_production_projects__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(814); /* harmony import */ var _utils_bazel__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(372); /* harmony import */ var _utils_fs__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(188); /* harmony import */ var _utils_log__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(186); @@ -63600,11 +63608,11 @@ const os = __webpack_require__(124); const pMap = __webpack_require__(567); const arrify = __webpack_require__(562); const globby = __webpack_require__(570); -const hasGlob = __webpack_require__(759); -const cpFile = __webpack_require__(761); -const junk = __webpack_require__(771); -const pFilter = __webpack_require__(772); -const CpyError = __webpack_require__(774); +const hasGlob = __webpack_require__(760); +const cpFile = __webpack_require__(762); +const junk = __webpack_require__(772); +const pFilter = __webpack_require__(773); +const CpyError = __webpack_require__(775); const defaultOptions = { ignoreJunk: true @@ -63948,8 +63956,8 @@ const fs = __webpack_require__(142); const arrayUnion = __webpack_require__(571); const glob = __webpack_require__(201); const fastGlob = __webpack_require__(573); -const dirGlob = __webpack_require__(752); -const gitignore = __webpack_require__(755); +const dirGlob = __webpack_require__(753); +const gitignore = __webpack_require__(756); const DEFAULT_FILTER = () => false; @@ -64200,11 +64208,11 @@ module.exports.generateTasks = pkg.generateTasks; Object.defineProperty(exports, "__esModule", { value: true }); var optionsManager = __webpack_require__(575); var taskManager = __webpack_require__(576); -var reader_async_1 = __webpack_require__(723); -var reader_stream_1 = __webpack_require__(747); -var reader_sync_1 = __webpack_require__(748); -var arrayUtils = __webpack_require__(750); -var streamUtils = __webpack_require__(751); +var reader_async_1 = __webpack_require__(724); +var reader_stream_1 = __webpack_require__(748); +var reader_sync_1 = __webpack_require__(749); +var arrayUtils = __webpack_require__(751); +var streamUtils = __webpack_require__(752); /** * Synchronous API. */ @@ -64785,13 +64793,13 @@ module.exports.win32 = win32; var util = __webpack_require__(115); var braces = __webpack_require__(582); var toRegex = __webpack_require__(583); -var extend = __webpack_require__(691); +var extend = __webpack_require__(690); /** * Local dependencies */ -var compilers = __webpack_require__(693); +var compilers = __webpack_require__(692); var parsers = __webpack_require__(719); var cache = __webpack_require__(720); var utils = __webpack_require__(721); @@ -65667,17 +65675,17 @@ module.exports = micromatch; */ var toRegex = __webpack_require__(583); -var unique = __webpack_require__(603); -var extend = __webpack_require__(604); +var unique = __webpack_require__(605); +var extend = __webpack_require__(606); /** * Local dependencies */ -var compilers = __webpack_require__(606); -var parsers = __webpack_require__(619); -var Braces = __webpack_require__(624); -var utils = __webpack_require__(607); +var compilers = __webpack_require__(608); +var parsers = __webpack_require__(621); +var Braces = __webpack_require__(625); +var utils = __webpack_require__(609); var MAX_LENGTH = 1024 * 64; var cache = {}; @@ -65989,8 +65997,8 @@ module.exports = braces; var safe = __webpack_require__(584); var define = __webpack_require__(590); -var extend = __webpack_require__(596); -var not = __webpack_require__(600); +var extend = __webpack_require__(598); +var not = __webpack_require__(602); var MAX_LENGTH = 1024 * 64; /** @@ -66803,7 +66811,7 @@ module.exports = function isObject(val) { var typeOf = __webpack_require__(593); var isAccessor = __webpack_require__(594); -var isData = __webpack_require__(595); +var isData = __webpack_require__(596); module.exports = function isDescriptor(obj, key) { if (typeOf(obj) !== 'object') { @@ -66965,7 +66973,7 @@ function isBuffer(val) { -var typeOf = __webpack_require__(593); +var typeOf = __webpack_require__(595); // accessor descriptor properties var accessor = { @@ -67029,6 +67037,141 @@ module.exports = isAccessorDescriptor; /***/ }), /* 595 */ +/***/ (function(module, exports) { + +var toString = Object.prototype.toString; + +module.exports = function kindOf(val) { + if (val === void 0) return 'undefined'; + if (val === null) return 'null'; + + var type = typeof val; + if (type === 'boolean') return 'boolean'; + if (type === 'string') return 'string'; + if (type === 'number') return 'number'; + if (type === 'symbol') return 'symbol'; + if (type === 'function') { + return isGeneratorFn(val) ? 'generatorfunction' : 'function'; + } + + if (isArray(val)) return 'array'; + if (isBuffer(val)) return 'buffer'; + if (isArguments(val)) return 'arguments'; + if (isDate(val)) return 'date'; + if (isError(val)) return 'error'; + if (isRegexp(val)) return 'regexp'; + + switch (ctorName(val)) { + case 'Symbol': return 'symbol'; + case 'Promise': return 'promise'; + + // Set, Map, WeakSet, WeakMap + case 'WeakMap': return 'weakmap'; + case 'WeakSet': return 'weakset'; + case 'Map': return 'map'; + case 'Set': return 'set'; + + // 8-bit typed arrays + case 'Int8Array': return 'int8array'; + case 'Uint8Array': return 'uint8array'; + case 'Uint8ClampedArray': return 'uint8clampedarray'; + + // 16-bit typed arrays + case 'Int16Array': return 'int16array'; + case 'Uint16Array': return 'uint16array'; + + // 32-bit typed arrays + case 'Int32Array': return 'int32array'; + case 'Uint32Array': return 'uint32array'; + case 'Float32Array': return 'float32array'; + case 'Float64Array': return 'float64array'; + } + + if (isGeneratorObj(val)) { + return 'generator'; + } + + // Non-plain objects + type = toString.call(val); + switch (type) { + case '[object Object]': return 'object'; + // iterators + case '[object Map Iterator]': return 'mapiterator'; + case '[object Set Iterator]': return 'setiterator'; + case '[object String Iterator]': return 'stringiterator'; + case '[object Array Iterator]': return 'arrayiterator'; + } + + // other + return type.slice(8, -1).toLowerCase().replace(/\s/g, ''); +}; + +function ctorName(val) { + return typeof val.constructor === 'function' ? val.constructor.name : null; +} + +function isArray(val) { + if (Array.isArray) return Array.isArray(val); + return val instanceof Array; +} + +function isError(val) { + return val instanceof Error || (typeof val.message === 'string' && val.constructor && typeof val.constructor.stackTraceLimit === 'number'); +} + +function isDate(val) { + if (val instanceof Date) return true; + return typeof val.toDateString === 'function' + && typeof val.getDate === 'function' + && typeof val.setDate === 'function'; +} + +function isRegexp(val) { + if (val instanceof RegExp) return true; + return typeof val.flags === 'string' + && typeof val.ignoreCase === 'boolean' + && typeof val.multiline === 'boolean' + && typeof val.global === 'boolean'; +} + +function isGeneratorFn(name, val) { + return ctorName(name) === 'GeneratorFunction'; +} + +function isGeneratorObj(val) { + return typeof val.throw === 'function' + && typeof val.return === 'function' + && typeof val.next === 'function'; +} + +function isArguments(val) { + try { + if (typeof val.length === 'number' && typeof val.callee === 'function') { + return true; + } + } catch (err) { + if (err.message.indexOf('callee') !== -1) { + return true; + } + } + return false; +} + +/** + * If you need to support Safari 5-7 (8-10 yr-old browser), + * take a look at https://github.com/feross/is-buffer + */ + +function isBuffer(val) { + if (val.constructor && typeof val.constructor.isBuffer === 'function') { + return val.constructor.isBuffer(val); + } + return false; +} + + +/***/ }), +/* 596 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67041,7 +67184,7 @@ module.exports = isAccessorDescriptor; -var typeOf = __webpack_require__(593); +var typeOf = __webpack_require__(597); module.exports = function isDataDescriptor(obj, prop) { // data descriptor properties @@ -67084,14 +67227,149 @@ module.exports = function isDataDescriptor(obj, prop) { /***/ }), -/* 596 */ +/* 597 */ +/***/ (function(module, exports) { + +var toString = Object.prototype.toString; + +module.exports = function kindOf(val) { + if (val === void 0) return 'undefined'; + if (val === null) return 'null'; + + var type = typeof val; + if (type === 'boolean') return 'boolean'; + if (type === 'string') return 'string'; + if (type === 'number') return 'number'; + if (type === 'symbol') return 'symbol'; + if (type === 'function') { + return isGeneratorFn(val) ? 'generatorfunction' : 'function'; + } + + if (isArray(val)) return 'array'; + if (isBuffer(val)) return 'buffer'; + if (isArguments(val)) return 'arguments'; + if (isDate(val)) return 'date'; + if (isError(val)) return 'error'; + if (isRegexp(val)) return 'regexp'; + + switch (ctorName(val)) { + case 'Symbol': return 'symbol'; + case 'Promise': return 'promise'; + + // Set, Map, WeakSet, WeakMap + case 'WeakMap': return 'weakmap'; + case 'WeakSet': return 'weakset'; + case 'Map': return 'map'; + case 'Set': return 'set'; + + // 8-bit typed arrays + case 'Int8Array': return 'int8array'; + case 'Uint8Array': return 'uint8array'; + case 'Uint8ClampedArray': return 'uint8clampedarray'; + + // 16-bit typed arrays + case 'Int16Array': return 'int16array'; + case 'Uint16Array': return 'uint16array'; + + // 32-bit typed arrays + case 'Int32Array': return 'int32array'; + case 'Uint32Array': return 'uint32array'; + case 'Float32Array': return 'float32array'; + case 'Float64Array': return 'float64array'; + } + + if (isGeneratorObj(val)) { + return 'generator'; + } + + // Non-plain objects + type = toString.call(val); + switch (type) { + case '[object Object]': return 'object'; + // iterators + case '[object Map Iterator]': return 'mapiterator'; + case '[object Set Iterator]': return 'setiterator'; + case '[object String Iterator]': return 'stringiterator'; + case '[object Array Iterator]': return 'arrayiterator'; + } + + // other + return type.slice(8, -1).toLowerCase().replace(/\s/g, ''); +}; + +function ctorName(val) { + return typeof val.constructor === 'function' ? val.constructor.name : null; +} + +function isArray(val) { + if (Array.isArray) return Array.isArray(val); + return val instanceof Array; +} + +function isError(val) { + return val instanceof Error || (typeof val.message === 'string' && val.constructor && typeof val.constructor.stackTraceLimit === 'number'); +} + +function isDate(val) { + if (val instanceof Date) return true; + return typeof val.toDateString === 'function' + && typeof val.getDate === 'function' + && typeof val.setDate === 'function'; +} + +function isRegexp(val) { + if (val instanceof RegExp) return true; + return typeof val.flags === 'string' + && typeof val.ignoreCase === 'boolean' + && typeof val.multiline === 'boolean' + && typeof val.global === 'boolean'; +} + +function isGeneratorFn(name, val) { + return ctorName(name) === 'GeneratorFunction'; +} + +function isGeneratorObj(val) { + return typeof val.throw === 'function' + && typeof val.return === 'function' + && typeof val.next === 'function'; +} + +function isArguments(val) { + try { + if (typeof val.length === 'number' && typeof val.callee === 'function') { + return true; + } + } catch (err) { + if (err.message.indexOf('callee') !== -1) { + return true; + } + } + return false; +} + +/** + * If you need to support Safari 5-7 (8-10 yr-old browser), + * take a look at https://github.com/feross/is-buffer + */ + +function isBuffer(val) { + if (val.constructor && typeof val.constructor.isBuffer === 'function') { + return val.constructor.isBuffer(val); + } + return false; +} + + +/***/ }), +/* 598 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(597); -var assignSymbols = __webpack_require__(599); +var isExtendable = __webpack_require__(599); +var assignSymbols = __webpack_require__(601); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -67151,7 +67429,7 @@ function isEnum(obj, key) { /***/ }), -/* 597 */ +/* 599 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67164,7 +67442,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(598); +var isPlainObject = __webpack_require__(600); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -67172,7 +67450,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 598 */ +/* 600 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67216,7 +67494,7 @@ module.exports = function isPlainObject(o) { /***/ }), -/* 599 */ +/* 601 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67263,13 +67541,13 @@ module.exports = function(receiver, objects) { /***/ }), -/* 600 */ +/* 602 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(601); +var extend = __webpack_require__(603); var safe = __webpack_require__(584); /** @@ -67342,14 +67620,14 @@ module.exports = toRegex; /***/ }), -/* 601 */ +/* 603 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(602); -var assignSymbols = __webpack_require__(599); +var isExtendable = __webpack_require__(604); +var assignSymbols = __webpack_require__(601); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -67409,7 +67687,7 @@ function isEnum(obj, key) { /***/ }), -/* 602 */ +/* 604 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67422,7 +67700,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(598); +var isPlainObject = __webpack_require__(600); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -67430,7 +67708,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 603 */ +/* 605 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67480,13 +67758,13 @@ module.exports.immutable = function uniqueImmutable(arr) { /***/ }), -/* 604 */ +/* 606 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isObject = __webpack_require__(605); +var isObject = __webpack_require__(607); module.exports = function extend(o/*, objects*/) { if (!isObject(o)) { o = {}; } @@ -67520,7 +67798,7 @@ function hasOwn(obj, key) { /***/ }), -/* 605 */ +/* 607 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -67540,13 +67818,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 606 */ +/* 608 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var utils = __webpack_require__(607); +var utils = __webpack_require__(609); module.exports = function(braces, options) { braces.compiler @@ -67829,25 +68107,25 @@ function hasQueue(node) { /***/ }), -/* 607 */ +/* 609 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var splitString = __webpack_require__(608); +var splitString = __webpack_require__(610); var utils = module.exports; /** * Module dependencies */ -utils.extend = __webpack_require__(604); -utils.flatten = __webpack_require__(611); +utils.extend = __webpack_require__(606); +utils.flatten = __webpack_require__(613); utils.isObject = __webpack_require__(591); -utils.fillRange = __webpack_require__(612); -utils.repeat = __webpack_require__(618); -utils.unique = __webpack_require__(603); +utils.fillRange = __webpack_require__(614); +utils.repeat = __webpack_require__(620); +utils.unique = __webpack_require__(605); utils.define = function(obj, key, val) { Object.defineProperty(obj, key, { @@ -68179,7 +68457,7 @@ utils.escapeRegex = function(str) { /***/ }), -/* 608 */ +/* 610 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68192,7 +68470,7 @@ utils.escapeRegex = function(str) { -var extend = __webpack_require__(609); +var extend = __webpack_require__(611); module.exports = function(str, options, fn) { if (typeof str !== 'string') { @@ -68357,14 +68635,14 @@ function keepEscaping(opts, str, idx) { /***/ }), -/* 609 */ +/* 611 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(610); -var assignSymbols = __webpack_require__(599); +var isExtendable = __webpack_require__(612); +var assignSymbols = __webpack_require__(601); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -68424,7 +68702,7 @@ function isEnum(obj, key) { /***/ }), -/* 610 */ +/* 612 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68437,7 +68715,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(598); +var isPlainObject = __webpack_require__(600); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -68445,7 +68723,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 611 */ +/* 613 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68474,7 +68752,7 @@ function flat(arr, res) { /***/ }), -/* 612 */ +/* 614 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68488,10 +68766,10 @@ function flat(arr, res) { var util = __webpack_require__(115); -var isNumber = __webpack_require__(613); -var extend = __webpack_require__(604); -var repeat = __webpack_require__(616); -var toRegex = __webpack_require__(617); +var isNumber = __webpack_require__(615); +var extend = __webpack_require__(606); +var repeat = __webpack_require__(618); +var toRegex = __webpack_require__(619); /** * Return a range of numbers or letters. @@ -68689,7 +68967,7 @@ module.exports = fillRange; /***/ }), -/* 613 */ +/* 615 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68702,7 +68980,7 @@ module.exports = fillRange; -var typeOf = __webpack_require__(614); +var typeOf = __webpack_require__(616); module.exports = function isNumber(num) { var type = typeOf(num); @@ -68718,10 +68996,10 @@ module.exports = function isNumber(num) { /***/ }), -/* 614 */ +/* 616 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(615); +var isBuffer = __webpack_require__(617); var toString = Object.prototype.toString; /** @@ -68840,7 +69118,7 @@ module.exports = function kindOf(val) { /***/ }), -/* 615 */ +/* 617 */ /***/ (function(module, exports) { /*! @@ -68867,7 +69145,7 @@ function isSlowBuffer (obj) { /***/ }), -/* 616 */ +/* 618 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68944,7 +69222,7 @@ function repeat(str, num) { /***/ }), -/* 617 */ +/* 619 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -68957,8 +69235,8 @@ function repeat(str, num) { -var repeat = __webpack_require__(616); -var isNumber = __webpack_require__(613); +var repeat = __webpack_require__(618); +var isNumber = __webpack_require__(615); var cache = {}; function toRegexRange(min, max, options) { @@ -69245,7 +69523,7 @@ module.exports = toRegexRange; /***/ }), -/* 618 */ +/* 620 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -69270,14 +69548,14 @@ module.exports = function repeat(ele, num) { /***/ }), -/* 619 */ +/* 621 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Node = __webpack_require__(620); -var utils = __webpack_require__(607); +var Node = __webpack_require__(622); +var utils = __webpack_require__(609); /** * Braces parsers @@ -69637,15 +69915,15 @@ function concatNodes(pos, node, parent, options) { /***/ }), -/* 620 */ +/* 622 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var isObject = __webpack_require__(591); -var define = __webpack_require__(621); -var utils = __webpack_require__(622); +var define = __webpack_require__(623); +var utils = __webpack_require__(624); var ownNames; /** @@ -70136,7 +70414,7 @@ exports = module.exports = Node; /***/ }), -/* 621 */ +/* 623 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -70174,13 +70452,13 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 622 */ +/* 624 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var typeOf = __webpack_require__(623); +var typeOf = __webpack_require__(616); var utils = module.exports; /** @@ -71200,139 +71478,17 @@ function assert(val, message) { /***/ }), -/* 623 */ -/***/ (function(module, exports, __webpack_require__) { - -var isBuffer = __webpack_require__(615); -var toString = Object.prototype.toString; - -/** - * Get the native `typeof` a value. - * - * @param {*} `val` - * @return {*} Native javascript type - */ - -module.exports = function kindOf(val) { - // primitivies - if (typeof val === 'undefined') { - return 'undefined'; - } - if (val === null) { - return 'null'; - } - if (val === true || val === false || val instanceof Boolean) { - return 'boolean'; - } - if (typeof val === 'string' || val instanceof String) { - return 'string'; - } - if (typeof val === 'number' || val instanceof Number) { - return 'number'; - } - - // functions - if (typeof val === 'function' || val instanceof Function) { - return 'function'; - } - - // array - if (typeof Array.isArray !== 'undefined' && Array.isArray(val)) { - return 'array'; - } - - // check for instances of RegExp and Date before calling `toString` - if (val instanceof RegExp) { - return 'regexp'; - } - if (val instanceof Date) { - return 'date'; - } - - // other objects - var type = toString.call(val); - - if (type === '[object RegExp]') { - return 'regexp'; - } - if (type === '[object Date]') { - return 'date'; - } - if (type === '[object Arguments]') { - return 'arguments'; - } - if (type === '[object Error]') { - return 'error'; - } - - // buffer - if (isBuffer(val)) { - return 'buffer'; - } - - // es6: Map, WeakMap, Set, WeakSet - if (type === '[object Set]') { - return 'set'; - } - if (type === '[object WeakSet]') { - return 'weakset'; - } - if (type === '[object Map]') { - return 'map'; - } - if (type === '[object WeakMap]') { - return 'weakmap'; - } - if (type === '[object Symbol]') { - return 'symbol'; - } - - // typed arrays - if (type === '[object Int8Array]') { - return 'int8array'; - } - if (type === '[object Uint8Array]') { - return 'uint8array'; - } - if (type === '[object Uint8ClampedArray]') { - return 'uint8clampedarray'; - } - if (type === '[object Int16Array]') { - return 'int16array'; - } - if (type === '[object Uint16Array]') { - return 'uint16array'; - } - if (type === '[object Int32Array]') { - return 'int32array'; - } - if (type === '[object Uint32Array]') { - return 'uint32array'; - } - if (type === '[object Float32Array]') { - return 'float32array'; - } - if (type === '[object Float64Array]') { - return 'float64array'; - } - - // must be a plain object - return 'object'; -}; - - -/***/ }), -/* 624 */ +/* 625 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var extend = __webpack_require__(604); -var Snapdragon = __webpack_require__(625); -var compilers = __webpack_require__(606); -var parsers = __webpack_require__(619); -var utils = __webpack_require__(607); +var extend = __webpack_require__(606); +var Snapdragon = __webpack_require__(626); +var compilers = __webpack_require__(608); +var parsers = __webpack_require__(621); +var utils = __webpack_require__(609); /** * Customize Snapdragon parser and renderer @@ -71433,17 +71589,17 @@ module.exports = Braces; /***/ }), -/* 625 */ +/* 626 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var Base = __webpack_require__(626); +var Base = __webpack_require__(627); var define = __webpack_require__(654); -var Compiler = __webpack_require__(665); -var Parser = __webpack_require__(688); -var utils = __webpack_require__(668); +var Compiler = __webpack_require__(664); +var Parser = __webpack_require__(687); +var utils = __webpack_require__(667); var regexCache = {}; var cache = {}; @@ -71614,16 +71770,16 @@ module.exports.Parser = Parser; /***/ }), -/* 626 */ +/* 627 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(115); -var define = __webpack_require__(627); -var CacheBase = __webpack_require__(628); -var Emitter = __webpack_require__(629); +var define = __webpack_require__(628); +var CacheBase = __webpack_require__(629); +var Emitter = __webpack_require__(630); var isObject = __webpack_require__(591); var merge = __webpack_require__(648); var pascal = __webpack_require__(651); @@ -72056,7 +72212,7 @@ module.exports.namespace = namespace; /***/ }), -/* 627 */ +/* 628 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72094,16 +72250,16 @@ module.exports = function defineProperty(obj, prop, val) { /***/ }), -/* 628 */ +/* 629 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var isObject = __webpack_require__(591); -var Emitter = __webpack_require__(629); -var visit = __webpack_require__(630); -var toPath = __webpack_require__(633); +var Emitter = __webpack_require__(630); +var visit = __webpack_require__(631); +var toPath = __webpack_require__(634); var union = __webpack_require__(635); var del = __webpack_require__(639); var get = __webpack_require__(637); @@ -72362,7 +72518,7 @@ module.exports.namespace = namespace; /***/ }), -/* 629 */ +/* 630 */ /***/ (function(module, exports, __webpack_require__) { @@ -72531,7 +72687,7 @@ Emitter.prototype.hasListeners = function(event){ /***/ }), -/* 630 */ +/* 631 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72544,8 +72700,8 @@ Emitter.prototype.hasListeners = function(event){ -var visit = __webpack_require__(631); -var mapVisit = __webpack_require__(632); +var visit = __webpack_require__(632); +var mapVisit = __webpack_require__(633); module.exports = function(collection, method, val) { var result; @@ -72568,7 +72724,7 @@ module.exports = function(collection, method, val) { /***/ }), -/* 631 */ +/* 632 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72608,14 +72764,14 @@ module.exports = function visit(thisArg, method, target, val) { /***/ }), -/* 632 */ +/* 633 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; var util = __webpack_require__(115); -var visit = __webpack_require__(631); +var visit = __webpack_require__(632); /** * Map `visit` over an array of objects. @@ -72652,7 +72808,7 @@ function isObject(val) { /***/ }), -/* 633 */ +/* 634 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -72665,7 +72821,7 @@ function isObject(val) { -var typeOf = __webpack_require__(634); +var typeOf = __webpack_require__(616); module.exports = function toPath(args) { if (typeOf(args) !== 'arguments') { @@ -72691,128 +72847,6 @@ function filter(arr) { } -/***/ }), -/* 634 */ -/***/ (function(module, exports, __webpack_require__) { - -var isBuffer = __webpack_require__(615); -var toString = Object.prototype.toString; - -/** - * Get the native `typeof` a value. - * - * @param {*} `val` - * @return {*} Native javascript type - */ - -module.exports = function kindOf(val) { - // primitivies - if (typeof val === 'undefined') { - return 'undefined'; - } - if (val === null) { - return 'null'; - } - if (val === true || val === false || val instanceof Boolean) { - return 'boolean'; - } - if (typeof val === 'string' || val instanceof String) { - return 'string'; - } - if (typeof val === 'number' || val instanceof Number) { - return 'number'; - } - - // functions - if (typeof val === 'function' || val instanceof Function) { - return 'function'; - } - - // array - if (typeof Array.isArray !== 'undefined' && Array.isArray(val)) { - return 'array'; - } - - // check for instances of RegExp and Date before calling `toString` - if (val instanceof RegExp) { - return 'regexp'; - } - if (val instanceof Date) { - return 'date'; - } - - // other objects - var type = toString.call(val); - - if (type === '[object RegExp]') { - return 'regexp'; - } - if (type === '[object Date]') { - return 'date'; - } - if (type === '[object Arguments]') { - return 'arguments'; - } - if (type === '[object Error]') { - return 'error'; - } - - // buffer - if (isBuffer(val)) { - return 'buffer'; - } - - // es6: Map, WeakMap, Set, WeakSet - if (type === '[object Set]') { - return 'set'; - } - if (type === '[object WeakSet]') { - return 'weakset'; - } - if (type === '[object Map]') { - return 'map'; - } - if (type === '[object WeakMap]') { - return 'weakmap'; - } - if (type === '[object Symbol]') { - return 'symbol'; - } - - // typed arrays - if (type === '[object Int8Array]') { - return 'int8array'; - } - if (type === '[object Uint8Array]') { - return 'uint8array'; - } - if (type === '[object Uint8ClampedArray]') { - return 'uint8clampedarray'; - } - if (type === '[object Int16Array]') { - return 'int16array'; - } - if (type === '[object Uint16Array]') { - return 'uint16array'; - } - if (type === '[object Int32Array]') { - return 'int32array'; - } - if (type === '[object Uint32Array]') { - return 'uint32array'; - } - if (type === '[object Float32Array]') { - return 'float32array'; - } - if (type === '[object Float64Array]') { - return 'float64array'; - } - - // must be a plain object - return 'object'; -}; - - /***/ }), /* 635 */ /***/ (function(module, exports, __webpack_require__) { @@ -72820,7 +72854,7 @@ module.exports = function kindOf(val) { "use strict"; -var isObject = __webpack_require__(605); +var isObject = __webpack_require__(607); var union = __webpack_require__(636); var get = __webpack_require__(637); var set = __webpack_require__(638); @@ -72956,10 +72990,10 @@ function toString(val) { -var split = __webpack_require__(608); -var extend = __webpack_require__(604); -var isPlainObject = __webpack_require__(598); -var isObject = __webpack_require__(605); +var split = __webpack_require__(610); +var extend = __webpack_require__(606); +var isPlainObject = __webpack_require__(600); +var isObject = __webpack_require__(607); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -73182,7 +73216,7 @@ module.exports = function(val, prop) { var typeOf = __webpack_require__(646); -var isNumber = __webpack_require__(613); +var isNumber = __webpack_require__(615); module.exports = function hasValue(val) { // is-number checks for NaN and other edge cases @@ -73238,7 +73272,7 @@ module.exports = function hasValue(val) { /* 646 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(615); +var isBuffer = __webpack_require__(617); var toString = Object.prototype.toString; /** @@ -73373,10 +73407,10 @@ module.exports = function kindOf(val) { -var split = __webpack_require__(608); -var extend = __webpack_require__(604); -var isPlainObject = __webpack_require__(598); -var isObject = __webpack_require__(605); +var split = __webpack_require__(610); +var extend = __webpack_require__(606); +var isPlainObject = __webpack_require__(600); +var isObject = __webpack_require__(607); module.exports = function(obj, prop, val) { if (!isObject(obj)) { @@ -73506,7 +73540,7 @@ module.exports = mixinDeep; -var isPlainObject = __webpack_require__(598); +var isPlainObject = __webpack_require__(600); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -74263,7 +74297,7 @@ module.exports = isAccessorDescriptor; /* 658 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(615); +var isBuffer = __webpack_require__(617); var toString = Object.prototype.toString; /** @@ -74447,7 +74481,7 @@ module.exports = isDataDescriptor; /* 660 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(615); +var isBuffer = __webpack_require__(617); var toString = Object.prototype.toString; /** @@ -74669,8 +74703,8 @@ module.exports = extend; "use strict"; -var typeOf = __webpack_require__(663); -var copyDescriptor = __webpack_require__(664); +var typeOf = __webpack_require__(616); +var copyDescriptor = __webpack_require__(663); var define = __webpack_require__(654); /** @@ -74847,128 +74881,6 @@ module.exports.has = has; /* 663 */ /***/ (function(module, exports, __webpack_require__) { -var isBuffer = __webpack_require__(615); -var toString = Object.prototype.toString; - -/** - * Get the native `typeof` a value. - * - * @param {*} `val` - * @return {*} Native javascript type - */ - -module.exports = function kindOf(val) { - // primitivies - if (typeof val === 'undefined') { - return 'undefined'; - } - if (val === null) { - return 'null'; - } - if (val === true || val === false || val instanceof Boolean) { - return 'boolean'; - } - if (typeof val === 'string' || val instanceof String) { - return 'string'; - } - if (typeof val === 'number' || val instanceof Number) { - return 'number'; - } - - // functions - if (typeof val === 'function' || val instanceof Function) { - return 'function'; - } - - // array - if (typeof Array.isArray !== 'undefined' && Array.isArray(val)) { - return 'array'; - } - - // check for instances of RegExp and Date before calling `toString` - if (val instanceof RegExp) { - return 'regexp'; - } - if (val instanceof Date) { - return 'date'; - } - - // other objects - var type = toString.call(val); - - if (type === '[object RegExp]') { - return 'regexp'; - } - if (type === '[object Date]') { - return 'date'; - } - if (type === '[object Arguments]') { - return 'arguments'; - } - if (type === '[object Error]') { - return 'error'; - } - - // buffer - if (isBuffer(val)) { - return 'buffer'; - } - - // es6: Map, WeakMap, Set, WeakSet - if (type === '[object Set]') { - return 'set'; - } - if (type === '[object WeakSet]') { - return 'weakset'; - } - if (type === '[object Map]') { - return 'map'; - } - if (type === '[object WeakMap]') { - return 'weakmap'; - } - if (type === '[object Symbol]') { - return 'symbol'; - } - - // typed arrays - if (type === '[object Int8Array]') { - return 'int8array'; - } - if (type === '[object Uint8Array]') { - return 'uint8array'; - } - if (type === '[object Uint8ClampedArray]') { - return 'uint8clampedarray'; - } - if (type === '[object Int16Array]') { - return 'int16array'; - } - if (type === '[object Uint16Array]') { - return 'uint16array'; - } - if (type === '[object Int32Array]') { - return 'int32array'; - } - if (type === '[object Uint32Array]') { - return 'uint32array'; - } - if (type === '[object Float32Array]') { - return 'float32array'; - } - if (type === '[object Float64Array]') { - return 'float64array'; - } - - // must be a plain object - return 'object'; -}; - - -/***/ }), -/* 664 */ -/***/ (function(module, exports, __webpack_require__) { - "use strict"; /*! * copy-descriptor @@ -75054,16 +74966,16 @@ function isObject(val) { /***/ }), -/* 665 */ +/* 664 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(666); +var use = __webpack_require__(665); var define = __webpack_require__(654); var debug = __webpack_require__(544)('snapdragon:compiler'); -var utils = __webpack_require__(668); +var utils = __webpack_require__(667); /** * Create a new `Compiler` with the given `options`. @@ -75217,7 +75129,7 @@ Compiler.prototype = { // source map support if (opts.sourcemap) { - var sourcemaps = __webpack_require__(687); + var sourcemaps = __webpack_require__(686); sourcemaps(this); this.mapVisit(this.ast.nodes); this.applySourceMaps(); @@ -75238,7 +75150,7 @@ module.exports = Compiler; /***/ }), -/* 666 */ +/* 665 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75251,7 +75163,7 @@ module.exports = Compiler; -var utils = __webpack_require__(667); +var utils = __webpack_require__(666); module.exports = function base(app, opts) { if (!utils.isObject(app) && typeof app !== 'function') { @@ -75366,7 +75278,7 @@ module.exports = function base(app, opts) { /***/ }), -/* 667 */ +/* 666 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75396,7 +75308,7 @@ module.exports = utils; /***/ }), -/* 668 */ +/* 667 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -75406,9 +75318,9 @@ module.exports = utils; * Module dependencies */ -exports.extend = __webpack_require__(604); -exports.SourceMap = __webpack_require__(669); -exports.sourceMapResolve = __webpack_require__(680); +exports.extend = __webpack_require__(606); +exports.SourceMap = __webpack_require__(668); +exports.sourceMapResolve = __webpack_require__(679); /** * Convert backslash in the given string to forward slashes @@ -75451,7 +75363,7 @@ exports.last = function(arr, n) { /***/ }), -/* 669 */ +/* 668 */ /***/ (function(module, exports, __webpack_require__) { /* @@ -75459,13 +75371,13 @@ exports.last = function(arr, n) { * Licensed under the New BSD license. See LICENSE.txt or: * http://opensource.org/licenses/BSD-3-Clause */ -exports.SourceMapGenerator = __webpack_require__(670).SourceMapGenerator; -exports.SourceMapConsumer = __webpack_require__(676).SourceMapConsumer; -exports.SourceNode = __webpack_require__(679).SourceNode; +exports.SourceMapGenerator = __webpack_require__(669).SourceMapGenerator; +exports.SourceMapConsumer = __webpack_require__(675).SourceMapConsumer; +exports.SourceNode = __webpack_require__(678).SourceNode; /***/ }), -/* 670 */ +/* 669 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75475,10 +75387,10 @@ exports.SourceNode = __webpack_require__(679).SourceNode; * http://opensource.org/licenses/BSD-3-Clause */ -var base64VLQ = __webpack_require__(671); -var util = __webpack_require__(673); -var ArraySet = __webpack_require__(674).ArraySet; -var MappingList = __webpack_require__(675).MappingList; +var base64VLQ = __webpack_require__(670); +var util = __webpack_require__(672); +var ArraySet = __webpack_require__(673).ArraySet; +var MappingList = __webpack_require__(674).MappingList; /** * An instance of the SourceMapGenerator represents a source map which is @@ -75887,7 +75799,7 @@ exports.SourceMapGenerator = SourceMapGenerator; /***/ }), -/* 671 */ +/* 670 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -75927,7 +75839,7 @@ exports.SourceMapGenerator = SourceMapGenerator; * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -var base64 = __webpack_require__(672); +var base64 = __webpack_require__(671); // A single base 64 digit can contain 6 bits of data. For the base 64 variable // length quantities we use in the source map spec, the first bit is the sign, @@ -76033,7 +75945,7 @@ exports.decode = function base64VLQ_decode(aStr, aIndex, aOutParam) { /***/ }), -/* 672 */ +/* 671 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76106,7 +76018,7 @@ exports.decode = function (charCode) { /***/ }), -/* 673 */ +/* 672 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76529,7 +76441,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate /***/ }), -/* 674 */ +/* 673 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76539,7 +76451,7 @@ exports.compareByGeneratedPositionsInflated = compareByGeneratedPositionsInflate * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(673); +var util = __webpack_require__(672); var has = Object.prototype.hasOwnProperty; var hasNativeMap = typeof Map !== "undefined"; @@ -76656,7 +76568,7 @@ exports.ArraySet = ArraySet; /***/ }), -/* 675 */ +/* 674 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76666,7 +76578,7 @@ exports.ArraySet = ArraySet; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(673); +var util = __webpack_require__(672); /** * Determine whether mappingB is after mappingA with respect to generated @@ -76741,7 +76653,7 @@ exports.MappingList = MappingList; /***/ }), -/* 676 */ +/* 675 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -76751,11 +76663,11 @@ exports.MappingList = MappingList; * http://opensource.org/licenses/BSD-3-Clause */ -var util = __webpack_require__(673); -var binarySearch = __webpack_require__(677); -var ArraySet = __webpack_require__(674).ArraySet; -var base64VLQ = __webpack_require__(671); -var quickSort = __webpack_require__(678).quickSort; +var util = __webpack_require__(672); +var binarySearch = __webpack_require__(676); +var ArraySet = __webpack_require__(673).ArraySet; +var base64VLQ = __webpack_require__(670); +var quickSort = __webpack_require__(677).quickSort; function SourceMapConsumer(aSourceMap) { var sourceMap = aSourceMap; @@ -77829,7 +77741,7 @@ exports.IndexedSourceMapConsumer = IndexedSourceMapConsumer; /***/ }), -/* 677 */ +/* 676 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -77946,7 +77858,7 @@ exports.search = function search(aNeedle, aHaystack, aCompare, aBias) { /***/ }), -/* 678 */ +/* 677 */ /***/ (function(module, exports) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -78066,7 +77978,7 @@ exports.quickSort = function (ary, comparator) { /***/ }), -/* 679 */ +/* 678 */ /***/ (function(module, exports, __webpack_require__) { /* -*- Mode: js; js-indent-level: 2; -*- */ @@ -78076,8 +77988,8 @@ exports.quickSort = function (ary, comparator) { * http://opensource.org/licenses/BSD-3-Clause */ -var SourceMapGenerator = __webpack_require__(670).SourceMapGenerator; -var util = __webpack_require__(673); +var SourceMapGenerator = __webpack_require__(669).SourceMapGenerator; +var util = __webpack_require__(672); // Matches a Windows-style `\r\n` newline or a `\n` newline used by all other // operating systems these days (capturing the result). @@ -78485,17 +78397,17 @@ exports.SourceNode = SourceNode; /***/ }), -/* 680 */ +/* 679 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014, 2015, 2016, 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var sourceMappingURL = __webpack_require__(681) -var resolveUrl = __webpack_require__(682) -var decodeUriComponent = __webpack_require__(683) -var urix = __webpack_require__(685) -var atob = __webpack_require__(686) +var sourceMappingURL = __webpack_require__(680) +var resolveUrl = __webpack_require__(681) +var decodeUriComponent = __webpack_require__(682) +var urix = __webpack_require__(684) +var atob = __webpack_require__(685) @@ -78793,7 +78705,7 @@ module.exports = { /***/ }), -/* 681 */ +/* 680 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_RESULT__;// Copyright 2014 Simon Lydell @@ -78856,7 +78768,7 @@ void (function(root, factory) { /***/ }), -/* 682 */ +/* 681 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -78874,13 +78786,13 @@ module.exports = resolveUrl /***/ }), -/* 683 */ +/* 682 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2017 Simon Lydell // X11 (“MIT”) Licensed. (See LICENSE.) -var decodeUriComponent = __webpack_require__(684) +var decodeUriComponent = __webpack_require__(683) function customDecodeUriComponent(string) { // `decodeUriComponent` turns `+` into ` `, but that's not wanted. @@ -78891,7 +78803,7 @@ module.exports = customDecodeUriComponent /***/ }), -/* 684 */ +/* 683 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -78992,7 +78904,7 @@ module.exports = function (encodedURI) { /***/ }), -/* 685 */ +/* 684 */ /***/ (function(module, exports, __webpack_require__) { // Copyright 2014 Simon Lydell @@ -79015,7 +78927,7 @@ module.exports = urix /***/ }), -/* 686 */ +/* 685 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79029,7 +78941,7 @@ module.exports = atob.atob = atob; /***/ }), -/* 687 */ +/* 686 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79038,7 +78950,7 @@ module.exports = atob.atob = atob; var fs = __webpack_require__(142); var path = __webpack_require__(4); var define = __webpack_require__(654); -var utils = __webpack_require__(668); +var utils = __webpack_require__(667); /** * Expose `mixin()`. @@ -79181,19 +79093,19 @@ exports.comment = function(node) { /***/ }), -/* 688 */ +/* 687 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var use = __webpack_require__(666); +var use = __webpack_require__(665); var util = __webpack_require__(115); -var Cache = __webpack_require__(689); +var Cache = __webpack_require__(688); var define = __webpack_require__(654); var debug = __webpack_require__(544)('snapdragon:parser'); -var Position = __webpack_require__(690); -var utils = __webpack_require__(668); +var Position = __webpack_require__(689); +var utils = __webpack_require__(667); /** * Create a new `Parser` with the given `input` and `options`. @@ -79721,7 +79633,7 @@ module.exports = Parser; /***/ }), -/* 689 */ +/* 688 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79828,7 +79740,7 @@ MapCache.prototype.del = function mapDelete(key) { /***/ }), -/* 690 */ +/* 689 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79849,14 +79761,14 @@ module.exports = function Position(start, parser) { /***/ }), -/* 691 */ +/* 690 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(692); -var assignSymbols = __webpack_require__(599); +var isExtendable = __webpack_require__(691); +var assignSymbols = __webpack_require__(601); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -79916,7 +79828,7 @@ function isEnum(obj, key) { /***/ }), -/* 692 */ +/* 691 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -79929,7 +79841,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(598); +var isPlainObject = __webpack_require__(600); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -79937,13 +79849,13 @@ module.exports = function isExtendable(val) { /***/ }), -/* 693 */ +/* 692 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var nanomatch = __webpack_require__(694); +var nanomatch = __webpack_require__(693); var extglob = __webpack_require__(708); module.exports = function(snapdragon) { @@ -80021,7 +79933,7 @@ function escapeExtglobs(compiler) { /***/ }), -/* 694 */ +/* 693 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80033,16 +79945,16 @@ function escapeExtglobs(compiler) { var util = __webpack_require__(115); var toRegex = __webpack_require__(583); -var extend = __webpack_require__(695); +var extend = __webpack_require__(694); /** * Local dependencies */ -var compilers = __webpack_require__(697); -var parsers = __webpack_require__(698); -var cache = __webpack_require__(701); -var utils = __webpack_require__(703); +var compilers = __webpack_require__(696); +var parsers = __webpack_require__(697); +var cache = __webpack_require__(700); +var utils = __webpack_require__(702); var MAX_LENGTH = 1024 * 64; /** @@ -80866,14 +80778,14 @@ module.exports = nanomatch; /***/ }), -/* 695 */ +/* 694 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var isExtendable = __webpack_require__(696); -var assignSymbols = __webpack_require__(599); +var isExtendable = __webpack_require__(695); +var assignSymbols = __webpack_require__(601); module.exports = Object.assign || function(obj/*, objects*/) { if (obj === null || typeof obj === 'undefined') { @@ -80933,7 +80845,7 @@ function isEnum(obj, key) { /***/ }), -/* 696 */ +/* 695 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -80946,7 +80858,7 @@ function isEnum(obj, key) { -var isPlainObject = __webpack_require__(598); +var isPlainObject = __webpack_require__(600); module.exports = function isExtendable(val) { return isPlainObject(val) || typeof val === 'function' || Array.isArray(val); @@ -80954,7 +80866,7 @@ module.exports = function isExtendable(val) { /***/ }), -/* 697 */ +/* 696 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81300,15 +81212,15 @@ module.exports = function(nanomatch, options) { /***/ }), -/* 698 */ +/* 697 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -var regexNot = __webpack_require__(600); +var regexNot = __webpack_require__(602); var toRegex = __webpack_require__(583); -var isOdd = __webpack_require__(699); +var isOdd = __webpack_require__(698); /** * Characters to use in negation regex (we want to "not" match @@ -81694,7 +81606,7 @@ module.exports.not = NOT_REGEX; /***/ }), -/* 699 */ +/* 698 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81707,7 +81619,7 @@ module.exports.not = NOT_REGEX; -var isNumber = __webpack_require__(700); +var isNumber = __webpack_require__(699); module.exports = function isOdd(i) { if (!isNumber(i)) { @@ -81721,7 +81633,7 @@ module.exports = function isOdd(i) { /***/ }), -/* 700 */ +/* 699 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81749,14 +81661,14 @@ module.exports = function isNumber(num) { /***/ }), -/* 701 */ +/* 700 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(702))(); +module.exports = new (__webpack_require__(701))(); /***/ }), -/* 702 */ +/* 701 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81769,7 +81681,7 @@ module.exports = new (__webpack_require__(702))(); -var MapCache = __webpack_require__(689); +var MapCache = __webpack_require__(688); /** * Create a new `FragmentCache` with an optional object to use for `caches`. @@ -81891,7 +81803,7 @@ exports = module.exports = FragmentCache; /***/ }), -/* 703 */ +/* 702 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -81904,14 +81816,14 @@ var path = __webpack_require__(4); * Module dependencies */ -var isWindows = __webpack_require__(704)(); -var Snapdragon = __webpack_require__(625); -utils.define = __webpack_require__(705); -utils.diff = __webpack_require__(706); -utils.extend = __webpack_require__(695); -utils.pick = __webpack_require__(707); -utils.typeOf = __webpack_require__(593); -utils.unique = __webpack_require__(603); +var isWindows = __webpack_require__(703)(); +var Snapdragon = __webpack_require__(626); +utils.define = __webpack_require__(704); +utils.diff = __webpack_require__(705); +utils.extend = __webpack_require__(694); +utils.pick = __webpack_require__(706); +utils.typeOf = __webpack_require__(707); +utils.unique = __webpack_require__(605); /** * Returns true if the given value is effectively an empty string @@ -82277,7 +82189,7 @@ utils.unixify = function(options) { /***/ }), -/* 704 */ +/* 703 */ /***/ (function(module, exports, __webpack_require__) { var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_DEFINE_RESULT__;/*! @@ -82305,7 +82217,7 @@ var __WEBPACK_AMD_DEFINE_FACTORY__, __WEBPACK_AMD_DEFINE_ARRAY__, __WEBPACK_AMD_ /***/ }), -/* 705 */ +/* 704 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82350,7 +82262,7 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), -/* 706 */ +/* 705 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82404,7 +82316,7 @@ function diffArray(one, two) { /***/ }), -/* 707 */ +/* 706 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -82445,6 +82357,141 @@ module.exports = function pick(obj, keys) { }; +/***/ }), +/* 707 */ +/***/ (function(module, exports) { + +var toString = Object.prototype.toString; + +module.exports = function kindOf(val) { + if (val === void 0) return 'undefined'; + if (val === null) return 'null'; + + var type = typeof val; + if (type === 'boolean') return 'boolean'; + if (type === 'string') return 'string'; + if (type === 'number') return 'number'; + if (type === 'symbol') return 'symbol'; + if (type === 'function') { + return isGeneratorFn(val) ? 'generatorfunction' : 'function'; + } + + if (isArray(val)) return 'array'; + if (isBuffer(val)) return 'buffer'; + if (isArguments(val)) return 'arguments'; + if (isDate(val)) return 'date'; + if (isError(val)) return 'error'; + if (isRegexp(val)) return 'regexp'; + + switch (ctorName(val)) { + case 'Symbol': return 'symbol'; + case 'Promise': return 'promise'; + + // Set, Map, WeakSet, WeakMap + case 'WeakMap': return 'weakmap'; + case 'WeakSet': return 'weakset'; + case 'Map': return 'map'; + case 'Set': return 'set'; + + // 8-bit typed arrays + case 'Int8Array': return 'int8array'; + case 'Uint8Array': return 'uint8array'; + case 'Uint8ClampedArray': return 'uint8clampedarray'; + + // 16-bit typed arrays + case 'Int16Array': return 'int16array'; + case 'Uint16Array': return 'uint16array'; + + // 32-bit typed arrays + case 'Int32Array': return 'int32array'; + case 'Uint32Array': return 'uint32array'; + case 'Float32Array': return 'float32array'; + case 'Float64Array': return 'float64array'; + } + + if (isGeneratorObj(val)) { + return 'generator'; + } + + // Non-plain objects + type = toString.call(val); + switch (type) { + case '[object Object]': return 'object'; + // iterators + case '[object Map Iterator]': return 'mapiterator'; + case '[object Set Iterator]': return 'setiterator'; + case '[object String Iterator]': return 'stringiterator'; + case '[object Array Iterator]': return 'arrayiterator'; + } + + // other + return type.slice(8, -1).toLowerCase().replace(/\s/g, ''); +}; + +function ctorName(val) { + return typeof val.constructor === 'function' ? val.constructor.name : null; +} + +function isArray(val) { + if (Array.isArray) return Array.isArray(val); + return val instanceof Array; +} + +function isError(val) { + return val instanceof Error || (typeof val.message === 'string' && val.constructor && typeof val.constructor.stackTraceLimit === 'number'); +} + +function isDate(val) { + if (val instanceof Date) return true; + return typeof val.toDateString === 'function' + && typeof val.getDate === 'function' + && typeof val.setDate === 'function'; +} + +function isRegexp(val) { + if (val instanceof RegExp) return true; + return typeof val.flags === 'string' + && typeof val.ignoreCase === 'boolean' + && typeof val.multiline === 'boolean' + && typeof val.global === 'boolean'; +} + +function isGeneratorFn(name, val) { + return ctorName(name) === 'GeneratorFunction'; +} + +function isGeneratorObj(val) { + return typeof val.throw === 'function' + && typeof val.return === 'function' + && typeof val.next === 'function'; +} + +function isArguments(val) { + try { + if (typeof val.length === 'number' && typeof val.callee === 'function') { + return true; + } + } catch (err) { + if (err.message.indexOf('callee') !== -1) { + return true; + } + } + return false; +} + +/** + * If you need to support Safari 5-7 (8-10 yr-old browser), + * take a look at https://github.com/feross/is-buffer + */ + +function isBuffer(val) { + if (val.constructor && typeof val.constructor.isBuffer === 'function') { + return val.constructor.isBuffer(val); + } + return false; +} + + /***/ }), /* 708 */ /***/ (function(module, exports, __webpack_require__) { @@ -82456,8 +82503,8 @@ module.exports = function pick(obj, keys) { * Module dependencies */ -var extend = __webpack_require__(604); -var unique = __webpack_require__(603); +var extend = __webpack_require__(606); +var unique = __webpack_require__(605); var toRegex = __webpack_require__(583); /** @@ -82978,8 +83025,8 @@ var parsers = __webpack_require__(713); */ var debug = __webpack_require__(544)('expand-brackets'); -var extend = __webpack_require__(604); -var Snapdragon = __webpack_require__(625); +var extend = __webpack_require__(606); +var Snapdragon = __webpack_require__(626); var toRegex = __webpack_require__(583); /** @@ -83534,7 +83581,7 @@ module.exports.TEXT_REGEX = TEXT_REGEX; var toRegex = __webpack_require__(583); -var regexNot = __webpack_require__(600); +var regexNot = __webpack_require__(602); var cached; /** @@ -83775,8 +83822,8 @@ module.exports = function defineProperty(obj, prop, val) { "use strict"; -var regex = __webpack_require__(600); -var Cache = __webpack_require__(702); +var regex = __webpack_require__(602); +var Cache = __webpack_require__(701); /** * Utils @@ -83855,9 +83902,9 @@ utils.createRegex = function(str) { * Module dependencies */ -var Snapdragon = __webpack_require__(625); +var Snapdragon = __webpack_require__(626); var define = __webpack_require__(716); -var extend = __webpack_require__(604); +var extend = __webpack_require__(606); /** * Local dependencies @@ -83937,8 +83984,8 @@ module.exports = Extglob; var extglob = __webpack_require__(708); -var nanomatch = __webpack_require__(694); -var regexNot = __webpack_require__(600); +var nanomatch = __webpack_require__(693); +var regexNot = __webpack_require__(602); var toRegex = __webpack_require__(583); var not; @@ -84023,7 +84070,7 @@ function textRegex(pattern) { /* 720 */ /***/ (function(module, exports, __webpack_require__) { -module.exports = new (__webpack_require__(702))(); +module.exports = new (__webpack_require__(701))(); /***/ }), @@ -84040,13 +84087,13 @@ var path = __webpack_require__(4); * Module dependencies */ -var Snapdragon = __webpack_require__(625); +var Snapdragon = __webpack_require__(626); utils.define = __webpack_require__(722); -utils.diff = __webpack_require__(706); -utils.extend = __webpack_require__(691); -utils.pick = __webpack_require__(707); -utils.typeOf = __webpack_require__(593); -utils.unique = __webpack_require__(603); +utils.diff = __webpack_require__(705); +utils.extend = __webpack_require__(690); +utils.pick = __webpack_require__(706); +utils.typeOf = __webpack_require__(723); +utils.unique = __webpack_require__(605); /** * Returns true if the platform is windows, or `path.sep` is `\\`. @@ -84389,6 +84436,141 @@ module.exports = function defineProperty(obj, key, val) { /***/ }), /* 723 */ +/***/ (function(module, exports) { + +var toString = Object.prototype.toString; + +module.exports = function kindOf(val) { + if (val === void 0) return 'undefined'; + if (val === null) return 'null'; + + var type = typeof val; + if (type === 'boolean') return 'boolean'; + if (type === 'string') return 'string'; + if (type === 'number') return 'number'; + if (type === 'symbol') return 'symbol'; + if (type === 'function') { + return isGeneratorFn(val) ? 'generatorfunction' : 'function'; + } + + if (isArray(val)) return 'array'; + if (isBuffer(val)) return 'buffer'; + if (isArguments(val)) return 'arguments'; + if (isDate(val)) return 'date'; + if (isError(val)) return 'error'; + if (isRegexp(val)) return 'regexp'; + + switch (ctorName(val)) { + case 'Symbol': return 'symbol'; + case 'Promise': return 'promise'; + + // Set, Map, WeakSet, WeakMap + case 'WeakMap': return 'weakmap'; + case 'WeakSet': return 'weakset'; + case 'Map': return 'map'; + case 'Set': return 'set'; + + // 8-bit typed arrays + case 'Int8Array': return 'int8array'; + case 'Uint8Array': return 'uint8array'; + case 'Uint8ClampedArray': return 'uint8clampedarray'; + + // 16-bit typed arrays + case 'Int16Array': return 'int16array'; + case 'Uint16Array': return 'uint16array'; + + // 32-bit typed arrays + case 'Int32Array': return 'int32array'; + case 'Uint32Array': return 'uint32array'; + case 'Float32Array': return 'float32array'; + case 'Float64Array': return 'float64array'; + } + + if (isGeneratorObj(val)) { + return 'generator'; + } + + // Non-plain objects + type = toString.call(val); + switch (type) { + case '[object Object]': return 'object'; + // iterators + case '[object Map Iterator]': return 'mapiterator'; + case '[object Set Iterator]': return 'setiterator'; + case '[object String Iterator]': return 'stringiterator'; + case '[object Array Iterator]': return 'arrayiterator'; + } + + // other + return type.slice(8, -1).toLowerCase().replace(/\s/g, ''); +}; + +function ctorName(val) { + return typeof val.constructor === 'function' ? val.constructor.name : null; +} + +function isArray(val) { + if (Array.isArray) return Array.isArray(val); + return val instanceof Array; +} + +function isError(val) { + return val instanceof Error || (typeof val.message === 'string' && val.constructor && typeof val.constructor.stackTraceLimit === 'number'); +} + +function isDate(val) { + if (val instanceof Date) return true; + return typeof val.toDateString === 'function' + && typeof val.getDate === 'function' + && typeof val.setDate === 'function'; +} + +function isRegexp(val) { + if (val instanceof RegExp) return true; + return typeof val.flags === 'string' + && typeof val.ignoreCase === 'boolean' + && typeof val.multiline === 'boolean' + && typeof val.global === 'boolean'; +} + +function isGeneratorFn(name, val) { + return ctorName(name) === 'GeneratorFunction'; +} + +function isGeneratorObj(val) { + return typeof val.throw === 'function' + && typeof val.return === 'function' + && typeof val.next === 'function'; +} + +function isArguments(val) { + try { + if (typeof val.length === 'number' && typeof val.callee === 'function') { + return true; + } + } catch (err) { + if (err.message.indexOf('callee') !== -1) { + return true; + } + } + return false; +} + +/** + * If you need to support Safari 5-7 (8-10 yr-old browser), + * take a look at https://github.com/feross/is-buffer + */ + +function isBuffer(val) { + if (val.constructor && typeof val.constructor.isBuffer === 'function') { + return val.constructor.isBuffer(val); + } + return false; +} + + +/***/ }), +/* 724 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84407,9 +84589,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(724); -var reader_1 = __webpack_require__(737); -var fs_stream_1 = __webpack_require__(741); +var readdir = __webpack_require__(725); +var reader_1 = __webpack_require__(738); +var fs_stream_1 = __webpack_require__(742); var ReaderAsync = /** @class */ (function (_super) { __extends(ReaderAsync, _super); function ReaderAsync() { @@ -84470,15 +84652,15 @@ exports.default = ReaderAsync; /***/ }), -/* 724 */ +/* 725 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const readdirSync = __webpack_require__(725); -const readdirAsync = __webpack_require__(733); -const readdirStream = __webpack_require__(736); +const readdirSync = __webpack_require__(726); +const readdirAsync = __webpack_require__(734); +const readdirStream = __webpack_require__(737); module.exports = exports = readdirAsyncPath; exports.readdir = exports.readdirAsync = exports.async = readdirAsyncPath; @@ -84562,7 +84744,7 @@ function readdirStreamStat (dir, options) { /***/ }), -/* 725 */ +/* 726 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84570,11 +84752,11 @@ function readdirStreamStat (dir, options) { module.exports = readdirSync; -const DirectoryReader = __webpack_require__(726); +const DirectoryReader = __webpack_require__(727); let syncFacade = { - fs: __webpack_require__(731), - forEach: __webpack_require__(732), + fs: __webpack_require__(732), + forEach: __webpack_require__(733), sync: true }; @@ -84603,7 +84785,7 @@ function readdirSync (dir, options, internalOptions) { /***/ }), -/* 726 */ +/* 727 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -84612,9 +84794,9 @@ function readdirSync (dir, options, internalOptions) { const Readable = __webpack_require__(134).Readable; const EventEmitter = __webpack_require__(166).EventEmitter; const path = __webpack_require__(4); -const normalizeOptions = __webpack_require__(727); -const stat = __webpack_require__(729); -const call = __webpack_require__(730); +const normalizeOptions = __webpack_require__(728); +const stat = __webpack_require__(730); +const call = __webpack_require__(731); /** * Asynchronously reads the contents of a directory and streams the results @@ -84990,14 +85172,14 @@ module.exports = DirectoryReader; /***/ }), -/* 727 */ +/* 728 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const globToRegExp = __webpack_require__(728); +const globToRegExp = __webpack_require__(729); module.exports = normalizeOptions; @@ -85174,7 +85356,7 @@ function normalizeOptions (options, internalOptions) { /***/ }), -/* 728 */ +/* 729 */ /***/ (function(module, exports) { module.exports = function (glob, opts) { @@ -85311,13 +85493,13 @@ module.exports = function (glob, opts) { /***/ }), -/* 729 */ +/* 730 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const call = __webpack_require__(730); +const call = __webpack_require__(731); module.exports = stat; @@ -85392,7 +85574,7 @@ function symlinkStat (fs, path, lstats, callback) { /***/ }), -/* 730 */ +/* 731 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85453,14 +85635,14 @@ function callOnce (fn) { /***/ }), -/* 731 */ +/* 732 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(142); -const call = __webpack_require__(730); +const call = __webpack_require__(731); /** * A facade around {@link fs.readdirSync} that allows it to be called @@ -85524,7 +85706,7 @@ exports.lstat = function (path, callback) { /***/ }), -/* 732 */ +/* 733 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85553,7 +85735,7 @@ function syncForEach (array, iterator, done) { /***/ }), -/* 733 */ +/* 734 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85561,12 +85743,12 @@ function syncForEach (array, iterator, done) { module.exports = readdirAsync; -const maybe = __webpack_require__(734); -const DirectoryReader = __webpack_require__(726); +const maybe = __webpack_require__(735); +const DirectoryReader = __webpack_require__(727); let asyncFacade = { fs: __webpack_require__(142), - forEach: __webpack_require__(735), + forEach: __webpack_require__(736), async: true }; @@ -85608,7 +85790,7 @@ function readdirAsync (dir, options, callback, internalOptions) { /***/ }), -/* 734 */ +/* 735 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85635,7 +85817,7 @@ module.exports = function maybe (cb, promise) { /***/ }), -/* 735 */ +/* 736 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85671,7 +85853,7 @@ function asyncForEach (array, iterator, done) { /***/ }), -/* 736 */ +/* 737 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85679,11 +85861,11 @@ function asyncForEach (array, iterator, done) { module.exports = readdirStream; -const DirectoryReader = __webpack_require__(726); +const DirectoryReader = __webpack_require__(727); let streamFacade = { fs: __webpack_require__(142), - forEach: __webpack_require__(735), + forEach: __webpack_require__(736), async: true }; @@ -85703,16 +85885,16 @@ function readdirStream (dir, options, internalOptions) { /***/ }), -/* 737 */ +/* 738 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var path = __webpack_require__(4); -var deep_1 = __webpack_require__(738); -var entry_1 = __webpack_require__(740); -var pathUtil = __webpack_require__(739); +var deep_1 = __webpack_require__(739); +var entry_1 = __webpack_require__(741); +var pathUtil = __webpack_require__(740); var Reader = /** @class */ (function () { function Reader(options) { this.options = options; @@ -85778,13 +85960,13 @@ exports.default = Reader; /***/ }), -/* 738 */ +/* 739 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(739); +var pathUtils = __webpack_require__(740); var patternUtils = __webpack_require__(577); var DeepFilter = /** @class */ (function () { function DeepFilter(options, micromatchOptions) { @@ -85868,7 +86050,7 @@ exports.default = DeepFilter; /***/ }), -/* 739 */ +/* 740 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -85899,13 +86081,13 @@ exports.makeAbsolute = makeAbsolute; /***/ }), -/* 740 */ +/* 741 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -var pathUtils = __webpack_require__(739); +var pathUtils = __webpack_require__(740); var patternUtils = __webpack_require__(577); var EntryFilter = /** @class */ (function () { function EntryFilter(options, micromatchOptions) { @@ -85991,7 +86173,7 @@ exports.default = EntryFilter; /***/ }), -/* 741 */ +/* 742 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86011,8 +86193,8 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(134); -var fsStat = __webpack_require__(742); -var fs_1 = __webpack_require__(746); +var fsStat = __webpack_require__(743); +var fs_1 = __webpack_require__(747); var FileSystemStream = /** @class */ (function (_super) { __extends(FileSystemStream, _super); function FileSystemStream() { @@ -86062,14 +86244,14 @@ exports.default = FileSystemStream; /***/ }), -/* 742 */ +/* 743 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const optionsManager = __webpack_require__(743); -const statProvider = __webpack_require__(745); +const optionsManager = __webpack_require__(744); +const statProvider = __webpack_require__(746); /** * Asynchronous API. */ @@ -86100,13 +86282,13 @@ exports.statSync = statSync; /***/ }), -/* 743 */ +/* 744 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const fsAdapter = __webpack_require__(744); +const fsAdapter = __webpack_require__(745); function prepare(opts) { const options = Object.assign({ fs: fsAdapter.getFileSystemAdapter(opts ? opts.fs : undefined), @@ -86119,7 +86301,7 @@ exports.prepare = prepare; /***/ }), -/* 744 */ +/* 745 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86142,7 +86324,7 @@ exports.getFileSystemAdapter = getFileSystemAdapter; /***/ }), -/* 745 */ +/* 746 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86194,7 +86376,7 @@ exports.isFollowedSymlink = isFollowedSymlink; /***/ }), -/* 746 */ +/* 747 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86225,7 +86407,7 @@ exports.default = FileSystem; /***/ }), -/* 747 */ +/* 748 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86245,9 +86427,9 @@ var __extends = (this && this.__extends) || (function () { })(); Object.defineProperty(exports, "__esModule", { value: true }); var stream = __webpack_require__(134); -var readdir = __webpack_require__(724); -var reader_1 = __webpack_require__(737); -var fs_stream_1 = __webpack_require__(741); +var readdir = __webpack_require__(725); +var reader_1 = __webpack_require__(738); +var fs_stream_1 = __webpack_require__(742); var TransformStream = /** @class */ (function (_super) { __extends(TransformStream, _super); function TransformStream(reader) { @@ -86315,7 +86497,7 @@ exports.default = ReaderStream; /***/ }), -/* 748 */ +/* 749 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86334,9 +86516,9 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var readdir = __webpack_require__(724); -var reader_1 = __webpack_require__(737); -var fs_sync_1 = __webpack_require__(749); +var readdir = __webpack_require__(725); +var reader_1 = __webpack_require__(738); +var fs_sync_1 = __webpack_require__(750); var ReaderSync = /** @class */ (function (_super) { __extends(ReaderSync, _super); function ReaderSync() { @@ -86396,7 +86578,7 @@ exports.default = ReaderSync; /***/ }), -/* 749 */ +/* 750 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86415,8 +86597,8 @@ var __extends = (this && this.__extends) || (function () { }; })(); Object.defineProperty(exports, "__esModule", { value: true }); -var fsStat = __webpack_require__(742); -var fs_1 = __webpack_require__(746); +var fsStat = __webpack_require__(743); +var fs_1 = __webpack_require__(747); var FileSystemSync = /** @class */ (function (_super) { __extends(FileSystemSync, _super); function FileSystemSync() { @@ -86462,7 +86644,7 @@ exports.default = FileSystemSync; /***/ }), -/* 750 */ +/* 751 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86478,7 +86660,7 @@ exports.flatten = flatten; /***/ }), -/* 751 */ +/* 752 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86499,13 +86681,13 @@ exports.merge = merge; /***/ }), -/* 752 */ +/* 753 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); -const pathType = __webpack_require__(753); +const pathType = __webpack_require__(754); const getExtensions = extensions => extensions.length > 1 ? `{${extensions.join(',')}}` : extensions[0]; @@ -86571,13 +86753,13 @@ module.exports.sync = (input, opts) => { /***/ }), -/* 753 */ +/* 754 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const fs = __webpack_require__(142); -const pify = __webpack_require__(754); +const pify = __webpack_require__(755); function type(fn, fn2, fp) { if (typeof fp !== 'string') { @@ -86620,7 +86802,7 @@ exports.symlinkSync = typeSync.bind(null, 'lstatSync', 'isSymbolicLink'); /***/ }), -/* 754 */ +/* 755 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86711,7 +86893,7 @@ module.exports = (obj, opts) => { /***/ }), -/* 755 */ +/* 756 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -86719,9 +86901,9 @@ module.exports = (obj, opts) => { const fs = __webpack_require__(142); const path = __webpack_require__(4); const fastGlob = __webpack_require__(573); -const gitIgnore = __webpack_require__(756); -const pify = __webpack_require__(757); -const slash = __webpack_require__(758); +const gitIgnore = __webpack_require__(757); +const pify = __webpack_require__(758); +const slash = __webpack_require__(759); const DEFAULT_IGNORE = [ '**/node_modules/**', @@ -86819,7 +87001,7 @@ module.exports.sync = options => { /***/ }), -/* 756 */ +/* 757 */ /***/ (function(module, exports) { // A simple implementation of make-array @@ -87288,7 +87470,7 @@ module.exports = options => new IgnoreBase(options) /***/ }), -/* 757 */ +/* 758 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87363,7 +87545,7 @@ module.exports = (input, options) => { /***/ }), -/* 758 */ +/* 759 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87381,7 +87563,7 @@ module.exports = input => { /***/ }), -/* 759 */ +/* 760 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87394,7 +87576,7 @@ module.exports = input => { -var isGlob = __webpack_require__(760); +var isGlob = __webpack_require__(761); module.exports = function hasGlob(val) { if (val == null) return false; @@ -87414,7 +87596,7 @@ module.exports = function hasGlob(val) { /***/ }), -/* 760 */ +/* 761 */ /***/ (function(module, exports, __webpack_require__) { /*! @@ -87445,17 +87627,17 @@ module.exports = function isGlob(str) { /***/ }), -/* 761 */ +/* 762 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const path = __webpack_require__(4); const {constants: fsConstants} = __webpack_require__(142); -const pEvent = __webpack_require__(762); -const CpFileError = __webpack_require__(765); -const fs = __webpack_require__(767); -const ProgressEmitter = __webpack_require__(770); +const pEvent = __webpack_require__(763); +const CpFileError = __webpack_require__(766); +const fs = __webpack_require__(768); +const ProgressEmitter = __webpack_require__(771); const cpFileAsync = async (source, destination, options, progressEmitter) => { let readError; @@ -87569,12 +87751,12 @@ module.exports.sync = (source, destination, options) => { /***/ }), -/* 762 */ +/* 763 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pTimeout = __webpack_require__(763); +const pTimeout = __webpack_require__(764); const symbolAsyncIterator = Symbol.asyncIterator || '@@asyncIterator'; @@ -87865,12 +88047,12 @@ module.exports.iterator = (emitter, event, options) => { /***/ }), -/* 763 */ +/* 764 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pFinally = __webpack_require__(764); +const pFinally = __webpack_require__(765); class TimeoutError extends Error { constructor(message) { @@ -87916,7 +88098,7 @@ module.exports.TimeoutError = TimeoutError; /***/ }), -/* 764 */ +/* 765 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -87938,12 +88120,12 @@ module.exports = (promise, onFinally) => { /***/ }), -/* 765 */ +/* 766 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(766); +const NestedError = __webpack_require__(767); class CpFileError extends NestedError { constructor(message, nested) { @@ -87957,7 +88139,7 @@ module.exports = CpFileError; /***/ }), -/* 766 */ +/* 767 */ /***/ (function(module, exports, __webpack_require__) { var inherits = __webpack_require__(115).inherits; @@ -88013,16 +88195,16 @@ module.exports = NestedError; /***/ }), -/* 767 */ +/* 768 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const {promisify} = __webpack_require__(115); const fs = __webpack_require__(190); -const makeDir = __webpack_require__(768); -const pEvent = __webpack_require__(762); -const CpFileError = __webpack_require__(765); +const makeDir = __webpack_require__(769); +const pEvent = __webpack_require__(763); +const CpFileError = __webpack_require__(766); const stat = promisify(fs.stat); const lstat = promisify(fs.lstat); @@ -88119,7 +88301,7 @@ exports.copyFileSync = (source, destination, flags) => { /***/ }), -/* 768 */ +/* 769 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -88127,7 +88309,7 @@ exports.copyFileSync = (source, destination, flags) => { const fs = __webpack_require__(142); const path = __webpack_require__(4); const {promisify} = __webpack_require__(115); -const semver = __webpack_require__(769); +const semver = __webpack_require__(770); const useNativeRecursiveOption = semver.satisfies(process.version, '>=10.12.0'); @@ -88282,7 +88464,7 @@ module.exports.sync = (input, options) => { /***/ }), -/* 769 */ +/* 770 */ /***/ (function(module, exports) { exports = module.exports = SemVer @@ -89884,7 +90066,7 @@ function coerce (version, options) { /***/ }), -/* 770 */ +/* 771 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89925,7 +90107,7 @@ module.exports = ProgressEmitter; /***/ }), -/* 771 */ +/* 772 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -89971,12 +90153,12 @@ exports.default = module.exports; /***/ }), -/* 772 */ +/* 773 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const pMap = __webpack_require__(773); +const pMap = __webpack_require__(774); const pFilter = async (iterable, filterer, options) => { const values = await pMap( @@ -89993,7 +90175,7 @@ module.exports.default = pFilter; /***/ }), -/* 773 */ +/* 774 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90072,12 +90254,12 @@ module.exports.default = pMap; /***/ }), -/* 774 */ +/* 775 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const NestedError = __webpack_require__(766); +const NestedError = __webpack_require__(767); class CpyError extends NestedError { constructor(message, nested) { @@ -90091,7 +90273,7 @@ module.exports = CpyError; /***/ }), -/* 775 */ +/* 776 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90099,10 +90281,10 @@ module.exports = CpyError; const fs = __webpack_require__(142); const arrayUnion = __webpack_require__(199); const merge2 = __webpack_require__(200); -const fastGlob = __webpack_require__(776); +const fastGlob = __webpack_require__(777); const dirGlob = __webpack_require__(283); -const gitignore = __webpack_require__(811); -const {FilterStream, UniqueStream} = __webpack_require__(812); +const gitignore = __webpack_require__(812); +const {FilterStream, UniqueStream} = __webpack_require__(813); const DEFAULT_FILTER = () => false; @@ -90279,17 +90461,17 @@ module.exports.gitignore = gitignore; /***/ }), -/* 776 */ +/* 777 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const taskManager = __webpack_require__(777); -const async_1 = __webpack_require__(797); -const stream_1 = __webpack_require__(807); -const sync_1 = __webpack_require__(808); -const settings_1 = __webpack_require__(810); -const utils = __webpack_require__(778); +const taskManager = __webpack_require__(778); +const async_1 = __webpack_require__(798); +const stream_1 = __webpack_require__(808); +const sync_1 = __webpack_require__(809); +const settings_1 = __webpack_require__(811); +const utils = __webpack_require__(779); async function FastGlob(source, options) { assertPatternsInput(source); const works = getWorks(source, async_1.default, options); @@ -90353,14 +90535,14 @@ module.exports = FastGlob; /***/ }), -/* 777 */ +/* 778 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.convertPatternGroupToTask = exports.convertPatternGroupsToTasks = exports.groupPatternsByBaseDirectory = exports.getNegativePatternsAsPositive = exports.getPositivePatterns = exports.convertPatternsToTasks = exports.generate = void 0; -const utils = __webpack_require__(778); +const utils = __webpack_require__(779); function generate(patterns, settings) { const positivePatterns = getPositivePatterns(patterns); const negativePatterns = getNegativePatternsAsPositive(patterns, settings.ignore); @@ -90425,31 +90607,31 @@ exports.convertPatternGroupToTask = convertPatternGroupToTask; /***/ }), -/* 778 */ +/* 779 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.string = exports.stream = exports.pattern = exports.path = exports.fs = exports.errno = exports.array = void 0; -const array = __webpack_require__(779); +const array = __webpack_require__(780); exports.array = array; -const errno = __webpack_require__(780); +const errno = __webpack_require__(781); exports.errno = errno; -const fs = __webpack_require__(781); +const fs = __webpack_require__(782); exports.fs = fs; -const path = __webpack_require__(782); +const path = __webpack_require__(783); exports.path = path; -const pattern = __webpack_require__(783); +const pattern = __webpack_require__(784); exports.pattern = pattern; -const stream = __webpack_require__(795); +const stream = __webpack_require__(796); exports.stream = stream; -const string = __webpack_require__(796); +const string = __webpack_require__(797); exports.string = string; /***/ }), -/* 779 */ +/* 780 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90478,7 +90660,7 @@ exports.splitWhen = splitWhen; /***/ }), -/* 780 */ +/* 781 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90492,7 +90674,7 @@ exports.isEnoentCodeError = isEnoentCodeError; /***/ }), -/* 781 */ +/* 782 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90518,7 +90700,7 @@ exports.createDirentFromStats = createDirentFromStats; /***/ }), -/* 782 */ +/* 783 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90558,7 +90740,7 @@ exports.removeLeadingDotSegment = removeLeadingDotSegment; /***/ }), -/* 783 */ +/* 784 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -90567,7 +90749,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.matchAny = exports.convertPatternsToRe = exports.makeRe = exports.getPatternParts = exports.expandBraceExpansion = exports.expandPatternsWithBraceExpansion = exports.isAffectDepthOfReadingPattern = exports.endsWithSlashGlobStar = exports.hasGlobStar = exports.getBaseDirectory = exports.getPositivePatterns = exports.getNegativePatterns = exports.isPositivePattern = exports.isNegativePattern = exports.convertToNegativePattern = exports.convertToPositivePattern = exports.isDynamicPattern = exports.isStaticPattern = void 0; const path = __webpack_require__(4); const globParent = __webpack_require__(222); -const micromatch = __webpack_require__(784); +const micromatch = __webpack_require__(785); const picomatch = __webpack_require__(236); const GLOBSTAR = '**'; const ESCAPE_SYMBOL = '\\'; @@ -90697,14 +90879,14 @@ exports.matchAny = matchAny; /***/ }), -/* 784 */ +/* 785 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; const util = __webpack_require__(115); -const braces = __webpack_require__(785); +const braces = __webpack_require__(786); const picomatch = __webpack_require__(236); const utils = __webpack_require__(239); const isEmptyString = val => typeof val === 'string' && (val === '' || val === './'); @@ -91171,16 +91353,16 @@ module.exports = micromatch; /***/ }), -/* 785 */ +/* 786 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(786); -const compile = __webpack_require__(788); -const expand = __webpack_require__(792); -const parse = __webpack_require__(793); +const stringify = __webpack_require__(787); +const compile = __webpack_require__(789); +const expand = __webpack_require__(793); +const parse = __webpack_require__(794); /** * Expand the given pattern or create a regex-compatible string. @@ -91348,13 +91530,13 @@ module.exports = braces; /***/ }), -/* 786 */ +/* 787 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const utils = __webpack_require__(787); +const utils = __webpack_require__(788); module.exports = (ast, options = {}) => { let stringify = (node, parent = {}) => { @@ -91387,7 +91569,7 @@ module.exports = (ast, options = {}) => { /***/ }), -/* 787 */ +/* 788 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91506,14 +91688,14 @@ exports.flatten = (...args) => { /***/ }), -/* 788 */ +/* 789 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(789); -const utils = __webpack_require__(787); +const fill = __webpack_require__(790); +const utils = __webpack_require__(788); const compile = (ast, options = {}) => { let walk = (node, parent = {}) => { @@ -91570,7 +91752,7 @@ module.exports = compile; /***/ }), -/* 789 */ +/* 790 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91584,7 +91766,7 @@ module.exports = compile; const util = __webpack_require__(115); -const toRegexRange = __webpack_require__(790); +const toRegexRange = __webpack_require__(791); const isObject = val => val !== null && typeof val === 'object' && !Array.isArray(val); @@ -91826,7 +92008,7 @@ module.exports = fill; /***/ }), -/* 790 */ +/* 791 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -91839,7 +92021,7 @@ module.exports = fill; -const isNumber = __webpack_require__(791); +const isNumber = __webpack_require__(792); const toRegexRange = (min, max, options) => { if (isNumber(min) === false) { @@ -92121,7 +92303,7 @@ module.exports = toRegexRange; /***/ }), -/* 791 */ +/* 792 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92146,15 +92328,15 @@ module.exports = function(num) { /***/ }), -/* 792 */ +/* 793 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const fill = __webpack_require__(789); -const stringify = __webpack_require__(786); -const utils = __webpack_require__(787); +const fill = __webpack_require__(790); +const stringify = __webpack_require__(787); +const utils = __webpack_require__(788); const append = (queue = '', stash = '', enclose = false) => { let result = []; @@ -92266,13 +92448,13 @@ module.exports = expand; /***/ }), -/* 793 */ +/* 794 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; -const stringify = __webpack_require__(786); +const stringify = __webpack_require__(787); /** * Constants @@ -92294,7 +92476,7 @@ const { CHAR_SINGLE_QUOTE, /* ' */ CHAR_NO_BREAK_SPACE, CHAR_ZERO_WIDTH_NOBREAK_SPACE -} = __webpack_require__(794); +} = __webpack_require__(795); /** * parse @@ -92606,7 +92788,7 @@ module.exports = parse; /***/ }), -/* 794 */ +/* 795 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92670,7 +92852,7 @@ module.exports = { /***/ }), -/* 795 */ +/* 796 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92694,7 +92876,7 @@ function propagateCloseEventToSources(streams) { /***/ }), -/* 796 */ +/* 797 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92712,14 +92894,14 @@ exports.isEmpty = isEmpty; /***/ }), -/* 797 */ +/* 798 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const stream_1 = __webpack_require__(798); -const provider_1 = __webpack_require__(800); +const stream_1 = __webpack_require__(799); +const provider_1 = __webpack_require__(801); class ProviderAsync extends provider_1.default { constructor() { super(...arguments); @@ -92747,7 +92929,7 @@ exports.default = ProviderAsync; /***/ }), -/* 798 */ +/* 799 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92756,7 +92938,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(134); const fsStat = __webpack_require__(246); const fsWalk = __webpack_require__(251); -const reader_1 = __webpack_require__(799); +const reader_1 = __webpack_require__(800); class ReaderStream extends reader_1.default { constructor() { super(...arguments); @@ -92809,7 +92991,7 @@ exports.default = ReaderStream; /***/ }), -/* 799 */ +/* 800 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -92817,7 +92999,7 @@ exports.default = ReaderStream; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); const fsStat = __webpack_require__(246); -const utils = __webpack_require__(778); +const utils = __webpack_require__(779); class Reader { constructor(_settings) { this._settings = _settings; @@ -92849,17 +93031,17 @@ exports.default = Reader; /***/ }), -/* 800 */ +/* 801 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const path = __webpack_require__(4); -const deep_1 = __webpack_require__(801); -const entry_1 = __webpack_require__(804); -const error_1 = __webpack_require__(805); -const entry_2 = __webpack_require__(806); +const deep_1 = __webpack_require__(802); +const entry_1 = __webpack_require__(805); +const error_1 = __webpack_require__(806); +const entry_2 = __webpack_require__(807); class Provider { constructor(_settings) { this._settings = _settings; @@ -92904,14 +93086,14 @@ exports.default = Provider; /***/ }), -/* 801 */ +/* 802 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(778); -const partial_1 = __webpack_require__(802); +const utils = __webpack_require__(779); +const partial_1 = __webpack_require__(803); class DeepFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -92973,13 +93155,13 @@ exports.default = DeepFilter; /***/ }), -/* 802 */ +/* 803 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const matcher_1 = __webpack_require__(803); +const matcher_1 = __webpack_require__(804); class PartialMatcher extends matcher_1.default { match(filepath) { const parts = filepath.split('/'); @@ -93018,13 +93200,13 @@ exports.default = PartialMatcher; /***/ }), -/* 803 */ +/* 804 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(778); +const utils = __webpack_require__(779); class Matcher { constructor(_patterns, _settings, _micromatchOptions) { this._patterns = _patterns; @@ -93075,13 +93257,13 @@ exports.default = Matcher; /***/ }), -/* 804 */ +/* 805 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(778); +const utils = __webpack_require__(779); class EntryFilter { constructor(_settings, _micromatchOptions) { this._settings = _settings; @@ -93138,13 +93320,13 @@ exports.default = EntryFilter; /***/ }), -/* 805 */ +/* 806 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(778); +const utils = __webpack_require__(779); class ErrorFilter { constructor(_settings) { this._settings = _settings; @@ -93160,13 +93342,13 @@ exports.default = ErrorFilter; /***/ }), -/* 806 */ +/* 807 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const utils = __webpack_require__(778); +const utils = __webpack_require__(779); class EntryTransformer { constructor(_settings) { this._settings = _settings; @@ -93193,15 +93375,15 @@ exports.default = EntryTransformer; /***/ }), -/* 807 */ +/* 808 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const stream_1 = __webpack_require__(134); -const stream_2 = __webpack_require__(798); -const provider_1 = __webpack_require__(800); +const stream_2 = __webpack_require__(799); +const provider_1 = __webpack_require__(801); class ProviderStream extends provider_1.default { constructor() { super(...arguments); @@ -93231,14 +93413,14 @@ exports.default = ProviderStream; /***/ }), -/* 808 */ +/* 809 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -const sync_1 = __webpack_require__(809); -const provider_1 = __webpack_require__(800); +const sync_1 = __webpack_require__(810); +const provider_1 = __webpack_require__(801); class ProviderSync extends provider_1.default { constructor() { super(...arguments); @@ -93261,7 +93443,7 @@ exports.default = ProviderSync; /***/ }), -/* 809 */ +/* 810 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93269,7 +93451,7 @@ exports.default = ProviderSync; Object.defineProperty(exports, "__esModule", { value: true }); const fsStat = __webpack_require__(246); const fsWalk = __webpack_require__(251); -const reader_1 = __webpack_require__(799); +const reader_1 = __webpack_require__(800); class ReaderSync extends reader_1.default { constructor() { super(...arguments); @@ -93311,7 +93493,7 @@ exports.default = ReaderSync; /***/ }), -/* 810 */ +/* 811 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93375,7 +93557,7 @@ exports.default = Settings; /***/ }), -/* 811 */ +/* 812 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93383,7 +93565,7 @@ exports.default = Settings; const {promisify} = __webpack_require__(115); const fs = __webpack_require__(142); const path = __webpack_require__(4); -const fastGlob = __webpack_require__(776); +const fastGlob = __webpack_require__(777); const gitIgnore = __webpack_require__(286); const slash = __webpack_require__(287); @@ -93502,7 +93684,7 @@ module.exports.sync = options => { /***/ }), -/* 812 */ +/* 813 */ /***/ (function(module, exports, __webpack_require__) { "use strict"; @@ -93555,7 +93737,7 @@ module.exports = { /***/ }), -/* 813 */ +/* 814 */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; diff --git a/packages/kbn-pm/src/commands/bootstrap.ts b/packages/kbn-pm/src/commands/bootstrap.ts index 48cce0365ca8..87276bc58410 100644 --- a/packages/kbn-pm/src/commands/bootstrap.ts +++ b/packages/kbn-pm/src/commands/bootstrap.ts @@ -102,9 +102,20 @@ export const BootstrapCommand: ICommand = { // NOTE: We don't probably need this anymore, is actually not being used await linkProjectExecutables(projects, projectGraph); + // Update vscode settings + await spawnStreaming( + process.execPath, + ['scripts/update_vscode_config'], + { + cwd: kbn.getAbsolute(), + env: process.env, + }, + { prefix: '[vscode]', debug: false } + ); + // Build typescript references await spawnStreaming( - 'node', + process.execPath, ['scripts/build_ts_refs', '--ignore-type-failures', '--info'], { cwd: kbn.getAbsolute(), diff --git a/packages/kbn-securitysolution-list-hooks/.babelrc b/packages/kbn-securitysolution-list-hooks/.babelrc new file mode 100644 index 000000000000..40a198521b90 --- /dev/null +++ b/packages/kbn-securitysolution-list-hooks/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@kbn/babel-preset/node_preset"], + "ignore": ["**/*.test.ts", "**/*.test.tsx"] +} diff --git a/packages/kbn-securitysolution-list-hooks/.babelrc.browser b/packages/kbn-securitysolution-list-hooks/.babelrc.browser new file mode 100644 index 000000000000..71bbfbcd6eb2 --- /dev/null +++ b/packages/kbn-securitysolution-list-hooks/.babelrc.browser @@ -0,0 +1,4 @@ +{ + "presets": ["@kbn/babel-preset/webpack_preset"], + "ignore": ["**/*.test.ts", "**/*.test.tsx"] +} diff --git a/packages/kbn-securitysolution-list-hooks/BUILD.bazel b/packages/kbn-securitysolution-list-hooks/BUILD.bazel index 87075604a75c..ba8c579bb97d 100644 --- a/packages/kbn-securitysolution-list-hooks/BUILD.bazel +++ b/packages/kbn-securitysolution-list-hooks/BUILD.bazel @@ -1,5 +1,6 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("//src/dev/bazel:index.bzl", "jsts_transpiler") PKG_BASE_NAME = "kbn-securitysolution-list-hooks" @@ -27,28 +28,42 @@ NPM_MODULE_EXTRA_FILES = [ "README.md", ] -SRC_DEPS = [ +RUNTIME_DEPS = [ "//packages/kbn-securitysolution-hook-utils", "//packages/kbn-securitysolution-io-ts-list-types", "//packages/kbn-securitysolution-list-api", "//packages/kbn-securitysolution-list-constants", "//packages/kbn-securitysolution-list-utils", "//packages/kbn-securitysolution-utils", - "@npm//lodash", - "@npm//tslib", + "@npm//@testing-library/react-hooks", "@npm//react", - "@npm//react-intl", ] TYPES_DEPS = [ + "//packages/kbn-securitysolution-hook-utils", + "//packages/kbn-securitysolution-io-ts-list-types", + "//packages/kbn-securitysolution-list-api", + "//packages/kbn-securitysolution-list-constants", + "//packages/kbn-securitysolution-list-utils", + "//packages/kbn-securitysolution-utils", "@npm//@types/jest", - "@npm//@types/lodash", "@npm//@types/node", "@npm//@types/react", - "@npm//@types/react-intl", + "@npm//@types/testing-library__react-hooks", ] -DEPS = SRC_DEPS + TYPES_DEPS +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + config_file = ".babelrc.browser" +) ts_config( name = "tsconfig", @@ -60,24 +75,25 @@ ts_config( ) ts_project( - name = "tsc", - srcs = SRCS, + name = "tsc_types", args = ["--pretty"], + srcs = SRCS, + deps = TYPES_DEPS, declaration = True, declaration_map = True, - out_dir = "target", + emit_declaration_only = True, + out_dir = "target_types", root_dir = "src", source_map = True, tsconfig = ":tsconfig", - deps = DEPS, ) js_library( name = PKG_BASE_NAME, - package_name = PKG_REQUIRE_NAME, srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], - deps = DEPS + [":tsc"], ) pkg_npm( diff --git a/packages/kbn-securitysolution-list-hooks/package.json b/packages/kbn-securitysolution-list-hooks/package.json index 2eb456da97b7..ffd4ad61850f 100644 --- a/packages/kbn-securitysolution-list-hooks/package.json +++ b/packages/kbn-securitysolution-list-hooks/package.json @@ -3,7 +3,8 @@ "version": "1.0.0", "description": "Security solution list ReactJS hooks", "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target/index.js", - "types": "./target/index.d.ts", + "browser": "./target_web/index.js", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", "private": true } diff --git a/packages/kbn-securitysolution-list-hooks/tsconfig.json b/packages/kbn-securitysolution-list-hooks/tsconfig.json index 9b09c02bd4aa..41ec03f2ebf3 100644 --- a/packages/kbn-securitysolution-list-hooks/tsconfig.json +++ b/packages/kbn-securitysolution-list-hooks/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "declaration": true, "declarationMap": true, - "outDir": "target", + "emitDeclarationOnly": true, + "outDir": "target_types", "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-securitysolution-list-hooks/src", diff --git a/packages/kbn-securitysolution-list-utils/.babelrc b/packages/kbn-securitysolution-list-utils/.babelrc new file mode 100644 index 000000000000..40a198521b90 --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["@kbn/babel-preset/node_preset"], + "ignore": ["**/*.test.ts", "**/*.test.tsx"] +} diff --git a/packages/kbn-securitysolution-list-utils/.babelrc.browser b/packages/kbn-securitysolution-list-utils/.babelrc.browser new file mode 100644 index 000000000000..71bbfbcd6eb2 --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/.babelrc.browser @@ -0,0 +1,4 @@ +{ + "presets": ["@kbn/babel-preset/webpack_preset"], + "ignore": ["**/*.test.ts", "**/*.test.tsx"] +} diff --git a/packages/kbn-securitysolution-list-utils/BUILD.bazel b/packages/kbn-securitysolution-list-utils/BUILD.bazel index b35b13004b1a..4701723286ef 100644 --- a/packages/kbn-securitysolution-list-utils/BUILD.bazel +++ b/packages/kbn-securitysolution-list-utils/BUILD.bazel @@ -1,5 +1,6 @@ load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") +load("//src/dev/bazel:index.bzl", "jsts_transpiler") PKG_BASE_NAME = "kbn-securitysolution-list-utils" @@ -27,23 +28,38 @@ NPM_MODULE_EXTRA_FILES = [ "README.md", ] -SRC_DEPS = [ +RUNTIME_DEPS = [ + "//packages/kbn-es-query", "//packages/kbn-i18n", - "//packages/kbn-securitysolution-list-constants", "//packages/kbn-securitysolution-io-ts-list-types", + "//packages/kbn-securitysolution-list-constants", "//packages/kbn-securitysolution-utils", - "//packages/kbn-es-query", "@npm//lodash", - "@npm//tslib", ] TYPES_DEPS = [ + "//packages/kbn-es-query", + "//packages/kbn-i18n", + "//packages/kbn-securitysolution-io-ts-list-types", + "//packages/kbn-securitysolution-list-constants", + "//packages/kbn-securitysolution-utils", "@npm//@types/jest", "@npm//@types/lodash", "@npm//@types/node", ] -DEPS = SRC_DEPS + TYPES_DEPS +jsts_transpiler( + name = "target_node", + srcs = SRCS, + build_pkg_name = package_name(), +) + +jsts_transpiler( + name = "target_web", + srcs = SRCS, + build_pkg_name = package_name(), + config_file = ".babelrc.browser" +) ts_config( name = "tsconfig", @@ -55,24 +71,26 @@ ts_config( ) ts_project( - name = "tsc", - srcs = SRCS, + name = "tsc_types", args = ["--pretty"], + srcs = SRCS, + deps = TYPES_DEPS, declaration = True, declaration_map = True, - out_dir = "target", + emit_declaration_only = True, + out_dir = "target_types", root_dir = "src", source_map = True, tsconfig = ":tsconfig", - deps = DEPS, ) js_library( name = PKG_BASE_NAME, - package_name = PKG_REQUIRE_NAME, srcs = NPM_MODULE_EXTRA_FILES, + deps = RUNTIME_DEPS + [":target_node", ":target_web", ":tsc_types"], + package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], - deps = DEPS + [":tsc"], + ) pkg_npm( diff --git a/packages/kbn-securitysolution-list-utils/package.json b/packages/kbn-securitysolution-list-utils/package.json index efd1401ab433..0a6671e83774 100644 --- a/packages/kbn-securitysolution-list-utils/package.json +++ b/packages/kbn-securitysolution-list-utils/package.json @@ -3,7 +3,8 @@ "version": "1.0.0", "description": "security solution list utilities", "license": "SSPL-1.0 OR Elastic License 2.0", - "main": "./target/index.js", - "types": "./target/index.d.ts", + "browser": "./target_web/index.js", + "main": "./target_node/index.js", + "types": "./target_types/index.d.ts", "private": true } diff --git a/packages/kbn-securitysolution-list-utils/tsconfig.json b/packages/kbn-securitysolution-list-utils/tsconfig.json index da48f4af29ea..fa50bd798121 100644 --- a/packages/kbn-securitysolution-list-utils/tsconfig.json +++ b/packages/kbn-securitysolution-list-utils/tsconfig.json @@ -3,7 +3,8 @@ "compilerOptions": { "declaration": true, "declarationMap": true, - "outDir": "target", + "emitDeclarationOnly": true, + "outDir": "target_types", "rootDir": "src", "sourceMap": true, "sourceRoot": "../../../../packages/kbn-securitysolution-list-utils/src", diff --git a/packages/kbn-storybook/src/webpack.config.ts b/packages/kbn-storybook/src/webpack.config.ts index 97fbf4046842..e3cfc149bbea 100644 --- a/packages/kbn-storybook/src/webpack.config.ts +++ b/packages/kbn-storybook/src/webpack.config.ts @@ -55,12 +55,13 @@ export default function ({ config: storybookConfig }: { config: Configuration }) { loader: 'sass-loader', options: { - prependData(loaderContext: any) { + additionalData(content: string, loaderContext: any) { return `@import ${stringifyRequest( loaderContext, resolve(REPO_ROOT, 'src/core/public/core_app/styles/_globals_v7light.scss') - )};\n`; + )};\n${content}`; }, + implementation: require('node-sass'), sassOptions: { includePaths: [resolve(REPO_ROOT, 'node_modules')], }, diff --git a/renovate.json5 b/renovate.json5 index faf9859f2120..b1464ad5040f 100644 --- a/renovate.json5 +++ b/renovate.json5 @@ -1,6 +1,7 @@ { extends: [ 'config:base', + ':disableDependencyDashboard', ], ignorePaths: [ '**/__fixtures__/**', @@ -12,12 +13,11 @@ baseBranches: [ 'master', '7.x', - '7.13', + '7.15', ], prConcurrentLimit: 0, prHourlyLimit: 0, separateMajorMinor: false, - masterIssue: true, rangeStrategy: 'bump', semanticCommits: false, vulnerabilityAlerts: { @@ -39,7 +39,7 @@ packageNames: ['@elastic/charts'], reviewers: ['markov00', 'nickofthyme'], matchBaseBranches: ['master'], - labels: ['release_note:skip', 'v8.0.0', 'v7.14.0', 'auto-backport'], + labels: ['release_note:skip', 'v8.0.0', 'v7.16.0', 'auto-backport'], enabled: true, }, { @@ -69,9 +69,9 @@ { groupName: 'vega related modules', packageNames: ['vega', 'vega-lite', 'vega-schema-url-parser', 'vega-tooltip'], - reviewers: ['team:kibana-app'], + reviewers: ['team:kibana-vis-editors'], matchBaseBranches: ['master'], - labels: ['Feature:Vega', 'Team:KibanaApp'], + labels: ['Feature:Vega', 'Team:VisEditors'], enabled: true, }, ], diff --git a/scripts/update_vscode_config.js b/scripts/update_vscode_config.js new file mode 100644 index 000000000000..10ed9fa200b7 --- /dev/null +++ b/scripts/update_vscode_config.js @@ -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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +require('../src/setup_node_env'); +require('@kbn/dev-utils').runUpdateVscodeConfigCli(); diff --git a/src/core/public/application/integration_tests/utils.tsx b/src/core/public/application/integration_tests/utils.tsx index dcf071719c11..455d19956f7e 100644 --- a/src/core/public/application/integration_tests/utils.tsx +++ b/src/core/public/application/integration_tests/utils.tsx @@ -21,13 +21,18 @@ export const createRenderer = (element: ReactElement | null): Renderer => { const dom: Dom = element && mount({element}); return () => - new Promise(async (resolve) => { - if (dom) { - await act(async () => { - dom.update(); - }); + new Promise(async (resolve, reject) => { + try { + if (dom) { + await act(async () => { + dom.update(); + }); + } + + setImmediate(() => resolve(dom)); // flushes any pending promises + } catch (error) { + reject(error); } - setImmediate(() => resolve(dom)); // flushes any pending promises }); }; diff --git a/src/core/public/application/ui/app_container.test.tsx b/src/core/public/application/ui/app_container.test.tsx index 86cb9198e069..4c056e748f06 100644 --- a/src/core/public/application/ui/app_container.test.tsx +++ b/src/core/public/application/ui/app_container.test.tsx @@ -27,8 +27,12 @@ describe('AppContainer', () => { }); const flushPromises = async () => { - await new Promise(async (resolve) => { - setImmediate(() => resolve()); + await new Promise(async (resolve, reject) => { + try { + setImmediate(() => resolve()); + } catch (error) { + reject(error); + } }); }; diff --git a/src/core/public/chrome/ui/header/header_action_menu.test.tsx b/src/core/public/chrome/ui/header/header_action_menu.test.tsx index 386e48e745e8..201be8848bac 100644 --- a/src/core/public/chrome/ui/header/header_action_menu.test.tsx +++ b/src/core/public/chrome/ui/header/header_action_menu.test.tsx @@ -26,13 +26,18 @@ describe('HeaderActionMenu', () => { }); const refresh = () => { - new Promise(async (resolve) => { - if (component) { - act(() => { - component.update(); - }); + new Promise(async (resolve, reject) => { + try { + if (component) { + act(() => { + component.update(); + }); + } + + setImmediate(() => resolve(component)); // flushes any pending promises + } catch (error) { + reject(error); } - setImmediate(() => resolve(component)); // flushes any pending promises }); }; diff --git a/src/core/public/deprecations/deprecations_client.test.ts b/src/core/public/deprecations/deprecations_client.test.ts index a998a03772cc..cca81f4687a9 100644 --- a/src/core/public/deprecations/deprecations_client.test.ts +++ b/src/core/public/deprecations/deprecations_client.test.ts @@ -82,6 +82,7 @@ describe('DeprecationsClient', () => { it('returns true if deprecation has correctiveActions.api', async () => { const deprecationsClient = new DeprecationsClient({ http }); const mockDeprecationDetails: DomainDeprecationDetails = { + title: 'some-title', domainId: 'testPluginId-1', message: 'some-message', level: 'warning', @@ -102,6 +103,7 @@ describe('DeprecationsClient', () => { it('returns false if deprecation is missing correctiveActions.api', async () => { const deprecationsClient = new DeprecationsClient({ http }); const mockDeprecationDetails: DomainDeprecationDetails = { + title: 'some-title', domainId: 'testPluginId-1', message: 'some-message', level: 'warning', @@ -120,6 +122,7 @@ describe('DeprecationsClient', () => { it('fails if deprecation is not resolvable', async () => { const deprecationsClient = new DeprecationsClient({ http }); const mockDeprecationDetails: DomainDeprecationDetails = { + title: 'some-title', domainId: 'testPluginId-1', message: 'some-message', level: 'warning', @@ -129,15 +132,18 @@ describe('DeprecationsClient', () => { }; const result = await deprecationsClient.resolveDeprecation(mockDeprecationDetails); - expect(result).toEqual({ - status: 'fail', - reason: 'deprecation has no correctiveAction via api.', - }); + expect(result).toMatchInlineSnapshot(` + Object { + "reason": "This deprecation cannot be resolved automatically.", + "status": "fail", + } + `); }); it('fetches the deprecation api', async () => { const deprecationsClient = new DeprecationsClient({ http }); const mockDeprecationDetails: DomainDeprecationDetails = { + title: 'some-title', domainId: 'testPluginId-1', message: 'some-message', level: 'warning', @@ -171,6 +177,7 @@ describe('DeprecationsClient', () => { const deprecationsClient = new DeprecationsClient({ http }); const mockResponse = 'Failed to fetch'; const mockDeprecationDetails: DomainDeprecationDetails = { + title: 'some-title', domainId: 'testPluginId-1', message: 'some-message', level: 'warning', diff --git a/src/core/public/deprecations/deprecations_client.ts b/src/core/public/deprecations/deprecations_client.ts index e510ab1e79d1..4b9cfca1986b 100644 --- a/src/core/public/deprecations/deprecations_client.ts +++ b/src/core/public/deprecations/deprecations_client.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { i18n } from '@kbn/i18n'; import type { HttpStart } from '../http'; import type { DomainDeprecationDetails, DeprecationsGetResponse } from '../../server/types'; @@ -52,7 +53,9 @@ export class DeprecationsClient { if (typeof correctiveActions.api !== 'object') { return { status: 'fail', - reason: 'deprecation has no correctiveAction via api.', + reason: i18n.translate('core.deprecations.noCorrectiveAction', { + defaultMessage: 'This deprecation cannot be resolved automatically.', + }), }; } diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 88075b66ad04..9ff95c0e04d1 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -204,6 +204,7 @@ export class DocLinksService { siem: { guide: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, gettingStarted: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/index.html`, + privileges: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/sec-requirements.html`, ml: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/machine-learning.html`, ruleChangeLog: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/prebuilt-rules-changelog.html`, detectionsReq: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/detections-permissions-section.html`, @@ -276,6 +277,7 @@ export class DocLinksService { alerting: { guide: `${KIBANA_DOCS}create-and-manage-rules.html`, actionTypes: `${KIBANA_DOCS}action-types.html`, + apmRules: `${KIBANA_DOCS}apm-alerts.html`, emailAction: `${KIBANA_DOCS}email-action-type.html`, emailActionConfig: `${KIBANA_DOCS}email-action-type.html`, generalSettings: `${KIBANA_DOCS}alert-action-settings-kb.html#general-alert-action-settings`, @@ -568,6 +570,7 @@ export interface DocLinksStart { readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { + readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 043759378faa..3a432ae50ea7 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -592,6 +592,7 @@ export interface DocLinksStart { readonly rollupJobs: string; readonly elasticsearch: Record; readonly siem: { + readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; diff --git a/src/core/server/config/deprecation/core_deprecations.test.ts b/src/core/server/config/deprecation/core_deprecations.test.ts index 06c7116c8beb..759e2375ce98 100644 --- a/src/core/server/config/deprecation/core_deprecations.test.ts +++ b/src/core/server/config/deprecation/core_deprecations.test.ts @@ -62,7 +62,7 @@ describe('core deprecations', () => { expect(migrated.server.xsrf.allowlist).toEqual(['/path']); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"server.xsrf.whitelist\\" is deprecated and has been replaced by \\"server.xsrf.allowlist\\"", + "Setting \\"server.xsrf.whitelist\\" has been replaced by \\"server.xsrf.allowlist\\"", ] `); }); diff --git a/src/core/server/config/integration_tests/config_deprecation.test.ts b/src/core/server/config/integration_tests/config_deprecation.test.ts index 2d86281ce40d..c941053a2f0a 100644 --- a/src/core/server/config/integration_tests/config_deprecation.test.ts +++ b/src/core/server/config/integration_tests/config_deprecation.test.ts @@ -51,8 +51,8 @@ describe('configuration deprecations', () => { const logs = loggingSystemMock.collect(mockLoggingSystem); expect(logs.warn.flat()).toMatchInlineSnapshot(` Array [ - "optimize.lazy is deprecated and is no longer used", - "optimize.lazyPort is deprecated and is no longer used", + "You no longer need to configure \\"optimize.lazy\\".", + "You no longer need to configure \\"optimize.lazyPort\\".", "\\"logging.silent\\" has been deprecated and will be removed in 8.0. Moving forward, you can use \\"logging.root.level:off\\" in your logging configuration. ", ] `); diff --git a/src/core/server/deprecations/README.mdx b/src/core/server/deprecations/README.mdx index 9a2ad689974e..197b25ac909c 100644 --- a/src/core/server/deprecations/README.mdx +++ b/src/core/server/deprecations/README.mdx @@ -58,17 +58,26 @@ The service will show the following deprecation details when the users set the a config `ui_metric.debug`. ```ts +import { i18n } from '@kbn/i18n'; + { deprecationsInfo:[{ + domainId: 'usageCollection', + title: i18n.translate('usageCollection.uiCounters.deprecations.uiMetricDebugTitle', { + defaultMessage: 'Setting "ui_metric.debug" is deprecated', + }); level: 'critical', - message: `"ui_metric.debug" is deprecated and has been replaced by "usageCollection.uiCounters.debug"`, + title: i18n.translate('usageCollection.uiCounters.deprecations.uiMetricDebugMessage', { + defaultMessage: '"ui_metric.debug" is deprecated and has been replaced by "usageCollection.uiCounters.debug"', + }); documentationUrl: 'elastic.co/some-url', correctiveActions:{ manualSteps: [ - `Replace "ui_metric.debug" with "usageCollection.uiCounters.debug" in the Kibana config file, CLI flag, or environment variable (in Docker only).`, + title: i18n.translate('usageCollection.uiCounters.deprecations.uiMetricDebug.manualStepOneMessage', { + defaultMessage: 'Replace "ui_metric.debug" with "usageCollection.uiCounters.debug" in the Kibana config file, CLI flag, or environment variable (in Docker only).', + }), ] }, - domainId: 'usageCollection', }], } ``` @@ -79,6 +88,8 @@ Custom config deprecation handling allows specifying the deprecation details via ##### Example ```ts +import { i18n } from '@kbn/i18n'; + export const config: PluginConfigDescriptor = { exposeToBrowser: { defaultAppId: true, @@ -97,12 +108,23 @@ export const config: PluginConfigDescriptor = { return completeConfig; } addDeprecation({ - message: `kibana.defaultAppId is deprecated and will be removed in 8.0. Please use the "defaultRoute" advanced setting instead`, + title: i18n.translate('kibana_legacy.deprecations.defaultAppIdTitle', { + defaultMessage: 'Setting "kibana.defaultAppId" is deprecated', + }), + message: i18n.translate('kibana_legacy.deprecations.defaultAppIdMessage', { + defaultMessage: 'Use the "defaultRoute" advanced setting instead of "kibana.defaultAppId".', + }), correctiveActions: { manualSteps: [ - 'Go to Stack Management > Advanced Settings', - 'Update the "defaultRoute" setting under the General section', - 'Remove "kibana.defaultAppId" from the kibana.yml config file', + i18n.translate('kibana_legacy.deprecations.defaultAppId.manualStepOneMessage', { + defaultMessage: 'Go to Stack Management > Advanced Settings.', + }), + i18n.translate('kibana_legacy.deprecations.defaultAppId.manualStepTwoMessage', { + defaultMessage: 'Update the "defaultRoute" setting in the General section.', + }), + i18n.translate('kibana_legacy.deprecations.defaultAppId.manualStepThreeMessage', { + defaultMessage: 'Remove "kibana.defaultAppId" from the kibana.yml config file.', + }), ], }, }); @@ -138,39 +160,49 @@ To check the full TS types of the service please check the [generated core docs] ### Example ```ts import { DeprecationsDetails, GetDeprecationsContext } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecationsContext): Promise { const deprecations: DeprecationsDetails[] = []; const testDashboardUser = await getTestDashboardUser(savedObjectsClient); if (testDashboardUser) { - deprecations.push({ - message: 'User "test_dashboard_user" is using a deprecated role: "kibana_user"', - documentationUrl: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-user.html', - level: 'critical', - correctiveActions: { - api: { - path: '/internal/security/users/test_dashboard_user', - method: 'POST', - body: { - username: 'test_dashboard_user', - roles: [ - 'machine_learning_user', - 'enrich_user', - 'kibana_admin' - ], - full_name: 'Alison Goryachev', - email: 'alisongoryachev@gmail.com', - metadata: {}, - enabled: true - } + deprecations.push({ + title: i18n.translate('security.deprecations.kibanaUserRoleTitle', { + defaultMessage: 'Deprecated roles are assigned to some users', + }), + message: i18n.translate('security.deprecations.kibanaUserRoleMessage', { + defaultMessage: 'User "test_dashboard_user" is using a deprecated role: "kibana_user".', + }), + documentationUrl: 'https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-user.html', + level: 'critical', + correctiveActions: { + api: { + path: '/internal/security/users/test_dashboard_user', + method: 'POST', + body: { + username: 'test_dashboard_user', + roles: [ + 'machine_learning_user', + 'enrich_user', + 'kibana_admin' + ], + full_name: 'Alison Goryachev', + email: 'alisongoryachev@gmail.com', + metadata: {}, + enabled: true + } + }, + manualSteps: [ + i18n.translate('security.deprecations.kibanaUserRole.manualStepOneMessage', { + defaultMessage: 'Switch all users with the "kibana_user" role to the kibana_admin role in Management > Security > Users.', + }), + i18n.translate('security.deprecations.kibanaUserRole.manualStepTwoMessage', { + defaultMessage: 'Update all mappings in Management > Security > Role Mappings to assign the "kibana_admin" role instead of the "kibana_user" role.' + }), + ], }, - manualSteps: [ - 'Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.', - 'Using Kibana role-mapping management, change all role-mappings which assing the kibana_user role to the kibana_admin role.' - ] - }, - }); + }); } return deprecations; @@ -204,7 +236,128 @@ Currently we do not have test objects to run functional tests against the Upgrad Yes. Using this service should help users find and resolve any issues specific to their deployment before upgrading. We recommend adding a `documentationUrl` for every deprecation you expose to further assist our users if they need extra help. +## Writing deprecation details + +State what is being deprecated and what action the user needs to take: + +> Abc is deprecated. Use Xyz to do the thing. + +Provide as much context as possible for what triggered the deprecation warning. +If action is not required (for example the default behavior is changing), describe the impact of doing nothing. + +Examples: +- > Setting `xpack.reporting.roles.enabled` is deprecated. Use feature controls to grant reporting privileges. +- > The Joda Time century-of era-formatter (C) is deprecated. Use a `java.time` formatter instead. +- > The default for the `cluster.routing.allocation.disk.watermark` setting is changing from false to true. + > If you do not explicitly configure this setting when you upgrade, indices in this one node cluster will + > become read-only if disk usage reaches 95%. + ## Note on i18n -We have decided to support i18n to the exposed deprecations for a better user experience when using the UA. -We will inject `i18n` into the deprecation function to enable teams to use it before fully documenting its usage. -For context follow [this issue](https://github.com/elastic/kibana/issues/99072). +All deprecation titles, messsages, and manual steps should be wrapped in `i18n.translate`. This +provides a better user experience using different locales. Follow the writing guidelines below for +best practices to writing the i18n messages and ids. + +### Writing guidelines +The deprecation service enables you to specify a `title`, `message`, `documentationUrl`, +and the `manual steps` for resolving a deprecation issue. + +#### Title: +No end punctuation is required. +i18n id: `{plugin_domain}.deprecations.{deprecationTitle}Title` + +Example: +```ts +title: i18n.translate('xpack.reporting.deprecations.reportingRoleTitle', { + defaultMessage: `Found deprecated reporting roles`, +}) +``` + +#### Message +Keep it brief, but multiple sentences are allowed if needed. +i18n id: `{plugin_domain}.deprecations.{deprecationTitle}Message` + +Example: +```ts +message: i18n.translate('xpack.reporting.deprecations.reportingRoleMessage', { + defaultMessage: `The deprecated "${deprecatedRole}" role has been found for ${numReportingUsers} user(s): "${usernames}"`, + values: { deprecatedRole, numReportingUsers, usernames }, +}), +``` + +#### Documentation URL +Don’t link to the Migration guide/breaking changes. +Only specify a doc URL if the user truly needs to “learn more” to understand what actions they need to take. + +Example: +```ts +documentationUrl: 'https://www.elastic.co/guide/en/kibana/current/secure-reporting.html', +``` +#### Manual steps +State the action first for each step. +i18n id: `{plugin_domain}.deprecations.{deprecationTitle}.manualStep{Step#}Message` + +Example: +```ts +manualSteps: [ + i18n.translate('xpack.reporting.deprecations.reportingRole.manualStepTwoMessage', { + defaultMessage: `Create one or more custom roles that provide Kibana application privileges to reporting features in **Management > Security > Roles**.`, + }), + i18n.translate('xpack.reporting.deprecations.reportingRole.manualStepThreeMessage', { + defaultMessage: `Assign the custom role(s) as desired, and remove the "${deprecatedRole}" role from the user(s).`, + values: { deprecatedRole }, + }), +] +``` + +#### General Guidelines + +##### What is deprecated +Use the present tense: +- Types are deprecated in geo_shape queries. +- Sorting is deprecated in reindex requests. + +Avoid: +- The type should no longer be specified in geo_shape queries. +- Sorting has been deprecated in reindex requests. + +##### What action the user needs to take +Use the imperative voice: +- Do not specify a type in the indexed_shape section. +- Use query filtering to reindex a subset of documents. + +Avoid: +- Please use query filtering instead. +- You should use query filtering instead. +- Instead consider using query filtering to find the desired subset of data. + +##### Context +Where possible, provide the specific context that resulted in the warning: +- The Abc timezone used by rollup job Def is deprecated. Use Xyz instead. + +##### Impact +Many deprecations are clearcut--you are using this old thing and need to switch to using this new thing. +Others are more nuanced and don’t necessarily require any changes. In this case, the warning needs to address +the impact of not taking action: +- The default for the `cluster.routing.allocation.disk.watermark` setting is changing from false to true. + If you do not explicitly configure this setting when you upgrade, indices in this one node cluster will + become read-only if disk usage reaches 95%. + +##### Version +You do not need to include any form of "and will be removed in a future release". +The assumption is that deprecated things are going to be removed, and the standard schedule for removal +is the next major version. + +If things are targeted for removal in a specific minor release, the message should include that information: +- Abc is deprecated. Use Xyz to do the thing. Support for Abc will be removed in n.n. + +If an item is deprecated, but won’t be removed in the next major version, the message should indicate that: +- Abc is deprecated. Use Xyz to do the thing. Support for Abc will be removed following the release of n.0. + +Avoid: +- Xyz is deprecated and will be removed in 8.0. +- Xyz is deprecated and will be unsupported in future. +- Xyz is deprecated and will not be supported in the next major version of Elasticsearch. + +##### Formatting +- Sentence style capitalization and punctuation. +- Avoid quotes for emphasis. diff --git a/src/core/server/deprecations/deprecations_factory.test.ts b/src/core/server/deprecations/deprecations_factory.test.ts index 187f3880f999..73beb84f57fa 100644 --- a/src/core/server/deprecations/deprecations_factory.test.ts +++ b/src/core/server/deprecations/deprecations_factory.test.ts @@ -124,16 +124,21 @@ describe('DeprecationsFactory', () => { `Failed to get deprecations info for plugin "${domainId}".`, mockError ); - expect(derpecations).toStrictEqual([ - { - domainId, - message: `Failed to get deprecations info for plugin "${domainId}".`, - level: 'fetch_error', - correctiveActions: { - manualSteps: ['Check Kibana server logs for error message.'], + expect(derpecations).toMatchInlineSnapshot(` + Array [ + Object { + "correctiveActions": Object { + "manualSteps": Array [ + "Check Kibana server logs for error message.", + ], + }, + "domainId": "mockPlugin", + "level": "fetch_error", + "message": "Unable to fetch deprecations info for plugin mockPlugin.", + "title": "Failed to fetch deprecations for mockPlugin", }, - }, - ]); + ] + `); }); it(`returns successful results even when some getDeprecations fail`, async () => { @@ -167,7 +172,8 @@ describe('DeprecationsFactory', () => { ...mockPluginDeprecationsInfo.map((info) => ({ ...info, domainId: 'mockPlugin' })), { domainId: 'anotherMockPlugin', - message: `Failed to get deprecations info for plugin "anotherMockPlugin".`, + title: 'Failed to fetch deprecations for anotherMockPlugin', + message: 'Unable to fetch deprecations info for plugin anotherMockPlugin.', level: 'fetch_error', correctiveActions: { manualSteps: ['Check Kibana server logs for error message.'], diff --git a/src/core/server/deprecations/deprecations_factory.ts b/src/core/server/deprecations/deprecations_factory.ts index 3699c088e20f..9905f0b26b4f 100644 --- a/src/core/server/deprecations/deprecations_factory.ts +++ b/src/core/server/deprecations/deprecations_factory.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { i18n } from '@kbn/i18n'; import { DeprecationsRegistry } from './deprecations_registry'; import type { Logger } from '../logging'; import type { @@ -89,10 +90,24 @@ export class DeprecationsFactory { ); return [ { - message: `Failed to get deprecations info for plugin "${domainId}".`, + title: i18n.translate('core.deprecations.deprecations.fetchFailedTitle', { + defaultMessage: `Failed to fetch deprecations for {domainId}`, + values: { domainId }, + }), + message: i18n.translate('core.deprecations.deprecations.fetchFailedMessage', { + defaultMessage: 'Unable to fetch deprecations info for plugin {domainId}.', + values: { domainId }, + }), level: 'fetch_error', correctiveActions: { - manualSteps: ['Check Kibana server logs for error message.'], + manualSteps: [ + i18n.translate( + 'core.deprecations.deprecations.fetchFailed.manualStepOneMessage', + { + defaultMessage: 'Check Kibana server logs for error message.', + } + ), + ], }, }, ]; diff --git a/src/core/server/deprecations/deprecations_service.test.ts b/src/core/server/deprecations/deprecations_service.test.ts index 75a0d6a63d91..0e8aaf3de49c 100644 --- a/src/core/server/deprecations/deprecations_service.test.ts +++ b/src/core/server/deprecations/deprecations_service.test.ts @@ -110,6 +110,7 @@ describe('DeprecationsService', () => { "level": "critical", "message": "testMessage", "requireRestart": true, + "title": "testDomain has a deprecated setting", }, ] `); diff --git a/src/core/server/deprecations/deprecations_service.ts b/src/core/server/deprecations/deprecations_service.ts index 7c4f74fe7d0e..c41567d88a2a 100644 --- a/src/core/server/deprecations/deprecations_service.ts +++ b/src/core/server/deprecations/deprecations_service.ts @@ -33,6 +33,7 @@ import { SavedObjectsClientContract } from '../saved_objects/types'; * @example * ```ts * import { DeprecationsDetails, GetDeprecationsContext, CoreSetup } from 'src/core/server'; + * import { i18n } from '@kbn/i18n'; * * async function getDeprecations({ esClient, savedObjectsClient }: GetDeprecationsContext): Promise { * const deprecations: DeprecationsDetails[] = []; @@ -41,52 +42,44 @@ import { SavedObjectsClientContract } from '../saved_objects/types'; * if (count > 0) { * // Example of a manual correctiveAction * deprecations.push({ - * message: `You have ${count} Timelion worksheets. The Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard.`, + * title: i18n.translate('xpack.timelion.deprecations.worksheetsTitle', { + * defaultMessage: 'Timelion worksheets are deprecated' + * }), + * message: i18n.translate('xpack.timelion.deprecations.worksheetsMessage', { + * defaultMessage: 'You have {count} Timelion worksheets. Migrate your Timelion worksheets to a dashboard to continue using them.', + * values: { count }, + * }), * documentationUrl: * 'https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html', * level: 'warning', * correctiveActions: { * manualSteps: [ - * 'Navigate to the Kibana Dashboard and click "Create dashboard".', - * 'Select Timelion from the "New Visualization" window.', - * 'Open a new tab, open the Timelion app, select the chart you want to copy, then copy the chart expression.', - * 'Go to Timelion, paste the chart expression in the Timelion expression field, then click Update.', - * 'In the toolbar, click Save.', - * 'On the Save visualization window, enter the visualization Title, then click Save and return.', + * i18n.translate('xpack.timelion.deprecations.worksheets.manualStepOneMessage', { + * defaultMessage: 'Navigate to the Kibana Dashboard and click "Create dashboard".', + * }), + * i18n.translate('xpack.timelion.deprecations.worksheets.manualStepTwoMessage', { + * defaultMessage: 'Select Timelion from the "New Visualization" window.', + * }), * ], + * api: { + * path: '/internal/security/users/test_dashboard_user', + * method: 'POST', + * body: { + * username: 'test_dashboard_user', + * roles: [ + * "machine_learning_user", + * "enrich_user", + * "kibana_admin" + * ], + * full_name: "Alison Goryachev", + * email: "alisongoryachev@gmail.com", + * metadata: {}, + * enabled: true + * } + * }, * }, * }); * } - * - * // Example of an api correctiveAction - * deprecations.push({ - * "message": "User 'test_dashboard_user' is using a deprecated role: 'kibana_user'", - * "documentationUrl": "https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-put-user.html", - * "level": "critical", - * "correctiveActions": { - * "api": { - * "path": "/internal/security/users/test_dashboard_user", - * "method": "POST", - * "body": { - * "username": "test_dashboard_user", - * "roles": [ - * "machine_learning_user", - * "enrich_user", - * "kibana_admin" - * ], - * "full_name": "Alison Goryachev", - * "email": "alisongoryachev@gmail.com", - * "metadata": {}, - * "enabled": true - * } - * }, - * "manualSteps": [ - * "Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.", - * "Using Kibana role-mapping management, change all role-mappings which assing the kibana_user role to the kibana_admin role." - * ] - * }, - * }); - * * return deprecations; * } * @@ -192,16 +185,19 @@ export class DeprecationsService const deprecationsRegistry = deprecationsFactory.getRegistry(domainId); deprecationsRegistry.registerDeprecations({ getDeprecations: () => { - return deprecationsContexts.map(({ message, correctiveActions, documentationUrl }) => { - return { - level: 'critical', - deprecationType: 'config', - message, - correctiveActions, - documentationUrl, - requireRestart: true, - }; - }); + return deprecationsContexts.map( + ({ title, message, correctiveActions, documentationUrl }) => { + return { + title: title || `${domainId} has a deprecated setting`, + level: 'critical', + deprecationType: 'config', + message, + correctiveActions, + documentationUrl, + requireRestart: true, + }; + } + ); }, }); } diff --git a/src/core/server/deprecations/types.ts b/src/core/server/deprecations/types.ts index 486fec5dfd8b..c924cacd02e2 100644 --- a/src/core/server/deprecations/types.ts +++ b/src/core/server/deprecations/types.ts @@ -16,7 +16,15 @@ export interface DomainDeprecationDetails extends DeprecationsDetails { } export interface DeprecationsDetails { - /* The message to be displayed for the deprecation. */ + /** + * The title of the deprecation. + * Check the README for writing deprecations in `src/core/server/deprecations/README.mdx` + */ + title: string; + /** + * The description message to be displayed for the deprecation. + * Check the README for writing deprecations in `src/core/server/deprecations/README.mdx` + */ message: string; /** * levels: @@ -60,6 +68,7 @@ export interface DeprecationsDetails { * Specify a list of manual steps users need to follow to * fix the deprecation before upgrade. Required even if an API * corrective action is set in case the API fails. + * Check the README for writing deprecations in `src/core/server/deprecations/README.mdx` */ manualSteps: string[]; }; diff --git a/src/core/server/elasticsearch/client/client_config.ts b/src/core/server/elasticsearch/client/client_config.ts index 27d6f877a557..a6b0891fc12d 100644 --- a/src/core/server/elasticsearch/client/client_config.ts +++ b/src/core/server/elasticsearch/client/client_config.ts @@ -56,6 +56,9 @@ export function parseClientOptions( ...DEFAULT_HEADERS, ...config.customHeaders, }, + // do not make assumption on user-supplied data content + // fixes https://github.com/elastic/kibana/issues/101944 + disablePrototypePoisoningProtection: true, }; if (config.pingTimeout != null) { diff --git a/src/core/server/elasticsearch/client/configure_client.test.ts b/src/core/server/elasticsearch/client/configure_client.test.ts index f954b121320f..4e2c9c22f42f 100644 --- a/src/core/server/elasticsearch/client/configure_client.test.ts +++ b/src/core/server/elasticsearch/client/configure_client.test.ts @@ -10,6 +10,7 @@ import { Buffer } from 'buffer'; import { Readable } from 'stream'; import { RequestEvent, errors } from '@elastic/elasticsearch'; +import type { Client } from '@elastic/elasticsearch'; import type { TransportRequestOptions, TransportRequestParams, @@ -18,7 +19,6 @@ import type { import { parseClientOptionsMock, ClientMock } from './configure_client.test.mocks'; import { loggingSystemMock } from '../../logging/logging_system.mock'; -import { EventEmitter } from 'events'; import type { ElasticsearchClientConfig } from './client_config'; import { configureClient } from './configure_client'; @@ -32,7 +32,10 @@ const createFakeConfig = ( }; const createFakeClient = () => { - const client = new EventEmitter(); + const actualEs = jest.requireActual('@elastic/elasticsearch'); + const client = new actualEs.Client({ + nodes: ['http://localhost'], // Enforcing `nodes` because it's mandatory + }); jest.spyOn(client, 'on'); return client; }; @@ -67,6 +70,14 @@ const createApiResponse = ({ }; }; +function getProductCheckValue(client: Client) { + const tSymbol = Object.getOwnPropertySymbols(client.transport || client).filter( + (symbol) => symbol.description === 'product check' + )[0]; + // @ts-expect-error `tSymbol` is missing in the index signature of Transport + return (client.transport || client)[tSymbol]; +} + describe('configureClient', () => { let logger: ReturnType; let config: ElasticsearchClientConfig; @@ -117,6 +128,24 @@ describe('configureClient', () => { expect(client.on).toHaveBeenCalledWith('response', expect.any(Function)); }); + describe('Product check', () => { + it('should not skip the product check for the unscoped client', () => { + const client = configureClient(config, { logger, type: 'test', scoped: false }); + expect(getProductCheckValue(client)).toBe(0); + }); + + it('should skip the product check for the scoped client', () => { + const client = configureClient(config, { logger, type: 'test', scoped: true }); + expect(getProductCheckValue(client)).toBe(2); + }); + + it('should skip the product check for the children of the scoped client', () => { + const client = configureClient(config, { logger, type: 'test', scoped: true }); + const asScoped = client.child({ headers: { 'x-custom-header': 'Custom value' } }); + expect(getProductCheckValue(asScoped)).toBe(2); + }); + }); + describe('Client logging', () => { function createResponseWithBody(body?: RequestBody) { return createApiResponse({ diff --git a/src/core/server/elasticsearch/client/configure_client.ts b/src/core/server/elasticsearch/client/configure_client.ts index 35825ef765db..efd22365d44f 100644 --- a/src/core/server/elasticsearch/client/configure_client.ts +++ b/src/core/server/elasticsearch/client/configure_client.ts @@ -49,6 +49,12 @@ export const configureClient = ( const client = new Client({ ...clientOptions, Transport: KibanaTransport }); addLogging(client, logger.get('query', type)); + // --------------------------------------------------------------------------------- // + // Hack to disable the "Product check" only in the scoped clients while we // + // come up with a better approach in https://github.com/elastic/kibana/issues/110675 // + if (scoped) skipProductCheck(client); + // --------------------------------------------------------------------------------- // + return client; }; @@ -131,3 +137,21 @@ const addLogging = (client: Client, logger: Logger) => { } }); }; + +/** + * Hack to skip the Product Check performed by the Elasticsearch-js client. + * We noticed that the scoped clients are always performing this check because + * of the way we initialize the clients. We'll discuss changing this in the issue + * https://github.com/elastic/kibana/issues/110675. In the meanwhile, let's skip + * it for the scoped clients. + * + * The hack is copied from the test/utils in the elasticsearch-js repo + * (https://github.com/elastic/elasticsearch-js/blob/master/test/utils/index.js#L45-L56) + */ +function skipProductCheck(client: Client) { + const tSymbol = Object.getOwnPropertySymbols(client.transport || client).filter( + (symbol) => symbol.description === 'product check' + )[0]; + // @ts-expect-error `tSymbol` is missing in the index signature of Transport + (client.transport || client)[tSymbol] = 2; +} diff --git a/src/core/server/logging/integration_tests/rolling_file_appender.test.ts b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts index b560748026ac..83533e29ad12 100644 --- a/src/core/server/logging/integration_tests/rolling_file_appender.test.ts +++ b/src/core/server/logging/integration_tests/rolling_file_appender.test.ts @@ -60,7 +60,8 @@ describe('RollingFileAppender', () => { const message = (index: number) => `some message of around 40 bytes number ${index}`; const expectedFileContent = (indices: number[]) => indices.map(message).join('\n') + '\n'; - describe('`size-limit` policy with `numeric` strategy', () => { + // FLAKY: https://github.com/elastic/kibana/issues/108633 + describe.skip('`size-limit` policy with `numeric` strategy', () => { it('rolls the log file in the correct order', async () => { root = createRoot({ type: 'rolling-file', diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts index 530203e65908..9471bbc1b87a 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.mock.ts @@ -11,6 +11,7 @@ import { buildActiveMappings } from '../core'; const { mergeTypes } = jest.requireActual('./kibana_migrator'); import { SavedObjectsType } from '../../types'; import { BehaviorSubject } from 'rxjs'; +import { ByteSizeValue } from '@kbn/config-schema'; const defaultSavedObjectTypes: SavedObjectsType[] = [ { @@ -37,6 +38,7 @@ const createMigrator = ( kibanaVersion: '8.0.0-testing', soMigrationsConfig: { batchSize: 100, + maxBatchSizeBytes: ByteSizeValue.parse('30kb'), scrollDuration: '15m', pollInterval: 1500, skip: false, diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index d0cc52f2dd9b..6e10349f4b57 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -15,6 +15,7 @@ import { loggingSystemMock } from '../../../logging/logging_system.mock'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { SavedObjectsType } from '../../types'; import { DocumentMigrator } from '../core/document_migrator'; +import { ByteSizeValue } from '@kbn/config-schema'; jest.mock('../core/document_migrator', () => { return { // Create a mock for spying on the constructor @@ -396,6 +397,7 @@ const mockOptions = ({ enableV2 }: { enableV2: boolean } = { enableV2: false }) } as KibanaMigratorOptions['kibanaConfig'], soMigrationsConfig: { batchSize: 20, + maxBatchSizeBytes: ByteSizeValue.parse('20mb'), pollInterval: 20000, scrollDuration: '10m', skip: false, diff --git a/src/core/server/saved_objects/migrationsv2/README.md b/src/core/server/saved_objects/migrationsv2/README.md index 5bdc54898784..5121e66052f4 100644 --- a/src/core/server/saved_objects/migrationsv2/README.md +++ b/src/core/server/saved_objects/migrationsv2/README.md @@ -316,7 +316,10 @@ completed this step: - temp index has a write block - temp index is not found ### New control state +1. If `currentBatch` is the last batch in `transformedDocBatches` → `REINDEX_SOURCE_TO_TEMP_READ` +2. If there are more batches left in `transformedDocBatches` + → `REINDEX_SOURCE_TO_TEMP_INDEX_BULK` ## REINDEX_SOURCE_TO_TEMP_CLOSE_PIT ### Next action diff --git a/src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.ts b/src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.ts index 4217ca599297..82f642b92805 100644 --- a/src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.ts +++ b/src/core/server/saved_objects/migrationsv2/actions/bulk_overwrite_transformed_documents.ts @@ -23,6 +23,27 @@ import type { IndexNotFound, } from './index'; +/** + * Given a document and index, creates a valid body for the Bulk API. + */ +export const createBulkOperationBody = (doc: SavedObjectsRawDoc, index: string) => { + return [ + { + index: { + _index: index, + _id: doc._id, + // overwrite existing documents + op_type: 'index', + // use optimistic concurrency control to ensure that outdated + // documents are only overwritten once with the latest version + if_seq_no: doc._seq_no, + if_primary_term: doc._primary_term, + }, + }, + doc._source, + ]; +}; + /** @internal */ export interface BulkOverwriteTransformedDocumentsParams { client: ElasticsearchClient; @@ -47,6 +68,10 @@ export const bulkOverwriteTransformedDocuments = ({ | RequestEntityTooLargeException, 'bulk_index_succeeded' > => () => { + const body = transformedDocs.flatMap((doc) => { + return createBulkOperationBody(doc, index); + }); + return client .bulk({ // Because we only add aliases in the MARK_VERSION_INDEX_READY step we @@ -60,23 +85,7 @@ export const bulkOverwriteTransformedDocuments = ({ wait_for_active_shards: WAIT_FOR_ALL_SHARDS_TO_BE_ACTIVE, refresh, filter_path: ['items.*.error'], - body: transformedDocs.flatMap((doc) => { - return [ - { - index: { - _index: index, - _id: doc._id, - // overwrite existing documents - op_type: 'index', - // use optimistic concurrency control to ensure that outdated - // documents are only overwritten once with the latest version - if_seq_no: doc._seq_no, - if_primary_term: doc._primary_term, - }, - }, - doc._source, - ]; - }), + body, }) .then((res) => { // Filter out version_conflict_engine_exception since these just mean diff --git a/src/core/server/saved_objects/migrationsv2/initial_state.test.ts b/src/core/server/saved_objects/migrationsv2/initial_state.test.ts index 4066efeb65de..26ba129cbeab 100644 --- a/src/core/server/saved_objects/migrationsv2/initial_state.test.ts +++ b/src/core/server/saved_objects/migrationsv2/initial_state.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { ByteSizeValue } from '@kbn/config-schema'; import * as Option from 'fp-ts/Option'; import { SavedObjectsMigrationConfigType } from '../saved_objects_config'; import { SavedObjectTypeRegistry } from '../saved_objects_type_registry'; @@ -21,6 +22,7 @@ describe('createInitialState', () => { const migrationsConfig = ({ retryAttempts: 15, batchSize: 1000, + maxBatchSizeBytes: ByteSizeValue.parse('100mb'), } as unknown) as SavedObjectsMigrationConfigType; it('creates the initial state for the model based on the passed in parameters', () => { expect( @@ -37,6 +39,7 @@ describe('createInitialState', () => { }) ).toEqual({ batchSize: 1000, + maxBatchSizeBytes: ByteSizeValue.parse('100mb').getValueInBytes(), controlState: 'INIT', currentAlias: '.kibana_task_manager', excludeFromUpgradeFilterHooks: {}, diff --git a/src/core/server/saved_objects/migrationsv2/initial_state.ts b/src/core/server/saved_objects/migrationsv2/initial_state.ts index dce37b384a4f..a61967be9242 100644 --- a/src/core/server/saved_objects/migrationsv2/initial_state.ts +++ b/src/core/server/saved_objects/migrationsv2/initial_state.ts @@ -82,6 +82,7 @@ export const createInitialState = ({ retryDelay: 0, retryAttempts: migrationsConfig.retryAttempts, batchSize: migrationsConfig.batchSize, + maxBatchSizeBytes: migrationsConfig.maxBatchSizeBytes.getValueInBytes(), logs: [], unusedTypesQuery: excludeUnusedTypesQuery, knownTypes, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/7.7.2_xpack_100k.test.ts similarity index 94% rename from src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts rename to src/core/server/saved_objects/migrationsv2/integration_tests/7.7.2_xpack_100k.test.ts index ed21349a700f..41d89e2a0154 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7.7.2_xpack_100k.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/7.7.2_xpack_100k.test.ts @@ -17,7 +17,7 @@ import { InternalCoreStart } from '../../../internal_types'; import { Root } from '../../../root'; const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; -const logFilePath = path.join(__dirname, 'migration_test_kibana.log'); +const logFilePath = path.join(__dirname, '7.7.2_xpack_100k.log'); async function removeLogFile() { // ignore errors if it doesn't exist @@ -61,9 +61,12 @@ describe('migration from 7.7.2-xpack with 100k objects', () => { }, }, }, - root: { - appenders: ['default', 'file'], - }, + loggers: [ + { + name: 'root', + appenders: ['file'], + }, + ], }, }, { diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_failed_action_tasks.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_failed_action_tasks.test.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_failed_action_tasks.test.ts rename to src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_failed_action_tasks.test.ts index 0788a7ecdf0b..d70e03470315 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_failed_action_tasks.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_failed_action_tasks.test.ts @@ -12,7 +12,7 @@ import * as kbnTestServer from '../../../../test_helpers/kbn_server'; import { Root } from '../../../root'; import { ElasticsearchClient } from '../../../elasticsearch'; -const logFilePath = Path.join(__dirname, '7_13_failed_action_tasks_test.log'); +const logFilePath = Path.join(__dirname, '7_13_failed_action_tasks.log'); async function removeLogFile() { // ignore errors if it doesn't exist diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_transform_failures.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_transform_failures.test.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_transform_failures.test.ts rename to src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_transform_failures.test.ts index 3258732c6fdd..fb40bda81cba 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_transform_failures.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_transform_failures.test.ts @@ -12,7 +12,7 @@ import Util from 'util'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; import { Root } from '../../../root'; -const logFilePath = Path.join(__dirname, '7_13_corrupt_transform_failures_test.log'); +const logFilePath = Path.join(__dirname, '7_13_corrupt_transform_failures.log'); const asyncUnlink = Util.promisify(Fs.unlink); diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_unknown_types.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_unknown_types.test.ts similarity index 86% rename from src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_unknown_types.test.ts rename to src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_unknown_types.test.ts index aded389bbb59..0be8b1187af7 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration_7_13_0_unknown_types.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/7_13_0_unknown_types.test.ts @@ -16,10 +16,12 @@ import { ElasticsearchClient } from '../../../elasticsearch'; import { Env } from '@kbn/config'; import { REPO_ROOT } from '@kbn/utils'; import { getEnvOptions } from '../../../config/mocks'; +import { retryAsync } from '../test_helpers/retry_async'; +import { LogRecord } from '@kbn/logging'; const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; const targetIndex = `.kibana_${kibanaVersion}_001`; -const logFilePath = Path.join(__dirname, '7_13_unknown_types_test.log'); +const logFilePath = Path.join(__dirname, '7_13_unknown_types.log'); async function removeLogFile() { // ignore errors if it doesn't exist @@ -68,23 +70,30 @@ describe('migration v2', () => { await root.setup(); await root.start(); - const logFileContent = await fs.readFile(logFilePath, 'utf-8'); - const records = logFileContent - .split('\n') - .filter(Boolean) - .map((str) => JSON5.parse(str)); + let unknownDocsWarningLog: LogRecord; - const unknownDocsWarningLog = records.find((rec) => - rec.message.startsWith(`[.kibana] CHECK_UNKNOWN_DOCUMENTS`) - ); + await retryAsync( + async () => { + const logFileContent = await fs.readFile(logFilePath, 'utf-8'); + const records = logFileContent + .split('\n') + .filter(Boolean) + .map((str) => JSON5.parse(str)); + + unknownDocsWarningLog = records.find((rec) => + rec.message.startsWith(`[.kibana] CHECK_UNKNOWN_DOCUMENTS`) + ); - expect( - unknownDocsWarningLog.message.startsWith( - '[.kibana] CHECK_UNKNOWN_DOCUMENTS Upgrades will fail for 8.0+ because documents were found for unknown saved ' + - 'object types. To ensure that upgrades will succeed in the future, either re-enable plugins or delete ' + - `these documents from the "${targetIndex}" index after the current upgrade completes.` - ) - ).toBeTruthy(); + expect( + unknownDocsWarningLog.message.startsWith( + '[.kibana] CHECK_UNKNOWN_DOCUMENTS Upgrades will fail for 8.0+ because documents were found for unknown saved ' + + 'object types. To ensure that upgrades will succeed in the future, either re-enable plugins or delete ' + + `these documents from the "${targetIndex}" index after the current upgrade completes.` + ) + ).toBeTruthy(); + }, + { retryAttempts: 10, retryDelayMs: 200 } + ); const unknownDocs = [ { type: 'space', id: 'space:default' }, diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.14.0_xpack_sample_saved_objects.zip b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.14.0_xpack_sample_saved_objects.zip new file mode 100644 index 000000000000..70d68587e360 Binary files /dev/null and b/src/core/server/saved_objects/migrationsv2/integration_tests/archives/7.14.0_xpack_sample_saved_objects.zip differ diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes.test.ts new file mode 100644 index 000000000000..e96aeb6a93b6 --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes.test.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import fs from 'fs/promises'; +import JSON5 from 'json5'; +import * as kbnTestServer from '../../../../test_helpers/kbn_server'; +import { Root } from '../../../root'; +import { ElasticsearchClient } from '../../../elasticsearch'; +import { Env } from '@kbn/config'; +import { REPO_ROOT } from '@kbn/utils'; +import { getEnvOptions } from '../../../config/mocks'; +import { LogRecord } from '@kbn/logging'; +import { retryAsync } from '../test_helpers/retry_async'; + +const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; +const targetIndex = `.kibana_${kibanaVersion}_001`; +const logFilePath = Path.join(__dirname, 'batch_size_bytes.log'); + +async function removeLogFile() { + // ignore errors if it doesn't exist + await fs.unlink(logFilePath).catch(() => void 0); +} + +describe('migration v2', () => { + let esServer: kbnTestServer.TestElasticsearchUtils; + let root: Root; + let startES: () => Promise; + + beforeAll(async () => { + await removeLogFile(); + }); + + beforeEach(() => { + ({ startES } = kbnTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + es: { + license: 'basic', + dataArchive: Path.join(__dirname, 'archives', '7.14.0_xpack_sample_saved_objects.zip'), + esArgs: ['http.max_content_length=1715275b'], + }, + }, + })); + }); + + afterEach(async () => { + if (root) { + await root.shutdown(); + } + if (esServer) { + await esServer.stop(); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + }); + + it('completes the migration even when a full batch would exceed ES http.max_content_length', async () => { + root = createRoot({ maxBatchSizeBytes: 1715275 }); + esServer = await startES(); + await root.preboot(); + await root.setup(); + await expect(root.start()).resolves.toBeTruthy(); + + await new Promise((resolve) => setTimeout(resolve, 1000)); + + const esClient: ElasticsearchClient = esServer.es.getClient(); + const migratedIndexResponse = await esClient.count({ + index: targetIndex, + }); + const oldIndexResponse = await esClient.count({ + index: '.kibana_7.14.0_001', + }); + + // Use a >= comparison since once Kibana has started it might create new + // documents like telemetry tasks + expect(migratedIndexResponse.body.count).toBeGreaterThanOrEqual(oldIndexResponse.body.count); + }); + + it('fails with a descriptive message when a single document exceeds maxBatchSizeBytes', async () => { + root = createRoot({ maxBatchSizeBytes: 1015275 }); + esServer = await startES(); + await root.preboot(); + await root.setup(); + await expect(root.start()).rejects.toMatchInlineSnapshot( + `[Error: Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715275 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.]` + ); + + await retryAsync( + async () => { + const logFileContent = await fs.readFile(logFilePath, 'utf-8'); + const records = logFileContent + .split('\n') + .filter(Boolean) + .map((str) => JSON5.parse(str)) as LogRecord[]; + expect( + records.find((rec) => + rec.message.startsWith( + `Unable to complete saved object migrations for the [.kibana] index: The document with _id "canvas-workpad-template:workpad-template-061d7868-2b4e-4dc8-8bf7-3772b52926e5" is 1715275 bytes which exceeds the configured maximum batch size of 1015275 bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.` + ) + ) + ).toBeDefined(); + }, + { retryAttempts: 10, retryDelayMs: 200 } + ); + }); +}); + +function createRoot(options: { maxBatchSizeBytes?: number }) { + return kbnTestServer.createRootWithCorePlugins( + { + migrations: { + skip: false, + enableV2: true, + batchSize: 1000, + maxBatchSizeBytes: options.maxBatchSizeBytes, + }, + logging: { + appenders: { + file: { + type: 'file', + fileName: logFilePath, + layout: { + type: 'json', + }, + }, + }, + loggers: [ + { + name: 'root', + appenders: ['file'], + }, + ], + }, + }, + { + oss: true, + } + ); +} diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts new file mode 100644 index 000000000000..192321227d4a --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/batch_size_bytes_exceeds_es_content_length.test.ts @@ -0,0 +1,117 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; +import fs from 'fs/promises'; +import JSON5 from 'json5'; +import * as kbnTestServer from '../../../../test_helpers/kbn_server'; +import { Root } from '../../../root'; +import { retryAsync } from '../test_helpers/retry_async'; + +const logFilePath = Path.join(__dirname, 'batch_size_bytes_exceeds_es_content_length.log'); + +async function removeLogFile() { + // ignore errors if it doesn't exist + await fs.unlink(logFilePath).catch(() => void 0); +} + +describe('migration v2', () => { + let esServer: kbnTestServer.TestElasticsearchUtils; + let root: Root; + let startES: () => Promise; + + beforeAll(async () => { + await removeLogFile(); + }); + + beforeEach(() => { + ({ startES } = kbnTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + settings: { + es: { + license: 'basic', + dataArchive: Path.join(__dirname, 'archives', '7.14.0_xpack_sample_saved_objects.zip'), + esArgs: ['http.max_content_length=1mb'], + }, + }, + })); + }); + + afterEach(async () => { + if (root) { + await root.shutdown(); + } + if (esServer) { + await esServer.stop(); + } + + await new Promise((resolve) => setTimeout(resolve, 10000)); + }); + + it('fails with a descriptive message when maxBatchSizeBytes exceeds ES http.max_content_length', async () => { + root = createRoot({ maxBatchSizeBytes: 1715275 }); + esServer = await startES(); + await root.preboot(); + await root.setup(); + await expect(root.start()).rejects.toMatchInlineSnapshot( + `[Error: Unable to complete saved object migrations for the [.kibana] index: While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Ensure that the Kibana configuration option 'migrations.maxBatchSizeBytes' is set to a value that is lower than or equal to the Elasticsearch 'http.max_content_length' configuration option.]` + ); + + await retryAsync( + async () => { + const logFileContent = await fs.readFile(logFilePath, 'utf-8'); + const records = logFileContent + .split('\n') + .filter(Boolean) + .map((str) => JSON5.parse(str)) as any[]; + + expect( + records.find((rec) => + rec.message.startsWith( + `Unable to complete saved object migrations for the [.kibana] index: While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Ensure that the Kibana configuration option 'migrations.maxBatchSizeBytes' is set to a value that is lower than or equal to the Elasticsearch 'http.max_content_length' configuration option.` + ) + ) + ).toBeDefined(); + }, + { retryAttempts: 10, retryDelayMs: 200 } + ); + }); +}); + +function createRoot(options: { maxBatchSizeBytes?: number }) { + return kbnTestServer.createRootWithCorePlugins( + { + migrations: { + skip: false, + enableV2: true, + batchSize: 1000, + maxBatchSizeBytes: options.maxBatchSizeBytes, + }, + logging: { + appenders: { + file: { + type: 'file', + fileName: logFilePath, + layout: { + type: 'json', + }, + }, + }, + loggers: [ + { + name: 'root', + appenders: ['file'], + }, + ], + }, + }, + { + oss: true, + } + ); +} diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts index 684b75056bf4..bb408d14df6d 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/cleanup.test.ts @@ -13,7 +13,7 @@ import JSON5 from 'json5'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; import type { Root } from '../../../root'; -const logFilePath = Path.join(__dirname, 'cleanup_test.log'); +const logFilePath = Path.join(__dirname, 'cleanup.log'); const asyncUnlink = Util.promisify(Fs.unlink); const asyncReadFile = Util.promisify(Fs.readFile); diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/type_migration_failure.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/collects_corrupt_docs.test.ts similarity index 98% rename from src/core/server/saved_objects/migrationsv2/integration_tests/type_migration_failure.test.ts rename to src/core/server/saved_objects/migrationsv2/integration_tests/collects_corrupt_docs.test.ts index b3721d603d7d..02b7d0eae2a9 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/type_migration_failure.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/collects_corrupt_docs.test.ts @@ -12,7 +12,7 @@ import Util from 'util'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; import { Root } from '../../../root'; -const logFilePath = Path.join(__dirname, 'migration_test_corrupt_docs_kibana.log'); +const logFilePath = Path.join(__dirname, 'collects_corrupt_docs.log'); const asyncUnlink = Util.promisify(Fs.unlink); diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts index de58dded6942..446542cc3730 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/corrupt_outdated_docs.test.ts @@ -12,7 +12,7 @@ import Util from 'util'; import * as kbnTestServer from '../../../../test_helpers/kbn_server'; import { Root } from '../../../root'; -const logFilePath = Path.join(__dirname, 'migration_test_corrupt_docs_kibana.log'); +const logFilePath = Path.join(__dirname, 'corrupt_outdated_docs.log'); const asyncUnlink = Util.promisify(Fs.unlink); diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_v1.test.ts similarity index 99% rename from src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts rename to src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_v1.test.ts index 2a1d6bff0c24..fc01e6a40849 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/migration.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/migration_from_v1.test.ts @@ -21,7 +21,7 @@ import { Root } from '../../../root'; const kibanaVersion = Env.createDefault(REPO_ROOT, getEnvOptions()).packageInfo.version; -const logFilePath = Path.join(__dirname, 'migration_test_kibana_from_v1.log'); +const logFilePath = Path.join(__dirname, 'migration_from_v1.log'); const asyncUnlink = Util.promisify(Fs.unlink); async function removeLogFile() { diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/outdated_docs.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/outdated_docs.test.ts index 822a44fb22dc..58ff34913f5d 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/outdated_docs.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/outdated_docs.test.ts @@ -14,7 +14,7 @@ import * as kbnTestServer from '../../../../test_helpers/kbn_server'; import type { ElasticsearchClient } from '../../../elasticsearch'; import { Root } from '../../../root'; -const logFilePath = Path.join(__dirname, 'migration_test_kibana.log'); +const logFilePath = Path.join(__dirname, 'outdated_docs.log'); const asyncUnlink = Util.promisify(Fs.unlink); async function removeLogFile() { diff --git a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts index 0bdf7a0d9876..4564a89ee081 100644 --- a/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts +++ b/src/core/server/saved_objects/migrationsv2/integration_tests/rewriting_id.test.ts @@ -15,7 +15,7 @@ import type { ElasticsearchClient } from '../../../elasticsearch'; import { Root } from '../../../root'; import { deterministicallyRegenerateObjectId } from '../../migrations/core/document_migrator'; -const logFilePath = Path.join(__dirname, 'migration_test_kibana.log'); +const logFilePath = Path.join(__dirname, 'rewriting_id.log'); const asyncUnlink = Util.promisify(Fs.unlink); async function removeLogFile() { diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts index 773a0af469bd..a312ac6be0c3 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.test.ts @@ -17,6 +17,7 @@ import { elasticsearchClientMock } from '../../elasticsearch/client/mocks'; import { LoggerAdapter } from '../../logging/logger_adapter'; import { AllControlStates, State } from './types'; import { createInitialState } from './initial_state'; +import { ByteSizeValue } from '@kbn/config-schema'; const esClient = elasticsearchServiceMock.createElasticsearchClient(); @@ -40,6 +41,7 @@ describe('migrationsStateActionMachine', () => { indexPrefix: '.my-so-index', migrationsConfig: { batchSize: 1000, + maxBatchSizeBytes: new ByteSizeValue(1e8), pollInterval: 0, scrollDuration: '0s', skip: false, @@ -235,6 +237,7 @@ describe('migrationsStateActionMachine', () => { ...initialState, reason: 'the fatal reason', outdatedDocuments: [{ _id: '1234', password: 'sensitive password' }], + transformedDocBatches: [[{ _id: '1234', password: 'sensitive transformed password' }]], } as State, logger: mockLogger.get(), model: transitionModel(['LEGACY_DELETE', 'FATAL']), @@ -257,6 +260,7 @@ describe('migrationsStateActionMachine', () => { kibana: { migrationState: { batchSize: 1000, + maxBatchSizeBytes: 1e8, controlState: 'LEGACY_DELETE', currentAlias: '.my-so-index', excludeFromUpgradeFilterHooks: {}, @@ -270,7 +274,7 @@ describe('migrationsStateActionMachine', () => { message: 'Log from LEGACY_DELETE control state', }, ], - outdatedDocuments: ['1234'], + outdatedDocuments: [{ _id: '1234' }], outdatedDocumentsQuery: expect.any(Object), preMigrationScript: { _tag: 'None', @@ -284,6 +288,7 @@ describe('migrationsStateActionMachine', () => { }, tempIndex: '.my-so-index_7.11.0_reindex_temp', tempIndexMappings: expect.any(Object), + transformedDocBatches: [[{ _id: '1234' }]], unusedTypesQuery: expect.any(Object), versionAlias: '.my-so-index_7.11.0', versionIndex: '.my-so-index_7.11.0_001', @@ -304,6 +309,7 @@ describe('migrationsStateActionMachine', () => { kibana: { migrationState: { batchSize: 1000, + maxBatchSizeBytes: 1e8, controlState: 'FATAL', currentAlias: '.my-so-index', excludeFromUpgradeFilterHooks: {}, @@ -321,7 +327,7 @@ describe('migrationsStateActionMachine', () => { message: 'Log from FATAL control state', }, ], - outdatedDocuments: ['1234'], + outdatedDocuments: [{ _id: '1234' }], outdatedDocumentsQuery: expect.any(Object), preMigrationScript: { _tag: 'None', @@ -335,6 +341,7 @@ describe('migrationsStateActionMachine', () => { }, tempIndex: '.my-so-index_7.11.0_reindex_temp', tempIndexMappings: expect.any(Object), + transformedDocBatches: [[{ _id: '1234' }]], unusedTypesQuery: expect.any(Object), versionAlias: '.my-so-index_7.11.0', versionIndex: '.my-so-index_7.11.0_001', @@ -447,6 +454,7 @@ describe('migrationsStateActionMachine', () => { kibana: { migrationState: { batchSize: 1000, + maxBatchSizeBytes: 1e8, controlState: 'LEGACY_REINDEX', currentAlias: '.my-so-index', excludeFromUpgradeFilterHooks: {}, @@ -474,6 +482,7 @@ describe('migrationsStateActionMachine', () => { }, tempIndex: '.my-so-index_7.11.0_reindex_temp', tempIndexMappings: expect.any(Object), + transformedDocBatches: [], unusedTypesQuery: expect.any(Object), versionAlias: '.my-so-index_7.11.0', versionIndex: '.my-so-index_7.11.0_001', @@ -488,6 +497,7 @@ describe('migrationsStateActionMachine', () => { kibana: { migrationState: { batchSize: 1000, + maxBatchSizeBytes: 1e8, controlState: 'LEGACY_DELETE', currentAlias: '.my-so-index', excludeFromUpgradeFilterHooks: {}, @@ -519,6 +529,7 @@ describe('migrationsStateActionMachine', () => { }, tempIndex: '.my-so-index_7.11.0_reindex_temp', tempIndexMappings: expect.any(Object), + transformedDocBatches: [], unusedTypesQuery: expect.any(Object), versionAlias: '.my-so-index_7.11.0', versionIndex: '.my-so-index_7.11.0_001', diff --git a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts index 8e3b8ee4ab55..58c299b77fc6 100644 --- a/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts +++ b/src/core/server/saved_objects/migrationsv2/migrations_state_action_machine.ts @@ -13,7 +13,8 @@ import type { ElasticsearchClient } from '../../elasticsearch'; import { getErrorMessage, getRequestDebugMeta } from '../../elasticsearch'; import { Model, Next, stateActionMachine } from './state_action_machine'; import { cleanup } from './migrations_state_machine_cleanup'; -import { State } from './types'; +import { ReindexSourceToTempIndex, ReindexSourceToTempIndexBulk, State } from './types'; +import { SavedObjectsRawDoc } from '../serialization'; interface StateLogMeta extends LogMeta { kibana: { @@ -140,11 +141,22 @@ export async function migrationStateActionMachine({ const newState = model(state, res); // Redact the state to reduce the memory consumption and so that we // don't log sensitive information inside documents by only keeping - // the _id's of outdatedDocuments + // the _id's of documents const redactedNewState = { ...newState, - // @ts-expect-error outdatedDocuments don't exist in all states - ...{ outdatedDocuments: (newState.outdatedDocuments ?? []).map((doc) => doc._id) }, + ...{ + outdatedDocuments: ((newState as ReindexSourceToTempIndex).outdatedDocuments ?? []).map( + (doc) => + ({ + _id: doc._id, + } as SavedObjectsRawDoc) + ), + }, + ...{ + transformedDocBatches: ( + (newState as ReindexSourceToTempIndexBulk).transformedDocBatches ?? [] + ).map((batches) => batches.map((doc) => ({ _id: doc._id }))) as [SavedObjectsRawDoc[]], + }, }; executionLog.push({ type: 'transition', diff --git a/src/core/server/saved_objects/migrationsv2/model/create_batches.test.ts b/src/core/server/saved_objects/migrationsv2/model/create_batches.test.ts new file mode 100644 index 000000000000..552c4c237675 --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/model/create_batches.test.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import * as Either from 'fp-ts/lib/Either'; +import { SavedObjectsRawDoc } from '../../serialization'; +import { createBatches } from './create_batches'; + +describe('createBatches', () => { + const DOCUMENT_SIZE_BYTES = 128; + const INDEX = '.kibana_version_index'; + it('returns right one batch if all documents fit in maxBatchSizeBytes', () => { + const documents = [ + { _id: '', _source: { type: 'dashboard', title: 'my saved object title ¹' } }, + { _id: '', _source: { type: 'dashboard', title: 'my saved object title ²' } }, + { _id: '', _source: { type: 'dashboard', title: 'my saved object title ®' } }, + ]; + + expect(createBatches(documents, INDEX, DOCUMENT_SIZE_BYTES * 3)).toEqual( + Either.right([documents]) + ); + }); + it('creates multiple batches with each batch limited to maxBatchSizeBytes', () => { + const documents = [ + { _id: '', _source: { type: 'dashboard', title: 'my saved object title ¹' } }, + { _id: '', _source: { type: 'dashboard', title: 'my saved object title ²' } }, + { _id: '', _source: { type: 'dashboard', title: 'my saved object title ®' } }, + { _id: '', _source: { type: 'dashboard', title: 'my saved object title 44' } }, + { _id: '', _source: { type: 'dashboard', title: 'my saved object title 55' } }, + ]; + expect(createBatches(documents, INDEX, DOCUMENT_SIZE_BYTES * 2)).toEqual( + Either.right([[documents[0], documents[1]], [documents[2], documents[3]], [documents[4]]]) + ); + }); + it('creates a single empty batch if there are no documents', () => { + const documents = [] as SavedObjectsRawDoc[]; + expect(createBatches(documents, INDEX, 100)).toEqual(Either.right([[]])); + }); + it('throws if any one document exceeds the maxBatchSizeBytes', () => { + const documents = [ + { _id: '', _source: { type: 'dashboard', title: 'my saved object title ¹' } }, + { + _id: '', + _source: { + type: 'dashboard', + title: 'my saved object title ² with a very long title that exceeds max size bytes', + }, + }, + { _id: '', _source: { type: 'dashboard', title: 'my saved object title ®' } }, + ]; + expect(createBatches(documents, INDEX, 178)).toEqual( + Either.left({ + maxBatchSizeBytes: 178, + docSizeBytes: 179, + type: 'document_exceeds_batch_size_bytes', + document: documents[1], + }) + ); + }); +}); diff --git a/src/core/server/saved_objects/migrationsv2/model/create_batches.ts b/src/core/server/saved_objects/migrationsv2/model/create_batches.ts new file mode 100644 index 000000000000..c80003fef09f --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/model/create_batches.ts @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import * as Either from 'fp-ts/lib/Either'; +import { SavedObjectsRawDoc } from '../..'; +import { createBulkOperationBody } from '../actions/bulk_overwrite_transformed_documents'; + +/** + * Creates batches of documents to be used by the bulk API. Each batch will + * have a request body content length that's <= maxBatchSizeBytes + */ +export function createBatches( + docs: SavedObjectsRawDoc[], + index: string, + maxBatchSizeBytes: number +) { + /* To build up the NDJSON request body we construct an array of objects like: + * [ + * {"index": ...} + * {"title": "my saved object"} + * ... + * ] + * However, when we call JSON.stringify on this array the resulting string + * will be surrounded by `[]` which won't be present in the NDJSON so these + * two characters need to be removed from the size calculation. + */ + const BRACKETS_BYTES = 2; + /* Each document in the NDJSON (including the last one) needs to be + * terminated by a newline, so we need to account for an extra newline + * character + */ + const NDJSON_NEW_LINE_BYTES = 1; + + const batches = [[]] as [SavedObjectsRawDoc[]]; + let currBatch = 0; + let currBatchSizeBytes = 0; + for (const doc of docs) { + const bulkOperationBody = createBulkOperationBody(doc, index); + const docSizeBytes = + Buffer.byteLength(JSON.stringify(bulkOperationBody), 'utf8') - + BRACKETS_BYTES + + NDJSON_NEW_LINE_BYTES; + if (docSizeBytes > maxBatchSizeBytes) { + return Either.left({ + type: 'document_exceeds_batch_size_bytes', + docSizeBytes, + maxBatchSizeBytes, + document: doc, + }); + } else if (currBatchSizeBytes + docSizeBytes <= maxBatchSizeBytes) { + batches[currBatch].push(doc); + currBatchSizeBytes = currBatchSizeBytes + docSizeBytes; + } else { + currBatch++; + batches[currBatch] = [doc]; + currBatchSizeBytes = docSizeBytes; + } + } + + return Either.right(batches); +} diff --git a/src/core/server/saved_objects/migrationsv2/model/model.test.ts b/src/core/server/saved_objects/migrationsv2/model/model.test.ts index f24d175f416a..1d017116bf3f 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.test.ts +++ b/src/core/server/saved_objects/migrationsv2/model/model.test.ts @@ -58,6 +58,7 @@ describe('migrations v2 model', () => { retryDelay: 0, retryAttempts: 15, batchSize: 1000, + maxBatchSizeBytes: 1e8, indexPrefix: '.kibana', outdatedDocumentsQuery: {}, targetIndexMappings: { @@ -1065,6 +1066,8 @@ describe('migrations v2 model', () => { }); const newState = model(state, res) as ReindexSourceToTempIndexBulk; expect(newState.controlState).toEqual('REINDEX_SOURCE_TO_TEMP_INDEX_BULK'); + expect(newState.currentBatch).toEqual(0); + expect(newState.transformedDocBatches).toEqual([processedDocs]); expect(newState.progress.processed).toBe(0); // Result of `(undefined ?? 0) + corruptDocumentsId.length` }); @@ -1119,16 +1122,19 @@ describe('migrations v2 model', () => { }); describe('REINDEX_SOURCE_TO_TEMP_INDEX_BULK', () => { - const transformedDocs = [ - { - _id: 'a:b', - _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, - }, - ] as SavedObjectsRawDoc[]; + const transformedDocBatches = [ + [ + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + ], + ] as [SavedObjectsRawDoc[]]; const reindexSourceToTempIndexBulkState: ReindexSourceToTempIndexBulk = { ...baseState, controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK', - transformedDocs, + transformedDocBatches, + currentBatch: 0, versionIndexReadyActions: Option.none, sourceIndex: Option.some('.kibana') as Option.Some, sourceIndexPitId: 'pit_id', @@ -1171,7 +1177,7 @@ describe('migrations v2 model', () => { const newState = model(reindexSourceToTempIndexBulkState, res) as FatalState; expect(newState.controlState).toEqual('FATAL'); expect(newState.reason).toMatchInlineSnapshot( - `"While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Try to use smaller batches by changing the Kibana 'migrations.batchSize' configuration option and restarting Kibana."` + `"While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Ensure that the Kibana configuration option 'migrations.maxBatchSizeBytes' is set to a value that is lower than or equal to the Elasticsearch 'http.max_content_length' configuration option."` ); }); test('REINDEX_SOURCE_TO_TEMP_INDEX_BULK should throw a throwBadResponse error if action failed', () => { @@ -1438,7 +1444,8 @@ describe('migrations v2 model', () => { res ) as TransformedDocumentsBulkIndex; expect(newState.controlState).toEqual('TRANSFORMED_DOCUMENTS_BULK_INDEX'); - expect(newState.transformedDocs).toEqual(processedDocs); + expect(newState.transformedDocBatches).toEqual([processedDocs]); + expect(newState.currentBatch).toEqual(0); expect(newState.retryCount).toEqual(0); expect(newState.retryDelay).toEqual(0); expect(newState.progress.processed).toBe(outdatedDocuments.length); @@ -1521,16 +1528,31 @@ describe('migrations v2 model', () => { }); describe('TRANSFORMED_DOCUMENTS_BULK_INDEX', () => { - const transformedDocs = [ - { - _id: 'a:b', - _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, - }, - ] as SavedObjectsRawDoc[]; + const transformedDocBatches = [ + [ + // batch 0 + { + _id: 'a:b', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + { + _id: 'a:c', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + ], + [ + // batch 1 + { + _id: 'a:d', + _source: { type: 'a', a: { name: 'HOI!' }, migrationVersion: {}, references: [] }, + }, + ], + ] as SavedObjectsRawDoc[][]; const transformedDocumentsBulkIndexState: TransformedDocumentsBulkIndex = { ...baseState, controlState: 'TRANSFORMED_DOCUMENTS_BULK_INDEX', - transformedDocs, + transformedDocBatches, + currentBatch: 0, versionIndexReadyActions: Option.none, sourceIndex: Option.some('.kibana') as Option.Some, targetIndex: '.kibana_7.11.0_001', @@ -1540,6 +1562,29 @@ describe('migrations v2 model', () => { progress: createInitialProgress(), }; + test('TRANSFORMED_DOCUMENTS_BULK_INDEX -> TRANSFORMED_DOCUMENTS_BULK_INDEX and increments currentBatch if more batches are left', () => { + const res: ResponseType<'TRANSFORMED_DOCUMENTS_BULK_INDEX'> = Either.right( + 'bulk_index_succeeded' + ); + const newState = model( + transformedDocumentsBulkIndexState, + res + ) as TransformedDocumentsBulkIndex; + expect(newState.controlState).toEqual('TRANSFORMED_DOCUMENTS_BULK_INDEX'); + expect(newState.currentBatch).toEqual(1); + }); + + test('TRANSFORMED_DOCUMENTS_BULK_INDEX -> OUTDATED_DOCUMENTS_SEARCH_READ if all batches were written', () => { + const res: ResponseType<'TRANSFORMED_DOCUMENTS_BULK_INDEX'> = Either.right( + 'bulk_index_succeeded' + ); + const newState = model( + { ...transformedDocumentsBulkIndexState, ...{ currentBatch: 1 } }, + res + ); + expect(newState.controlState).toEqual('OUTDATED_DOCUMENTS_SEARCH_READ'); + }); + test('TRANSFORMED_DOCUMENTS_BULK_INDEX throws if action returns left index_not_found_exception', () => { const res: ResponseType<'TRANSFORMED_DOCUMENTS_BULK_INDEX'> = Either.left({ type: 'index_not_found_exception', @@ -1570,7 +1615,7 @@ describe('migrations v2 model', () => { const newState = model(transformedDocumentsBulkIndexState, res) as FatalState; expect(newState.controlState).toEqual('FATAL'); expect(newState.reason).toMatchInlineSnapshot( - `"While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Try to use smaller batches by changing the Kibana 'migrations.batchSize' configuration option and restarting Kibana."` + `"While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Ensure that the Kibana configuration option 'migrations.maxBatchSizeBytes' is set to a value that is lower than or equal to the Elasticsearch 'http.max_content_length' configuration option."` ); }); }); diff --git a/src/core/server/saved_objects/migrationsv2/model/model.ts b/src/core/server/saved_objects/migrationsv2/model/model.ts index 50be4a524f5c..8aa3d7b83b29 100644 --- a/src/core/server/saved_objects/migrationsv2/model/model.ts +++ b/src/core/server/saved_objects/migrationsv2/model/model.ts @@ -31,6 +31,19 @@ import { throwBadControlState, throwBadResponse, } from './helpers'; +import { createBatches } from './create_batches'; + +const FATAL_REASON_REQUEST_ENTITY_TOO_LARGE = `While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Ensure that the Kibana configuration option 'migrations.maxBatchSizeBytes' is set to a value that is lower than or equal to the Elasticsearch 'http.max_content_length' configuration option.`; +const fatalReasonDocumentExceedsMaxBatchSizeBytes = ({ + _id, + docSizeBytes, + maxBatchSizeBytes, +}: { + _id: string; + docSizeBytes: number; + maxBatchSizeBytes: number; +}) => + `The document with _id "${_id}" is ${docSizeBytes} bytes which exceeds the configured maximum batch size of ${maxBatchSizeBytes} bytes. To proceed, please increase the 'migrations.maxBatchSizeBytes' Kibana configuration option and ensure that the Elasticsearch 'http.max_content_length' configuration option is set to an equal or larger value.`; export const model = (currentState: State, resW: ResponseType): State => { // The action response `resW` is weakly typed, the type includes all action @@ -489,12 +502,30 @@ export const model = (currentState: State, resW: ResponseType): if (Either.isRight(res)) { if (stateP.corruptDocumentIds.length === 0 && stateP.transformErrors.length === 0) { - return { - ...stateP, - controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK', // handles the actual bulk indexing into temp index - transformedDocs: [...res.right.processedDocs], - progress, - }; + const batches = createBatches( + res.right.processedDocs, + stateP.tempIndex, + stateP.maxBatchSizeBytes + ); + if (Either.isRight(batches)) { + return { + ...stateP, + controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK', // handles the actual bulk indexing into temp index + transformedDocBatches: batches.right, + currentBatch: 0, + progress, + }; + } else { + return { + ...stateP, + controlState: 'FATAL', + reason: fatalReasonDocumentExceedsMaxBatchSizeBytes({ + _id: batches.left.document._id, + docSizeBytes: batches.left.docSizeBytes, + maxBatchSizeBytes: batches.left.maxBatchSizeBytes, + }), + }; + } } else { // we don't have any transform issues with the current batch of outdated docs but // we have carried through previous transformation issues. @@ -525,13 +556,21 @@ export const model = (currentState: State, resW: ResponseType): } else if (stateP.controlState === 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { - return { - ...stateP, - controlState: 'REINDEX_SOURCE_TO_TEMP_READ', - // we're still on the happy path with no transformation failures seen. - corruptDocumentIds: [], - transformErrors: [], - }; + if (stateP.currentBatch + 1 < stateP.transformedDocBatches.length) { + return { + ...stateP, + controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK', + currentBatch: stateP.currentBatch + 1, + }; + } else { + return { + ...stateP, + controlState: 'REINDEX_SOURCE_TO_TEMP_READ', + // we're still on the happy path with no transformation failures seen. + corruptDocumentIds: [], + transformErrors: [], + }; + } } else { if ( isLeftTypeof(res.left, 'target_index_had_write_block') || @@ -548,7 +587,7 @@ export const model = (currentState: State, resW: ResponseType): return { ...stateP, controlState: 'FATAL', - reason: `While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Try to use smaller batches by changing the Kibana 'migrations.batchSize' configuration option and restarting Kibana.`, + reason: FATAL_REASON_REQUEST_ENTITY_TOO_LARGE, }; } throwBadResponse(stateP, res.left); @@ -677,13 +716,31 @@ export const model = (currentState: State, resW: ResponseType): // we haven't seen corrupt documents or any transformation errors thus far in the migration // index the migrated docs if (stateP.corruptDocumentIds.length === 0 && stateP.transformErrors.length === 0) { - return { - ...stateP, - controlState: 'TRANSFORMED_DOCUMENTS_BULK_INDEX', - transformedDocs: [...res.right.processedDocs], - hasTransformedDocs: true, - progress, - }; + const batches = createBatches( + res.right.processedDocs, + stateP.targetIndex, + stateP.maxBatchSizeBytes + ); + if (Either.isRight(batches)) { + return { + ...stateP, + controlState: 'TRANSFORMED_DOCUMENTS_BULK_INDEX', + transformedDocBatches: batches.right, + currentBatch: 0, + hasTransformedDocs: true, + progress, + }; + } else { + return { + ...stateP, + controlState: 'FATAL', + reason: fatalReasonDocumentExceedsMaxBatchSizeBytes({ + _id: batches.left.document._id, + docSizeBytes: batches.left.docSizeBytes, + maxBatchSizeBytes: batches.left.maxBatchSizeBytes, + }), + }; + } } else { // We have seen corrupt documents and/or transformation errors // skip indexing and go straight to reading and transforming more docs @@ -711,6 +768,13 @@ export const model = (currentState: State, resW: ResponseType): } else if (stateP.controlState === 'TRANSFORMED_DOCUMENTS_BULK_INDEX') { const res = resW as ExcludeRetryableEsError>; if (Either.isRight(res)) { + if (stateP.currentBatch + 1 < stateP.transformedDocBatches.length) { + return { + ...stateP, + controlState: 'TRANSFORMED_DOCUMENTS_BULK_INDEX', + currentBatch: stateP.currentBatch + 1, + }; + } return { ...stateP, controlState: 'OUTDATED_DOCUMENTS_SEARCH_READ', @@ -723,7 +787,7 @@ export const model = (currentState: State, resW: ResponseType): return { ...stateP, controlState: 'FATAL', - reason: `While indexing a batch of saved objects, Elasticsearch returned a 413 Request Entity Too Large exception. Try to use smaller batches by changing the Kibana 'migrations.batchSize' configuration option and restarting Kibana.`, + reason: FATAL_REASON_REQUEST_ENTITY_TOO_LARGE, }; } else if ( isLeftTypeof(res.left, 'target_index_had_write_block') || diff --git a/src/core/server/saved_objects/migrationsv2/next.ts b/src/core/server/saved_objects/migrationsv2/next.ts index 9b091b6fc850..3f3714552725 100644 --- a/src/core/server/saved_objects/migrationsv2/next.ts +++ b/src/core/server/saved_objects/migrationsv2/next.ts @@ -111,7 +111,7 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra Actions.bulkOverwriteTransformedDocuments({ client, index: state.tempIndex, - transformedDocs: state.transformedDocs, + transformedDocs: state.transformedDocBatches[state.currentBatch], /** * Since we don't run a search against the target index, we disable "refresh" to speed up * the migration process. @@ -160,7 +160,7 @@ export const nextActionMap = (client: ElasticsearchClient, transformRawDocs: Tra Actions.bulkOverwriteTransformedDocuments({ client, index: state.targetIndex, - transformedDocs: state.transformedDocs, + transformedDocs: state.transformedDocBatches[state.currentBatch], /** * Since we don't run a search against the target index, we disable "refresh" to speed up * the migration process. diff --git a/src/core/server/saved_objects/migrationsv2/test_helpers/retry.test.ts b/src/core/server/saved_objects/migrationsv2/test_helpers/retry.test.ts new file mode 100644 index 000000000000..246f61c71ae4 --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/test_helpers/retry.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { retryAsync } from './retry_async'; + +describe('retry', () => { + it('retries throwing functions until they succeed', async () => { + let i = 0; + await expect( + retryAsync( + () => { + if (i++ < 2) { + return Promise.reject(new Error('boom')); + } else { + return Promise.resolve('done'); + } + }, + { retryAttempts: 3, retryDelayMs: 1 } + ) + ).resolves.toEqual('done'); + }); + + it('throws if all attempts are exhausted before success', async () => { + let attempts = 0; + await expect(() => + retryAsync( + () => { + attempts++; + return Promise.reject(new Error('boom')); + }, + { retryAttempts: 3, retryDelayMs: 1 } + ) + ).rejects.toMatchInlineSnapshot(`[Error: boom]`); + expect(attempts).toEqual(3); + }); + + it('waits retryDelayMs between each attempt ', async () => { + const now = Date.now(); + let i = 0; + await retryAsync( + () => { + if (i++ < 2) { + return Promise.reject(new Error('boom')); + } else { + return Promise.resolve('done'); + } + }, + { retryAttempts: 3, retryDelayMs: 100 } + ); + expect(Date.now() - now).toBeGreaterThanOrEqual(200); + }); +}); diff --git a/src/core/server/saved_objects/migrationsv2/test_helpers/retry_async.ts b/src/core/server/saved_objects/migrationsv2/test_helpers/retry_async.ts new file mode 100644 index 000000000000..f5dffede67a1 --- /dev/null +++ b/src/core/server/saved_objects/migrationsv2/test_helpers/retry_async.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +function delay(delayInMs: number) { + return new Promise((resolve) => setTimeout(resolve, delayInMs)); +} + +export async function retryAsync( + fn: () => Promise, + options: { retryAttempts: number; retryDelayMs: number } +): Promise { + try { + return await fn(); + } catch (e) { + if (options.retryAttempts > 1) { + await delay(options.retryDelayMs); + return retryAsync(fn, { + retryAttempts: options.retryAttempts - 1, + retryDelayMs: options.retryDelayMs, + }); + } else { + throw e; + } + } +} diff --git a/src/core/server/saved_objects/migrationsv2/types.ts b/src/core/server/saved_objects/migrationsv2/types.ts index ea03b64e03dc..49ce12c53aa1 100644 --- a/src/core/server/saved_objects/migrationsv2/types.ts +++ b/src/core/server/saved_objects/migrationsv2/types.ts @@ -76,19 +76,31 @@ export interface BaseState extends ControlState { readonly retryAttempts: number; /** - * The number of documents to fetch from Elasticsearch server to run migration over. + * The number of documents to process in each batch. This determines the + * maximum number of documents that will be read and written in a single + * request. * - * The higher the value, the faster the migration process will be performed since it reduces - * the number of round trips between Kibana and Elasticsearch servers. - * For the migration speed, we have to pay the price of increased memory consumption. + * The higher the value, the faster the migration process will be performed + * since it reduces the number of round trips between Kibana and + * Elasticsearch servers. For the migration speed, we have to pay the price + * of increased memory consumption and HTTP payload size. * - * Since batchSize defines the number of documents, not their size, it might happen that - * Elasticsearch fails a request with circuit_breaking_exception when it retrieves a set of - * saved objects of significant size. + * Since we cannot control the size in bytes of a batch when reading, + * Elasticsearch might fail with a circuit_breaking_exception when it + * retrieves a set of saved objects of significant size. In this case, you + * should set a smaller batchSize value and restart the migration process + * again. * - * In this case, you should set a smaller batchSize value and restart the migration process again. + * When writing batches, we limit the number of documents in a batch + * (batchSize) as well as the size of the batch in bytes (maxBatchSizeBytes). */ readonly batchSize: number; + /** + * When writing batches, limits the batch size in bytes to ensure that we + * don't construct HTTP requests which would exceed Elasticsearch's + * http.max_content_length which defaults to 100mb. + */ + readonly maxBatchSizeBytes: number; readonly logs: MigrationLog[]; /** * The current alias e.g. `.kibana` which always points to the latest @@ -233,7 +245,8 @@ export interface ReindexSourceToTempIndex extends PostInitState { export interface ReindexSourceToTempIndexBulk extends PostInitState { readonly controlState: 'REINDEX_SOURCE_TO_TEMP_INDEX_BULK'; - readonly transformedDocs: SavedObjectsRawDoc[]; + readonly transformedDocBatches: [SavedObjectsRawDoc[]]; + readonly currentBatch: number; readonly sourceIndexPitId: string; readonly lastHitSortValue: number[] | undefined; readonly progress: Progress; @@ -318,7 +331,8 @@ export interface TransformedDocumentsBulkIndex extends PostInitState { * Write the up-to-date transformed documents to the target index */ readonly controlState: 'TRANSFORMED_DOCUMENTS_BULK_INDEX'; - readonly transformedDocs: SavedObjectsRawDoc[]; + readonly transformedDocBatches: SavedObjectsRawDoc[][]; + readonly currentBatch: number; readonly lastHitSortValue: number[] | undefined; readonly hasTransformedDocs: boolean; readonly pitId: string; diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts index c62d322f0bf8..e7bbd706762f 100644 --- a/src/core/server/saved_objects/saved_objects_config.ts +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -12,6 +12,7 @@ import type { ConfigDeprecationProvider } from '../config'; const migrationSchema = schema.object({ batchSize: schema.number({ defaultValue: 1_000 }), + maxBatchSizeBytes: schema.byteSize({ defaultValue: '100mb' }), // 100mb is the default http.max_content_length Elasticsearch config value scrollDuration: schema.string({ defaultValue: '15m' }), pollInterval: schema.number({ defaultValue: 1_500 }), skip: schema.boolean({ defaultValue: false }), diff --git a/src/core/server/saved_objects/service/lib/integration_tests/repository_with_proxy.test.ts b/src/core/server/saved_objects/service/lib/integration_tests/repository_with_proxy.test.ts new file mode 100644 index 000000000000..8428e7be91ae --- /dev/null +++ b/src/core/server/saved_objects/service/lib/integration_tests/repository_with_proxy.test.ts @@ -0,0 +1,463 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Hapi from '@hapi/hapi'; +import h2o2 from '@hapi/h2o2'; +import { URL } from 'url'; +import { ISavedObjectsRepository } from '../repository'; +import { SavedObject } from '../../../types'; +import { InternalCoreSetup, InternalCoreStart } from '../../../../internal_types'; +import { Root } from '../../../../root'; +import * as kbnTestServer from '../../../../../test_helpers/kbn_server'; +import { + declareGetRoute, + declareDeleteRoute, + declarePostBulkRoute, + declarePostMgetRoute, + declareGetSearchRoute, + declarePostSearchRoute, + declarePostUpdateRoute, + declarePostPitRoute, + declarePostUpdateByQueryRoute, + declarePassthroughRoute, + setProxyInterrupt, +} from './repository_with_proxy_utils'; + +let esServer: kbnTestServer.TestElasticsearchUtils; +let hapiServer: Hapi.Server; + +const registerSOTypes = (setup: InternalCoreSetup) => { + setup.savedObjects.registerType({ + name: 'my_type', + hidden: false, + mappings: { + dynamic: false, + properties: { + title: { type: 'text' }, + }, + }, + namespaceType: 'single', + }); + setup.savedObjects.registerType({ + name: 'my_other_type', + hidden: false, + mappings: { + dynamic: false, + properties: { + title: { type: 'text' }, + }, + }, + namespaceType: 'single', + }); +}; + +describe('404s from proxies', () => { + let root: Root; + let start: InternalCoreStart; + + beforeAll(async () => { + setProxyInterrupt(null); + + const { startES } = kbnTestServer.createTestServers({ + adjustTimeout: (t: number) => jest.setTimeout(t), + }); + esServer = await startES(); + + const { hostname: esHostname, port: esPort } = new URL(esServer.hosts[0]); + + // inspired by https://github.com/elastic/kibana/pull/88919 + const proxyPort = process.env.TEST_PROXY_SERVER_PORT + ? parseInt(process.env.TEST_PROXY_SERVER_PORT, 10) + : 5698; + + // Setup custom hapi hapiServer with h2o2 plugin for proxying + hapiServer = Hapi.server({ + port: proxyPort, + }); + + await hapiServer.register(h2o2); + // register specific routes to modify the response and a catch-all to relay the request/response as-is + + declareGetRoute(hapiServer, esHostname, esPort); + declareDeleteRoute(hapiServer, esHostname, esPort); + declarePostUpdateRoute(hapiServer, esHostname, esPort); + + declareGetSearchRoute(hapiServer, esHostname, esPort); + declarePostSearchRoute(hapiServer, esHostname, esPort); + declarePostBulkRoute(hapiServer, esHostname, esPort); + declarePostMgetRoute(hapiServer, esHostname, esPort); + declarePostPitRoute(hapiServer, esHostname, esPort); + declarePostUpdateByQueryRoute(hapiServer, esHostname, esPort); + + declarePassthroughRoute(hapiServer, esHostname, esPort); + + await hapiServer.start(); + + // Setup kibana configured to use proxy as ES backend + root = kbnTestServer.createRootWithCorePlugins({ + elasticsearch: { + hosts: [`http://${esHostname}:${proxyPort}`], + }, + migrations: { + skip: false, + }, + }); + await root.preboot(); + const setup = await root.setup(); + registerSOTypes(setup); + + start = await root.start(); + }); + + afterAll(async () => { + await root.shutdown(); + await hapiServer.stop({ timeout: 1000 }); + await esServer.stop(); + }); + + describe('requests when a proxy relays request/responses with the correct product header', () => { + let repository: ISavedObjectsRepository; + let myOtherType: SavedObject; + const myOtherTypeDocs: SavedObject[] = []; + + beforeAll(async () => { + repository = start.savedObjects.createInternalRepository(); + + myOtherType = await repository.create( + 'my_other_type', + { title: 'my_other_type1' }, + { overwrite: false, references: [] } + ); + for (let i = 1; i < 11; i++) { + myOtherTypeDocs.push({ + type: 'my_other_type', + id: `myOtherTypeId${i}`, + attributes: { title: `MyOtherTypeTitle${i}` }, + references: [], + }); + } + await repository.bulkCreate(myOtherTypeDocs, { + overwrite: true, + namespace: 'default', + }); + }); + + beforeEach(() => { + setProxyInterrupt(null); + }); + + it('does not alter a Not Found response if the document does not exist and the proxy returns the correct product header', async () => { + let customErr: any; + try { + await repository.get('my_other_type', '123'); + } catch (err) { + customErr = err; + } + expect(customErr?.output?.statusCode).toBe(404); + expect(customErr?.output?.payload?.message).toBe( + 'Saved object [my_other_type/123] not found' + ); + }); + + it('returns a document if it exists and if the proxy passes through the product header', async () => { + const myOtherTypeDoc = await repository.get('my_other_type', `${myOtherType.id}`); + expect(myOtherTypeDoc.type).toBe('my_other_type'); + }); + + it('handles `update` requests that are successful', async () => { + const docToUpdate = await repository.create( + 'my_other_type', + { title: 'original title' }, + { overwrite: false, references: [] } + ); + + const updatedDoc = await repository.update('my_other_type', `${docToUpdate.id}`, { + title: 'updated title', + }); + expect(updatedDoc.type).toBe('my_other_type'); + expect(updatedDoc.attributes.title).toBe('updated title'); + }); + + it('handles `bulkCreate` requests when the proxy relays request/responses correctly', async () => { + const bulkObjects = [ + { + type: 'my_other_type', + id: 'my_other_type1', + attributes: { + title: 'bulkType1', + }, + references: [], + }, + { + type: 'my_other_type', + id: 'my_other_type2', + attributes: { + title: 'bulkType2', + }, + references: [], + }, + ]; + const bulkResponse = await repository.bulkCreate(bulkObjects, { + namespace: 'default', + overwrite: true, + }); + expect(bulkResponse.saved_objects.length).toEqual(2); + }); + + it('returns matches from `find` when the proxy passes through the response and product header', async () => { + const type = 'my_other_type'; + const result = await repository.find({ type }); + expect(result.saved_objects.length).toBeGreaterThan(0); + }); + + it('handles `delete` requests that are successful', async () => { + let deleteErr: any; + const docToDelete = await repository.create( + 'my_other_type', + { title: 'delete me please' }, + { id: 'docToDelete1', overwrite: true, references: [] } + ); + const deleteResult = await repository.delete('my_other_type', 'docToDelete1', { + namespace: 'default', + }); + expect(deleteResult).toStrictEqual({}); + try { + await repository.get('my_other_type', 'docToDelete1'); + } catch (err) { + deleteErr = err; + } + expect(deleteErr?.output?.statusCode).toBe(404); + expect(deleteErr?.output?.payload?.message).toBe( + `Saved object [my_other_type/${docToDelete.id}] not found` + ); + }); + + it('handles `bulkGet` requests that are successful when the proxy passes through the product header', async () => { + const docsToGet = myOtherTypeDocs; + const docsFound = await repository.bulkGet( + docsToGet.map((doc) => ({ id: doc.id, type: 'my_other_type' })) + ); + expect(docsFound.saved_objects.length).toBeGreaterThan(0); + }); + + it('handles `resolve` requests that are successful with an exact match', async () => { + const resolvedExactMatch = await repository.resolve('my_other_type', `${myOtherType.id}`); + expect(resolvedExactMatch.outcome).toBe('exactMatch'); + }); + + it('handles `openPointInTime` requests when the proxy passes through the product header', async () => { + const openPitResult = await repository.openPointInTimeForType('my_other_type'); + expect(Object.keys(openPitResult)).toContain('id'); + }); + + it('handles `checkConflicts` requests that are successful when the proxy passes through the product header', async () => { + const checkConflictsResult = await repository.checkConflicts( + [ + { id: myOtherTypeDocs[0].id, type: myOtherTypeDocs[0].type }, + { id: 'myOtherType456', type: 'my_other_type' }, + ], + { namespace: 'default' } + ); + expect(checkConflictsResult.errors.length).toEqual(1); + expect(checkConflictsResult.errors[0].error.error).toStrictEqual('Conflict'); + }); + + // this test must come last, it deletes all saved objects in the default space + it('handles `deleteByNamespace` requests when the proxy passes through the product header', async () => { + const deleteByNamespaceResult = await repository.deleteByNamespace('default'); + expect(Object.keys(deleteByNamespaceResult)).toEqual( + expect.arrayContaining(['total', 'updated', 'deleted']) + ); + }); + }); + + describe('requests when a proxy returns Not Found with an incorrect product header', () => { + let repository: ISavedObjectsRepository; + const myTypeDocs: SavedObject[] = []; + + const genericNotFoundEsUnavailableError = (err: any, type?: string, id?: string) => { + expect(err?.output?.statusCode).toBe(503); + if (type && id) { + expect(err?.output?.payload?.message).toBe( + `x-elastic-product not present or not recognized: Saved object [${type}/${id}] not found` + ); + } else { + expect(err?.output?.payload?.message).toBe( + `x-elastic-product not present or not recognized: Not Found` + ); + } + }; + + beforeAll(async () => { + setProxyInterrupt(null); // allow saved object creation + repository = start.savedObjects.createInternalRepository(); + + for (let i = 1; i < 11; i++) { + myTypeDocs.push({ + type: 'my_type', + id: `myTypeId${i}`, + attributes: { title: `MyTypeTitle${i}` }, + references: [], + }); + } + await repository.bulkCreate( + [ + ...myTypeDocs, + { + type: 'my_type', + id: 'myTypeToUpdate', + attributes: { title: 'myTypeToUpdateTitle' }, + references: [], + }, + ], + { + overwrite: true, + namespace: 'default', + } + ); + }); + beforeEach(() => { + setProxyInterrupt(null); // switch to non-proxied handler + }); + + it('returns an EsUnavailable error if the document exists but the proxy cannot find the es node (mimics allocator changes)', async () => { + let myError; + try { + await repository.get('my_type', 'myTypeId1'); + } catch (err) { + myError = err; + } + expect(genericNotFoundEsUnavailableError(myError, 'my_type', 'myTypeId1')); + }); + + it('returns an EsUnavailable error on `update` requests that are interrupted', async () => { + let updateError; + try { + await repository.update('my_type', 'myTypeToUpdate', { + title: 'updated title', + }); + expect(false).toBe(true); // Should not get here (we expect the call to throw) + } catch (err) { + updateError = err; + } + expect(genericNotFoundEsUnavailableError(updateError)); + }); + + it('returns an EsUnavailable error on `bulkCreate` requests with a 404 proxy response and wrong product header', async () => { + setProxyInterrupt('bulkCreate'); + let bulkCreateError: any; + const bulkObjects = [ + { + type: 'my_type', + id: '1', + attributes: { + title: 'bulkType1', + }, + references: [], + }, + { + type: 'my_type', + id: '2', + attributes: { + title: 'bulkType2', + }, + references: [], + }, + ]; + try { + await repository.bulkCreate(bulkObjects, { namespace: 'default', overwrite: true }); + } catch (err) { + bulkCreateError = err; + } + expect(genericNotFoundEsUnavailableError(bulkCreateError)); + }); + + it('returns an EsUnavailable error on `find` requests with a 404 proxy response and wrong product header', async () => { + setProxyInterrupt('find'); + let findErr: any; + try { + await repository.find({ type: 'my_type' }); + } catch (err) { + findErr = err; + } + expect(genericNotFoundEsUnavailableError(findErr)); + expect(findErr?.output?.payload?.error).toBe('Service Unavailable'); + }); + + it('returns an EsUnavailable error on `delete` requests with a 404 proxy response and wrong product header', async () => { + let deleteErr: any; + try { + await repository.delete('my_type', 'myTypeId1', { namespace: 'default' }); + } catch (err) { + deleteErr = err; + } + expect(genericNotFoundEsUnavailableError(deleteErr, 'my_type', 'myTypeId1')); + }); + + it('returns an EsUnavailable error on `resolve` requests with a 404 proxy response and wrong product header for an exact match', async () => { + let testResolveErr: any; + try { + await repository.resolve('my_type', 'myTypeId1'); + } catch (err) { + testResolveErr = err; + } + expect(genericNotFoundEsUnavailableError(testResolveErr, 'my_type', 'myTypeId1')); + }); + + it('returns an EsUnavailable error on `bulkGet` requests with a 404 proxy response and wrong product header', async () => { + const docsToGet = myTypeDocs; + let bulkGetError: any; + setProxyInterrupt('bulkGetMyType'); + try { + await repository.bulkGet(docsToGet.map((doc) => ({ id: doc.id, type: 'my_type' }))); + } catch (err) { + bulkGetError = err; + } + expect(genericNotFoundEsUnavailableError(bulkGetError)); + }); + + it('returns an EsUnavailable error on `openPointInTimeForType` requests with a 404 proxy response and wrong product header', async () => { + setProxyInterrupt('openPit'); + let openPitErr: any; + try { + await repository.openPointInTimeForType('my_other_type'); + } catch (err) { + openPitErr = err; + } + expect(genericNotFoundEsUnavailableError(openPitErr)); + }); + + it('returns an EsUnavailable error on `checkConflicts` requests with a 404 proxy response and wrong product header', async () => { + setProxyInterrupt('checkConficts'); + let checkConflictsErr: any; + try { + await repository.checkConflicts( + [ + { id: myTypeDocs[0].id, type: myTypeDocs[0].type }, + { id: 'myType456', type: 'my_type' }, + ], + { namespace: 'default' } + ); + } catch (err) { + checkConflictsErr = err; + } + expect(genericNotFoundEsUnavailableError(checkConflictsErr)); + }); + + it('returns an EsUnavailable error on `deleteByNamespace` requests with a 404 proxy response and wrong product header', async () => { + setProxyInterrupt('deleteByNamespace'); + let deleteByNamespaceErr: any; + try { + await repository.deleteByNamespace('default'); + } catch (err) { + deleteByNamespaceErr = err; + } + expect(genericNotFoundEsUnavailableError(deleteByNamespaceErr)); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/integration_tests/repository_with_proxy_utils.ts b/src/core/server/saved_objects/service/lib/integration_tests/repository_with_proxy_utils.ts new file mode 100644 index 000000000000..cb0b2bd835bb --- /dev/null +++ b/src/core/server/saved_objects/service/lib/integration_tests/repository_with_proxy_utils.ts @@ -0,0 +1,240 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import Hapi from '@hapi/hapi'; +import { IncomingMessage } from 'http'; +import { kibanaPackageJson as pkg } from '@kbn/utils'; + +// proxy setup +const defaultProxyOptions = (hostname: string, port: string) => ({ + host: hostname, + port, + protocol: 'http' as 'http', + passThrough: true, +}); + +let proxyInterrupt: string | null | undefined = null; + +export const setProxyInterrupt = ( + testArg: + | 'bulkCreate' + | 'bulkGetMyType' + | 'checkConficts' + | 'find' + | 'openPit' + | 'deleteByNamespace' + | null +) => (proxyInterrupt = testArg); + +// passes the req/response directly as is +const relayHandler = (h: Hapi.ResponseToolkit, hostname: string, port: string) => { + return h.proxy(defaultProxyOptions(hostname, port)); +}; + +const proxyResponseHandler = (h: Hapi.ResponseToolkit, hostname: string, port: string) => { + return h.proxy({ + ...defaultProxyOptions(hostname, port), + // eslint-disable-next-line @typescript-eslint/no-shadow + onResponse: async (err, res, request, h, settings, ttl) => proxyOnResponseHandler(res, h), + }); +}; + +// mimics a 404 'unexpected' response from the proxy +const proxyOnResponseHandler = async (res: IncomingMessage, h: Hapi.ResponseToolkit) => { + return h + .response(res) + .header('x-elastic-product', 'somethingitshouldnotbe', { override: true }) + .code(404); +}; + +const kbnIndex = `.kibana_${pkg.version}`; + +// GET /.kibana_8.0.0/_doc/{type*} route (repository.get calls) +export const declareGetRoute = (hapiServer: Hapi.Server, hostname: string, port: string) => + hapiServer.route({ + method: 'GET', + path: `/${kbnIndex}/_doc/{type*}`, + options: { + handler: (req, h) => { + if (req.params.type === 'my_type:myTypeId1' || req.params.type === 'my_type:myType_123') { + return proxyResponseHandler(h, hostname, port); + } else { + return relayHandler(h, hostname, port); + } + }, + }, + }); +// DELETE /.kibana_8.0.0/_doc/{type*} route (repository.delete calls) +export const declareDeleteRoute = (hapiServer: Hapi.Server, hostname: string, port: string) => + hapiServer.route({ + method: 'DELETE', + path: `/${kbnIndex}/_doc/{_id*}`, + options: { + payload: { + output: 'data', + parse: false, + }, + handler: (req, h) => { + if (req.params._id === 'my_type:myTypeId1') { + return proxyResponseHandler(h, hostname, port); + } else { + return relayHandler(h, hostname, port); + } + }, + }, + }); + +// POST _bulk route +export const declarePostBulkRoute = (hapiServer: Hapi.Server, hostname: string, port: string) => + hapiServer.route({ + method: 'POST', + path: '/_bulk', + options: { + payload: { + output: 'data', + parse: false, + }, + handler: (req, h) => { + if (proxyInterrupt === 'bulkCreate') { + return proxyResponseHandler(h, hostname, port); + } else { + return relayHandler(h, hostname, port); + } + }, + }, + }); +// POST _mget route (repository.bulkGet calls) +export const declarePostMgetRoute = (hapiServer: Hapi.Server, hostname: string, port: string) => + hapiServer.route({ + method: 'POST', + path: '/_mget', + options: { + payload: { + output: 'data', + parse: false, + }, + handler: (req, h) => { + if (proxyInterrupt === 'bulkGetMyType' || proxyInterrupt === 'checkConficts') { + return proxyResponseHandler(h, hostname, port); + } else { + return relayHandler(h, hostname, port); + } + }, + }, + }); +// GET _search route +export const declareGetSearchRoute = (hapiServer: Hapi.Server, hostname: string, port: string) => + hapiServer.route({ + method: 'GET', + path: `/${kbnIndex}/_search`, + options: { + handler: (req, h) => { + const payload = req.payload; + if (!payload) { + return proxyResponseHandler(h, hostname, port); + } else { + return relayHandler(h, hostname, port); + } + }, + }, + }); +// POST _search route (`find` calls) +export const declarePostSearchRoute = (hapiServer: Hapi.Server, hostname: string, port: string) => + hapiServer.route({ + method: 'POST', + path: `/${kbnIndex}/_search`, + options: { + payload: { + output: 'data', + parse: false, + }, + handler: (req, h) => { + if (proxyInterrupt === 'find') { + return proxyResponseHandler(h, hostname, port); + } else { + return relayHandler(h, hostname, port); + } + }, + }, + }); +// POST _update +export const declarePostUpdateRoute = (hapiServer: Hapi.Server, hostname: string, port: string) => + hapiServer.route({ + method: 'POST', + path: `/${kbnIndex}/_update/{_id*}`, + options: { + payload: { + output: 'data', + parse: false, + }, + handler: (req, h) => { + if (req.params._id === 'my_type:myTypeToUpdate') { + return proxyResponseHandler(h, hostname, port); + } else { + return relayHandler(h, hostname, port); + } + }, + }, + }); +// POST _pit +export const declarePostPitRoute = (hapiServer: Hapi.Server, hostname: string, port: string) => + hapiServer.route({ + method: 'POST', + path: `/${kbnIndex}/_pit`, + options: { + payload: { + output: 'data', + parse: false, + }, + handler: (req, h) => { + if (proxyInterrupt === 'openPit') { + return proxyResponseHandler(h, hostname, port); + } else { + return relayHandler(h, hostname, port); + } + }, + }, + }); +// POST _update_by_query +export const declarePostUpdateByQueryRoute = ( + hapiServer: Hapi.Server, + hostname: string, + port: string +) => + hapiServer.route({ + method: 'POST', + path: `/${kbnIndex}/_update_by_query`, + options: { + payload: { + output: 'data', + parse: false, + }, + handler: (req, h) => { + if (proxyInterrupt === 'deleteByNamespace') { + return proxyResponseHandler(h, hostname, port); + } else { + return relayHandler(h, hostname, port); + } + }, + }, + }); + +// catch-all passthrough route +export const declarePassthroughRoute = (hapiServer: Hapi.Server, hostname: string, port: string) => + hapiServer.route({ + method: '*', + path: '/{any*}', + options: { + payload: { + output: 'data', + parse: false, + }, + handler: (req, h) => { + return relayHandler(h, hostname, port); + }, + }, + }); diff --git a/src/core/server/saved_objects/service/lib/repository_es_client.ts b/src/core/server/saved_objects/service/lib/repository_es_client.ts index 6a601b1ed0c8..4e8592fa94cc 100644 --- a/src/core/server/saved_objects/service/lib/repository_es_client.ts +++ b/src/core/server/saved_objects/service/lib/repository_es_client.ts @@ -39,6 +39,7 @@ export function createRepositoryEsClient(client: ElasticsearchClient): Repositor (client[key] as Function)(params, { maxRetries: 0, ...options }) ); } catch (e) { + // retry failures are caught here, as are 404's that aren't ignored (e.g update calls) throw decorateEsError(e); } }, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 333ef8e7bf34..d6c33cc17456 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -754,10 +754,10 @@ export interface DeprecationsDetails { // (undocumented) documentationUrl?: string; level: 'warning' | 'critical' | 'fetch_error'; - // (undocumented) message: string; // (undocumented) requireRestart?: boolean; + title: string; } // @public diff --git a/src/dev/build/build_distributables.ts b/src/dev/build/build_distributables.ts index 9ddf02e101a1..1042cdc484c1 100644 --- a/src/dev/build/build_distributables.ts +++ b/src/dev/build/build_distributables.ts @@ -105,6 +105,10 @@ export async function buildDistributables(log: ToolingLog, options: BuildOptions // control w/ --skip-archives await run(Tasks.CreateArchives); } + + if (options.createDebPackage || options.createRpmPackage) { + await run(Tasks.CreatePackageConfig); + } if (options.createDebPackage) { // control w/ --deb or --skip-os-packages await run(Tasks.CreateDebPackage); diff --git a/src/dev/build/tasks/os_packages/create_os_package_kibana_yml.ts b/src/dev/build/tasks/os_packages/create_os_package_kibana_yml.ts new file mode 100644 index 000000000000..e7137ada0218 --- /dev/null +++ b/src/dev/build/tasks/os_packages/create_os_package_kibana_yml.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { readFileSync, writeFileSync } from 'fs'; +import { resolve } from 'path'; +import { Build, Config, mkdirp } from '../../lib'; + +export async function createOSPackageKibanaYML(config: Config, build: Build) { + const configReadPath = config.resolveFromRepo('config', 'kibana.yml'); + const configWriteDir = config.resolveFromRepo('build', 'os_packages', 'config'); + const configWritePath = resolve(configWriteDir, 'kibana.yml'); + + await mkdirp(configWriteDir); + + let kibanaYML = readFileSync(configReadPath, { encoding: 'utf8' }); + + [ + [/#pid.file:.*/g, 'pid.file: /run/kibana/kibana.pid'], + [/#logging.dest:.*/g, 'logging.dest: /var/log/kibana/kibana.log'], + ].forEach((options) => { + const [regex, setting] = options; + const diff = kibanaYML; + const match = kibanaYML.search(regex) >= 0; + if (match) { + if (typeof setting === 'string') { + kibanaYML = kibanaYML.replace(regex, setting); + } + } + + if (!diff.localeCompare(kibanaYML)) { + throw new Error( + `OS package configuration unmodified. Verify match for ${regex} is available` + ); + } + }); + + try { + writeFileSync(configWritePath, kibanaYML, { flag: 'wx' }); + } catch (err) { + if (err.code === 'EEXIST') { + return; + } + throw err; + } +} diff --git a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts index 99d0e1998e78..67a9e86ee207 100644 --- a/src/dev/build/tasks/os_packages/create_os_package_tasks.ts +++ b/src/dev/build/tasks/os_packages/create_os_package_tasks.ts @@ -9,6 +9,15 @@ import { Task } from '../../lib'; import { runFpm } from './run_fpm'; import { runDockerGenerator } from './docker_generator'; +import { createOSPackageKibanaYML } from './create_os_package_kibana_yml'; + +export const CreatePackageConfig: Task = { + description: 'Creating OS package kibana.yml', + + async run(config, log, build) { + await createOSPackageKibanaYML(config, build); + }, +}; export const CreateDebPackage: Task = { description: 'Creating deb package', diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker index c883e0b68114..0af087f1427d 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/base/bin/kibana-docker @@ -108,6 +108,7 @@ kibana_vars=( map.tilemap.options.subdomains map.tilemap.url migrations.batchSize + migrations.maxBatchSizeBytes migrations.enableV2 migrations.pollInterval migrations.retryAttempts diff --git a/src/dev/build/tasks/os_packages/run_fpm.ts b/src/dev/build/tasks/os_packages/run_fpm.ts index b732e4c80ea3..c7d9f6997cdf 100644 --- a/src/dev/build/tasks/os_packages/run_fpm.ts +++ b/src/dev/build/tasks/os_packages/run_fpm.ts @@ -123,6 +123,7 @@ export async function runFpm( `${resolveWithTrailingSlash(fromBuild('.'))}=/usr/share/kibana/`, // copy the config directory to /etc/kibana + `${config.resolveFromRepo('build/os_packages/config/kibana.yml')}=/etc/kibana/kibana.yml`, `${resolveWithTrailingSlash(fromBuild('config'))}=/etc/kibana/`, // copy the data directory at /var/lib/kibana diff --git a/src/dev/build/tasks/os_packages/service_templates/systemd/usr/lib/systemd/system/kibana.service b/src/dev/build/tasks/os_packages/service_templates/systemd/usr/lib/systemd/system/kibana.service index 7a1508d91b21..df33b82f1f96 100644 --- a/src/dev/build/tasks/os_packages/service_templates/systemd/usr/lib/systemd/system/kibana.service +++ b/src/dev/build/tasks/os_packages/service_templates/systemd/usr/lib/systemd/system/kibana.service @@ -15,7 +15,7 @@ Environment=KBN_PATH_CONF=/etc/kibana EnvironmentFile=-/etc/default/kibana EnvironmentFile=-/etc/sysconfig/kibana -ExecStart=/usr/share/kibana/bin/kibana --logging.dest="/var/log/kibana/kibana.log" --pid.file="/run/kibana/kibana.pid" +ExecStart=/usr/share/kibana/bin/kibana Restart=on-failure RestartSec=3 diff --git a/src/dev/build/tasks/package_json/find_used_dependencies.ts b/src/dev/build/tasks/package_json/find_used_dependencies.ts index 004e17b87ac8..8cb8b3c986de 100644 --- a/src/dev/build/tasks/package_json/find_used_dependencies.ts +++ b/src/dev/build/tasks/package_json/find_used_dependencies.ts @@ -29,9 +29,9 @@ export async function findUsedDependencies(listedPkgDependencies: any, baseDir: ]; const discoveredPluginEntries = await globby([ - normalize(Path.resolve(baseDir, `src/plugins/*/server/index.js`)), + normalize(Path.resolve(baseDir, `src/plugins/**/server/index.js`)), `!${normalize(Path.resolve(baseDir, `/src/plugins/**/public`))}`, - normalize(Path.resolve(baseDir, `x-pack/plugins/*/server/index.js`)), + normalize(Path.resolve(baseDir, `x-pack/plugins/**/server/index.js`)), `!${normalize(Path.resolve(baseDir, `/x-pack/plugins/**/public`))}`, ]); diff --git a/src/dev/build/tasks/patch_native_modules_task.ts b/src/dev/build/tasks/patch_native_modules_task.ts index 7fb4ffed6fb9..be2571c029f9 100644 --- a/src/dev/build/tasks/patch_native_modules_task.ts +++ b/src/dev/build/tasks/patch_native_modules_task.ts @@ -31,17 +31,17 @@ interface Package { const packages: Package[] = [ { name: 're2', - version: '1.15.4', + version: '1.16.0', destinationPath: 'node_modules/re2/build/Release/re2.node', extractMethod: 'gunzip', archives: { 'darwin-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/darwin-x64-83.gz', - sha256: 'b45cd8296fd6eb2a091399c20111af43093ba30c99ed9e5d969278f5ff69ba8f', + url: 'https://github.com/uhop/node-re2/releases/download/1.16.0/darwin-x64-83.gz', + sha256: 'ef49febcba972b488727ce329ea9d2b57590bb44001ed494f2aa1397c0ebc32b', }, 'linux-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/linux-x64-83.gz', - sha256: '1bbc3f90f0ba105772b37c04e3a718f69544b4df01dda00435c2b8e50b2ad0d9', + url: 'https://github.com/uhop/node-re2/releases/download/1.16.0/linux-x64-83.gz', + sha256: '160217dd83eb7093b758e905ce09cb45182864c7df858bf2525a68924a23c509', }, // ARM build is currently done manually as Github Actions used in upstream project @@ -50,17 +50,18 @@ const packages: Package[] = [ // From a AWS Graviton instance: // * checkout the node-re2 project, // * install Node using the same minor used by Kibana + // * git submodule update --init --recursive to download re2 // * npm install, which will also create a build // * gzip -c build/Release/re2.node > linux-arm64-83.gz // * upload to kibana-ci-proxy-cache bucket 'linux-arm64': { url: - 'https://storage.googleapis.com/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.15.4/linux-arm64-83.gz', - sha256: '4eb524ca9a79dea9c07342e487fbe91591166fdbc022ae987104840df948a4e9', + 'https://storage.googleapis.com/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.16.0/linux-arm64-83.gz', + sha256: '114505c60dbf57ad30556937ac5f49213c6676ad79d92706b96949d3a63f53b4', }, 'win32-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.15.4/win32-x64-83.gz', - sha256: 'efe939d3cda1d64ee3ee3e60a20613b95166d55632e702c670763ea7e69fca06', + url: 'https://github.com/uhop/node-re2/releases/download/1.16.0/win32-x64-83.gz', + sha256: '92ad420a6bfcedeb58dadf807a2f2901b05251d1edd3950051699929eda23073', }, }, }, diff --git a/src/plugins/advanced_settings/kibana.json b/src/plugins/advanced_settings/kibana.json index cf00241ee276..7562b6a66019 100644 --- a/src/plugins/advanced_settings/kibana.json +++ b/src/plugins/advanced_settings/kibana.json @@ -7,7 +7,7 @@ "optionalPlugins": ["home", "usageCollection"], "requiredBundles": ["kibanaReact", "kibanaUtils", "home", "esUiShared"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" } } diff --git a/src/plugins/chart_expressions/expression_tagcloud/kibana.json b/src/plugins/chart_expressions/expression_tagcloud/kibana.json index 26d5ef9750e6..b1c3c1f02036 100755 --- a/src/plugins/chart_expressions/expression_tagcloud/kibana.json +++ b/src/plugins/chart_expressions/expression_tagcloud/kibana.json @@ -8,8 +8,8 @@ "requiredBundles": ["kibanaUtils"], "optionalPlugins": [], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "description": "Expression Tagcloud plugin adds a `tagcloud` renderer and function to the expression plugin. The renderer will display the `Wordcloud` chart." } diff --git a/src/plugins/charts/kibana.json b/src/plugins/charts/kibana.json index 799173fed094..86971d1018e0 100644 --- a/src/plugins/charts/kibana.json +++ b/src/plugins/charts/kibana.json @@ -5,7 +5,7 @@ "ui": true, "requiredPlugins": ["expressions"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" } } diff --git a/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts b/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts index efe5c9b49849..8bc78956a091 100644 --- a/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts +++ b/src/plugins/charts/public/services/active_cursor/use_active_cursor.test.ts @@ -24,42 +24,47 @@ describe('useActiveCursor', () => { events: Array>, eventsTimeout = 1 ) => - new Promise(async (resolve) => { - const activeCursor = new ActiveCursor(); - let allEventsExecuted = false; - - activeCursor.setup(); + new Promise(async (resolve, reject) => { + try { + const activeCursor = new ActiveCursor(); + let allEventsExecuted = false; + activeCursor.setup(); + dispatchExternalPointerEvent.mockImplementation((pointerEvent) => { + if (allEventsExecuted) { + resolve(pointerEvent); + } + }); + renderHook(() => + useActiveCursor( + activeCursor, + { + current: { + dispatchExternalPointerEvent: dispatchExternalPointerEvent as ( + pointerEvent: PointerEvent + ) => void, + }, + } as RefObject, + { ...syncOption, debounce: syncOption.debounce ?? 1 } + ) + ); - dispatchExternalPointerEvent.mockImplementation((pointerEvent) => { - if (allEventsExecuted) { - resolve(pointerEvent); + for (const e of events) { + await new Promise((eventResolve) => + setTimeout(() => { + if (e === events[events.length - 1]) { + allEventsExecuted = true; + } + + activeCursor.activeCursor$!.next({ + cursor, + ...e, + }); + eventResolve(null); + }, eventsTimeout) + ); } - }); - - renderHook(() => - useActiveCursor( - activeCursor, - { - current: { - dispatchExternalPointerEvent: dispatchExternalPointerEvent as ( - pointerEvent: PointerEvent - ) => void, - }, - } as RefObject, - { ...syncOption, debounce: syncOption.debounce ?? 1 } - ) - ); - - for (const e of events) { - await new Promise((eventResolve) => - setTimeout(() => { - if (e === events[events.length - 1]) { - allEventsExecuted = true; - } - activeCursor.activeCursor$!.next({ cursor, ...e }); - eventResolve(null); - }, eventsTimeout) - ); + } catch (error) { + reject(error); } }); diff --git a/src/plugins/console/kibana.json b/src/plugins/console/kibana.json index 9452f43647a1..69c7176ff6a4 100644 --- a/src/plugins/console/kibana.json +++ b/src/plugins/console/kibana.json @@ -7,7 +7,7 @@ "name": "Stack Management", "githubTeam": "kibana-stack-management" }, - "requiredPlugins": ["devTools"], + "requiredPlugins": ["devTools", "share"], "optionalPlugins": ["usageCollection", "home"], "requiredBundles": ["esUiShared", "kibanaReact", "kibanaUtils", "home"] } diff --git a/src/plugins/console/public/index.ts b/src/plugins/console/public/index.ts index 767673f45e02..8c4a10710856 100644 --- a/src/plugins/console/public/index.ts +++ b/src/plugins/console/public/index.ts @@ -10,6 +10,8 @@ import './index.scss'; import { ConsoleUIPlugin } from './plugin'; +export type { ConsoleUILocatorParams } from './plugin'; + export { ConsoleUIPlugin as Plugin }; export function plugin() { diff --git a/src/plugins/console/public/plugin.ts b/src/plugins/console/public/plugin.ts index d791e4e2231e..e3791df6a2db 100644 --- a/src/plugins/console/public/plugin.ts +++ b/src/plugins/console/public/plugin.ts @@ -7,15 +7,20 @@ */ import { i18n } from '@kbn/i18n'; +import { SerializableRecord } from '@kbn/utility-types'; import { Plugin, CoreSetup } from 'src/core/public'; import { FeatureCatalogueCategory } from '../../home/public'; import { AppSetupUIPluginDependencies } from './types'; +export interface ConsoleUILocatorParams extends SerializableRecord { + loadFrom?: string; +} + export class ConsoleUIPlugin implements Plugin { public setup( { notifications, getStartServices, http }: CoreSetup, - { devTools, home, usageCollection }: AppSetupUIPluginDependencies + { devTools, home, share, usageCollection }: AppSetupUIPluginDependencies ) { if (home) { home.featureCatalogue.register({ @@ -60,6 +65,19 @@ export class ConsoleUIPlugin implements Plugin({ + id: 'CONSOLE_APP_LOCATOR', + getLocation: async ({ loadFrom }) => { + return { + app: 'dev_tools', + path: `#/console${loadFrom ? `?load_from=${loadFrom}` : ''}`, + state: { loadFrom }, + }; + }, + }); + + return { locator }; } public start() {} diff --git a/src/plugins/console/public/types/plugin_dependencies.ts b/src/plugins/console/public/types/plugin_dependencies.ts index b4508a83b09b..444776f47ea1 100644 --- a/src/plugins/console/public/types/plugin_dependencies.ts +++ b/src/plugins/console/public/types/plugin_dependencies.ts @@ -9,9 +9,11 @@ import { HomePublicPluginSetup } from '../../../home/public'; import { DevToolsSetup } from '../../../dev_tools/public'; import { UsageCollectionSetup } from '../../../usage_collection/public'; +import { SharePluginSetup } from '../../../share/public'; export interface AppSetupUIPluginDependencies { home?: HomePublicPluginSetup; devTools: DevToolsSetup; + share: SharePluginSetup; usageCollection?: UsageCollectionSetup; } diff --git a/src/plugins/console/tsconfig.json b/src/plugins/console/tsconfig.json index ee6fbfebc77a..1597ce812edc 100644 --- a/src/plugins/console/tsconfig.json +++ b/src/plugins/console/tsconfig.json @@ -14,6 +14,7 @@ { "path": "../home/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, { "path": "../kibana_utils/tsconfig.json" }, - { "path": "../usage_collection/tsconfig.json" }, + { "path": "../share/tsconfig.json" }, + { "path": "../usage_collection/tsconfig.json" } ] } diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index f08cc273a00a..2c339d140823 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -10,7 +10,13 @@ export const DEFAULT_QUERY_LANGUAGE = 'kuery'; export const KIBANA_USER_QUERY_LANGUAGE_KEY = 'kibana.userQueryLanguage'; /** @public **/ -export const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; +export const DATA_VIEW_SAVED_OBJECT_TYPE = 'index-pattern'; + +/** + * @deprecated Use DATA_VIEW_SAVED_OBJECT_TYPE. All index pattern interfaces were renamed. + */ + +export const INDEX_PATTERN_SAVED_OBJECT_TYPE = DATA_VIEW_SAVED_OBJECT_TYPE; export type ValueSuggestionsMethod = 'terms_enum' | 'terms_agg'; diff --git a/src/plugins/data/common/index.ts b/src/plugins/data/common/index.ts index 2bc383db6f53..a36788f94939 100644 --- a/src/plugins/data/common/index.ts +++ b/src/plugins/data/common/index.ts @@ -22,4 +22,4 @@ export * from './exports'; * @removeBy 8.1 */ -export { IndexPatternAttributes } from './types'; +export { IndexPatternAttributes, DataViewAttributes } from './types'; diff --git a/src/plugins/data/common/index_patterns/errors/duplicate_index_pattern.ts b/src/plugins/data/common/index_patterns/errors/duplicate_index_pattern.ts index 6c059dc44a19..d35b09e39aa7 100644 --- a/src/plugins/data/common/index_patterns/errors/duplicate_index_pattern.ts +++ b/src/plugins/data/common/index_patterns/errors/duplicate_index_pattern.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -export class DuplicateIndexPatternError extends Error { +export class DuplicateDataViewError extends Error { constructor(message: string) { super(message); this.name = 'DuplicateIndexPatternError'; diff --git a/src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts b/src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts index 1c50f0704910..8fe9e40e0ac6 100644 --- a/src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts +++ b/src/plugins/data/common/index_patterns/expressions/load_index_pattern.ts @@ -8,8 +8,8 @@ import { i18n } from '@kbn/i18n'; import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -import { IndexPatternsContract } from '../index_patterns'; -import { IndexPatternSpec } from '..'; +import { DataViewsContract } from '../index_patterns'; +import { DataViewSpec } from '..'; import { SavedObjectReference } from '../../../../../core/types'; const name = 'indexPatternLoad'; @@ -17,7 +17,7 @@ const type = 'index_pattern'; export interface IndexPatternExpressionType { type: typeof type; - value: IndexPatternSpec; + value: DataViewSpec; } type Input = null; @@ -29,7 +29,7 @@ interface Arguments { /** @internal */ export interface IndexPatternLoadStartDependencies { - indexPatterns: IndexPatternsContract; + indexPatterns: DataViewsContract; } export type IndexPatternLoadExpressionFunctionDefinition = ExpressionFunctionDefinition< diff --git a/src/plugins/data/common/index_patterns/fields/field_list.ts b/src/plugins/data/common/index_patterns/fields/field_list.ts index 40503e6dbc1b..78cc390c8c13 100644 --- a/src/plugins/data/common/index_patterns/fields/field_list.ts +++ b/src/plugins/data/common/index_patterns/fields/field_list.ts @@ -8,24 +8,22 @@ import { findIndex } from 'lodash'; import { IFieldType } from './types'; -import { IndexPatternField } from './index_pattern_field'; -import { FieldSpec, IndexPatternFieldMap } from '../types'; -import { IndexPattern } from '../index_patterns'; +import { DataViewField } from './index_pattern_field'; +import { FieldSpec, DataViewFieldMap } from '../types'; +import { DataView } from '../index_patterns'; -type FieldMap = Map; +type FieldMap = Map; -export interface IIndexPatternFieldList extends Array { +export interface IIndexPatternFieldList extends Array { add(field: FieldSpec): void; - getAll(): IndexPatternField[]; - getByName(name: IndexPatternField['name']): IndexPatternField | undefined; - getByType(type: IndexPatternField['type']): IndexPatternField[]; + getAll(): DataViewField[]; + getByName(name: DataViewField['name']): DataViewField | undefined; + getByType(type: DataViewField['type']): DataViewField[]; remove(field: IFieldType): void; removeAll(): void; replaceAll(specs: FieldSpec[]): void; update(field: FieldSpec): void; - toSpec(options?: { - getFormatterForField?: IndexPattern['getFormatterForField']; - }): IndexPatternFieldMap; + toSpec(options?: { getFormatterForField?: DataView['getFormatterForField'] }): DataViewFieldMap; } // extending the array class and using a constructor doesn't work well @@ -35,11 +33,11 @@ export const fieldList = ( specs: FieldSpec[] = [], shortDotsEnable = false ): IIndexPatternFieldList => { - class FldList extends Array implements IIndexPatternFieldList { + class FldList extends Array implements IIndexPatternFieldList { private byName: FieldMap = new Map(); - private groups: Map = new Map(); - private setByName = (field: IndexPatternField) => this.byName.set(field.name, field); - private setByGroup = (field: IndexPatternField) => { + private groups: Map = new Map(); + private setByName = (field: DataViewField) => this.byName.set(field.name, field); + private setByGroup = (field: DataViewField) => { if (typeof this.groups.get(field.type) === 'undefined') { this.groups.set(field.type, new Map()); } @@ -53,12 +51,12 @@ export const fieldList = ( } public readonly getAll = () => [...this.byName.values()]; - public readonly getByName = (name: IndexPatternField['name']) => this.byName.get(name); - public readonly getByType = (type: IndexPatternField['type']) => [ + public readonly getByName = (name: DataViewField['name']) => this.byName.get(name); + public readonly getByType = (type: DataViewField['type']) => [ ...(this.groups.get(type) || new Map()).values(), ]; public readonly add = (field: FieldSpec) => { - const newField = new IndexPatternField({ ...field, shortDotsEnable }); + const newField = new DataViewField({ ...field, shortDotsEnable }); this.push(newField); this.setByName(newField); this.setByGroup(newField); @@ -73,7 +71,7 @@ export const fieldList = ( }; public readonly update = (field: FieldSpec) => { - const newField = new IndexPatternField(field); + const newField = new DataViewField(field); const index = this.findIndex((f) => f.name === newField.name); this.splice(index, 1, newField); this.setByName(newField); @@ -95,10 +93,10 @@ export const fieldList = ( public toSpec({ getFormatterForField, }: { - getFormatterForField?: IndexPattern['getFormatterForField']; + getFormatterForField?: DataView['getFormatterForField']; } = {}) { return { - ...this.reduce((collector, field) => { + ...this.reduce((collector, field) => { collector[field.name] = field.toSpec({ getFormatterForField }); return collector; }, {}), diff --git a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts index 0c7a668087da..fae0e14b95c0 100644 --- a/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts +++ b/src/plugins/data/common/index_patterns/fields/index_pattern_field.ts @@ -6,15 +6,17 @@ * Side Public License, v 1. */ +/* eslint-disable max-classes-per-file */ + +import { KbnFieldType, getKbnFieldType, castEsToKbnFieldTypeName } from '@kbn/field-types'; import type { RuntimeField } from '../types'; -import { KbnFieldType, getKbnFieldType, castEsToKbnFieldTypeName } from '../../kbn_field_types'; import { KBN_FIELD_TYPES } from '../../kbn_field_types/types'; import type { IFieldType } from './types'; -import { FieldSpec, IndexPattern } from '../..'; +import { FieldSpec, DataView } from '../..'; import { shortenDottedString } from '../../utils'; /** @public */ -export class IndexPatternField implements IFieldType { +export class DataViewField implements IFieldType { readonly spec: FieldSpec; // not writable or serialized private readonly kbnFieldType: KbnFieldType; @@ -182,7 +184,7 @@ export class IndexPatternField implements IFieldType { public toSpec({ getFormatterForField, }: { - getFormatterForField?: IndexPattern['getFormatterForField']; + getFormatterForField?: DataView['getFormatterForField']; } = {}): FieldSpec { return { count: this.count, @@ -205,3 +207,8 @@ export class IndexPatternField implements IFieldType { }; } } + +/** + * @deprecated Use DataViewField instead. All index pattern interfaces were renamed. + */ +export class IndexPatternField extends DataViewField {} diff --git a/src/plugins/data/common/index_patterns/fields/types.ts b/src/plugins/data/common/index_patterns/fields/types.ts index 8c8413f6894b..2c5934a8e7b3 100644 --- a/src/plugins/data/common/index_patterns/fields/types.ts +++ b/src/plugins/data/common/index_patterns/fields/types.ts @@ -5,14 +5,14 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -import { IndexPatternFieldBase } from '@kbn/es-query'; -import { FieldSpec, IndexPattern } from '../..'; +import { DataViewFieldBase } from '@kbn/es-query'; +import { FieldSpec, DataView } from '../..'; /** * @deprecated Use {@link IndexPatternField} * @removeBy 8.1 */ -export interface IFieldType extends IndexPatternFieldBase { +export interface IFieldType extends DataViewFieldBase { count?: number; // esTypes might be undefined on old index patterns that have not been refreshed since we added // this prop. It is also undefined on scripted fields. @@ -26,5 +26,5 @@ export interface IFieldType extends IndexPatternFieldBase { displayName?: string; customLabel?: string; format?: any; - toSpec?: (options?: { getFormatterForField?: IndexPattern['getFormatterForField'] }) => FieldSpec; + toSpec?: (options?: { getFormatterForField?: DataView['getFormatterForField'] }) => FieldSpec; } diff --git a/src/plugins/data/common/index_patterns/fields/utils.ts b/src/plugins/data/common/index_patterns/fields/utils.ts index 1ec59b0a2ce0..9e05bebc746f 100644 --- a/src/plugins/data/common/index_patterns/fields/utils.ts +++ b/src/plugins/data/common/index_patterns/fields/utils.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { getFilterableKbnTypeNames } from '../../kbn_field_types'; +import { getFilterableKbnTypeNames } from '@kbn/field-types'; import { IFieldType } from './types'; const filterableTypes = getFilterableKbnTypeNames(); diff --git a/src/plugins/data/common/index_patterns/index.ts b/src/plugins/data/common/index_patterns/index.ts index f493b417b47e..99639676bdab 100644 --- a/src/plugins/data/common/index_patterns/index.ts +++ b/src/plugins/data/common/index_patterns/index.ts @@ -9,7 +9,17 @@ export * from './constants'; export * from './fields'; export * from './types'; -export { IndexPatternsService, IndexPatternsContract } from './index_patterns'; -export type { IndexPattern, IndexPatternListItem } from './index_patterns'; +export { + IndexPatternsService, + IndexPatternsContract, + DataViewsService, + DataViewsContract, +} from './index_patterns'; +export type { + IndexPattern, + IndexPatternListItem, + DataView, + DataViewListItem, +} from './index_patterns'; export * from './errors'; export * from './expressions'; diff --git a/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts b/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts index a58a349a4697..a647f306ca7f 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/_pattern_cache.ts @@ -6,18 +6,18 @@ * Side Public License, v 1. */ -import { IndexPattern } from './index_pattern'; +import { DataView } from './index_pattern'; -export interface PatternCache { - get: (id: string) => Promise | undefined; - set: (id: string, value: Promise) => Promise; +export interface DataViewCache { + get: (id: string) => Promise | undefined; + set: (id: string, value: Promise) => Promise; clear: (id: string) => void; clearAll: () => void; } -export function createIndexPatternCache(): PatternCache { +export function createDataViewCache(): DataViewCache { const vals: Record = {}; - const cache: PatternCache = { + const cache: DataViewCache = { get: (id: string) => { return vals[id]; }, diff --git a/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts index 61ec1c5a4c09..da20053bf1fe 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/ensure_default_index_pattern.ts @@ -7,12 +7,12 @@ */ import { includes } from 'lodash'; -import { IndexPatternsContract } from './index_patterns'; +import { DataViewsContract } from './index_patterns'; import { UiSettingsCommon } from '../types'; -export type EnsureDefaultIndexPattern = () => Promise | undefined; +export type EnsureDefaultDataView = () => Promise | undefined; -export const createEnsureDefaultIndexPattern = ( +export const createEnsureDefaultDataView = ( uiSettings: UiSettingsCommon, onRedirectNoIndexPattern: () => Promise | void ) => { @@ -20,7 +20,7 @@ export const createEnsureDefaultIndexPattern = ( * Checks whether a default index pattern is set and exists and defines * one otherwise. */ - return async function ensureDefaultIndexPattern(this: IndexPatternsContract) { + return async function ensureDefaultDataView(this: DataViewsContract) { const patterns = await this.getIds(); let defaultId = await uiSettings.get('defaultIndex'); let defined = !!defaultId; diff --git a/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts index 7cd88c8a87c1..bcdcca3a4daa 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/flatten_hit.ts @@ -7,12 +7,12 @@ */ import _ from 'lodash'; -import { IndexPattern } from './index_pattern'; +import { DataView } from './index_pattern'; // Takes a hit, merges it with any stored/scripted fields, and with the metaFields // returns a flattened version -function flattenHit(indexPattern: IndexPattern, hit: Record, deep: boolean) { +function flattenHit(indexPattern: DataView, hit: Record, deep: boolean) { const flat = {} as Record; // recursively merge _source @@ -104,11 +104,7 @@ function decorateFlattenedWrapper(hit: Record, metaFields: Record, deep = false) { const decorateFlattened = decorateFlattenedWrapper(hit, metaFields); const cached = cache.get(hit); diff --git a/src/plugins/data/common/index_patterns/index_patterns/format_hit.ts b/src/plugins/data/common/index_patterns/index_patterns/format_hit.ts index fe872ae92989..9ae630e8de65 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/format_hit.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/format_hit.ts @@ -7,7 +7,7 @@ */ import _ from 'lodash'; -import { IndexPattern } from './index_pattern'; +import { DataView } from './index_pattern'; import { FieldFormatsContentType } from '../../../../field_formats/common'; const formattedCache = new WeakMap(); @@ -15,7 +15,7 @@ const partialFormattedCache = new WeakMap(); // Takes a hit, merges it with any stored/scripted fields, and with the metaFields // returns a formatted version -export function formatHitProvider(indexPattern: IndexPattern, defaultFormat: any) { +export function formatHitProvider(indexPattern: DataView, defaultFormat: any) { function convert( hit: Record, val: any, diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 48bcdf6982b6..e08d1e62bae0 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -6,22 +6,24 @@ * Side Public License, v 1. */ +/* eslint-disable max-classes-per-file */ + import _, { each, reject } from 'lodash'; -import { FieldAttrs, FieldAttrSet, IndexPatternAttributes } from '../..'; +import { castEsToKbnFieldTypeName } from '@kbn/field-types'; +import { FieldAttrs, FieldAttrSet, DataViewAttributes } from '../..'; import type { RuntimeField } from '../types'; import { DuplicateField } from '../../../../kibana_utils/common'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES, IIndexPattern, IFieldType } from '../../../common'; -import { IndexPatternField, IIndexPatternFieldList, fieldList } from '../fields'; +import { DataViewField, IIndexPatternFieldList, fieldList } from '../fields'; import { formatHitProvider } from './format_hit'; import { flattenHitWrapper } from './flatten_hit'; import { FieldFormatsStartCommon, FieldFormat } from '../../../../field_formats/common'; -import { IndexPatternSpec, TypeMeta, SourceFilter, IndexPatternFieldMap } from '../types'; +import { DataViewSpec, TypeMeta, SourceFilter, DataViewFieldMap } from '../types'; import { SerializedFieldFormat } from '../../../../expressions/common'; -import { castEsToKbnFieldTypeName } from '../../kbn_field_types'; -interface IndexPatternDeps { - spec?: IndexPatternSpec; +interface DataViewDeps { + spec?: DataViewSpec; fieldFormats: FieldFormatsStartCommon; shortDotsEnable?: boolean; metaFields?: string[]; @@ -41,7 +43,7 @@ interface SavedObjectBody { type FormatFieldFn = (hit: Record, fieldName: string) => any; -export class IndexPattern implements IIndexPattern { +export class DataView implements IIndexPattern { public id?: string; public title: string = ''; public fieldFormatMap: Record; @@ -49,7 +51,7 @@ export class IndexPattern implements IIndexPattern { * Only used by rollup indices, used by rollup specific endpoint to load field list */ public typeMeta?: TypeMeta; - public fields: IIndexPatternFieldList & { toSpec: () => IndexPatternFieldMap }; + public fields: IIndexPatternFieldList & { toSpec: () => DataViewFieldMap }; public timeFieldName: string | undefined; /** * @deprecated Used by time range index patterns @@ -84,12 +86,7 @@ export class IndexPattern implements IIndexPattern { */ public readonly allowNoIndex: boolean = false; - constructor({ - spec = {}, - fieldFormats, - shortDotsEnable = false, - metaFields = [], - }: IndexPatternDeps) { + constructor({ spec = {}, fieldFormats, shortDotsEnable = false, metaFields = [] }: DataViewDeps) { // set dependencies this.fieldFormats = fieldFormats; // set config @@ -206,7 +203,7 @@ export class IndexPattern implements IIndexPattern { /** * Create static representation of index pattern */ - public toSpec(): IndexPatternSpec { + public toSpec(): DataViewSpec { return { id: this.id, version: this.version, @@ -311,7 +308,7 @@ export class IndexPattern implements IIndexPattern { return this.fields.getByName(this.timeFieldName); } - getFieldByName(name: string): IndexPatternField | undefined { + getFieldByName(name: string): DataViewField | undefined { if (!this.fields || !this.fields.getByName) return undefined; return this.fields.getByName(name); } @@ -323,7 +320,7 @@ export class IndexPattern implements IIndexPattern { /** * Returns index pattern as saved object body for saving */ - getAsSavedObjectBody(): IndexPatternAttributes { + getAsSavedObjectBody(): DataViewAttributes { const fieldFormatMap = _.isEmpty(this.fieldFormatMap) ? undefined : JSON.stringify(this.fieldFormatMap); @@ -349,9 +346,7 @@ export class IndexPattern implements IIndexPattern { * Provide a field, get its formatter * @param field */ - getFormatterForField( - field: IndexPatternField | IndexPatternField['spec'] | IFieldType - ): FieldFormat { + getFormatterForField(field: DataViewField | DataViewField['spec'] | IFieldType): FieldFormat { const fieldFormat = this.getFormatterForFieldNoDefault(field.name); if (fieldFormat) { return fieldFormat; @@ -490,3 +485,8 @@ export class IndexPattern implements IIndexPattern { delete this.fieldFormatMap[fieldName]; }; } + +/** + * @deprecated Use DataView instead. All index pattern interfaces were renamed. + */ +export class IndexPattern extends DataView {} diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index d255abc52aac..5f389d36e3bb 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -7,7 +7,7 @@ */ import { defaults } from 'lodash'; -import { IndexPatternsService, IndexPattern } from '.'; +import { DataViewsService, DataView } from '.'; import { fieldFormatsMock } from '../../../../field_formats/common/mocks'; import { UiSettingsCommon, SavedObjectsClientCommon, SavedObject } from '../types'; @@ -47,7 +47,7 @@ const savedObject = { }; describe('IndexPatterns', () => { - let indexPatterns: IndexPatternsService; + let indexPatterns: DataViewsService; let savedObjectsClient: SavedObjectsClientCommon; let SOClientGetDelay = 0; @@ -85,7 +85,7 @@ describe('IndexPatterns', () => { }; }); - indexPatterns = new IndexPatternsService({ + indexPatterns = new DataViewsService({ uiSettings: ({ get: () => Promise.resolve(false), getAll: () => {}, @@ -207,7 +207,7 @@ describe('IndexPatterns', () => { indexPatterns.refreshFields = jest.fn(); const indexPattern = await indexPatterns.create({ title }, true); - expect(indexPattern).toBeInstanceOf(IndexPattern); + expect(indexPattern).toBeInstanceOf(DataView); expect(indexPattern.title).toBe(title); expect(indexPatterns.refreshFields).not.toBeCalled(); @@ -235,7 +235,7 @@ describe('IndexPatterns', () => { indexPatterns.createSavedObject = jest.fn(() => Promise.resolve(({ id: 'id', - } as unknown) as IndexPattern) + } as unknown) as DataView) ); indexPatterns.setDefault = jest.fn(); await indexPatterns.createAndSave({ title }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index 74f11badbb41..a72224d1c3fe 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -6,78 +6,80 @@ * Side Public License, v 1. */ +/* eslint-disable max-classes-per-file */ + import { i18n } from '@kbn/i18n'; import { PublicMethodsOf } from '@kbn/utility-types'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE, SavedObjectsClientCommon } from '../..'; +import { castEsToKbnFieldTypeName } from '@kbn/field-types'; +import { DATA_VIEW_SAVED_OBJECT_TYPE, SavedObjectsClientCommon } from '../..'; -import { createIndexPatternCache } from '.'; +import { createDataViewCache } from '.'; import type { RuntimeField } from '../types'; -import { IndexPattern } from './index_pattern'; -import { - createEnsureDefaultIndexPattern, - EnsureDefaultIndexPattern, -} from './ensure_default_index_pattern'; +import { DataView } from './index_pattern'; +import { createEnsureDefaultDataView, EnsureDefaultDataView } from './ensure_default_index_pattern'; import { OnNotification, OnError, UiSettingsCommon, - IIndexPatternsApiClient, + IDataViewsApiClient, GetFieldsOptions, - IndexPatternSpec, - IndexPatternAttributes, + DataViewSpec, + DataViewAttributes, FieldAttrs, FieldSpec, - IndexPatternFieldMap, + DataViewFieldMap, TypeMeta, } from '../types'; import { FieldFormatsStartCommon, FORMATS_UI_SETTINGS } from '../../../../field_formats/common/'; import { UI_SETTINGS, SavedObject } from '../../../common'; import { SavedObjectNotFound } from '../../../../kibana_utils/common'; -import { IndexPatternMissingIndices } from '../lib'; +import { DataViewMissingIndices } from '../lib'; import { findByTitle } from '../utils'; -import { DuplicateIndexPatternError } from '../errors'; -import { castEsToKbnFieldTypeName } from '../../kbn_field_types'; +import { DuplicateDataViewError } from '../errors'; const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; -export type IndexPatternSavedObjectAttrs = Pick< - IndexPatternAttributes, - 'title' | 'type' | 'typeMeta' ->; +export type IndexPatternSavedObjectAttrs = Pick; export type IndexPatternListSavedObjectAttrs = Pick< - IndexPatternAttributes, + DataViewAttributes, 'title' | 'type' | 'typeMeta' >; -export interface IndexPatternListItem { +export interface DataViewListItem { id: string; title: string; type?: string; typeMeta?: TypeMeta; } +/** + * @deprecated Use DataViewListItem. All index pattern interfaces were renamed. + */ + +export type IndexPatternListItem = DataViewListItem; + interface IndexPatternsServiceDeps { uiSettings: UiSettingsCommon; savedObjectsClient: SavedObjectsClientCommon; - apiClient: IIndexPatternsApiClient; + apiClient: IDataViewsApiClient; fieldFormats: FieldFormatsStartCommon; onNotification: OnNotification; onError: OnError; onRedirectNoIndexPattern?: () => void; } -export class IndexPatternsService { +export class DataViewsService { private config: UiSettingsCommon; private savedObjectsClient: SavedObjectsClientCommon; private savedObjectsCache?: Array> | null; - private apiClient: IIndexPatternsApiClient; + private apiClient: IDataViewsApiClient; private fieldFormats: FieldFormatsStartCommon; private onNotification: OnNotification; private onError: OnError; - private indexPatternCache: ReturnType; + private indexPatternCache: ReturnType; - ensureDefaultIndexPattern: EnsureDefaultIndexPattern; + ensureDefaultIndexPattern: EnsureDefaultDataView; constructor({ uiSettings, @@ -94,12 +96,12 @@ export class IndexPatternsService { this.fieldFormats = fieldFormats; this.onNotification = onNotification; this.onError = onError; - this.ensureDefaultIndexPattern = createEnsureDefaultIndexPattern( + this.ensureDefaultIndexPattern = createEnsureDefaultDataView( uiSettings, onRedirectNoIndexPattern ); - this.indexPatternCache = createIndexPatternCache(); + this.indexPatternCache = createDataViewCache(); } /** @@ -107,7 +109,7 @@ export class IndexPatternsService { */ private async refreshSavedObjectsCache() { const so = await this.savedObjectsClient.find({ - type: INDEX_PATTERN_SAVED_OBJECT_TYPE, + type: DATA_VIEW_SAVED_OBJECT_TYPE, fields: ['title', 'type', 'typeMeta'], perPage: 10000, }); @@ -148,9 +150,9 @@ export class IndexPatternsService { * @param size * @returns IndexPattern[] */ - find = async (search: string, size: number = 10): Promise => { + find = async (search: string, size: number = 10): Promise => { const savedObjects = await this.savedObjectsClient.find({ - type: INDEX_PATTERN_SAVED_OBJECT_TYPE, + type: DATA_VIEW_SAVED_OBJECT_TYPE, fields: ['title'], search, searchFields: ['title'], @@ -261,7 +263,7 @@ export class IndexPatternsService { * @returns FieldSpec[] */ getFieldsForIndexPattern = async ( - indexPattern: IndexPattern | IndexPatternSpec, + indexPattern: DataView | DataViewSpec, options?: GetFieldsOptions ) => this.getFieldsForWildcard({ @@ -275,7 +277,7 @@ export class IndexPatternsService { * Refresh field list for a given index pattern * @param indexPattern */ - refreshFields = async (indexPattern: IndexPattern) => { + refreshFields = async (indexPattern: DataView) => { try { const fields = (await this.getFieldsForIndexPattern(indexPattern)) as FieldSpec[]; fields.forEach((field) => (field.isMapped = true)); @@ -286,7 +288,7 @@ export class IndexPatternsService { ); indexPattern.fields.replaceAll(fieldsWithSavedAttrs); } catch (err) { - if (err instanceof IndexPatternMissingIndices) { + if (err instanceof DataViewMissingIndices) { this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); } @@ -308,7 +310,7 @@ export class IndexPatternsService { * @returns Record */ private refreshFieldSpecMap = async ( - fields: IndexPatternFieldMap, + fields: DataViewFieldMap, id: string, title: string, options: GetFieldsOptions, @@ -331,7 +333,7 @@ export class IndexPatternsService { return this.fieldArrayToMap(updatedFieldList, fieldAttrs); } catch (err) { - if (err instanceof IndexPatternMissingIndices) { + if (err instanceof DataViewMissingIndices) { this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); return {}; } @@ -353,7 +355,7 @@ export class IndexPatternsService { * @returns Record */ fieldArrayToMap = (fields: FieldSpec[], fieldAttrs?: FieldAttrs) => - fields.reduce((collector, field) => { + fields.reduce((collector, field) => { collector[field.name] = { ...field, customLabel: fieldAttrs?.[field.name]?.customLabel, @@ -368,7 +370,7 @@ export class IndexPatternsService { * @returns IndexPatternSpec */ - savedObjectToSpec = (savedObject: SavedObject): IndexPatternSpec => { + savedObjectToSpec = (savedObject: SavedObject): DataViewSpec => { const { id, version, @@ -413,15 +415,15 @@ export class IndexPatternsService { }; }; - private getSavedObjectAndInit = async (id: string): Promise => { - const savedObject = await this.savedObjectsClient.get( - INDEX_PATTERN_SAVED_OBJECT_TYPE, + private getSavedObjectAndInit = async (id: string): Promise => { + const savedObject = await this.savedObjectsClient.get( + DATA_VIEW_SAVED_OBJECT_TYPE, id ); if (!savedObject.version) { throw new SavedObjectNotFound( - INDEX_PATTERN_SAVED_OBJECT_TYPE, + DATA_VIEW_SAVED_OBJECT_TYPE, id, 'management/kibana/indexPatterns' ); @@ -431,8 +433,8 @@ export class IndexPatternsService { }; private initFromSavedObject = async ( - savedObject: SavedObject - ): Promise => { + savedObject: SavedObject + ): Promise => { const spec = this.savedObjectToSpec(savedObject); const { title, type, typeMeta, runtimeFieldMap } = spec; spec.fieldAttrs = savedObject.attributes.fieldAttrs @@ -471,7 +473,7 @@ export class IndexPatternsService { } } } catch (err) { - if (err instanceof IndexPatternMissingIndices) { + if (err instanceof DataViewMissingIndices) { this.onNotification({ title: (err as any).message, color: 'danger', @@ -501,7 +503,7 @@ export class IndexPatternsService { * @param id */ - get = async (id: string): Promise => { + get = async (id: string): Promise => { const indexPatternPromise = this.indexPatternCache.get(id) || this.indexPatternCache.set(id, this.getSavedObjectAndInit(id)); @@ -520,11 +522,11 @@ export class IndexPatternsService { * @param skipFetchFields * @returns IndexPattern */ - async create(spec: IndexPatternSpec, skipFetchFields = false): Promise { + async create(spec: DataViewSpec, skipFetchFields = false): Promise { const shortDotsEnable = await this.config.get(FORMATS_UI_SETTINGS.SHORT_DOTS_ENABLE); const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); - const indexPattern = new IndexPattern({ + const indexPattern = new DataView({ spec, fieldFormats: this.fieldFormats, shortDotsEnable, @@ -545,7 +547,7 @@ export class IndexPatternsService { * @param skipFetchFields Whether to skip field refresh step. */ - async createAndSave(spec: IndexPatternSpec, override = false, skipFetchFields = false) { + async createAndSave(spec: DataViewSpec, override = false, skipFetchFields = false) { const indexPattern = await this.create(spec, skipFetchFields); const createdIndexPattern = await this.createSavedObject(indexPattern, override); await this.setDefault(createdIndexPattern.id!); @@ -558,24 +560,24 @@ export class IndexPatternsService { * @param override Overwrite if existing index pattern exists */ - async createSavedObject(indexPattern: IndexPattern, override = false) { + async createSavedObject(indexPattern: DataView, override = false) { const dupe = await findByTitle(this.savedObjectsClient, indexPattern.title); if (dupe) { if (override) { await this.delete(dupe.id); } else { - throw new DuplicateIndexPatternError(`Duplicate index pattern: ${indexPattern.title}`); + throw new DuplicateDataViewError(`Duplicate index pattern: ${indexPattern.title}`); } } const body = indexPattern.getAsSavedObjectBody(); - const response: SavedObject = (await this.savedObjectsClient.create( - INDEX_PATTERN_SAVED_OBJECT_TYPE, + const response: SavedObject = (await this.savedObjectsClient.create( + DATA_VIEW_SAVED_OBJECT_TYPE, body, { id: indexPattern.id, } - )) as SavedObject; + )) as SavedObject; const createdIndexPattern = await this.initFromSavedObject(response); this.indexPatternCache.set(createdIndexPattern.id!, Promise.resolve(createdIndexPattern)); @@ -592,7 +594,7 @@ export class IndexPatternsService { */ async updateSavedObject( - indexPattern: IndexPattern, + indexPattern: DataView, saveAttempts: number = 0, ignoreErrors: boolean = false ): Promise { @@ -611,7 +613,7 @@ export class IndexPatternsService { }); return this.savedObjectsClient - .update(INDEX_PATTERN_SAVED_OBJECT_TYPE, indexPattern.id, body, { + .update(DATA_VIEW_SAVED_OBJECT_TYPE, indexPattern.id, body, { version: indexPattern.version, }) .then((resp) => { @@ -681,8 +683,18 @@ export class IndexPatternsService { */ async delete(indexPatternId: string) { this.indexPatternCache.clear(indexPatternId); - return this.savedObjectsClient.delete(INDEX_PATTERN_SAVED_OBJECT_TYPE, indexPatternId); + return this.savedObjectsClient.delete(DATA_VIEW_SAVED_OBJECT_TYPE, indexPatternId); } } -export type IndexPatternsContract = PublicMethodsOf; +/** + * @deprecated Use DataViewsService. All index pattern interfaces were renamed. + */ +export class IndexPatternsService extends DataViewsService {} + +export type DataViewsContract = PublicMethodsOf; + +/** + * @deprecated Use DataViewsContract. All index pattern interfaces were renamed. + */ +export type IndexPatternsContract = DataViewsContract; diff --git a/src/plugins/data/common/index_patterns/lib/errors.ts b/src/plugins/data/common/index_patterns/lib/errors.ts index 7a339e381def..20f422c5124e 100644 --- a/src/plugins/data/common/index_patterns/lib/errors.ts +++ b/src/plugins/data/common/index_patterns/lib/errors.ts @@ -13,7 +13,7 @@ import { KbnError } from '../../../../kibana_utils/common/'; /** * Tried to call a method that relies on SearchSource having an indexPattern assigned */ -export class IndexPatternMissingIndices extends KbnError { +export class DataViewMissingIndices extends KbnError { constructor(message: string) { const defaultMessage = "IndexPattern's configured pattern does not match any indices"; diff --git a/src/plugins/data/common/index_patterns/lib/get_title.ts b/src/plugins/data/common/index_patterns/lib/get_title.ts index 69afad486a74..efebbc302f22 100644 --- a/src/plugins/data/common/index_patterns/lib/get_title.ts +++ b/src/plugins/data/common/index_patterns/lib/get_title.ts @@ -7,14 +7,14 @@ */ import { SavedObjectsClientContract, SimpleSavedObject } from '../../../../../core/public'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../../constants'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../../constants'; export async function getTitle( client: SavedObjectsClientContract, indexPatternId: string ): Promise> { const savedObject = (await client.get( - INDEX_PATTERN_SAVED_OBJECT_TYPE, + DATA_VIEW_SAVED_OBJECT_TYPE, indexPatternId )) as SimpleSavedObject; diff --git a/src/plugins/data/common/index_patterns/lib/index.ts b/src/plugins/data/common/index_patterns/lib/index.ts index 57d8c50bda13..ae59c7d41781 100644 --- a/src/plugins/data/common/index_patterns/lib/index.ts +++ b/src/plugins/data/common/index_patterns/lib/index.ts @@ -6,9 +6,9 @@ * Side Public License, v 1. */ -export { IndexPatternMissingIndices } from './errors'; +export { DataViewMissingIndices } from './errors'; export { getTitle } from './get_title'; export { isDefault } from './is_default'; export * from './types'; -export { validateIndexPattern } from './validate_index_pattern'; +export { validateDataView } from './validate_index_pattern'; diff --git a/src/plugins/data/common/index_patterns/lib/validate_index_pattern.test.ts b/src/plugins/data/common/index_patterns/lib/validate_index_pattern.test.ts index b033fd800e4c..ed90da122484 100644 --- a/src/plugins/data/common/index_patterns/lib/validate_index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/lib/validate_index_pattern.test.ts @@ -8,24 +8,24 @@ import { CONTAINS_SPACES_KEY, ILLEGAL_CHARACTERS_KEY, ILLEGAL_CHARACTERS_VISIBLE } from './types'; -import { validateIndexPattern } from './validate_index_pattern'; +import { validateDataView } from './validate_index_pattern'; describe('Index Pattern Utils', () => { describe('Validation', () => { it('should not allow space in the pattern', () => { - const errors = validateIndexPattern('my pattern'); + const errors = validateDataView('my pattern'); expect(errors[CONTAINS_SPACES_KEY]).toBe(true); }); it('should not allow illegal characters', () => { ILLEGAL_CHARACTERS_VISIBLE.forEach((char) => { - const errors = validateIndexPattern(`pattern${char}`); + const errors = validateDataView(`pattern${char}`); expect(errors[ILLEGAL_CHARACTERS_KEY]).toEqual([char]); }); }); it('should return empty object when there are no errors', () => { - expect(validateIndexPattern('my-pattern-*')).toEqual({}); + expect(validateDataView('my-pattern-*')).toEqual({}); }); }); }); diff --git a/src/plugins/data/common/index_patterns/lib/validate_index_pattern.ts b/src/plugins/data/common/index_patterns/lib/validate_index_pattern.ts index cdafda2ee127..454d0bc1a0c6 100644 --- a/src/plugins/data/common/index_patterns/lib/validate_index_pattern.ts +++ b/src/plugins/data/common/index_patterns/lib/validate_index_pattern.ts @@ -8,7 +8,7 @@ import { ILLEGAL_CHARACTERS_VISIBLE, CONTAINS_SPACES_KEY, ILLEGAL_CHARACTERS_KEY } from './types'; -function indexPatternContainsSpaces(indexPattern: string): boolean { +function dataViewContainsSpaces(indexPattern: string): boolean { return indexPattern.includes(' '); } @@ -23,7 +23,7 @@ function findIllegalCharacters(indexPattern: string): string[] { return illegalCharacters; } -export function validateIndexPattern(indexPattern: string) { +export function validateDataView(indexPattern: string) { const errors: Record = {}; const illegalCharacters = findIllegalCharacters(indexPattern); @@ -32,7 +32,7 @@ export function validateIndexPattern(indexPattern: string) { errors[ILLEGAL_CHARACTERS_KEY] = illegalCharacters; } - if (indexPatternContainsSpaces(indexPattern)) { + if (dataViewContainsSpaces(indexPattern)) { errors[CONTAINS_SPACES_KEY] = true; } diff --git a/src/plugins/data/common/index_patterns/types.ts b/src/plugins/data/common/index_patterns/types.ts index c326e75aca41..d1e822aea4e9 100644 --- a/src/plugins/data/common/index_patterns/types.ts +++ b/src/plugins/data/common/index_patterns/types.ts @@ -6,14 +6,14 @@ * Side Public License, v 1. */ import type { estypes } from '@elastic/elasticsearch'; -import type { IndexPatternFieldBase, IFieldSubType, IndexPatternBase } from '@kbn/es-query'; +import type { DataViewFieldBase, IFieldSubType, DataViewBase } from '@kbn/es-query'; import { ToastInputFields, ErrorToastOptions } from 'src/core/public/notifications'; // eslint-disable-next-line import type { SavedObject } from 'src/core/server'; import { IFieldType } from './fields'; import { RUNTIME_FIELD_TYPES } from './constants'; import { SerializedFieldFormat } from '../../../expressions/common'; -import { KBN_FIELD_TYPES, IndexPatternField } from '..'; +import { KBN_FIELD_TYPES, DataViewField } from '..'; import { FieldFormat } from '../../../field_formats/common'; export type FieldFormatMap = Record; @@ -31,7 +31,7 @@ export interface RuntimeField { * IIndexPattern allows for an IndexPattern OR an index pattern saved object * Use IndexPattern or IndexPatternSpec instead */ -export interface IIndexPattern extends IndexPatternBase { +export interface IIndexPattern extends DataViewBase { title: string; fields: IFieldType[]; /** @@ -44,15 +44,13 @@ export interface IIndexPattern extends IndexPatternBase { /** * Look up a formatter for a given field */ - getFormatterForField?: ( - field: IndexPatternField | IndexPatternField['spec'] | IFieldType - ) => FieldFormat; + getFormatterForField?: (field: DataViewField | DataViewField['spec'] | IFieldType) => FieldFormat; } /** * Interface for an index pattern saved object */ -export interface IndexPatternAttributes { +export interface DataViewAttributes { fields: string; title: string; type?: string; @@ -69,6 +67,11 @@ export interface IndexPatternAttributes { allowNoIndex?: boolean; } +/** + * @deprecated Use DataViewAttributes. All index pattern interfaces were renamed. + */ +export type IndexPatternAttributes = DataViewAttributes; + /** * @intenal * Storage of field attributes. Necessary since the field list isn't saved. @@ -133,12 +136,17 @@ export interface GetFieldsOptionsTimePattern { interval: string; } -export interface IIndexPatternsApiClient { +export interface IDataViewsApiClient { getFieldsForTimePattern: (options: GetFieldsOptionsTimePattern) => Promise; getFieldsForWildcard: (options: GetFieldsOptions) => Promise; hasUserIndexPattern: () => Promise; } +/** + * @deprecated Use IDataViewsApiClient. All index pattern interfaces were renamed. + */ +export type IIndexPatternsApiClient = IDataViewsApiClient; + export type { SavedObject }; export type AggregationRestrictions = Record< @@ -160,11 +168,19 @@ export interface TypeMeta { }; } -export enum IndexPatternType { +export enum DataViewType { DEFAULT = 'default', ROLLUP = 'rollup', } +/** + * @deprecated Use DataViewType. All index pattern interfaces were renamed. + */ +export enum IndexPatternType { + DEFAULT = DataViewType.DEFAULT, + ROLLUP = DataViewType.ROLLUP, +} + export type FieldSpecConflictDescriptions = Record; // This should become FieldSpec once types are cleaned up @@ -189,7 +205,7 @@ export interface FieldSpecExportFmt { * @public * Serialized version of IndexPatternField */ -export interface FieldSpec extends IndexPatternFieldBase { +export interface FieldSpec extends DataViewFieldBase { /** * Popularity count is used by discover */ @@ -208,13 +224,18 @@ export interface FieldSpec extends IndexPatternFieldBase { isMapped?: boolean; } -export type IndexPatternFieldMap = Record; +export type DataViewFieldMap = Record; + +/** + * @deprecated Use DataViewFieldMap. All index pattern interfaces were renamed. + */ +export type IndexPatternFieldMap = DataViewFieldMap; /** * Static index pattern format * Serialized data object, representing index pattern attributes and state */ -export interface IndexPatternSpec { +export interface DataViewSpec { /** * saved object id */ @@ -231,7 +252,7 @@ export interface IndexPatternSpec { intervalName?: string; timeFieldName?: string; sourceFilters?: SourceFilter[]; - fields?: IndexPatternFieldMap; + fields?: DataViewFieldMap; typeMeta?: TypeMeta; type?: string; fieldFormats?: Record; @@ -240,6 +261,11 @@ export interface IndexPatternSpec { allowNoIndex?: boolean; } +/** + * @deprecated Use DataViewSpec. All index pattern interfaces were renamed. + */ +export type IndexPatternSpec = DataViewSpec; + export interface SourceFilter { value: string; } diff --git a/src/plugins/data/common/index_patterns/utils.ts b/src/plugins/data/common/index_patterns/utils.ts index 925f646b83bb..48a0776dc43a 100644 --- a/src/plugins/data/common/index_patterns/utils.ts +++ b/src/plugins/data/common/index_patterns/utils.ts @@ -9,7 +9,7 @@ import type { IndexPatternSavedObjectAttrs } from './index_patterns'; import type { SavedObjectsClientCommon } from '../types'; -import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../constants'; +import { DATA_VIEW_SAVED_OBJECT_TYPE } from '../constants'; /** * Returns an object matching a given title @@ -21,7 +21,7 @@ import { INDEX_PATTERN_SAVED_OBJECT_TYPE } from '../constants'; export async function findByTitle(client: SavedObjectsClientCommon, title: string) { if (title) { const savedObjects = await client.find({ - type: INDEX_PATTERN_SAVED_OBJECT_TYPE, + type: DATA_VIEW_SAVED_OBJECT_TYPE, perPage: 10, search: `"${title}"`, searchFields: ['title'], diff --git a/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts index f5cb7e957471..496225ad9f00 100644 --- a/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts +++ b/src/plugins/data/common/search/expressions/esaggs/esaggs_fn.ts @@ -9,12 +9,13 @@ import { i18n } from '@kbn/i18n'; import { Observable } from 'rxjs'; -import { Datatable, ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import type { Datatable, ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { buildExpressionFunction } from '../../../../../../plugins/expressions/common'; import { IndexPatternExpressionType } from '../../../index_patterns/expressions'; import { IndexPatternsContract } from '../../../index_patterns/index_patterns'; -import { AggsStart, AggExpressionType } from '../../aggs'; +import { AggsStart, AggExpressionType, aggCountFnName } from '../../aggs'; import { ISearchStartSearchSource } from '../../search_source'; import { KibanaContext } from '../kibana_context_type'; @@ -67,7 +68,7 @@ export const getEsaggsMeta: () => Omit aggs: { types: ['agg_type'], multi: true, - default: [], + default: `{${buildExpressionFunction(aggCountFnName, {}).toString()}}`, help: i18n.translate('data.search.functions.esaggs.aggConfigs.help', { defaultMessage: 'List of aggs configured with agg_type functions', }), diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index 0602f51889a6..f70733d1b3e8 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -43,7 +43,7 @@ import { ILLEGAL_CHARACTERS_VISIBLE, ILLEGAL_CHARACTERS, isDefault, - validateIndexPattern, + validateDataView, flattenHitWrapper, } from './index_patterns'; @@ -58,11 +58,17 @@ export const indexPatterns = { isDefault, isFilterable, isNestedField, - validate: validateIndexPattern, + validate: validateDataView, flattenHitWrapper, }; -export { IndexPatternsContract, IndexPattern, IndexPatternField, TypeMeta } from './index_patterns'; +export { + IndexPatternsContract, + DataViewsContract, + IndexPattern, + IndexPatternField, + TypeMeta, +} from './index_patterns'; export { IIndexPattern, @@ -82,7 +88,7 @@ export { IndexPatternListItem, } from '../common'; -export { DuplicateIndexPatternError } from '../common/index_patterns/errors'; +export { DuplicateDataViewError } from '../common/index_patterns/errors'; /* * Autocomplete query suggestions: diff --git a/src/plugins/data/public/index_patterns/index.ts b/src/plugins/data/public/index_patterns/index.ts index e23fc789656a..d1a2b0f28f1d 100644 --- a/src/plugins/data/public/index_patterns/index.ts +++ b/src/plugins/data/public/index_patterns/index.ts @@ -11,7 +11,7 @@ export { CONTAINS_SPACES_KEY, ILLEGAL_CHARACTERS_VISIBLE, ILLEGAL_CHARACTERS, - validateIndexPattern, + validateDataView, isDefault, } from '../../common/index_patterns/lib'; export { flattenHitWrapper, formatHitProvider, onRedirectNoIndexPattern } from './index_patterns'; @@ -23,6 +23,9 @@ export { IndexPatternsContract, IndexPattern, IndexPatternsApiClient, + DataViewsService, + DataViewsContract, + DataView, } from './index_patterns'; export { UiSettingsPublicToCommon } from './ui_settings_wrapper'; export { SavedObjectsClientPublicToCommon } from './saved_objects_client_wrapper'; diff --git a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts index d4e8e0624511..b3471f063039 100644 --- a/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts +++ b/src/plugins/data/public/index_patterns/index_patterns/index_patterns_api_client.ts @@ -7,7 +7,7 @@ */ import { HttpSetup } from 'src/core/public'; -import { IndexPatternMissingIndices } from '../../../common/index_patterns/lib'; +import { DataViewMissingIndices } from '../../../common/index_patterns/lib'; import { GetFieldsOptions, IIndexPatternsApiClient, @@ -30,7 +30,7 @@ export class IndexPatternsApiClient implements IIndexPatternsApiClient { }) .catch((resp: any) => { if (resp.body.statusCode === 404 && resp.body.attributes?.code === 'no_matching_indices') { - throw new IndexPatternMissingIndices(resp.body.message); + throw new DataViewMissingIndices(resp.body.message); } throw new Error(resp.body.message || resp.body.error || `${resp.body.statusCode} Response`); diff --git a/src/plugins/data/public/mocks.ts b/src/plugins/data/public/mocks.ts index b9b859fd9662..40882fa1134e 100644 --- a/src/plugins/data/public/mocks.ts +++ b/src/plugins/data/public/mocks.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { DataPlugin, IndexPatternsContract } from '.'; +import { DataPlugin, DataViewsContract } from '.'; import { fieldFormatsServiceMock } from '../../field_formats/public/mocks'; import { searchServiceMock } from './search/mocks'; import { queryServiceMock } from './query/mocks'; @@ -38,6 +38,20 @@ const createSetupContract = (): Setup => { const createStartContract = (): Start => { const queryStartMock = queryServiceMock.createStartContract(); + const dataViews = ({ + find: jest.fn((search) => [{ id: search, title: search }]), + createField: jest.fn(() => {}), + createFieldList: jest.fn(() => []), + ensureDefaultIndexPattern: jest.fn(), + make: () => ({ + fieldsFetcher: { + fetchForWildcard: jest.fn(), + }, + }), + get: jest.fn().mockReturnValue(Promise.resolve({})), + clearCache: jest.fn(), + } as unknown) as DataViewsContract; + return { actions: { createFiltersFromValueClickAction: jest.fn().mockResolvedValue(['yes']), @@ -51,19 +65,11 @@ const createStartContract = (): Start => { IndexPatternSelect: jest.fn(), SearchBar: jest.fn().mockReturnValue(null), }, - indexPatterns: ({ - find: jest.fn((search) => [{ id: search, title: search }]), - createField: jest.fn(() => {}), - createFieldList: jest.fn(() => []), - ensureDefaultIndexPattern: jest.fn(), - make: () => ({ - fieldsFetcher: { - fetchForWildcard: jest.fn(), - }, - }), - get: jest.fn().mockReturnValue(Promise.resolve({})), - clearCache: jest.fn(), - } as unknown) as IndexPatternsContract, + dataViews, + /** + * @deprecated Use dataViews service instead. All index pattern interfaces were renamed. + */ + indexPatterns: dataViews, nowProvider: createNowProviderMock(), }; }; diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts index 67adcc7a1716..a12bb5081598 100644 --- a/src/plugins/data/public/plugin.ts +++ b/src/plugins/data/public/plugin.ts @@ -197,6 +197,7 @@ export class DataPublicPlugin autocomplete: this.autocomplete.start(), fieldFormats, indexPatterns, + dataViews: indexPatterns, query, search, nowProvider: this.nowProvider, diff --git a/src/plugins/data/public/search/expressions/esaggs.test.ts b/src/plugins/data/public/search/expressions/esaggs.test.ts index 2e4bfc329352..e43069697f6e 100644 --- a/src/plugins/data/public/search/expressions/esaggs.test.ts +++ b/src/plugins/data/public/search/expressions/esaggs.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { omit } from 'lodash'; import { of as mockOf } from 'rxjs'; import type { MockedKeys } from '@kbn/utility-types/jest'; import type { ExecutionContext } from 'src/plugins/expressions/public'; @@ -90,6 +91,12 @@ describe('esaggs expression function - public', () => { ); }); + test('calls aggs.createAggConfigs with the empty aggs array when not provided', async () => { + await definition().fn(null, omit(args, 'aggs'), mockHandlers).toPromise(); + + expect(startDependencies.aggs.createAggConfigs).toHaveBeenCalledWith({}, []); + }); + test('calls getEsaggsMeta to retrieve meta', () => { const result = definition(); diff --git a/src/plugins/data/public/search/expressions/esaggs.ts b/src/plugins/data/public/search/expressions/esaggs.ts index cf3de20fea50..64b3f3c8dd7d 100644 --- a/src/plugins/data/public/search/expressions/esaggs.ts +++ b/src/plugins/data/public/search/expressions/esaggs.ts @@ -44,7 +44,7 @@ export function getFunctionDefinition({ const indexPattern = await indexPatterns.create(args.index.value, true); const aggConfigs = aggs.createAggConfigs( indexPattern, - args.aggs!.map((agg) => agg.value) + args.aggs?.map((agg) => agg.value) ?? [] ); aggConfigs.hierarchical = args.metricsAtAllLevels; diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index 4b52ddfb6882..b31a4ab933ae 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -17,7 +17,7 @@ import { AutocompleteSetup, AutocompleteStart } from './autocomplete'; import { createFiltersFromRangeSelectAction, createFiltersFromValueClickAction } from './actions'; import { ISearchSetup, ISearchStart } from './search'; import { QuerySetup, QueryStart } from './query'; -import { IndexPatternsContract } from './index_patterns'; +import { DataViewsContract } from './index_patterns'; import { IndexPatternSelectProps, StatefulSearchBarProps } from './ui'; import { UsageCollectionSetup, UsageCollectionStart } from '../../usage_collection/public'; import { Setup as InspectorSetup } from '../../inspector/public'; @@ -76,11 +76,17 @@ export interface DataPublicPluginStart { * {@link AutocompleteStart} */ autocomplete: AutocompleteStart; + /** + * data views service + * {@link DataViewsContract} + */ + dataViews: DataViewsContract; /** * index patterns service - * {@link IndexPatternsContract} + * {@link DataViewsContract} + * @deprecated Use dataViews service instead. All index pattern interfaces were renamed. */ - indexPatterns: IndexPatternsContract; + indexPatterns: DataViewsContract; /** * search service * {@link ISearchStart} diff --git a/src/plugins/data/server/config_deprecations.test.ts b/src/plugins/data/server/config_deprecations.test.ts index 28298c654f27..6d3f8d9d6f22 100644 --- a/src/plugins/data/server/config_deprecations.test.ts +++ b/src/plugins/data/server/config_deprecations.test.ts @@ -48,7 +48,7 @@ describe('Config Deprecations', () => { expect(migrated.data.autocomplete.valueSuggestions.terminateAfter).toEqual(123); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"kibana.autocompleteTerminateAfter\\" is deprecated and has been replaced by \\"data.autocomplete.valueSuggestions.terminateAfter\\"", + "Setting \\"kibana.autocompleteTerminateAfter\\" has been replaced by \\"data.autocomplete.valueSuggestions.terminateAfter\\"", ] `); }); @@ -64,7 +64,7 @@ describe('Config Deprecations', () => { expect(migrated.data.autocomplete.valueSuggestions.timeout).toEqual(123); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"kibana.autocompleteTimeout\\" is deprecated and has been replaced by \\"data.autocomplete.valueSuggestions.timeout\\"", + "Setting \\"kibana.autocompleteTimeout\\" has been replaced by \\"data.autocomplete.valueSuggestions.timeout\\"", ] `); }); diff --git a/src/plugins/data/server/index_patterns/deprecations/scripted_fields.ts b/src/plugins/data/server/index_patterns/deprecations/scripted_fields.ts index 7c9ce6f9ed33..0dedfeb72028 100644 --- a/src/plugins/data/server/index_patterns/deprecations/scripted_fields.ts +++ b/src/plugins/data/server/index_patterns/deprecations/scripted_fields.ts @@ -12,6 +12,7 @@ import { GetDeprecationsContext, RegisterDeprecationsConfig, } from 'kibana/server'; +import { i18n } from '@kbn/i18n'; import { IndexPatternAttributes } from '../../../common'; type IndexPatternAttributesWithFields = Pick; @@ -38,19 +39,35 @@ export const createScriptedFieldsDeprecationsConfig: ( if (indexPatternsWithScriptedFields.length > 0) { const PREVIEW_LIMIT = 3; const indexPatternTitles = indexPatternsWithScriptedFields.map((ip) => ip.title); - const titlesPreview = indexPatternTitles.slice(0, PREVIEW_LIMIT).join('; '); - const allTitles = indexPatternTitles.join('; '); return [ { - message: `You have ${indexPatternsWithScriptedFields.length} index patterns (${titlesPreview}...) that use scripted fields. Scripted fields are deprecated and will be removed in future. Use runtime fields instead.`, + title: i18n.translate('data.deprecations.scriptedFieldsTitle', { + defaultMessage: 'Found index patterns using scripted fields', + }), + message: i18n.translate('data.deprecations.scriptedFieldsMessage', { + defaultMessage: `You have {numberOfIndexPatternsWithScriptedFields} index patterns ({titlesPreview}...) that use scripted fields. Scripted fields are deprecated and will be removed in future. Use runtime fields instead.`, + values: { + titlesPreview: indexPatternTitles.slice(0, PREVIEW_LIMIT).join('; '), + numberOfIndexPatternsWithScriptedFields: indexPatternsWithScriptedFields.length, + }, + }), documentationUrl: 'https://www.elastic.co/guide/en/elasticsearch/reference/7.x/runtime.html', // TODO: documentation service is not available serverside https://github.com/elastic/kibana/issues/95389 level: 'warning', // warning because it is not set in stone WHEN we remove scripted fields, hence this deprecation is not a blocker for 8.0 upgrade correctiveActions: { manualSteps: [ - 'Navigate to Stack Management > Kibana > Index Patterns.', - `Update ${indexPatternsWithScriptedFields.length} index patterns that have scripted fields to use runtime fields instead. In most cases, to migrate existing scripts, you'll need to change "return ;" to "emit();". Index patterns with at least one scripted field: ${allTitles}`, + i18n.translate('data.deprecations.scriptedFields.manualStepOneMessage', { + defaultMessage: 'Navigate to Stack Management > Kibana > Index Patterns.', + }), + i18n.translate('data.deprecations.scriptedFields.manualStepTwoMessage', { + defaultMessage: + 'Update {numberOfIndexPatternsWithScriptedFields} index patterns that have scripted fields to use runtime fields instead. In most cases, to migrate existing scripts, you will need to change "return ;" to "emit();". Index patterns with at least one scripted field: {allTitles}', + values: { + allTitles: indexPatternTitles.join('; '), + numberOfIndexPatternsWithScriptedFields: indexPatternsWithScriptedFields.length, + }, + }), ], }, }, diff --git a/src/plugins/data/server/index_patterns/index_patterns_api_client.ts b/src/plugins/data/server/index_patterns/index_patterns_api_client.ts index fb76647a945b..eeb146aee46a 100644 --- a/src/plugins/data/server/index_patterns/index_patterns_api_client.ts +++ b/src/plugins/data/server/index_patterns/index_patterns_api_client.ts @@ -12,7 +12,7 @@ import { IIndexPatternsApiClient, GetFieldsOptionsTimePattern, } from '../../common/index_patterns/types'; -import { IndexPatternMissingIndices } from '../../common/index_patterns/lib'; +import { DataViewMissingIndices } from '../../common/index_patterns/lib'; import { IndexPatternsFetcher } from './fetcher'; import { hasUserIndexPattern } from './has_user_index_pattern'; @@ -44,7 +44,7 @@ export class IndexPatternsApiServer implements IIndexPatternsApiClient { err.output.payload.statusCode === 404 && err.output.payload.code === 'no_matching_indices' ) { - throw new IndexPatternMissingIndices(pattern); + throw new DataViewMissingIndices(pattern); } else { throw err; } diff --git a/src/plugins/data/server/search/expressions/esaggs.test.ts b/src/plugins/data/server/search/expressions/esaggs.test.ts index e7e12e9449af..37c5f90f00f4 100644 --- a/src/plugins/data/server/search/expressions/esaggs.test.ts +++ b/src/plugins/data/server/search/expressions/esaggs.test.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import { omit } from 'lodash'; import { of as mockOf } from 'rxjs'; import type { MockedKeys } from '@kbn/utility-types/jest'; import { KibanaRequest } from 'src/core/server'; @@ -98,6 +99,12 @@ describe('esaggs expression function - server', () => { ); }); + test('calls aggs.createAggConfigs with the empty aggs array when not provided', async () => { + await definition().fn(null, omit(args, 'aggs'), mockHandlers).toPromise(); + + expect(startDependencies.aggs.createAggConfigs).toHaveBeenCalledWith({}, []); + }); + test('calls getEsaggsMeta to retrieve meta', () => { const result = definition(); diff --git a/src/plugins/data/server/search/expressions/esaggs.ts b/src/plugins/data/server/search/expressions/esaggs.ts index 3a39276c8ed4..058538fad498 100644 --- a/src/plugins/data/server/search/expressions/esaggs.ts +++ b/src/plugins/data/server/search/expressions/esaggs.ts @@ -56,7 +56,7 @@ export function getFunctionDefinition({ const indexPattern = await indexPatterns.create(args.index.value, true); const aggConfigs = aggs.createAggConfigs( indexPattern, - args.aggs!.map((agg) => agg.value) + args.aggs?.map((agg) => agg.value) ?? [] ); aggConfigs.hierarchical = args.metricsAtAllLevels; diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 63f03655d935..b30fcf972eda 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -20,3 +20,4 @@ export const MODIFY_COLUMNS_ON_SWITCH = 'discover:modifyColumnsOnSwitch'; export const SEARCH_FIELDS_FROM_SOURCE = 'discover:searchFieldsFromSource'; export const MAX_DOC_FIELDS_DISPLAYED = 'discover:maxDocFieldsDisplayed'; export const SHOW_MULTIFIELDS = 'discover:showMultiFields'; +export const SEARCH_EMBEDDABLE_TYPE = 'search'; diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index f612e0ec2cc8..46eeb5af1470 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -17,9 +17,10 @@ ], "optionalPlugins": ["home", "share", "usageCollection"], "requiredBundles": ["kibanaUtils", "home", "kibanaReact", "fieldFormats"], + "extraPublicDirs": ["common"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Data Discovery", + "githubTeam": "kibana-data-discovery" }, "description": "This plugin contains the Discover application and the saved search embeddable." } diff --git a/src/plugins/discover/public/__mocks__/services.ts b/src/plugins/discover/public/__mocks__/services.ts index 96888a07be68..47c79c429f66 100644 --- a/src/plugins/discover/public/__mocks__/services.ts +++ b/src/plugins/discover/public/__mocks__/services.ts @@ -29,6 +29,7 @@ export const discoverServiceMock = ({ location: { search: '', }, + listen: jest.fn(), }), data: dataPlugin, docLinks: docLinksServiceMock.createStartContract(), @@ -68,6 +69,9 @@ export const discoverServiceMock = ({ return true; }, }, + http: { + basePath: '/', + }, indexPatternFieldEditor: { openEditor: jest.fn(), userPermissions: { diff --git a/src/plugins/discover/public/application/angular/context.html b/src/plugins/discover/public/application/angular/context.html deleted file mode 100644 index 6cb5088f6660..000000000000 --- a/src/plugins/discover/public/application/angular/context.html +++ /dev/null @@ -1,5 +0,0 @@ - - diff --git a/src/plugins/discover/public/application/angular/context.js b/src/plugins/discover/public/application/angular/context.js deleted file mode 100644 index 43e0c26b168f..000000000000 --- a/src/plugins/discover/public/application/angular/context.js +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { i18n } from '@kbn/i18n'; -import { getAngularModule, getServices } from '../../kibana_services'; -import contextAppRouteTemplate from './context.html'; -import { getRootBreadcrumbs } from '../helpers/breadcrumbs'; - -const k7Breadcrumbs = () => { - return [ - ...getRootBreadcrumbs(), - { - text: i18n.translate('discover.context.breadcrumb', { - defaultMessage: 'Surrounding documents', - }), - }, - ]; -}; - -getAngularModule().config(($routeProvider) => { - $routeProvider.when('/context/:indexPatternId/:id*', { - controller: function ($routeParams, $scope, $route) { - this.indexPattern = $route.current.locals.indexPattern.ip; - this.anchorId = $routeParams.id; - this.indexPatternId = $route.current.params.indexPatternId; - }, - k7Breadcrumbs, - controllerAs: 'contextAppRoute', - reloadOnSearch: false, - resolve: { - indexPattern: ($route, Promise) => { - const indexPattern = getServices().indexPatterns.get($route.current.params.indexPatternId); - return Promise.props({ ip: indexPattern }); - }, - }, - template: contextAppRouteTemplate, - }); -}); diff --git a/src/plugins/discover/public/application/angular/discover.js b/src/plugins/discover/public/application/angular/discover.js index aa1344a67fbe..e2a0a19b80cf 100644 --- a/src/plugins/discover/public/application/angular/discover.js +++ b/src/plugins/discover/public/application/angular/discover.js @@ -6,106 +6,14 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; -import { getState } from '../apps/main/services/discover_state'; -import indexTemplateLegacy from './discover_legacy.html'; -import { - getAngularModule, - getServices, - getUrlTracker, - redirectWhenMissing, -} from '../../kibana_services'; -import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../helpers/breadcrumbs'; -import { loadIndexPattern, resolveIndexPattern } from '../apps/main/utils/resolve_index_pattern'; +import { getAngularModule, getServices } from '../../kibana_services'; const services = getServices(); -const { - core, - capabilities, - chrome, - data, - history: getHistory, - toastNotifications, - uiSettings: config, -} = getServices(); +const { history: getHistory } = getServices(); const app = getAngularModule(); -app.config(($routeProvider) => { - const defaults = { - requireDefaultIndex: true, - requireUICapability: 'discover.show', - k7Breadcrumbs: ($route, $injector) => - $injector.invoke($route.current.params.id ? getSavedSearchBreadcrumbs : getRootBreadcrumbs), - badge: () => { - if (capabilities.discover.save) { - return undefined; - } - - return { - text: i18n.translate('discover.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - tooltip: i18n.translate('discover.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save searches', - }), - iconType: 'glasses', - }; - }, - }; - const discoverRoute = { - ...defaults, - template: indexTemplateLegacy, - reloadOnSearch: false, - resolve: { - savedObjects: function ($route, Promise) { - const history = getHistory(); - const savedSearchId = $route.current.params.id; - return data.indexPatterns.ensureDefaultIndexPattern(history).then(() => { - const { appStateContainer } = getState({ history, uiSettings: config }); - const { index } = appStateContainer.getState(); - return Promise.props({ - ip: loadIndexPattern(index, data.indexPatterns, config), - savedSearch: getServices() - .getSavedSearchById(savedSearchId) - .then((savedSearch) => { - if (savedSearchId) { - chrome.recentlyAccessed.add( - savedSearch.getFullPath(), - savedSearch.title, - savedSearchId - ); - } - return savedSearch; - }) - .catch( - redirectWhenMissing({ - history, - navigateToApp: core.application.navigateToApp, - mapping: { - search: '/', - 'index-pattern': { - app: 'management', - path: `kibana/objects/savedSearches/${$route.current.params.id}`, - }, - }, - toastNotifications, - onBeforeRedirect() { - getUrlTracker().setTrackedUrl('/'); - }, - }) - ), - }); - }); - }, - }, - }; - - $routeProvider.when('/view/:id?', discoverRoute); - $routeProvider.when('/', discoverRoute); -}); - app.directive('discoverApp', function () { return { restrict: 'E', @@ -114,21 +22,12 @@ app.directive('discoverApp', function () { }; }); -function discoverController($route, $scope) { - const savedSearch = $route.current.locals.savedObjects.savedSearch; - $scope.indexPattern = resolveIndexPattern( - $route.current.locals.savedObjects.ip, - savedSearch.searchSource, - toastNotifications - ); - +function discoverController(_, $scope) { const history = getHistory(); $scope.opts = { - savedSearch, history, services, - indexPatternList: $route.current.locals.savedObjects.ip.list, navigateTo: (path) => { $scope.$evalAsync(() => { history.push(path); @@ -136,8 +35,5 @@ function discoverController($route, $scope) { }, }; - $scope.$on('$destroy', () => { - savedSearch.destroy(); - data.search.session.clear(); - }); + $scope.$on('$destroy', () => {}); } diff --git a/src/plugins/discover/public/application/angular/doc.html b/src/plugins/discover/public/application/angular/doc.html deleted file mode 100644 index dcd5760eff15..000000000000 --- a/src/plugins/discover/public/application/angular/doc.html +++ /dev/null @@ -1,8 +0,0 @@ -
- -
diff --git a/src/plugins/discover/public/application/angular/doc.ts b/src/plugins/discover/public/application/angular/doc.ts deleted file mode 100644 index 27af3a96bbc8..000000000000 --- a/src/plugins/discover/public/application/angular/doc.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getAngularModule, getServices } from '../../kibana_services'; -import { getRootBreadcrumbs } from '../helpers/breadcrumbs'; -import html from './doc.html'; -import { Doc } from '../components/doc/doc'; - -interface LazyScope extends ng.IScope { - [key: string]: unknown; -} - -const { timefilter } = getServices(); -const app = getAngularModule(); -// eslint-disable-next-line @typescript-eslint/no-explicit-any -app.directive('discoverDoc', function (reactDirective: any) { - return reactDirective( - Doc, - [ - ['id', { watchDepth: 'value' }], - ['index', { watchDepth: 'value' }], - ['indexPatternId', { watchDepth: 'reference' }], - ['indexPatternService', { watchDepth: 'reference' }], - ], - { restrict: 'E' } - ); -}); - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -app.config(($routeProvider: any) => { - $routeProvider - .when('/doc/:indexPattern/:index/:type', { - redirectTo: '/doc/:indexPattern/:index', - }) - // the new route, es 7 deprecated types, es 8 removed them - .when('/doc/:indexPattern/:index', { - // have to be written as function expression, because it's not compiled in dev mode - // eslint-disable-next-line @typescript-eslint/no-explicit-any, object-shorthand - controller: function ($scope: LazyScope, $route: any) { - timefilter.disableAutoRefreshSelector(); - timefilter.disableTimeRangeSelector(); - $scope.id = $route.current.params.id; - $scope.index = $route.current.params.index; - $scope.indexPatternId = $route.current.params.indexPattern; - $scope.indexPatternService = getServices().indexPatterns; - }, - template: html, - // eslint-disable-next-line @typescript-eslint/no-explicit-any - k7Breadcrumbs: ($route: any) => [ - ...getRootBreadcrumbs(), - { - text: `${$route.current.params.index}#${$route.current.params.id}`, - }, - ], - }); -}); diff --git a/src/plugins/discover/public/application/angular/get_inner_angular.ts b/src/plugins/discover/public/application/angular/get_inner_angular.ts index 5f459c369ce4..5d2da5498080 100644 --- a/src/plugins/discover/public/application/angular/get_inner_angular.ts +++ b/src/plugins/discover/public/application/angular/get_inner_angular.ts @@ -16,7 +16,7 @@ import 'angular-sanitize'; import { EuiIcon } from '@elastic/eui'; import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; import { CoreStart, PluginInitializerContext } from 'kibana/public'; -import { DataPublicPluginStart } from '../../../../data/public'; +import { DataPublicPluginStart } from 'src/plugins/data/public'; import { Storage } from '../../../../kibana_utils/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../navigation/public'; import { createContextAppLegacy } from '../components/context_app/context_app_legacy_directive'; @@ -30,7 +30,6 @@ import { import { PromiseServiceCreator } from './helpers'; import { DiscoverStartPlugins } from '../../plugin'; import { getScopedHistory } from '../../kibana_services'; -import { createDiscoverDirective } from './create_discover_directive'; /** * returns the main inner angular module, it contains all the parts of Angular Discover @@ -94,7 +93,6 @@ export function initializeInnerAngularModule( return angular .module(name, [ 'ngSanitize', - 'ngRoute', 'react', 'ui.bootstrap', 'discoverI18n', @@ -104,8 +102,7 @@ export function initializeInnerAngularModule( 'discoverDocTable', ]) .config(watchMultiDecorator) - .run(registerListenEventListener) - .directive('discover', createDiscoverDirective); + .run(registerListenEventListener); } function createLocalPromiseModule() { diff --git a/src/plugins/discover/public/application/angular/index.ts b/src/plugins/discover/public/application/angular/index.ts index c4f6415c771f..643823a15ffc 100644 --- a/src/plugins/discover/public/application/angular/index.ts +++ b/src/plugins/discover/public/application/angular/index.ts @@ -8,10 +8,6 @@ // required for i18nIdDirective import 'angular-sanitize'; -// required for ngRoute -import 'angular-route'; -import './discover'; import './doc'; import './context'; -import './redirect'; diff --git a/src/plugins/discover/public/application/angular/redirect.ts b/src/plugins/discover/public/application/angular/redirect.ts deleted file mode 100644 index 5014376ff13a..000000000000 --- a/src/plugins/discover/public/application/angular/redirect.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0 and the Server Side Public License, v 1; you may not use this file except - * in compliance with, at your election, the Elastic License 2.0 or the Server - * Side Public License, v 1. - */ - -import { getAngularModule, getServices, getUrlTracker } from '../../kibana_services'; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -getAngularModule().config(($routeProvider: any) => { - $routeProvider.otherwise({ - // eslint-disable-next-line @typescript-eslint/no-explicit-any - resolveRedirectTo: ($rootScope: any) => { - const path = window.location.hash.substr(1); - getUrlTracker().restorePreviousUrl(); - $rootScope.$applyAsync(() => { - const { urlForwarding } = getServices(); - const { navigated } = urlForwarding.navigateToLegacyKibanaUrl(path); - if (!navigated) { - urlForwarding.navigateToDefaultApp(); - } - }); - // prevent angular from completing the navigation - return new Promise(() => {}); - }, - }); -}); diff --git a/src/plugins/discover/public/application/application.ts b/src/plugins/discover/public/application/application.ts index af3a23860d04..cca5c1f112bb 100644 --- a/src/plugins/discover/public/application/application.ts +++ b/src/plugins/discover/public/application/application.ts @@ -7,25 +7,34 @@ */ import './index.scss'; -import angular from 'angular'; +import { renderApp as renderReactApp } from './index'; /** * Here's where Discover's inner angular is mounted and rendered */ export async function renderApp(moduleName: string, element: HTMLElement) { - await import('./angular'); - const $injector = mountDiscoverApp(moduleName, element); - return () => $injector.get('$rootScope').$destroy(); + const app = mountDiscoverApp(moduleName, element); + return () => { + app(); + }; } -function mountDiscoverApp(moduleName: string, element: HTMLElement) { - const mountpoint = document.createElement('div'); +function buildDiscoverElement(mountpoint: HTMLElement) { + // due to legacy angular tags, we need some manual DOM intervention here const appWrapper = document.createElement('div'); - appWrapper.setAttribute('ng-view', ''); + const discoverApp = document.createElement('discover-app'); + const discover = document.createElement('discover'); + appWrapper.appendChild(discoverApp); + discoverApp.append(discover); mountpoint.appendChild(appWrapper); - // bootstrap angular into detached element and attach it later to - // make angular-within-angular possible - const $injector = angular.bootstrap(mountpoint, [moduleName]); + return discover; +} + +function mountDiscoverApp(moduleName: string, element: HTMLElement) { + const mountpoint = document.createElement('div'); + const discoverElement = buildDiscoverElement(mountpoint); + // @ts-expect-error + const app = renderReactApp({ element: discoverElement }); element.appendChild(mountpoint); - return $injector; + return app; } diff --git a/src/plugins/discover/public/application/apps/context/context_app_route.tsx b/src/plugins/discover/public/application/apps/context/context_app_route.tsx new file mode 100644 index 000000000000..7ced3955c70e --- /dev/null +++ b/src/plugins/discover/public/application/apps/context/context_app_route.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { DiscoverServices } from '../../../build_services'; +import { ContextApp } from '../../components/context_app/context_app'; +import { getRootBreadcrumbs } from '../../helpers/breadcrumbs'; +import { LoadingIndicator } from '../../components/common/loading_indicator'; +import { useIndexPattern } from '../../helpers/use_index_pattern'; + +export interface ContextAppProps { + /** + * Kibana core services used by discover + */ + services: DiscoverServices; +} + +export interface ContextUrlParams { + indexPatternId: string; + id: string; +} + +export function ContextAppRoute(props: ContextAppProps) { + const { services } = props; + const { chrome } = services; + + const { indexPatternId, id } = useParams(); + + useEffect(() => { + chrome.setBreadcrumbs([ + ...getRootBreadcrumbs(), + { + text: i18n.translate('discover.context.breadcrumb', { + defaultMessage: 'Surrounding documents', + }), + }, + ]); + }, [chrome]); + + const indexPattern = useIndexPattern(services.indexPatterns, indexPatternId); + + if (!indexPattern) { + return ; + } + + return ; +} diff --git a/src/plugins/discover/public/application/apps/context/index.ts b/src/plugins/discover/public/application/apps/context/index.ts new file mode 100644 index 000000000000..a8e457dc926e --- /dev/null +++ b/src/plugins/discover/public/application/apps/context/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { ContextAppRoute } from './context_app_route'; diff --git a/src/plugins/discover/public/application/apps/doc/index.ts b/src/plugins/discover/public/application/apps/doc/index.ts new file mode 100644 index 000000000000..c62b954c8b30 --- /dev/null +++ b/src/plugins/discover/public/application/apps/doc/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { SingleDocRoute } from './single_doc_route'; diff --git a/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx b/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx new file mode 100644 index 000000000000..9088464980c2 --- /dev/null +++ b/src/plugins/discover/public/application/apps/doc/single_doc_route.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useEffect } from 'react'; +import { useLocation, useParams } from 'react-router-dom'; +import { DiscoverServices } from '../../../build_services'; +import { getRootBreadcrumbs } from '../../helpers/breadcrumbs'; +import { Doc } from '../../components/doc/doc'; +import { LoadingIndicator } from '../../components/common/loading_indicator'; +import { useIndexPattern } from '../../helpers/use_index_pattern'; + +export interface SingleDocRouteProps { + /** + * Kibana core services used by discover + */ + services: DiscoverServices; +} + +export interface DocUrlParams { + indexPatternId: string; + index: string; +} + +function useQuery() { + return new URLSearchParams(useLocation().search); +} + +export function SingleDocRoute(props: SingleDocRouteProps) { + const { services } = props; + const { chrome, timefilter, indexPatterns } = services; + + const { indexPatternId, index } = useParams(); + + const query = useQuery(); + const docId = query.get('id') || ''; + + useEffect(() => { + chrome.setBreadcrumbs([ + ...getRootBreadcrumbs(), + { + text: `${index}#${docId}`, + }, + ]); + }, [chrome, index, docId]); + + useEffect(() => { + timefilter.disableAutoRefreshSelector(); + timefilter.disableTimeRangeSelector(); + }); + + const indexPattern = useIndexPattern(services.indexPatterns, indexPatternId); + + if (!indexPattern) { + return ; + } + + return ( +
+ +
+ ); +} diff --git a/src/plugins/discover/public/application/apps/main/discover_main_app.test.tsx b/src/plugins/discover/public/application/apps/main/discover_main_app.test.tsx index 0caa5f3f527c..aa5a2bc9bfba 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_app.test.tsx +++ b/src/plugins/discover/public/application/apps/main/discover_main_app.test.tsx @@ -26,18 +26,15 @@ describe('DiscoverMainApp', () => { return { ...ip, ...{ attributes: { title: ip.title } } }; }) as unknown) as Array>; - const component = mountWithIntl( - - ); + const props = { + indexPatternList, + services: discoverServiceMock, + savedSearch: savedSearchMock, + navigateTo: jest.fn(), + history, + }; + + const component = mountWithIntl(); expect(findTestSubject(component, 'indexPattern-switch-link').text()).toBe( indexPatternMock.title diff --git a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx index 0195421fd568..456f4ebfab62 100644 --- a/src/plugins/discover/public/application/apps/main/discover_main_app.tsx +++ b/src/plugins/discover/public/application/apps/main/discover_main_app.tsx @@ -12,7 +12,7 @@ import { setBreadcrumbsTitle } from '../../helpers/breadcrumbs'; import { addHelpMenuToAppChrome } from '../../components/help_menu/help_menu_util'; import { useDiscoverState } from './services/use_discover_state'; import { useUrl } from './services/use_url'; -import { IndexPattern, IndexPatternAttributes, SavedObject } from '../../../../../data/common'; +import { IndexPatternAttributes, SavedObject } from '../../../../../data/common'; import { DiscoverServices } from '../../../build_services'; import { SavedSearch } from '../../../saved_searches'; @@ -20,37 +20,33 @@ const DiscoverLayoutMemoized = React.memo(DiscoverLayout); export interface DiscoverMainProps { /** - * Current IndexPattern + * Instance of browser history */ - indexPattern: IndexPattern; - - opts: { - /** - * Use angular router for navigation - */ - navigateTo: () => void; - /** - * Instance of browser history - */ - history: History; - /** - * List of available index patterns - */ - indexPatternList: Array>; - /** - * Kibana core services used by discover - */ - services: DiscoverServices; - /** - * Current instance of SavedSearch - */ - savedSearch: SavedSearch; - }; + history: History; + /** + * List of available index patterns + */ + indexPatternList: Array>; + /** + * Kibana core services used by discover + */ + services: DiscoverServices; + /** + * Current instance of SavedSearch + */ + savedSearch: SavedSearch; } export function DiscoverMainApp(props: DiscoverMainProps) { - const { services, history, navigateTo, indexPatternList } = props.opts; + const { services, history, indexPatternList } = props; const { chrome, docLinks, uiSettings: config, data } = services; + const navigateTo = useCallback( + (path: string) => { + history.push(path); + }, + [history] + ); + const savedSearch = props.savedSearch; /** * State related logic @@ -63,15 +59,13 @@ export function DiscoverMainApp(props: DiscoverMainProps) { onUpdateQuery, refetch$, resetSavedSearch, - savedSearch, searchSource, state, stateContainer, } = useDiscoverState({ services, history, - initialIndexPattern: props.indexPattern, - initialSavedSearch: props.opts.savedSearch, + savedSearch, }); /** diff --git a/src/plugins/discover/public/application/apps/main/discover_main_route.tsx b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx new file mode 100644 index 000000000000..d7b49d023104 --- /dev/null +++ b/src/plugins/discover/public/application/apps/main/discover_main_route.tsx @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useEffect, useState, memo } from 'react'; +import { History } from 'history'; +import { useParams } from 'react-router-dom'; +import type { SavedObject as SavedObjectDeprecated } from 'src/plugins/saved_objects/public'; +import { IndexPatternAttributes, SavedObject } from 'src/plugins/data/common'; +import { DiscoverServices } from '../../../build_services'; +import { SavedSearch } from '../../../saved_searches'; +import { getState } from './services/discover_state'; +import { loadIndexPattern, resolveIndexPattern } from './utils/resolve_index_pattern'; +import { DiscoverMainApp } from './discover_main_app'; +import { getRootBreadcrumbs, getSavedSearchBreadcrumbs } from '../../helpers/breadcrumbs'; +import { redirectWhenMissing } from '../../../../../kibana_utils/public'; +import { getUrlTracker } from '../../../kibana_services'; +import { LoadingIndicator } from '../../components/common/loading_indicator'; + +const DiscoverMainAppMemoized = memo(DiscoverMainApp); + +export interface DiscoverMainProps { + /** + * Instance of browser history + */ + history: History; + /** + * Kibana core services used by discover + */ + services: DiscoverServices; +} + +interface DiscoverLandingParams { + id: string; +} + +export function DiscoverMainRoute({ services, history }: DiscoverMainProps) { + const { + core, + chrome, + uiSettings: config, + data, + toastNotifications, + http: { basePath }, + } = services; + + const [savedSearch, setSavedSearch] = useState(); + const indexPattern = savedSearch?.searchSource?.getField('index'); + const [indexPatternList, setIndexPatternList] = useState< + Array> + >([]); + + const { id } = useParams(); + + useEffect(() => { + const savedSearchId = id; + + async function loadDefaultOrCurrentIndexPattern(usedSavedSearch: SavedSearch) { + await data.indexPatterns.ensureDefaultIndexPattern(); + const { appStateContainer } = getState({ history, uiSettings: config }); + const { index } = appStateContainer.getState(); + const ip = await loadIndexPattern(index || '', data.indexPatterns, config); + const ipList = ip.list as Array>; + const indexPatternData = await resolveIndexPattern( + ip, + usedSavedSearch.searchSource, + toastNotifications + ); + setIndexPatternList(ipList); + return indexPatternData; + } + + async function loadSavedSearch() { + try { + // force a refresh if a given saved search without id was saved + setSavedSearch(undefined); + const loadedSavedSearch = await services.getSavedSearchById(savedSearchId); + const loadedIndexPattern = await loadDefaultOrCurrentIndexPattern(loadedSavedSearch); + if (loadedSavedSearch && !loadedSavedSearch?.searchSource.getField('index')) { + loadedSavedSearch.searchSource.setField('index', loadedIndexPattern); + } + setSavedSearch(loadedSavedSearch); + if (savedSearchId) { + chrome.recentlyAccessed.add( + ((loadedSavedSearch as unknown) as SavedObjectDeprecated).getFullPath(), + loadedSavedSearch.title, + loadedSavedSearch.id + ); + } + } catch (e) { + redirectWhenMissing({ + history, + navigateToApp: core.application.navigateToApp, + basePath, + mapping: { + search: '/', + 'index-pattern': { + app: 'management', + path: `kibana/objects/savedSearches/${id}`, + }, + }, + toastNotifications, + onBeforeRedirect() { + getUrlTracker().setTrackedUrl('/'); + }, + })(e); + } + } + + loadSavedSearch(); + }, [ + basePath, + chrome.recentlyAccessed, + config, + core.application.navigateToApp, + data.indexPatterns, + history, + id, + services, + toastNotifications, + ]); + + useEffect(() => { + chrome.setBreadcrumbs( + savedSearch && savedSearch.title + ? getSavedSearchBreadcrumbs(savedSearch.title) + : getRootBreadcrumbs() + ); + }, [chrome, savedSearch]); + + if (!indexPattern || !savedSearch) { + return ; + } + + return ( + + ); +} diff --git a/src/plugins/discover/public/application/apps/main/index.ts b/src/plugins/discover/public/application/apps/main/index.ts index af30b0c95343..f38b745da16a 100644 --- a/src/plugins/discover/public/application/apps/main/index.ts +++ b/src/plugins/discover/public/application/apps/main/index.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export { DiscoverMainApp } from './discover_main_app'; +export { DiscoverMainRoute } from './discover_main_route'; diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts index 4c3d819f063a..28f5f96acc14 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.test.ts @@ -22,14 +22,12 @@ describe('test useDiscoverState', () => { return useDiscoverState({ services: discoverServiceMock, history, - initialIndexPattern: indexPatternMock, - initialSavedSearch: savedSearchMock, + savedSearch: savedSearchMock, }); }); expect(result.current.state.index).toBe(indexPatternMock.id); expect(result.current.stateContainer).toBeInstanceOf(Object); expect(result.current.setState).toBeInstanceOf(Function); - expect(result.current.savedSearch.id).toBe(savedSearchMock.id); expect(result.current.searchSource).toBeInstanceOf(SearchSource); }); @@ -40,8 +38,7 @@ describe('test useDiscoverState', () => { return useDiscoverState({ services: discoverServiceMock, history, - initialIndexPattern: indexPatternMock, - initialSavedSearch: savedSearchMock, + savedSearch: savedSearchMock, }); }); await act(async () => { @@ -57,8 +54,7 @@ describe('test useDiscoverState', () => { return useDiscoverState({ services: discoverServiceMock, history, - initialIndexPattern: indexPatternMock, - initialSavedSearch: savedSearchMock, + savedSearch: savedSearchMock, }); }); diff --git a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts index a5a064a8fc1c..afe010379cff 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_discover_state.ts @@ -10,7 +10,6 @@ import { isEqual } from 'lodash'; import { History } from 'history'; import { getState } from './discover_state'; import { getStateDefaults } from '../utils/get_state_defaults'; -import { IndexPattern } from '../../../../../../data/public'; import { DiscoverServices } from '../../../../build_services'; import { SavedSearch } from '../../../../saved_searches'; import { loadIndexPattern } from '../utils/resolve_index_pattern'; @@ -29,24 +28,22 @@ import { SortPairArr } from '../components/doc_table/lib/get_sort'; export function useDiscoverState({ services, history, - initialIndexPattern, - initialSavedSearch, + savedSearch, }: { services: DiscoverServices; - initialSavedSearch: SavedSearch; + savedSearch: SavedSearch; history: History; - initialIndexPattern: IndexPattern; }) { const { uiSettings: config, data, filterManager, indexPatterns } = services; - const [indexPattern, setIndexPattern] = useState(initialIndexPattern); - const [savedSearch, setSavedSearch] = useState(initialSavedSearch); const useNewFieldsApi = useMemo(() => !config.get(SEARCH_FIELDS_FROM_SOURCE), [config]); const { timefilter } = data.query.timefilter; + const indexPattern = savedSearch.searchSource.getField('index')!; + const searchSource = useMemo(() => { savedSearch.searchSource.setField('index', indexPattern); return savedSearch.searchSource.createChild(); - }, [savedSearch.searchSource, indexPattern]); + }, [savedSearch, indexPattern]); const stateContainer = useMemo( () => @@ -121,12 +118,10 @@ export function useDiscoverState({ * That's because appState is updated before savedSearchData$ * The following line of code catches this, but should be improved */ - reset(); const nextIndexPattern = await loadIndexPattern(nextState.index, indexPatterns, config); + savedSearch.searchSource.setField('index', nextIndexPattern.loaded); - if (nextIndexPattern) { - setIndexPattern(nextIndexPattern.loaded); - } + reset(); } if (chartDisplayChanged || chartIntervalChanged || docTableSortChanged) { @@ -135,7 +130,17 @@ export function useDiscoverState({ setState(nextState); }); return () => unsubscribe(); - }, [config, indexPatterns, appStateContainer, setState, state, refetch$, reset]); + }, [ + config, + indexPatterns, + appStateContainer, + setState, + state, + refetch$, + data$, + reset, + savedSearch.searchSource, + ]); /** * function to revert any changes to a given saved search @@ -151,11 +156,8 @@ export function useDiscoverState({ }); await stateContainer.replaceUrlAppState(newAppState); setState(newAppState); - if (savedSearch.id !== newSavedSearch.id) { - setSavedSearch(newSavedSearch); - } }, - [services, indexPattern, config, data, stateContainer, savedSearch.id] + [services, indexPattern, config, data, stateContainer] ); /** @@ -191,6 +193,20 @@ export function useDiscoverState({ [refetch$, searchSessionManager] ); + useEffect(() => { + if (!savedSearch || !savedSearch.id) { + return; + } + // handling pushing to state of a persisted saved object + const newAppState = getStateDefaults({ + config, + data, + savedSearch, + }); + stateContainer.replaceUrlAppState(newAppState); + setState(newAppState); + }, [config, data, savedSearch, reset, stateContainer]); + /** * Initial data fetching, also triggered when index pattern changes */ @@ -211,7 +227,6 @@ export function useDiscoverState({ resetSavedSearch, onChangeIndexPattern, onUpdateQuery, - savedSearch, searchSource, setState, state, diff --git a/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts b/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts index f4d05e551a4a..7f252151920f 100644 --- a/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts +++ b/src/plugins/discover/public/application/apps/main/services/use_saved_search.test.ts @@ -10,7 +10,6 @@ import { renderHook } from '@testing-library/react-hooks'; import { createSearchSessionMock } from '../../../../__mocks__/search_session'; import { discoverServiceMock } from '../../../../__mocks__/services'; import { savedSearchMock } from '../../../../__mocks__/saved_search'; -import { indexPatternMock } from '../../../../__mocks__/index_pattern'; import { useSavedSearch } from './use_saved_search'; import { getState } from './discover_state'; import { uiSettingsMock } from '../../../../__mocks__/ui_settings'; @@ -59,8 +58,7 @@ describe('test useSavedSearch', () => { return useDiscoverState({ services: discoverServiceMock, history, - initialIndexPattern: indexPatternMock, - initialSavedSearch: savedSearchMock, + savedSearch: savedSearchMock, }); }); @@ -101,8 +99,7 @@ describe('test useSavedSearch', () => { return useDiscoverState({ services: discoverServiceMock, history, - initialIndexPattern: indexPatternMock, - initialSavedSearch: savedSearchMock, + savedSearch: savedSearchMock, }); }); diff --git a/src/plugins/discover/public/application/apps/not_found/index.ts b/src/plugins/discover/public/application/apps/not_found/index.ts new file mode 100644 index 000000000000..939af542fdf6 --- /dev/null +++ b/src/plugins/discover/public/application/apps/not_found/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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export { NotFoundRoute } from './not_found_route'; diff --git a/src/plugins/discover/public/application/apps/not_found/not_found_route.tsx b/src/plugins/discover/public/application/apps/not_found/not_found_route.tsx new file mode 100644 index 000000000000..ff515f27201a --- /dev/null +++ b/src/plugins/discover/public/application/apps/not_found/not_found_route.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React, { useEffect } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiCallOut } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { toMountPoint } from '../../../../../kibana_react/public'; +import { DiscoverServices } from '../../../build_services'; +import { getUrlTracker } from '../../../kibana_services'; + +export interface NotFoundRouteProps { + /** + * Kibana core services used by discover + */ + services: DiscoverServices; +} +let bannerId: string | undefined; + +export function NotFoundRoute(props: NotFoundRouteProps) { + const { services } = props; + const { urlForwarding } = services; + + useEffect(() => { + const path = window.location.hash.substr(1); + getUrlTracker().restorePreviousUrl(); + const { navigated } = urlForwarding.navigateToLegacyKibanaUrl(path); + if (!navigated) { + urlForwarding.navigateToDefaultApp(); + } + + const bannerMessage = i18n.translate('discover.noMatchRoute.bannerTitleText', { + defaultMessage: 'Page not found', + }); + + bannerId = services.core.overlays.banners.replace( + bannerId, + toMountPoint( + +

+ +

+
+ ) + ); + + // hide the message after the user has had a chance to acknowledge it -- so it doesn't permanently stick around + setTimeout(() => { + if (bannerId) { + services.core.overlays.banners.remove(bannerId); + } + }, 15000); + }, [services.core.overlays.banners, services.history, urlForwarding]); + + return null; +} diff --git a/src/plugins/discover/public/application/components/common/__snapshots__/loading_indicator.test.tsx.snap b/src/plugins/discover/public/application/components/common/__snapshots__/loading_indicator.test.tsx.snap new file mode 100644 index 000000000000..21f8a2b2c363 --- /dev/null +++ b/src/plugins/discover/public/application/components/common/__snapshots__/loading_indicator.test.tsx.snap @@ -0,0 +1,30 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Loading indicator renders correctly 1`] = ` + + +
+ +
+ + + +
+
+
+
+
+`; diff --git a/src/plugins/discover/public/application/components/common/loading_indicator.test.tsx b/src/plugins/discover/public/application/components/common/loading_indicator.test.tsx new file mode 100644 index 000000000000..1615333471d8 --- /dev/null +++ b/src/plugins/discover/public/application/components/common/loading_indicator.test.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { LoadingIndicator } from './loading_indicator'; +import React from 'react'; +import { mount } from 'enzyme'; + +describe('Loading indicator', () => { + it('renders correctly', () => { + const component = mount(); + expect(component).toMatchSnapshot(); + }); +}); diff --git a/src/plugins/discover/public/application/components/common/loading_indicator.tsx b/src/plugins/discover/public/application/components/common/loading_indicator.tsx new file mode 100644 index 000000000000..5261e374dfaf --- /dev/null +++ b/src/plugins/discover/public/application/components/common/loading_indicator.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; +import React from 'react'; + +export const LoadingIndicator = () => { + return ( + + + + + + ); +}; diff --git a/src/plugins/discover/public/application/components/context_app/context_app.tsx b/src/plugins/discover/public/application/components/context_app/context_app.tsx index 25590f331839..4121beab1dd2 100644 --- a/src/plugins/discover/public/application/components/context_app/context_app.tsx +++ b/src/plugins/discover/public/application/components/context_app/context_app.tsx @@ -66,7 +66,7 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp * Fetch docs on ui changes */ useEffect(() => { - if (!prevAppState.current) { + if (!prevAppState.current || fetchedState.anchor._id !== anchorId) { fetchAllRows(); } else if (prevAppState.current.predecessorCount !== appState.predecessorCount) { fetchSurroundingRows(SurrDocType.PREDECESSORS); @@ -77,7 +77,15 @@ export const ContextApp = ({ indexPattern, indexPatternId, anchorId }: ContextAp } prevAppState.current = cloneDeep(appState); - }, [appState, indexPatternId, anchorId, fetchContextRows, fetchAllRows, fetchSurroundingRows]); + }, [ + appState, + indexPatternId, + anchorId, + fetchContextRows, + fetchAllRows, + fetchSurroundingRows, + fetchedState.anchor._id, + ]); const { columns, onAddColumn, onRemoveColumn, onSetColumns } = useDataGridColumns({ capabilities, diff --git a/src/plugins/discover/public/application/discover_router.test.tsx b/src/plugins/discover/public/application/discover_router.test.tsx new file mode 100644 index 000000000000..59aede76c686 --- /dev/null +++ b/src/plugins/discover/public/application/discover_router.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Route, RouteProps } from 'react-router-dom'; +import { createSearchSessionMock } from '../__mocks__/search_session'; +import { discoverServiceMock as mockDiscoverServices } from '../__mocks__/services'; +import { discoverRouter } from './discover_router'; +import { DiscoverMainRoute } from './apps/main'; +import { DiscoverMainProps } from './apps/main/discover_main_route'; +import { SingleDocRoute } from './apps/doc'; +import { ContextAppRoute } from './apps/context'; + +const pathMap: Record = {}; +let mainRouteProps: DiscoverMainProps; + +describe('Discover router', () => { + beforeAll(() => { + const { history } = createSearchSessionMock(); + mainRouteProps = { + history, + services: mockDiscoverServices, + }; + const component = shallow(discoverRouter(mockDiscoverServices, history)); + component.find(Route).forEach((route) => { + const routeProps = route.props() as RouteProps; + const path = routeProps.path; + const children = routeProps.children; + if (typeof path === 'string') { + // @ts-expect-error + pathMap[path] = children; + } + }); + }); + + it('should show DiscoverMainRoute component for / route', () => { + expect(pathMap['/']).toMatchObject(); + }); + + it('should show DiscoverMainRoute component for /view/:id route', () => { + expect(pathMap['/view/:id']).toMatchObject(); + }); + + it('should show SingleDocRoute component for /doc/:indexPatternId/:index route', () => { + expect(pathMap['/doc/:indexPatternId/:index']).toMatchObject( + + ); + }); + + it('should show ContextAppRoute component for /context/:indexPatternId/:id route', () => { + expect(pathMap['/context/:indexPatternId/:id']).toMatchObject( + + ); + }); +}); diff --git a/src/plugins/discover/public/application/discover_router.tsx b/src/plugins/discover/public/application/discover_router.tsx new file mode 100644 index 000000000000..7c7921935a7f --- /dev/null +++ b/src/plugins/discover/public/application/discover_router.tsx @@ -0,0 +1,52 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { Redirect, Route, Router, Switch } from 'react-router-dom'; +import React from 'react'; +import { History } from 'history'; +import { KibanaContextProvider } from '../../../kibana_react/public'; +import { ContextAppRoute } from './apps/context'; +import { SingleDocRoute } from './apps/doc'; +import { DiscoverMainRoute } from './apps/main'; +import { NotFoundRoute } from './apps/not_found'; +import { DiscoverServices } from '../build_services'; +import { DiscoverMainProps } from './apps/main/discover_main_route'; + +export const discoverRouter = (services: DiscoverServices, history: History) => { + const mainRouteProps: DiscoverMainProps = { + services, + history, + }; + return ( + + + + } + /> + ( + + )} + /> + } + /> + } /> + } /> + + + + + ); +}; diff --git a/src/plugins/discover/public/application/embeddable/constants.ts b/src/plugins/discover/public/application/embeddable/constants.ts index 8fe927928065..57ff91049cd0 100644 --- a/src/plugins/discover/public/application/embeddable/constants.ts +++ b/src/plugins/discover/public/application/embeddable/constants.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export const SEARCH_EMBEDDABLE_TYPE = 'search'; +export { SEARCH_EMBEDDABLE_TYPE } from '../../../common/index'; diff --git a/src/plugins/discover/public/application/helpers/breadcrumbs.ts b/src/plugins/discover/public/application/helpers/breadcrumbs.ts index 8a8d0e7027c6..fe420328a317 100644 --- a/src/plugins/discover/public/application/helpers/breadcrumbs.ts +++ b/src/plugins/discover/public/application/helpers/breadcrumbs.ts @@ -21,12 +21,11 @@ export function getRootBreadcrumbs() { ]; } -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function getSavedSearchBreadcrumbs($route: any) { +export function getSavedSearchBreadcrumbs(id: string) { return [ ...getRootBreadcrumbs(), { - text: $route.current.locals.savedObjects.savedSearch.id, + text: id, }, ]; } diff --git a/src/plugins/discover/public/application/helpers/use_index_pattern.test.tsx b/src/plugins/discover/public/application/helpers/use_index_pattern.test.tsx new file mode 100644 index 000000000000..85282afb6fc3 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/use_index_pattern.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { useIndexPattern } from './use_index_pattern'; +import { indexPatternMock } from '../../__mocks__/index_pattern'; +import { indexPatternsMock } from '../../__mocks__/index_patterns'; +import { renderHook, act } from '@testing-library/react-hooks'; + +describe('Use Index Pattern', () => { + test('returning a valid index pattern', async () => { + const { result } = renderHook(() => useIndexPattern(indexPatternsMock, 'the-index-pattern-id')); + await act(() => Promise.resolve()); + expect(result.current).toBe(indexPatternMock); + }); +}); diff --git a/src/plugins/discover/public/application/helpers/use_index_pattern.tsx b/src/plugins/discover/public/application/helpers/use_index_pattern.tsx new file mode 100644 index 000000000000..f53d131920c5 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/use_index_pattern.tsx @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import { useEffect, useState } from 'react'; +import { IndexPattern, IndexPatternsContract } from '../../../../data/common'; + +export const useIndexPattern = (indexPatterns: IndexPatternsContract, indexPatternId: string) => { + const [indexPattern, setIndexPattern] = useState(undefined); + + useEffect(() => { + async function loadIndexPattern() { + const ip = await indexPatterns.get(indexPatternId); + setIndexPattern(ip); + } + loadIndexPattern(); + }); + return indexPattern; +}; diff --git a/src/plugins/discover/public/application/index.tsx b/src/plugins/discover/public/application/index.tsx new file mode 100644 index 000000000000..4ac50eecd518 --- /dev/null +++ b/src/plugins/discover/public/application/index.tsx @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ +import ReactDOM from 'react-dom'; + +import { AppMountParameters } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { getServices } from '../kibana_services'; +import { discoverRouter } from './discover_router'; + +export const renderApp = ({ element }: AppMountParameters) => { + const services = getServices(); + const { history: getHistory, capabilities, chrome, data } = services; + + const history = getHistory(); + if (!capabilities.discover.save) { + chrome.setBadge({ + text: i18n.translate('discover.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + tooltip: i18n.translate('discover.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save searches', + }), + iconType: 'glasses', + }); + } + const app = discoverRouter(services, history); + ReactDOM.render(app, element); + + return () => { + data.search.session.clear(); + ReactDOM.unmountComponentAtNode(element); + }; +}; diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index b42bf6a81742..c8e641088afa 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -17,6 +17,7 @@ import { ToastsStart, IUiSettingsClient, PluginInitializerContext, + HttpStart, } from 'kibana/public'; import { FilterManager, @@ -62,6 +63,7 @@ export interface DiscoverServices { uiSettings: IUiSettingsClient; trackUiMetric?: (metricType: UiCounterMetricType, eventName: string | string[]) => void; indexPatternFieldEditor: IndexPatternFieldEditorStart; + http: HttpStart; } export async function buildServices( @@ -104,5 +106,6 @@ export async function buildServices( uiSettings: core.uiSettings, trackUiMetric: usageCollection?.reportUiCounter.bind(usageCollection, 'discover'), indexPatternFieldEditor: plugins.indexPatternFieldEditor, + http: core.http, }; } diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index f657b24a5822..65fc3ce2a82f 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -336,6 +336,11 @@ export class DiscoverPlugin setHeaderActionMenuMounter(params.setHeaderActionMenu); syncHistoryLocations(); appMounted(); + // dispatch synthetic hash change event to update hash history objects + // this is necessary because hash updates triggered by using popState won't trigger this event naturally. + const unlistenParentHistory = params.history.listen(() => { + window.dispatchEvent(new HashChangeEvent('hashchange')); + }); const { plugins: { data: dataStart }, } = await this.initializeServices(); @@ -349,6 +354,7 @@ export class DiscoverPlugin const unmount = await renderApp(innerAngularName, params.element); return () => { params.element.classList.remove('dscAppWrapper'); + unlistenParentHistory(); unmount(); appUnMounted(); }; diff --git a/src/plugins/embeddable/kibana.json b/src/plugins/embeddable/kibana.json index 42dc716fe64e..25e95061ed2a 100644 --- a/src/plugins/embeddable/kibana.json +++ b/src/plugins/embeddable/kibana.json @@ -8,6 +8,6 @@ "githubTeam": "kibana-app-services" }, "requiredPlugins": ["inspector", "uiActions"], - "extraPublicDirs": ["public/lib/test_samples"], + "extraPublicDirs": ["public/lib/test_samples", "common"], "requiredBundles": ["savedObjects", "kibanaReact", "kibanaUtils"] } diff --git a/src/plugins/expressions/common/execution/execution.test.ts b/src/plugins/expressions/common/execution/execution.test.ts index 2e9d4b91908a..c478977f6076 100644 --- a/src/plugins/expressions/common/execution/execution.test.ts +++ b/src/plugins/expressions/common/execution/execution.test.ts @@ -763,13 +763,15 @@ describe('Execution', () => { }); test('saves duration it took to execute each function', async () => { + const startTime = Date.now(); const execution = createExecution('add val=1 | add val=2 | add val=3', {}, true); execution.start(-1); await execution.result.toPromise(); + const duration = Date.now() - startTime; for (const node of execution.state.get().ast.chain) { expect(typeof node.debug?.duration).toBe('number'); - expect(node.debug?.duration).toBeLessThan(100); + expect(node.debug?.duration).toBeLessThanOrEqual(duration); expect(node.debug?.duration).toBeGreaterThanOrEqual(0); } }); diff --git a/src/plugins/expressions/common/service/expressions_services.ts b/src/plugins/expressions/common/service/expressions_services.ts index b4dda3de5c93..2be4f5207bb8 100644 --- a/src/plugins/expressions/common/service/expressions_services.ts +++ b/src/plugins/expressions/common/service/expressions_services.ts @@ -54,6 +54,8 @@ export type ExpressionsServiceSetup = Pick< | 'registerType' | 'run' | 'fork' + | 'extract' + | 'inject' >; export interface ExpressionExecutionParams { diff --git a/src/plugins/expressions/public/mocks.tsx b/src/plugins/expressions/public/mocks.tsx index 84287aefe046..3a5450fc0283 100644 --- a/src/plugins/expressions/public/mocks.tsx +++ b/src/plugins/expressions/public/mocks.tsx @@ -16,6 +16,7 @@ export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { + extract: jest.fn(), fork: jest.fn(), getFunction: jest.fn(), getFunctions: jest.fn(), @@ -23,6 +24,7 @@ const createSetupContract = (): Setup => { getRenderers: jest.fn(), getType: jest.fn(), getTypes: jest.fn(), + inject: jest.fn(), registerFunction: jest.fn(), registerRenderer: jest.fn(), registerType: jest.fn(), diff --git a/src/plugins/expressions/server/mocks.ts b/src/plugins/expressions/server/mocks.ts index 9bc25d89a04c..f4379145f6a6 100644 --- a/src/plugins/expressions/server/mocks.ts +++ b/src/plugins/expressions/server/mocks.ts @@ -15,6 +15,7 @@ export type Start = jest.Mocked; const createSetupContract = (): Setup => { const setupContract: Setup = { + extract: jest.fn(), fork: jest.fn(), getFunction: jest.fn(), getFunctions: jest.fn(), @@ -22,6 +23,7 @@ const createSetupContract = (): Setup => { getRenderers: jest.fn(), getType: jest.fn(), getTypes: jest.fn(), + inject: jest.fn(), registerFunction: jest.fn(), registerRenderer: jest.fn(), registerType: jest.fn(), diff --git a/src/plugins/home/kibana.json b/src/plugins/home/kibana.json index ccf8d39307f3..b3bd915bee14 100644 --- a/src/plugins/home/kibana.json +++ b/src/plugins/home/kibana.json @@ -7,9 +7,7 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "urlForwarding"], + "requiredPlugins": ["data", "share", "urlForwarding"], "optionalPlugins": ["usageCollection", "telemetry"], - "requiredBundles": [ - "kibanaReact" - ] + "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/home/public/application/components/home.tsx b/src/plugins/home/public/application/components/home.tsx index 0572d7b80f98..d398311d3025 100644 --- a/src/plugins/home/public/application/components/home.tsx +++ b/src/plugins/home/public/application/components/home.tsx @@ -87,7 +87,7 @@ export class Home extends Component { if (this.state.isLoading) { this.setState({ isWelcomeEnabled: false }); } - }, 500); + }, 10000); const hasUserIndexPattern = await this.props.hasUserIndexPattern(); diff --git a/src/plugins/home/public/application/components/manage_data/__snapshots__/manage_data.test.tsx.snap b/src/plugins/home/public/application/components/manage_data/__snapshots__/manage_data.test.tsx.snap index 85727f76b974..401af74231e5 100644 --- a/src/plugins/home/public/application/components/manage_data/__snapshots__/manage_data.test.tsx.snap +++ b/src/plugins/home/public/application/components/manage_data/__snapshots__/manage_data.test.tsx.snap @@ -151,7 +151,7 @@ exports[`ManageData render 1`] = ` className="kbnOverviewPageHeader__actionButton" data-test-subj="homeDevTools" flush="both" - href="/app/dev_tools#/console" + href="" iconType="wrench" > { jest.mock('../../kibana_services', () => ({ getServices: () => ({ + share: { url: { locators: { get: () => ({ useUrl: () => '' }) } } }, trackUiMetric: jest.fn(), }), })); diff --git a/src/plugins/home/public/application/components/manage_data/manage_data.tsx b/src/plugins/home/public/application/components/manage_data/manage_data.tsx index fb60b7790aa5..746afad9d5b5 100644 --- a/src/plugins/home/public/application/components/manage_data/manage_data.tsx +++ b/src/plugins/home/public/application/components/manage_data/manage_data.tsx @@ -32,9 +32,13 @@ interface Props { } export const ManageData: FC = ({ addBasePath, application, features }) => { - if (features.length) { - const { trackUiMetric } = getServices(); + const { share, trackUiMetric } = getServices(); + const consoleHref = share.url.locators.get('CONSOLE_APP_LOCATOR')?.useUrl({}); + const managementHref = share.url.locators + .get('MANAGEMENT_APP_LOCATOR') + ?.useUrl({ sectionId: '' }); + if (features.length) { const { management: isManagementEnabled, dev_tools: isDevToolsEnabled, @@ -67,7 +71,7 @@ export const ManageData: FC = ({ addBasePath, application, features }) => className="kbnOverviewPageHeader__actionButton" flush="both" iconType="wrench" - href={addBasePath('/app/dev_tools#/console')} + href={consoleHref} > = ({ addBasePath, application, features }) => className="kbnOverviewPageHeader__actionButton" flush="both" iconType="gear" - href={addBasePath('/app/management')} + href={managementHref} > { beforeEach(() => { @@ -26,6 +28,7 @@ describe('HomePublicPlugin', () => { const setup = await new HomePublicPlugin(mockInitializerContext).setup( coreMock.createSetup() as any, { + share: mockShare, urlForwarding: urlForwardingPluginMock.createSetupContract(), } ); @@ -45,6 +48,7 @@ describe('HomePublicPlugin', () => { const setup = await new HomePublicPlugin(mockInitializerContext).setup( coreMock.createSetup() as any, { + share: mockShare, urlForwarding: urlForwardingPluginMock.createSetupContract(), } ); @@ -56,6 +60,7 @@ describe('HomePublicPlugin', () => { const setup = await new HomePublicPlugin(mockInitializerContext).setup( coreMock.createSetup() as any, { + share: {} as SharePluginSetup, urlForwarding: urlForwardingPluginMock.createSetupContract(), } ); @@ -67,6 +72,7 @@ describe('HomePublicPlugin', () => { const setup = await new HomePublicPlugin(mockInitializerContext).setup( coreMock.createSetup() as any, { + share: mockShare, urlForwarding: urlForwardingPluginMock.createSetupContract(), } ); diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index b3b5ce487b74..7dd1d8728ad7 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -35,6 +35,7 @@ import { UsageCollectionSetup } from '../../usage_collection/public'; import { UrlForwardingSetup, UrlForwardingStart } from '../../url_forwarding/public'; import { AppNavLinkStatus } from '../../../core/public'; import { PLUGIN_ID, HOME_APP_BASE_PATH } from '../common/constants'; +import { SharePluginSetup } from '../../share/public'; export interface HomePluginStartDependencies { data: DataPublicPluginStart; @@ -43,6 +44,7 @@ export interface HomePluginStartDependencies { } export interface HomePluginSetupDependencies { + share: SharePluginSetup; usageCollection?: UsageCollectionSetup; urlForwarding: UrlForwardingSetup; } @@ -64,7 +66,7 @@ export class HomePublicPlugin public setup( core: CoreSetup, - { urlForwarding, usageCollection }: HomePluginSetupDependencies + { share, urlForwarding, usageCollection }: HomePluginSetupDependencies ): HomePublicPluginSetup { core.application.register({ id: PLUGIN_ID, @@ -79,6 +81,7 @@ export class HomePublicPlugin { telemetry, data, urlForwarding: urlForwardingStart }, ] = await core.getStartServices(); setServices({ + share, trackUiMetric, kibanaVersion: this.initializerContext.env.packageInfo.version, http: coreStart.http, diff --git a/src/plugins/home/tsconfig.json b/src/plugins/home/tsconfig.json index 9324978b227d..f43c40e35349 100644 --- a/src/plugins/home/tsconfig.json +++ b/src/plugins/home/tsconfig.json @@ -7,18 +7,14 @@ "declarationMap": true, "isolatedModules": true }, - "include": [ - "common/**/*", - "public/**/*", - "server/**/*", - "config.ts", - ], + "include": ["common/**/*", "public/**/*", "server/**/*", "config.ts"], "references": [ { "path": "../../core/tsconfig.json" }, { "path": "../data/tsconfig.json" }, { "path": "../kibana_react/tsconfig.json" }, + { "path": "../share/tsconfig.json" }, { "path": "../url_forwarding/tsconfig.json" }, { "path": "../usage_collection/tsconfig.json" }, - { "path": "../telemetry/tsconfig.json" }, + { "path": "../telemetry/tsconfig.json" } ] } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index a8f89b471e4e..f23696ac6c7d 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -88,7 +88,7 @@ export const EditIndexPattern = withRouter( const removePattern = () => { async function doRemove() { if (indexPattern.id === defaultIndex) { - const indexPatterns = await data.indexPatterns.getIdsWithTitle(); + const indexPatterns = await data.dataViews.getIdsWithTitle(); uiSettings.remove('defaultIndex'); const otherPatterns = filter(indexPatterns, (pattern) => { return pattern.id !== indexPattern.id; @@ -99,7 +99,7 @@ export const EditIndexPattern = withRouter( } } if (indexPattern.id) { - Promise.resolve(data.indexPatterns.delete(indexPattern.id)).then(function () { + Promise.resolve(data.dataViews.delete(indexPattern.id)).then(function () { history.push(''); }); } diff --git a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx index 0a436f154161..c8617808b10e 100644 --- a/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx +++ b/src/plugins/index_pattern_management/public/components/index_pattern_table/index_pattern_table.tsx @@ -77,13 +77,13 @@ export const IndexPatternTable = ({ (async function () { const gettedIndexPatterns: IndexPatternTableItem[] = await getIndexPatterns( uiSettings.get('defaultIndex'), - data.indexPatterns + data.dataViews ); setIndexPatterns(gettedIndexPatterns); setIsLoadingIndexPatterns(false); if ( gettedIndexPatterns.length === 0 || - !(await data.indexPatterns.hasUserIndexPattern().catch(() => false)) + !(await data.dataViews.hasUserIndexPattern().catch(() => false)) ) { setShowCreateDialog(true); } diff --git a/src/plugins/inspector/common/adapters/request/index.ts b/src/plugins/inspector/common/adapters/request/index.ts index 6cee1c0588d7..807f11569ba2 100644 --- a/src/plugins/inspector/common/adapters/request/index.ts +++ b/src/plugins/inspector/common/adapters/request/index.ts @@ -6,6 +6,6 @@ * Side Public License, v 1. */ -export { RequestStatistic, RequestStatistics, RequestStatus } from './types'; +export { Request, RequestStatistic, RequestStatistics, RequestStatus } from './types'; export { RequestAdapter } from './request_adapter'; export { RequestResponder } from './request_responder'; diff --git a/src/plugins/inspector/common/adapters/request/request_adapter.ts b/src/plugins/inspector/common/adapters/request/request_adapter.ts index 3da528fb3082..913f16f74b8e 100644 --- a/src/plugins/inspector/common/adapters/request/request_adapter.ts +++ b/src/plugins/inspector/common/adapters/request/request_adapter.ts @@ -33,14 +33,19 @@ export class RequestAdapter extends EventEmitter { * {@link RequestResponder#error}. * * @param {string} name The name of this request as it should be shown in the UI. - * @param {object} args Additional arguments for the request. + * @param {RequestParams} params Additional arguments for the request. + * @param {number} [startTime] Set an optional start time for the request * @return {RequestResponder} An instance to add information to the request and finish it. */ - public start(name: string, params: RequestParams = {}): RequestResponder { + public start( + name: string, + params: RequestParams = {}, + startTime: number = Date.now() + ): RequestResponder { const req: Request = { ...params, name, - startTime: Date.now(), + startTime, status: RequestStatus.PENDING, id: params.id ?? uuid(), }; diff --git a/src/plugins/inspector/common/index.ts b/src/plugins/inspector/common/index.ts index 224500b6c43a..e92c9b670475 100644 --- a/src/plugins/inspector/common/index.ts +++ b/src/plugins/inspector/common/index.ts @@ -8,6 +8,7 @@ export { Adapters, + Request, RequestAdapter, RequestStatistic, RequestStatistics, diff --git a/src/plugins/inspector/kibana.json b/src/plugins/inspector/kibana.json index 66c6617924a7..28327e8d3b14 100644 --- a/src/plugins/inspector/kibana.json +++ b/src/plugins/inspector/kibana.json @@ -8,5 +8,6 @@ "githubTeam": "kibana-app-services" }, "extraPublicDirs": ["common", "common/adapters/request"], - "requiredBundles": ["kibanaReact"] + "requiredBundles": ["kibanaReact"], + "requiredPlugins": ["share"] } diff --git a/src/plugins/inspector/public/mocks.ts b/src/plugins/inspector/public/mocks.ts index 0ef519f9a2b1..25f339825409 100644 --- a/src/plugins/inspector/public/mocks.ts +++ b/src/plugins/inspector/public/mocks.ts @@ -10,6 +10,7 @@ import { Setup as PluginSetup, Start as PluginStart } from '.'; import { InspectorViewRegistry } from './view_registry'; import { plugin as pluginInitializer } from '.'; import { coreMock } from '../../../core/public/mocks'; +import type { SharePluginStart } from '../../share/public'; export type Setup = jest.Mocked; export type Start = jest.Mocked; @@ -48,6 +49,7 @@ const createPlugin = async () => { const coreStart = coreMock.createStart(); const plugin = pluginInitializer(pluginInitializerContext); const setup = await plugin.setup(coreSetup); + const share = {} as SharePluginStart; return { pluginInitializerContext, @@ -55,7 +57,7 @@ const createPlugin = async () => { coreStart, plugin, setup, - doStart: async () => await plugin.start(coreStart), + doStart: async () => await plugin.start(coreStart, { share }), }; }; diff --git a/src/plugins/inspector/public/plugin.tsx b/src/plugins/inspector/public/plugin.tsx index fca51adf0f65..e561a9719b3f 100644 --- a/src/plugins/inspector/public/plugin.tsx +++ b/src/plugins/inspector/public/plugin.tsx @@ -10,6 +10,7 @@ import { i18n } from '@kbn/i18n'; import * as React from 'react'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { toMountPoint } from '../../kibana_react/public'; +import { SharePluginStart } from '../../share/public'; import { InspectorViewRegistry } from './view_registry'; import { InspectorOptions, InspectorSession } from './types'; import { InspectorPanel } from './ui/inspector_panel'; @@ -17,6 +18,10 @@ import { Adapters } from '../common'; import { getRequestsViewDescription } from './views'; +export interface InspectorPluginStartDeps { + share: SharePluginStart; +} + export interface Setup { registerView: InspectorViewRegistry['register']; @@ -70,7 +75,7 @@ export class InspectorPublicPlugin implements Plugin { }; } - public start(core: CoreStart) { + public start(core: CoreStart, startDeps: InspectorPluginStartDeps) { const isAvailable: Start['isAvailable'] = (adapters) => this.views!.getVisible(adapters).length > 0; @@ -99,6 +104,7 @@ export class InspectorPublicPlugin implements Plugin { application: core.application, http: core.http, uiSettings: core.uiSettings, + share: startDeps.share, }} /> ), diff --git a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap index cafe65242bd1..0dafd8073e9c 100644 --- a/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap +++ b/src/plugins/inspector/public/ui/__snapshots__/inspector_panel.test.tsx.snap @@ -12,8 +12,76 @@ exports[`InspectorPanel should render as expected 1`] = ` } dependencies={ Object { - "application": Object {}, + "application": Object { + "applications$": BehaviorSubject { + "_isScalar": false, + "_value": Map {}, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [], + "thrownError": null, + }, + "capabilities": Object { + "catalogue": Object {}, + "management": Object {}, + "navLinks": Object {}, + }, + "currentAppId$": Observable { + "_isScalar": false, + "source": Subject { + "_isScalar": false, + "closed": false, + "hasError": false, + "isStopped": false, + "observers": Array [ + Subscriber { + "_parentOrParents": null, + "_subscriptions": Array [ + SubjectSubscription { + "_parentOrParents": [Circular], + "_subscriptions": null, + "closed": false, + "subject": [Circular], + "subscriber": [Circular], + }, + ], + "closed": false, + "destination": SafeSubscriber { + "_complete": undefined, + "_context": [Circular], + "_error": undefined, + "_next": [Function], + "_parentOrParents": null, + "_parentSubscriber": [Circular], + "_subscriptions": null, + "closed": false, + "destination": Object { + "closed": true, + "complete": [Function], + "error": [Function], + "next": [Function], + }, + "isStopped": false, + "syncErrorThrowable": false, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + "isStopped": false, + "syncErrorThrowable": true, + "syncErrorThrown": false, + "syncErrorValue": null, + }, + ], + "thrownError": null, + }, + }, + "getUrlForApp": [MockFunction], + "navigateToApp": [MockFunction], + "navigateToUrl": [MockFunction], + }, "http": Object {}, + "share": Object {}, "uiSettings": Object {}, } } @@ -145,8 +213,76 @@ exports[`InspectorPanel should render as expected 1`] = ` - } - > - +
-

- View 1 -

- - + } + > + +

+ View 1 +

+
+
+
+ diff --git a/src/plugins/inspector/public/ui/inspector_panel.test.tsx b/src/plugins/inspector/public/ui/inspector_panel.test.tsx index c40dd309c3ed..879cd3d18195 100644 --- a/src/plugins/inspector/public/ui/inspector_panel.test.tsx +++ b/src/plugins/inspector/public/ui/inspector_panel.test.tsx @@ -12,15 +12,23 @@ import { InspectorPanel } from './inspector_panel'; import { InspectorViewDescription } from '../types'; import { Adapters } from '../../common'; import type { ApplicationStart, HttpSetup, IUiSettingsClient } from 'kibana/public'; +import { SharePluginStart } from '../../../share/public'; +import { applicationServiceMock } from '../../../../core/public/mocks'; describe('InspectorPanel', () => { let adapters: Adapters; let views: InspectorViewDescription[]; - const dependencies = { - application: {}, + const dependencies = ({ + application: applicationServiceMock.createStartContract(), http: {}, + share: {}, uiSettings: {}, - } as { application: ApplicationStart; http: HttpSetup; uiSettings: IUiSettingsClient }; + } as unknown) as { + application: ApplicationStart; + http: HttpSetup; + share: SharePluginStart; + uiSettings: IUiSettingsClient; + }; beforeEach(() => { adapters = { diff --git a/src/plugins/inspector/public/ui/inspector_panel.tsx b/src/plugins/inspector/public/ui/inspector_panel.tsx index 34ab6d15941b..af59516a50e4 100644 --- a/src/plugins/inspector/public/ui/inspector_panel.tsx +++ b/src/plugins/inspector/public/ui/inspector_panel.tsx @@ -22,7 +22,8 @@ import { ApplicationStart, HttpStart, IUiSettingsClient } from 'kibana/public'; import { InspectorViewDescription } from '../types'; import { Adapters } from '../../common'; import { InspectorViewChooser } from './inspector_view_chooser'; -import { KibanaContextProvider } from '../../../kibana_react/public'; +import { KibanaContextProvider, RedirectAppLinks } from '../../../kibana_react/public'; +import { SharePluginStart } from '../../../share/public'; function hasAdaptersChanged(oldAdapters: Adapters, newAdapters: Adapters) { return ( @@ -44,6 +45,7 @@ interface InspectorPanelProps { application: ApplicationStart; http: HttpStart; uiSettings: IUiSettingsClient; + share: SharePluginStart; }; } @@ -133,7 +135,9 @@ export class InspectorPanel extends Component - {this.renderSelectedPanel()} + + {this.renderSelectedPanel()} + ); diff --git a/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx b/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx index a49dae164c99..12946866f3ec 100644 --- a/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx +++ b/src/plugins/inspector/public/views/requests/components/details/req_code_viewer.tsx @@ -6,18 +6,13 @@ * Side Public License, v 1. */ -// Since we're not using `RedirectAppLinks`, we need to use `navigateToUrl` when -// handling the click of the Open in Dev Tools link. We want to have both an -// `onClick` handler and an `href` attribute so it will work on click without a -// page reload, and on right-click to open in new tab. -/* eslint-disable @elastic/eui/href-or-on-click */ - import { EuiButtonEmpty, EuiCopy, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { XJsonLang } from '@kbn/monaco'; import { compressToEncodedURIComponent } from 'lz-string'; -import React, { MouseEvent, useCallback } from 'react'; +import React from 'react'; import { CodeEditor, useKibana } from '../../../../../../kibana_react/public'; +import { InspectorPluginStartDeps } from '../../../../plugin'; interface RequestCodeViewerProps { indexPattern?: string; @@ -36,24 +31,14 @@ const openInDevToolsLabel = i18n.translate('inspector.requests.openInDevToolsLab * @internal */ export const RequestCodeViewer = ({ indexPattern, json }: RequestCodeViewerProps) => { - const { services } = useKibana(); - const prepend = services.http?.basePath?.prepend; - const navigateToUrl = services.application?.navigateToUrl; + const { services } = useKibana(); const canShowDevTools = services.application?.capabilities?.dev_tools.show; const devToolsDataUri = compressToEncodedURIComponent(`GET ${indexPattern}/_search\n${json}`); - const devToolsUrl = `/app/dev_tools#/console?load_from=data:text/plain,${devToolsDataUri}`; + const devToolsHref = services.share.url.locators + .get('CONSOLE_APP_LOCATOR') + ?.useUrl({ loadFrom: `data:text/plain,${devToolsDataUri}` }); const shouldShowDevToolsLink = !!(indexPattern && canShowDevTools); - const handleDevToolsLinkClick = useCallback( - (event: MouseEvent) => { - event.preventDefault(); - if (navigateToUrl && prepend) { - navigateToUrl(prepend(devToolsUrl)); - } - }, - [devToolsUrl, navigateToUrl, prepend] - ); - return ( {openInDevToolsLabel} diff --git a/src/plugins/inspector/tsconfig.json b/src/plugins/inspector/tsconfig.json index 4554a90821d4..fd82c73d087c 100644 --- a/src/plugins/inspector/tsconfig.json +++ b/src/plugins/inspector/tsconfig.json @@ -9,6 +9,7 @@ "include": ["common/**/*", "public/**/*", "index.ts"], "references": [ { "path": "../../core/tsconfig.json" }, - { "path": "../kibana_react/tsconfig.json" } + { "path": "../kibana_react/tsconfig.json" }, + { "path": "../share/tsconfig.json" } ] } diff --git a/src/plugins/interactive_setup/common/index.ts b/src/plugins/interactive_setup/common/index.ts index f736d1e23012..ab8c00cfa5a8 100644 --- a/src/plugins/interactive_setup/common/index.ts +++ b/src/plugins/interactive_setup/common/index.ts @@ -6,5 +6,5 @@ * Side Public License, v 1. */ -export type { InteractiveSetupViewState, EnrollmentToken } from './types'; +export type { InteractiveSetupViewState, EnrollmentToken, Certificate, PingResult } from './types'; export { ElasticsearchConnectionStatus } from './elasticsearch_connection_status'; diff --git a/src/plugins/interactive_setup/common/types.ts b/src/plugins/interactive_setup/common/types.ts index 4df7c8eaa972..de3f54dbf9a2 100644 --- a/src/plugins/interactive_setup/common/types.ts +++ b/src/plugins/interactive_setup/common/types.ts @@ -6,6 +6,8 @@ * Side Public License, v 1. */ +import type { PeerCertificate } from 'tls'; + import type { ElasticsearchConnectionStatus } from './elasticsearch_connection_status'; /** @@ -43,3 +45,24 @@ export interface EnrollmentToken { */ key: string; } + +export interface Certificate { + issuer: Partial; + valid_from: PeerCertificate['valid_from']; + valid_to: PeerCertificate['valid_to']; + subject: Partial; + fingerprint256: PeerCertificate['fingerprint256']; + raw: string; +} + +export interface PingResult { + /** + * Indicates whether the cluster requires authentication. + */ + authRequired: boolean; + + /** + * Full certificate chain of cluster at requested address. Only present if cluster uses HTTPS. + */ + certificateChain?: Certificate[]; +} diff --git a/src/plugins/interactive_setup/public/app.scss b/src/plugins/interactive_setup/public/app.scss new file mode 100644 index 000000000000..119a2377dd7d --- /dev/null +++ b/src/plugins/interactive_setup/public/app.scss @@ -0,0 +1,26 @@ +.interactiveSetup { + @include kibanaFullScreenGraphics; +} + +.interactiveSetup__header { + position: relative; + z-index: 10; + padding: $euiSizeXL; +} + +.interactiveSetup__logo { + @include kibanaCircleLogo; + @include euiBottomShadowMedium; + + margin-bottom: $euiSizeXL; +} + +.interactiveSetup__content { + position: relative; + z-index: 10; + margin: auto; + margin-bottom: $euiSizeXL; + max-width: map-get($euiBreakpoints, 's') - $euiSizeXL; + padding-left: $euiSizeXL; + padding-right: $euiSizeXL; +} diff --git a/src/plugins/interactive_setup/public/app.tsx b/src/plugins/interactive_setup/public/app.tsx index 2b6b70895397..0c206cb4fa21 100644 --- a/src/plugins/interactive_setup/public/app.tsx +++ b/src/plugins/interactive_setup/public/app.tsx @@ -6,22 +6,76 @@ * Side Public License, v 1. */ -import { EuiPageTemplate, EuiPanel, EuiText } from '@elastic/eui'; -import React from 'react'; +import './app.scss'; + +import { EuiIcon, EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; +import type { FunctionComponent } from 'react'; +import React, { useState } from 'react'; + +import { FormattedMessage } from '@kbn/i18n/react'; + +import { ClusterAddressForm } from './cluster_address_form'; +import type { ClusterConfigurationFormProps } from './cluster_configuration_form'; +import { ClusterConfigurationForm } from './cluster_configuration_form'; +import { EnrollmentTokenForm } from './enrollment_token_form'; +import { ProgressIndicator } from './progress_indicator'; + +export const App: FunctionComponent = () => { + const [page, setPage] = useState<'token' | 'manual' | 'success'>('token'); + const [cluster, setCluster] = useState< + Omit + >(); -export const App = () => { return ( - - - Kibana server is not ready yet. - - +
+
+ + + + + +

+ +

+
+ +
+
+ + + + {page === 'success' && ( + window.location.replace(window.location.href)} /> + )} + +
+
); }; diff --git a/src/plugins/interactive_setup/public/cluster_address_form.test.tsx b/src/plugins/interactive_setup/public/cluster_address_form.test.tsx new file mode 100644 index 000000000000..e063205a9043 --- /dev/null +++ b/src/plugins/interactive_setup/public/cluster_address_form.test.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { fireEvent, render, waitFor } from '@testing-library/react'; +import React from 'react'; + +import { coreMock } from 'src/core/public/mocks'; + +import { ClusterAddressForm } from './cluster_address_form'; +import { Providers } from './plugin'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +describe('ClusterAddressForm', () => { + jest.setTimeout(20_000); + + it('calls enrollment API when submitting form', async () => { + const coreStart = coreMock.createStart(); + coreStart.http.post.mockResolvedValue({}); + + const onSuccess = jest.fn(); + + const { findByRole, findByLabelText } = render( + + + + ); + fireEvent.change(await findByLabelText('Address'), { + target: { value: 'https://localhost' }, + }); + fireEvent.click(await findByRole('button', { name: 'Check address', hidden: true })); + + await waitFor(() => { + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/interactive_setup/ping', { + body: JSON.stringify({ + host: 'https://localhost:9200', + }), + }); + expect(onSuccess).toHaveBeenCalled(); + }); + }); + + it('validates form', async () => { + const coreStart = coreMock.createStart(); + const onSuccess = jest.fn(); + + const { findAllByText, findByRole, findByLabelText } = render( + + + + ); + + fireEvent.change(await findByLabelText('Address'), { + target: { value: 'localhost' }, + }); + + fireEvent.click(await findByRole('button', { name: 'Check address', hidden: true })); + + await findAllByText(/Enter a valid address including protocol/i); + + expect(coreStart.http.post).not.toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/interactive_setup/public/cluster_address_form.tsx b/src/plugins/interactive_setup/public/cluster_address_form.tsx new file mode 100644 index 000000000000..ba7b1d46182a --- /dev/null +++ b/src/plugins/interactive_setup/public/cluster_address_form.tsx @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiSpacer, +} from '@elastic/eui'; +import type { FunctionComponent } from 'react'; +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { IHttpFetchError } from 'kibana/public'; + +import type { PingResult } from '../common'; +import type { ValidationErrors } from './use_form'; +import { useForm } from './use_form'; +import { useHttp } from './use_http'; + +export interface ClusterAddressFormValues { + host: string; +} + +export interface ClusterAddressFormProps { + defaultValues?: ClusterAddressFormValues; + onCancel?(): void; + onSuccess?(result: PingResult, values: ClusterAddressFormValues): void; +} + +export const ClusterAddressForm: FunctionComponent = ({ + defaultValues = { + host: 'https://localhost:9200', + }, + onCancel, + onSuccess, +}) => { + const http = useHttp(); + + const [form, eventHandlers] = useForm({ + defaultValues, + validate: async (values) => { + const errors: ValidationErrors = {}; + + if (!values.host) { + errors.host = i18n.translate('interactiveSetup.clusterAddressForm.hostRequiredError', { + defaultMessage: 'Enter an address.', + }); + } else { + try { + const url = new URL(values.host); + if (!url.protocol || !url.hostname) { + throw new Error(); + } + } catch (error) { + errors.host = i18n.translate('interactiveSetup.clusterAddressForm.hostInvalidError', { + defaultMessage: 'Enter a valid address including protocol.', + }); + } + } + + return errors; + }, + onSubmit: async (values) => { + const url = new URL(values.host); + const host = `${url.protocol}//${url.hostname}:${url.port || 9200}`; + + const result = await http.post('/internal/interactive_setup/ping', { + body: JSON.stringify({ host }), + }); + + onSuccess?.(result, { host }); + }, + }); + + return ( + + {form.submitError && ( + <> + + {(form.submitError as IHttpFetchError).body?.message} + + + + )} + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/src/plugins/interactive_setup/public/cluster_configuration_form.test.tsx b/src/plugins/interactive_setup/public/cluster_configuration_form.test.tsx new file mode 100644 index 000000000000..93f3fa11a1ce --- /dev/null +++ b/src/plugins/interactive_setup/public/cluster_configuration_form.test.tsx @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { fireEvent, render, waitFor } from '@testing-library/react'; +import React from 'react'; + +import { coreMock } from 'src/core/public/mocks'; + +import { ClusterConfigurationForm } from './cluster_configuration_form'; +import { Providers } from './plugin'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +describe('ClusterConfigurationForm', () => { + jest.setTimeout(20_000); + + it('calls enrollment API when submitting form', async () => { + const coreStart = coreMock.createStart(); + coreStart.http.post.mockResolvedValue({}); + + const onSuccess = jest.fn(); + + const { findByRole, findByLabelText } = render( + + + + ); + fireEvent.change(await findByLabelText('Username'), { + target: { value: 'kibana_system' }, + }); + fireEvent.change(await findByLabelText('Password'), { + target: { value: 'changeme' }, + }); + fireEvent.click(await findByLabelText('Certificate authority')); + fireEvent.click(await findByRole('button', { name: 'Connect to cluster', hidden: true })); + + await waitFor(() => { + expect(coreStart.http.post).toHaveBeenLastCalledWith( + '/internal/interactive_setup/configure', + { + body: JSON.stringify({ + host: 'https://localhost:9200', + username: 'kibana_system', + password: 'changeme', + caCert: 'cert', + }), + } + ); + expect(onSuccess).toHaveBeenCalled(); + }); + }); + + it('validates form', async () => { + const coreStart = coreMock.createStart(); + const onSuccess = jest.fn(); + + const { findAllByText, findByRole, findByLabelText } = render( + + + + ); + + fireEvent.click(await findByRole('button', { name: 'Connect to cluster', hidden: true })); + + await findAllByText(/Enter a password/i); + await findAllByText(/Confirm that you recognize and trust this certificate/i); + + fireEvent.change(await findByLabelText('Username'), { + target: { value: 'elastic' }, + }); + + await findAllByText(/User 'elastic' can't be used as Kibana system user/i); + + expect(coreStart.http.post).not.toHaveBeenCalled(); + }); +}); diff --git a/src/plugins/interactive_setup/public/cluster_configuration_form.tsx b/src/plugins/interactive_setup/public/cluster_configuration_form.tsx new file mode 100644 index 000000000000..cd3541fe0318 --- /dev/null +++ b/src/plugins/interactive_setup/public/cluster_configuration_form.tsx @@ -0,0 +1,322 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiCheckableCard, + EuiFieldPassword, + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiIcon, + EuiLink, + EuiPanel, + EuiSpacer, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import type { FunctionComponent } from 'react'; +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { IHttpFetchError } from 'kibana/public'; + +import type { Certificate } from '../common'; +import { TextTruncate } from './text_truncate'; +import type { ValidationErrors } from './use_form'; +import { useForm } from './use_form'; +import { useHtmlId } from './use_html_id'; +import { useHttp } from './use_http'; + +export interface ClusterConfigurationFormValues { + username: string; + password: string; + caCert: string; +} + +export interface ClusterConfigurationFormProps { + host: string; + authRequired: boolean; + certificateChain?: Certificate[]; + defaultValues?: ClusterConfigurationFormValues; + onCancel?(): void; + onSuccess?(): void; +} + +export const ClusterConfigurationForm: FunctionComponent = ({ + host, + authRequired, + certificateChain, + defaultValues = { + username: 'kibana_system', + password: '', + caCert: '', + }, + onCancel, + onSuccess, +}) => { + const http = useHttp(); + + const [form, eventHandlers] = useForm({ + defaultValues, + validate: async (values) => { + const errors: ValidationErrors = {}; + + if (authRequired) { + if (!values.username) { + errors.username = i18n.translate( + 'interactiveSetup.clusterConfigurationForm.usernameRequiredError', + { + defaultMessage: 'Enter a username.', + } + ); + } else if (values.username === 'elastic') { + errors.username = i18n.translate( + 'interactiveSetup.clusterConfigurationForm.usernameReservedError', + { + defaultMessage: "User 'elastic' can't be used as Kibana system user.", + } + ); + } + + if (!values.password) { + errors.password = i18n.translate( + 'interactiveSetup.clusterConfigurationForm.passwordRequiredError', + { + defaultMessage: `Enter a password.`, + } + ); + } + } + + if (certificateChain && !values.caCert) { + errors.caCert = i18n.translate( + 'interactiveSetup.clusterConfigurationForm.caCertConfirmationRequiredError', + { + defaultMessage: 'Confirm that you recognize and trust this certificate.', + } + ); + } + + return errors; + }, + onSubmit: async (values) => { + await http.post('/internal/interactive_setup/configure', { + body: JSON.stringify({ + host, + username: values.username, + password: values.password, + caCert: values.caCert, + }), + }); + onSuccess?.(); + }, + }); + + const trustCaCertId = useHtmlId('clusterConfigurationForm', 'trustCaCert'); + + return ( + + {form.submitError && ( + <> + + {(form.submitError as IHttpFetchError).body?.message} + + + + )} + + + + + + + + {host} + + + + + + {authRequired ? ( + <> + + + + + + + + + ) : ( + <> + +

+ +

+

+ + + +

+
+ + + )} + + {certificateChain && certificateChain.length > 0 && ( + <> + + { + const intermediateCa = certificateChain[Math.min(1, certificateChain.length - 1)]; + form.setValue('caCert', form.values.caCert ? '' : intermediateCa.raw); + form.setTouched('caCert'); + }} + > + + + + + + )} + + + + + + + + + + + + + +
+ ); +}; + +export interface CertificatePanelProps { + certificate: Certificate; +} + +export const CertificatePanel: FunctionComponent = ({ certificate }) => { + return ( + + + + + + + +

{certificate.subject.O || certificate.subject.CN}

+
+ + + + + + +
+
+
+ ); +}; diff --git a/src/plugins/interactive_setup/public/enrollment_token_form.test.tsx b/src/plugins/interactive_setup/public/enrollment_token_form.test.tsx new file mode 100644 index 000000000000..d2f08eac1fac --- /dev/null +++ b/src/plugins/interactive_setup/public/enrollment_token_form.test.tsx @@ -0,0 +1,113 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { fireEvent, render, waitFor } from '@testing-library/react'; +import React from 'react'; + +import { coreMock } from 'src/core/public/mocks'; + +import type { EnrollmentToken } from '../common'; +import { decodeEnrollmentToken, EnrollmentTokenForm } from './enrollment_token_form'; +import { Providers } from './plugin'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +const token: EnrollmentToken = { + ver: '8.0.0', + adr: ['localhost:9200'], + fgr: + 'AA:C8:2C:2E:09:58:F4:FE:A1:D2:AB:7F:13:70:C2:7D:EB:FD:A2:23:88:13:E4:DA:3A:D0:59:D0:09:00:07:36', + key: 'JH-36HoBo4EYIoVhHh2F:uEo4dksARMq_BSHaAHUr8Q', +}; + +describe('EnrollmentTokenForm', () => { + jest.setTimeout(20_000); + + it('calls enrollment API when submitting form', async () => { + const coreStart = coreMock.createStart(); + coreStart.http.post.mockResolvedValue({}); + + const onSuccess = jest.fn(); + + const { findByRole, findByLabelText } = render( + + + + ); + fireEvent.change(await findByLabelText('Enrollment token'), { + target: { value: btoa(JSON.stringify(token)) }, + }); + fireEvent.click(await findByRole('button', { name: 'Connect to cluster', hidden: true })); + + await waitFor(() => { + expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/interactive_setup/enroll', { + body: JSON.stringify({ + hosts: [`https://${token.adr[0]}`], + apiKey: btoa(token.key), + caFingerprint: token.fgr, + }), + }); + expect(onSuccess).toHaveBeenCalled(); + }); + }); + + it('validates form', async () => { + const coreStart = coreMock.createStart(); + const onSuccess = jest.fn(); + + const { findAllByText, findByRole, findByLabelText } = render( + + + + ); + + fireEvent.click(await findByRole('button', { name: 'Connect to cluster', hidden: true })); + + await findAllByText(/Enter an enrollment token/i); + + fireEvent.change(await findByLabelText('Enrollment token'), { + target: { value: 'invalid' }, + }); + + await findAllByText(/Enter a valid enrollment token/i); + }); +}); + +describe('decodeEnrollmentToken', () => { + it('should decode a valid token', () => { + expect(decodeEnrollmentToken(btoa(JSON.stringify(token)))).toEqual({ + adr: ['https://localhost:9200'], + fgr: + 'AA:C8:2C:2E:09:58:F4:FE:A1:D2:AB:7F:13:70:C2:7D:EB:FD:A2:23:88:13:E4:DA:3A:D0:59:D0:09:00:07:36', + key: 'SkgtMzZIb0JvNEVZSW9WaEhoMkY6dUVvNGRrc0FSTXFfQlNIYUFIVXI4UQ==', + ver: '8.0.0', + }); + }); + + it('should not decode an invalid token', () => { + expect(decodeEnrollmentToken(JSON.stringify(token))).toBeUndefined(); + expect( + decodeEnrollmentToken( + btoa( + JSON.stringify({ + ver: [''], + adr: null, + fgr: false, + key: undefined, + }) + ) + ) + ).toBeUndefined(); + expect(decodeEnrollmentToken(btoa(JSON.stringify({})))).toBeUndefined(); + expect(decodeEnrollmentToken(btoa(JSON.stringify([])))).toBeUndefined(); + expect(decodeEnrollmentToken(btoa(JSON.stringify(null)))).toBeUndefined(); + expect(decodeEnrollmentToken(btoa(JSON.stringify('')))).toBeUndefined(); + }); +}); diff --git a/src/plugins/interactive_setup/public/enrollment_token_form.tsx b/src/plugins/interactive_setup/public/enrollment_token_form.tsx new file mode 100644 index 000000000000..3b5c751874a1 --- /dev/null +++ b/src/plugins/interactive_setup/public/enrollment_token_form.tsx @@ -0,0 +1,204 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiFlexGroup, + EuiFlexItem, + EuiForm, + EuiFormRow, + EuiIcon, + EuiSpacer, + EuiText, + EuiTextArea, +} from '@elastic/eui'; +import type { FunctionComponent } from 'react'; +import React from 'react'; + +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; +import type { IHttpFetchError } from 'kibana/public'; + +import type { EnrollmentToken } from '../common'; +import { TextTruncate } from './text_truncate'; +import type { ValidationErrors } from './use_form'; +import { useForm } from './use_form'; +import { useHttp } from './use_http'; + +export interface EnrollmentTokenFormValues { + token: string; +} + +export interface EnrollmentTokenFormProps { + defaultValues?: EnrollmentTokenFormValues; + onCancel?(): void; + onSuccess?(): void; +} + +export const EnrollmentTokenForm: FunctionComponent = ({ + defaultValues = { + token: '', + }, + onCancel, + onSuccess, +}) => { + const http = useHttp(); + const [form, eventHandlers] = useForm({ + defaultValues, + validate: (values) => { + const errors: ValidationErrors = {}; + + if (!values.token) { + errors.token = i18n.translate('interactiveSetup.enrollmentTokenForm.tokenRequiredError', { + defaultMessage: 'Enter an enrollment token.', + }); + } else { + const decoded = decodeEnrollmentToken(values.token); + if (!decoded) { + errors.token = i18n.translate('interactiveSetup.enrollmentTokenForm.tokenInvalidError', { + defaultMessage: 'Enter a valid enrollment token.', + }); + } + } + + return errors; + }, + onSubmit: async (values) => { + const decoded = decodeEnrollmentToken(values.token)!; + await http.post('/internal/interactive_setup/enroll', { + body: JSON.stringify({ + hosts: decoded.adr, + apiKey: decoded.key, + caFingerprint: decoded.fgr, + }), + }); + onSuccess?.(); + }, + }); + + const enrollmentToken = decodeEnrollmentToken(form.values.token); + + return ( + + {form.submitError && ( + <> + + {(form.submitError as IHttpFetchError).body?.message} + + + + )} + + } + fullWidth + > + + + + + + + + + + + + + + + + + + ); +}; + +interface EnrollmentTokenDetailsProps { + token: EnrollmentToken; +} + +const EnrollmentTokenDetails: FunctionComponent = ({ token }) => ( + + + + + + + + + + {token.adr[0]} + + + + + + + + + +); + +export function decodeEnrollmentToken(enrollmentToken: string) { + try { + const json = JSON.parse(atob(enrollmentToken)) as EnrollmentToken; + if ( + !Array.isArray(json.adr) || + json.adr.some((adr) => typeof adr !== 'string') || + typeof json.fgr !== 'string' || + typeof json.key !== 'string' || + typeof json.ver !== 'string' + ) { + return; + } + return { + ...json, + adr: json.adr.map((host) => `https://${host}`), + key: btoa(json.key), + }; + } catch (error) {} // eslint-disable-line no-empty +} diff --git a/src/plugins/interactive_setup/public/index.ts b/src/plugins/interactive_setup/public/index.ts index 153bc92a0dd0..7855b90b810d 100644 --- a/src/plugins/interactive_setup/public/index.ts +++ b/src/plugins/interactive_setup/public/index.ts @@ -6,6 +6,6 @@ * Side Public License, v 1. */ -import { UserSetupPlugin } from './plugin'; +import { InteractiveSetupPlugin } from './plugin'; -export const plugin = () => new UserSetupPlugin(); +export const plugin = () => new InteractiveSetupPlugin(); diff --git a/src/plugins/interactive_setup/public/plugin.tsx b/src/plugins/interactive_setup/public/plugin.tsx index 375f04e5047d..00fd38d3e78a 100644 --- a/src/plugins/interactive_setup/public/plugin.tsx +++ b/src/plugins/interactive_setup/public/plugin.tsx @@ -6,21 +6,30 @@ * Side Public License, v 1. */ +import type { FunctionComponent } from 'react'; import React from 'react'; import ReactDOM from 'react-dom'; -import type { CoreSetup, CoreStart, Plugin } from 'src/core/public'; +import { I18nProvider } from '@kbn/i18n/react'; +import type { CoreSetup, CoreStart, HttpSetup, Plugin } from 'src/core/public'; import { App } from './app'; +import { HttpProvider } from './use_http'; -export class UserSetupPlugin implements Plugin { +export class InteractiveSetupPlugin implements Plugin { public setup(core: CoreSetup) { core.application.register({ id: 'interactiveSetup', - title: 'Interactive Setup', + title: 'Configure Elastic to get started', + appRoute: '/', chromeless: true, mount: (params) => { - ReactDOM.render(, params.element); + ReactDOM.render( + + + , + params.element + ); return () => ReactDOM.unmountComponentAtNode(params.element); }, }); @@ -28,3 +37,13 @@ export class UserSetupPlugin implements Plugin { public start(core: CoreStart) {} } + +export interface ProvidersProps { + http: HttpSetup; +} + +export const Providers: FunctionComponent = ({ http, children }) => ( + + {children} + +); diff --git a/src/plugins/interactive_setup/public/progress_indicator.tsx b/src/plugins/interactive_setup/public/progress_indicator.tsx new file mode 100644 index 000000000000..a6d499f6a571 --- /dev/null +++ b/src/plugins/interactive_setup/public/progress_indicator.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { EuiStepProps } from '@elastic/eui'; +import { EuiPanel, EuiSteps } from '@elastic/eui'; +import type { FunctionComponent } from 'react'; +import React, { useEffect } from 'react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; +import useTimeoutFn from 'react-use/lib/useTimeoutFn'; + +import { i18n } from '@kbn/i18n'; + +import { useHttp } from './use_http'; + +export interface ProgressIndicatorProps { + onSuccess?(): void; +} + +export const ProgressIndicator: FunctionComponent = ({ onSuccess }) => { + const http = useHttp(); + const [status, checkStatus] = useAsyncFn(async () => { + let isAvailable: boolean | undefined = false; + let isPastPreboot: boolean | undefined = false; + try { + const { response } = await http.get('/api/status', { asResponse: true }); + isAvailable = response ? response.status < 500 : undefined; + isPastPreboot = response?.headers.get('content-type')?.includes('application/json'); + } catch ({ response }) { + isAvailable = response ? response.status < 500 : undefined; + isPastPreboot = response?.headers.get('content-type')?.includes('application/json'); + } + return isAvailable === true && isPastPreboot === true + ? 'complete' + : isAvailable === false + ? 'unavailable' + : isAvailable === true && isPastPreboot === false + ? 'preboot' + : 'unknown'; + }); + + const [, cancelPolling, resetPolling] = useTimeoutFn(checkStatus, 1000); + + useEffect(() => { + if (status.value === 'complete') { + cancelPolling(); + onSuccess?.(); + } else if (status.loading === false) { + resetPolling(); + } + }, [status.loading, status.value]); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + + + + ); +}; + +type Optional = Omit & Partial; + +export interface LoadingStepsProps { + currentStepId?: string; + steps: Array>; +} + +export const LoadingSteps: FunctionComponent = ({ currentStepId, steps }) => { + const currentStepIndex = steps.findIndex((step) => step.id === currentStepId); + return ( + ({ + status: + i <= currentStepIndex + ? 'complete' + : steps[i - 1]?.id === currentStepId + ? 'loading' + : 'incomplete', + children: null, + ...step, + }))} + /> + ); +}; diff --git a/src/plugins/interactive_setup/public/text_truncate.tsx b/src/plugins/interactive_setup/public/text_truncate.tsx new file mode 100644 index 000000000000..32736e80211a --- /dev/null +++ b/src/plugins/interactive_setup/public/text_truncate.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { EuiToolTip } from '@elastic/eui'; +import type { FunctionComponent } from 'react'; +import React, { useLayoutEffect, useRef, useState } from 'react'; + +export const TextTruncate: FunctionComponent = ({ children }) => { + const textRef = useRef(null); + const [showTooltip, setShowTooltip] = useState(false); + + useLayoutEffect(() => { + if (textRef.current) { + const { clientWidth, scrollWidth } = textRef.current; + setShowTooltip(scrollWidth > clientWidth); + } + }, [children]); + + const truncated = ( + + {children} + + ); + + if (showTooltip) { + return ( + + {truncated} + + ); + } + + return truncated; +}; diff --git a/src/plugins/interactive_setup/public/use_form.ts b/src/plugins/interactive_setup/public/use_form.ts new file mode 100644 index 000000000000..8ed1d89ea087 --- /dev/null +++ b/src/plugins/interactive_setup/public/use_form.ts @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { set } from '@elastic/safer-lodash-set'; +import { cloneDeep, cloneDeepWith, get } from 'lodash'; +import type { ChangeEventHandler, FocusEventHandler, ReactEventHandler } from 'react'; +import { useRef } from 'react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; + +export type FormReturnTuple = [FormState, FormProps]; + +export interface FormProps { + onSubmit: ReactEventHandler; + onChange: ChangeEventHandler; + onBlur: FocusEventHandler; +} + +export interface FormOptions { + onSubmit: SubmitCallback; + validate: ValidateCallback; + defaultValues: Values; +} + +/** + * Returns state and {@link HTMLFormElement} event handlers useful for creating + * forms with inline validation. + * + * @see {@link useFormState} if you don't want to use {@link HTMLFormElement}. + * + * @example + * ```typescript + * const [form, eventHandlers] = useForm({ + * onSubmit: (values) => apiClient.create(values), + * validate: (values) => !values.email ? { email: 'Required' } : {} + * }); + * + * + * + * Submit + * + * ``` + */ +export function useForm( + options: FormOptions +): FormReturnTuple { + const form = useFormState(options); + + const eventHandlers: FormProps = { + onSubmit: (event) => { + event.preventDefault(); + form.submit(); + }, + onChange: (event) => { + const { name, type, checked, value } = event.target; + if (name) { + form.setValue(name, type === 'checkbox' ? checked : value); + } + }, + onBlur: (event) => { + const { name } = event.target; + if (name) { + form.setTouched(event.target.name); + } + }, + }; + + return [form, eventHandlers]; +} + +export type FormValues = Record; +export type SubmitCallback = (values: Values) => Promise; +export type ValidateCallback = ( + values: Values +) => ValidationErrors | Promise>; +export type ValidationErrors = DeepMap; +export type TouchedFields = DeepMap; + +export interface FormState { + setValue(name: string, value: any, revalidate?: boolean): Promise; + setError(name: string, message: string): void; + setTouched(name: string, touched?: boolean, revalidate?: boolean): Promise; + reset(values: Values): void; + submit(): Promise; + validate(): Promise>; + values: Values; + errors: ValidationErrors; + touched: TouchedFields; + isValidating: boolean; + isSubmitting: boolean; + submitError: Error | undefined; + isInvalid: boolean; + isSubmitted: boolean; +} + +/** + * Returns state useful for creating forms with inline validation. + * + * @example + * ```typescript + * const form = useFormState({ + * onSubmit: (values) => apiClient.create(values), + * validate: (values) => !values.toggle ? { toggle: 'Required' } : {} + * }); + * + * form.setValue('toggle', e.target.checked)} + * onBlur={() => form.setTouched('toggle')} + * isInvalid={!!form.errors.toggle} + * /> + * + * Submit + * + * ``` + */ +export function useFormState({ + onSubmit, + validate, + defaultValues, +}: FormOptions): FormState { + const valuesRef = useRef(defaultValues); + const errorsRef = useRef>({}); + const touchedRef = useRef>({}); + const submitCountRef = useRef(0); + + const [validationState, validateForm] = useAsyncFn(async (formValues: Values) => { + const nextErrors = await validate(formValues); + errorsRef.current = nextErrors; + if (Object.keys(nextErrors).length === 0) { + submitCountRef.current = 0; + } + return nextErrors; + }, []); + + const [submitState, submitForm] = useAsyncFn(async (formValues: Values) => { + const nextErrors = await validateForm(formValues); + touchedRef.current = mapDeep(formValues, true); + submitCountRef.current += 1; + if (Object.keys(nextErrors).length === 0) { + return onSubmit(formValues); + } + }, []); + + return { + setValue: async (name, value, revalidate = true) => { + const nextValues = setDeep(valuesRef.current, name, value); + valuesRef.current = nextValues; + if (revalidate) { + await validateForm(nextValues); + } + }, + setTouched: async (name, touched = true, revalidate = true) => { + touchedRef.current = setDeep(touchedRef.current, name, touched); + if (revalidate) { + await validateForm(valuesRef.current); + } + }, + setError: (name, message) => { + errorsRef.current = setDeep(errorsRef.current, name, message); + touchedRef.current = setDeep(touchedRef.current, name, true); + }, + reset: (nextValues) => { + valuesRef.current = nextValues; + errorsRef.current = {}; + touchedRef.current = {}; + submitCountRef.current = 0; + }, + submit: () => submitForm(valuesRef.current), + validate: () => validateForm(valuesRef.current), + values: valuesRef.current, + errors: errorsRef.current, + touched: touchedRef.current, + isValidating: validationState.loading, + isSubmitting: submitState.loading, + submitError: submitState.error, + isInvalid: Object.keys(errorsRef.current).length > 0, + isSubmitted: submitCountRef.current > 0, + }; +} + +type DeepMap = { + [K in keyof T]?: T[K] extends any[] + ? T[K][number] extends object + ? Array> + : TValue + : T[K] extends object + ? DeepMap + : TValue; +}; + +function mapDeep(values: T, value: V): DeepMap { + return cloneDeepWith(values, (v) => { + if (typeof v !== 'object' && v !== null) { + return value; + } + }); +} + +function setDeep(values: T, name: string, value: V): T { + if (get(values, name) !== value) { + return set(cloneDeep(values), name, value); + } + return values; +} diff --git a/src/plugins/interactive_setup/public/use_html_id.ts b/src/plugins/interactive_setup/public/use_html_id.ts new file mode 100644 index 000000000000..d2b568bf2631 --- /dev/null +++ b/src/plugins/interactive_setup/public/use_html_id.ts @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { htmlIdGenerator } from '@elastic/eui'; +import { useMemo } from 'react'; + +/** + * Generates an ID that can be used for HTML elements. + * + * @param prefix Prefix of the id to be generated + * @param suffix Suffix of the id to be generated + * + * @example + * ```typescript + * const titleId = useHtmlId('changePasswordForm', 'title'); + * + * + *

Change password

+ *
+ * ``` + */ +export function useHtmlId(prefix?: string, suffix?: string) { + return useMemo(() => htmlIdGenerator(prefix)(suffix), [prefix, suffix]); +} diff --git a/src/plugins/interactive_setup/public/use_http.ts b/src/plugins/interactive_setup/public/use_http.ts new file mode 100644 index 000000000000..6d2a9f03d4c7 --- /dev/null +++ b/src/plugins/interactive_setup/public/use_http.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 + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import constate from 'constate'; + +import type { HttpSetup } from 'src/core/public'; + +export const [HttpProvider, useHttp] = constate(({ http }: { http: HttpSetup }) => { + return http; +}); diff --git a/src/plugins/interactive_setup/server/elasticsearch_service.mock.ts b/src/plugins/interactive_setup/server/elasticsearch_service.mock.ts index 8bc7e4307e76..9b59ab596726 100644 --- a/src/plugins/interactive_setup/server/elasticsearch_service.mock.ts +++ b/src/plugins/interactive_setup/server/elasticsearch_service.mock.ts @@ -16,5 +16,7 @@ export const elasticsearchServiceMock = { ElasticsearchConnectionStatus.Configured ), enroll: jest.fn(), + authenticate: jest.fn(), + ping: jest.fn(), }), }; diff --git a/src/plugins/interactive_setup/server/elasticsearch_service.test.ts b/src/plugins/interactive_setup/server/elasticsearch_service.test.ts index 546ab7ea8f9c..ce4893112eba 100644 --- a/src/plugins/interactive_setup/server/elasticsearch_service.test.ts +++ b/src/plugins/interactive_setup/server/elasticsearch_service.test.ts @@ -7,6 +7,7 @@ */ import { errors } from '@elastic/elasticsearch'; +import tls from 'tls'; import { nextTick } from '@kbn/test/jest'; import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; @@ -17,6 +18,10 @@ import type { ElasticsearchServiceSetup } from './elasticsearch_service'; import { ElasticsearchService } from './elasticsearch_service'; import { interactiveSetupMock } from './mocks'; +jest.mock('tls'); + +const tlsConnectMock = tls.connect as jest.MockedFunction; + describe('ElasticsearchService', () => { let service: ElasticsearchService; let mockElasticsearchPreboot: ReturnType; @@ -33,17 +38,21 @@ describe('ElasticsearchService', () => { let mockAuthenticateClient: ReturnType< typeof elasticsearchServiceMock.createCustomClusterClient >; + let mockPingClient: ReturnType; let setupContract: ElasticsearchServiceSetup; beforeEach(() => { mockConnectionStatusClient = elasticsearchServiceMock.createCustomClusterClient(); mockEnrollClient = elasticsearchServiceMock.createCustomClusterClient(); mockAuthenticateClient = elasticsearchServiceMock.createCustomClusterClient(); + mockPingClient = elasticsearchServiceMock.createCustomClusterClient(); mockElasticsearchPreboot.createClient.mockImplementation((type) => { switch (type) { case 'enroll': return mockEnrollClient; case 'authenticate': return mockAuthenticateClient; + case 'ping': + return mockPingClient; default: return mockConnectionStatusClient; } @@ -414,7 +423,7 @@ some weird+ca/with caFingerprint: 'DE:AD:BE:EF', }) ).resolves.toEqual({ - ca: expectedCa, + caCert: expectedCa, host: 'host2', serviceAccountToken: { name: 'some-name', @@ -478,6 +487,133 @@ some weird+ca/with expect(mockAuthenticateClient.close).toHaveBeenCalledTimes(1); }); }); + + describe('#authenticate()', () => { + it('fails if ping call fails', async () => { + mockAuthenticateClient.asInternalUser.ping.mockRejectedValue( + new errors.ConnectionError( + 'some-message', + interactiveSetupMock.createApiResponse({ body: {} }) + ) + ); + + await expect( + setupContract.authenticate({ host: 'http://localhost:9200' }) + ).rejects.toMatchInlineSnapshot(`[ConnectionError: some-message]`); + }); + + it('succeeds if ping call succeeds', async () => { + mockAuthenticateClient.asInternalUser.ping.mockResolvedValue( + interactiveSetupMock.createApiResponse({ statusCode: 200, body: true }) + ); + + await expect( + setupContract.authenticate({ host: 'http://localhost:9200' }) + ).resolves.toEqual(undefined); + }); + }); + + describe('#ping()', () => { + it('fails if host is not reachable', async () => { + mockPingClient.asInternalUser.ping.mockRejectedValue( + new errors.ConnectionError( + 'some-message', + interactiveSetupMock.createApiResponse({ body: {} }) + ) + ); + + await expect(setupContract.ping('http://localhost:9200')).rejects.toMatchInlineSnapshot( + `[ConnectionError: some-message]` + ); + }); + + it('fails if host is not supported', async () => { + mockPingClient.asInternalUser.ping.mockRejectedValue( + new errors.ProductNotSupportedError(interactiveSetupMock.createApiResponse({ body: {} })) + ); + + await expect(setupContract.ping('http://localhost:9200')).rejects.toMatchInlineSnapshot( + `[ProductNotSupportedError: The client noticed that the server is not Elasticsearch and we do not support this unknown product.]` + ); + }); + + it('succeeds if host does not require authentication', async () => { + mockPingClient.asInternalUser.ping.mockResolvedValue( + interactiveSetupMock.createApiResponse({ statusCode: 200, body: true }) + ); + + await expect(setupContract.ping('http://localhost:9200')).resolves.toEqual({ + authRequired: false, + certificateChain: undefined, + }); + }); + + it('succeeds if host requires authentication', async () => { + mockPingClient.asInternalUser.ping.mockRejectedValue( + new errors.ResponseError( + interactiveSetupMock.createApiResponse({ statusCode: 401, body: {} }) + ) + ); + + await expect(setupContract.ping('http://localhost:9200')).resolves.toEqual({ + authRequired: true, + certificateChain: undefined, + }); + }); + + it('succeeds if host requires SSL', async () => { + mockPingClient.asInternalUser.ping.mockRejectedValue( + new errors.ResponseError( + interactiveSetupMock.createApiResponse({ statusCode: 401, body: {} }) + ) + ); + + tlsConnectMock.mockReturnValue(({ + once: jest.fn((event, fn) => { + if (event === 'secureConnect') { + fn(); + } + }), + getPeerCertificate: jest.fn().mockReturnValue({ raw: Buffer.from('cert') }), + destroy: jest.fn(), + } as unknown) as tls.TLSSocket); + + await expect(setupContract.ping('https://localhost:9200')).resolves.toEqual({ + authRequired: true, + certificateChain: [ + expect.objectContaining({ + raw: 'Y2VydA==', + }), + ], + }); + + expect(tlsConnectMock).toHaveBeenCalledWith({ + host: 'localhost', + port: 9200, + rejectUnauthorized: false, + }); + }); + + it('fails if peer certificate cannot be fetched', async () => { + mockPingClient.asInternalUser.ping.mockRejectedValue( + new errors.ResponseError( + interactiveSetupMock.createApiResponse({ statusCode: 401, body: {} }) + ) + ); + + tlsConnectMock.mockReturnValue(({ + once: jest.fn((event, fn) => { + if (event === 'error') { + fn(new Error('some-message')); + } + }), + } as unknown) as tls.TLSSocket); + + await expect(setupContract.ping('https://localhost:9200')).rejects.toMatchInlineSnapshot( + `[Error: some-message]` + ); + }); + }); }); describe('#stop()', () => { @@ -489,7 +625,7 @@ some weird+ca/with const mockConnectionStatusClient = elasticsearchServiceMock.createCustomClusterClient(); mockElasticsearchPreboot.createClient.mockImplementation((type) => { switch (type) { - case 'ping': + case 'connectionStatus': return mockConnectionStatusClient; default: throw new Error(`Unexpected client type: ${type}`); diff --git a/src/plugins/interactive_setup/server/elasticsearch_service.ts b/src/plugins/interactive_setup/server/elasticsearch_service.ts index c88ac0f0798c..edfe203df8e4 100644 --- a/src/plugins/interactive_setup/server/elasticsearch_service.ts +++ b/src/plugins/interactive_setup/server/elasticsearch_service.ts @@ -19,9 +19,9 @@ import { shareReplay, takeWhile, } from 'rxjs/operators'; +import tls from 'tls'; import type { - ElasticsearchClientConfig, ElasticsearchServicePreboot, ICustomClusterClient, Logger, @@ -29,14 +29,22 @@ import type { } from 'src/core/server'; import { ElasticsearchConnectionStatus } from '../common'; -import { getDetailedErrorMessage } from './errors'; +import type { Certificate, PingResult } from '../common'; +import { getDetailedErrorMessage, getErrorStatusCode } from './errors'; -interface EnrollParameters { +export interface EnrollParameters { apiKey: string; hosts: string[]; caFingerprint: string; } +export interface AuthenticateParameters { + host: string; + username?: string; + password?: string; + caCert?: string; +} + export interface ElasticsearchServiceSetupDeps { /** * Core Elasticsearch service preboot contract; @@ -63,6 +71,16 @@ export interface ElasticsearchServiceSetup { * to point to exactly same Elasticsearch node, potentially available via different network interfaces. */ enroll: (params: EnrollParameters) => Promise; + + /** + * Tries to authenticate specified user with cluster. + */ + authenticate: (params: AuthenticateParameters) => Promise; + + /** + * Tries to connect to specified cluster and fetches certificate chain. + */ + ping: (host: string) => Promise; } /** @@ -76,13 +94,20 @@ export interface EnrollResult { /** * PEM CA certificate for the Elasticsearch HTTP certificates. */ - ca: string; + caCert: string; /** * Service account token for the "elastic/kibana" service account. */ serviceAccountToken: { name: string; value: string }; } +export interface AuthenticateResult { + host: string; + username?: string; + password?: string; + caCert?: string; +} + export class ElasticsearchService { /** * Elasticsearch client used to check Elasticsearch connection status. @@ -95,7 +120,7 @@ export class ElasticsearchService { connectionCheckInterval, }: ElasticsearchServiceSetupDeps): ElasticsearchServiceSetup { const connectionStatusClient = (this.connectionStatusClient = elasticsearch.createClient( - 'ping' + 'connectionStatus' )); return { @@ -120,6 +145,8 @@ export class ElasticsearchService { shareReplay({ refCount: true, bufferSize: 1 }) ), enroll: this.enroll.bind(this, elasticsearch), + authenticate: this.authenticate.bind(this, elasticsearch), + ping: this.ping.bind(this, elasticsearch), }; } @@ -145,11 +172,8 @@ export class ElasticsearchService { private async enroll( elasticsearch: ElasticsearchServicePreboot, { apiKey, hosts, caFingerprint }: EnrollParameters - ): Promise { + ) { const scopeableRequest: ScopeableRequest = { headers: { authorization: `ApiKey ${apiKey}` } }; - const elasticsearchConfig: Partial = { - ssl: { verificationMode: 'none' }, - }; // We should iterate through all provided hosts until we find an accessible one. for (const host of hosts) { @@ -158,9 +182,9 @@ export class ElasticsearchService { ); const enrollClient = elasticsearch.createClient('enroll', { - ...elasticsearchConfig, hosts: [host], caFingerprint, + ssl: { verificationMode: 'none' }, }); let enrollmentResponse; @@ -176,51 +200,51 @@ export class ElasticsearchService { // that enrollment will fail for any other host and we should bail out. if (err instanceof errors.ConnectionError || err instanceof errors.TimeoutError) { this.logger.error( - `Unable to connect to "${host}" host, will proceed to the next host if available: ${getDetailedErrorMessage( + `Unable to connect to host "${host}", will proceed to the next host if available: ${getDetailedErrorMessage( err )}` ); continue; } - this.logger.error(`Failed to enroll with "${host}" host: ${getDetailedErrorMessage(err)}`); + this.logger.error(`Failed to enroll with host "${host}": ${getDetailedErrorMessage(err)}`); throw err; } finally { await enrollClient.close(); } this.logger.debug( - `Successfully enrolled with "${host}" host, token name: ${enrollmentResponse.body.token.name}, CA certificate: ${enrollmentResponse.body.http_ca}` + `Successfully enrolled with host "${host}", token name: ${enrollmentResponse.body.token.name}, CA certificate: ${enrollmentResponse.body.http_ca}` ); - const enrollResult = { + const enrollResult: EnrollResult = { host, - ca: ElasticsearchService.createPemCertificate(enrollmentResponse.body.http_ca), + caCert: ElasticsearchService.createPemCertificate(enrollmentResponse.body.http_ca), serviceAccountToken: enrollmentResponse.body.token, }; - // Now try to use retrieved password and CA certificate to authenticate to this host. + // Now try to use retrieved service account and CA certificate to authenticate to this host. const authenticateClient = elasticsearch.createClient('authenticate', { caFingerprint, hosts: [host], serviceAccountToken: enrollResult.serviceAccountToken.value, - ssl: { certificateAuthorities: [enrollResult.ca] }, + ssl: { certificateAuthorities: [enrollResult.caCert] }, }); this.logger.debug( - `Verifying if "${enrollmentResponse.body.token.name}" token can authenticate to "${host}" host.` + `Verifying if "${enrollmentResponse.body.token.name}" token can authenticate to host "${host}".` ); try { await authenticateClient.asInternalUser.security.authenticate(); this.logger.debug( - `Successfully authenticated "${enrollmentResponse.body.token.name}" token to "${host}" host.` + `Successfully authenticated "${enrollmentResponse.body.token.name}" token to host "${host}".` ); } catch (err) { this.logger.error( `Failed to authenticate "${ enrollmentResponse.body.token.name - }" token to "${host}" host: ${getDetailedErrorMessage(err)}.` + }" token to host "${host}": ${getDetailedErrorMessage(err)}.` ); throw err; } finally { @@ -233,7 +257,114 @@ export class ElasticsearchService { throw new Error('Unable to connect to any of the provided hosts.'); } - private static createPemCertificate(derCaString: string) { + private async authenticate( + elasticsearch: ElasticsearchServicePreboot, + { host, username, password, caCert }: AuthenticateParameters + ) { + const client = elasticsearch.createClient('authenticate', { + hosts: [host], + username, + password, + ssl: caCert ? { certificateAuthorities: [caCert] } : undefined, + }); + + try { + // Using `ping` instead of `authenticate` allows us to verify clusters with both + // security enabled and disabled. + await client.asInternalUser.ping(); + } catch (error) { + this.logger.error( + `Failed to authenticate with host "${host}": ${getDetailedErrorMessage(error)}` + ); + throw error; + } finally { + await client.close(); + } + } + + private async ping(elasticsearch: ElasticsearchServicePreboot, host: string) { + const client = elasticsearch.createClient('ping', { + hosts: [host], + username: '', + password: '', + ssl: { verificationMode: 'none' }, + }); + + let authRequired = false; + try { + await client.asInternalUser.ping(); + } catch (error) { + if ( + error instanceof errors.ConnectionError || + error instanceof errors.TimeoutError || + error instanceof errors.ProductNotSupportedError + ) { + this.logger.error(`Unable to connect to host "${host}": ${getDetailedErrorMessage(error)}`); + throw error; + } + + authRequired = getErrorStatusCode(error) === 401; + } finally { + await client.close(); + } + + let certificateChain: Certificate[] | undefined; + const { protocol, hostname, port } = new URL(host); + if (protocol === 'https:') { + try { + const cert = await ElasticsearchService.fetchPeerCertificate(hostname, port); + certificateChain = ElasticsearchService.flattenCertificateChain(cert).map( + ElasticsearchService.getCertificate + ); + } catch (error) { + this.logger.error( + `Failed to fetch peer certificate from host "${host}": ${getDetailedErrorMessage(error)}` + ); + throw error; + } + } + + return { + authRequired, + certificateChain, + }; + } + + private static fetchPeerCertificate(host: string, port: string | number) { + return new Promise((resolve, reject) => { + const socket = tls.connect({ host, port: Number(port), rejectUnauthorized: false }); + socket.once('secureConnect', () => { + const cert = socket.getPeerCertificate(true); + socket.destroy(); + resolve(cert); + }); + socket.once('error', reject); + }); + } + + private static flattenCertificateChain( + cert: tls.DetailedPeerCertificate, + accumulator: tls.DetailedPeerCertificate[] = [] + ) { + accumulator.push(cert); + if (cert.issuerCertificate && cert.fingerprint256 !== cert.issuerCertificate.fingerprint256) { + ElasticsearchService.flattenCertificateChain(cert.issuerCertificate, accumulator); + } + return accumulator; + } + + private static getCertificate(cert: tls.DetailedPeerCertificate): Certificate { + return { + issuer: cert.issuer, + valid_from: cert.valid_from, + valid_to: cert.valid_to, + subject: cert.subject, + fingerprint256: cert.fingerprint256, + raw: cert.raw.toString('base64'), + }; + } + + public static createPemCertificate(derCaString: string) { // Use `X509Certificate` class once we upgrade to Node v16. return `-----BEGIN CERTIFICATE-----\n${derCaString .replace(/_/g, '/') diff --git a/src/plugins/interactive_setup/server/index.ts b/src/plugins/interactive_setup/server/index.ts index 018c6875b3c0..2f9a2cf3adec 100644 --- a/src/plugins/interactive_setup/server/index.ts +++ b/src/plugins/interactive_setup/server/index.ts @@ -14,7 +14,7 @@ import type { } from 'src/core/server'; import { ConfigSchema } from './config'; -import { UserSetupPlugin } from './plugin'; +import { InteractiveSetupPlugin } from './plugin'; export const config: PluginConfigDescriptor> = { schema: ConfigSchema, @@ -22,4 +22,4 @@ export const config: PluginConfigDescriptor> = { export const plugin: PluginInitializer = ( initializerContext: PluginInitializerContext -) => new UserSetupPlugin(initializerContext); +) => new InteractiveSetupPlugin(initializerContext); diff --git a/src/plugins/interactive_setup/server/kibana_config_writer.test.ts b/src/plugins/interactive_setup/server/kibana_config_writer.test.ts index 7ae98157ba15..7dc119b87f20 100644 --- a/src/plugins/interactive_setup/server/kibana_config_writer.test.ts +++ b/src/plugins/interactive_setup/server/kibana_config_writer.test.ts @@ -74,7 +74,7 @@ describe('KibanaConfigWriter', () => { await expect( kibanaConfigWriter.writeConfig({ - ca: 'ca-content', + caCert: 'ca-content', host: '', serviceAccountToken: { name: '', value: '' }, }) @@ -90,7 +90,7 @@ describe('KibanaConfigWriter', () => { await expect( kibanaConfigWriter.writeConfig({ - ca: 'ca-content', + caCert: 'ca-content', host: 'some-host', serviceAccountToken: { name: 'some-token', value: 'some-value' }, }) @@ -103,7 +103,7 @@ describe('KibanaConfigWriter', () => { '/some/path/kibana.yml', ` -# This section was automatically generated during setup (service account token name is "some-token"). +# This section was automatically generated during setup. elasticsearch.hosts: [some-host] elasticsearch.serviceAccountToken: some-value elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] @@ -112,10 +112,10 @@ elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] ); }); - it('can successfully write CA certificate and elasticsearch config to the disk', async () => { + it('can successfully write CA certificate and elasticsearch config with service token', async () => { await expect( kibanaConfigWriter.writeConfig({ - ca: 'ca-content', + caCert: 'ca-content', host: 'some-host', serviceAccountToken: { name: 'some-token', value: 'some-value' }, }) @@ -128,11 +128,62 @@ elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] '/some/path/kibana.yml', ` -# This section was automatically generated during setup (service account token name is "some-token"). +# This section was automatically generated during setup. elasticsearch.hosts: [some-host] elasticsearch.serviceAccountToken: some-value elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] +` + ); + }); + + it('can successfully write CA certificate and elasticsearch config with credentials', async () => { + await expect( + kibanaConfigWriter.writeConfig({ + caCert: 'ca-content', + host: 'some-host', + username: 'username', + password: 'password', + }) + ).resolves.toBeUndefined(); + + expect(mockWriteFile).toHaveBeenCalledTimes(1); + expect(mockWriteFile).toHaveBeenCalledWith('/some/path/ca_1234.crt', 'ca-content'); + expect(mockAppendFile).toHaveBeenCalledTimes(1); + expect(mockAppendFile).toHaveBeenCalledWith( + '/some/path/kibana.yml', + ` + +# This section was automatically generated during setup. +elasticsearch.hosts: [some-host] +elasticsearch.password: password +elasticsearch.username: username +elasticsearch.ssl.certificateAuthorities: [/some/path/ca_1234.crt] + +` + ); + }); + + it('can successfully write elasticsearch config without CA certificate', async () => { + await expect( + kibanaConfigWriter.writeConfig({ + host: 'some-host', + username: 'username', + password: 'password', + }) + ).resolves.toBeUndefined(); + + expect(mockWriteFile).not.toHaveBeenCalled(); + expect(mockAppendFile).toHaveBeenCalledTimes(1); + expect(mockAppendFile).toHaveBeenCalledWith( + '/some/path/kibana.yml', + ` + +# This section was automatically generated during setup. +elasticsearch.hosts: [some-host] +elasticsearch.password: password +elasticsearch.username: username + ` ); }); diff --git a/src/plugins/interactive_setup/server/kibana_config_writer.ts b/src/plugins/interactive_setup/server/kibana_config_writer.ts index b3178d9a909b..a59aa7640caa 100644 --- a/src/plugins/interactive_setup/server/kibana_config_writer.ts +++ b/src/plugins/interactive_setup/server/kibana_config_writer.ts @@ -15,11 +15,19 @@ import type { Logger } from 'src/core/server'; import { getDetailedErrorMessage } from './errors'; -export interface WriteConfigParameters { +export type WriteConfigParameters = { host: string; - ca: string; - serviceAccountToken: { name: string; value: string }; -} + caCert?: string; +} & ( + | { + username: string; + password: string; + } + | { + serviceAccountToken: { name: string; value: string }; + } + | {} +); export class KibanaConfigWriter { constructor(private readonly configPath: string, private readonly logger: Logger) {} @@ -54,31 +62,37 @@ export class KibanaConfigWriter { public async writeConfig(params: WriteConfigParameters) { const caPath = path.join(path.dirname(this.configPath), `ca_${Date.now()}.crt`); - this.logger.debug(`Writing CA certificate to ${caPath}.`); - try { - await fs.writeFile(caPath, params.ca); - this.logger.debug(`Successfully wrote CA certificate to ${caPath}.`); - } catch (err) { - this.logger.error( - `Failed to write CA certificate to ${caPath}: ${getDetailedErrorMessage(err)}.` - ); - throw err; + if (params.caCert) { + this.logger.debug(`Writing CA certificate to ${caPath}.`); + try { + await fs.writeFile(caPath, params.caCert); + this.logger.debug(`Successfully wrote CA certificate to ${caPath}.`); + } catch (err) { + this.logger.error( + `Failed to write CA certificate to ${caPath}: ${getDetailedErrorMessage(err)}.` + ); + throw err; + } + } + + const config: Record = { 'elasticsearch.hosts': [params.host] }; + if ('serviceAccountToken' in params) { + config['elasticsearch.serviceAccountToken'] = params.serviceAccountToken.value; + } else if ('username' in params) { + config['elasticsearch.password'] = params.password; + config['elasticsearch.username'] = params.username; + } + if (params.caCert) { + config['elasticsearch.ssl.certificateAuthorities'] = [caPath]; } this.logger.debug(`Writing Elasticsearch configuration to ${this.configPath}.`); try { await fs.appendFile( this.configPath, - `\n\n# This section was automatically generated during setup (service account token name is "${ - params.serviceAccountToken.name - }").\n${yaml.safeDump( - { - 'elasticsearch.hosts': [params.host], - 'elasticsearch.serviceAccountToken': params.serviceAccountToken.value, - 'elasticsearch.ssl.certificateAuthorities': [caPath], - }, - { flowLevel: 1 } - )}\n` + `\n\n# This section was automatically generated during setup.\n${yaml.safeDump(config, { + flowLevel: 1, + })}\n` ); this.logger.debug(`Successfully wrote Elasticsearch configuration to ${this.configPath}.`); } catch (err) { diff --git a/src/plugins/interactive_setup/server/plugin.ts b/src/plugins/interactive_setup/server/plugin.ts index 06ece32ba9c4..91a151e17b69 100644 --- a/src/plugins/interactive_setup/server/plugin.ts +++ b/src/plugins/interactive_setup/server/plugin.ts @@ -6,6 +6,7 @@ * Side Public License, v 1. */ +import chalk from 'chalk'; import type { Subscription } from 'rxjs'; import type { TypeOf } from '@kbn/config-schema'; @@ -17,13 +18,11 @@ import { ElasticsearchService } from './elasticsearch_service'; import { KibanaConfigWriter } from './kibana_config_writer'; import { defineRoutes } from './routes'; -export class UserSetupPlugin implements PrebootPlugin { +export class InteractiveSetupPlugin implements PrebootPlugin { readonly #logger: Logger; + readonly #elasticsearch: ElasticsearchService; #elasticsearchConnectionStatusSubscription?: Subscription; - readonly #elasticsearch = new ElasticsearchService( - this.initializerContext.logger.get('elasticsearch') - ); #configSubscription?: Subscription; #config?: ConfigType; @@ -36,6 +35,9 @@ export class UserSetupPlugin implements PrebootPlugin { constructor(private readonly initializerContext: PluginInitializerContext) { this.#logger = this.initializerContext.logger.get(); + this.#elasticsearch = new ElasticsearchService( + this.initializerContext.logger.get('elasticsearch') + ); } public setup(core: CorePreboot) { @@ -90,6 +92,14 @@ export class UserSetupPlugin implements PrebootPlugin { this.#logger.debug( 'Starting interactive setup mode since Kibana cannot to connect to Elasticsearch at http://localhost:9200.' ); + const serverInfo = core.http.getServerInfo(); + const url = `${serverInfo.protocol}://${serverInfo.hostname}:${serverInfo.port}`; + this.#logger.info(` + +${chalk.whiteBright.bold(`${chalk.cyanBright('i')} Kibana has not been configured.`)} + +Go to ${chalk.cyanBright.underline(url)} to get started. +`); } } ); diff --git a/src/plugins/interactive_setup/server/routes/configure.test.ts b/src/plugins/interactive_setup/server/routes/configure.test.ts new file mode 100644 index 000000000000..d6b7404fce51 --- /dev/null +++ b/src/plugins/interactive_setup/server/routes/configure.test.ts @@ -0,0 +1,276 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { errors } from '@elastic/elasticsearch'; + +import type { ObjectType } from '@kbn/config-schema'; +import type { IRouter, RequestHandler, RequestHandlerContext, RouteConfig } from 'src/core/server'; +import { kibanaResponseFactory } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +import { ElasticsearchConnectionStatus } from '../../common'; +import { interactiveSetupMock } from '../mocks'; +import { defineConfigureRoute } from './configure'; +import { routeDefinitionParamsMock } from './index.mock'; + +describe('Configure routes', () => { + let router: jest.Mocked; + let mockRouteParams: ReturnType; + let mockContext: RequestHandlerContext; + beforeEach(() => { + mockRouteParams = routeDefinitionParamsMock.create(); + router = mockRouteParams.router; + + mockContext = ({} as unknown) as RequestHandlerContext; + + defineConfigureRoute(mockRouteParams); + }); + + describe('#configure', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + + beforeEach(() => { + const [configureRouteConfig, configureRouteHandler] = router.post.mock.calls.find( + ([{ path }]) => path === '/internal/interactive_setup/configure' + )!; + + routeConfig = configureRouteConfig; + routeHandler = configureRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toEqual({ authRequired: false }); + + const bodySchema = (routeConfig.validate as any).body as ObjectType; + expect(() => bodySchema.validate({})).toThrowErrorMatchingInlineSnapshot( + `"[host]: expected value of type [string] but got [undefined]."` + ); + expect(() => bodySchema.validate({ host: '' })).toThrowErrorMatchingInlineSnapshot( + `"[host]: \\"host\\" is not allowed to be empty"` + ); + expect(() => + bodySchema.validate({ host: 'localhost:9200' }) + ).toThrowErrorMatchingInlineSnapshot(`"[host]: expected URI with scheme [http|https]."`); + expect(() => bodySchema.validate({ host: 'http://localhost:9200' })).not.toThrowError(); + expect(() => + bodySchema.validate({ host: 'http://localhost:9200', username: 'elastic' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[username]: value of \\"elastic\\" is forbidden. This is a superuser account that can obfuscate privilege-related issues. You should use the \\"kibana_system\\" user instead."` + ); + expect(() => + bodySchema.validate({ host: 'http://localhost:9200', username: 'kibana_system' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[password]: expected value of type [string] but got [undefined]"` + ); + expect(() => + bodySchema.validate({ host: 'http://localhost:9200', password: 'password' }) + ).toThrowErrorMatchingInlineSnapshot(`"[password]: a value wasn't expected to be present"`); + expect(() => + bodySchema.validate({ + host: 'http://localhost:9200', + username: 'kibana_system', + password: '', + }) + ).not.toThrowError(); + expect(() => + bodySchema.validate({ host: 'https://localhost:9200' }) + ).toThrowErrorMatchingInlineSnapshot( + `"[caCert]: expected value of type [string] but got [undefined]"` + ); + expect(() => + bodySchema.validate({ host: 'https://localhost:9200', caCert: 'der' }) + ).not.toThrowError(); + }); + + it('fails if setup is not on hold.', async () => { + mockRouteParams.preboot.isSetupOnHold.mockReturnValue(false); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { host: 'host1' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({ + status: 400, + options: { body: 'Cannot process request outside of preboot stage.' }, + payload: 'Cannot process request outside of preboot stage.', + }); + + expect(mockRouteParams.elasticsearch.authenticate).not.toHaveBeenCalled(); + expect(mockRouteParams.kibanaConfigWriter.writeConfig).not.toHaveBeenCalled(); + expect(mockRouteParams.preboot.completeSetup).not.toHaveBeenCalled(); + }); + + it('fails if Elasticsearch connection is already configured.', async () => { + mockRouteParams.preboot.isSetupOnHold.mockReturnValue(true); + mockRouteParams.elasticsearch.connectionStatus$.next( + ElasticsearchConnectionStatus.Configured + ); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { host: 'host1' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({ + status: 400, + options: { + body: { + message: 'Elasticsearch connection is already configured.', + attributes: { type: 'elasticsearch_connection_configured' }, + }, + }, + payload: { + message: 'Elasticsearch connection is already configured.', + attributes: { type: 'elasticsearch_connection_configured' }, + }, + }); + + expect(mockRouteParams.elasticsearch.authenticate).not.toHaveBeenCalled(); + expect(mockRouteParams.kibanaConfigWriter.writeConfig).not.toHaveBeenCalled(); + expect(mockRouteParams.preboot.completeSetup).not.toHaveBeenCalled(); + }); + + it('fails if Kibana config is not writable.', async () => { + mockRouteParams.preboot.isSetupOnHold.mockReturnValue(true); + mockRouteParams.elasticsearch.connectionStatus$.next( + ElasticsearchConnectionStatus.NotConfigured + ); + mockRouteParams.kibanaConfigWriter.isConfigWritable.mockResolvedValue(false); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { host: 'host1' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({ + status: 500, + options: { + body: { + message: 'Kibana process does not have enough permissions to write to config file.', + attributes: { type: 'kibana_config_not_writable' }, + }, + statusCode: 500, + }, + payload: { + message: 'Kibana process does not have enough permissions to write to config file.', + attributes: { type: 'kibana_config_not_writable' }, + }, + }); + + expect(mockRouteParams.elasticsearch.authenticate).not.toHaveBeenCalled(); + expect(mockRouteParams.kibanaConfigWriter.writeConfig).not.toHaveBeenCalled(); + expect(mockRouteParams.preboot.completeSetup).not.toHaveBeenCalled(); + }); + + it('fails if authenticate call fails.', async () => { + mockRouteParams.preboot.isSetupOnHold.mockReturnValue(true); + mockRouteParams.elasticsearch.connectionStatus$.next( + ElasticsearchConnectionStatus.NotConfigured + ); + mockRouteParams.kibanaConfigWriter.isConfigWritable.mockResolvedValue(true); + mockRouteParams.elasticsearch.authenticate.mockRejectedValue( + new errors.ResponseError( + interactiveSetupMock.createApiResponse({ + statusCode: 401, + body: { message: 'some-secret-message' }, + }) + ) + ); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { host: 'host1' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({ + status: 500, + options: { + body: { message: 'Failed to configure.', attributes: { type: 'configure_failure' } }, + statusCode: 500, + }, + payload: { message: 'Failed to configure.', attributes: { type: 'configure_failure' } }, + }); + + expect(mockRouteParams.elasticsearch.authenticate).toHaveBeenCalledTimes(1); + expect(mockRouteParams.kibanaConfigWriter.writeConfig).not.toHaveBeenCalled(); + expect(mockRouteParams.preboot.completeSetup).not.toHaveBeenCalled(); + }); + + it('fails if cannot write configuration to the disk.', async () => { + mockRouteParams.preboot.isSetupOnHold.mockReturnValue(true); + mockRouteParams.elasticsearch.connectionStatus$.next( + ElasticsearchConnectionStatus.NotConfigured + ); + mockRouteParams.kibanaConfigWriter.isConfigWritable.mockResolvedValue(true); + mockRouteParams.kibanaConfigWriter.writeConfig.mockRejectedValue( + new Error('Some error with sensitive path') + ); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { host: 'host1' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({ + status: 500, + options: { + body: { + message: 'Failed to save configuration.', + attributes: { type: 'kibana_config_failure' }, + }, + statusCode: 500, + }, + payload: { + message: 'Failed to save configuration.', + attributes: { type: 'kibana_config_failure' }, + }, + }); + + expect(mockRouteParams.elasticsearch.authenticate).toHaveBeenCalledTimes(1); + expect(mockRouteParams.kibanaConfigWriter.writeConfig).toHaveBeenCalledTimes(1); + expect(mockRouteParams.preboot.completeSetup).not.toHaveBeenCalled(); + }); + + it('can successfully authenticate and save configuration to the disk.', async () => { + mockRouteParams.preboot.isSetupOnHold.mockReturnValue(true); + mockRouteParams.elasticsearch.connectionStatus$.next( + ElasticsearchConnectionStatus.NotConfigured + ); + mockRouteParams.kibanaConfigWriter.isConfigWritable.mockResolvedValue(true); + mockRouteParams.kibanaConfigWriter.writeConfig.mockResolvedValue(); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { host: 'host', username: 'username', password: 'password', caCert: 'der' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({ + status: 204, + options: {}, + payload: undefined, + }); + + expect(mockRouteParams.elasticsearch.authenticate).toHaveBeenCalledTimes(1); + expect(mockRouteParams.elasticsearch.authenticate).toHaveBeenCalledWith({ + host: 'host', + username: 'username', + password: 'password', + caCert: '-----BEGIN CERTIFICATE-----\nder\n-----END CERTIFICATE-----\n', + }); + + expect(mockRouteParams.kibanaConfigWriter.writeConfig).toHaveBeenCalledTimes(1); + expect(mockRouteParams.kibanaConfigWriter.writeConfig).toHaveBeenCalledWith({ + host: 'host', + username: 'username', + password: 'password', + caCert: '-----BEGIN CERTIFICATE-----\nder\n-----END CERTIFICATE-----\n', + }); + + expect(mockRouteParams.preboot.completeSetup).toHaveBeenCalledTimes(1); + expect(mockRouteParams.preboot.completeSetup).toHaveBeenCalledWith({ + shouldReloadConfig: true, + }); + }); + }); +}); diff --git a/src/plugins/interactive_setup/server/routes/configure.ts b/src/plugins/interactive_setup/server/routes/configure.ts new file mode 100644 index 000000000000..a34af0296ea0 --- /dev/null +++ b/src/plugins/interactive_setup/server/routes/configure.ts @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { first } from 'rxjs/operators'; + +import { schema } from '@kbn/config-schema'; + +import type { RouteDefinitionParams } from '.'; +import { ElasticsearchConnectionStatus } from '../../common'; +import type { AuthenticateParameters } from '../elasticsearch_service'; +import { ElasticsearchService } from '../elasticsearch_service'; +import type { WriteConfigParameters } from '../kibana_config_writer'; + +export function defineConfigureRoute({ + router, + logger, + kibanaConfigWriter, + elasticsearch, + preboot, +}: RouteDefinitionParams) { + router.post( + { + path: '/internal/interactive_setup/configure', + validate: { + query: schema.object({ + code: schema.maybe(schema.string()), + }), + body: schema.object({ + host: schema.uri({ scheme: ['http', 'https'] }), + username: schema.maybe( + schema.string({ + validate: (value: string) => { + if (value === 'elastic') { + return ( + 'value of "elastic" is forbidden. This is a superuser account that can obfuscate ' + + 'privilege-related issues. You should use the "kibana_system" user instead.' + ); + } + }, + }) + ), + password: schema.conditional( + schema.siblingRef('username'), + schema.string(), + schema.string(), + schema.never() + ), + caCert: schema.conditional( + schema.siblingRef('host'), + schema.uri({ scheme: 'https' }), + schema.string(), + schema.never() + ), + }), + }, + options: { authRequired: false }, + }, + async (context, request, response) => { + if (!preboot.isSetupOnHold()) { + logger.error(`Invalid request to [path=${request.url.pathname}] outside of preboot stage`); + return response.badRequest({ body: 'Cannot process request outside of preboot stage.' }); + } + + const connectionStatus = await elasticsearch.connectionStatus$.pipe(first()).toPromise(); + if (connectionStatus === ElasticsearchConnectionStatus.Configured) { + logger.error( + `Invalid request to [path=${request.url.pathname}], Elasticsearch connection is already configured.` + ); + return response.badRequest({ + body: { + message: 'Elasticsearch connection is already configured.', + attributes: { type: 'elasticsearch_connection_configured' }, + }, + }); + } + + // The most probable misconfiguration case is when Kibana process isn't allowed to write to the + // Kibana configuration file. We'll still have to handle possible filesystem access errors + // when we actually write to the disk, but this preliminary check helps us to avoid unnecessary + // enrollment call and communicate that to the user early. + const isConfigWritable = await kibanaConfigWriter.isConfigWritable(); + if (!isConfigWritable) { + logger.error('Kibana process does not have enough permissions to write to config file'); + return response.customError({ + statusCode: 500, + body: { + message: 'Kibana process does not have enough permissions to write to config file.', + attributes: { type: 'kibana_config_not_writable' }, + }, + }); + } + + const configToWrite: WriteConfigParameters & AuthenticateParameters = { + host: request.body.host, + username: request.body.username, + password: request.body.password, + caCert: request.body.caCert + ? ElasticsearchService.createPemCertificate(request.body.caCert) + : undefined, + }; + + try { + await elasticsearch.authenticate(configToWrite); + } catch { + // For security reasons, we shouldn't leak to the user whether Elasticsearch node couldn't process enrollment + // request or we just couldn't connect to any of the provided hosts. + return response.customError({ + statusCode: 500, + body: { message: 'Failed to configure.', attributes: { type: 'configure_failure' } }, + }); + } + + try { + await kibanaConfigWriter.writeConfig(configToWrite); + } catch { + // For security reasons, we shouldn't leak any filesystem related errors. + return response.customError({ + statusCode: 500, + body: { + message: 'Failed to save configuration.', + attributes: { type: 'kibana_config_failure' }, + }, + }); + } + + preboot.completeSetup({ shouldReloadConfig: true }); + + return response.noContent(); + } + ); +} diff --git a/src/plugins/interactive_setup/server/routes/enroll.ts b/src/plugins/interactive_setup/server/routes/enroll.ts index 6a2da28787a4..41291246802e 100644 --- a/src/plugins/interactive_setup/server/routes/enroll.ts +++ b/src/plugins/interactive_setup/server/routes/enroll.ts @@ -12,6 +12,7 @@ import { schema } from '@kbn/config-schema'; import { ElasticsearchConnectionStatus } from '../../common'; import type { EnrollResult } from '../elasticsearch_service'; +import type { WriteConfigParameters } from '../kibana_config_writer'; import type { RouteDefinitionParams } from './'; /** @@ -81,9 +82,9 @@ export function defineEnrollRoutes({ .match(/.{1,2}/g) ?.join(':') ?? ''; - let enrollResult: EnrollResult; + let configToWrite: WriteConfigParameters & EnrollResult; try { - enrollResult = await elasticsearch.enroll({ + configToWrite = await elasticsearch.enroll({ apiKey: request.body.apiKey, hosts: request.body.hosts, caFingerprint: colonFormattedCaFingerprint, @@ -98,7 +99,7 @@ export function defineEnrollRoutes({ } try { - await kibanaConfigWriter.writeConfig(enrollResult); + await kibanaConfigWriter.writeConfig(configToWrite); } catch { // For security reasons, we shouldn't leak any filesystem related errors. return response.customError({ diff --git a/src/plugins/interactive_setup/server/routes/index.ts b/src/plugins/interactive_setup/server/routes/index.ts index 752c5828ecb5..75c383176e7e 100644 --- a/src/plugins/interactive_setup/server/routes/index.ts +++ b/src/plugins/interactive_setup/server/routes/index.ts @@ -12,7 +12,9 @@ import type { IBasePath, IRouter, Logger, PrebootServicePreboot } from 'src/core import type { ConfigType } from '../config'; import type { ElasticsearchServiceSetup } from '../elasticsearch_service'; import type { KibanaConfigWriter } from '../kibana_config_writer'; +import { defineConfigureRoute } from './configure'; import { defineEnrollRoutes } from './enroll'; +import { definePingRoute } from './ping'; /** * Describes parameters used to define HTTP routes. @@ -31,4 +33,6 @@ export interface RouteDefinitionParams { export function defineRoutes(params: RouteDefinitionParams) { defineEnrollRoutes(params); + defineConfigureRoute(params); + definePingRoute(params); } diff --git a/src/plugins/interactive_setup/server/routes/ping.test.ts b/src/plugins/interactive_setup/server/routes/ping.test.ts new file mode 100644 index 000000000000..295ad3b61299 --- /dev/null +++ b/src/plugins/interactive_setup/server/routes/ping.test.ts @@ -0,0 +1,125 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { errors } from '@elastic/elasticsearch'; + +import type { ObjectType } from '@kbn/config-schema'; +import type { IRouter, RequestHandler, RequestHandlerContext, RouteConfig } from 'src/core/server'; +import { kibanaResponseFactory } from 'src/core/server'; +import { httpServerMock } from 'src/core/server/mocks'; + +import { interactiveSetupMock } from '../mocks'; +import { routeDefinitionParamsMock } from './index.mock'; +import { definePingRoute } from './ping'; + +describe('Configure routes', () => { + let router: jest.Mocked; + let mockRouteParams: ReturnType; + let mockContext: RequestHandlerContext; + beforeEach(() => { + mockRouteParams = routeDefinitionParamsMock.create(); + router = mockRouteParams.router; + + mockContext = ({} as unknown) as RequestHandlerContext; + + definePingRoute(mockRouteParams); + }); + + describe('#ping', () => { + let routeHandler: RequestHandler; + let routeConfig: RouteConfig; + + beforeEach(() => { + const [configureRouteConfig, configureRouteHandler] = router.post.mock.calls.find( + ([{ path }]) => path === '/internal/interactive_setup/ping' + )!; + + routeConfig = configureRouteConfig; + routeHandler = configureRouteHandler; + }); + + it('correctly defines route.', () => { + expect(routeConfig.options).toEqual({ authRequired: false }); + + const bodySchema = (routeConfig.validate as any).body as ObjectType; + expect(() => bodySchema.validate({})).toThrowErrorMatchingInlineSnapshot( + `"[host]: expected value of type [string] but got [undefined]."` + ); + expect(() => bodySchema.validate({ host: '' })).toThrowErrorMatchingInlineSnapshot( + `"[host]: \\"host\\" is not allowed to be empty"` + ); + expect(() => + bodySchema.validate({ host: 'localhost:9200' }) + ).toThrowErrorMatchingInlineSnapshot(`"[host]: expected URI with scheme [http|https]."`); + expect(() => bodySchema.validate({ host: 'http://localhost:9200' })).not.toThrowError(); + }); + + it('fails if setup is not on hold.', async () => { + mockRouteParams.preboot.isSetupOnHold.mockReturnValue(false); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { host: 'host' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({ + status: 400, + options: { body: 'Cannot process request outside of preboot stage.' }, + payload: 'Cannot process request outside of preboot stage.', + }); + + expect(mockRouteParams.elasticsearch.authenticate).not.toHaveBeenCalled(); + expect(mockRouteParams.preboot.completeSetup).not.toHaveBeenCalled(); + }); + + it('fails if ping call fails.', async () => { + mockRouteParams.preboot.isSetupOnHold.mockReturnValue(true); + mockRouteParams.elasticsearch.ping.mockRejectedValue( + new errors.ResponseError( + interactiveSetupMock.createApiResponse({ + statusCode: 401, + body: { message: 'some-secret-message' }, + }) + ) + ); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { host: 'host' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({ + status: 500, + options: { + body: { message: 'Failed to ping cluster.', attributes: { type: 'ping_failure' } }, + statusCode: 500, + }, + payload: { message: 'Failed to ping cluster.', attributes: { type: 'ping_failure' } }, + }); + + expect(mockRouteParams.elasticsearch.ping).toHaveBeenCalledTimes(1); + expect(mockRouteParams.preboot.completeSetup).not.toHaveBeenCalled(); + }); + + it('can successfully ping.', async () => { + mockRouteParams.preboot.isSetupOnHold.mockReturnValue(true); + + const mockRequest = httpServerMock.createKibanaRequest({ + body: { host: 'host' }, + }); + + await expect(routeHandler(mockContext, mockRequest, kibanaResponseFactory)).resolves.toEqual({ + status: 200, + options: {}, + payload: undefined, + }); + + expect(mockRouteParams.elasticsearch.ping).toHaveBeenCalledTimes(1); + expect(mockRouteParams.elasticsearch.ping).toHaveBeenCalledWith('host'); + expect(mockRouteParams.preboot.completeSetup).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/plugins/interactive_setup/server/routes/ping.ts b/src/plugins/interactive_setup/server/routes/ping.ts new file mode 100644 index 000000000000..50ed7514bab6 --- /dev/null +++ b/src/plugins/interactive_setup/server/routes/ping.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { schema } from '@kbn/config-schema'; + +import type { RouteDefinitionParams } from '.'; +import type { PingResult } from '../../common/types'; + +export function definePingRoute({ router, logger, elasticsearch, preboot }: RouteDefinitionParams) { + router.post( + { + path: '/internal/interactive_setup/ping', + validate: { + body: schema.object({ + host: schema.uri({ scheme: ['http', 'https'] }), + }), + }, + options: { authRequired: false }, + }, + async (context, request, response) => { + if (!preboot.isSetupOnHold()) { + logger.error(`Invalid request to [path=${request.url.pathname}] outside of preboot stage`); + return response.badRequest({ body: 'Cannot process request outside of preboot stage.' }); + } + + let result: PingResult; + try { + result = await elasticsearch.ping(request.body.host); + } catch { + return response.customError({ + statusCode: 500, + body: { message: 'Failed to ping cluster.', attributes: { type: 'ping_failure' } }, + }); + } + + return response.ok({ body: result }); + } + ); +} diff --git a/src/plugins/kibana_legacy/kibana.json b/src/plugins/kibana_legacy/kibana.json index b9c808101b39..b1d7b10f9527 100644 --- a/src/plugins/kibana_legacy/kibana.json +++ b/src/plugins/kibana_legacy/kibana.json @@ -4,7 +4,7 @@ "server": true, "ui": true, "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" } } diff --git a/src/plugins/kibana_overview/kibana.json b/src/plugins/kibana_overview/kibana.json index 1b052a239ba3..2924e71ed828 100644 --- a/src/plugins/kibana_overview/kibana.json +++ b/src/plugins/kibana_overview/kibana.json @@ -7,7 +7,7 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["navigation", "data", "home"], + "requiredPlugins": ["navigation", "data", "home", "share"], "optionalPlugins": ["newsfeed", "usageCollection"], "requiredBundles": ["kibanaReact", "newsfeed"] } diff --git a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap index b00ad2565117..6da2f95fa394 100644 --- a/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap +++ b/src/plugins/kibana_overview/public/components/overview/__snapshots__/overview.test.tsx.snap @@ -225,6 +225,9 @@ exports[`Overview render 1`] = ` addBasePath={ [MockFunction] { "calls": Array [ + Array [ + "/app/home#/tutorial_directory", + ], Array [ "home#/tutorial_directory", ], @@ -254,6 +257,10 @@ exports[`Overview render 1`] = ` ], ], "results": Array [ + Object { + "type": "return", + "value": "/app/home#/tutorial_directory", + }, Object { "type": "return", "value": "home#/tutorial_directory", @@ -525,6 +532,9 @@ exports[`Overview without features 1`] = ` addBasePath={ [MockFunction] { "calls": Array [ + Array [ + "/app/home#/tutorial_directory", + ], Array [ "home#/tutorial_directory", ], @@ -552,9 +562,15 @@ exports[`Overview without features 1`] = ` Array [ "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], + Array [ + "/app/home#/tutorial_directory", + ], Array [ "home#/tutorial_directory", ], + Array [ + "/app/home#/tutorial_directory", + ], Array [ "home#/tutorial_directory", ], @@ -584,6 +600,10 @@ exports[`Overview without features 1`] = ` ], ], "results": Array [ + Object { + "type": "return", + "value": "/app/home#/tutorial_directory", + }, Object { "type": "return", "value": "home#/tutorial_directory", @@ -620,10 +640,18 @@ exports[`Overview without features 1`] = ` "type": "return", "value": "/plugins/kibanaReact/assets/solutions_solution_4.svg", }, + Object { + "type": "return", + "value": "/app/home#/tutorial_directory", + }, Object { "type": "return", "value": "home#/tutorial_directory", }, + Object { + "type": "return", + "value": "/app/home#/tutorial_directory", + }, Object { "type": "return", "value": "home#/tutorial_directory", @@ -772,6 +800,9 @@ exports[`Overview without solutions 1`] = ` addBasePath={ [MockFunction] { "calls": Array [ + Array [ + "/app/home#/tutorial_directory", + ], Array [ "home#/tutorial_directory", ], @@ -799,11 +830,18 @@ exports[`Overview without solutions 1`] = ` Array [ "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], + Array [ + "/app/home#/tutorial_directory", + ], Array [ "home#/tutorial_directory", ], ], "results": Array [ + Object { + "type": "return", + "value": "/app/home#/tutorial_directory", + }, Object { "type": "return", "value": "home#/tutorial_directory", @@ -840,6 +878,10 @@ exports[`Overview without solutions 1`] = ` "type": "return", "value": "/plugins/kibanaReact/assets/solutions_solution_4.svg", }, + Object { + "type": "return", + "value": "/app/home#/tutorial_directory", + }, Object { "type": "return", "value": "home#/tutorial_directory", @@ -855,6 +897,9 @@ exports[`Overview without solutions 1`] = ` addBasePath={ [MockFunction] { "calls": Array [ + Array [ + "/app/home#/tutorial_directory", + ], Array [ "home#/tutorial_directory", ], @@ -882,11 +927,18 @@ exports[`Overview without solutions 1`] = ` Array [ "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], + Array [ + "/app/home#/tutorial_directory", + ], Array [ "home#/tutorial_directory", ], ], "results": Array [ + Object { + "type": "return", + "value": "/app/home#/tutorial_directory", + }, Object { "type": "return", "value": "home#/tutorial_directory", @@ -923,6 +975,10 @@ exports[`Overview without solutions 1`] = ` "type": "return", "value": "/plugins/kibanaReact/assets/solutions_solution_4.svg", }, + Object { + "type": "return", + "value": "/app/home#/tutorial_directory", + }, Object { "type": "return", "value": "home#/tutorial_directory", @@ -944,6 +1000,9 @@ exports[`Overview without solutions 1`] = ` addBasePath={ [MockFunction] { "calls": Array [ + Array [ + "/app/home#/tutorial_directory", + ], Array [ "home#/tutorial_directory", ], @@ -971,11 +1030,18 @@ exports[`Overview without solutions 1`] = ` Array [ "/plugins/kibanaReact/assets/solutions_solution_4.svg", ], + Array [ + "/app/home#/tutorial_directory", + ], Array [ "home#/tutorial_directory", ], ], "results": Array [ + Object { + "type": "return", + "value": "/app/home#/tutorial_directory", + }, Object { "type": "return", "value": "home#/tutorial_directory", @@ -1012,6 +1078,10 @@ exports[`Overview without solutions 1`] = ` "type": "return", "value": "/plugins/kibanaReact/assets/solutions_solution_4.svg", }, + Object { + "type": "return", + "value": "/app/home#/tutorial_directory", + }, Object { "type": "return", "value": "home#/tutorial_directory", diff --git a/src/plugins/kibana_overview/public/components/overview/overview.test.tsx b/src/plugins/kibana_overview/public/components/overview/overview.test.tsx index d7e922a40d6f..ca9b404ed981 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.test.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.test.tsx @@ -17,6 +17,7 @@ jest.mock('../../../../../../src/plugins/kibana_react/public', () => ({ services: { http: { basePath: { prepend: jest.fn((path: string) => (path ? path : 'path')) } }, data: { indexPatterns: {} }, + share: { url: { locators: { get: () => ({ useUrl: () => '' }) } } }, uiSettings: { get: jest.fn() }, docLinks: { links: { diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index 68a469c753ce..b49be0670b58 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -54,12 +54,19 @@ interface Props { export const Overview: FC = ({ newsFetchResult, solutions, features }) => { const [isNewKibanaInstance, setNewKibanaInstance] = useState(false); const { - services: { http, docLinks, data, uiSettings, application }, + services: { http, docLinks, data, share, uiSettings, application }, } = useKibana(); const addBasePath = http.basePath.prepend; const indexPatternService = data.indexPatterns; const IS_DARK_THEME = uiSettings.get('theme:darkMode'); + // Home does not have a locator implemented, so hard-code it here. + const addDataHref = addBasePath('/app/home#/tutorial_directory'); + const devToolsHref = share.url.locators.get('CONSOLE_APP_LOCATOR')?.useUrl({}); + const managementHref = share.url.locators + .get('MANAGEMENT_APP_LOCATOR') + ?.useUrl({ sectionId: '' }); + const getFeaturesByCategory = (category: string) => features .filter((feature) => feature.showOnHomePage && feature.category === category) @@ -135,9 +142,13 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => iconType: 'logoKibana', pageTitle: , rightSideItems: overviewPageActions({ - addBasePath, + addDataHref, application, + devToolsHref, hidden: isNewKibanaInstance, + managementHref, + showDevToolsLink: !!devTools, + showManagementLink: !!manageDataFeatures, }), }} noDataConfig={isNewKibanaInstance ? noDataConfig : undefined} diff --git a/src/plugins/kibana_overview/public/types.ts b/src/plugins/kibana_overview/public/types.ts index 0459650a5074..032706c9e76a 100644 --- a/src/plugins/kibana_overview/public/types.ts +++ b/src/plugins/kibana_overview/public/types.ts @@ -11,6 +11,7 @@ import { NavigationPublicPluginStart } from 'src/plugins/navigation/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; import { NewsfeedPublicPluginStart } from 'src/plugins/newsfeed/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; +import { SharePluginStart } from '../../share/public'; // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface KibanaOverviewPluginSetup {} @@ -28,4 +29,5 @@ export interface AppPluginStartDependencies { data: DataPublicPluginStart; navigation: NavigationPublicPluginStart; newsfeed?: NewsfeedPublicPluginStart; + share: SharePluginStart; } diff --git a/src/plugins/kibana_react/public/overview_page/overview_page_actions/__snapshots__/overview_page_actions.test.tsx.snap b/src/plugins/kibana_react/public/overview_page/overview_page_actions/__snapshots__/overview_page_actions.test.tsx.snap index 91e03bcd7cf1..7492f9e1b620 100644 --- a/src/plugins/kibana_react/public/overview_page/overview_page_actions/__snapshots__/overview_page_actions.test.tsx.snap +++ b/src/plugins/kibana_react/public/overview_page/overview_page_actions/__snapshots__/overview_page_actions.test.tsx.snap @@ -18,56 +18,12 @@ Array [ className="kbnOverviewPageHeader__actionButton" data-test-subj="homeAddData" flush="both" - href="/app/home#/tutorial_directory" + href="" iconType="plusInCircle" > Add data , - - - Manage - - , - - - Dev tools - - , ] `; @@ -91,7 +47,7 @@ Array [ className="kbnOverviewPageHeader__actionButton" data-test-subj="homeAddData" flush="both" - href="/app/home#/tutorial_directory" + href="" iconType="plusInCircle" > Add data diff --git a/src/plugins/kibana_react/public/overview_page/overview_page_actions/overview_page_actions.test.tsx b/src/plugins/kibana_react/public/overview_page/overview_page_actions/overview_page_actions.test.tsx index f324d5619614..34abf9439c1c 100644 --- a/src/plugins/kibana_react/public/overview_page/overview_page_actions/overview_page_actions.test.tsx +++ b/src/plugins/kibana_react/public/overview_page/overview_page_actions/overview_page_actions.test.tsx @@ -15,8 +15,6 @@ jest.mock('../../app_links', () => ({ afterAll(() => jest.clearAllMocks()); -const addBasePathMock = jest.fn((path: string) => (path ? path : 'path')); - const applicationStartMock = ({ capabilities: { navLinks: { management: true, dev_tools: true } }, } as unknown) as ApplicationStart; @@ -24,7 +22,7 @@ const applicationStartMock = ({ describe('overviewPageActions', () => { test('only add data button', () => { const array = overviewPageActions({ - addBasePath: addBasePathMock, + addDataHref: '', application: applicationStartMock, }); expect(array).toMatchSnapshot(); @@ -32,7 +30,7 @@ describe('overviewPageActions', () => { test('all buttons', () => { const array = overviewPageActions({ - addBasePath: addBasePathMock, + addDataHref: '', application: applicationStartMock, showDevToolsLink: true, showManagementLink: true, @@ -42,7 +40,7 @@ describe('overviewPageActions', () => { test('no buttons', () => { const array = overviewPageActions({ - addBasePath: addBasePathMock, + addDataHref: '', application: applicationStartMock, hidden: true, }); diff --git a/src/plugins/kibana_react/public/overview_page/overview_page_actions/overview_page_actions.tsx b/src/plugins/kibana_react/public/overview_page/overview_page_actions/overview_page_actions.tsx index 186becf757db..5903a51ae317 100644 --- a/src/plugins/kibana_react/public/overview_page/overview_page_actions/overview_page_actions.tsx +++ b/src/plugins/kibana_react/public/overview_page/overview_page_actions/overview_page_actions.tsx @@ -13,17 +13,21 @@ import { ApplicationStart } from 'kibana/public'; import { RedirectAppLinks } from '../../app_links'; interface Props { - addBasePath: (path: string) => string; + addDataHref: string; application: ApplicationStart; + devToolsHref?: string; hidden?: boolean; + managementHref?: string; showDevToolsLink?: boolean; showManagementLink?: boolean; } export const overviewPageActions = ({ - addBasePath, + addDataHref, application, + devToolsHref, hidden, + managementHref, showDevToolsLink, showManagementLink, }: Props) => { @@ -38,7 +42,7 @@ export const overviewPageActions = ({ data-test-subj="homeAddData" className="kbnOverviewPageHeader__actionButton" flush="both" - href={addBasePath('/app/home#/tutorial_directory')} + href={addDataHref} iconType="plusInCircle" > {i18n.translate('kibana-react.kbnOverviewPageHeader.addDataButtonLabel', { @@ -49,14 +53,14 @@ export const overviewPageActions = ({ ); const actionStackManagement = - showManagementLink && isManagementEnabled ? ( + managementHref && showManagementLink && isManagementEnabled ? ( {i18n.translate('kibana-react.kbnOverviewPageHeader.stackManagementButtonLabel', { defaultMessage: 'Manage', @@ -66,14 +70,14 @@ export const overviewPageActions = ({ ) : null; const actionDevTools = - showDevToolsLink && isDevToolsEnabled ? ( + devToolsHref && showDevToolsLink && isDevToolsEnabled ? ( {i18n.translate('kibana-react.kbnOverviewPageHeader.devToolsButtonLabel', { defaultMessage: 'Dev tools', diff --git a/src/plugins/kibana_react/public/util/mount_point_portal.test.tsx b/src/plugins/kibana_react/public/util/mount_point_portal.test.tsx index 53503b197567..39e345568a29 100644 --- a/src/plugins/kibana_react/public/util/mount_point_portal.test.tsx +++ b/src/plugins/kibana_react/public/util/mount_point_portal.test.tsx @@ -19,13 +19,18 @@ describe('MountPointPortal', () => { let dom: ReactWrapper; const refresh = () => { - new Promise(async (resolve) => { - if (dom) { - act(() => { - dom.update(); - }); + new Promise(async (resolve, reject) => { + try { + if (dom) { + act(() => { + dom.update(); + }); + } + + setImmediate(() => resolve(dom)); // flushes any pending promises + } catch (error) { + reject(error); } - setImmediate(() => resolve(dom)); // flushes any pending promises }); }; diff --git a/src/plugins/management/kibana.json b/src/plugins/management/kibana.json index ad1d6d01f140..9f033637de96 100644 --- a/src/plugins/management/kibana.json +++ b/src/plugins/management/kibana.json @@ -6,7 +6,7 @@ "optionalPlugins": ["home", "share"], "requiredBundles": ["kibanaReact", "kibanaUtils", "home"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" } } diff --git a/src/plugins/maps_ems/config.ts b/src/plugins/maps_ems/config.ts index 1deff36a10e4..ed1648ebbe8b 100644 --- a/src/plugins/maps_ems/config.ts +++ b/src/plugins/maps_ems/config.ts @@ -36,33 +36,7 @@ export const tilemapConfigSchema = schema.object({ options: tileMapConfigOptionsSchema, }); -const layerConfigSchema = schema.object({ - url: schema.string(), - format: schema.object({ - type: schema.string({ defaultValue: 'geojson' }), - }), - meta: schema.object({ - feature_collection_path: schema.string({ defaultValue: 'data' }), - }), - attribution: schema.string(), - name: schema.string(), - fields: schema.arrayOf( - schema.object({ - name: schema.string(), - description: schema.string(), - }) - ), -}); - -export type LayerConfig = TypeOf; - -const regionmapConfigSchema = schema.object({ - includeElasticMapsService: schema.boolean({ defaultValue: true }), - layers: schema.arrayOf(layerConfigSchema, { defaultValue: [] }), -}); - export const emsConfigSchema = schema.object({ - regionmap: regionmapConfigSchema, tilemap: tilemapConfigSchema, includeElasticMapsService: schema.boolean({ defaultValue: true }), proxyElasticMapsServiceInMaps: schema.boolean({ defaultValue: false }), diff --git a/src/plugins/maps_ems/public/index.ts b/src/plugins/maps_ems/public/index.ts index 0dfe808ca6f6..1784613faa94 100644 --- a/src/plugins/maps_ems/public/index.ts +++ b/src/plugins/maps_ems/public/index.ts @@ -24,7 +24,7 @@ export function plugin(initializerContext: PluginInitializerContext) { return new MapsEmsPlugin(initializerContext); } -export type { MapsEmsConfig, LayerConfig } from '../config'; +export type { MapsEmsConfig } from '../config'; export * from '../common'; diff --git a/src/plugins/maps_ems/public/lazy_load_bundle/get_service_settings.ts b/src/plugins/maps_ems/public/lazy_load_bundle/get_service_settings.ts index 6e32ff5d4e41..8eafada176e7 100644 --- a/src/plugins/maps_ems/public/lazy_load_bundle/get_service_settings.ts +++ b/src/plugins/maps_ems/public/lazy_load_bundle/get_service_settings.ts @@ -16,10 +16,14 @@ export async function getServiceSettings(): Promise { return loadPromise; } - loadPromise = new Promise(async (resolve) => { - const { ServiceSettings } = await import('./lazy'); - const config = getMapsEmsConfig(); - resolve(new ServiceSettings(config, config.tilemap)); + loadPromise = new Promise(async (resolve, reject) => { + try { + const { ServiceSettings } = await import('./lazy'); + const config = getMapsEmsConfig(); + resolve(new ServiceSettings(config, config.tilemap)); + } catch (error) { + reject(error); + } }); return loadPromise; } diff --git a/src/plugins/maps_ems/server/index.ts b/src/plugins/maps_ems/server/index.ts index 0bfa68c8edd7..b8b84d222b95 100644 --- a/src/plugins/maps_ems/server/index.ts +++ b/src/plugins/maps_ems/server/index.ts @@ -16,7 +16,6 @@ import { MapsEmsConfig, emsConfigSchema } from '../config'; export const config: PluginConfigDescriptor = { exposeToBrowser: { - regionmap: true, tilemap: true, includeElasticMapsService: true, proxyElasticMapsServiceInMaps: true, diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx index 558739a44dd4..45b9b4c7a885 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.test.tsx @@ -109,13 +109,18 @@ describe('TopNavMenu', () => { let dom: ReactWrapper; const refresh = () => { - new Promise(async (resolve) => { - if (dom) { - act(() => { - dom.update(); - }); + new Promise(async (resolve, reject) => { + try { + if (dom) { + act(() => { + dom.update(); + }); + } + + setImmediate(() => resolve(dom)); // flushes any pending promises + } catch (error) { + reject(error); } - setImmediate(() => resolve(dom)); // flushes any pending promises }); }; diff --git a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts index b96c81819c39..95bd41745553 100644 --- a/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts +++ b/src/plugins/saved_objects_management/public/lib/resolve_saved_objects.ts @@ -17,7 +17,7 @@ import { IndexPatternSpec, } from '../../../data/public'; import { FailedImport } from './process_import_response'; -import { DuplicateIndexPatternError, IndexPattern } from '../../../data/public'; +import { DuplicateDataViewError, IndexPattern } from '../../../data/public'; type SavedObjectsRawDoc = Record; @@ -89,7 +89,7 @@ async function importIndexPattern( try { emptyPattern = await indexPatterns.createAndSave(indexPatternSpec, overwriteAll, true); } catch (err) { - if (err instanceof DuplicateIndexPatternError) { + if (err instanceof DuplicateDataViewError) { // We can override and we want to prompt for confirmation const isConfirmed = await openConfirm( i18n.translate('savedObjectsManagement.indexPattern.confirmOverwriteLabel', { diff --git a/src/plugins/share/common/url_service/locators/README.md b/src/plugins/share/common/url_service/locators/README.md index 1ca990f92d7b..c3e0721e63f1 100644 --- a/src/plugins/share/common/url_service/locators/README.md +++ b/src/plugins/share/common/url_service/locators/README.md @@ -65,7 +65,7 @@ plugins.share.url.locators.create({ }; }, - migrations = { + migrations: { '7.20.0': ({productId, ...rest}) => { return { id: productId, @@ -76,20 +76,20 @@ plugins.share.url.locators.create({ }); ``` -The migration version should correspond to Kibana relase when the chagne was +The migration version should correspond to Kibana relase when the change was introduced. It is the responsibility of the *consumer* to make sure they migrate their stored parameters using the provided migration function to the latest version. Migrations versions are ordered by semver. As a consumer, -if persist somewhere a locator parameters object, you also want to store +if you persist somewhere a locator parameters object, you also want to store the version of that object, so later you know from starting from which version you need to execute migrations. ## Consumer Usage -Consumers of the Locators service can use the locators to generate deeps links -into Kibana apps, or navigate to the apps while passing to the destination -app the *location state*. +Consumers of the Locators service can use the locators to generate deep links +into Kibana apps, or navigate to the apps while passing the *location state* to +the destination app. First you will need to get hold of the *locator* for the app you want to navigate to. @@ -115,7 +115,7 @@ class MyPlugin { } ``` -Once you have the locator, you can use it to navigate to some kibana app: +Once you have the locator, you can use it to navigate to some Kibana app: ```ts await locator.navigate({ @@ -136,10 +136,10 @@ const url = await locator.getUrl({ **As a consumer, you should not persist the resulting URL string!** As soon as you do, you have lost your migration options. Instead you should -store the ID, version and params of your locator. This will let you +store the ID, version, and params of your locator. This will let you re-create the migrated URL later. -If, as a consumer, you store the ID, version and params of the locator, you +If, as a consumer, you store the ID, version, and params of the locator, you should use the migration functions provided by the locator when migrating between Kibana versions. diff --git a/src/plugins/timelion/kibana.json b/src/plugins/timelion/kibana.json index b6513c46f806..4d48462a1ed6 100644 --- a/src/plugins/timelion/kibana.json +++ b/src/plugins/timelion/kibana.json @@ -17,7 +17,7 @@ "kibanaLegacy" ], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" } } diff --git a/src/plugins/timelion/server/deprecations.ts b/src/plugins/timelion/server/deprecations.ts index 3c344e1d4a8d..2358dd313b74 100644 --- a/src/plugins/timelion/server/deprecations.ts +++ b/src/plugins/timelion/server/deprecations.ts @@ -49,6 +49,7 @@ export async function getDeprecations({ if (count > 0) { deprecations.push({ + title: 'Found Timelion worksheets', message: `You have ${count} Timelion worksheets. The Timelion app will be removed in 7.16. To continue using your Timelion worksheets, migrate them to a dashboard.`, documentationUrl: 'https://www.elastic.co/guide/en/kibana/current/create-panels-with-timelion.html', diff --git a/src/plugins/url_forwarding/kibana.json b/src/plugins/url_forwarding/kibana.json index 253466631f2e..a8b0571230b7 100644 --- a/src/plugins/url_forwarding/kibana.json +++ b/src/plugins/url_forwarding/kibana.json @@ -4,8 +4,8 @@ "server": false, "ui": true, "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "requiredPlugins": ["kibanaLegacy"] } diff --git a/src/plugins/vis_default_editor/kibana.json b/src/plugins/vis_default_editor/kibana.json index 93a5afed6a92..e85c5713eb82 100644 --- a/src/plugins/vis_default_editor/kibana.json +++ b/src/plugins/vis_default_editor/kibana.json @@ -5,8 +5,8 @@ "optionalPlugins": ["visualize"], "requiredBundles": ["kibanaUtils", "kibanaReact", "data", "fieldFormats"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "description": "The default editor used in most aggregation-based visualizations." } diff --git a/src/plugins/vis_type_metric/kibana.json b/src/plugins/vis_type_metric/kibana.json index 2b4e1917ccad..950abea1c5a9 100644 --- a/src/plugins/vis_type_metric/kibana.json +++ b/src/plugins/vis_type_metric/kibana.json @@ -7,8 +7,8 @@ "requiredPlugins": ["data", "visualizations", "charts", "expressions"], "requiredBundles": ["kibanaUtils", "visDefaultEditor"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "description": "Registers the Metric aggregation-based visualization." } diff --git a/src/plugins/vis_type_table/kibana.json b/src/plugins/vis_type_table/kibana.json index de77941d93f3..389094802e99 100644 --- a/src/plugins/vis_type_table/kibana.json +++ b/src/plugins/vis_type_table/kibana.json @@ -17,8 +17,8 @@ ], "optionalPlugins": ["usageCollection"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "description": "Registers the datatable aggregation-based visualization. Currently it contains two implementations, the one based on EUI datagrid and the angular one. The second one is going to be removed in future minors." } diff --git a/src/plugins/vis_type_table/public/legacy/vis_controller.ts b/src/plugins/vis_type_table/public/legacy/vis_controller.ts index ec198aa96f1f..a9cb22a05691 100644 --- a/src/plugins/vis_type_table/public/legacy/vis_controller.ts +++ b/src/plugins/vis_type_table/public/legacy/vis_controller.ts @@ -71,35 +71,42 @@ export function getTableVisualizationControllerClass( await this.initLocalAngular(); return new Promise(async (resolve, reject) => { - if (!this.$rootScope) { - const $injector = this.getInjector(); - this.$rootScope = $injector.get('$rootScope'); - this.$compile = $injector.get('$compile'); - } - const updateScope = () => { - if (!this.$scope) { - return; + try { + if (!this.$rootScope) { + const $injector = this.getInjector(); + this.$rootScope = $injector.get('$rootScope'); + this.$compile = $injector.get('$compile'); } - this.$scope.visState = { params: visParams, title: visParams.title }; - this.$scope.esResponse = esResponse; - - this.$scope.visParams = visParams; - this.$scope.renderComplete = resolve; - this.$scope.renderFailed = reject; - this.$scope.resize = Date.now(); - this.$scope.$apply(); - }; - - if (!this.$scope && this.$compile) { - this.$scope = this.$rootScope.$new(); - this.$scope.uiState = handlers.uiState; - this.$scope.filter = handlers.event; - updateScope(); - this.el.find('div').append(this.$compile(tableVisTemplate)(this.$scope)); - this.$scope.$apply(); - } else { - updateScope(); + const updateScope = () => { + if (!this.$scope) { + return; + } + + this.$scope.visState = { + params: visParams, + title: visParams.title, + }; + this.$scope.esResponse = esResponse; + this.$scope.visParams = visParams; + this.$scope.renderComplete = resolve; + this.$scope.renderFailed = reject; + this.$scope.resize = Date.now(); + this.$scope.$apply(); + }; + + if (!this.$scope && this.$compile) { + this.$scope = this.$rootScope.$new(); + this.$scope.uiState = handlers.uiState; + this.$scope.filter = handlers.event; + updateScope(); + this.el.find('div').append(this.$compile(tableVisTemplate)(this.$scope)); + this.$scope.$apply(); + } else { + updateScope(); + } + } catch (error) { + reject(error); } }); } diff --git a/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts b/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts index 3f8f4289321b..76f067e3a23d 100644 --- a/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts +++ b/src/plugins/vis_type_table/server/usage_collector/get_stats.test.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ -import { SavedObjectsClientContract } from 'kibana/server'; import { getStats } from './get_stats'; +import type { SavedObjectsClientContract } from '../../../../core/server'; const mockVisualizations = { saved_objects: [ @@ -42,15 +42,23 @@ const mockVisualizations = { describe('vis_type_table getStats', () => { const mockSoClient = ({ - find: jest.fn().mockResolvedValue(mockVisualizations), + createPointInTimeFinder: jest.fn().mockResolvedValue({ + close: jest.fn(), + find: function* asyncGenerator() { + yield mockVisualizations; + }, + }), } as unknown) as SavedObjectsClientContract; test('Returns stats from saved objects for table vis only', async () => { const result = await getStats(mockSoClient); - expect(mockSoClient.find).toHaveBeenCalledWith({ + + expect(mockSoClient.createPointInTimeFinder).toHaveBeenCalledWith({ type: 'visualization', - perPage: 10000, + perPage: 1000, + namespaces: ['*'], }); + expect(result).toEqual({ total: 4, total_split: 3, diff --git a/src/plugins/vis_type_table/server/usage_collector/get_stats.ts b/src/plugins/vis_type_table/server/usage_collector/get_stats.ts index 6fdb555c8632..ef948c2d7b70 100644 --- a/src/plugins/vis_type_table/server/usage_collector/get_stats.ts +++ b/src/plugins/vis_type_table/server/usage_collector/get_stats.ts @@ -6,12 +6,14 @@ * Side Public License, v 1. */ -import { ISavedObjectsRepository, SavedObjectsClientContract } from 'kibana/server'; -import { - SavedVisState, - VisualizationSavedObjectAttributes, -} from 'src/plugins/visualizations/common'; -import { TableVisParams, VIS_TYPE_TABLE } from '../../common'; +import { VIS_TYPE_TABLE } from '../../common'; + +import type { + ISavedObjectsRepository, + SavedObjectsClientContract, + SavedObjectsFindResult, +} from '../../../../core/server'; +import type { SavedVisState } from '../../../visualizations/common'; export interface VisTypeTableUsage { /** @@ -44,17 +46,14 @@ export interface VisTypeTableUsage { export async function getStats( soClient: SavedObjectsClientContract | ISavedObjectsRepository ): Promise { - const visualizations = await soClient.find({ + const finder = await soClient.createPointInTimeFinder({ type: 'visualization', - perPage: 10000, + perPage: 1000, + namespaces: ['*'], }); - const tableVisualizations = visualizations.saved_objects - .map>(({ attributes }) => JSON.parse(attributes.visState)) - .filter(({ type }) => type === VIS_TYPE_TABLE); - - const defaultStats = { - total: tableVisualizations.length, + const stats: VisTypeTableUsage = { + total: 0, total_split: 0, split_columns: { total: 0, @@ -66,20 +65,39 @@ export async function getStats( }, }; - return tableVisualizations.reduce((acc, { aggs, params }) => { + const doTelemetry = ({ aggs, params }: SavedVisState) => { + stats.total += 1; + const hasSplitAgg = aggs.find((agg) => agg.schema === 'split'); if (hasSplitAgg) { - acc.total_split += 1; + stats.total_split += 1; const isSplitRow = params.row; const isSplitEnabled = hasSplitAgg.enabled; + const container = isSplitRow ? stats.split_rows : stats.split_columns; - const container = isSplitRow ? acc.split_rows : acc.split_columns; container.total += 1; container.enabled = isSplitEnabled ? container.enabled + 1 : container.enabled; } + }; + + for await (const response of finder.find()) { + (response.saved_objects || []).forEach(({ attributes }: SavedObjectsFindResult) => { + if (attributes?.visState) { + try { + const visState: SavedVisState = JSON.parse(attributes.visState); + + if (visState.type === VIS_TYPE_TABLE) { + doTelemetry(visState); + } + } catch { + // nothing to be here, "so" not valid + } + } + }); + } + await finder.close(); - return acc; - }, defaultStats); + return stats; } diff --git a/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts index e045788897b6..d32435ac4540 100644 --- a/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts +++ b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.test.ts @@ -6,20 +6,19 @@ * Side Public License, v 1. */ -jest.mock('./get_stats', () => ({ - getStats: jest.fn().mockResolvedValue({ somestat: 1 }), -})); - import { createUsageCollectionSetupMock, createCollectorFetchContextMock, -} from 'src/plugins/usage_collection/server/mocks'; - +} from '../../../usage_collection/server/mocks'; import { registerVisTypeTableUsageCollector } from './register_usage_collector'; import { getStats } from './get_stats'; +jest.mock('./get_stats', () => ({ + getStats: jest.fn().mockResolvedValue({ somestat: 1 }), +})); + describe('registerVisTypeTableUsageCollector', () => { - it('Usage collector configs fit the shape', () => { + test('Usage collector configs fit the shape', () => { const mockCollectorSet = createUsageCollectionSetupMock(); registerVisTypeTableUsageCollector(mockCollectorSet); expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1); @@ -45,7 +44,7 @@ describe('registerVisTypeTableUsageCollector', () => { expect(usageCollectorConfig.isReady()).toBe(true); }); - it('Usage collector config.fetch calls getStats', async () => { + test('Usage collector config.fetch calls getStats', async () => { const mockCollectorSet = createUsageCollectionSetupMock(); registerVisTypeTableUsageCollector(mockCollectorSet); const usageCollector = mockCollectorSet.makeUsageCollector.mock.results[0].value; diff --git a/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts index d3d3204e0841..74044c9ae70c 100644 --- a/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts +++ b/src/plugins/vis_type_table/server/usage_collector/register_usage_collector.ts @@ -6,9 +6,8 @@ * Side Public License, v 1. */ -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; - import { getStats, VisTypeTableUsage } from './get_stats'; +import type { UsageCollectionSetup } from '../../../usage_collection/server'; export function registerVisTypeTableUsageCollector(collectorSet: UsageCollectionSetup) { const collector = collectorSet.makeUsageCollector({ diff --git a/src/plugins/vis_type_tagcloud/kibana.json b/src/plugins/vis_type_tagcloud/kibana.json index b51d5d49cb7b..67d57dec9f1f 100644 --- a/src/plugins/vis_type_tagcloud/kibana.json +++ b/src/plugins/vis_type_tagcloud/kibana.json @@ -6,8 +6,8 @@ "requiredPlugins": ["data", "expressions", "visualizations", "charts"], "requiredBundles": ["kibanaReact", "visDefaultEditor"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "description": "Registers the tagcloud visualization. It is based on elastic-charts wordcloud." diff --git a/src/plugins/vis_type_timelion/kibana.json b/src/plugins/vis_type_timelion/kibana.json index bf537f4ffbc7..7f40f965e754 100644 --- a/src/plugins/vis_type_timelion/kibana.json +++ b/src/plugins/vis_type_timelion/kibana.json @@ -7,8 +7,8 @@ "requiredPlugins": ["visualizations", "data", "expressions", "charts"], "requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "description": "Registers the timelion visualization. Also contains the backend for both timelion app and timelion visualization." } diff --git a/src/plugins/vis_type_timeseries/kibana.json b/src/plugins/vis_type_timeseries/kibana.json index c172640a4c14..5cc425e4edf7 100644 --- a/src/plugins/vis_type_timeseries/kibana.json +++ b/src/plugins/vis_type_timeseries/kibana.json @@ -8,8 +8,8 @@ "optionalPlugins": ["usageCollection"], "requiredBundles": ["kibanaUtils", "kibanaReact", "fieldFormats"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "description": "Registers the TSVB visualization. TSVB has its one editor, works with index patterns and index strings and contains 6 types of charts: timeseries, topN, table. markdown, metric and gauge." } diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx index 6ba213a47cd4..17af812ae5ce 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/agg.tsx @@ -13,14 +13,13 @@ import { aggToComponent } from '../lib/agg_to_component'; import { isMetricEnabled } from '../../lib/check_ui_restrictions'; import { UnsupportedAgg } from './unsupported_agg'; import { TemporaryUnsupportedAgg } from './temporary_unsupported_agg'; -import type { Metric, Panel, Series } from '../../../../common/types'; +import type { Metric, Panel, Series, SanitizedFieldType } from '../../../../common/types'; import { DragHandleProps } from '../../../types'; import { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions'; -import { IFieldType } from '../../../../../data/common/index_patterns/fields'; interface AggProps extends HTMLAttributes { disableDelete: boolean; - fields: IFieldType[]; + fields: Record; model: Metric; panel: Panel; series: Series; diff --git a/src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.tsx b/src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.tsx index 192cdd4985c6..0edd8b9c3feb 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/aggs/aggs.tsx @@ -15,9 +15,8 @@ import { Agg } from './agg'; import { seriesChangeHandler } from '../lib/series_change_handler'; import { handleAdd, handleDelete } from '../lib/collection_actions'; import { newMetricAggFn } from '../lib/new_metric_agg_fn'; -import type { Panel, Series } from '../../../../common/types'; +import type { Panel, Series, SanitizedFieldType } from '../../../../common/types'; import type { TimeseriesUIRestrictions } from '../../../../common/ui_restrictions'; -import { IFieldType } from '../../../../../data/common/index_patterns/fields'; const DROPPABLE_ID = 'aggs_dnd'; @@ -25,7 +24,7 @@ export interface AggsProps { name: keyof Series; panel: Panel; model: Series; - fields: IFieldType[]; + fields: Record; uiRestrictions: TimeseriesUIRestrictions; onChange(): void; } diff --git a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx index 3633f8add745..d9d5401fd131 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx +++ b/src/plugins/vis_type_timeseries/public/application/components/panel_config/table.tsx @@ -42,6 +42,7 @@ import { BUCKET_TYPES } from '../../../../common/enums'; import { PanelConfigProps, PANEL_CONFIG_TABS } from './types'; import { TimeseriesVisParams } from '../../../types'; import { getIndexPatternKey } from '../../../../common/index_patterns_utils'; +import { KBN_FIELD_TYPES } from '../../../../../data/public'; export class TablePanelConfig extends Component< PanelConfigProps, @@ -115,6 +116,13 @@ export class TablePanelConfig extends Component< defaultMessage="Group by field" /> } + restrict={[ + KBN_FIELD_TYPES.NUMBER, + KBN_FIELD_TYPES.BOOLEAN, + KBN_FIELD_TYPES.DATE, + KBN_FIELD_TYPES.IP, + KBN_FIELD_TYPES.STRING, + ]} fields={this.props.fields} value={model.pivot_id} indexPattern={model.index_pattern} diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap index ce381a0e539d..524e35f9d29e 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/__snapshots__/terms.test.js.snap @@ -54,6 +54,15 @@ exports[`src/legacy/core_plugins/metrics/public/components/splits/terms.test.js /> } onChange={[Function]} + restrict={ + Array [ + "number", + "boolean", + "date", + "ip", + "string", + ] + } type="terms" value="OriginCityName" /> diff --git a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js index 9c097de38d56..a668e5b727b4 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js +++ b/src/plugins/vis_type_timeseries/public/application/components/splits/terms.js @@ -120,6 +120,13 @@ export const SplitByTermsUI = ({ description="This labels a field selector allowing the user to chose 'by' which field to group." /> } + restrict={[ + KBN_FIELD_TYPES.NUMBER, + KBN_FIELD_TYPES.BOOLEAN, + KBN_FIELD_TYPES.DATE, + KBN_FIELD_TYPES.IP, + KBN_FIELD_TYPES.STRING, + ]} data-test-subj="groupByField" indexPattern={indexPattern} onChange={handleSelectChange('terms_field')} diff --git a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js index e55c5d708e48..945a7ac986d3 100644 --- a/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js +++ b/src/plugins/vis_type_timeseries/public/application/components/vis_with_splits.js @@ -40,7 +40,8 @@ export function visWithSplits(WrappedComponent) { [model, palettesService, syncColors, visData] ); - if (!model || !visData || !visData[model.id]) return ; + if (!model || !visData || !visData[model.id] || visData[model.id].series.length === 1) + return ; if (visData[model.id].series.every((s) => s.id.split(':').length === 1)) { return ; } diff --git a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts index 7add5cb4a455..767d5c00d7a3 100644 --- a/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts +++ b/src/plugins/vis_type_timeseries/server/lib/search_strategies/strategies/abstract_search_strategy.test.ts @@ -10,7 +10,7 @@ import { IndexPatternsService } from '../../../../../data/common'; import { from } from 'rxjs'; import { AbstractSearchStrategy } from './abstract_search_strategy'; -import type { IFieldType } from '../../../../../data/common'; +import type { FieldSpec } from '../../../../../data/common'; import type { CachedIndexPatternFetcher } from '../lib/cached_index_pattern_fetcher'; import type { VisTypeTimeseriesRequestHandlerContext, @@ -21,7 +21,7 @@ class FooSearchStrategy extends AbstractSearchStrategy {} describe('AbstractSearchStrategy', () => { let abstractSearchStrategy: AbstractSearchStrategy; - let mockedFields: IFieldType[]; + let mockedFields: FieldSpec[]; let requestContext: VisTypeTimeseriesRequestHandlerContext; beforeEach(() => { diff --git a/src/plugins/vis_type_timeseries/server/plugin.ts b/src/plugins/vis_type_timeseries/server/plugin.ts index 68c1db3c0358..58cd58c812e4 100644 --- a/src/plugins/vis_type_timeseries/server/plugin.ts +++ b/src/plugins/vis_type_timeseries/server/plugin.ts @@ -120,7 +120,7 @@ export class VisTypeTimeseriesPlugin implements Plugin { fieldsRoutes(router, framework); if (plugins.usageCollection) { - registerTimeseriesUsageCollector(plugins.usageCollection, globalConfig$); + registerTimeseriesUsageCollector(plugins.usageCollection); } return { diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.test.ts b/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.test.ts index 81ac2be0e7da..6bb5f3bbfcad 100644 --- a/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.test.ts +++ b/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.test.ts @@ -7,15 +7,14 @@ */ import { getStats } from './get_usage_collector'; -import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; +import { createCollectorFetchContextMock } from '../../../usage_collection/server/mocks'; +import type { SavedObjectsClientContract, SavedObjectsFindResponse } from '../../../../core/server'; import { TIME_RANGE_DATA_MODES } from '../../common/enums'; -const mockedSavedObjects = [ - { - _id: 'visualization:timeseries-123', - _source: { - type: 'visualization', - visualization: { +const mockedSavedObject = { + saved_objects: [ + { + attributes: { visState: JSON.stringify({ type: 'metrics', title: 'TSVB visualization 1', @@ -25,12 +24,8 @@ const mockedSavedObjects = [ }), }, }, - }, - { - _id: 'visualization:timeseries-321', - _source: { - type: 'visualization', - visualization: { + { + attributes: { visState: JSON.stringify({ type: 'metrics', title: 'TSVB visualization 2', @@ -40,12 +35,8 @@ const mockedSavedObjects = [ }), }, }, - }, - { - _id: 'visualization:timeseries-456', - _source: { - type: 'visualization', - visualization: { + { + attributes: { visState: JSON.stringify({ type: 'metrics', title: 'TSVB visualization 3', @@ -55,8 +46,8 @@ const mockedSavedObjects = [ }), }, }, - }, -]; + ], +} as SavedObjectsFindResponse; const mockedSavedObjectsByValue = [ { @@ -91,42 +82,43 @@ const mockedSavedObjectsByValue = [ }, ]; -const getMockCollectorFetchContext = (hits?: unknown[], savedObjectsByValue: unknown[] = []) => { +const getMockCollectorFetchContext = ( + savedObjects: SavedObjectsFindResponse, + savedObjectsByValue: unknown[] = [] +) => { const fetchParamsMock = createCollectorFetchContextMock(); - fetchParamsMock.esClient.search = jest.fn().mockResolvedValue({ body: { hits: { hits } } }); - fetchParamsMock.soClient.find = jest.fn().mockResolvedValue({ - saved_objects: savedObjectsByValue, - }); + fetchParamsMock.soClient = ({ + find: jest.fn().mockResolvedValue({ + saved_objects: savedObjectsByValue, + }), + createPointInTimeFinder: jest.fn().mockResolvedValue({ + close: jest.fn(), + find: function* asyncGenerator() { + yield savedObjects; + }, + }), + } as unknown) as SavedObjectsClientContract; return fetchParamsMock; }; describe('Timeseries visualization usage collector', () => { - const mockIndex = 'mock_index'; - test('Returns undefined when no results found (undefined)', async () => { - const mockCollectorFetchContext = getMockCollectorFetchContext([], []); - const result = await getStats( - mockCollectorFetchContext.esClient, - mockCollectorFetchContext.soClient, - mockIndex + const mockCollectorFetchContext = getMockCollectorFetchContext( + ({ saved_objects: [] } as unknown) as SavedObjectsFindResponse, + [] ); + const result = await getStats(mockCollectorFetchContext.soClient); expect(result).toBeUndefined(); }); test('Returns undefined when no timeseries saved objects found', async () => { - const mockCollectorFetchContext = getMockCollectorFetchContext( - [ + const mockCollectorFetchContext = getMockCollectorFetchContext({ + saved_objects: [ { - _id: 'visualization:myvis-123', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "area"}' }, - }, + attributes: { visState: '{"type": "area"}' }, }, - ], - [ { attributes: { panelsJSON: JSON.stringify({ @@ -139,27 +131,20 @@ describe('Timeseries visualization usage collector', () => { }), }, }, - ] - ); - const result = await getStats( - mockCollectorFetchContext.esClient, - mockCollectorFetchContext.soClient, - mockIndex - ); + ], + } as SavedObjectsFindResponse); + + const result = await getStats(mockCollectorFetchContext.soClient); expect(result).toBeUndefined(); }); test('Summarizes visualizations response data', async () => { const mockCollectorFetchContext = getMockCollectorFetchContext( - mockedSavedObjects, + mockedSavedObject, mockedSavedObjectsByValue ); - const result = await getStats( - mockCollectorFetchContext.esClient, - mockCollectorFetchContext.soClient, - mockIndex - ); + const result = await getStats(mockCollectorFetchContext.soClient); expect(result).toMatchObject({ timeseries_use_last_value_mode_total: 3, diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.ts b/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.ts index 5146b52a8e50..b5b22e06b51c 100644 --- a/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.ts +++ b/src/plugins/vis_type_timeseries/server/usage_collector/get_usage_collector.ts @@ -6,49 +6,65 @@ * Side Public License, v 1. */ -import { ElasticsearchClient } from 'src/core/server'; -import { SavedObjectsClientContract, ISavedObjectsRepository } from 'kibana/server'; import { TIME_RANGE_DATA_MODES } from '../../common/enums'; import { findByValueEmbeddables } from '../../../dashboard/server'; +import type { + SavedObjectsClientContract, + ISavedObjectsRepository, + SavedObjectsFindResult, +} from '../../../../core/server'; +import type { SavedVisState } from '../../../visualizations/common'; + export interface TimeseriesUsage { timeseries_use_last_value_mode_total: number; } -interface VisState { - type?: string; - params?: any; -} +const doTelemetryFoVisualizations = async ( + soClient: SavedObjectsClientContract | ISavedObjectsRepository, + telemetryUseLastValueMode: (savedVis: SavedVisState) => void +) => { + const finder = await soClient.createPointInTimeFinder({ + type: 'visualization', + perPage: 1000, + namespaces: ['*'], + }); -export const getStats = async ( - esClient: ElasticsearchClient, + for await (const response of finder.find()) { + (response.saved_objects || []).forEach(({ attributes }: SavedObjectsFindResult) => { + if (attributes?.visState) { + try { + const visState: SavedVisState = JSON.parse(attributes.visState); + + telemetryUseLastValueMode(visState); + } catch { + // nothing to be here, "so" not valid + } + } + }); + } + await finder.close(); +}; + +const doTelemetryForByValueVisualizations = async ( soClient: SavedObjectsClientContract | ISavedObjectsRepository, - index: string + telemetryUseLastValueMode: (savedVis: SavedVisState) => void +) => { + const byValueVisualizations = await findByValueEmbeddables(soClient, 'visualization'); + + for (const item of byValueVisualizations) { + telemetryUseLastValueMode((item.savedVis as unknown) as SavedVisState); + } +}; + +export const getStats = async ( + soClient: SavedObjectsClientContract | ISavedObjectsRepository ): Promise => { const timeseriesUsage = { timeseries_use_last_value_mode_total: 0, }; - const searchParams = { - size: 10000, - index, - ignoreUnavailable: true, - filterPath: ['hits.hits._id', 'hits.hits._source.visualization'], - body: { - query: { - bool: { - filter: { term: { type: 'visualization' } }, - }, - }, - }, - }; - - const { body: esResponse } = await esClient.search<{ - visualization: { visState: string }; - updated_at: string; - }>(searchParams); - - function telemetryUseLastValueMode(visState: VisState) { + function telemetryUseLastValueMode(visState: SavedVisState) { if ( visState.type === 'metrics' && visState.params.type !== 'timeseries' && @@ -59,28 +75,10 @@ export const getStats = async ( } } - if (esResponse?.hits?.hits?.length) { - for (const hit of esResponse.hits.hits) { - if (hit._source && 'visualization' in hit._source) { - const { visualization } = hit._source!; - - let visState: VisState = {}; - try { - visState = JSON.parse(visualization?.visState ?? '{}'); - } catch (e) { - // invalid visState - } - - telemetryUseLastValueMode(visState); - } - } - } - - const byValueVisualizations = await findByValueEmbeddables(soClient, 'visualization'); - - for (const item of byValueVisualizations) { - telemetryUseLastValueMode(item.savedVis as VisState); - } + await Promise.all([ + doTelemetryFoVisualizations(soClient, telemetryUseLastValueMode), + doTelemetryForByValueVisualizations(soClient, telemetryUseLastValueMode), + ]); return timeseriesUsage.timeseries_use_last_value_mode_total ? timeseriesUsage : undefined; }; diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.test.ts b/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.test.ts index 726ad972ab8d..0dfe5ae5f935 100644 --- a/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.test.ts +++ b/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.test.ts @@ -6,27 +6,22 @@ * Side Public License, v 1. */ -import { of } from 'rxjs'; import { mockStats, mockGetStats } from './get_usage_collector.mock'; -import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/mocks'; -import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; +import { createUsageCollectionSetupMock } from '../../../usage_collection/server/mocks'; +import { createCollectorFetchContextMock } from '../../../usage_collection/server/mocks'; import { registerTimeseriesUsageCollector } from './register_timeseries_collector'; -import { ConfigObservable } from '../types'; describe('registerTimeseriesUsageCollector', () => { - const mockIndex = 'mock_index'; - const mockConfig = of({ kibana: { index: mockIndex } }) as ConfigObservable; - it('makes a usage collector and registers it`', () => { const mockCollectorSet = createUsageCollectionSetupMock(); - registerTimeseriesUsageCollector(mockCollectorSet, mockConfig); + registerTimeseriesUsageCollector(mockCollectorSet); expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1); expect(mockCollectorSet.registerCollector).toBeCalledTimes(1); }); it('makeUsageCollector configs fit the shape', () => { const mockCollectorSet = createUsageCollectionSetupMock(); - registerTimeseriesUsageCollector(mockCollectorSet, mockConfig); + registerTimeseriesUsageCollector(mockCollectorSet); expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({ type: 'vis_type_timeseries', isReady: expect.any(Function), @@ -39,23 +34,19 @@ describe('registerTimeseriesUsageCollector', () => { it('makeUsageCollector config.isReady returns true', () => { const mockCollectorSet = createUsageCollectionSetupMock(); - registerTimeseriesUsageCollector(mockCollectorSet, mockConfig); + registerTimeseriesUsageCollector(mockCollectorSet); const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; expect(usageCollectorConfig.isReady()).toBe(true); }); it('makeUsageCollector config.fetch calls getStats', async () => { const mockCollectorSet = createUsageCollectionSetupMock(); - registerTimeseriesUsageCollector(mockCollectorSet, mockConfig); + registerTimeseriesUsageCollector(mockCollectorSet); const usageCollector = mockCollectorSet.makeUsageCollector.mock.results[0].value; const mockedCollectorFetchContext = createCollectorFetchContextMock(); const fetchResult = await usageCollector.fetch(mockedCollectorFetchContext); expect(mockGetStats).toBeCalledTimes(1); - expect(mockGetStats).toBeCalledWith( - mockedCollectorFetchContext.esClient, - mockedCollectorFetchContext.soClient, - mockIndex - ); + expect(mockGetStats).toBeCalledWith(mockedCollectorFetchContext.soClient); expect(fetchResult).toBe(mockStats); }); }); diff --git a/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.ts b/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.ts index 5edeb6654020..7e9294f03ba1 100644 --- a/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.ts +++ b/src/plugins/vis_type_timeseries/server/usage_collector/register_timeseries_collector.ts @@ -6,15 +6,10 @@ * Side Public License, v 1. */ -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { first } from 'rxjs/operators'; import { getStats, TimeseriesUsage } from './get_usage_collector'; -import { ConfigObservable } from '../types'; +import type { UsageCollectionSetup } from '../../../usage_collection/server'; -export function registerTimeseriesUsageCollector( - collectorSet: UsageCollectionSetup, - config: ConfigObservable -) { +export function registerTimeseriesUsageCollector(collectorSet: UsageCollectionSetup) { const collector = collectorSet.makeUsageCollector({ type: 'vis_type_timeseries', isReady: () => true, @@ -24,11 +19,7 @@ export function registerTimeseriesUsageCollector( _meta: { description: 'Number of TSVB visualizations using "last value" as a time range' }, }, }, - fetch: async ({ esClient, soClient }) => { - const { index } = (await config.pipe(first()).toPromise()).kibana; - - return await getStats(esClient, soClient, index); - }, + fetch: async ({ soClient }) => await getStats(soClient), }); collectorSet.registerCollector(collector); diff --git a/src/plugins/vis_type_vega/kibana.json b/src/plugins/vis_type_vega/kibana.json index a579e85c0caf..1a499e284c1a 100644 --- a/src/plugins/vis_type_vega/kibana.json +++ b/src/plugins/vis_type_vega/kibana.json @@ -7,8 +7,8 @@ "optionalPlugins": ["home","usageCollection"], "requiredBundles": ["kibanaUtils", "kibanaReact", "visDefaultEditor", "esUiShared"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "description": "Registers the vega visualization. Is the elastic version of vega and vega-lite libraries." } diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts index ce815cba4a4e..82aba087dedc 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.test.ts @@ -7,67 +7,63 @@ */ import { getStats } from './get_usage_collector'; -import { HomeServerPluginSetup } from '../../../home/server'; -import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; +import { createCollectorFetchContextMock } from '../../../usage_collection/server/mocks'; +import type { HomeServerPluginSetup } from '../../../home/server'; +import type { SavedObjectsClientContract } from '../../../../core/server'; const mockedSavedObjects = [ // vega-lite lib spec { - _id: 'visualization:vega-1', - _source: { - type: 'visualization', - visualization: { - visState: JSON.stringify({ - type: 'vega', - params: { - spec: '{"$schema": "https://vega.github.io/schema/vega-lite/v5.json" }', - }, - }), - }, + attributes: { + visState: JSON.stringify({ + type: 'vega', + params: { + spec: '{"$schema": "https://vega.github.io/schema/vega-lite/v5.json" }', + }, + }), }, }, // vega lib spec { - _id: 'visualization:vega-2', - _source: { - type: 'visualization', - visualization: { - visState: JSON.stringify({ - type: 'vega', - params: { - spec: '{"$schema": "https://vega.github.io/schema/vega/v5.json" }', - }, - }), - }, + attributes: { + visState: JSON.stringify({ + type: 'vega', + params: { + spec: '{"$schema": "https://vega.github.io/schema/vega/v5.json" }', + }, + }), }, }, // map layout { - _id: 'visualization:vega-3', - _source: { - type: 'visualization', - visualization: { - visState: JSON.stringify({ - type: 'vega', - params: { - spec: - '{"$schema": "https://vega.github.io/schema/vega/v3.json" \n "config": { "kibana" : { "type": "map" }} }', - }, - }), - }, + attributes: { + visState: JSON.stringify({ + type: 'vega', + params: { + spec: + '{"$schema": "https://vega.github.io/schema/vega/v3.json" \n "config": { "kibana" : { "type": "map" }} }', + }, + }), }, }, ]; -const getMockCollectorFetchContext = (hits?: unknown[]) => { +const getMockCollectorFetchContext = (savedObjects?: unknown[]) => { const fetchParamsMock = createCollectorFetchContextMock(); - fetchParamsMock.esClient.search = jest.fn().mockResolvedValue({ body: { hits: { hits } } }); + fetchParamsMock.soClient = ({ + createPointInTimeFinder: jest.fn().mockResolvedValue({ + close: jest.fn(), + find: function* asyncGenerator() { + yield { saved_objects: savedObjects }; + }, + }), + } as unknown) as SavedObjectsClientContract; + return fetchParamsMock; }; describe('Vega visualization usage collector', () => { - const mockIndex = 'mock_index'; const mockDeps = { home: ({ sampleData: { @@ -94,13 +90,13 @@ describe('Vega visualization usage collector', () => { }; test('Returns undefined when no results found (undefined)', async () => { - const result = await getStats(getMockCollectorFetchContext().esClient, mockIndex, mockDeps); + const result = await getStats(getMockCollectorFetchContext().soClient, mockDeps); expect(result).toBeUndefined(); }); test('Returns undefined when no results found (0 results)', async () => { - const result = await getStats(getMockCollectorFetchContext([]).esClient, mockIndex, mockDeps); + const result = await getStats(getMockCollectorFetchContext([]).soClient, mockDeps); expect(result).toBeUndefined(); }); @@ -115,7 +111,7 @@ describe('Vega visualization usage collector', () => { }, }, ]); - const result = await getStats(mockCollectorFetchContext.esClient, mockIndex, mockDeps); + const result = await getStats(mockCollectorFetchContext.soClient, mockDeps); expect(result).toBeUndefined(); }); @@ -123,30 +119,26 @@ describe('Vega visualization usage collector', () => { test('Should ingnore sample data visualizations', async () => { const mockCollectorFetchContext = getMockCollectorFetchContext([ { - _id: 'visualization:sampledata-123', - _source: { - type: 'visualization', - visualization: { - visState: JSON.stringify({ - type: 'vega', - title: 'sample vega visualization', - params: { - spec: '{"$schema": "https://vega.github.io/schema/vega/v5.json" }', - }, - }), - }, + attributes: { + visState: JSON.stringify({ + type: 'vega', + title: 'sample vega visualization', + params: { + spec: '{"$schema": "https://vega.github.io/schema/vega/v5.json" }', + }, + }), }, }, ]); - const result = await getStats(mockCollectorFetchContext.esClient, mockIndex, mockDeps); + const result = await getStats(mockCollectorFetchContext.soClient, mockDeps); expect(result).toBeUndefined(); }); test('Summarizes visualizations response data', async () => { const mockCollectorFetchContext = getMockCollectorFetchContext(mockedSavedObjects); - const result = await getStats(mockCollectorFetchContext.esClient, mockIndex, mockDeps); + const result = await getStats(mockCollectorFetchContext.soClient, mockDeps); expect(result).toMatchObject({ vega_lib_specs_total: 2, diff --git a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts index 310486bfdfff..ae99021745a0 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/get_usage_collector.ts @@ -7,14 +7,20 @@ */ import { parse } from 'hjson'; -import type { ElasticsearchClient } from 'src/core/server'; -import { VegaSavedObjectAttributes, VisTypeVegaPluginSetupDependencies } from '../types'; +import type { SavedObjectsClientContract, SavedObjectsFindResult } from '../../../../core/server'; +import type { SavedVisState } from '../../../visualizations/common'; +import type { VegaSavedObjectAttributes, VisTypeVegaPluginSetupDependencies } from '../types'; type UsageCollectorDependencies = Pick; - type VegaType = 'vega' | 'vega-lite'; +export interface VegaUsage { + vega_lib_specs_total: number; + vega_lite_lib_specs_total: number; + vega_use_map_total: number; +} + function isVegaType(attributes: any): attributes is VegaSavedObjectAttributes { return attributes && attributes.type === 'vega' && attributes.params?.spec; } @@ -45,15 +51,8 @@ const getDefaultVegaVisualizations = (home: UsageCollectorDependencies['home']) return titles; }; -export interface VegaUsage { - vega_lib_specs_total: number; - vega_lite_lib_specs_total: number; - vega_use_map_total: number; -} - export const getStats = async ( - esClient: ElasticsearchClient, - index: string, + soClient: SavedObjectsClientContract, { home }: UsageCollectorDependencies ): Promise => { let shouldPublishTelemetry = false; @@ -64,58 +63,54 @@ export const getStats = async ( vega_use_map_total: 0, }; - const searchParams = { - size: 10000, - index, - ignoreUnavailable: true, - filterPath: ['hits.hits._id', 'hits.hits._source.visualization'], - body: { - query: { - bool: { - filter: { term: { type: 'visualization' } }, - }, - }, - }, - }; - - const { body: esResponse } = await esClient.search<{ visualization: { visState: string } }>( - searchParams - ); - const size = esResponse?.hits?.hits?.length ?? 0; - - if (!size) { - return; - } - // we want to exclude the Vega Sample Data visualizations from the stats // in order to have more accurate results const excludedFromStatsVisualizations = getDefaultVegaVisualizations(home); - for (const hit of esResponse.hits.hits) { - const visualization = hit._source?.visualization; - const visState = JSON.parse(visualization?.visState ?? '{}'); - if (isVegaType(visState) && !excludedFromStatsVisualizations.includes(visState.title)) { - try { - const spec = parse(visState.params.spec, { legacyRoot: false }); + const finder = await soClient.createPointInTimeFinder({ + type: 'visualization', + perPage: 1000, + namespaces: ['*'], + }); - if (spec) { - shouldPublishTelemetry = true; + const doTelemetry = ({ params }: SavedVisState) => { + try { + const spec = parse(params.spec, { legacyRoot: false }); - if (checkVegaSchemaType(spec.$schema, 'vega')) { - vegaUsage.vega_lib_specs_total++; - } - if (checkVegaSchemaType(spec.$schema, 'vega-lite')) { - vegaUsage.vega_lite_lib_specs_total++; - } - if (spec.config?.kibana?.type === 'map') { - vegaUsage.vega_use_map_total++; - } + if (spec) { + shouldPublishTelemetry = true; + + if (checkVegaSchemaType(spec.$schema, 'vega')) { + vegaUsage.vega_lib_specs_total++; + } + if (checkVegaSchemaType(spec.$schema, 'vega-lite')) { + vegaUsage.vega_lite_lib_specs_total++; + } + if (spec.config?.kibana?.type === 'map') { + vegaUsage.vega_use_map_total++; } - } catch (e) { - // Let it go, the data is invalid and we'll don't need to handle it } + } catch (e) { + // Let it go, the data is invalid and we'll don't need to handle it } + }; + + for await (const response of finder.find()) { + (response.saved_objects || []).forEach(({ attributes }: SavedObjectsFindResult) => { + if (attributes?.visState) { + try { + const visState: SavedVisState = JSON.parse(attributes.visState); + + if (isVegaType(visState) && !excludedFromStatsVisualizations.includes(visState.title)) { + doTelemetry(visState); + } + } catch { + // nothing to be here, "so" not valid + } + } + }); } + await finder.close(); return shouldPublishTelemetry ? vegaUsage : undefined; }; diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts index 7933da3e675f..fc488540293a 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.test.ts @@ -6,27 +6,28 @@ * Side Public License, v 1. */ -import { of } from 'rxjs'; +import { + createUsageCollectionSetupMock, + createCollectorFetchContextMock, +} from '../../../usage_collection/server/mocks'; import { mockStats, mockGetStats } from './get_usage_collector.mock'; -import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/mocks'; -import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; -import { HomeServerPluginSetup } from '../../../home/server'; import { registerVegaUsageCollector } from './register_vega_collector'; -import { ConfigObservable } from '../types'; + +import type { HomeServerPluginSetup } from '../../../home/server'; +import type { ConfigObservable } from '../types'; describe('registerVegaUsageCollector', () => { - const mockIndex = 'mock_index'; const mockDeps = { home: ({} as unknown) as HomeServerPluginSetup }; - const mockConfig = of({ kibana: { index: mockIndex } }) as ConfigObservable; + const mockConfig = {} as ConfigObservable; - it('makes a usage collector and registers it`', () => { + test('makes a usage collector and registers it`', () => { const mockCollectorSet = createUsageCollectionSetupMock(); registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps); expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1); expect(mockCollectorSet.registerCollector).toBeCalledTimes(1); }); - it('makeUsageCollector configs fit the shape', () => { + test('makeUsageCollector configs fit the shape', () => { const mockCollectorSet = createUsageCollectionSetupMock(); registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps); expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({ @@ -39,21 +40,21 @@ describe('registerVegaUsageCollector', () => { expect(usageCollectorConfig.isReady()).toBe(true); }); - it('makeUsageCollector config.isReady returns true', () => { + test('makeUsageCollector config.isReady returns true', () => { const mockCollectorSet = createUsageCollectionSetupMock(); registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps); const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; expect(usageCollectorConfig.isReady()).toBe(true); }); - it('makeUsageCollector config.fetch calls getStats', async () => { + test('makeUsageCollector config.fetch calls getStats', async () => { const mockCollectorSet = createUsageCollectionSetupMock(); registerVegaUsageCollector(mockCollectorSet, mockConfig, mockDeps); const usageCollector = mockCollectorSet.makeUsageCollector.mock.results[0].value; const mockedCollectorFetchContext = createCollectorFetchContextMock(); const fetchResult = await usageCollector.fetch(mockedCollectorFetchContext); expect(mockGetStats).toBeCalledTimes(1); - expect(mockGetStats).toBeCalledWith(mockedCollectorFetchContext.esClient, mockIndex, mockDeps); + expect(mockGetStats).toBeCalledWith(mockedCollectorFetchContext.soClient, mockDeps); expect(fetchResult).toBe(mockStats); }); }); diff --git a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts index 45f1758c9045..ef65b58a8315 100644 --- a/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts +++ b/src/plugins/vis_type_vega/server/usage_collector/register_vega_collector.ts @@ -6,10 +6,9 @@ * Side Public License, v 1. */ -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { first } from 'rxjs/operators'; import { getStats, VegaUsage } from './get_usage_collector'; -import { ConfigObservable, VisTypeVegaPluginSetupDependencies } from '../types'; +import type { UsageCollectionSetup } from '../../../usage_collection/server'; +import type { ConfigObservable, VisTypeVegaPluginSetupDependencies } from '../types'; export function registerVegaUsageCollector( collectorSet: UsageCollectionSetup, @@ -24,11 +23,7 @@ export function registerVegaUsageCollector( vega_lite_lib_specs_total: { type: 'long' }, vega_use_map_total: { type: 'long' }, }, - fetch: async ({ esClient }) => { - const { index } = (await config.pipe(first()).toPromise()).kibana; - - return await getStats(esClient, index, dependencies); - }, + fetch: async ({ soClient }) => await getStats(soClient, dependencies), }); collectorSet.registerCollector(collector); diff --git a/src/plugins/vis_types/pie/kibana.json b/src/plugins/vis_types/pie/kibana.json index eebefc42681b..b3a5c1fc8aa0 100644 --- a/src/plugins/vis_types/pie/kibana.json +++ b/src/plugins/vis_types/pie/kibana.json @@ -7,9 +7,8 @@ "requiredBundles": ["visDefaultEditor"], "extraPublicDirs": ["common/index"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "description": "Contains the pie chart implementation using the elastic-charts library. The goal is to eventually deprecate the old implementation and keep only this. Until then, the library used is defined by the Legacy charts library advanced setting." } - \ No newline at end of file diff --git a/src/plugins/vis_types/vislib/kibana.json b/src/plugins/vis_types/vislib/kibana.json index 412264a3e727..39cc9bb53c01 100644 --- a/src/plugins/vis_types/vislib/kibana.json +++ b/src/plugins/vis_types/vislib/kibana.json @@ -6,8 +6,8 @@ "requiredPlugins": ["charts", "data", "expressions", "visualizations", "kibanaLegacy"], "requiredBundles": ["kibanaUtils", "visDefaultEditor", "visTypeXy", "visTypePie", "fieldFormats"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "description": "Contains the vislib visualizations. These are the classical area/line/bar, pie, gauge/goal and heatmap charts. We want to replace them with elastic-charts." } diff --git a/src/plugins/vis_types/vislib/public/vislib/components/legend/legend.tsx b/src/plugins/vis_types/vislib/public/vislib/components/legend/legend.tsx index 56f9025a6bd0..4701d07ab83e 100644 --- a/src/plugins/vis_types/vislib/public/vislib/components/legend/legend.tsx +++ b/src/plugins/vis_types/vislib/public/vislib/components/legend/legend.tsx @@ -124,16 +124,25 @@ export class VisLegend extends PureComponent { }; setFilterableLabels = (items: LegendItem[]): Promise => - new Promise(async (resolve) => { - const filterableLabels = new Set(); - items.forEach(async (item) => { - const canFilter = await this.canFilter(item); - if (canFilter) { - filterableLabels.add(item.label); - } - }); - - this.setState({ filterableLabels }, resolve); + new Promise(async (resolve, reject) => { + try { + const filterableLabels = new Set(); + items.forEach(async (item) => { + const canFilter = await this.canFilter(item); + + if (canFilter) { + filterableLabels.add(item.label); + } + }); + this.setState( + { + filterableLabels, + }, + resolve + ); + } catch (error) { + reject(error); + } }); setLabels = (data: any, type: string) => { diff --git a/src/plugins/vis_types/xy/kibana.json b/src/plugins/vis_types/xy/kibana.json index c25f035fb6d4..1606af5944ad 100644 --- a/src/plugins/vis_types/xy/kibana.json +++ b/src/plugins/vis_types/xy/kibana.json @@ -7,8 +7,8 @@ "requiredBundles": ["kibanaUtils", "visDefaultEditor"], "extraPublicDirs": ["common/index"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "description": "Contains the new xy-axis chart using the elastic-charts library, which will eventually replace the vislib xy-axis charts including bar, area, and line." } diff --git a/src/plugins/visualizations/common/constants.ts b/src/plugins/visualizations/common/constants.ts index a8a0963ac894..96f97b4b8193 100644 --- a/src/plugins/visualizations/common/constants.ts +++ b/src/plugins/visualizations/common/constants.ts @@ -7,3 +7,4 @@ */ export const VISUALIZE_ENABLE_LABS_SETTING = 'visualize:enableLabs'; +export const VISUALIZE_EMBEDDABLE_TYPE = 'visualization'; diff --git a/src/plugins/visualizations/kibana.json b/src/plugins/visualizations/kibana.json index a234f7ce80eb..0afbec24c7c3 100644 --- a/src/plugins/visualizations/kibana.json +++ b/src/plugins/visualizations/kibana.json @@ -15,8 +15,8 @@ "requiredBundles": ["kibanaUtils", "discover"], "extraPublicDirs": ["common/constants", "common/prepare_log_table", "common/expression_functions"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "description": "Contains the shared architecture among all the legacy visualizations, e.g. the visualization type registry or the visualization embeddable." } diff --git a/src/plugins/visualizations/public/embeddable/constants.ts b/src/plugins/visualizations/public/embeddable/constants.ts index 7bee811cddd6..cec3bd6cdfc8 100644 --- a/src/plugins/visualizations/public/embeddable/constants.ts +++ b/src/plugins/visualizations/public/embeddable/constants.ts @@ -6,4 +6,4 @@ * Side Public License, v 1. */ -export const VISUALIZE_EMBEDDABLE_TYPE = 'visualization'; +export { VISUALIZE_EMBEDDABLE_TYPE } from '../../common/constants'; diff --git a/src/plugins/visualizations/server/plugin.ts b/src/plugins/visualizations/server/plugin.ts index 5a5a80b2689d..6b87c0206347 100644 --- a/src/plugins/visualizations/server/plugin.ts +++ b/src/plugins/visualizations/server/plugin.ts @@ -8,33 +8,29 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; -import { Observable } from 'rxjs'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { + +import { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants'; +import { visualizationSavedObjectType } from './saved_objects'; +import { registerVisualizationsCollector } from './usage_collector'; + +import type { VisualizationsPluginSetup, VisualizationsPluginStart } from './types'; +import type { PluginInitializerContext, CoreSetup, CoreStart, Plugin, Logger, } from '../../../core/server'; - -import { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants'; - -import { visualizationSavedObjectType } from './saved_objects'; - -import { VisualizationsPluginSetup, VisualizationsPluginStart } from './types'; -import { registerVisualizationsCollector } from './usage_collector'; -import { EmbeddableSetup } from '../../embeddable/server'; +import type { UsageCollectionSetup } from '../../usage_collection/server'; +import type { EmbeddableSetup } from '../../embeddable/server'; import { visualizeEmbeddableFactory } from './embeddable/visualize_embeddable_factory'; export class VisualizationsPlugin implements Plugin { private readonly logger: Logger; - private readonly config: Observable<{ kibana: { index: string } }>; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); - this.config = initializerContext.config.legacy.globalConfig$; } public setup( @@ -61,7 +57,7 @@ export class VisualizationsPlugin }); if (plugins.usageCollection) { - registerVisualizationsCollector(plugins.usageCollection, this.config); + registerVisualizationsCollector(plugins.usageCollection); } plugins.embeddable.registerEmbeddableFactory(visualizeEmbeddableFactory()); diff --git a/src/plugins/visualizations/server/usage_collector/get_usage_collector.test.ts b/src/plugins/visualizations/server/usage_collector/get_usage_collector.test.ts index 325b52eaec36..24c290ca849e 100644 --- a/src/plugins/visualizations/server/usage_collector/get_usage_collector.test.ts +++ b/src/plugins/visualizations/server/usage_collector/get_usage_collector.test.ts @@ -7,126 +7,106 @@ */ import moment from 'moment'; -import { ElasticsearchClient } from 'src/core/server'; import { getStats } from './get_usage_collector'; +import type { SavedObjectsClientContract } from '../../../../core/server'; const defaultMockSavedObjects = [ { - _id: 'visualization:coolviz-123', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "shell_beads"}' }, - updated_at: moment().subtract(7, 'days').startOf('day').toString(), - }, + id: 'visualization:coolviz-123', + attributes: { visState: '{"type": "shell_beads"}' }, + updated_at: moment().subtract(7, 'days').startOf('day').toString(), }, ]; const enlargedMockSavedObjects = [ // default space { - _id: 'visualization:coolviz-123', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "cave_painting"}' }, - updated_at: moment().subtract(7, 'days').startOf('day').toString(), - }, + id: 'visualization:coolviz-123', + namespaces: ['default'], + attributes: { visState: '{"type": "cave_painting"}' }, + updated_at: moment().subtract(7, 'days').startOf('day').toString(), }, { - _id: 'visualization:coolviz-456', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "printing_press"}' }, - updated_at: moment().subtract(20, 'days').startOf('day').toString(), - }, + id: 'visualization:coolviz-456', + namespaces: ['default'], + attributes: { visState: '{"type": "printing_press"}' }, + updated_at: moment().subtract(20, 'days').startOf('day').toString(), }, { - _id: 'meat:visualization:coolviz-789', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "floppy_disk"}' }, - updated_at: moment().subtract(2, 'months').startOf('day').toString(), - }, + id: 'meat:visualization:coolviz-789', + namespaces: ['default'], + attributes: { visState: '{"type": "floppy_disk"}' }, + updated_at: moment().subtract(2, 'months').startOf('day').toString(), }, // meat space { - _id: 'meat:visualization:coolviz-789', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "cave_painting"}' }, - updated_at: moment().subtract(89, 'days').startOf('day').toString(), - }, + id: 'meat:visualization:coolviz-789', + namespaces: ['meat'], + attributes: { visState: '{"type": "cave_painting"}' }, + updated_at: moment().subtract(89, 'days').startOf('day').toString(), }, { - _id: 'meat:visualization:coolviz-789', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "cuneiform"}' }, - updated_at: moment().subtract(5, 'months').startOf('day').toString(), - }, + id: 'meat:visualization:coolviz-789', + namespaces: ['meat'], + attributes: { visState: '{"type": "cuneiform"}' }, + updated_at: moment().subtract(5, 'months').startOf('day').toString(), }, { - _id: 'meat:visualization:coolviz-789', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "cuneiform"}' }, - updated_at: moment().subtract(2, 'days').startOf('day').toString(), - }, + id: 'meat:visualization:coolviz-789', + namespaces: ['meat'], + attributes: { visState: '{"type": "cuneiform"}' }, + updated_at: moment().subtract(2, 'days').startOf('day').toString(), }, { - _id: 'meat:visualization:coolviz-789', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "floppy_disk"}' }, - updated_at: moment().subtract(7, 'days').startOf('day').toString(), - }, + id: 'meat:visualization:coolviz-789', + attributes: { visState: '{"type": "floppy_disk"}' }, + updated_at: moment().subtract(7, 'days').startOf('day').toString(), }, // cyber space { - _id: 'cyber:visualization:coolviz-789', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "floppy_disk"}' }, - updated_at: moment().subtract(7, 'months').startOf('day').toString(), - }, + id: 'cyber:visualization:coolviz-789', + namespaces: ['cyber'], + attributes: { visState: '{"type": "floppy_disk"}' }, + updated_at: moment().subtract(7, 'months').startOf('day').toString(), }, { - _id: 'cyber:visualization:coolviz-789', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "floppy_disk"}' }, - updated_at: moment().subtract(3, 'days').startOf('day').toString(), - }, + id: 'cyber:visualization:coolviz-789', + namespaces: ['cyber'], + attributes: { visState: '{"type": "floppy_disk"}' }, + updated_at: moment().subtract(3, 'days').startOf('day').toString(), }, { - _id: 'cyber:visualization:coolviz-123', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "cave_painting"}' }, - updated_at: moment().subtract(15, 'days').startOf('day').toString(), - }, + id: 'cyber:visualization:coolviz-123', + namespaces: ['cyber'], + attributes: { visState: '{"type": "cave_painting"}' }, + updated_at: moment().subtract(15, 'days').startOf('day').toString(), }, ]; describe('Visualizations usage collector', () => { - const mockIndex = ''; - - const getMockCallCluster = (hits: unknown[]) => - ({ - search: () => Promise.resolve({ body: { hits: { hits } } }) as unknown, - } as ElasticsearchClient); + const getMockCallCluster = (savedObjects: unknown[]) => + (({ + createPointInTimeFinder: jest.fn().mockResolvedValue({ + close: jest.fn(), + find: function* asyncGenerator() { + yield { saved_objects: savedObjects }; + }, + }), + } as unknown) as SavedObjectsClientContract); test('Returns undefined when no results found (undefined)', async () => { - const result = await getStats(getMockCallCluster(undefined as any), mockIndex); + const result = await getStats(getMockCallCluster(undefined as any)); + expect(result).toBeUndefined(); }); test('Returns undefined when no results found (0 results)', async () => { - const result = await getStats(getMockCallCluster([]), mockIndex); + const result = await getStats(getMockCallCluster([])); expect(result).toBeUndefined(); }); test('Summarizes visualizations response data', async () => { - const result = await getStats(getMockCallCluster(defaultMockSavedObjects), mockIndex); + const result = await getStats(getMockCallCluster(defaultMockSavedObjects)); expect(result).toMatchObject({ shell_beads: { @@ -181,7 +161,7 @@ describe('Visualizations usage collector', () => { }, }; - const result = await getStats(getMockCallCluster(enlargedMockSavedObjects), mockIndex); + const result = await getStats(getMockCallCluster(enlargedMockSavedObjects)); expect(result).toMatchObject(expectedStats); }); diff --git a/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts b/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts index 2cd715b7b02c..c2fa148cf121 100644 --- a/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts +++ b/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts @@ -6,12 +6,11 @@ * Side Public License, v 1. */ -import { countBy, get, groupBy, mapValues, max, min, values } from 'lodash'; -import { ElasticsearchClient } from 'kibana/server'; -import type { estypes } from '@elastic/elasticsearch'; - +import { countBy, groupBy, mapValues, max, min, values } from 'lodash'; import { getPastDays } from './get_past_days'; -type ESResponse = estypes.SearchResponse<{ visualization: { visState: string } }>; + +import type { SavedObjectsClientContract, SavedObjectsFindResult } from '../../../../core/server'; +import type { SavedVisState } from '../../../visualizations/common'; interface VisSummary { type: string; @@ -35,61 +34,50 @@ export interface VisualizationUsage { * Parse the response data into telemetry payload */ export async function getStats( - esClient: ElasticsearchClient, - index: string + soClient: SavedObjectsClientContract ): Promise { - const searchParams = { - size: 10000, // elasticsearch index.max_result_window default value - index, - ignoreUnavailable: true, - filterPath: [ - 'hits.hits._id', - 'hits.hits._source.visualization', - 'hits.hits._source.updated_at', - ], - body: { - query: { - bool: { filter: { term: { type: 'visualization' } } }, - }, - }, - }; - const { body: esResponse } = await esClient.search(searchParams); - const size = get(esResponse, 'hits.hits.length', 0); - if (size < 1) { - return; - } - - // `map` to get the raw types - const visSummaries: VisSummary[] = esResponse.hits.hits.map((hit) => { - const spacePhrases = hit._id.toString().split(':'); - const lastUpdated: string = get(hit, '_source.updated_at'); - const space = spacePhrases.length === 3 ? spacePhrases[0] : 'default'; // if in a custom space, the format of a saved object ID is space:type:id - const visualization = get(hit, '_source.visualization', { visState: '{}' }); - const visState: { type?: string } = JSON.parse(visualization.visState); - return { - type: visState.type || '_na_', - space, - past_days: getPastDays(lastUpdated), - }; + const finder = await soClient.createPointInTimeFinder({ + type: 'visualization', + perPage: 1000, + namespaces: ['*'], }); - // organize stats per type - const visTypes = groupBy(visSummaries, 'type'); + const visSummaries: VisSummary[] = []; - // get the final result - return mapValues(visTypes, (curr) => { - const total = curr.length; - const spacesBreakdown = countBy(curr, 'space'); - const spaceCounts: number[] = values(spacesBreakdown); + for await (const response of finder.find()) { + (response.saved_objects || []).forEach((so: SavedObjectsFindResult) => { + if (so.attributes?.visState) { + const visState: SavedVisState = JSON.parse(so.attributes.visState); - return { - total, - spaces_min: min(spaceCounts), - spaces_max: max(spaceCounts), - spaces_avg: total / spaceCounts.length, - saved_7_days_total: curr.filter((c) => c.past_days <= 7).length, - saved_30_days_total: curr.filter((c) => c.past_days <= 30).length, - saved_90_days_total: curr.filter((c) => c.past_days <= 90).length, - }; - }); + visSummaries.push({ + type: visState.type ?? '_na_', + space: so.namespaces?.[0] ?? 'default', + past_days: getPastDays(so.updated_at!), + }); + } + }); + } + await finder.close(); + + if (visSummaries.length) { + // organize stats per type + const visTypes = groupBy(visSummaries, 'type'); + + // get the final result + return mapValues(visTypes, (curr) => { + const total = curr.length; + const spacesBreakdown = countBy(curr, 'space'); + const spaceCounts: number[] = values(spacesBreakdown); + + return { + total, + spaces_min: min(spaceCounts), + spaces_max: max(spaceCounts), + spaces_avg: total / spaceCounts.length, + saved_7_days_total: curr.filter((c) => c.past_days <= 7).length, + saved_30_days_total: curr.filter((c) => c.past_days <= 30).length, + saved_90_days_total: curr.filter((c) => c.past_days <= 90).length, + }; + }); + } } diff --git a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts index a3617631f734..e8f9df851663 100644 --- a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts +++ b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.test.ts @@ -5,28 +5,24 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - -import { of } from 'rxjs'; +import { + createUsageCollectionSetupMock, + createCollectorFetchContextMock, +} from '../../../usage_collection/server/mocks'; import { mockStats, mockGetStats } from './get_usage_collector.mock'; -import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/mocks'; -import { createCollectorFetchContextMock } from 'src/plugins/usage_collection/server/mocks'; - import { registerVisualizationsCollector } from './register_visualizations_collector'; describe('registerVisualizationsCollector', () => { - const mockIndex = 'mock_index'; - const mockConfig = of({ kibana: { index: mockIndex } }); - - it('makes a usage collector and registers it`', () => { + test('makes a usage collector and registers it`', () => { const mockCollectorSet = createUsageCollectionSetupMock(); - registerVisualizationsCollector(mockCollectorSet, mockConfig); + registerVisualizationsCollector(mockCollectorSet); expect(mockCollectorSet.makeUsageCollector).toBeCalledTimes(1); expect(mockCollectorSet.registerCollector).toBeCalledTimes(1); }); - it('makeUsageCollector configs fit the shape', () => { + test('makeUsageCollector configs fit the shape', () => { const mockCollectorSet = createUsageCollectionSetupMock(); - registerVisualizationsCollector(mockCollectorSet, mockConfig); + registerVisualizationsCollector(mockCollectorSet); expect(mockCollectorSet.makeUsageCollector).toHaveBeenCalledWith({ type: 'visualization_types', isReady: expect.any(Function), @@ -37,21 +33,21 @@ describe('registerVisualizationsCollector', () => { expect(usageCollectorConfig.isReady()).toBe(true); }); - it('makeUsageCollector config.isReady returns true', () => { + test('makeUsageCollector config.isReady returns true', () => { const mockCollectorSet = createUsageCollectionSetupMock(); - registerVisualizationsCollector(mockCollectorSet, mockConfig); + registerVisualizationsCollector(mockCollectorSet); const usageCollectorConfig = mockCollectorSet.makeUsageCollector.mock.calls[0][0]; expect(usageCollectorConfig.isReady()).toBe(true); }); - it('makeUsageCollector config.fetch calls getStats', async () => { + test('makeUsageCollector config.fetch calls getStats', async () => { const mockCollectorSet = createUsageCollectionSetupMock(); - registerVisualizationsCollector(mockCollectorSet, mockConfig); + registerVisualizationsCollector(mockCollectorSet); const usageCollector = mockCollectorSet.makeUsageCollector.mock.results[0].value; const mockCollectorFetchContext = createCollectorFetchContextMock(); const fetchResult = await usageCollector.fetch(mockCollectorFetchContext); expect(mockGetStats).toBeCalledTimes(1); - expect(mockGetStats).toBeCalledWith(mockCollectorFetchContext.esClient, mockIndex); + expect(mockGetStats).toBeCalledWith(mockCollectorFetchContext.soClient); expect(fetchResult).toBe(mockStats); }); }); diff --git a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts index 1836af66233b..b7ca4268c9a8 100644 --- a/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts +++ b/src/plugins/visualizations/server/usage_collector/register_visualizations_collector.ts @@ -6,16 +6,10 @@ * Side Public License, v 1. */ -import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; - import { getStats, VisualizationUsage } from './get_usage_collector'; +import type { UsageCollectionSetup } from '../../../usage_collection/server'; -export function registerVisualizationsCollector( - collectorSet: UsageCollectionSetup, - config: Observable<{ kibana: { index: string } }> -) { +export function registerVisualizationsCollector(collectorSet: UsageCollectionSetup) { const collector = collectorSet.makeUsageCollector({ type: 'visualization_types', isReady: () => true, @@ -30,10 +24,7 @@ export function registerVisualizationsCollector( saved_90_days_total: { type: 'long' }, }, }, - fetch: async ({ esClient }) => { - const index = (await config.pipe(first()).toPromise()).kibana.index; - return await getStats(esClient, index); - }, + fetch: async ({ soClient }) => await getStats(soClient), }); collectorSet.registerCollector(collector); } diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json index c2c628875e00..afa9e3ce055b 100644 --- a/src/plugins/visualize/kibana.json +++ b/src/plugins/visualize/kibana.json @@ -26,8 +26,8 @@ "discover" ], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "description": "Contains the visualize application which includes the listing page and the app frame, which will load the visualization's editor." } diff --git a/src/setup_node_env/exit_on_warning.js b/src/setup_node_env/exit_on_warning.js index 5fbee0270808..e9c96f2c49bb 100644 --- a/src/setup_node_env/exit_on_warning.js +++ b/src/setup_node_env/exit_on_warning.js @@ -34,8 +34,7 @@ var IGNORE_WARNINGS = [ // that the security features are blocking such check. // Such emit is causing Node.js to crash unless we explicitly catch it. // We need to discard that warning - message: - 'The client is unable to verify that the server is Elasticsearch due to security privileges on the server side. Some functionality may not be compatible if the server is running an unsupported product.', + name: 'ProductNotSupportedSecurityError', }, ]; diff --git a/test/accessibility/apps/discover.ts b/test/accessibility/apps/discover.ts index 6705598ef6e7..c7794c5023ba 100644 --- a/test/accessibility/apps/discover.ts +++ b/test/accessibility/apps/discover.ts @@ -19,8 +19,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Discover a11y tests', () => { before(async () => { - await esArchiver.load('test/functional/fixtures/es_archiver/discover'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); await kibanaServer.uiSettings.update({ defaultIndex: 'logstash-*', 'doc_table:legacy': true, @@ -30,6 +30,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); }); diff --git a/test/accessibility/apps/management.ts b/test/accessibility/apps/management.ts index 2fb3de63a81a..69b799cc3b9e 100644 --- a/test/accessibility/apps/management.ts +++ b/test/accessibility/apps/management.ts @@ -22,8 +22,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { describe('Management', () => { before(async () => { - await esArchiver.load('test/functional/fixtures/es_archiver/discover'); await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/logstash_functional'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); await kibanaServer.uiSettings.update({ defaultIndex: 'logstash-*', }); @@ -31,6 +31,7 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { }); after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); await esArchiver.unload('test/functional/fixtures/es_archiver/logstash_functional'); }); diff --git a/test/accessibility/apps/visualize.ts b/test/accessibility/apps/visualize.ts index 6478907fcb96..d0592352170f 100644 --- a/test/accessibility/apps/visualize.ts +++ b/test/accessibility/apps/visualize.ts @@ -11,14 +11,18 @@ import { FtrProviderContext } from '../ftr_provider_context'; export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'visualize', 'header']); const a11y = getService('a11y'); - const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); describe('Visualize', () => { before(async () => { - await esArchiver.loadIfNeeded('test/functional/fixtures/es_archiver/discover'); + await kibanaServer.importExport.load('test/functional/fixtures/kbn_archiver/discover'); await PageObjects.common.navigateToApp('visualize'); }); + after(async () => { + await kibanaServer.importExport.unload('test/functional/fixtures/kbn_archiver/discover'); + }); + it('visualize', async () => { await a11y.testAppSnapshot(); }); diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts b/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts index 9922e56f44bd..8e78a2c279d6 100644 --- a/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts +++ b/test/plugin_functional/plugins/core_plugin_deprecations/server/plugin.ts @@ -15,6 +15,7 @@ async function getDeprecations({ const { total } = await savedObjectsClient.find({ type: 'test-deprecations-plugin', perPage: 1 }); deprecations.push({ + title: 'CorePluginDeprecationsPlugin plugin is deprecated', message: `CorePluginDeprecationsPlugin is a deprecated feature for testing.`, documentationUrl: 'test-url', level: 'warning', @@ -26,6 +27,7 @@ async function getDeprecations({ if (total > 0) { deprecations.push({ + title: 'Detected saved objects in test-deprecations-plugin', message: `SavedObject test-deprecations-plugin is still being used.`, documentationUrl: 'another-test-url', level: 'critical', diff --git a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/kibana.json b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/kibana.json index 2bc636371fb0..35be433601f4 100644 --- a/test/plugin_functional/plugins/kbn_tp_custom_visualizations/kibana.json +++ b/test/plugin_functional/plugins/kbn_tp_custom_visualizations/kibana.json @@ -1,8 +1,8 @@ { "id": "kbnTpCustomVisualizations", "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "version": "0.0.1", "kibanaVersion": "kibana", diff --git a/test/plugin_functional/test_suites/core/deprecations.ts b/test/plugin_functional/test_suites/core/deprecations.ts index 2bdd3cf59549..25ac5f24a97c 100644 --- a/test/plugin_functional/test_suites/core/deprecations.ts +++ b/test/plugin_functional/test_suites/core/deprecations.ts @@ -19,9 +19,10 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const CorePluginDeprecationsPluginDeprecations: DomainDeprecationDetails[] = [ { + title: 'Setting "corePluginDeprecations.oldProperty" is deprecated', level: 'critical', message: - '"corePluginDeprecations.oldProperty" is deprecated and has been replaced by "corePluginDeprecations.newProperty"', + 'Setting "corePluginDeprecations.oldProperty" has been replaced by "corePluginDeprecations.newProperty"', correctiveActions: { manualSteps: [ 'Replace "corePluginDeprecations.oldProperty" with "corePluginDeprecations.newProperty" in the Kibana config file, CLI flag, or environment variable (in Docker only).', @@ -32,11 +33,12 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide requireRestart: true, }, { + title: 'Setting "corePluginDeprecations.noLongerUsed" is deprecated', level: 'critical', - message: 'corePluginDeprecations.noLongerUsed is deprecated and is no longer used', + message: 'You no longer need to configure "corePluginDeprecations.noLongerUsed".', correctiveActions: { manualSteps: [ - 'Remove "corePluginDeprecations.noLongerUsed" from the Kibana config file, CLI flag, or environment variable (in Docker only)', + 'Remove "corePluginDeprecations.noLongerUsed" from the Kibana config file, CLI flag, or environment variable (in Docker only).', ], }, deprecationType: 'config', @@ -44,6 +46,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide requireRestart: true, }, { + title: 'corePluginDeprecations has a deprecated setting', level: 'critical', message: 'Kibana plugin functional tests will no longer allow corePluginDeprecations.secret config to be set to anything except 42.', @@ -58,6 +61,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide requireRestart: true, }, { + title: 'CorePluginDeprecationsPlugin plugin is deprecated', message: 'CorePluginDeprecationsPlugin is a deprecated feature for testing.', documentationUrl: 'test-url', level: 'warning', @@ -68,6 +72,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide domainId: 'corePluginDeprecations', }, { + title: 'Detected saved objects in test-deprecations-plugin', message: 'SavedObject test-deprecations-plugin is still being used.', documentationUrl: 'another-test-url', level: 'critical', @@ -128,6 +133,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const resolveResult = await browser.executeAsync((cb) => { return window._coreProvider.start.core.deprecations .resolveDeprecation({ + title: 'CorePluginDeprecationsPlugin plugin is deprecated', message: 'CorePluginDeprecationsPlugin is a deprecated feature for testing.', documentationUrl: 'test-url', level: 'warning', @@ -140,7 +146,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide }); expect(resolveResult).to.eql({ - reason: 'deprecation has no correctiveAction via api.', + reason: 'This deprecation cannot be resolved automatically.', status: 'fail', }); }); @@ -149,6 +155,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const resolveResult = await browser.executeAsync((cb) => { return window._coreProvider.start.core.deprecations .resolveDeprecation({ + title: 'CorePluginDeprecationsPlugin plugin is deprecated', message: 'CorePluginDeprecationsPlugin is a deprecated feature for testing.', documentationUrl: 'test-url', level: 'warning', @@ -177,6 +184,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const resolveResult = await browser.executeAsync((cb) => { return window._coreProvider.start.core.deprecations .resolveDeprecation({ + title: 'CorePluginDeprecationsPlugin plugin is deprecated', message: 'CorePluginDeprecationsPlugin is a deprecated feature for testing.', documentationUrl: 'test-url', level: 'warning', @@ -215,6 +223,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide (keyId, cb) => { return window._coreProvider.start.core.deprecations .resolveDeprecation({ + title: 'CorePluginDeprecationsPlugin plugin is deprecated', message: 'CorePluginDeprecationsPlugin is a deprecated feature for testing.', documentationUrl: 'test-url', level: 'warning', diff --git a/vars/kibanaPipeline.groovy b/vars/kibanaPipeline.groovy index 473c04182f69..2b30e558eb90 100644 --- a/vars/kibanaPipeline.groovy +++ b/vars/kibanaPipeline.groovy @@ -91,6 +91,8 @@ def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) { def fleetPackageRegistryPort = "64${parallelId}1" def alertingProxyPort = "64${parallelId}2" def corsTestServerPort = "64${parallelId}3" + // needed for https://github.com/elastic/kibana/issues/107246 + def proxyTestServerPort = "64${parallelId}4" def apmActive = githubPr.isPr() ? "false" : "true" withEnv([ @@ -103,6 +105,7 @@ def withFunctionalTestEnv(List additionalEnvs = [], Closure closure) { "TEST_ES_URL=http://elastic:changeme@localhost:${esPort}", "TEST_ES_TRANSPORT_PORT=${esTransportPort}", "TEST_CORS_SERVER_PORT=${corsTestServerPort}", + "TEST_PROXY_SERVER_PORT=${proxyTestServerPort}", "KBN_NP_PLUGINS_BUILT=true", "FLEET_PACKAGE_REGISTRY_PORT=${fleetPackageRegistryPort}", "ALERTING_PROXY_PORT=${alertingProxyPort}", diff --git a/x-pack/examples/embedded_lens_example/kibana.json b/x-pack/examples/embedded_lens_example/kibana.json index 13417c658f87..dc979e21aeff 100644 --- a/x-pack/examples/embedded_lens_example/kibana.json +++ b/x-pack/examples/embedded_lens_example/kibana.json @@ -14,7 +14,7 @@ "optionalPlugins": [], "requiredBundles": [], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" } } diff --git a/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts index ceea9f3cff18..6d7fd940612f 100644 --- a/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.test.ts @@ -356,6 +356,14 @@ describe('successful migrations', () => { }); }); }); + + describe('8.0.0', () => { + test('no op migration for rules SO', () => { + const migration800 = getActionTaskParamsMigrations(encryptedSavedObjectsSetup, [])['8.0.0']; + const actionTaskParam = getMockData(); + expect(migration800(actionTaskParam, context)).toEqual(actionTaskParam); + }); + }); }); describe('handles errors during migrations', () => { diff --git a/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.ts b/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.ts index 361264216044..ceb82146a03e 100644 --- a/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.ts +++ b/x-pack/plugins/actions/server/saved_objects/action_task_params_migrations.ts @@ -48,8 +48,17 @@ export function getActionTaskParamsMigrations( pipeMigrations(getUseSavedObjectReferencesFn(preconfiguredActions)) ); + const migrationActionsTaskParams800 = createEsoMigration( + encryptedSavedObjects, + ( + doc: SavedObjectUnsanitizedDoc + ): doc is SavedObjectUnsanitizedDoc => true, + (doc) => doc // no-op + ); + return { '7.16.0': executeMigrationWithErrorHandling(migrationActionTaskParamsSixteen, '7.16.0'), + '8.0.0': executeMigrationWithErrorHandling(migrationActionsTaskParams800, '8.0.0'), }; } diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts index bc0e59279abc..7dc1426c13a4 100644 --- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts +++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.test.ts @@ -118,6 +118,14 @@ describe('successful migrations', () => { }); }); }); + + describe('8.0.0', () => { + test('no op migration for rules SO', () => { + const migration800 = getActionsMigrations(encryptedSavedObjectsSetup)['8.0.0']; + const action = getMockData({}); + expect(migration800(action, context)).toEqual(action); + }); + }); }); describe('handles errors during migrations', () => { diff --git a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts index a72565e00ef7..7857a9e1f833 100644 --- a/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts +++ b/x-pack/plugins/actions/server/saved_objects/actions_migrations.ts @@ -62,10 +62,18 @@ export function getActionsMigrations( pipeMigrations(addisMissingSecretsField) ); + const migrationActions800 = createEsoMigration( + encryptedSavedObjects, + (doc: SavedObjectUnsanitizedDoc): doc is SavedObjectUnsanitizedDoc => + true, + (doc) => doc // no-op + ); + return { '7.10.0': executeMigrationWithErrorHandling(migrationActionsTen, '7.10.0'), '7.11.0': executeMigrationWithErrorHandling(migrationActionsEleven, '7.11.0'), '7.14.0': executeMigrationWithErrorHandling(migrationActionsFourteen, '7.14.0'), + '8.0.0': executeMigrationWithErrorHandling(migrationActions800, '8.0.0'), }; } diff --git a/x-pack/plugins/actions/server/saved_objects/index.ts b/x-pack/plugins/actions/server/saved_objects/index.ts index 71ec92645b24..14b425d20af1 100644 --- a/x-pack/plugins/actions/server/saved_objects/index.ts +++ b/x-pack/plugins/actions/server/saved_objects/index.ts @@ -35,7 +35,8 @@ export function setupSavedObjects( savedObjects.registerType({ name: ACTION_SAVED_OBJECT_TYPE, hidden: true, - namespaceType: 'single', + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', mappings: mappings.action as SavedObjectsTypeMappingDefinition, migrations: getActionsMigrations(encryptedSavedObjects), management: { @@ -71,7 +72,8 @@ export function setupSavedObjects( savedObjects.registerType({ name: ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, hidden: true, - namespaceType: 'single', + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', mappings: mappings.action_task_params as SavedObjectsTypeMappingDefinition, migrations: getActionTaskParamsMigrations(encryptedSavedObjects, preconfiguredActions), excludeOnUpgrade: async ({ readonlyEsClient }) => { diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts index b1d56a364a3d..f1afba147a2f 100644 --- a/x-pack/plugins/alerting/server/saved_objects/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -53,7 +53,8 @@ export function setupSavedObjects( savedObjects.registerType({ name: 'alert', hidden: true, - namespaceType: 'single', + namespaceType: 'multiple-isolated', + convertToMultiNamespaceTypeVersion: '8.0.0', migrations: getMigrations(encryptedSavedObjects, isPreconfigured), mappings: mappings.alert as SavedObjectsTypeMappingDefinition, management: { diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index e460167b40d2..5e850ad3226f 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -1700,6 +1700,14 @@ describe('successful migrations', () => { }); }); }); + + describe('8.0.0', () => { + test('no op migration for rules SO', () => { + const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + const alert = getMockData({}, true); + expect(migration800(alert, migrationContext)).toEqual(alert); + }); + }); }); describe('handles errors during migrations', () => { diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index c0af554cd7a4..287636c69bb7 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -106,6 +106,12 @@ export function getMigrations( pipeMigrations(setLegacyId, getRemovePreconfiguredConnectorsFromReferencesFn(isPreconfigured)) ); + const migrationRules800 = createEsoMigration( + encryptedSavedObjects, + (doc: SavedObjectUnsanitizedDoc): doc is SavedObjectUnsanitizedDoc => true, + (doc) => doc // no-op + ); + return { '7.10.0': executeMigrationWithErrorHandling(migrationWhenRBACWasIntroduced, '7.10.0'), '7.11.0': executeMigrationWithErrorHandling(migrationAlertUpdatedAtAndNotifyWhen, '7.11.0'), @@ -114,6 +120,7 @@ export function getMigrations( '7.14.1': executeMigrationWithErrorHandling(migrationSecurityRules714, '7.14.1'), '7.15.0': executeMigrationWithErrorHandling(migrationSecurityRules715, '7.15.0'), '7.16.0': executeMigrationWithErrorHandling(migrateRules716, '7.16.0'), + '8.0.0': executeMigrationWithErrorHandling(migrationRules800, '8.0.0'), }; } diff --git a/x-pack/plugins/apm/common/backends.ts b/x-pack/plugins/apm/common/backends.ts index 35a52cf3778f..53418fe7f9b6 100644 --- a/x-pack/plugins/apm/common/backends.ts +++ b/x-pack/plugins/apm/common/backends.ts @@ -14,9 +14,9 @@ import { import { environmentQuery } from './utils/environment_query'; export const kueryBarPlaceholder = i18n.translate( - 'xpack.apm.backends.kueryBarPlaceholder', + 'xpack.apm.dependencies.kueryBarPlaceholder', { - defaultMessage: `Search backend metrics (e.g. span.destination.service.resource:elasticsearch)`, + defaultMessage: `Search dependency metrics (e.g. span.destination.service.resource:elasticsearch)`, } ); diff --git a/x-pack/plugins/apm/ftr_e2e/config.ts b/x-pack/plugins/apm/ftr_e2e/config.ts index 5f919fb7f075..12cc8845264c 100644 --- a/x-pack/plugins/apm/ftr_e2e/config.ts +++ b/x-pack/plugins/apm/ftr_e2e/config.ts @@ -35,6 +35,7 @@ async function config({ readConfigFile }: FtrConfigProviderContext) { ...xpackFunctionalTestsConfig.get('kbnTestServer.serverArgs'), '--home.disableWelcomeScreen=true', '--csp.strict=false', + '--csp.warnLegacyBrowsers=false', // define custom kibana server args here `--elasticsearch.ssl.certificateAuthorities=${CA_CERT_PATH}`, ], diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/custom_links.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/custom_links.spec.ts new file mode 100644 index 000000000000..eeb46db04b9d --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/settings/custom_links.spec.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +const basePath = '/app/apm/settings/customize-ui'; + +describe('Custom links', () => { + beforeEach(() => { + cy.loginAsPowerUser(); + }); + + it('shows empty message and create button', () => { + cy.visit(basePath); + cy.contains('No links found'); + cy.contains('Create custom link'); + }); + + it('creates custom link', () => { + cy.visit(basePath); + const emptyPrompt = cy.get('[data-test-subj="customLinksEmptyPrompt"]'); + cy.contains('Create custom link').click(); + cy.contains('Create link'); + cy.contains('Save').should('be.disabled'); + cy.get('input[name="label"]').type('foo'); + cy.get('input[name="url"]').type('https://foo.com'); + cy.contains('Save').should('not.be.disabled'); + cy.contains('Save').click(); + emptyPrompt.should('not.exist'); + cy.contains('foo'); + cy.contains('https://foo.com'); + cy.get('[data-test-subj="editCustomLink"]').click(); + cy.contains('Delete').click(); + }); +}); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts index de8969cbecde..b90ad12b4602 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/read_only_user/service_overview/service_overview.spec.ts @@ -74,6 +74,10 @@ describe('Service Overview', () => { cy.contains('Service Map'); // Waits until the agent request is finished to check the tab. cy.wait('@agentRequest'); - cy.contains('Dependencies').should('not.exist'); + cy.get('.euiTabs .euiTab__content').then((elements) => { + elements.map((index, element) => { + expect(element.innerText).to.not.equal('Dependencies'); + }); + }); }); }); diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts index 31eab9507ef5..93dbe4ba5122 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts @@ -11,8 +11,8 @@ Cypress.Commands.add('loginAsReadOnlyUser', () => { cy.loginAs({ username: 'apm_read_user', password: 'changeme' }); }); -Cypress.Commands.add('loginAsSuperUser', () => { - cy.loginAs({ username: 'elastic', password: 'changeme' }); +Cypress.Commands.add('loginAsPowerUser', () => { + cy.loginAs({ username: 'apm_power_user', password: 'changeme' }); }); Cypress.Commands.add( diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts index b47e664e0a0f..2d9ef090eef6 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/types.d.ts @@ -8,7 +8,7 @@ declare namespace Cypress { interface Chainable { loginAsReadOnlyUser(): void; - loginAsSuperUser(): void; + loginAsPowerUser(): void; loginAs(params: { username: string; password: string }): void; changeTimeRange(value: string): void; expectAPIsToHaveBeenCalledWith(params: { diff --git a/x-pack/plugins/apm/kibana.json b/x-pack/plugins/apm/kibana.json index 40e724e306bc..5bc365e35cb2 100644 --- a/x-pack/plugins/apm/kibana.json +++ b/x-pack/plugins/apm/kibana.json @@ -12,6 +12,7 @@ "embeddable", "features", "infra", + "inspector", "licensing", "observability", "ruleRegistry", diff --git a/x-pack/plugins/apm/public/application/index.tsx b/x-pack/plugins/apm/public/application/index.tsx index a6b0dc61a326..feb1ff372dc9 100644 --- a/x-pack/plugins/apm/public/application/index.tsx +++ b/x-pack/plugins/apm/public/application/index.tsx @@ -48,6 +48,7 @@ export const renderApp = ({ core: coreStart, plugins: pluginsSetup, data: pluginsStart.data, + inspector: pluginsStart.inspector, observability: pluginsStart.observability, observabilityRuleTypeRegistry, }; diff --git a/x-pack/plugins/apm/public/application/uxApp.tsx b/x-pack/plugins/apm/public/application/uxApp.tsx index 1b36008e5c35..ddcccf45ccab 100644 --- a/x-pack/plugins/apm/public/application/uxApp.tsx +++ b/x-pack/plugins/apm/public/application/uxApp.tsx @@ -91,7 +91,7 @@ export function UXAppRoot({ core, deps, config, - corePlugins: { embeddable, maps, observability, data }, + corePlugins: { embeddable, inspector, maps, observability, data }, observabilityRuleTypeRegistry, }: { appMountParameters: AppMountParameters; @@ -108,6 +108,7 @@ export function UXAppRoot({ appMountParameters, config, core, + inspector, plugins, observability, observabilityRuleTypeRegistry, diff --git a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts index dc52d572e2f3..4fc2dcefc9c5 100644 --- a/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts +++ b/x-pack/plugins/apm/public/components/alerting/register_apm_alerts.ts @@ -78,7 +78,7 @@ export function registerApmAlerts( }, iconClass: 'bell', documentationUrl(docLinks) { - return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; + return `${docLinks.links.alerting.apmRules}`; }, alertParamsExpression: lazy(() => import('./error_count_alert_trigger')), validate: () => ({ @@ -126,7 +126,7 @@ export function registerApmAlerts( }), iconClass: 'bell', documentationUrl(docLinks) { - return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; + return `${docLinks.links.alerting.apmRules}`; }, alertParamsExpression: lazy( () => import('./transaction_duration_alert_trigger') @@ -177,7 +177,7 @@ export function registerApmAlerts( }), iconClass: 'bell', documentationUrl(docLinks) { - return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; + return `${docLinks.links.alerting.apmRules}`; }, alertParamsExpression: lazy( () => import('./transaction_error_rate_alert_trigger') @@ -226,7 +226,7 @@ export function registerApmAlerts( }), iconClass: 'bell', documentationUrl(docLinks) { - return `${docLinks.ELASTIC_WEBSITE_URL}guide/en/kibana/${docLinks.DOC_LINK_VERSION}/apm-alerts.html`; + return `${docLinks.links.alerting.apmRules}`; }, alertParamsExpression: lazy( () => import('./transaction_duration_anomaly_alert_trigger') diff --git a/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/EmptyPrompt.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/EmptyPrompt.tsx index 9d6a3eef3f7e..498e17b9c359 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/EmptyPrompt.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/EmptyPrompt.tsx @@ -17,6 +17,7 @@ export function EmptyPrompt({ }) { return ( )} -
- - - -

- {i18n.translate( - 'xpack.apm.settings.customizeUI.customLink.flyout.title', + + + + +

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

+
+
+ + +

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

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

-
+ /> +

+ - + - + - + - + - + - -
+ + - -
- - + + + ); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/custom_link_table.tsx b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/custom_link_table.tsx index 4a242bb661e3..86a7a8742eae 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/custom_link_table.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/customize_ui/custom_link/custom_link_table.tsx @@ -79,6 +79,7 @@ export function CustomLinkTable({ items = [], onCustomLinkSelected }: Props) { icon: 'pencil', color: 'primary', type: 'icon', + 'data-test-subj': 'editCustomLink', onClick: (customLink: CustomLink) => { onCustomLinkSelected(customLink); }, diff --git a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx index 1adb41acab70..2c9ec0a23297 100644 --- a/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_detail_overview/index.tsx @@ -18,7 +18,7 @@ import { useApmParams } from '../../../hooks/use_apm_params'; import { useApmRouter } from '../../../hooks/use_apm_router'; import { SearchBar } from '../../shared/search_bar'; import { BackendLatencyChart } from './backend_latency_chart'; -import { BackendInventoryTitle } from '../../routing/home'; +import { DependenciesInventoryTitle } from '../../routing/home'; import { BackendDetailDependenciesTable } from './backend_detail_dependencies_table'; import { BackendThroughputChart } from './backend_throughput_chart'; import { BackendFailedTransactionRateChart } from './backend_error_rate_chart'; @@ -39,7 +39,7 @@ export function BackendDetailOverview() { useBreadcrumb([ { - title: BackendInventoryTitle, + title: DependenciesInventoryTitle, href: apmRouter.link('/backends', { query: { rangeFrom, rangeTo, environment, kuery }, }), diff --git a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx index 7ccf3f166fc6..ea135104982e 100644 --- a/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/backend_inventory/backend_inventory_dependencies_table/index.tsx @@ -98,9 +98,9 @@ export function BackendInventoryDependenciesTable() { dependencies={dependencies} title={null} nameColumnTitle={i18n.translate( - 'xpack.apm.backendInventory.dependenciesTableColumnBackend', + 'xpack.apm.backendInventory.dependencyTableColumn', { - defaultMessage: 'Backend', + defaultMessage: 'Dependency', } )} status={status} diff --git a/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts b/x-pack/plugins/apm/public/components/app/service_logs/index.test.ts new file mode 100644 index 000000000000..b0cc134778d2 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_logs/index.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { getInfrastructureKQLFilter } from './'; + +describe('service logs', () => { + describe('getInfrastructureKQLFilter', () => { + it('filter by container id', () => { + expect( + getInfrastructureKQLFilter({ + serviceInfrastructure: { + containerIds: ['foo', 'bar'], + hostNames: ['baz', `quz`], + }, + }) + ).toEqual('container.id: "foo" or container.id: "bar"'); + }); + it('filter by host names', () => { + expect( + getInfrastructureKQLFilter({ + serviceInfrastructure: { + containerIds: [], + hostNames: ['baz', `quz`], + }, + }) + ).toEqual('host.name: "baz" or host.name: "quz"'); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx index e8ac37036836..ac4a4fb51ce8 100644 --- a/x-pack/plugins/apm/public/components/app/service_logs/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_logs/index.tsx @@ -18,7 +18,6 @@ import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { CONTAINER_ID, HOSTNAME, - POD_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useTimeRange } from '../../../hooks/use_time_range'; @@ -55,8 +54,7 @@ export function ServiceLogs() { const noInfrastructureData = useMemo(() => { return ( isEmpty(data?.serviceInfrastructure?.containerIds) && - isEmpty(data?.serviceInfrastructure?.hostNames) && - isEmpty(data?.serviceInfrastructure?.podNames) + isEmpty(data?.serviceInfrastructure?.hostNames) ); }, [data]); @@ -93,16 +91,15 @@ export function ServiceLogs() { ); } -const getInfrastructureKQLFilter = ( +export const getInfrastructureKQLFilter = ( data?: APIReturnType<'GET /api/apm/services/{serviceName}/infrastructure'> ) => { const containerIds = data?.serviceInfrastructure?.containerIds ?? []; const hostNames = data?.serviceInfrastructure?.hostNames ?? []; - const podNames = data?.serviceInfrastructure?.podNames ?? []; - return [ - ...containerIds.map((id) => `${CONTAINER_ID}: "${id}"`), - ...hostNames.map((id) => `${HOSTNAME}: "${id}"`), - ...podNames.map((id) => `${POD_NAME}: "${id}"`), - ].join(' or '); + const kqlFilter = containerIds.length + ? containerIds.map((id) => `${CONTAINER_ID}: "${id}"`) + : hostNames.map((id) => `${HOSTNAME}: "${id}"`); + + return kqlFilter.join(' or '); }; diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx index 0a42dbab9a45..9bc30ee67d2c 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/backend_contents.tsx @@ -85,8 +85,8 @@ export function BackendContents({ }); }} > - {i18n.translate('xpack.apm.serviceMap.backendDetailsButtonText', { - defaultMessage: 'Backend Details', + {i18n.translate('xpack.apm.serviceMap.dependencyDetailsButtonText', { + defaultMessage: 'Dependency Details', })} diff --git a/x-pack/plugins/apm/public/components/app/service_map/Popover/popover.test.tsx b/x-pack/plugins/apm/public/components/app/service_map/Popover/popover.test.tsx index 9678258c4740..5bec70b9eb84 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/Popover/popover.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/Popover/popover.test.tsx @@ -14,12 +14,12 @@ const { Backend, ExternalsList, Resource, Service } = composeStories(stories); describe('Popover', () => { describe('with backend data', () => { - it('renders a backend link', async () => { + it('renders a dependency link', async () => { render(); await waitFor(() => { expect( - screen.getByRole('link', { name: /backend details/i }) + screen.getByRole('link', { name: /Dependency Details/i }) ).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx index 0d0842582335..08f29d7727cd 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_dependencies_table/index.tsx @@ -125,13 +125,13 @@ export function ServiceOverviewDependenciesTable({ title={i18n.translate( 'xpack.apm.serviceOverview.dependenciesTableTitle', { - defaultMessage: 'Downstream services and backends', + defaultMessage: 'Dependencies', } )} nameColumnTitle={i18n.translate( - 'xpack.apm.serviceOverview.dependenciesTableColumnBackend', + 'xpack.apm.serviceOverview.dependenciesTableColumn', { - defaultMessage: 'Backend', + defaultMessage: 'Dependency', } )} status={status} diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx index 9b8706fe1103..6751e76cfa33 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx @@ -37,10 +37,12 @@ export function ServiceOverviewThroughputChart({ height, environment, kuery, + transactionName, }: { height?: number; environment: string; kuery: string; + transactionName?: string; }) { const theme = useTheme(); @@ -80,6 +82,7 @@ export function ServiceOverviewThroughputChart({ transactionType, comparisonStart, comparisonEnd, + transactionName, }, }, }); @@ -94,6 +97,7 @@ export function ServiceOverviewThroughputChart({ transactionType, comparisonStart, comparisonEnd, + transactionName, ] ); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx index c4ecc71941b8..06acaeeb5dd3 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/index.tsx @@ -12,6 +12,8 @@ import { ChartPointerEventContextProvider } from '../../../context/chart_pointer import { useApmParams } from '../../../hooks/use_apm_params'; import { useApmRouter } from '../../../hooks/use_apm_router'; import { useTimeRange } from '../../../hooks/use_time_range'; +import { useFallbackToTransactionsFetcher } from '../../../hooks/use_fallback_to_transactions_fetcher'; +import { AggregatedTransactionsBadge } from '../../shared/aggregated_transactions_badge'; import { TransactionCharts } from '../../shared/charts/transaction_charts'; import { TransactionDetailsTabs } from './transaction_details_tabs'; @@ -34,8 +36,14 @@ export function TransactionDetails() { }), }); + const { kuery } = query; + const { fallbackToTransactions } = useFallbackToTransactionsFetcher({ + kuery, + }); + return ( <> + {fallbackToTransactions && } @@ -50,6 +58,7 @@ export function TransactionDetails() { environment={query.environment} start={start} end={end} + transactionName={transactionName} /> diff --git a/x-pack/plugins/apm/public/components/routing/app_root.tsx b/x-pack/plugins/apm/public/components/routing/app_root.tsx index 498d489691e7..c32828eca2f6 100644 --- a/x-pack/plugins/apm/public/components/routing/app_root.tsx +++ b/x-pack/plugins/apm/public/components/routing/app_root.tsx @@ -26,6 +26,7 @@ import { } from '../../context/apm_plugin/apm_plugin_context'; import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; import { BreadcrumbsContextProvider } from '../../context/breadcrumbs/context'; +import { InspectorContextProvider } from '../../context/inspector/inspector_context'; import { LicenseProvider } from '../../context/license/license_context'; import { TimeRangeIdContextProvider } from '../../context/time_range_id/time_range_id_context'; import { UrlParamsProvider } from '../../context/url_params_context/url_params_context'; @@ -62,12 +63,14 @@ export function ApmAppRoot({ - - + + + - - - + + + + diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx index d1304e192ddc..1430f5d8e475 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -45,10 +45,10 @@ export const ServiceInventoryTitle = i18n.translate( } ); -export const BackendInventoryTitle = i18n.translate( - 'xpack.apm.views.backendInventory.title', +export const DependenciesInventoryTitle = i18n.translate( + 'xpack.apm.views.dependenciesInventory.title', { - defaultMessage: 'Backends', + defaultMessage: 'Dependencies', } ); @@ -114,7 +114,7 @@ export const home = { }, page({ path: '/', - title: BackendInventoryTitle, + title: DependenciesInventoryTitle, element: , }), ], diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx index 7d000a29dcbe..633d03ce8e1d 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/index.tsx @@ -14,6 +14,7 @@ import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_ import { AlertingPopoverAndFlyout } from './alerting_popover_flyout'; import { AnomalyDetectionSetupLink } from './anomaly_detection_setup_link'; import { useServiceName } from '../../../hooks/use_service_name'; +import { InspectorHeaderLink } from './inspector_header_link'; export function ApmHeaderActionMenu() { const { core, plugins } = useApmPluginContext(); @@ -65,6 +66,7 @@ export function ApmHeaderActionMenu() { defaultMessage: 'Add data', })} + ); } diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/inspector_header_link.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/inspector_header_link.tsx new file mode 100644 index 000000000000..7f1848e76d28 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/inspector_header_link.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiHeaderLink } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { enableInspectEsQueries } from '../../../../../observability/public'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { useInspectorContext } from '../../../context/inspector/use_inspector_context'; + +export function InspectorHeaderLink() { + const { inspector } = useApmPluginContext(); + const { inspectorAdapters } = useInspectorContext(); + const { + services: { uiSettings }, + } = useKibana(); + const isInspectorEnabled = uiSettings?.get(enableInspectEsQueries); + + const inspect = () => { + inspector.open(inspectorAdapters); + }; + + if (!isInspectorEnabled) { + return null; + } + + return ( + + {i18n.translate('xpack.apm.inspectButtonText', { + defaultMessage: 'Inspect', + })} + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx index 6e2ed04776e1..4fdce0dfa705 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_charts/index.tsx @@ -19,11 +19,13 @@ export function TransactionCharts({ environment, start, end, + transactionName, }: { kuery: string; environment: string; start: string; end: string; + transactionName?: string; }) { return ( <> @@ -44,6 +46,7 @@ export function TransactionCharts({ diff --git a/x-pack/plugins/apm/public/components/shared/search_bar.tsx b/x-pack/plugins/apm/public/components/shared/search_bar.tsx index 6e5896c9b5e4..55e19e547b28 100644 --- a/x-pack/plugins/apm/public/components/shared/search_bar.tsx +++ b/x-pack/plugins/apm/public/components/shared/search_bar.tsx @@ -7,19 +7,12 @@ import { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; import { - EuiCallOut, EuiFlexGroup, + EuiFlexGroupProps, EuiFlexItem, - EuiLink, EuiSpacer, - EuiFlexGroupProps, } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { enableInspectEsQueries } from '../../../../observability/public'; -import { useApmPluginContext } from '../../context/apm_plugin/use_apm_plugin_context'; -import { useKibanaUrl } from '../../hooks/useKibanaUrl'; import { useBreakPoints } from '../../hooks/use_break_points'; import { DatePicker } from './DatePicker'; import { KueryBar } from './kuery_bar'; @@ -35,52 +28,6 @@ interface Props { kueryBarBoolFilter?: QueryDslQueryContainer[]; } -function DebugQueryCallout() { - const { uiSettings } = useApmPluginContext().core; - const advancedSettingsUrl = useKibanaUrl('/app/management/kibana/settings', { - query: { - query: 'category:(observability)', - }, - }); - - if (!uiSettings.get(enableInspectEsQueries)) { - return null; - } - - return ( - - - - - {i18n.translate( - 'xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description.advancedSettings', - { defaultMessage: 'Advanced Settings' } - )} - - ), - }} - /> - - - - ); -} - export function SearchBar({ hidden = false, showKueryBar = true, @@ -100,7 +47,6 @@ export function SearchBar({ return ( <> - (result: FetcherResult) => void; + inspectorAdapters: { requests: RequestAdapter }; +} + +const value: InspectorContextValue = { + addInspectorRequest: () => {}, + inspectorAdapters: { requests: new RequestAdapter() }, +}; + +export const InspectorContext = createContext(value); + +export function InspectorContextProvider({ + children, +}: { + children: ReactNode; +}) { + const history = useHistory(); + const { inspectorAdapters } = value; + + function addInspectorRequest( + result: FetcherResult<{ + mainStatisticsData?: { _inspect?: InspectResponse }; + _inspect?: InspectResponse; + }> + ) { + const operations = + result.data?._inspect ?? result.data?.mainStatisticsData?._inspect ?? []; + + operations.forEach((operation) => { + if (operation.response) { + const { id, name } = operation; + const requestParams = { id, name }; + + const requestResponder = inspectorAdapters.requests.start( + id, + requestParams, + operation.startTime + ); + + requestResponder.json(operation.json as object); + + if (operation.stats) { + requestResponder.stats(operation.stats); + } + + requestResponder.finish(operation.status, operation.response); + } + }); + } + + useEffect(() => { + const unregisterCallback = history.listen((newLocation) => { + if (history.location.pathname !== newLocation.pathname) { + inspectorAdapters.requests.reset(); + } + }); + + return () => { + unregisterCallback(); + }; + }, [history, inspectorAdapters]); + + return ( + + {children} + + ); +} diff --git a/x-pack/plugins/apm/public/context/inspector/use_inspector_context.tsx b/x-pack/plugins/apm/public/context/inspector/use_inspector_context.tsx new file mode 100644 index 000000000000..a60ed6c8c72e --- /dev/null +++ b/x-pack/plugins/apm/public/context/inspector/use_inspector_context.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useContext } from 'react'; +import { InspectorContext } from './inspector_context'; + +export function useInspectorContext() { + return useContext(InspectorContext); +} diff --git a/x-pack/plugins/apm/public/hooks/use_fetcher.tsx b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx index df7487290848..d5a10a6e9153 100644 --- a/x-pack/plugins/apm/public/hooks/use_fetcher.tsx +++ b/x-pack/plugins/apm/public/hooks/use_fetcher.tsx @@ -9,6 +9,7 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useMemo, useState } from 'react'; import { IHttpFetchError } from 'src/core/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; +import { useInspectorContext } from '../context/inspector/use_inspector_context'; import { useTimeRangeId } from '../context/time_range_id/use_time_range_id'; import { AutoAbortedAPMClient, @@ -77,6 +78,7 @@ export function useFetcher( }); const [counter, setCounter] = useState(0); const { timeRangeId } = useTimeRangeId(); + const { addInspectorRequest } = useInspectorContext(); useEffect(() => { let controller: AbortController = new AbortController(); @@ -165,6 +167,14 @@ export function useFetcher( /* eslint-enable react-hooks/exhaustive-deps */ ]); + useEffect(() => { + if (result.error) { + addInspectorRequest({ ...result, data: result.error.body?.attributes }); + } else { + addInspectorRequest(result); + } + }, [addInspectorRequest, result]); + return useMemo(() => { return { ...result, diff --git a/x-pack/plugins/apm/public/plugin.ts b/x-pack/plugins/apm/public/plugin.ts index c884f228c85d..da2ea0ba8ae5 100644 --- a/x-pack/plugins/apm/public/plugin.ts +++ b/x-pack/plugins/apm/public/plugin.ts @@ -23,13 +23,14 @@ import type { DataPublicPluginStart, } from '../../../../src/plugins/data/public'; import type { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; -import type { FleetStart } from '../../fleet/public'; import type { HomePublicPluginSetup } from '../../../../src/plugins/home/public'; +import { Start as InspectorPluginStart } from '../../../../src/plugins/inspector/public'; import type { PluginSetupContract as AlertingPluginPublicSetup, PluginStartContract as AlertingPluginPublicStart, } from '../../alerting/public'; import type { FeaturesPluginSetup } from '../../features/public'; +import type { FleetStart } from '../../fleet/public'; import type { LicensingPluginSetup } from '../../licensing/public'; import type { MapsStartApi } from '../../maps/public'; import type { MlPluginSetup, MlPluginStart } from '../../ml/public'; @@ -45,15 +46,14 @@ import type { TriggersAndActionsUIPublicPluginStart, } from '../../triggers_actions_ui/public'; import { registerApmAlerts } from './components/alerting/register_apm_alerts'; -import { featureCatalogueEntry } from './featureCatalogueEntry'; import { getApmEnrollmentFlyoutData, LazyApmCustomAssetsExtension, } from './components/fleet_integration'; +import { getLazyApmAgentsTabExtension } from './components/fleet_integration/lazy_apm_agents_tab_extension'; import { getLazyAPMPolicyCreateExtension } from './components/fleet_integration/lazy_apm_policy_create_extension'; import { getLazyAPMPolicyEditExtension } from './components/fleet_integration/lazy_apm_policy_edit_extension'; -import { getLazyApmAgentsTabExtension } from './components/fleet_integration/lazy_apm_agents_tab_extension'; - +import { featureCatalogueEntry } from './featureCatalogueEntry'; export type ApmPluginSetup = ReturnType; export type ApmPluginStart = void; @@ -74,6 +74,7 @@ export interface ApmPluginStartDeps { data: DataPublicPluginStart; embeddable: EmbeddableStart; home: void; + inspector: InspectorPluginStart; licensing: void; maps?: MapsStartApi; ml?: MlPluginStart; @@ -92,9 +93,12 @@ const serviceMapTitle = i18n.translate('xpack.apm.navigation.serviceMapTitle', { defaultMessage: 'Service Map', }); -const backendsTitle = i18n.translate('xpack.apm.navigation.backendsTitle', { - defaultMessage: 'Backends', -}); +const dependenciesTitle = i18n.translate( + 'xpack.apm.navigation.dependenciesTitle', + { + defaultMessage: 'Dependencies', + } +); export class ApmPlugin implements Plugin { constructor( @@ -125,7 +129,7 @@ export class ApmPlugin implements Plugin { { label: servicesTitle, app: 'apm', path: '/services' }, { label: tracesTitle, app: 'apm', path: '/traces' }, { - label: backendsTitle, + label: dependenciesTitle, app: 'apm', path: '/backends', isNewFeature: true, @@ -269,7 +273,7 @@ export class ApmPlugin implements Plugin { { id: 'services', title: servicesTitle, path: '/services' }, { id: 'traces', title: tracesTitle, path: '/traces' }, { id: 'service-map', title: serviceMapTitle, path: '/service-map' }, - { id: 'backends', title: backendsTitle, path: '/backends' }, + { id: 'backends', title: dependenciesTitle, path: '/backends' }, ], async mount(appMountParameters: AppMountParameters) { diff --git a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts index 217d7e050369..35dbca1b0c95 100644 --- a/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts +++ b/x-pack/plugins/apm/public/services/rest/createCallApmApi.ts @@ -25,10 +25,10 @@ import { FetchOptions } from '../../../common/fetch_options'; import { callApi } from './callApi'; import type { APMServerRouteRepository, - InspectResponse, APMRouteHandlerResources, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../server'; +import { InspectResponse } from '../../../typings/common'; export type APMClientOptions = Omit< FetchOptions, diff --git a/x-pack/plugins/apm/server/feature.ts b/x-pack/plugins/apm/server/feature.ts index f3e2bba2d978..0d1ed4745d00 100644 --- a/x-pack/plugins/apm/server/feature.ts +++ b/x-pack/plugins/apm/server/feature.ts @@ -64,7 +64,7 @@ export const APM_FEATURE = { management: { insightsAndAlerting: ['triggersActions'], }, - ui: ['show', 'alerting:show', 'alerting:save'], + ui: ['show', 'alerting:show'], }, }, subFeatures: [ diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index b6dd22c528e9..5b9717360195 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -131,6 +131,6 @@ export { APM_SERVER_FEATURE_ID } from '../common/alert_types'; export { APMPlugin } from './plugin'; export { APMPluginSetup } from './types'; export { APMServerRouteRepository } from './routes/get_global_apm_server_route_repository'; -export { InspectResponse, APMRouteHandlerResources } from './routes/typings'; +export { APMRouteHandlerResources } from './routes/typings'; export type { ProcessorEvent } from '../common/processor_event'; diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts index a9d2110117d8..fdcfffaf4bac 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.test.ts @@ -44,7 +44,6 @@ describe('get buckets', () => { get: () => 'myIndex', } ) as APMConfig, - uiFilters: {}, indices: { /* eslint-disable @typescript-eslint/naming-convention */ 'apm_oss.sourcemapIndices': 'apm-*', diff --git a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts index b339c1f1f0be..afe3a95d7902 100644 --- a/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts +++ b/x-pack/plugins/apm/server/lib/fleet/get_apm_package_policy_definition.ts @@ -16,6 +16,7 @@ interface GetApmPackagePolicyDefinitionOptions { export function getApmPackagePolicyDefinition( options: GetApmPackagePolicyDefinitionOptions ) { + const { apmServerSchema, cloudPluginSetup } = options; return { name: 'Elastic APM', namespace: 'default', @@ -27,7 +28,10 @@ export function getApmPackagePolicyDefinition( type: 'apm', enabled: true, streams: [], - vars: getApmPackageInputVars(options), + vars: getApmPackageInputVars({ + cloudPluginSetup, + apmServerSchema: preprocessLegacyFields({ apmServerSchema }), + }), }, ], package: { @@ -38,6 +42,34 @@ export function getApmPackagePolicyDefinition( }; } +function preprocessLegacyFields({ + apmServerSchema, +}: { + apmServerSchema: Record; +}) { + const copyOfApmServerSchema = { ...apmServerSchema }; + [ + { + key: 'apm-server.auth.anonymous.rate_limit.event_limit', + legacyKey: 'apm-server.rum.event_rate.limit', + }, + { + key: 'apm-server.auth.anonymous.rate_limit.ip_limit', + legacyKey: 'apm-server.rum.event_rate.lru_size', + }, + { + key: 'apm-server.auth.anonymous.allow_service', + legacyKey: 'apm-server.rum.allow_service_names', + }, + ].forEach(({ key, legacyKey }) => { + if (!copyOfApmServerSchema[key]) { + copyOfApmServerSchema[key] = copyOfApmServerSchema[legacyKey]; + delete copyOfApmServerSchema[legacyKey]; + } + }); + return copyOfApmServerSchema; +} + function getApmPackageInputVars(options: GetApmPackagePolicyDefinitionOptions) { const { apmServerSchema } = options; const apmServerConfigs = Object.entries( @@ -90,6 +122,18 @@ export const apmConfigMapping: Record< name: 'rum_allow_headers', type: 'text', }, + 'apm-server.rum.event_rate.limit': { + name: 'rum_event_rate_limit', + type: 'integer', + }, + 'apm-server.rum.allow_service_names': { + name: 'rum_allow_service_names', + type: 'text', + }, + 'apm-server.rum.event_rate.lru_size': { + name: 'rum_event_rate_lru_size', + type: 'integer', + }, 'apm-server.rum.response_headers': { name: 'rum_response_headers', type: 'yaml', diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/__snapshots__/get_is_using_transaction_events.test.ts.snap b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/__snapshots__/get_is_using_transaction_events.test.ts.snap new file mode 100644 index 000000000000..77984c4d5f1d --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/__snapshots__/get_is_using_transaction_events.test.ts.snap @@ -0,0 +1,145 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getIsUsingTransactionEvents with config xpack.apm.searchAggregatedTransactions: always should query for data when kuery is set 1`] = ` +Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "exists": Object { + "field": "transaction.duration.histogram", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + Object { + "bool": Object { + "minimum_should_match": 1, + "should": Array [ + Object { + "match_phrase": Object { + "proccessor.event": "transaction", + }, + }, + ], + }, + }, + ], + }, + }, + }, + "terminateAfter": 1, +} +`; + +exports[`getIsUsingTransactionEvents with config xpack.apm.searchAggregatedTransactions: auto should query for data once if metrics data found 1`] = ` +Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "exists": Object { + "field": "transaction.duration.histogram", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + ], + }, + }, + }, + "terminateAfter": 1, +} +`; + +exports[`getIsUsingTransactionEvents with config xpack.apm.searchAggregatedTransactions: auto should query for data twice if metrics data not found 1`] = ` +Array [ + Array [ + "get_has_aggregated_transactions", + Object { + "apm": Object { + "events": Array [ + "metric", + ], + }, + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "exists": Object { + "field": "transaction.duration.histogram", + }, + }, + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + ], + }, + }, + }, + "terminateAfter": 1, + }, + ], + Array [ + "get_has_transactions", + Object { + "apm": Object { + "events": Array [ + "transaction", + ], + }, + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "range": Object { + "@timestamp": Object { + "format": "epoch_millis", + "gte": 1528113600000, + "lte": 1528977600000, + }, + }, + }, + ], + }, + }, + }, + "terminateAfter": 1, + }, + ], +] +`; diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_fallback_to_transactions.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_fallback_to_transactions.ts deleted file mode 100644 index e3c8f52146e9..000000000000 --- a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_fallback_to_transactions.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { getSearchAggregatedTransactions } from '.'; -import { SearchAggregatedTransactionSetting } from '../../../../common/aggregated_transactions'; -import { Setup, SetupTimeRange } from '../setup_request'; - -export async function getFallbackToTransactions({ - setup: { config, start, end, apmEventClient }, - kuery, -}: { - setup: Setup & Partial; - kuery: string; -}): Promise { - const searchAggregatedTransactions = - config['xpack.apm.searchAggregatedTransactions']; - const neverSearchAggregatedTransactions = - searchAggregatedTransactions === SearchAggregatedTransactionSetting.never; - - if (neverSearchAggregatedTransactions) { - return false; - } - - const searchesAggregatedTransactions = await getSearchAggregatedTransactions({ - config, - start, - end, - apmEventClient, - kuery, - }); - return !searchesAggregatedTransactions; -} diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_is_using_transaction_events.test.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_is_using_transaction_events.test.ts new file mode 100644 index 000000000000..f17224384842 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_is_using_transaction_events.test.ts @@ -0,0 +1,257 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getIsUsingTransactionEvents } from './get_is_using_transaction_events'; +import { + SearchParamsMock, + inspectSearchParams, +} from '../../../utils/test_helpers'; +import { SearchAggregatedTransactionSetting } from '../../../../common/aggregated_transactions'; + +const mockResponseNoHits = { + took: 398, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 0, + relation: 'gte' as const, + max_score: 0, + }, + hits: [], + }, +}; + +const mockResponseSomeHits = { + took: 398, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0, + }, + hits: { + total: { + value: 3, + relation: 'gte' as const, + }, + hits: [], + }, +}; + +describe('getIsUsingTransactionEvents', () => { + let mock: SearchParamsMock; + + afterEach(() => { + mock.teardown(); + }); + + describe('with config xpack.apm.searchAggregatedTransactions: never', () => { + const config = { + 'xpack.apm.searchAggregatedTransactions': + SearchAggregatedTransactionSetting.never, + }; + + it('should be false', async () => { + mock = await inspectSearchParams( + (setup) => getIsUsingTransactionEvents({ setup, kuery: '' }), + { config } + ); + expect(mock.response).toBe(false); + }); + + it('should not query for data', async () => { + mock = await inspectSearchParams( + (setup) => getIsUsingTransactionEvents({ setup, kuery: '' }), + { config } + ); + expect(mock.spy).toHaveBeenCalledTimes(0); + }); + }); + + describe('with config xpack.apm.searchAggregatedTransactions: always', () => { + const config = { + 'xpack.apm.searchAggregatedTransactions': + SearchAggregatedTransactionSetting.always, + }; + it('should be false when kuery is empty', async () => { + mock = await inspectSearchParams( + (setup) => getIsUsingTransactionEvents({ setup, kuery: '' }), + { config } + ); + expect(mock.response).toBe(false); + }); + + it('should be false when kuery is set and metrics data found', async () => { + mock = await inspectSearchParams( + (setup) => + getIsUsingTransactionEvents({ + setup, + kuery: 'proccessor.event: "transaction"', + }), + { + config, + mockResponse: (request) => { + if (request === 'get_has_aggregated_transactions') { + return mockResponseSomeHits; + } + if (request === 'get_has_transactions') { + return mockResponseNoHits; + } + return mockResponseNoHits; + }, + } + ); + expect(mock.spy).toHaveBeenCalledTimes(1); + expect(mock.response).toBe(false); + }); + + it('should be true when kuery is set and metrics data are not found', async () => { + mock = await inspectSearchParams( + (setup) => + getIsUsingTransactionEvents({ + setup, + kuery: 'proccessor.event: "transaction"', + }), + { + config, + mockResponse: (request) => { + if (request === 'get_has_aggregated_transactions') { + return mockResponseNoHits; + } + if (request === 'get_has_transactions') { + return mockResponseSomeHits; + } + return mockResponseNoHits; + }, + } + ); + expect(mock.spy).toHaveBeenCalledTimes(2); + expect(mock.response).toBe(true); + }); + + it('should not query for data when kuery is empty', async () => { + mock = await inspectSearchParams( + (setup) => getIsUsingTransactionEvents({ setup, kuery: '' }), + { config } + ); + expect(mock.spy).toHaveBeenCalledTimes(0); + }); + + it('should query for data when kuery is set', async () => { + mock = await inspectSearchParams( + (setup) => + getIsUsingTransactionEvents({ + setup, + kuery: 'proccessor.event: "transaction"', + }), + { config } + ); + expect(mock.spy).toHaveBeenCalledTimes(1); + expect(mock.params).toMatchSnapshot(); + }); + }); + + describe('with config xpack.apm.searchAggregatedTransactions: auto', () => { + const config = { + 'xpack.apm.searchAggregatedTransactions': + SearchAggregatedTransactionSetting.auto, + }; + + it('should query for data once if metrics data found', async () => { + mock = await inspectSearchParams( + (setup) => getIsUsingTransactionEvents({ setup, kuery: '' }), + { + config, + mockResponse: (request) => { + if (request === 'get_has_aggregated_transactions') { + return mockResponseSomeHits; + } + if (request === 'get_has_transactions') { + return mockResponseNoHits; + } + return mockResponseNoHits; + }, + } + ); + expect(mock.spy).toHaveBeenCalledTimes(1); + expect(mock.params).toMatchSnapshot(); + }); + + it('should query for data twice if metrics data not found', async () => { + mock = await inspectSearchParams( + (setup) => getIsUsingTransactionEvents({ setup, kuery: '' }), + { + config, + mockResponse: (request) => { + if (request === 'get_has_aggregated_transactions') { + return mockResponseNoHits; + } + if (request === 'get_has_transactions') { + return mockResponseSomeHits; + } + return mockResponseNoHits; + }, + } + ); + expect(mock.spy).toHaveBeenCalledTimes(2); + expect(mock.spy.mock.calls).toMatchSnapshot(); + }); + + it('should be false if metrics data are found', async () => { + mock = await inspectSearchParams( + (setup) => getIsUsingTransactionEvents({ setup, kuery: '' }), + { + config, + mockResponse: (request) => { + if (request === 'get_has_aggregated_transactions') { + return mockResponseSomeHits; + } + if (request === 'get_has_transactions') { + return mockResponseNoHits; + } + return mockResponseNoHits; + }, + } + ); + expect(mock.response).toBe(false); + }); + + it('should be true if no metrics data are found', async () => { + mock = await inspectSearchParams( + (setup) => getIsUsingTransactionEvents({ setup, kuery: '' }), + { + config, + mockResponse: (request) => { + if (request === 'get_has_aggregated_transactions') { + return mockResponseNoHits; + } + if (request === 'get_has_transactions') { + return mockResponseSomeHits; + } + return mockResponseNoHits; + }, + } + ); + expect(mock.response).toBe(true); + }); + + it('should be false if no metrics or transactions data are found', async () => { + mock = await inspectSearchParams( + (setup) => getIsUsingTransactionEvents({ setup, kuery: '' }), + { config, mockResponse: () => mockResponseNoHits } + ); + expect(mock.response).toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_is_using_transaction_events.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_is_using_transaction_events.ts new file mode 100644 index 000000000000..7660f236d237 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/get_is_using_transaction_events.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getSearchAggregatedTransactions } from '.'; +import { SearchAggregatedTransactionSetting } from '../../../../common/aggregated_transactions'; +import { Setup, SetupTimeRange } from '../setup_request'; +import { kqlQuery, rangeQuery } from '../../../../../observability/server'; +import { ProcessorEvent } from '../../../../common/processor_event'; +import { APMEventClient } from '../create_es_client/create_apm_event_client'; + +export async function getIsUsingTransactionEvents({ + setup: { config, start, end, apmEventClient }, + kuery, +}: { + setup: Setup & Partial; + kuery: string; +}): Promise { + const searchAggregatedTransactions = + config['xpack.apm.searchAggregatedTransactions']; + + if ( + searchAggregatedTransactions === SearchAggregatedTransactionSetting.never + ) { + return false; + } + if ( + !kuery && + searchAggregatedTransactions === SearchAggregatedTransactionSetting.always + ) { + return false; + } + + const searchesAggregatedTransactions = await getSearchAggregatedTransactions({ + config, + start, + end, + apmEventClient, + kuery, + }); + + if (!searchesAggregatedTransactions) { + // if no aggregrated transactions, check if any transactions at all + return await getHasTransactions({ + start, + end, + apmEventClient, + kuery, + }); + } + + return false; +} + +async function getHasTransactions({ + start, + end, + apmEventClient, + kuery, +}: { + start?: number; + end?: number; + apmEventClient: APMEventClient; + kuery: string; +}) { + const response = await apmEventClient.search('get_has_transactions', { + apm: { + events: [ProcessorEvent.transaction], + }, + body: { + query: { + bool: { + filter: [ + ...(start && end ? rangeQuery(start, end) : []), + ...kqlQuery(kuery), + ], + }, + }, + }, + terminateAfter: 1, + }); + + return response.hits.total.value > 0; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts index ba069a8b3feb..478f3218ef38 100644 --- a/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/aggregated_transactions/index.ts @@ -47,11 +47,7 @@ export async function getHasAggregatedTransactions({ } ); - if (response.hits.total.value > 0) { - return true; - } - - return false; + return response.hits.total.value > 0; } export async function getSearchAggregatedTransactions({ diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts index 644416e41b1a..b58a11f637c2 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/call_async_with_debug.ts @@ -7,10 +7,12 @@ /* eslint-disable no-console */ -import { omit } from 'lodash'; import chalk from 'chalk'; import { KibanaRequest } from '../../../../../../../src/core/server'; +import { RequestStatus } from '../../../../../../../src/plugins/inspector'; +import { WrappedElasticsearchClientError } from '../../../../../observability/server'; import { inspectableEsQueriesMap } from '../../../routes/register_routes'; +import { getInspectResponse } from './get_inspect_response'; function formatObj(obj: Record) { return JSON.stringify(obj, null, 2); @@ -39,20 +41,24 @@ export async function callAsyncWithDebug({ return cb(); } - const startTime = process.hrtime(); + const hrStartTime = process.hrtime(); + const startTime = Date.now(); let res: any; - let esError = null; + let esError: WrappedElasticsearchClientError | null = null; + let esRequestStatus: RequestStatus = RequestStatus.PENDING; try { res = await cb(); + esRequestStatus = RequestStatus.OK; } catch (e) { // catch error and throw after outputting debug info esError = e; + esRequestStatus = RequestStatus.ERROR; } if (debug) { const highlightColor = esError ? 'bgRed' : 'inverse'; - const diff = process.hrtime(startTime); + const diff = process.hrtime(hrStartTime); const duration = Math.round(diff[0] * 1000 + diff[1] / 1e6); // duration in ms const { title, body } = getDebugMessage(); @@ -66,14 +72,17 @@ export async function callAsyncWithDebug({ const inspectableEsQueries = inspectableEsQueriesMap.get(request); if (!isCalledWithInternalUser && inspectableEsQueries) { - inspectableEsQueries.push({ - operationName, - response: res, - duration, - requestType, - requestParams: omit(requestParams, 'headers'), - esError: esError?.response ?? esError?.message, - }); + inspectableEsQueries.push( + getInspectResponse({ + esError, + esRequestParams: requestParams, + esRequestStatus, + esResponse: res, + kibanaRequest: request, + operationName, + startTime, + }) + ); } } diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index 7b5425321d23..2f2435c21dc1 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -95,6 +95,7 @@ export function createApmEventClient({ ...withPossibleLegacyDataFilter, ignore_throttled: !includeFrozen, ignore_unavailable: true, + preference: 'any', }; // only "search" operation is currently supported diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/get_inspect_response.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/get_inspect_response.ts new file mode 100644 index 000000000000..ae91daf9d2e0 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/get_inspect_response.ts @@ -0,0 +1,171 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import type { KibanaRequest } from '../../../../../../../src/core/server'; +import type { + RequestStatistics, + RequestStatus, +} from '../../../../../../../src/plugins/inspector'; +import { WrappedElasticsearchClientError } from '../../../../../observability/server'; +import type { InspectResponse } from '../../../../typings/common'; + +/** + * Get statistics to show on inspector tab. + * + * If you're using searchSource (which we're not), this gets populated from + * https://github.com/elastic/kibana/blob/c7d742cb8b8935f3812707a747a139806e4be203/src/plugins/data/common/search/search_source/inspect/inspector_stats.ts + * + * We do most of the same here, but not using searchSource. + */ +function getStats({ + esRequestParams, + esResponse, + kibanaRequest, +}: { + esRequestParams: Record; + esResponse: any; + kibanaRequest: KibanaRequest; +}) { + const stats: RequestStatistics = { + kibanaApiQueryParameters: { + label: i18n.translate( + 'xpack.apm.inspector.stats.kibanaApiQueryParametersLabel', + { + defaultMessage: 'Kibana API query parameters', + } + ), + description: i18n.translate( + 'xpack.apm.inspector.stats.kibanaApiQueryParametersDescription', + { + defaultMessage: + 'The query parameters used in the Kibana API request that initiated the Elasticsearch request.', + } + ), + value: JSON.stringify(kibanaRequest.query, null, 2), + }, + kibanaApiRoute: { + label: i18n.translate('xpack.apm.inspector.stats.kibanaApiRouteLabel', { + defaultMessage: 'Kibana API route', + }), + description: i18n.translate( + 'xpack.apm.inspector.stats.kibanaApiRouteDescription', + { + defaultMessage: + 'The route of the Kibana API request that initiated the Elasticsearch request.', + } + ), + value: `${kibanaRequest.route.method.toUpperCase()} ${ + kibanaRequest.route.path + }`, + }, + indexPattern: { + label: i18n.translate('xpack.apm.inspector.stats.indexPatternLabel', { + defaultMessage: 'Index pattern', + }), + value: esRequestParams.index, + description: i18n.translate( + 'xpack.apm.inspector.stats.indexPatternDescription', + { + defaultMessage: + 'The index pattern that connected to the Elasticsearch indices.', + } + ), + }, + }; + + if (esResponse?.hits) { + stats.hits = { + label: i18n.translate('xpack.apm.inspector.stats.hitsLabel', { + defaultMessage: 'Hits', + }), + value: `${esResponse.hits.hits.length}`, + description: i18n.translate('xpack.apm.inspector.stats.hitsDescription', { + defaultMessage: 'The number of documents returned by the query.', + }), + }; + } + + if (esResponse?.took) { + stats.queryTime = { + label: i18n.translate('xpack.apm.inspector.stats.queryTimeLabel', { + defaultMessage: 'Query time', + }), + value: i18n.translate('xpack.apm.inspector.stats.queryTimeValue', { + defaultMessage: '{queryTime}ms', + values: { queryTime: esResponse.took }, + }), + description: i18n.translate( + 'xpack.apm.inspector.stats.queryTimeDescription', + { + defaultMessage: + 'The time it took to process the query. ' + + 'Does not include the time to send the request or parse it in the browser.', + } + ), + }; + } + + if (esResponse?.hits?.total !== undefined) { + const total = esResponse.hits.total as { + relation: string; + value: number; + }; + const hitsTotalValue = + total.relation === 'eq' ? `${total.value}` : `> ${total.value}`; + + stats.hitsTotal = { + label: i18n.translate('xpack.apm.inspector.stats.hitsTotalLabel', { + defaultMessage: 'Hits (total)', + }), + value: hitsTotalValue, + description: i18n.translate( + 'xpack.apm.inspector.stats.hitsTotalDescription', + { + defaultMessage: 'The number of documents that match the query.', + } + ), + }; + } + return stats; +} + +/** + * Create a formatted response to be sent in the _inspect key for use in the + * inspector. + */ +export function getInspectResponse({ + esError, + esRequestParams, + esRequestStatus, + esResponse, + kibanaRequest, + operationName, + startTime, +}: { + esError: WrappedElasticsearchClientError | null; + esRequestParams: Record; + esRequestStatus: RequestStatus; + esResponse: any; + kibanaRequest: KibanaRequest; + operationName: string; + startTime: number; +}): InspectResponse[0] { + const id = `${operationName} (${kibanaRequest.route.path})`; + + return { + id, + json: esRequestParams.body, + name: id, + response: { + json: esError ? esError.originalError : esResponse, + }, + startTime, + stats: getStats({ esRequestParams, esResponse, kibanaRequest }), + status: esRequestStatus, + }; +} diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts index 66b3c91fc6f2..00f3f15786ad 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.test.ts @@ -131,6 +131,7 @@ describe('setupRequest', () => { }, ignore_unavailable: true, ignore_throttled: true, + preference: 'any', }); }); diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index ba67a42fbbad..d2527b4a25d1 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -5,12 +5,10 @@ * 2.0. */ -import { Logger } from 'kibana/server'; import { isActivePlatinumLicense } from '../../../common/license_check'; import { APMConfig } from '../..'; import { KibanaRequest } from '../../../../../../src/core/server'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; -import { UxUIFilters } from '../../../typings/ui_filters'; import { APMRouteHandlerResources } from '../../routes/typings'; import { ApmIndicesConfig, @@ -35,7 +33,6 @@ export interface Setup { ml?: ReturnType; config: APMConfig; indices: ApmIndicesConfig; - uiFilters: UxUIFilters; } export interface SetupTimeRange { @@ -43,7 +40,7 @@ export interface SetupTimeRange { end: number; } -interface SetupRequestParams { +export interface SetupRequestParams { query: { _inspect?: boolean; @@ -56,7 +53,6 @@ interface SetupRequestParams { * Timestamp in ms since epoch */ end?: number; - uiFilters?: string; }; } @@ -74,7 +70,6 @@ export async function setupRequest({ plugins, request, config, - logger, }: APMRouteHandlerResources & { params: TParams; }): Promise> { @@ -91,8 +86,6 @@ export async function setupRequest({ ), ]); - const uiFilters = decodeUiFilters(logger, query.uiFilters); - const coreSetupRequest = { indices, apmEventClient: createApmEventClient({ @@ -116,7 +109,6 @@ export async function setupRequest({ ) : undefined, config, - uiFilters, }; return { @@ -138,18 +130,3 @@ function getMlSetup( modules: ml.modulesProvider(request, savedObjectsClient), }; } - -function decodeUiFilters( - logger: Logger, - uiFiltersEncoded?: string -): UxUIFilters { - if (!uiFiltersEncoded) { - return {}; - } - try { - return JSON.parse(uiFiltersEncoded); - } catch (error) { - logger.error(error); - return {}; - } -} diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts index e56f234c0633..60f0eaff28e8 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_client_metrics.ts @@ -7,7 +7,8 @@ import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { SetupTimeRange } from '../helpers/setup_request'; +import { SetupUX } from '../../routes/rum_client'; import { TRANSACTION_TIME_TO_FIRST_BYTE, TRANSACTION_DURATION, @@ -18,7 +19,7 @@ export async function getClientMetrics({ urlQuery, percentile = 50, }: { - setup: Setup & SetupTimeRange; + setup: SetupUX & SetupTimeRange; urlQuery?: string; percentile?: number; }) { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts b/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts index 6f734a214501..6967dd3d1660 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_js_errors.ts @@ -6,7 +6,8 @@ */ import { mergeProjection } from '../../projections/util/merge_projection'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { SetupTimeRange } from '../helpers/setup_request'; +import { SetupUX } from '../../routes/rum_client'; import { getRumErrorsProjection } from '../../projections/rum_page_load_transactions'; import { ERROR_EXC_MESSAGE, @@ -23,7 +24,7 @@ export async function getJSErrors({ pageIndex, urlQuery, }: { - setup: Setup & SetupTimeRange; + setup: SetupUX & SetupTimeRange; pageSize: number; pageIndex: number; urlQuery?: string; 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 c4c6f613172d..751534272bd7 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 @@ -7,7 +7,8 @@ import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { SetupTimeRange } from '../helpers/setup_request'; +import { SetupUX } from '../../routes/rum_client'; const LONG_TASK_SUM_FIELD = 'transaction.experience.longtask.sum'; const LONG_TASK_COUNT_FIELD = 'transaction.experience.longtask.count'; @@ -18,7 +19,7 @@ export async function getLongTaskMetrics({ urlQuery, percentile = 50, }: { - setup: Setup & SetupTimeRange; + setup: SetupUX & SetupTimeRange; urlQuery?: string; percentile?: number; }) { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts index 73d634e3134d..92b6eea76ab5 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_load_distribution.ts @@ -8,7 +8,8 @@ import { TRANSACTION_DURATION } from '../../../common/elasticsearch_fieldnames'; import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { SetupTimeRange } from '../helpers/setup_request'; +import { SetupUX } from '../../routes/rum_client'; export const MICRO_TO_SEC = 1000000; @@ -64,7 +65,7 @@ export async function getPageLoadDistribution({ maxPercentile, urlQuery, }: { - setup: Setup & SetupTimeRange; + setup: SetupUX & SetupTimeRange; minPercentile?: string; maxPercentile?: string; urlQuery?: string; @@ -176,7 +177,7 @@ const getPercentilesDistribution = async ({ minDuration, maxDuration, }: { - setup: Setup & SetupTimeRange; + setup: SetupUX & SetupTimeRange; minDuration: number; maxDuration: number; }) => { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts index 41af2ae166aa..3eae873f0391 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_page_view_trends.ts @@ -7,7 +7,8 @@ import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { SetupTimeRange } from '../helpers/setup_request'; +import { SetupUX } from '../../routes/rum_client'; import { BreakdownItem } from '../../../typings/ui_filters'; export async function getPageViewTrends({ @@ -15,7 +16,7 @@ export async function getPageViewTrends({ breakdowns, urlQuery, }: { - setup: Setup & SetupTimeRange; + setup: SetupUX & SetupTimeRange; breakdowns?: string; urlQuery?: string; }) { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts index e63d834307a5..b26c76d3fe67 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_pl_dist_breakdown.ts @@ -8,7 +8,8 @@ import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { ProcessorEvent } from '../../../common/processor_event'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { SetupTimeRange } from '../helpers/setup_request'; +import { SetupUX } from '../../routes/rum_client'; import { CLIENT_GEO_COUNTRY_ISO_CODE, USER_AGENT_DEVICE, @@ -44,7 +45,7 @@ export const getPageLoadDistBreakdown = async ({ breakdown, urlQuery, }: { - setup: Setup & SetupTimeRange; + setup: SetupUX & SetupTimeRange; minPercentile: number; maxPercentile: number; breakdown: string; diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts index a2e6b55738d3..42766c286035 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_rum_services.ts @@ -4,16 +4,16 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import { SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { SetupTimeRange } from '../helpers/setup_request'; +import { SetupUX } from '../../routes/rum_client'; import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { mergeProjection } from '../../projections/util/merge_projection'; export async function getRumServices({ setup, }: { - setup: Setup & SetupTimeRange; + setup: SetupUX & SetupTimeRange; }) { const projection = getRumPageLoadTransactionsProjection({ setup, diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts b/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts index ae65cdbd121e..6d5d501ccb6b 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_url_search.ts @@ -6,7 +6,8 @@ */ import { mergeProjection } from '../../projections/util/merge_projection'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { SetupTimeRange } from '../helpers/setup_request'; +import { SetupUX } from '../../routes/rum_client'; import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { TRANSACTION_DURATION, @@ -18,7 +19,7 @@ export async function getUrlSearch({ urlQuery, percentile, }: { - setup: Setup & SetupTimeRange; + setup: SetupUX & SetupTimeRange; urlQuery?: string; percentile: number; }) { diff --git a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts index 9c7a64d7c648..eab2ddaeb1cc 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/get_visitor_breakdown.ts @@ -7,7 +7,8 @@ import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { SetupTimeRange } from '../helpers/setup_request'; +import { SetupUX } from '../../routes/rum_client'; import { USER_AGENT_NAME, USER_AGENT_OS, @@ -17,7 +18,7 @@ export async function getVisitorBreakdown({ setup, urlQuery, }: { - setup: Setup & SetupTimeRange; + setup: SetupUX & SetupTimeRange; urlQuery?: string; }) { const projection = getRumPageLoadTransactionsProjection({ 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 bbb301e22aa8..c9046e01cdeb 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 @@ -7,7 +7,8 @@ import { getRumPageLoadTransactionsProjection } from '../../projections/rum_page_load_transactions'; import { mergeProjection } from '../../projections/util/merge_projection'; -import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { SetupTimeRange } from '../helpers/setup_request'; +import { SetupUX } from '../../routes/rum_client'; import { CLS_FIELD, FCP_FIELD, @@ -21,7 +22,7 @@ export async function getWebCoreVitals({ urlQuery, percentile = 50, }: { - setup: Setup & SetupTimeRange; + setup: SetupUX & SetupTimeRange; urlQuery?: string; percentile?: number; }) { diff --git a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts index 28fab3369b1e..1d4fa3c31a26 100644 --- a/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts +++ b/x-pack/plugins/apm/server/lib/rum_client/has_rum_data.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { Setup, SetupTimeRange } from '../helpers/setup_request'; +import { SetupTimeRange } from '../helpers/setup_request'; +import { SetupUX } from '../../routes/rum_client'; import { SERVICE_NAME, TRANSACTION_TYPE, @@ -17,7 +18,7 @@ import { TRANSACTION_PAGE_LOAD } from '../../../common/transaction_types'; export async function hasRumData({ setup, }: { - setup: Setup & Partial; + setup: SetupUX & Partial; }) { try { const { start, end } = setup; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts b/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts index b6621d590b17..90be97d9497b 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts @@ -14,7 +14,6 @@ import { SERVICE_NAME, CONTAINER_ID, HOSTNAME, - POD_NAME, } from '../../../common/elasticsearch_fieldnames'; export const getServiceInfrastructure = async ({ @@ -61,12 +60,6 @@ export const getServiceInfrastructure = async ({ size: 500, }, }, - podNames: { - terms: { - field: POD_NAME, - size: 500, - }, - }, }, }, }); @@ -74,13 +67,11 @@ export const getServiceInfrastructure = async ({ return { containerIds: response.aggregations?.containerIds?.buckets.map( - (bucket) => bucket.key + (bucket) => bucket.key as string ) ?? [], hostNames: - response.aggregations?.hostNames?.buckets.map((bucket) => bucket.key) ?? - [], - podNames: - response.aggregations?.podNames?.buckets.map((bucket) => bucket.key) ?? - [], + response.aggregations?.hostNames?.buckets.map( + (bucket) => bucket.key as string + ) ?? [], }; }; diff --git a/x-pack/plugins/apm/server/lib/services/get_throughput.ts b/x-pack/plugins/apm/server/lib/services/get_throughput.ts index e866918fc29b..76d6000a161e 100644 --- a/x-pack/plugins/apm/server/lib/services/get_throughput.ts +++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts @@ -8,6 +8,7 @@ import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; import { SERVICE_NAME, + TRANSACTION_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; import { kqlQuery, rangeQuery } from '../../../../observability/server'; @@ -25,6 +26,7 @@ interface Options { serviceName: string; setup: Setup; transactionType: string; + transactionName?: string; start: number; end: number; intervalString: string; @@ -38,6 +40,7 @@ export async function getThroughput({ serviceName, setup, transactionType, + transactionName, start, end, intervalString, @@ -56,6 +59,14 @@ export async function getThroughput({ ...kqlQuery(kuery), ]; + if (transactionName) { + filter.push({ + term: { + [TRANSACTION_NAME]: transactionName, + }, + }); + } + const params = { apm: { events: [ diff --git a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts index b0a9b27ba771..73294a0bd123 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/index.ts @@ -22,6 +22,7 @@ export async function getAnomalySeries({ serviceName, transactionType, transactionName, + kuery, setup, logger, }: { @@ -29,6 +30,7 @@ export async function getAnomalySeries({ serviceName: string; transactionType: string; transactionName?: string; + kuery: string; setup: Setup & SetupTimeRange; logger: Logger; }) { @@ -50,13 +52,8 @@ export async function getAnomalySeries({ return undefined; } - // Don't fetch anomalies if uiFilters are applied. This filters out anything - // with empty values so `kuery: ''` returns false but `kuery: 'x:y'` returns true. - const hasUiFiltersApplied = - Object.entries(setup.uiFilters).filter(([_key, value]) => !!value).length > - 0; - - if (hasUiFiltersApplied) { + // Don't fetch anomalies if kuery is present + if (kuery) { return undefined; } diff --git a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts index b8cf92f15c70..ac46a6df08e8 100644 --- a/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts +++ b/x-pack/plugins/apm/server/projections/rum_page_load_transactions.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { Setup, SetupTimeRange } from '../../server/lib/helpers/setup_request'; +import { SetupTimeRange } from '../../server/lib/helpers/setup_request'; +import { SetupUX } from '../routes/rum_client'; import { AGENT_NAME, TRANSACTION_TYPE, @@ -21,7 +22,7 @@ export function getRumPageLoadTransactionsProjection({ urlQuery, checkFetchStartFieldExists = true, }: { - setup: Setup & SetupTimeRange; + setup: SetupUX & SetupTimeRange; urlQuery?: string; checkFetchStartFieldExists?: boolean; }) { @@ -72,7 +73,7 @@ export function getRumErrorsProjection({ setup, urlQuery, }: { - setup: Setup & SetupTimeRange; + setup: SetupUX & SetupTimeRange; urlQuery?: string; }) { const { start, end, uiFilters } = setup; diff --git a/x-pack/plugins/apm/server/routes/fallback_to_transactions.ts b/x-pack/plugins/apm/server/routes/fallback_to_transactions.ts index 3011d0c3864e..2c89893ea87f 100644 --- a/x-pack/plugins/apm/server/routes/fallback_to_transactions.ts +++ b/x-pack/plugins/apm/server/routes/fallback_to_transactions.ts @@ -6,7 +6,7 @@ */ import * as t from 'io-ts'; -import { getFallbackToTransactions } from '../lib/helpers/aggregated_transactions/get_fallback_to_transactions'; +import { getIsUsingTransactionEvents } from '../lib/helpers/aggregated_transactions/get_is_using_transaction_events'; import { setupRequest } from '../lib/helpers/setup_request'; import { createApmServerRoute } from './create_apm_server_route'; import { createApmServerRouteRepository } from './create_apm_server_route_repository'; @@ -26,7 +26,10 @@ const fallbackToTransactionsRoute = createApmServerRoute({ }, } = resources; return { - fallbackToTransactions: await getFallbackToTransactions({ setup, kuery }), + fallbackToTransactions: await getIsUsingTransactionEvents({ + setup, + kuery, + }), }; }, }); diff --git a/x-pack/plugins/apm/server/routes/register_routes/index.ts b/x-pack/plugins/apm/server/routes/register_routes/index.ts index 16e77f59f4d0..c66048948550 100644 --- a/x-pack/plugins/apm/server/routes/register_routes/index.ts +++ b/x-pack/plugins/apm/server/routes/register_routes/index.ts @@ -19,12 +19,9 @@ import { } from '@kbn/server-route-repository'; import { mergeRt, jsonRt } from '@kbn/io-ts-utils'; import { pickKeys } from '../../../common/utils/pick_keys'; -import { - APMRouteHandlerResources, - InspectResponse, - TelemetryUsageCounter, -} from '../typings'; +import { APMRouteHandlerResources, TelemetryUsageCounter } from '../typings'; import type { ApmPluginRequestHandlerContext } from '../typings'; +import { InspectResponse } from '../../../typings/common'; const inspectRt = t.exact( t.partial({ diff --git a/x-pack/plugins/apm/server/routes/rum_client.ts b/x-pack/plugins/apm/server/routes/rum_client.ts index c43917e32b7e..f6f4aca3a1d9 100644 --- a/x-pack/plugins/apm/server/routes/rum_client.ts +++ b/x-pack/plugins/apm/server/routes/rum_client.ts @@ -4,10 +4,14 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - import * as t from 'io-ts'; +import { Logger } from 'kibana/server'; import { isoToEpochRt } from '@kbn/io-ts-utils'; -import { setupRequest } from '../lib/helpers/setup_request'; +import { + setupRequest, + Setup, + SetupRequestParams, +} from '../lib/helpers/setup_request'; import { getClientMetrics } from '../lib/rum_client/get_client_metrics'; import { getJSErrors } from '../lib/rum_client/get_js_errors'; import { getLongTaskMetrics } from '../lib/rum_client/get_long_task_metrics'; @@ -22,6 +26,18 @@ import { hasRumData } from '../lib/rum_client/has_rum_data'; import { createApmServerRoute } from './create_apm_server_route'; import { createApmServerRouteRepository } from './create_apm_server_route_repository'; import { rangeRt } from './default_api_types'; +import { UxUIFilters } from '../../typings/ui_filters'; +import { APMRouteHandlerResources } from '../routes/typings'; + +export type SetupUX = Setup & { + uiFilters: UxUIFilters; +}; + +type SetupUXRequestParams = Omit & { + query: SetupRequestParams['query'] & { + uiFilters?: string; + }; +}; export const percentileRangeRt = t.partial({ minPercentile: t.string, @@ -43,7 +59,7 @@ const rumClientMetricsRoute = createApmServerRoute({ }), options: { tags: ['access:apm'] }, handler: async (resources) => { - const setup = await setupRequest(resources); + const setup = await setupUXRequest(resources); const { query: { urlQuery, percentile }, @@ -64,7 +80,7 @@ const rumPageLoadDistributionRoute = createApmServerRoute({ }), options: { tags: ['access:apm'] }, handler: async (resources) => { - const setup = await setupRequest(resources); + const setup = await setupUXRequest(resources); const { query: { minPercentile, maxPercentile, urlQuery }, @@ -92,7 +108,7 @@ const rumPageLoadDistBreakdownRoute = createApmServerRoute({ }), options: { tags: ['access:apm'] }, handler: async (resources) => { - const setup = await setupRequest(resources); + const setup = await setupUXRequest(resources); const { query: { minPercentile, maxPercentile, breakdown, urlQuery }, @@ -117,7 +133,7 @@ const rumPageViewsTrendRoute = createApmServerRoute({ }), options: { tags: ['access:apm'] }, handler: async (resources) => { - const setup = await setupRequest(resources); + const setup = await setupUXRequest(resources); const { query: { breakdowns, urlQuery }, @@ -138,7 +154,7 @@ const rumServicesRoute = createApmServerRoute({ }), options: { tags: ['access:apm'] }, handler: async (resources) => { - const setup = await setupRequest(resources); + const setup = await setupUXRequest(resources); const rumServices = await getRumServices({ setup }); return { rumServices }; @@ -152,7 +168,7 @@ const rumVisitorsBreakdownRoute = createApmServerRoute({ }), options: { tags: ['access:apm'] }, handler: async (resources) => { - const setup = await setupRequest(resources); + const setup = await setupUXRequest(resources); const { query: { urlQuery }, @@ -172,7 +188,7 @@ const rumWebCoreVitals = createApmServerRoute({ }), options: { tags: ['access:apm'] }, handler: async (resources) => { - const setup = await setupRequest(resources); + const setup = await setupUXRequest(resources); const { query: { urlQuery, percentile }, @@ -193,7 +209,7 @@ const rumLongTaskMetrics = createApmServerRoute({ }), options: { tags: ['access:apm'] }, handler: async (resources) => { - const setup = await setupRequest(resources); + const setup = await setupUXRequest(resources); const { query: { urlQuery, percentile }, @@ -214,7 +230,7 @@ const rumUrlSearch = createApmServerRoute({ }), options: { tags: ['access:apm'] }, handler: async (resources) => { - const setup = await setupRequest(resources); + const setup = await setupUXRequest(resources); const { query: { urlQuery, percentile }, @@ -236,7 +252,7 @@ const rumJSErrors = createApmServerRoute({ }), options: { tags: ['access:apm'] }, handler: async (resources) => { - const setup = await setupRequest(resources); + const setup = await setupUXRequest(resources); const { query: { pageSize, pageIndex, urlQuery }, @@ -262,11 +278,39 @@ const rumHasDataRoute = createApmServerRoute({ }), options: { tags: ['access:apm'] }, handler: async (resources) => { - const setup = await setupRequest(resources); + const setup = await setupUXRequest(resources); return await hasRumData({ setup }); }, }); +function decodeUiFilters( + logger: Logger, + uiFiltersEncoded?: string +): UxUIFilters { + if (!uiFiltersEncoded) { + return {}; + } + try { + return JSON.parse(uiFiltersEncoded); + } catch (error) { + logger.error(error); + return {}; + } +} + +async function setupUXRequest( + resources: APMRouteHandlerResources & { params: TParams } +) { + const setup = await setupRequest(resources); + return { + ...setup, + uiFilters: decodeUiFilters( + resources.logger, + resources.params.query.uiFilters + ), + }; +} + export const rumRouteRepository = createApmServerRouteRepository() .add(rumClientMetricsRoute) .add(rumPageLoadDistributionRoute) diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 32a7dcefb5cc..550781cc1a02 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -451,10 +451,8 @@ const serviceThroughputRoute = createApmServerRoute({ }), query: t.intersection([ t.type({ transactionType: t.string }), - environmentRt, - kueryRt, - rangeRt, - comparisonRangeRt, + t.partial({ transactionName: t.string }), + t.intersection([environmentRt, kueryRt, rangeRt, comparisonRangeRt]), ]), }), options: { tags: ['access:apm'] }, @@ -466,6 +464,7 @@ const serviceThroughputRoute = createApmServerRoute({ environment, kuery, transactionType, + transactionName, comparisonStart, comparisonEnd, } = params.query; @@ -493,6 +492,7 @@ const serviceThroughputRoute = createApmServerRoute({ serviceName, setup, transactionType, + transactionName, throughputUnit, intervalString, }; diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 76f19a6a0ca3..6cb43fe64ba7 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -26,15 +26,6 @@ export interface ApmPluginRequestHandlerContext extends RequestHandlerContext { rac: RacApiRequestHandlerContext; } -export type InspectResponse = Array<{ - response: any; - duration: number; - requestType: string; - requestParams: Record; - esError: Error; - operationName: string; -}>; - export interface APMRouteCreateOptions { options: { tags: Array< diff --git a/x-pack/plugins/apm/server/utils/test_helpers.tsx b/x-pack/plugins/apm/server/utils/test_helpers.tsx index 1d0a47ece9a6..e4fabe815d57 100644 --- a/x-pack/plugins/apm/server/utils/test_helpers.tsx +++ b/x-pack/plugins/apm/server/utils/test_helpers.tsx @@ -18,6 +18,7 @@ interface Options { request: ESSearchRequest ) => ESSearchResponse; uiFilters?: Record; + config?: Partial; } interface MockSetup { @@ -70,7 +71,12 @@ export async function inspectSearchParams( config: new Proxy( {}, { - get: (_, key) => { + get: (_, key: keyof APMConfig) => { + const { config } = options; + if (config?.[key]) { + return config?.[key]; + } + switch (key) { default: return 'myIndex'; @@ -110,7 +116,7 @@ export async function inspectSearchParams( } return { - params: spy.mock.calls[0][1], + params: spy.mock.calls[0]?.[1], response, error, spy, diff --git a/x-pack/plugins/apm/tsconfig.json b/x-pack/plugins/apm/tsconfig.json index 6eaf1a3bf183..c1030d2a4be1 100644 --- a/x-pack/plugins/apm/tsconfig.json +++ b/x-pack/plugins/apm/tsconfig.json @@ -24,6 +24,7 @@ { "path": "../../../src/plugins/embeddable/tsconfig.json" }, { "path": "../../../src/plugins/home/tsconfig.json" }, { "path": "../../../src/plugins/index_pattern_management/tsconfig.json" }, + { "path": "../../../src/plugins/inspector/tsconfig.json" }, { "path": "../../../src/plugins/kibana_react/tsconfig.json" }, { "path": "../../../src/plugins/kibana_utils/tsconfig.json" }, { "path": "../../../src/plugins/usage_collection/tsconfig.json" }, diff --git a/x-pack/plugins/apm/typings/common.d.ts b/x-pack/plugins/apm/typings/common.d.ts index b94eb6cd97b0..4c0b8520924b 100644 --- a/x-pack/plugins/apm/typings/common.d.ts +++ b/x-pack/plugins/apm/typings/common.d.ts @@ -6,6 +6,7 @@ */ import type { UnwrapPromise } from '@kbn/utility-types'; +import type { Request } from '../../../../src/plugins/inspector/common'; import '../../../typings/rison_node'; import '../../infra/types/eui'; // EUIBasicTable @@ -27,3 +28,5 @@ type AllowUnknownObjectProperties = T extends object export type PromiseValueType> = UnwrapPromise; export type Maybe = T | null | undefined; + +export type InspectResponse = Request[]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts index 76383007adb7..ac2e8e8babee 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts @@ -6,7 +6,7 @@ */ import { ExpressionTypeDefinition } from '../../../../../src/plugins/expressions'; -import { EmbeddableInput } from '../../../../../src/plugins/embeddable/public'; +import { EmbeddableInput } from '../../../../../src/plugins/embeddable/common/'; import { EmbeddableTypes } from './embeddable_types'; export const EmbeddableExpressionType = 'embeddable'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts index 1e340567825b..78fc82393994 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable_types.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { MAP_SAVED_OBJECT_TYPE } from '../../../../plugins/maps/public'; -import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../src/plugins/visualizations/public'; +import { MAP_SAVED_OBJECT_TYPE } from '../../../../plugins/maps/common/constants'; +import { VISUALIZE_EMBEDDABLE_TYPE } from '../../../../../src/plugins/visualizations/common/constants'; import { LENS_EMBEDDABLE_TYPE } from '../../../../plugins/lens/common/constants'; -import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../src/plugins/discover/public'; +import { SEARCH_EMBEDDABLE_TYPE } from '../../../../../src/plugins/discover/common'; export const EmbeddableTypes: { lens: string; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts index fd5765ce529b..407a0e2ebfe0 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts @@ -10,4 +10,4 @@ import { savedMap } from './saved_map'; import { savedSearch } from './saved_search'; import { savedVisualization } from './saved_visualization'; -export const functions = [savedLens, savedMap, savedSearch, savedVisualization]; +export const functions = [savedLens, savedMap, savedVisualization, savedSearch]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts index 55ad7fd4d3ce..e3e6afae8bd6 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.test.ts @@ -6,7 +6,7 @@ */ import { savedLens } from './saved_lens'; -import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; const filterContext: ExpressionValueFilter = { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts index 3ffa20de55aa..bd844dd3335e 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts @@ -7,9 +7,10 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { PaletteOutput } from 'src/plugins/charts/common'; -import { TimeRange, Filter as DataFilter } from 'src/plugins/data/public'; -import { EmbeddableInput } from 'src/plugins/embeddable/public'; -import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { Filter as DataFilter } from '@kbn/es-query'; +import { TimeRange } from 'src/plugins/data/common'; +import { EmbeddableInput } from 'src/plugins/embeddable/common'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter, TimeRange as TimeRangeArg } from '../../../types'; import { EmbeddableTypes, @@ -18,7 +19,6 @@ import { } from '../../expression_types'; import { getFunctionHelp } from '../../../i18n'; import { SavedObjectReference } from '../../../../../../src/core/types'; - interface Arguments { id: string; title: string | null; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts index 02c0af298452..88acf0deabb3 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.test.ts @@ -6,7 +6,7 @@ */ import { savedMap } from './saved_map'; -import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; const filterContext: ExpressionValueFilter = { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts index 395c6e112f75..538ed3f91982 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts @@ -6,7 +6,7 @@ */ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; -import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter, MapCenter, TimeRange as TimeRangeArg } from '../../../types'; import { EmbeddableTypes, @@ -14,7 +14,7 @@ import { EmbeddableExpression, } from '../../expression_types'; import { getFunctionHelp } from '../../../i18n'; -import { MapEmbeddableInput } from '../../../../../plugins/maps/public/embeddable'; +import { MapEmbeddableInput } from '../../../../../plugins/maps/public'; import { SavedObjectReference } from '../../../../../../src/core/types'; interface Arguments { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts index f7cf8c47803a..da0ffcd8f388 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.test.ts @@ -6,7 +6,7 @@ */ import { savedSearch } from './saved_search'; -import { buildEmbeddableFilters } from '../../../public/lib/build_embeddable_filters'; +import { buildEmbeddableFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; const filterContext: ExpressionValueFilter = { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.ts index 8e3ec9dc9e18..ebe9a275c44a 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_search.ts @@ -13,7 +13,7 @@ import { EmbeddableExpression, } from '../../expression_types'; -import { buildEmbeddableFilters } from '../../../public/lib/build_embeddable_filters'; +import { buildEmbeddableFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; import { SavedObjectReference } from '../../../../../../src/core/types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts index 3b1d431f2d64..52c452e61bd5 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.test.ts @@ -6,7 +6,7 @@ */ import { savedVisualization } from './saved_visualization'; -import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter } from '../../../types'; const filterContext: ExpressionValueFilter = { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts index 92ddf6420f0e..5c0442b43250 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts @@ -12,7 +12,7 @@ import { EmbeddableExpressionType, EmbeddableExpression, } from '../../expression_types'; -import { getQueryFilters } from '../../../public/lib/build_embeddable_filters'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; import { ExpressionValueFilter, TimeRange as TimeRangeArg, SeriesStyle } from '../../../types'; import { getFunctionHelp } from '../../../i18n'; import { SavedObjectReference } from '../../../../../../src/core/types'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts index a30b3bf9b212..91c573fc4148 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts @@ -15,6 +15,7 @@ import { Start as InspectorStart } from '../../../../src/plugins/inspector/publi import { functions } from './functions/browser'; import { typeFunctions } from './expression_types'; import { renderFunctions, renderFunctionFactories } from './renderers'; + interface SetupDeps { canvas: CanvasSetup; } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx index de90bc8c7cbe..d956c6291c60 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/filters/dropdown_filter/index.tsx @@ -47,7 +47,7 @@ export const dropdownFilter: RendererFactory = () => ({ render(domNode, config, handlers) { let filterExpression = handlers.getFilter(); - if (filterExpression === undefined || filterExpression.indexOf('exactly')) { + if (filterExpression === undefined || !filterExpression.includes('exactly')) { filterExpression = ''; handlers.setFilter(filterExpression); } else if (filterExpression !== '') { diff --git a/x-pack/plugins/canvas/public/lib/build_bool_array.js b/x-pack/plugins/canvas/common/lib/build_bool_array.js similarity index 100% rename from x-pack/plugins/canvas/public/lib/build_bool_array.js rename to x-pack/plugins/canvas/common/lib/build_bool_array.js diff --git a/x-pack/plugins/canvas/public/lib/build_embeddable_filters.test.ts b/x-pack/plugins/canvas/common/lib/build_embeddable_filters.test.ts similarity index 100% rename from x-pack/plugins/canvas/public/lib/build_embeddable_filters.test.ts rename to x-pack/plugins/canvas/common/lib/build_embeddable_filters.test.ts diff --git a/x-pack/plugins/canvas/public/lib/build_embeddable_filters.ts b/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts similarity index 95% rename from x-pack/plugins/canvas/public/lib/build_embeddable_filters.ts rename to x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts index 9dc935771925..57fdc7d7309c 100644 --- a/x-pack/plugins/canvas/public/lib/build_embeddable_filters.ts +++ b/x-pack/plugins/canvas/common/lib/build_embeddable_filters.ts @@ -9,7 +9,7 @@ import { buildQueryFilter, Filter } from '@kbn/es-query'; import { ExpressionValueFilter } from '../../types'; // @ts-expect-error untyped local import { buildBoolArray } from './build_bool_array'; -import { TimeRange } from '../../../../../src/plugins/data/public'; +import { TimeRange } from '../../../../../src/plugins/data/common'; export interface EmbeddableFilterInput { filters: Filter[]; diff --git a/x-pack/plugins/canvas/public/lib/filters.js b/x-pack/plugins/canvas/common/lib/filters.js similarity index 100% rename from x-pack/plugins/canvas/public/lib/filters.js rename to x-pack/plugins/canvas/common/lib/filters.js diff --git a/x-pack/plugins/canvas/public/lib/get_es_filter.js b/x-pack/plugins/canvas/common/lib/get_es_filter.js similarity index 100% rename from x-pack/plugins/canvas/public/lib/get_es_filter.js rename to x-pack/plugins/canvas/common/lib/get_es_filter.js diff --git a/x-pack/plugins/canvas/public/components/datatable/datatable.tsx b/x-pack/plugins/canvas/public/components/datatable/datatable.tsx index db21379dfed9..5668c93c28b2 100644 --- a/x-pack/plugins/canvas/public/components/datatable/datatable.tsx +++ b/x-pack/plugins/canvas/public/components/datatable/datatable.tsx @@ -40,6 +40,8 @@ const getIcon = (type: DatatableColumnType | null) => { const getColumnName = (col: DatatableColumn) => (typeof col === 'string' ? col : col.name); +const getColumnId = (col: DatatableColumn) => (typeof col === 'string' ? col : col.id); + const getColumnType = (col: DatatableColumn) => col.meta?.type || null; const getFormattedValue = (val: any, type: any) => { @@ -85,7 +87,7 @@ export const Datatable: FC = ({ {datatable.columns.map((col) => ( - {getFormattedValue(row[getColumnName(col)], getColumnType(col))} + {getFormattedValue(row[getColumnId(col)], getColumnType(col))} ))} diff --git a/x-pack/plugins/canvas/public/components/workpad_app/index.ts b/x-pack/plugins/canvas/public/components/workpad_app/index.ts index af5dc1cf21ad..f40f0a2644f8 100644 --- a/x-pack/plugins/canvas/public/components/workpad_app/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_app/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export { WorkpadApp } from './workpad_app'; +export { WorkpadApp, WORKPAD_CONTAINER_ID } from './workpad_app'; export { WorkpadApp as WorkpadAppComponent } from './workpad_app.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_page/integration_utils.js b/x-pack/plugins/canvas/public/components/workpad_page/integration_utils.js index 132ee551c8b7..9e0627b2c089 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/integration_utils.js +++ b/x-pack/plugins/canvas/public/components/workpad_page/integration_utils.js @@ -12,8 +12,8 @@ import { selectToplevelNodes } from '../../state/actions/transient'; import { arrayToMap, flatten, identity } from '../../lib/aeroelastic/functional'; import { getLocalTransformMatrix } from '../../lib/aeroelastic/layout_functions'; import { matrixToAngle } from '../../lib/aeroelastic/matrix'; -import { isGroupId, elementToShape } from './utils'; -export * from './utils'; +import { isGroupId, elementToShape } from './positioning_utils'; +export * from './positioning_utils'; const shapeToElement = (shape) => ({ left: shape.transformMatrix[12] - shape.a, diff --git a/x-pack/plugins/canvas/public/components/workpad_page/utils.js b/x-pack/plugins/canvas/public/components/workpad_page/utils.js deleted file mode 100644 index 1cc2710b2d3d..000000000000 --- a/x-pack/plugins/canvas/public/components/workpad_page/utils.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { multiply, rotateZ, translate } from '../../lib/aeroelastic/matrix'; - -export const isGroupId = (id) => id.startsWith('group'); - -const headerData = (id) => - isGroupId(id) - ? { id, type: 'group', subtype: 'persistentGroup' } - : { id, type: 'rectangleElement', subtype: '' }; - -const transformData = ({ top, left, width, height, angle }, z) => - multiply( - translate(left + width / 2, top + height / 2, z), // painter's algo: latest item (highest z) goes to top - rotateZ((-angle / 180) * Math.PI) // minus angle as transform:matrix3d uses a left-handed coordinate system - ); - -/** - * elementToShape - * - * converts a `kibana-canvas` element to an `aeroelastic` shape. - * - * Shape: the layout algorithms need to deal with objects through their geometric properties, excluding other aspects, - * such as what's inside the element, eg. image or scatter plot. This representation is, at its core, a transform matrix - * that establishes a new local coordinate system https://drafts.csswg.org/css-transforms/#local-coordinate-system plus a - * size descriptor. There are two versions of the transform matrix: - * - `transformMatrix` is analogous to the SVG https://drafts.csswg.org/css-transforms/#current-transformation-matrix - * - `localTransformMatrix` is analogous to the SVG https://drafts.csswg.org/css-transforms/#transformation-matrix - * - * Element: it also needs to represent the geometry, primarily because of the need to persist it in `redux` and on the - * server, and to accept such data from the server. The redux and server representations will need to change as more general - * projections such as 3D are added. The element also needs to maintain its content, such as an image or a plot. - * - * While all elements on the current page also exist as shapes, there are shapes that are not elements: annotations. - * For example, `rotation_handle`, `border_resize_handle` and `border_connection` are modeled as shapes by the layout - * library, simply for generality. - */ -export const elementToShape = ({ id, position }, z) => ({ - ...headerData(id), - parent: (position && position.parent) || null, - transformMatrix: transformData(position, z), - a: position.width / 2, // we currently specify half-width, half-height as it leads to - b: position.height / 2, // more regular math (like ellipsis radii rather than diameters) -}); - -const simplePosition = ({ id, position, filter }, z) => ({ - ...headerData(id), - width: position.width, - height: position.height, - transformMatrix: transformData(position, z), - filter, -}); - -export const simplePositioning = ({ elements }) => ({ elements: elements.map(simplePosition) }); diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/event_handlers.js b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/event_handlers.ts similarity index 58% rename from x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/event_handlers.js rename to x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/event_handlers.ts index f0814172fe66..e8527089c9ac 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/event_handlers.js +++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/event_handlers.ts @@ -5,7 +5,27 @@ * 2.0. */ -const localMousePosition = (canvasOrigin, clientX, clientY, zoomScale = 1) => { +import { CommitFn } from '../../../lib/aeroelastic'; +import { WORKPAD_CONTAINER_ID } from '../../workpad_app/workpad_app.component'; + +type CanvasOriginFn = () => { left: number; top: number }; + +export interface Props { + commit: CommitFn | undefined; + canvasOrigin: CanvasOriginFn; + zoomScale: number; + canDragElement: (target: EventTarget | null) => boolean; +} + +const isInCanvas = (target: EventTarget | null) => + target instanceof Element && target.closest(`#${WORKPAD_CONTAINER_ID}`); + +const localMousePosition = ( + canvasOrigin: CanvasOriginFn, + clientX: number, + clientY: number, + zoomScale = 1 +) => { const { left, top } = canvasOrigin(); return { // commit unscaled coordinates @@ -19,12 +39,26 @@ const resetHandler = () => { window.onmouseup = null; }; -const setupHandler = (commit, canvasOrigin, zoomScale) => { +const setupHandler = (commit: CommitFn, canvasOrigin: CanvasOriginFn, zoomScale?: number) => { // Ancestor has to be identified on setup, rather than 1st interaction, otherwise events may be triggered on // DOM elements that had been removed: kibana-canvas github issue #1093 - window.onmousemove = ({ buttons, clientX, clientY, altKey, metaKey, shiftKey, ctrlKey }) => { + window.onmousemove = ({ + buttons, + clientX, + clientY, + altKey, + metaKey, + shiftKey, + ctrlKey, + target, + }: MouseEvent) => { + if (!isInCanvas(target)) { + return; + } + const { x, y } = localMousePosition(canvasOrigin, clientX, clientY, zoomScale); + // only commits the cursor position if there's a way to latch onto x/y calculation (canvasOrigin is knowable) // or if left button is being held down (i.e. an element is being dragged) if (buttons === 1 || canvasOrigin) { @@ -34,9 +68,15 @@ const setupHandler = (commit, canvasOrigin, zoomScale) => { commit('cursorPosition', {}); } }; - window.onmouseup = (e) => { + + window.onmouseup = (e: MouseEvent) => { + const { clientX, clientY, altKey, metaKey, shiftKey, ctrlKey, target } = e; + + if (!isInCanvas(target)) { + return; + } + e.stopPropagation(); - const { clientX, clientY, altKey, metaKey, shiftKey, ctrlKey } = e; const { x, y } = localMousePosition(canvasOrigin, clientX, clientY, zoomScale); commit('mouseEvent', { event: 'mouseUp', x, y, altKey, metaKey, shiftKey, ctrlKey }); resetHandler(); @@ -44,26 +84,46 @@ const setupHandler = (commit, canvasOrigin, zoomScale) => { }; const handleMouseMove = ( - commit, - { clientX, clientY, altKey, metaKey, shiftKey, ctrlKey }, - canvasOrigin, - zoomScale + commit: CommitFn | undefined, + { clientX, clientY, altKey, metaKey, shiftKey, ctrlKey, target }: MouseEvent, + canvasOrigin: CanvasOriginFn, + zoomScale?: number ) => { + if (!isInCanvas(target)) { + return; + } + const { x, y } = localMousePosition(canvasOrigin, clientX, clientY, zoomScale); + if (commit) { commit('cursorPosition', { x, y, altKey, metaKey, shiftKey, ctrlKey }); } }; -const handleMouseLeave = (commit, { buttons }) => { +const handleMouseLeave = (commit: CommitFn | undefined, { buttons, target }: MouseEvent) => { + if (!isInCanvas(target)) { + return; + } + if (buttons !== 1 && commit) { commit('cursorPosition', {}); // reset hover only if we're not holding down left key (ie. drag in progress) } }; -const handleMouseDown = (commit, e, canvasOrigin, zoomScale, allowDrag = true) => { +const handleMouseDown = ( + commit: CommitFn | undefined, + e: MouseEvent, + canvasOrigin: CanvasOriginFn, + zoomScale: number, + allowDrag = true +) => { + const { clientX, clientY, buttons, altKey, metaKey, shiftKey, ctrlKey, target } = e; + + if (!isInCanvas(target)) { + return; + } + e.stopPropagation(); - const { clientX, clientY, buttons, altKey, metaKey, shiftKey, ctrlKey } = e; if (buttons !== 1 || !commit) { resetHandler(); return; // left-click only @@ -82,7 +142,7 @@ const handleMouseDown = (commit, e, canvasOrigin, zoomScale, allowDrag = true) = }; export const eventHandlers = { - onMouseDown: (props) => (e) => + onMouseDown: (props: Props) => (e: MouseEvent) => handleMouseDown( props.commit, e, @@ -90,9 +150,10 @@ export const eventHandlers = { props.zoomScale, props.canDragElement(e.target) ), - onMouseMove: (props) => (e) => + onMouseMove: (props: Props) => (e: MouseEvent) => handleMouseMove(props.commit, e, props.canvasOrigin, props.zoomScale), - onMouseLeave: (props) => (e) => handleMouseLeave(props.commit, e), - onWheel: (props) => (e) => handleMouseMove(props.commit, e, props.canvasOrigin), + onMouseLeave: (props: Props) => (e: MouseEvent) => handleMouseLeave(props.commit, e), + onWheel: (props: Props) => (e: WheelEvent) => + handleMouseMove(props.commit, e, props.canvasOrigin), resetHandler: () => () => resetHandler(), }; diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js index 04cc3dcbfacc..f03547df8de9 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js +++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/index.js @@ -243,17 +243,7 @@ export const InteractivePage = compose( })), withProps((...props) => ({ ...props, - canDragElement: (element) => { - return !isEmbeddableBody(element) && isInWorkpad(element); - - const hasClosest = typeof element.closest === 'function'; - - if (hasClosest) { - return !element.closest('.embeddable') || element.closest('.embPanel__header'); - } else { - return !closest.call(element, '.embeddable') || closest.call(element, '.embPanel__header'); - } - }, + canDragElement: (element) => !isEmbeddableBody(element) && isInWorkpad(element), })), withHandlers(eventHandlers), // Captures user intent, needs to have reconciled state () => InteractiveComponent diff --git a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx index a15458f4c727..db0aef22512c 100644 --- a/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_page/workpad_interactive_page/interaction_boundary.tsx @@ -6,7 +6,6 @@ */ import React, { CSSProperties, PureComponent } from 'react'; -// @ts-expect-error untyped local import { WORKPAD_CONTAINER_ID } from '../../workpad_app'; interface State { diff --git a/x-pack/plugins/canvas/public/functions/timelion.ts b/x-pack/plugins/canvas/public/functions/timelion.ts index 99bd1b72434c..81893db1bbd4 100644 --- a/x-pack/plugins/canvas/public/functions/timelion.ts +++ b/x-pack/plugins/canvas/public/functions/timelion.ts @@ -13,7 +13,7 @@ import { TimeRange } from 'src/plugins/data/common'; import { ExpressionFunctionDefinition, DatatableRow } from 'src/plugins/expressions/public'; import { fetch } from '../../common/lib/fetch'; // @ts-expect-error untyped local -import { buildBoolArray } from '../../public/lib/build_bool_array'; +import { buildBoolArray } from '../../common/lib/build_bool_array'; import { Datatable, ExpressionValueFilter } from '../../types'; import { getFunctionHelp } from '../../i18n'; import { InitializeArguments } from './'; diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/index.d.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/index.d.ts index 4b5ff917fe7e..d959890e3e34 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/index.d.ts +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/index.d.ts @@ -44,6 +44,8 @@ export type TypeName = string; export type Payload = JsonMap; export type UpdaterFunction = (arg: State) => State; +export type CommitFn = (type: TypeName, payload: Payload) => void; + export interface Store { getCurrentState: () => State; setCurrentState: (state: State) => void; diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/layout_functions.js b/x-pack/plugins/canvas/public/lib/aeroelastic/layout_functions.js index f620de03bea4..a2f212704638 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/layout_functions.js +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/layout_functions.js @@ -819,7 +819,7 @@ const resizePointAnnotations = (config, parent, a, b) => ([x, y, cursorAngle]) = const xName = xNames[x]; const yName = yNames[y]; return { - id: [config.resizeHandleName, xName, yName, parent].join('_'), + id: [config.resizeHandleName, xName, yName, parent.id].join('_'), type: 'annotation', subtype: config.resizeHandleName, horizontalPosition: xName, diff --git a/x-pack/plugins/canvas/public/lib/aeroelastic/store.ts b/x-pack/plugins/canvas/public/lib/aeroelastic/store.ts index 7e6197fb160a..84e37968b784 100644 --- a/x-pack/plugins/canvas/public/lib/aeroelastic/store.ts +++ b/x-pack/plugins/canvas/public/lib/aeroelastic/store.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { ActionId, Payload, State, Store, TypeName, UpdaterFunction } from '.'; +import { ActionId, CommitFn, State, Store, UpdaterFunction } from '.'; let counter = 0 as ActionId; export const createStore = (initialState: State, updater: UpdaterFunction): Store => { let currentState = initialState; - const commit = (type: TypeName, payload: Payload) => { + const commit: CommitFn = (type, payload) => { return (currentState = updater({ ...currentState, primaryUpdate: { diff --git a/x-pack/plugins/cases/server/services/alerts/types.ts b/x-pack/plugins/canvas/server/mocks/index.ts similarity index 72% rename from x-pack/plugins/cases/server/services/alerts/types.ts rename to x-pack/plugins/canvas/server/mocks/index.ts index 5ddc57fa5861..1cb39c690df9 100644 --- a/x-pack/plugins/cases/server/services/alerts/types.ts +++ b/x-pack/plugins/canvas/server/mocks/index.ts @@ -5,9 +5,4 @@ * 2.0. */ -export interface Alert { - id: string; - index: string; - error?: Error; - source?: unknown; -} +export { workpadRouteContextMock, MockWorkpadRouteContext } from './workpad_route_context'; diff --git a/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts b/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts new file mode 100644 index 000000000000..abba97639a4c --- /dev/null +++ b/x-pack/plugins/canvas/server/mocks/workpad_route_context.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CanvasRouteHandlerContext } from '../workpad_route_context'; + +export interface MockWorkpadRouteContext extends CanvasRouteHandlerContext { + canvas: { + workpad: { + create: jest.Mock; + get: jest.Mock; + update: jest.Mock; + }; + }; +} + +export const workpadRouteContextMock = { + create: (): MockWorkpadRouteContext['canvas'] => ({ + workpad: { + create: jest.fn(), + get: jest.fn(), + update: jest.fn(), + }, + }), +}; diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index f0b7c0243000..35b1d0025ea5 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -26,6 +26,7 @@ import { customElementType, workpadType, workpadTemplateType } from './saved_obj import { initializeTemplates } from './templates'; import { essqlSearchStrategyProvider } from './lib/essql_strategy'; import { getUISettings } from './ui_settings'; +import { CanvasRouteHandlerContext, createWorkpadRouteContext } from './workpad_route_context'; interface PluginsSetup { expressions: ExpressionsServerSetup; @@ -48,6 +49,8 @@ export class CanvasPlugin implements Plugin { } public setup(coreSetup: CoreSetup, plugins: PluginsSetup) { + const expressionsFork = plugins.expressions.fork(); + coreSetup.uiSettings.register(getUISettings()); coreSetup.savedObjects.registerType(customElementType); coreSetup.savedObjects.registerType(workpadType); @@ -55,11 +58,17 @@ export class CanvasPlugin implements Plugin { plugins.features.registerKibanaFeature(getCanvasFeature(plugins)); - const canvasRouter = coreSetup.http.createRouter(); + const contextProvider = createWorkpadRouteContext({ expressions: expressionsFork }); + coreSetup.http.registerRouteHandlerContext( + 'canvas', + contextProvider + ); + + const canvasRouter = coreSetup.http.createRouter(); initRoutes({ router: canvasRouter, - expressions: plugins.expressions, + expressions: expressionsFork, bfetch: plugins.bfetch, logger: this.logger, }); @@ -73,7 +82,7 @@ export class CanvasPlugin implements Plugin { const globalConfig = this.initializerContext.config.legacy.get(); registerCanvasUsageCollector(plugins.usageCollection, globalConfig.kibana.index); - setupInterpreter(plugins.expressions); + setupInterpreter(expressionsFork); coreSetup.getStartServices().then(([_, depsStart]) => { const strategy = essqlSearchStrategyProvider(); diff --git a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts index b1fe4bc798f6..f565c911274f 100644 --- a/x-pack/plugins/canvas/server/routes/catch_error_handler.ts +++ b/x-pack/plugins/canvas/server/routes/catch_error_handler.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { RequestHandler } from 'src/core/server'; +import { RequestHandler, RequestHandlerContext } from 'src/core/server'; -export const catchErrorHandler: ( - fn: RequestHandler -) => RequestHandler = (fn) => { +export const catchErrorHandler: ( + fn: RequestHandler +) => RequestHandler = (fn) => { return async (context, request, response) => { try { return await fn(context, request, response); diff --git a/x-pack/plugins/canvas/server/routes/index.ts b/x-pack/plugins/canvas/server/routes/index.ts index ccc8f7e27826..b7ba5637119f 100644 --- a/x-pack/plugins/canvas/server/routes/index.ts +++ b/x-pack/plugins/canvas/server/routes/index.ts @@ -14,9 +14,10 @@ import { initShareablesRoutes } from './shareables'; import { initWorkpadRoutes } from './workpad'; import { initTemplateRoutes } from './templates'; import { initFunctionsRoutes } from './functions'; +import { CanvasRouteHandlerContext } from '../workpad_route_context'; export interface RouteInitializerDeps { - router: IRouter; + router: IRouter; logger: Logger; expressions: ExpressionsServerSetup; bfetch: BfetchServerSetup; diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.test.ts b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts index f6a16a2a20af..158c68f0ea0a 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/create.test.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/create.test.ts @@ -5,11 +5,10 @@ * 2.0. */ -import sinon from 'sinon'; import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; -import { CANVAS_TYPE } from '../../../common/lib/constants'; +import { workpadRouteContextMock, MockWorkpadRouteContext } from '../../mocks'; import { initializeCreateWorkpadRoute } from './create'; -import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { getMockedRouterDeps } from '../test_helpers'; let mockRouteContext = ({ @@ -18,17 +17,13 @@ let mockRouteContext = ({ client: savedObjectsClientMock.create(), }, }, -} as unknown) as RequestHandlerContext; - -const mockedUUID = '123abc'; -const now = new Date(); -const nowIso = now.toISOString(); + canvas: workpadRouteContextMock.create(), +} as unknown) as MockWorkpadRouteContext; jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); describe('POST workpad', () => { let routeHandler: RequestHandler; - let clock: sinon.SinonFakeTimers; beforeEach(() => { mockRouteContext = ({ @@ -37,9 +32,8 @@ describe('POST workpad', () => { client: savedObjectsClientMock.create(), }, }, - } as unknown) as RequestHandlerContext; - - clock = sinon.useFakeTimers(now); + canvas: workpadRouteContextMock.create(), + } as unknown) as MockWorkpadRouteContext; const routerDeps = getMockedRouterDeps(); initializeCreateWorkpadRoute(routerDeps); @@ -47,11 +41,12 @@ describe('POST workpad', () => { routeHandler = routerDeps.router.post.mock.calls[0][1]; }); - afterEach(() => { - clock.restore(); - }); - it(`returns 200 when the workpad is created`, async () => { + const id = 'my-id'; + mockRouteContext.canvas.workpad.create.mockResolvedValue({ + id, + }); + const mockWorkpad = { pages: [], }; @@ -65,18 +60,8 @@ describe('POST workpad', () => { const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); expect(response.status).toBe(200); - expect(response.payload).toEqual({ ok: true, id: `workpad-${mockedUUID}` }); - expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( - CANVAS_TYPE, - { - ...mockWorkpad, - '@timestamp': nowIso, - '@created': nowIso, - }, - { - id: `workpad-${mockedUUID}`, - } - ); + expect(response.payload).toEqual({ ok: true, id }); + expect(mockRouteContext.canvas.workpad.create).toBeCalledWith(mockWorkpad); }); it(`returns bad request if create is unsuccessful`, async () => { @@ -86,7 +71,7 @@ describe('POST workpad', () => { body: {}, }); - (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementation(() => { + mockRouteContext.canvas.workpad.create.mockImplementation(() => { throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); }); @@ -109,6 +94,11 @@ describe('POST workpad', () => { }, }; + const id = 'my-id'; + mockRouteContext.canvas.workpad.create.mockResolvedValue({ + id, + }); + (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockResolvedValue( mockTemplateResponse ); @@ -122,17 +112,9 @@ describe('POST workpad', () => { const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); expect(response.status).toBe(200); - expect(response.payload).toEqual({ ok: true, id: `workpad-${mockedUUID}` }); - expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( - CANVAS_TYPE, - { - ...mockTemplateResponse.attributes.template, - '@timestamp': nowIso, - '@created': nowIso, - }, - { - id: `workpad-${mockedUUID}`, - } + expect(response.payload).toEqual({ ok: true, id }); + expect(mockRouteContext.canvas.workpad.create).toBeCalledWith( + mockTemplateResponse.attributes.template ); }); }); diff --git a/x-pack/plugins/canvas/server/routes/workpad/create.ts b/x-pack/plugins/canvas/server/routes/workpad/create.ts index 1fa8ab4412ac..2a0c47fee4c9 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/create.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/create.ts @@ -7,10 +7,8 @@ import { schema } from '@kbn/config-schema'; import { RouteInitializerDeps } from '../'; -import { CANVAS_TYPE, API_ROUTE_WORKPAD, TEMPLATE_TYPE } from '../../../common/lib/constants'; +import { API_ROUTE_WORKPAD, TEMPLATE_TYPE } from '../../../common/lib/constants'; import { CanvasWorkpad } from '../../../types'; -import { getId } from '../../../common/lib/get_id'; -import { WorkpadAttributes } from './workpad_attributes'; import { WorkpadSchema } from './workpad_schema'; import { okResponse } from '../ok_response'; import { catchErrorHandler } from '../catch_error_handler'; @@ -59,23 +57,10 @@ export function initializeCreateWorkpadRoute(deps: RouteInitializerDeps) { workpad = templateSavedObject.attributes.template; } - const now = new Date().toISOString(); - const { id: maybeId, ...payload } = workpad; - - const id = maybeId ? maybeId : getId('workpad'); - - await context.core.savedObjects.client.create( - CANVAS_TYPE, - { - ...payload, - '@timestamp': now, - '@created': now, - }, - { id } - ); + const createdObject = await context.canvas.workpad.create(workpad); return response.ok({ - body: { ...okResponse, id }, + body: { ...okResponse, id: createdObject.id }, }); }) ); diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.test.ts b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts index 5934ece23448..e19a20cc3f54 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/get.test.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/get.test.ts @@ -7,19 +7,16 @@ import { CANVAS_TYPE } from '../../../common/lib/constants'; import { initializeGetWorkpadRoute } from './get'; -import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; import { workpadWithGroupAsElement } from '../../../__fixtures__/workpads'; import { CanvasWorkpad } from '../../../types'; import { getMockedRouterDeps } from '../test_helpers'; +import { workpadRouteContextMock, MockWorkpadRouteContext } from '../../mocks'; const mockRouteContext = ({ - core: { - savedObjects: { - client: savedObjectsClientMock.create(), - }, - }, -} as unknown) as RequestHandlerContext; + canvas: workpadRouteContextMock.create(), +} as unknown) as MockWorkpadRouteContext; describe('GET workpad', () => { let routeHandler: RequestHandler; @@ -31,6 +28,10 @@ describe('GET workpad', () => { routeHandler = routerDeps.router.get.mock.calls[0][1]; }); + afterEach(() => { + jest.resetAllMocks(); + }); + it(`returns 200 when the workpad is found`, async () => { const request = httpServerMock.createKibanaRequest({ method: 'get', @@ -40,16 +41,13 @@ describe('GET workpad', () => { }, }); - const savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get.mockResolvedValueOnce({ + mockRouteContext.canvas.workpad.get.mockResolvedValue({ id: '123', type: CANVAS_TYPE, attributes: { foo: true }, references: [], }); - mockRouteContext.core.savedObjects.client = savedObjectsClient; - const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); expect(response.status).toBe(200); @@ -60,10 +58,9 @@ describe('GET workpad', () => { } `); - expect(savedObjectsClient.get.mock.calls).toMatchInlineSnapshot(` + expect(mockRouteContext.canvas.workpad.get.mock.calls).toMatchInlineSnapshot(` Array [ Array [ - "canvas-workpad", "123", ], ] @@ -79,16 +76,13 @@ describe('GET workpad', () => { }, }); - const savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get.mockResolvedValueOnce({ + mockRouteContext.canvas.workpad.get.mockResolvedValue({ id: '123', type: CANVAS_TYPE, attributes: workpadWithGroupAsElement as any, references: [], }); - mockRouteContext.core.savedObjects.client = savedObjectsClient; - const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); const workpad = response.payload as CanvasWorkpad; @@ -110,10 +104,9 @@ describe('GET workpad', () => { }); const savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get.mockImplementation(() => { + mockRouteContext.canvas.workpad.get.mockImplementation(() => { throw savedObjectsClient.errors.createGenericNotFoundError(CANVAS_TYPE, id); }); - mockRouteContext.core.savedObjects.client = savedObjectsClient; const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); diff --git a/x-pack/plugins/canvas/server/routes/workpad/get.ts b/x-pack/plugins/canvas/server/routes/workpad/get.ts index 19c01ae1f01e..ff3ed4bad55b 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/get.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/get.ts @@ -7,8 +7,7 @@ import { schema } from '@kbn/config-schema'; import { RouteInitializerDeps } from '../'; -import { CANVAS_TYPE, API_ROUTE_WORKPAD } from '../../../common/lib/constants'; -import { WorkpadAttributes } from './workpad_attributes'; +import { API_ROUTE_WORKPAD } from '../../../common/lib/constants'; import { catchErrorHandler } from '../catch_error_handler'; export function initializeGetWorkpadRoute(deps: RouteInitializerDeps) { @@ -23,10 +22,7 @@ export function initializeGetWorkpadRoute(deps: RouteInitializerDeps) { }, }, catchErrorHandler(async (context, request, response) => { - const workpad = await context.core.savedObjects.client.get( - CANVAS_TYPE, - request.params.id - ); + const workpad = await context.canvas.workpad.get(request.params.id); if ( // not sure if we need to be this defensive diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts index 1f3143a5d93d..8feadf433f07 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/update.test.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/update.test.ts @@ -8,11 +8,12 @@ import sinon from 'sinon'; import { CANVAS_TYPE } from '../../../common/lib/constants'; import { initializeUpdateWorkpadRoute, initializeUpdateWorkpadAssetsRoute } from './update'; -import { kibanaResponseFactory, RequestHandlerContext, RequestHandler } from 'src/core/server'; +import { kibanaResponseFactory, RequestHandler } from 'src/core/server'; import { savedObjectsClientMock, httpServerMock } from 'src/core/server/mocks'; import { workpads } from '../../../__fixtures__/workpads'; import { okResponse } from '../ok_response'; import { getMockedRouterDeps } from '../test_helpers'; +import { workpadRouteContextMock, MockWorkpadRouteContext } from '../../mocks'; const mockRouteContext = ({ core: { @@ -20,11 +21,11 @@ const mockRouteContext = ({ client: savedObjectsClientMock.create(), }, }, -} as unknown) as RequestHandlerContext; + canvas: workpadRouteContextMock.create(), +} as unknown) as MockWorkpadRouteContext; const workpad = workpads[0]; const now = new Date(); -const nowIso = now.toISOString(); jest.mock('uuid/v4', () => jest.fn().mockReturnValue('123abc')); @@ -48,7 +49,7 @@ describe('PUT workpad', () => { it(`returns 200 ok when the workpad is updated`, async () => { const updatedWorkpad = { name: 'new name' }; - const { id, ...workpadAttributes } = workpad; + const { id } = workpad; const request = httpServerMock.createKibanaRequest({ method: 'put', @@ -59,33 +60,13 @@ describe('PUT workpad', () => { body: updatedWorkpad, }); - const savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get.mockResolvedValueOnce({ - id, - type: CANVAS_TYPE, - attributes: workpadAttributes as any, - references: [], - }); - - mockRouteContext.core.savedObjects.client = savedObjectsClient; + mockRouteContext.canvas.workpad.update.mockResolvedValue(true); const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); expect(response.status).toBe(200); expect(response.payload).toEqual(okResponse); - expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( - CANVAS_TYPE, - { - ...workpadAttributes, - ...updatedWorkpad, - '@timestamp': nowIso, - '@created': workpad['@created'], - }, - { - overwrite: true, - id, - } - ); + expect(mockRouteContext.canvas.workpad.update).toBeCalledWith(id, updatedWorkpad); }); it(`returns not found if existing workpad is not found`, async () => { @@ -98,7 +79,7 @@ describe('PUT workpad', () => { body: {}, }); - (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockImplementationOnce(() => { + mockRouteContext.canvas.workpad.update.mockImplementationOnce(() => { throw mockRouteContext.core.savedObjects.client.errors.createGenericNotFoundError( 'not found' ); @@ -119,17 +100,7 @@ describe('PUT workpad', () => { body: {}, }); - const savedObjectsClient = savedObjectsClientMock.create(); - savedObjectsClient.get.mockResolvedValueOnce({ - id: 'some-id', - type: CANVAS_TYPE, - attributes: {}, - references: [], - }); - - mockRouteContext.core.savedObjects.client = savedObjectsClient; - - (mockRouteContext.core.savedObjects.client.create as jest.Mock).mockImplementationOnce(() => { + mockRouteContext.canvas.workpad.update.mockImplementationOnce(() => { throw mockRouteContext.core.savedObjects.client.errors.createBadRequestError('bad request'); }); @@ -182,7 +153,7 @@ describe('update assets', () => { body: assets, }); - (mockRouteContext.core.savedObjects.client.get as jest.Mock).mockResolvedValueOnce({ + mockRouteContext.canvas.workpad.update.mockResolvedValueOnce({ id, type: CANVAS_TYPE, attributes: attributes as any, @@ -192,17 +163,8 @@ describe('update assets', () => { const response = await routeHandler(mockRouteContext, request, kibanaResponseFactory); expect(response.status).toBe(200); - expect(mockRouteContext.core.savedObjects.client.create).toBeCalledWith( - CANVAS_TYPE, - { - ...attributes, - '@timestamp': nowIso, - assets, - }, - { - id, - overwrite: true, - } - ); + expect(mockRouteContext.canvas.workpad.update).toBeCalledWith(id, { + assets, + }); }); }); diff --git a/x-pack/plugins/canvas/server/routes/workpad/update.ts b/x-pack/plugins/canvas/server/routes/workpad/update.ts index bb1a347437c0..2fe3c8fc9e3e 100644 --- a/x-pack/plugins/canvas/server/routes/workpad/update.ts +++ b/x-pack/plugins/canvas/server/routes/workpad/update.ts @@ -5,13 +5,10 @@ * 2.0. */ -import { schema, TypeOf } from '@kbn/config-schema'; -import { omit } from 'lodash'; -import { KibanaResponseFactory, SavedObjectsClientContract } from 'src/core/server'; +import { schema } from '@kbn/config-schema'; import { CanvasWorkpad } from '../../../types'; import { RouteInitializerDeps } from '../'; import { - CANVAS_TYPE, API_ROUTE_WORKPAD, API_ROUTE_WORKPAD_STRUCTURES, API_ROUTE_WORKPAD_ASSETS, @@ -22,35 +19,6 @@ import { catchErrorHandler } from '../catch_error_handler'; const AssetsRecordSchema = schema.recordOf(schema.string(), WorkpadAssetSchema); -const AssetPayloadSchema = schema.object({ - assets: AssetsRecordSchema, -}); - -const workpadUpdateHandler = async ( - payload: TypeOf | TypeOf, - id: string, - savedObjectsClient: SavedObjectsClientContract, - response: KibanaResponseFactory -) => { - const now = new Date().toISOString(); - - const workpadObject = await savedObjectsClient.get(CANVAS_TYPE, id); - await savedObjectsClient.create( - CANVAS_TYPE, - { - ...workpadObject.attributes, - ...omit(payload, 'id'), // never write the id property - '@timestamp': now, // always update the modified time - '@created': workpadObject.attributes['@created'], // ensure created is not modified - }, - { overwrite: true, id } - ); - - return response.ok({ - body: okResponse, - }); -}; - export function initializeUpdateWorkpadRoute(deps: RouteInitializerDeps) { const { router } = deps; // TODO: This route is likely deprecated and everything is using the workpad_structures @@ -72,12 +40,11 @@ export function initializeUpdateWorkpadRoute(deps: RouteInitializerDeps) { }, }, catchErrorHandler(async (context, request, response) => { - return workpadUpdateHandler( - request.body, - request.params.id, - context.core.savedObjects.client, - response - ); + await context.canvas.workpad.update(request.params.id, request.body as CanvasWorkpad); + + return response.ok({ + body: okResponse, + }); }) ); @@ -98,12 +65,11 @@ export function initializeUpdateWorkpadRoute(deps: RouteInitializerDeps) { }, }, catchErrorHandler(async (context, request, response) => { - return workpadUpdateHandler( - request.body, - request.params.id, - context.core.savedObjects.client, - response - ); + await context.canvas.workpad.update(request.params.id, request.body as CanvasWorkpad); + + return response.ok({ + body: okResponse, + }); }) ); } @@ -131,12 +97,15 @@ export function initializeUpdateWorkpadAssetsRoute(deps: RouteInitializerDeps) { }, }, async (context, request, response) => { - return workpadUpdateHandler( - { assets: AssetsRecordSchema.validate(request.body) }, - request.params.id, - context.core.savedObjects.client, - response - ); + const workpadAssets = { + assets: AssetsRecordSchema.validate(request.body), + }; + + await context.canvas.workpad.update(request.params.id, workpadAssets as CanvasWorkpad); + + return response.ok({ + body: okResponse, + }); } ); } diff --git a/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts b/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts new file mode 100644 index 000000000000..b0d20add2f79 --- /dev/null +++ b/x-pack/plugins/canvas/server/saved_objects/workpad_references.ts @@ -0,0 +1,68 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { fromExpression, toExpression } from '@kbn/interpreter/common'; +import { SavedObjectReference } from '../../../../../src/core/server'; +import { WorkpadAttributes } from '../routes/workpad/workpad_attributes'; + +import { ExpressionsServerSetup } from '../../../../../src/plugins/expressions/server'; + +export const extractReferences = ( + workpad: WorkpadAttributes, + expressions: ExpressionsServerSetup +): { workpad: WorkpadAttributes; references: SavedObjectReference[] } => { + // We need to find every element in the workpad and extract references + const references: SavedObjectReference[] = []; + + const pages = workpad.pages.map((page) => { + const elements = page.elements.map((element) => { + const extract = expressions.extract(fromExpression(element.expression)); + + // Prefix references with the element id so we will know later which element it goes with + references.push( + ...extract.references.map((reference) => ({ + ...reference, + name: `${element.id}:${reference.name}`, + })) + ); + + return { ...element, expression: toExpression(extract.state) }; + }); + + return { ...page, elements }; + }); + + return { workpad: { ...workpad, pages }, references }; +}; + +export const injectReferences = ( + workpad: WorkpadAttributes, + references: SavedObjectReference[], + expressions: ExpressionsServerSetup +) => { + const pages = workpad.pages.map((page) => { + const elements = page.elements.map((element) => { + const referencesForElement = references + .filter(({ name }) => name.indexOf(element.id) === 0) + .map((reference) => ({ + ...reference, + name: reference.name.replace(`${element.id}:`, ''), + })); + + const injectedAst = expressions.inject( + fromExpression(element.expression), + referencesForElement + ); + + return { ...element, expression: toExpression(injectedAst) }; + }); + + return { ...page, elements }; + }); + + return { ...workpad, pages }; +}; diff --git a/x-pack/plugins/canvas/server/setup_interpreter.ts b/x-pack/plugins/canvas/server/setup_interpreter.ts index b61689d7c861..2fe23eb86c08 100644 --- a/x-pack/plugins/canvas/server/setup_interpreter.ts +++ b/x-pack/plugins/canvas/server/setup_interpreter.ts @@ -7,7 +7,9 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { functions } from '../canvas_plugin_src/functions/server'; +import { functions as externalFunctions } from '../canvas_plugin_src/functions/external'; export function setupInterpreter(expressions: ExpressionsServerSetup) { functions.forEach((f) => expressions.registerFunction(f)); + externalFunctions.forEach((f) => expressions.registerFunction(f)); } diff --git a/x-pack/plugins/canvas/server/workpad_route_context.test.ts b/x-pack/plugins/canvas/server/workpad_route_context.test.ts new file mode 100644 index 000000000000..ec09a364b594 --- /dev/null +++ b/x-pack/plugins/canvas/server/workpad_route_context.test.ts @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import sinon from 'sinon'; +import { fromExpression } from '@kbn/interpreter/common'; +import { createWorkpadRouteContext } from './workpad_route_context'; +import { RequestHandlerContext, SavedObjectReference } from 'src/core/server'; +import { savedObjectsClientMock } from 'src/core/server/mocks'; +import { CanvasWorkpad } from '../types'; +import { CANVAS_TYPE } from '../common/lib/constants'; + +const mockedExpressionService = { + inject: jest.fn(), + extract: jest.fn(), +}; + +const savedObjectsClient = savedObjectsClientMock.create(); + +const mockContext = ({ + core: { + savedObjects: { + client: savedObjectsClient, + }, + }, +} as unknown) as RequestHandlerContext; + +const workpadRouteContext = createWorkpadRouteContext({ + expressions: mockedExpressionService as any, +}); + +const now = new Date(); + +const injectedExpression = 'fn extracted=false'; +const extractedExpression = 'fn extracted=true'; + +const injectedWorkpad = { + id: 'workpad-id', + pages: [ + { + elements: [ + { + id: 'element-id', + expression: injectedExpression, + }, + ], + }, + ], +}; + +const extractedWorkpad = { + pages: [ + { + elements: [ + { + id: 'element-id', + expression: extractedExpression, + }, + ], + }, + ], +}; + +const references: SavedObjectReference[] = [{ id: 'my-id', name: 'name', type: 'type' }]; + +describe('workpad route context', () => { + let clock: sinon.SinonFakeTimers; + beforeEach(() => { + jest.resetAllMocks(); + clock = sinon.useFakeTimers(now); + }); + + afterEach(() => { + clock.restore(); + }); + + describe('CREATE', () => { + it('extracts references before saving', async () => { + const expectedBody = { + '@created': now.toISOString(), + '@timestamp': now.toISOString(), + ...extractedWorkpad, + }; + + const canvasContext = await workpadRouteContext( + mockContext, + undefined as any, + undefined as any + ); + + mockedExpressionService.extract.mockReturnValue({ + state: fromExpression(extractedExpression), + references, + }); + + const soResponse = {}; + (mockContext.core.savedObjects.client.create as jest.Mock).mockResolvedValue(soResponse); + + const result = await canvasContext.workpad.create(injectedWorkpad as CanvasWorkpad); + + expect(mockContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + expectedBody, + { + id: injectedWorkpad.id, + references: references.map((r) => ({ + ...r, + name: `element-id:${r.name}`, + })), + } + ); + expect(result).toBe(soResponse); + }); + }); + + describe('GET', () => { + it('injects references to the saved object', async () => { + const id = 'so-id'; + const canvasContext = await workpadRouteContext( + mockContext, + undefined as any, + undefined as any + ); + + (mockContext.core.savedObjects.client.get as jest.Mock).mockResolvedValue({ + attributes: extractedWorkpad, + references, + }); + + mockedExpressionService.inject.mockReturnValue(fromExpression(injectedExpression)); + + const result = await canvasContext.workpad.get(id); + const { id: ingnoredId, ...expectedAttributes } = injectedWorkpad; + + expect(mockContext.core.savedObjects.client.get).toBeCalledWith(CANVAS_TYPE, id); + + expect(result.attributes).toEqual(expectedAttributes); + }); + }); + + describe('UPDATE', () => { + it('extracts from the given attributes', async () => { + const id = 'workpad-id'; + const createdDate = new Date(2020, 1, 1).toISOString(); + + const canvasContext = await workpadRouteContext( + mockContext, + undefined as any, + undefined as any + ); + + (mockContext.core.savedObjects.client.get as jest.Mock).mockReturnValue({ + attributes: { + ...extractedWorkpad, + '@created': createdDate, + }, + references, + }); + + const updatedInjectedExpression = 'fn ref="my-value"'; + const updatedExtractedExpression = 'fn ref="extracted"'; + const updatedWorkpad = { + id: 'workpad-id', + pages: [ + { + elements: [ + { + id: 'new-element-id', + expression: updatedInjectedExpression, + }, + ], + }, + ], + }; + + const expectedWorkpad = { + '@created': createdDate, + '@timestamp': now.toISOString(), + pages: [ + { + elements: [ + { + id: 'new-element-id', + expression: updatedExtractedExpression, + }, + ], + }, + ], + }; + + mockedExpressionService.inject.mockReturnValue(fromExpression(injectedExpression)); + mockedExpressionService.extract.mockReturnValue({ + state: fromExpression(updatedExtractedExpression), + references, + }); + + await canvasContext.workpad.update(id, updatedWorkpad as CanvasWorkpad); + + expect(mockContext.core.savedObjects.client.create).toBeCalledWith( + CANVAS_TYPE, + expectedWorkpad, + { + id, + references: references.map((r) => ({ + ...r, + name: `new-element-id:${r.name}`, + })), + overwrite: true, + } + ); + }); + }); +}); diff --git a/x-pack/plugins/canvas/server/workpad_route_context.ts b/x-pack/plugins/canvas/server/workpad_route_context.ts new file mode 100644 index 000000000000..5689bf9961f7 --- /dev/null +++ b/x-pack/plugins/canvas/server/workpad_route_context.ts @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { RequestHandlerContext, RequestHandlerContextProvider, SavedObject } from 'kibana/server'; +import { ExpressionsService } from 'src/plugins/expressions'; +import { WorkpadAttributes } from './routes/workpad/workpad_attributes'; +import { CANVAS_TYPE } from '../common/lib/constants'; +import { injectReferences, extractReferences } from './saved_objects/workpad_references'; +import { getId } from '../common/lib/get_id'; +import { CanvasWorkpad } from '../types'; + +export interface CanvasRouteHandlerContext extends RequestHandlerContext { + canvas: { + workpad: { + create: (attributes: CanvasWorkpad) => Promise>; + get: (id: string) => Promise>; + update: ( + id: string, + attributes: Partial + ) => Promise>; + }; + }; +} + +interface Deps { + expressions: ExpressionsService; +} + +export const createWorkpadRouteContext: ( + deps: Deps +) => RequestHandlerContextProvider = ({ expressions }) => { + return (context) => ({ + workpad: { + create: async (workpad: CanvasWorkpad) => { + const now = new Date().toISOString(); + const { id: maybeId, ...attributes } = workpad; + + const id = maybeId ? maybeId : getId('workpad'); + + const { workpad: extractedAttributes, references } = extractReferences( + attributes, + expressions + ); + + return await context.core.savedObjects.client.create( + CANVAS_TYPE, + { + ...extractedAttributes, + '@timestamp': now, + '@created': now, + }, + { id, references } + ); + }, + get: async (id: string) => { + const workpad = await context.core.savedObjects.client.get( + CANVAS_TYPE, + id + ); + + workpad.attributes = injectReferences(workpad.attributes, workpad.references, expressions); + + return workpad; + }, + update: async (id: string, { id: omittedId, ...workpad }: Partial) => { + const now = new Date().toISOString(); + + const workpadObject = await context.core.savedObjects.client.get( + CANVAS_TYPE, + id + ); + + const injectedAttributes = injectReferences( + workpadObject.attributes, + workpadObject.references, + expressions + ); + + const updatedAttributes = { + ...injectedAttributes, + ...workpad, + '@timestamp': now, // always update the modified time + '@created': workpadObject.attributes['@created'], // ensure created is not modified + } as WorkpadAttributes; + + const extracted = extractReferences(updatedAttributes, expressions); + + return await context.core.savedObjects.client.create(CANVAS_TYPE, extracted.workpad, { + overwrite: true, + id, + references: extracted.references, + }); + }, + }, + }); +}; diff --git a/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx b/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx index 6caabd04060c..01c0a633fbdb 100644 --- a/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx +++ b/x-pack/plugins/canvas/shareable_runtime/components/rendered_element.tsx @@ -10,8 +10,7 @@ import React, { FC, PureComponent } from 'react'; import Style from 'style-it'; import { AnyExpressionFunctionDefinition } from '../../../../../src/plugins/expressions'; import { Positionable } from '../../public/components/positionable/positionable'; -// @ts-expect-error untyped local -import { elementToShape } from '../../public/components/workpad_page/utils'; +import { elementToShape } from '../../public/components/workpad_page/positioning_utils'; import { CanvasRenderedElement } from '../types'; import { CanvasShareableContext, useCanvasShareableState } from '../context'; import { AnyRendererSpec } from '../../types'; diff --git a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js index d0bdc292619d..dc516060ea36 100644 --- a/x-pack/plugins/canvas/shareable_runtime/webpack.config.js +++ b/x-pack/plugins/canvas/shareable_runtime/webpack.config.js @@ -116,6 +116,7 @@ module.exports = { { loader: 'sass-loader', options: { + implementation: require('node-sass'), sourceMap: !isProd, }, }, @@ -146,12 +147,13 @@ module.exports = { { loader: 'sass-loader', options: { - prependData(loaderContext) { + additionalData(content, loaderContext) { return `@import ${stringifyRequest( loaderContext, path.resolve(KIBANA_ROOT, 'src/core/public/core_app/styles/_globals_v7light.scss') - )};\n`; + )};\n${content}`; }, + implementation: require('node-sass'), webpackImporter: false, sassOptions: { outputStyle: 'nested', diff --git a/x-pack/plugins/canvas/storybook/main.ts b/x-pack/plugins/canvas/storybook/main.ts index a043efd7c87f..5d77dd0fc85e 100644 --- a/x-pack/plugins/canvas/storybook/main.ts +++ b/x-pack/plugins/canvas/storybook/main.ts @@ -38,6 +38,9 @@ const canvasWebpack = { }, { loader: 'sass-loader', + options: { + implementation: require('node-sass'), + }, }, ], }, diff --git a/x-pack/plugins/cases/kibana.json b/x-pack/plugins/cases/kibana.json index 3889c559238b..ebac6295166d 100644 --- a/x-pack/plugins/cases/kibana.json +++ b/x-pack/plugins/cases/kibana.json @@ -10,7 +10,6 @@ "id":"cases", "kibanaVersion":"kibana", "optionalPlugins":[ - "ruleRegistry", "security", "spaces" ], diff --git a/x-pack/plugins/cases/public/components/create/form.test.tsx b/x-pack/plugins/cases/public/components/create/form.test.tsx index 9c3071fe27ee..c9d087a08ba2 100644 --- a/x-pack/plugins/cases/public/components/create/form.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { act, waitFor } from '@testing-library/react'; +import { act, render, waitFor } from '@testing-library/react'; import { useForm, Form, FormHook } from '../../common/shared_imports'; import { useGetTags } from '../../containers/use_get_tags'; @@ -119,4 +119,14 @@ describe('CreateCaseForm', () => { }); }); }); + + it('hides the sync alerts toggle', () => { + const { queryByText } = render( + + + + ); + + expect(queryByText('Sync alert')).not.toBeInTheDocument(); + }); }); diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index cbd4fd765425..d9b928753460 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -53,7 +53,6 @@ export const CreateCaseForm: React.FC = React.memo( withSteps = true, }) => { const { isSubmitting } = useFormContext(); - const firstStep = useMemo( () => ({ title: i18n.STEP_ONE_TITLE, diff --git a/x-pack/plugins/cases/server/client/alerts/get.ts b/x-pack/plugins/cases/server/client/alerts/get.ts index 391279aab5a8..2048ccae4fa6 100644 --- a/x-pack/plugins/cases/server/client/alerts/get.ts +++ b/x-pack/plugins/cases/server/client/alerts/get.ts @@ -12,11 +12,19 @@ export const get = async ( { alertsInfo }: AlertGet, clientArgs: CasesClientArgs ): Promise => { - const { alertsService, logger } = clientArgs; + const { alertsService, scopedClusterClient, logger } = clientArgs; if (alertsInfo.length === 0) { return []; } - const alerts = await alertsService.getAlerts({ alertsInfo, logger }); - return alerts ?? []; + const alerts = await alertsService.getAlerts({ alertsInfo, scopedClusterClient, logger }); + if (!alerts) { + return []; + } + + return alerts.docs.map((alert) => ({ + id: alert._id, + index: alert._index, + ...alert._source, + })); }; diff --git a/x-pack/plugins/cases/server/client/alerts/types.ts b/x-pack/plugins/cases/server/client/alerts/types.ts index 6b3a49f20d1e..95cd9ae33bff 100644 --- a/x-pack/plugins/cases/server/client/alerts/types.ts +++ b/x-pack/plugins/cases/server/client/alerts/types.ts @@ -7,7 +7,17 @@ import { CaseStatuses } from '../../../common/api'; import { AlertInfo } from '../../common'; -import { Alert } from '../../services/alerts/types'; + +interface Alert { + id: string; + index: string; + destination?: { + ip: string; + }; + source?: { + ip: string; + }; +} export type CasesClientGetAlertsResponse = Alert[]; diff --git a/x-pack/plugins/cases/server/client/alerts/update_status.ts b/x-pack/plugins/cases/server/client/alerts/update_status.ts index 9c8cc3326441..a0684b59241b 100644 --- a/x-pack/plugins/cases/server/client/alerts/update_status.ts +++ b/x-pack/plugins/cases/server/client/alerts/update_status.ts @@ -16,6 +16,6 @@ export const updateStatus = async ( { alerts }: UpdateAlertsStatusArgs, clientArgs: CasesClientArgs ): Promise => { - const { alertsService, logger } = clientArgs; - await alertsService.updateAlertsStatus({ alerts, logger }); + const { alertsService, scopedClusterClient, logger } = clientArgs; + await alertsService.updateAlertsStatus({ alerts, scopedClusterClient, logger }); }; diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index 5393a108d6af..166ae2ae6501 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -40,7 +40,12 @@ import { } from '../../services/user_actions/helpers'; import { AttachmentService, CasesService, CaseUserActionService } from '../../services'; -import { createCaseError, CommentableCase, isCommentRequestTypeGenAlert } from '../../common'; +import { + createCaseError, + CommentableCase, + createAlertUpdateRequest, + isCommentRequestTypeGenAlert, +} from '../../common'; import { CasesClientArgs, CasesClientInternal } from '..'; import { decodeCommentRequest } from '../utils'; @@ -190,9 +195,22 @@ const addGeneratedAlerts = async ( user: userDetails, commentReq: query, id: savedObjectID, - casesClientInternal, }); + if ( + (newComment.attributes.type === CommentType.alert || + newComment.attributes.type === CommentType.generatedAlert) && + caseInfo.attributes.settings.syncAlerts + ) { + const alertsToUpdate = createAlertUpdateRequest({ + comment: query, + status: subCase.attributes.status, + }); + await casesClientInternal.alerts.updateStatus({ + alerts: alertsToUpdate, + }); + } + await userActionService.bulkCreate({ unsecuredSavedObjectsClient, actions: [ @@ -368,9 +386,19 @@ export const addComment = async ( user: userInfo, commentReq: query, id: savedObjectID, - casesClientInternal, }); + if (newComment.attributes.type === CommentType.alert && updatedCase.settings.syncAlerts) { + const alertsToUpdate = createAlertUpdateRequest({ + comment: query, + status: updatedCase.status, + }); + + await casesClientInternal.alerts.updateStatus({ + alerts: alertsToUpdate, + }); + } + await userActionService.bulkCreate({ unsecuredSavedObjectsClient, actions: [ diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 80e69d53e9e8..3048cf01bb3b 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -6,7 +6,7 @@ */ import Boom from '@hapi/boom'; -import { SavedObjectsFindResponse, SavedObject, Logger } from 'kibana/server'; +import { SavedObjectsFindResponse, SavedObject } from 'kibana/server'; import { ActionConnector, @@ -22,16 +22,26 @@ import { import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { createIncident, getCommentContextFromAttributes } from './utils'; -import { - AlertInfo, - createCaseError, - flattenCaseSavedObject, - getAlertInfoFromComments, -} from '../../common'; +import { createCaseError, flattenCaseSavedObject, getAlertInfoFromComments } from '../../common'; import { CasesClient, CasesClientArgs, CasesClientInternal } from '..'; import { Operations } from '../../authorization'; import { casesConnectors } from '../../connectors'; -import { CasesClientGetAlertsResponse } from '../alerts/types'; + +/** + * Returns true if the case should be closed based on the configuration settings and whether the case + * is a collection. Collections are not closable because we aren't allowing their status to be changed. + * In the future we could allow push to close all the sub cases of a collection but that's not currently supported. + */ +function shouldCloseByPush( + configureSettings: SavedObjectsFindResponse, + caseInfo: SavedObject +): boolean { + return ( + configureSettings.total > 0 && + configureSettings.saved_objects[0].attributes.closure_type === 'close-by-pushing' && + caseInfo.attributes.type !== CaseType.collection + ); +} /** * Parameters for pushing a case to an external system @@ -96,7 +106,9 @@ export const push = async ( const alertsInfo = getAlertInfoFromComments(theCase?.comments); - const alerts = await getAlertsCatchErrors({ casesClientInternal, alertsInfo, logger }); + const alerts = await casesClientInternal.alerts.get({ + alertsInfo, + }); const getMappingsResponse = await casesClientInternal.configuration.getMappings({ connector: theCase.connector, @@ -266,38 +278,3 @@ export const push = async ( throw createCaseError({ message: `Failed to push case: ${error}`, error, logger }); } }; - -async function getAlertsCatchErrors({ - casesClientInternal, - alertsInfo, - logger, -}: { - casesClientInternal: CasesClientInternal; - alertsInfo: AlertInfo[]; - logger: Logger; -}): Promise { - try { - return await casesClientInternal.alerts.get({ - alertsInfo, - }); - } catch (error) { - logger.error(`Failed to retrieve alerts during push: ${error}`); - return []; - } -} - -/** - * Returns true if the case should be closed based on the configuration settings and whether the case - * is a collection. Collections are not closable because we aren't allowing their status to be changed. - * In the future we could allow push to close all the sub cases of a collection but that's not currently supported. - */ -function shouldCloseByPush( - configureSettings: SavedObjectsFindResponse, - caseInfo: SavedObject -): boolean { - return ( - configureSettings.total > 0 && - configureSettings.saved_objects[0].attributes.closure_type === 'close-by-pushing' && - caseInfo.attributes.type !== CaseType.collection - ); -} diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 611c9e09fa76..ed19444414d5 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -12,7 +12,6 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { - Logger, SavedObject, SavedObjectsClientContract, SavedObjectsFindResponse, @@ -308,14 +307,12 @@ async function updateAlerts({ caseService, unsecuredSavedObjectsClient, casesClientInternal, - logger, }: { casesWithSyncSettingChangedToOn: UpdateRequestWithOriginalCase[]; casesWithStatusChangedAndSynced: UpdateRequestWithOriginalCase[]; caseService: CasesService; unsecuredSavedObjectsClient: SavedObjectsClientContract; casesClientInternal: CasesClientInternal; - logger: Logger; }) { /** * It's possible that a case ID can appear multiple times in each array. I'm intentionally placing the status changes @@ -364,9 +361,7 @@ async function updateAlerts({ [] ); - await casesClientInternal.alerts.updateStatus({ - alerts: alertsToUpdate, - }); + await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); } function partitionPatchRequest( @@ -567,6 +562,15 @@ export const update = async ( ); }); + // Update the alert's status to match any case status or sync settings changes + await updateAlerts({ + casesWithStatusChangedAndSynced, + casesWithSyncSettingChangedToOn, + caseService, + unsecuredSavedObjectsClient, + casesClientInternal, + }); + const returnUpdatedCase = myCases.saved_objects .filter((myCase) => updatedCases.saved_objects.some((updatedCase) => updatedCase.id === myCase.id) @@ -594,17 +598,6 @@ export const update = async ( }), }); - // Update the alert's status to match any case status or sync settings changes - // Attempt to do this after creating/changing the other entities just in case it fails - await updateAlerts({ - casesWithStatusChangedAndSynced, - casesWithSyncSettingChangedToOn, - caseService, - unsecuredSavedObjectsClient, - casesClientInternal, - logger, - }); - return CasesResponseRt.encode(returnUpdatedCase); } catch (error) { const idVersions = cases.cases.map((caseInfo) => ({ diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index a1a3ccdd3bc5..2fae6996f4aa 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -5,7 +5,12 @@ * 2.0. */ -import { KibanaRequest, SavedObjectsServiceStart, Logger } from 'kibana/server'; +import { + KibanaRequest, + SavedObjectsServiceStart, + Logger, + ElasticsearchClient, +} from 'kibana/server'; import { SecurityPluginSetup, SecurityPluginStart } from '../../../security/server'; import { SAVED_OBJECT_TYPES } from '../../common'; import { Authorization } from '../authorization/authorization'; @@ -20,8 +25,8 @@ import { } from '../services'; import { PluginStartContract as FeaturesPluginStart } from '../../../features/server'; import { PluginStartContract as ActionsPluginStart } from '../../../actions/server'; -import { RuleRegistryPluginStartContract } from '../../../rule_registry/server'; import { LensServerPluginSetup } from '../../../lens/server'; + import { AuthorizationAuditLogger } from '../authorization'; import { CasesClient, createCasesClient } from '.'; @@ -31,7 +36,6 @@ interface CasesClientFactoryArgs { getSpace: GetSpaceFn; featuresPluginStart: FeaturesPluginStart; actionsPluginStart: ActionsPluginStart; - ruleRegistryPluginStart?: RuleRegistryPluginStartContract; lensEmbeddableFactory: LensServerPluginSetup['lensEmbeddableFactory']; } @@ -65,10 +69,12 @@ export class CasesClientFactory { */ public async create({ request, + scopedClusterClient, savedObjectsService, }: { request: KibanaRequest; savedObjectsService: SavedObjectsServiceStart; + scopedClusterClient: ElasticsearchClient; }): Promise { if (!this.isInitialized || !this.options) { throw new Error('CasesClientFactory must be initialized before calling create'); @@ -88,12 +94,9 @@ export class CasesClientFactory { const caseService = new CasesService(this.logger, this.options?.securityPluginStart?.authc); const userInfo = caseService.getUser({ request }); - const alertsClient = await this.options.ruleRegistryPluginStart?.getRacClientWithRequest( - request - ); - return createCasesClient({ - alertsService: new AlertService(alertsClient), + alertsService: new AlertService(), + scopedClusterClient, unsecuredSavedObjectsClient: savedObjectsService.getScopedClient(request, { includedHiddenTypes: SAVED_OBJECT_TYPES, // this tells the security plugin to not perform SO authorization and audit logging since we are handling diff --git a/x-pack/plugins/cases/server/client/sub_cases/update.ts b/x-pack/plugins/cases/server/client/sub_cases/update.ts index 56610ea6858e..c8cb96cbb6b8 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/update.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/update.ts @@ -246,9 +246,7 @@ async function updateAlerts({ [] ); - await casesClientInternal.alerts.updateStatus({ - alerts: alertsToUpdate, - }); + await casesClientInternal.alerts.updateStatus({ alerts: alertsToUpdate }); } catch (error) { throw createCaseError({ message: `Failed to update alert status while updating sub cases: ${JSON.stringify( @@ -357,6 +355,14 @@ export async function update({ ); }); + await updateAlerts({ + caseService, + unsecuredSavedObjectsClient, + casesClientInternal, + subCasesToSync: subCasesToSyncAlertsFor, + logger: clientArgs.logger, + }); + const returnUpdatedSubCases = updatedCases.saved_objects.reduce( (acc, updatedSO) => { const originalSubCase = subCasesMap.get(updatedSO.id); @@ -388,15 +394,6 @@ export async function update({ }), }); - // attempt to update the status of the alerts after creating all the user actions just in case it fails - await updateAlerts({ - caseService, - unsecuredSavedObjectsClient, - casesClientInternal, - subCasesToSync: subCasesToSyncAlertsFor, - logger: clientArgs.logger, - }); - return SubCasesResponseRt.encode(returnUpdatedSubCases); } catch (error) { const idVersions = query.subCases.map((subCase) => ({ diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index 3979c19949d9..27829d2539c7 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -6,7 +6,7 @@ */ import type { PublicMethodsOf } from '@kbn/utility-types'; -import { SavedObjectsClientContract, Logger } from 'kibana/server'; +import { ElasticsearchClient, SavedObjectsClientContract, Logger } from 'kibana/server'; import { User } from '../../common'; import { Authorization } from '../authorization/authorization'; import { @@ -24,6 +24,7 @@ import { LensServerPluginSetup } from '../../../lens/server'; * Parameters for initializing a cases client */ export interface CasesClientArgs { + readonly scopedClusterClient: ElasticsearchClient; readonly caseConfigureService: CaseConfigureService; readonly caseService: CasesService; readonly connectorMappingsService: ConnectorMappingsService; diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index e540332b1ff8..856d6378d590 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -34,16 +34,10 @@ import { CommentRequestUserType, CaseAttributes, } from '../../../common'; -import { - createAlertUpdateRequest, - flattenCommentSavedObjects, - flattenSubCaseSavedObject, - transformNewComment, -} from '..'; +import { flattenCommentSavedObjects, flattenSubCaseSavedObject, transformNewComment } from '..'; import { AttachmentService, CasesService } from '../../services'; import { createCaseError } from '../error'; import { countAlertsForID } from '../index'; -import { CasesClientInternal } from '../../client'; import { getOrUpdateLensReferences } from '../utils'; interface UpdateCommentResp { @@ -279,13 +273,11 @@ export class CommentableCase { user, commentReq, id, - casesClientInternal, }: { createdDate: string; user: User; commentReq: CommentRequest; id: string; - casesClientInternal: CasesClientInternal; }): Promise { try { if (commentReq.type === CommentType.alert) { @@ -302,10 +294,6 @@ export class CommentableCase { throw Boom.badRequest('The owner field of the comment must match the case'); } - // Let's try to sync the alert's status before creating the attachment, that way if the alert doesn't exist - // we'll throw an error early before creating the attachment - await this.syncAlertStatus(commentReq, casesClientInternal); - let references = this.buildRefsToCase(); if (commentReq.type === CommentType.user && commentReq?.comment) { @@ -343,26 +331,6 @@ export class CommentableCase { } } - private async syncAlertStatus( - commentRequest: CommentRequest, - casesClientInternal: CasesClientInternal - ) { - if ( - (commentRequest.type === CommentType.alert || - commentRequest.type === CommentType.generatedAlert) && - this.settings.syncAlerts - ) { - const alertsToUpdate = createAlertUpdateRequest({ - comment: commentRequest, - status: this.status, - }); - - await casesClientInternal.alerts.updateStatus({ - alerts: alertsToUpdate, - }); - } - } - private formatCollectionForEncoding(totalComment: number) { return { id: this.collection.id, diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts index 7a1efe8b366d..fa103d4c1142 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts @@ -24,7 +24,7 @@ describe('ITSM formatter', () => { } as CaseResponse; it('it formats correctly without alerts', async () => { - const res = format(theCase, []); + const res = await format(theCase, []); expect(res).toEqual({ dest_ip: null, source_ip: null, @@ -38,7 +38,7 @@ describe('ITSM formatter', () => { it('it formats correctly when fields do not exist ', async () => { const invalidFields = { connector: { fields: null } } as CaseResponse; - const res = format(invalidFields, []); + const res = await format(invalidFields, []); expect(res).toEqual({ dest_ip: null, source_ip: null, @@ -55,31 +55,25 @@ describe('ITSM formatter', () => { { id: 'alert-1', index: 'index-1', - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.2' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com' }, + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.2' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, }, + url: { full: 'https://attack.com' }, }, { id: 'alert-2', index: 'index-2', - source: { - source: { - ip: '192.168.1.3', - }, - destination: { ip: '192.168.1.4' }, - file: { - hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' }, - }, - url: { full: 'https://attack.com/api' }, + destination: { ip: '192.168.1.4' }, + source: { ip: '192.168.1.3' }, + file: { + hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' }, }, + url: { full: 'https://attack.com/api' }, }, ]; - const res = format(theCase, alerts); + const res = await format(theCase, alerts); expect(res).toEqual({ dest_ip: '192.168.1.1,192.168.1.4', source_ip: '192.168.1.2,192.168.1.3', @@ -92,109 +86,30 @@ describe('ITSM formatter', () => { }); }); - it('it ignores alerts with an error', async () => { - const alerts = [ - { - id: 'alert-1', - index: 'index-1', - error: new Error('an error'), - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.2' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com' }, - }, - }, - { - id: 'alert-2', - index: 'index-2', - source: { - source: { - ip: '192.168.1.3', - }, - destination: { ip: '192.168.1.4' }, - file: { - hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' }, - }, - url: { full: 'https://attack.com/api' }, - }, - }, - ]; - const res = format(theCase, alerts); - expect(res).toEqual({ - dest_ip: '192.168.1.4', - source_ip: '192.168.1.3', - category: 'Denial of Service', - subcategory: 'Inbound DDos', - malware_hash: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752', - malware_url: 'https://attack.com/api', - priority: '2 - High', - }); - }); - - it('it ignores alerts without a source field', async () => { - const alerts = [ - { - id: 'alert-1', - index: 'index-1', - }, - { - id: 'alert-2', - index: 'index-2', - source: { - source: { - ip: '192.168.1.3', - }, - destination: { ip: '192.168.1.4' }, - file: { - hash: { sha256: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752' }, - }, - url: { full: 'https://attack.com/api' }, - }, - }, - ]; - const res = format(theCase, alerts); - expect(res).toEqual({ - dest_ip: '192.168.1.4', - source_ip: '192.168.1.3', - category: 'Denial of Service', - subcategory: 'Inbound DDos', - malware_hash: '60303ae22b998861bce3b28f33eec1be758a213c86c93c076dbe9f558c11c752', - malware_url: 'https://attack.com/api', - priority: '2 - High', - }); - }); - it('it handles duplicates correctly', async () => { const alerts = [ { id: 'alert-1', index: 'index-1', - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.2' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com' }, + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.2' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, }, + url: { full: 'https://attack.com' }, }, { id: 'alert-2', index: 'index-2', - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.3' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com/api' }, + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.3' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, }, + url: { full: 'https://attack.com/api' }, }, ]; - const res = format(theCase, alerts); + const res = await format(theCase, alerts); expect(res).toEqual({ dest_ip: '192.168.1.1', source_ip: '192.168.1.2,192.168.1.3', @@ -211,26 +126,22 @@ describe('ITSM formatter', () => { { id: 'alert-1', index: 'index-1', - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.2' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com' }, + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.2' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, }, + url: { full: 'https://attack.com' }, }, { id: 'alert-2', index: 'index-2', - source: { - destination: { ip: '192.168.1.1' }, - source: { ip: '192.168.1.3' }, - file: { - hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, - }, - url: { full: 'https://attack.com/api' }, + destination: { ip: '192.168.1.1' }, + source: { ip: '192.168.1.3' }, + file: { + hash: { sha256: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08' }, }, + url: { full: 'https://attack.com/api' }, }, ]; @@ -239,7 +150,7 @@ describe('ITSM formatter', () => { connector: { fields: { ...theCase.connector.fields, destIp: false, malwareHash: false } }, } as CaseResponse; - const res = format(newCase, alerts); + const res = await format(newCase, alerts); expect(res).toEqual({ dest_ip: null, source_ip: '192.168.1.2,192.168.1.3', diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts index 88b8f79d3ba5..b48a1b7f734c 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts @@ -44,25 +44,23 @@ export const format: ServiceNowSIRFormat = (theCase, alerts) => { ); if (fieldsToAdd.length > 0) { - sirFields = alerts - .filter((alert) => !alert.error && alert.source != null) - .reduce>((acc, alert) => { - fieldsToAdd.forEach((alertField) => { - const field = get(alertFieldMapping[alertField].alertPath, alert.source); - if (field && !manageDuplicate[alertFieldMapping[alertField].sirFieldKey].has(field)) { - manageDuplicate[alertFieldMapping[alertField].sirFieldKey].add(field); - acc = { - ...acc, - [alertFieldMapping[alertField].sirFieldKey]: `${ - acc[alertFieldMapping[alertField].sirFieldKey] != null - ? `${acc[alertFieldMapping[alertField].sirFieldKey]},${field}` - : field - }`, - }; - } - }); - return acc; - }, sirFields); + sirFields = alerts.reduce>((acc, alert) => { + fieldsToAdd.forEach((alertField) => { + const field = get(alertFieldMapping[alertField].alertPath, alert); + if (field && !manageDuplicate[alertFieldMapping[alertField].sirFieldKey].has(field)) { + manageDuplicate[alertFieldMapping[alertField].sirFieldKey].add(field); + acc = { + ...acc, + [alertFieldMapping[alertField].sirFieldKey]: `${ + acc[alertFieldMapping[alertField].sirFieldKey] != null + ? `${acc[alertFieldMapping[alertField].sirFieldKey]},${field}` + : field + }`, + }; + } + }); + return acc; + }, sirFields); } return { diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 49220fc71603..bb1be163585a 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -32,7 +32,6 @@ import type { CasesRequestHandlerContext } from './types'; import { CasesClientFactory } from './client/factory'; import { SpacesPluginStart } from '../../spaces/server'; import { PluginStartContract as FeaturesPluginStart } from '../../features/server'; -import { RuleRegistryPluginStartContract } from '../../rule_registry/server'; import { LensServerPluginSetup } from '../../lens/server'; function createConfig(context: PluginInitializerContext) { @@ -50,7 +49,6 @@ export interface PluginsStart { features: FeaturesPluginStart; spaces?: SpacesPluginStart; actions: ActionsPluginStart; - ruleRegistry?: RuleRegistryPluginStartContract; } /** @@ -139,13 +137,15 @@ export class CasePlugin { }, featuresPluginStart: plugins.features, actionsPluginStart: plugins.actions, - ruleRegistryPluginStart: plugins.ruleRegistry, lensEmbeddableFactory: this.lensEmbeddableFactory!, }); + const client = core.elasticsearch.client; + const getCasesClientWithRequest = async (request: KibanaRequest): Promise => { return this.clientFactory.create({ request, + scopedClusterClient: client.asScoped(request).asCurrentUser, savedObjectsService: core.savedObjects, }); }; @@ -171,6 +171,7 @@ export class CasePlugin { return this.clientFactory.create({ request, + scopedClusterClient: context.core.elasticsearch.client.asCurrentUser, savedObjectsService: savedObjects, }); }, diff --git a/x-pack/plugins/cases/server/services/alerts/index.test.ts b/x-pack/plugins/cases/server/services/alerts/index.test.ts index 0e1ad03a32af..d7dd44b33628 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.test.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.test.ts @@ -7,73 +7,280 @@ import { CaseStatuses } from '../../../common'; import { AlertService, AlertServiceContract } from '.'; -import { loggingSystemMock } from 'src/core/server/mocks'; -import { ruleRegistryMocks } from '../../../../rule_registry/server/mocks'; -import { AlertsClient } from '../../../../rule_registry/server'; -import { PublicMethodsOf } from '@kbn/utility-types'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; +import { ALERT_WORKFLOW_STATUS } from '../../../../rule_registry/common/technical_rule_data_field_names'; describe('updateAlertsStatus', () => { + const esClient = elasticsearchServiceMock.createElasticsearchClient(); const logger = loggingSystemMock.create().get('case'); - let alertsClient: jest.Mocked>; - let alertService: AlertServiceContract; - - beforeEach(async () => { - alertsClient = ruleRegistryMocks.createAlertsClientMock.create(); - alertService = new AlertService(alertsClient); - jest.restoreAllMocks(); - }); describe('happy path', () => { - const args = { - alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses.closed }], - logger, - }; + let alertService: AlertServiceContract; + + beforeEach(async () => { + alertService = new AlertService(); + jest.resetAllMocks(); + }); it('updates the status of the alert correctly', async () => { + const args = { + alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses.closed }], + scopedClusterClient: esClient, + logger, + }; + await alertService.updateAlertsStatus(args); - expect(alertsClient.update).toHaveBeenCalledWith({ - id: 'alert-id-1', + expect(esClient.updateByQuery).toHaveBeenCalledWith({ index: '.siem-signals', - status: CaseStatuses.closed, + conflicts: 'abort', + body: { + script: { + source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) { + ctx._source['${ALERT_WORKFLOW_STATUS}'] = 'closed' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'closed' + }`, + lang: 'painless', + }, + query: { + ids: { + values: ['alert-id-1'], + }, + }, + }, + ignore_unavailable: true, }); }); - it('translates the in-progress status to acknowledged', async () => { - await alertService.updateAlertsStatus({ - alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: CaseStatuses['in-progress'] }], + it('buckets the alerts by index', async () => { + const args = { + alerts: [ + { id: 'id1', index: '1', status: CaseStatuses.closed }, + { id: 'id2', index: '1', status: CaseStatuses.closed }, + ], + scopedClusterClient: esClient, logger, - }); + }; - expect(alertsClient.update).toHaveBeenCalledWith({ - id: 'alert-id-1', - index: '.siem-signals', - status: 'acknowledged', + await alertService.updateAlertsStatus(args); + + expect(esClient.updateByQuery).toBeCalledTimes(1); + expect(esClient.updateByQuery).toHaveBeenCalledWith({ + index: '1', + conflicts: 'abort', + body: { + script: { + source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) { + ctx._source['${ALERT_WORKFLOW_STATUS}'] = 'closed' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'closed' + }`, + lang: 'painless', + }, + query: { + ids: { + values: ['id1', 'id2'], + }, + }, + }, + ignore_unavailable: true, }); }); - it('defaults an unknown status to open', async () => { - await alertService.updateAlertsStatus({ - alerts: [{ id: 'alert-id-1', index: '.siem-signals', status: 'bananas' as CaseStatuses }], + it('translates in-progress to acknowledged', async () => { + const args = { + alerts: [{ id: 'id1', index: '1', status: CaseStatuses['in-progress'] }], + scopedClusterClient: esClient, logger, - }); + }; - expect(alertsClient.update).toHaveBeenCalledWith({ - id: 'alert-id-1', - index: '.siem-signals', - status: 'open', - }); + await alertService.updateAlertsStatus(args); + + expect(esClient.updateByQuery).toBeCalledTimes(1); + expect(esClient.updateByQuery.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "ids": Object { + "values": Array [ + "id1", + ], + }, + }, + "script": Object { + "lang": "painless", + "source": "if (ctx._source['kibana.alert.workflow_status'] != null) { + ctx._source['kibana.alert.workflow_status'] = 'acknowledged' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'acknowledged' + }", + }, + }, + "conflicts": "abort", + "ignore_unavailable": true, + "index": "1", + }, + ] + `); + }); + + it('makes two calls when the statuses are different', async () => { + const args = { + alerts: [ + { id: 'id1', index: '1', status: CaseStatuses.closed }, + { id: 'id2', index: '1', status: CaseStatuses.open }, + ], + scopedClusterClient: esClient, + logger, + }; + + await alertService.updateAlertsStatus(args); + + expect(esClient.updateByQuery).toBeCalledTimes(2); + // id1 should be closed + expect(esClient.updateByQuery.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "ids": Object { + "values": Array [ + "id1", + ], + }, + }, + "script": Object { + "lang": "painless", + "source": "if (ctx._source['kibana.alert.workflow_status'] != null) { + ctx._source['kibana.alert.workflow_status'] = 'closed' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'closed' + }", + }, + }, + "conflicts": "abort", + "ignore_unavailable": true, + "index": "1", + }, + ] + `); + + // id2 should be open + expect(esClient.updateByQuery.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "ids": Object { + "values": Array [ + "id2", + ], + }, + }, + "script": Object { + "lang": "painless", + "source": "if (ctx._source['kibana.alert.workflow_status'] != null) { + ctx._source['kibana.alert.workflow_status'] = 'open' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'open' + }", + }, + }, + "conflicts": "abort", + "ignore_unavailable": true, + "index": "1", + }, + ] + `); + }); + + it('makes two calls when the indices are different', async () => { + const args = { + alerts: [ + { id: 'id1', index: '1', status: CaseStatuses.closed }, + { id: 'id2', index: '2', status: CaseStatuses.open }, + ], + scopedClusterClient: esClient, + logger, + }; + + await alertService.updateAlertsStatus(args); + + expect(esClient.updateByQuery).toBeCalledTimes(2); + // id1 should be closed in index 1 + expect(esClient.updateByQuery.mock.calls[0]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "ids": Object { + "values": Array [ + "id1", + ], + }, + }, + "script": Object { + "lang": "painless", + "source": "if (ctx._source['kibana.alert.workflow_status'] != null) { + ctx._source['kibana.alert.workflow_status'] = 'closed' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'closed' + }", + }, + }, + "conflicts": "abort", + "ignore_unavailable": true, + "index": "1", + }, + ] + `); + + // id2 should be open in index 2 + expect(esClient.updateByQuery.mock.calls[1]).toMatchInlineSnapshot(` + Array [ + Object { + "body": Object { + "query": Object { + "ids": Object { + "values": Array [ + "id2", + ], + }, + }, + "script": Object { + "lang": "painless", + "source": "if (ctx._source['kibana.alert.workflow_status'] != null) { + ctx._source['kibana.alert.workflow_status'] = 'open' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = 'open' + }", + }, + }, + "conflicts": "abort", + "ignore_unavailable": true, + "index": "2", + }, + ] + `); }); - }); - describe('unhappy path', () => { it('ignores empty indices', async () => { - expect( - await alertService.updateAlertsStatus({ - alerts: [{ id: 'alert-id-1', index: '', status: CaseStatuses.closed }], - logger, - }) - ).toBeUndefined(); + await alertService.updateAlertsStatus({ + alerts: [{ id: 'alert-id-1', index: '', status: CaseStatuses.open }], + scopedClusterClient: esClient, + logger, + }); + + expect(esClient.updateByQuery).not.toHaveBeenCalled(); }); }); }); diff --git a/x-pack/plugins/cases/server/services/alerts/index.ts b/x-pack/plugins/cases/server/services/alerts/index.ts index ccb0fca4f995..6bb2fb3ee3c5 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.ts @@ -5,71 +5,62 @@ * 2.0. */ +import pMap from 'p-map'; import { isEmpty } from 'lodash'; import type { PublicMethodsOf } from '@kbn/utility-types'; -import { Logger } from 'kibana/server'; -import { CaseStatuses, MAX_ALERTS_PER_SUB_CASE } from '../../../common'; +import { ElasticsearchClient, Logger } from 'kibana/server'; +import { CaseStatuses, MAX_ALERTS_PER_SUB_CASE, MAX_CONCURRENT_SEARCHES } from '../../../common'; import { AlertInfo, createCaseError } from '../../common'; import { UpdateAlertRequest } from '../../client/alerts/types'; -import { AlertsClient } from '../../../../rule_registry/server'; -import { Alert } from './types'; -import { STATUS_VALUES } from '../../../../rule_registry/common/technical_rule_data_field_names'; +import { + ALERT_WORKFLOW_STATUS, + STATUS_VALUES, +} from '../../../../rule_registry/common/technical_rule_data_field_names'; export type AlertServiceContract = PublicMethodsOf; interface UpdateAlertsStatusArgs { alerts: UpdateAlertRequest[]; + scopedClusterClient: ElasticsearchClient; logger: Logger; } interface GetAlertsArgs { alertsInfo: AlertInfo[]; + scopedClusterClient: ElasticsearchClient; logger: Logger; } +interface Alert { + _id: string; + _index: string; + _source: Record; +} + +interface AlertsResponse { + docs: Alert[]; +} + function isEmptyAlert(alert: AlertInfo): boolean { return isEmpty(alert.id) || isEmpty(alert.index); } export class AlertService { - constructor(private readonly alertsClient?: PublicMethodsOf) {} + constructor() {} - public async updateAlertsStatus({ alerts, logger }: UpdateAlertsStatusArgs) { + public async updateAlertsStatus({ alerts, scopedClusterClient, logger }: UpdateAlertsStatusArgs) { try { - if (!this.alertsClient) { - throw new Error( - 'Alert client is undefined, the rule registry plugin must be enabled to updated the status of alerts' - ); - } - - const alertsToUpdate = alerts.filter((alert) => !isEmptyAlert(alert)); - - if (alertsToUpdate.length <= 0) { - return; - } - - const updatedAlerts = await Promise.allSettled( - alertsToUpdate.map((alert) => - this.alertsClient?.update({ - id: alert.id, - index: alert.index, - status: translateStatus({ alert, logger }), - _version: undefined, - }) - ) + const bucketedAlerts = bucketAlertsByIndexAndStatus(alerts, logger); + const indexBuckets = Array.from(bucketedAlerts.entries()); + + await pMap( + indexBuckets, + async (indexBucket: [string, Map]) => + updateByQuery(indexBucket, scopedClusterClient), + { concurrency: MAX_CONCURRENT_SEARCHES } ); - - updatedAlerts.forEach((updatedAlert, index) => { - if (updatedAlert.status === 'rejected') { - logger.error( - `Failed to update status for alert: ${JSON.stringify(alertsToUpdate[index])}: ${ - updatedAlert.reason - }` - ); - } - }); } catch (error) { throw createCaseError({ message: `Failed to update alert status ids: ${JSON.stringify(alerts)}: ${error}`, @@ -79,51 +70,25 @@ export class AlertService { } } - public async getAlerts({ alertsInfo, logger }: GetAlertsArgs): Promise { + public async getAlerts({ + scopedClusterClient, + alertsInfo, + logger, + }: GetAlertsArgs): Promise { try { - if (!this.alertsClient) { - throw new Error( - 'Alert client is undefined, the rule registry plugin must be enabled to retrieve alerts' - ); - } + const docs = alertsInfo + .filter((alert) => !isEmptyAlert(alert)) + .slice(0, MAX_ALERTS_PER_SUB_CASE) + .map((alert) => ({ _id: alert.id, _index: alert.index })); - const alertsToGet = alertsInfo - .filter((alert) => !isEmpty(alert)) - .slice(0, MAX_ALERTS_PER_SUB_CASE); - - if (alertsToGet.length <= 0) { + if (docs.length <= 0) { return; } - const retrievedAlerts = await Promise.allSettled( - alertsToGet.map(({ id, index }) => this.alertsClient?.get({ id, index })) - ); - - retrievedAlerts.forEach((alert, index) => { - if (alert.status === 'rejected') { - logger.error( - `Failed to retrieve alert: ${JSON.stringify(alertsToGet[index])}: ${alert.reason}` - ); - } - }); + const results = await scopedClusterClient.mget({ body: { docs } }); - return retrievedAlerts.map((alert, index) => { - let source: unknown | undefined; - let error: Error | undefined; - - if (alert.status === 'fulfilled') { - source = alert.value; - } else { - error = alert.reason; - } - - return { - id: alertsToGet[index].id, - index: alertsToGet[index].index, - source, - error, - }; - }); + // @ts-expect-error @elastic/elasticsearch _source is optional + return results.body; } catch (error) { throw createCaseError({ message: `Failed to retrieve alerts ids: ${JSON.stringify(alertsInfo)}: ${error}`, @@ -134,6 +99,44 @@ export class AlertService { } } +interface TranslatedUpdateAlertRequest { + id: string; + index: string; + status: STATUS_VALUES; +} + +function bucketAlertsByIndexAndStatus( + alerts: UpdateAlertRequest[], + logger: Logger +): Map> { + return alerts.reduce>>( + (acc, alert) => { + // skip any alerts that are empty + if (isEmptyAlert(alert)) { + return acc; + } + + const translatedAlert = { ...alert, status: translateStatus({ alert, logger }) }; + const statusToAlertId = acc.get(translatedAlert.index); + + // if we haven't seen the index before + if (!statusToAlertId) { + // add a new index in the parent map, with an entry for the status the alert set to pointing + // to an initial array of only the current alert + acc.set(translatedAlert.index, createStatusToAlertMap(translatedAlert)); + } else { + // We had the index in the map so check to see if we have a bucket for the + // status, if not add a new status entry with the alert, if so update the status entry + // with the alert + updateIndexEntryWithStatus(statusToAlertId, translatedAlert); + } + + return acc; + }, + new Map() + ); +} + function translateStatus({ alert, logger, @@ -157,3 +160,53 @@ function translateStatus({ } return translatedStatus ?? 'open'; } + +function createStatusToAlertMap( + alert: TranslatedUpdateAlertRequest +): Map { + return new Map([[alert.status, [alert]]]); +} + +function updateIndexEntryWithStatus( + statusToAlerts: Map, + alert: TranslatedUpdateAlertRequest +) { + const statusBucket = statusToAlerts.get(alert.status); + + if (!statusBucket) { + statusToAlerts.set(alert.status, [alert]); + } else { + statusBucket.push(alert); + } +} + +async function updateByQuery( + [index, statusToAlertMap]: [string, Map], + scopedClusterClient: ElasticsearchClient +) { + const statusBuckets = Array.from(statusToAlertMap); + return Promise.all( + // this will create three update by query calls one for each of the three statuses + statusBuckets.map(([status, translatedAlerts]) => + scopedClusterClient.updateByQuery({ + index, + conflicts: 'abort', + body: { + script: { + source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) { + ctx._source['${ALERT_WORKFLOW_STATUS}'] = '${status}' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = '${status}' + }`, + lang: 'painless', + }, + // the query here will contain all the ids that have the same status for the same index + // being updated + query: { ids: { values: translatedAlerts.map(({ id }) => id) } }, + }, + ignore_unavailable: true, + }) + ) + ); +} diff --git a/x-pack/plugins/data_visualizer/public/lazy_load_bundle/index.ts b/x-pack/plugins/data_visualizer/public/lazy_load_bundle/index.ts index 57f0872d6258..f04c611c2fae 100644 --- a/x-pack/plugins/data_visualizer/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/data_visualizer/public/lazy_load_bundle/index.ts @@ -22,13 +22,13 @@ export async function lazyLoadModules(): Promise { return loadModulesPromise; } - loadModulesPromise = new Promise(async (resolve) => { - const lazyImports = await import('./lazy'); - - resolve({ - ...lazyImports, - getHttp: () => getCoreStart().http, - }); + loadModulesPromise = new Promise(async (resolve, reject) => { + try { + const lazyImports = await import('./lazy'); + resolve({ ...lazyImports, getHttp: () => getCoreStart().http }); + } catch (error) { + reject(error); + } }); return loadModulesPromise; } diff --git a/x-pack/plugins/discover_enhanced/kibana.json b/x-pack/plugins/discover_enhanced/kibana.json index 8d8ac11f8352..cb05cbd3e724 100644 --- a/x-pack/plugins/discover_enhanced/kibana.json +++ b/x-pack/plugins/discover_enhanced/kibana.json @@ -9,7 +9,7 @@ "configPath": ["xpack", "discoverEnhanced"], "requiredBundles": ["kibanaUtils", "data"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Data Discovery", + "githubTeam": "kibana-data-discovery" } } diff --git a/x-pack/plugins/file_upload/public/lazy_load_bundle/index.ts b/x-pack/plugins/file_upload/public/lazy_load_bundle/index.ts index 9c7c6ff1e518..192a7ffb5e78 100644 --- a/x-pack/plugins/file_upload/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/file_upload/public/lazy_load_bundle/index.ts @@ -44,15 +44,18 @@ export async function lazyLoadModules(): Promise { return loadModulesPromise; } - loadModulesPromise = new Promise(async (resolve) => { - const { JsonUploadAndParse, importerFactory, IndexNameForm } = await import('./lazy'); - - resolve({ - JsonUploadAndParse, - importerFactory, - getHttp, - IndexNameForm, - }); + loadModulesPromise = new Promise(async (resolve, reject) => { + try { + const { JsonUploadAndParse, importerFactory, IndexNameForm } = await import('./lazy'); + resolve({ + JsonUploadAndParse, + importerFactory, + getHttp, + IndexNameForm, + }); + } catch (error) { + reject(error); + } }); return loadModulesPromise; } diff --git a/x-pack/plugins/fleet/common/services/validate_package_policy.ts b/x-pack/plugins/fleet/common/services/validate_package_policy.ts index 5107cbf8121d..67df65b2f12b 100644 --- a/x-pack/plugins/fleet/common/services/validate_package_policy.ts +++ b/x-pack/plugins/fleet/common/services/validate_package_policy.ts @@ -75,7 +75,7 @@ export const validatePackagePolicy = ( const packageVars = Object.entries(packagePolicy.vars || {}); if (packageVars.length) { validationResults.vars = packageVars.reduce((results, [name, varEntry]) => { - results[name] = validatePackagePolicyConfig(varEntry, packageVarsByName[name]); + results[name] = validatePackagePolicyConfig(varEntry, packageVarsByName[name], name); return results; }, {} as ValidationEntry); } @@ -138,7 +138,8 @@ export const validatePackagePolicy = ( results[name] = input.enabled ? validatePackagePolicyConfig( configEntry, - inputVarDefsByPolicyTemplateAndType[inputKey][name] + inputVarDefsByPolicyTemplateAndType[inputKey][name], + name ) : null; return results; @@ -161,7 +162,7 @@ export const validatePackagePolicy = ( (results, [name, configEntry]) => { results[name] = streamVarDefs && streamVarDefs[name] && input.enabled && stream.enabled - ? validatePackagePolicyConfig(configEntry, streamVarDefs[name]) + ? validatePackagePolicyConfig(configEntry, streamVarDefs[name], name) : null; return results; }, @@ -183,12 +184,14 @@ export const validatePackagePolicy = ( if (Object.entries(validationResults.inputs!).length === 0) { validationResults.inputs = null; } + return validationResults; }; export const validatePackagePolicyConfig = ( configEntry: PackagePolicyConfigRecordEntry, - varDef: RegistryVarsEntry + varDef: RegistryVarsEntry, + varName: string ): string[] | null => { const errors = []; const { value } = configEntry; @@ -198,6 +201,13 @@ export const validatePackagePolicyConfig = ( parsedValue = value.trim(); } + if (varDef === undefined) { + // eslint-disable-next-line no-console + console.debug(`No variable definition for ${varName} found`); + + return null; + } + if (varDef.required) { if (parsedValue === undefined || (typeof parsedValue === 'string' && !parsedValue)) { errors.push( diff --git a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx index 3b0ab9c62ca1..3149a454c6c5 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/hooks/use_breadcrumbs.tsx @@ -113,6 +113,24 @@ const breadcrumbGetters: { }), }, ], + upgrade_package_policy: ({ policyName, policyId }) => [ + BASE_BREADCRUMB, + { + href: pagePathGetters.policies()[1], + text: i18n.translate('xpack.fleet.breadcrumbs.policiesPageTitle', { + defaultMessage: 'Agent policies', + }), + }, + { + href: pagePathGetters.policy_details({ policyId })[1], + text: policyName, + }, + { + text: i18n.translate('xpack.fleet.breadcrumbs.upgradePacagePolicyPageTitle', { + defaultMessage: 'Upgrade integration ', + }), + }, + ], agent_list: () => [ BASE_BREADCRUMB, { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/has_invalid_but_required_var.ts b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/has_invalid_but_required_var.ts index e204d86b5151..bf75b05f41b8 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/has_invalid_but_required_var.ts +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/services/has_invalid_but_required_var.ts @@ -22,7 +22,11 @@ export const hasInvalidButRequiredVar = ( registryVar.required && (!packagePolicyVars || !packagePolicyVars[registryVar.name] || - validatePackagePolicyConfig(packagePolicyVars[registryVar.name], registryVar)?.length) + validatePackagePolicyConfig( + packagePolicyVars[registryVar.name], + registryVar, + registryVar.name + )?.length) ) ) ); diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx index 67cff3e2d030..ea027f95eb9e 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/edit_package_policy_page/index.tsx @@ -525,15 +525,14 @@ export const EditPackagePolicyForm = memo<{ /> ) : ( <> - {from === 'package' || from === 'package-edit' ? ( - - ) : ( - - )} + {formState === 'CONFIRM' && ( setFormState('VALID')} /> )} - {isUpgrade && dryRunData && ( <> )} - {configurePackage} - {/* Extra space to accomodate the EuiBottomBar height */} - @@ -602,14 +597,56 @@ export const EditPackagePolicyForm = memo<{ ); }); -const PoliciesBreadcrumb: React.FunctionComponent<{ policyName: string; policyId: string }> = ({ - policyName, - policyId, -}) => { +const Breadcrumb = memo<{ + agentPolicyName: string; + from: EditPackagePolicyFrom; + packagePolicyName: string; + pkgkey: string; + pkgTitle: string; + policyId: string; +}>(({ agentPolicyName, from, packagePolicyName, pkgkey, pkgTitle, policyId }) => { + let breadcrumb = ; + + if ( + from === 'package' || + from === 'package-edit' || + from === 'upgrade-from-integrations-policy-list' + ) { + breadcrumb = ( + + ); + } else if (from === 'upgrade-from-fleet-policy-list') { + breadcrumb = ; + } + + return breadcrumb; +}); + +const IntegrationsBreadcrumb = memo<{ + pkgTitle: string; + policyName: string; + pkgkey: string; +}>(({ pkgTitle, policyName, pkgkey }) => { + useIntegrationsBreadcrumbs('integration_policy_edit', { policyName, pkgTitle, pkgkey }); + return null; +}); + +const PoliciesBreadcrumb: React.FunctionComponent<{ + policyName: string; + policyId: string; +}> = ({ policyName, policyId }) => { useBreadcrumbs('edit_integration', { policyName, policyId }); return null; }; +const UpgradeBreadcrumb: React.FunctionComponent<{ + policyName: string; + policyId: string; +}> = ({ policyName, policyId }) => { + useBreadcrumbs('upgrade_package_policy', { policyName, policyId }); + return null; +}; + const UpgradeStatusCallout: React.FunctionComponent<{ dryRunData: UpgradePackagePolicyDryRunResponse; }> = ({ dryRunData }) => { @@ -658,7 +695,7 @@ const UpgradeStatusCallout: React.FunctionComponent<{ )} - {isReadyForUpgrade ? ( + {isReadyForUpgrade && currentPackagePolicy ? ( ); }; - -const IntegrationsBreadcrumb = memo<{ - pkgTitle: string; - policyName: string; - pkgkey: string; -}>(({ pkgTitle, policyName, pkgkey }) => { - useIntegrationsBreadcrumbs('integration_policy_edit', { policyName, pkgTitle, pkgkey }); - return null; -}); diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 6f74550ad45b..8ff3c20b7aa1 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -944,7 +944,7 @@ export function overridePackageInputs( // If there's no corresponding input on the original package policy, just // take the override value from the new package as-is. This case typically // occurs when inputs or package policies are added/removed between versions. - if (!originalInput) { + if (originalInput === undefined) { inputs.push(override as NewPackagePolicyInput); continue; } @@ -958,7 +958,7 @@ export function overridePackageInputs( } if (override.vars) { - originalInput = deepMergeVars(originalInput, override); + originalInput = deepMergeVars(originalInput, override) as NewPackagePolicyInput; } if (override.streams) { @@ -967,6 +967,11 @@ export function overridePackageInputs( (s) => s.data_stream.dataset === stream.data_stream.dataset ); + if (originalStream === undefined) { + originalInput.streams.push(stream); + continue; + } + if (typeof stream.enabled !== 'undefined' && originalStream) { originalStream.enabled = stream.enabled; } @@ -1015,12 +1020,12 @@ export function overridePackageInputs( } function deepMergeVars(original: any, override: any): any { - const result = { ...original }; - - if (!result.vars || !override.vars) { - return; + if (!original.vars) { + original.vars = { ...override.vars }; } + const result = { ...original }; + const overrideVars = Array.isArray(override.vars) ? override.vars : Object.entries(override.vars!).map(([key, rest]) => ({ @@ -1030,11 +1035,6 @@ function deepMergeVars(original: any, override: any): any { for (const { name, ...overrideVal } of overrideVars) { const originalVar = original.vars[name]; - - if (!result.vars) { - result.vars = {}; - } - result.vars[name] = { ...overrideVal, ...originalVar }; } diff --git a/x-pack/plugins/graph/kibana.json b/x-pack/plugins/graph/kibana.json index e13cd8a0adba..463893e2425a 100644 --- a/x-pack/plugins/graph/kibana.json +++ b/x-pack/plugins/graph/kibana.json @@ -9,7 +9,7 @@ "configPath": ["xpack", "graph"], "requiredBundles": ["kibanaUtils", "kibanaReact", "home"], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" } } diff --git a/x-pack/plugins/graph/public/_main.scss b/x-pack/plugins/graph/public/_main.scss index 6b32de32c06d..22a849b0b2a6 100644 --- a/x-pack/plugins/graph/public/_main.scss +++ b/x-pack/plugins/graph/public/_main.scss @@ -21,6 +21,7 @@ */ .gphNoUserSelect { + padding-right: $euiSizeXS; user-select: none; -webkit-touch-callout: none; -webkit-tap-highlight-color: transparent; diff --git a/x-pack/plugins/graph/public/angular/templates/_index.scss b/x-pack/plugins/graph/public/angular/templates/_index.scss deleted file mode 100644 index 0e603b5c98cb..000000000000 --- a/x-pack/plugins/graph/public/angular/templates/_index.scss +++ /dev/null @@ -1,3 +0,0 @@ -@import './graph'; -@import './sidebar'; -@import './inspect'; diff --git a/x-pack/plugins/graph/public/angular/templates/index.html b/x-pack/plugins/graph/public/angular/templates/index.html deleted file mode 100644 index 14c37cab9d9f..000000000000 --- a/x-pack/plugins/graph/public/angular/templates/index.html +++ /dev/null @@ -1,362 +0,0 @@ -
- - - - - - - - - -
- -
-
- - - - -
- - -
diff --git a/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html b/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html deleted file mode 100644 index b2363ffbaa64..000000000000 --- a/x-pack/plugins/graph/public/angular/templates/listing_ng_wrapper.html +++ /dev/null @@ -1,13 +0,0 @@ - diff --git a/x-pack/plugins/graph/public/app.js b/x-pack/plugins/graph/public/app.js deleted file mode 100644 index 13661798cabe..000000000000 --- a/x-pack/plugins/graph/public/app.js +++ /dev/null @@ -1,646 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { Provider } from 'react-redux'; -import { isColorDark, hexToRgb } from '@elastic/eui'; - -import { toMountPoint } from '../../../../src/plugins/kibana_react/public'; -import { showSaveModal } from '../../../../src/plugins/saved_objects/public'; - -import appTemplate from './angular/templates/index.html'; -import listingTemplate from './angular/templates/listing_ng_wrapper.html'; -import { getReadonlyBadge } from './badge'; - -import { GraphApp } from './components/app'; -import { VennDiagram } from './components/venn_diagram'; -import { Listing } from './components/listing'; -import { Settings } from './components/settings'; -import { GraphVisualization } from './components/graph_visualization'; - -import { createWorkspace } from './angular/graph_client_workspace.js'; -import { getEditUrl, getNewPath, getEditPath, setBreadcrumbs } from './services/url'; -import { createCachedIndexPatternProvider } from './services/index_pattern_cache'; -import { urlTemplateRegex } from './helpers/url_template'; -import { asAngularSyncedObservable } from './helpers/as_observable'; -import { colorChoices } from './helpers/style_choices'; -import { createGraphStore, datasourceSelector, hasFieldsSelector } from './state_management'; -import { formatHttpError } from './helpers/format_http_error'; -import { - findSavedWorkspace, - getSavedWorkspace, - deleteSavedWorkspace, -} from './helpers/saved_workspace_utils'; -import { InspectPanel } from './components/inspect_panel/inspect_panel'; - -export function initGraphApp(angularModule, deps) { - const { - chrome, - toastNotifications, - savedObjectsClient, - indexPatterns, - addBasePath, - getBasePath, - data, - capabilities, - coreStart, - storage, - canEditDrillDownUrls, - graphSavePolicy, - overlays, - savedObjects, - setHeaderActionMenu, - uiSettings, - } = deps; - - const app = angularModule; - - app.directive('vennDiagram', function (reactDirective) { - return reactDirective(VennDiagram); - }); - - app.directive('graphVisualization', function (reactDirective) { - return reactDirective(GraphVisualization); - }); - - app.directive('graphListing', function (reactDirective) { - return reactDirective(Listing, [ - ['coreStart', { watchDepth: 'reference' }], - ['createItem', { watchDepth: 'reference' }], - ['findItems', { watchDepth: 'reference' }], - ['deleteItems', { watchDepth: 'reference' }], - ['editItem', { watchDepth: 'reference' }], - ['getViewUrl', { watchDepth: 'reference' }], - ['listingLimit', { watchDepth: 'reference' }], - ['hideWriteControls', { watchDepth: 'reference' }], - ['capabilities', { watchDepth: 'reference' }], - ['initialFilter', { watchDepth: 'reference' }], - ['initialPageSize', { watchDepth: 'reference' }], - ]); - }); - - app.directive('graphApp', function (reactDirective) { - return reactDirective( - GraphApp, - [ - ['storage', { watchDepth: 'reference' }], - ['isInitialized', { watchDepth: 'reference' }], - ['currentIndexPattern', { watchDepth: 'reference' }], - ['indexPatternProvider', { watchDepth: 'reference' }], - ['isLoading', { watchDepth: 'reference' }], - ['onQuerySubmit', { watchDepth: 'reference' }], - ['initialQuery', { watchDepth: 'reference' }], - ['confirmWipeWorkspace', { watchDepth: 'reference' }], - ['coreStart', { watchDepth: 'reference' }], - ['noIndexPatterns', { watchDepth: 'reference' }], - ['reduxStore', { watchDepth: 'reference' }], - ['pluginDataStart', { watchDepth: 'reference' }], - ], - { restrict: 'A' } - ); - }); - - app.directive('graphVisualization', function (reactDirective) { - return reactDirective(GraphVisualization, undefined, { restrict: 'A' }); - }); - - app.directive('inspectPanel', function (reactDirective) { - return reactDirective( - InspectPanel, - [ - ['showInspect', { watchDepth: 'reference' }], - ['lastRequest', { watchDepth: 'reference' }], - ['lastResponse', { watchDepth: 'reference' }], - ['indexPattern', { watchDepth: 'reference' }], - ['uiSettings', { watchDepth: 'reference' }], - ], - { restrict: 'E' }, - { - uiSettings, - } - ); - }); - - app.config(function ($routeProvider) { - $routeProvider - .when('/home', { - template: listingTemplate, - badge: getReadonlyBadge, - controller: function ($location, $scope) { - $scope.listingLimit = savedObjects.settings.getListingLimit(); - $scope.initialPageSize = savedObjects.settings.getPerPage(); - $scope.create = () => { - $location.url(getNewPath()); - }; - $scope.find = (search) => { - return findSavedWorkspace( - { savedObjectsClient, basePath: coreStart.http.basePath }, - search, - $scope.listingLimit - ); - }; - $scope.editItem = (workspace) => { - $location.url(getEditPath(workspace)); - }; - $scope.getViewUrl = (workspace) => getEditUrl(addBasePath, workspace); - $scope.delete = (workspaces) => - deleteSavedWorkspace( - savedObjectsClient, - workspaces.map(({ id }) => id) - ); - $scope.capabilities = capabilities; - $scope.initialFilter = $location.search().filter || ''; - $scope.coreStart = coreStart; - setBreadcrumbs({ chrome }); - }, - }) - .when('/workspace/:id?', { - template: appTemplate, - badge: getReadonlyBadge, - resolve: { - savedWorkspace: function ($rootScope, $route, $location) { - return $route.current.params.id - ? getSavedWorkspace(savedObjectsClient, $route.current.params.id).catch(function (e) { - toastNotifications.addError(e, { - title: i18n.translate('xpack.graph.missingWorkspaceErrorMessage', { - defaultMessage: "Couldn't load graph with ID", - }), - }); - $rootScope.$eval(() => { - $location.path('/home'); - $location.replace(); - }); - // return promise that never returns to prevent the controller from loading - return new Promise(); - }) - : getSavedWorkspace(savedObjectsClient); - }, - indexPatterns: function () { - return savedObjectsClient - .find({ - type: 'index-pattern', - fields: ['title', 'type'], - perPage: 10000, - }) - .then((response) => response.savedObjects); - }, - GetIndexPatternProvider: function () { - return indexPatterns; - }, - }, - }) - .otherwise({ - redirectTo: '/home', - }); - }); - - //======== Controller for basic UI ================== - app.controller('graphuiPlugin', function ($scope, $route, $location) { - function handleError(err) { - const toastTitle = i18n.translate('xpack.graph.errorToastTitle', { - defaultMessage: 'Graph Error', - description: '"Graph" is a product name and should not be translated.', - }); - if (err instanceof Error) { - toastNotifications.addError(err, { - title: toastTitle, - }); - } else { - toastNotifications.addDanger({ - title: toastTitle, - text: String(err), - }); - } - } - - async function handleHttpError(error) { - toastNotifications.addDanger(formatHttpError(error)); - } - - // Replacement function for graphClientWorkspace's comms so - // that it works with Kibana. - function callNodeProxy(indexName, query, responseHandler) { - const request = { - body: JSON.stringify({ - index: indexName, - query: query, - }), - }; - $scope.loading = true; - return coreStart.http - .post('../api/graph/graphExplore', request) - .then(function (data) { - const response = data.resp; - if (response.timed_out) { - toastNotifications.addWarning( - i18n.translate('xpack.graph.exploreGraph.timedOutWarningText', { - defaultMessage: 'Exploration timed out', - }) - ); - } - responseHandler(response); - }) - .catch(handleHttpError) - .finally(() => { - $scope.loading = false; - $scope.$digest(); - }); - } - - //Helper function for the graphClientWorkspace to perform a query - const callSearchNodeProxy = function (indexName, query, responseHandler) { - const request = { - body: JSON.stringify({ - index: indexName, - body: query, - }), - }; - $scope.loading = true; - coreStart.http - .post('../api/graph/searchProxy', request) - .then(function (data) { - const response = data.resp; - responseHandler(response); - }) - .catch(handleHttpError) - .finally(() => { - $scope.loading = false; - $scope.$digest(); - }); - }; - - $scope.indexPatternProvider = createCachedIndexPatternProvider( - $route.current.locals.GetIndexPatternProvider.get - ); - - const store = createGraphStore({ - basePath: getBasePath(), - addBasePath, - indexPatternProvider: $scope.indexPatternProvider, - indexPatterns: $route.current.locals.indexPatterns, - createWorkspace: (indexPattern, exploreControls) => { - const options = { - indexName: indexPattern, - vertex_fields: [], - // Here we have the opportunity to look up labels for nodes... - nodeLabeller: function () { - // console.log(newNodes); - }, - changeHandler: function () { - //Allows DOM to update with graph layout changes. - $scope.$apply(); - }, - graphExploreProxy: callNodeProxy, - searchProxy: callSearchNodeProxy, - exploreControls, - }; - $scope.workspace = createWorkspace(options); - }, - setLiveResponseFields: (fields) => { - $scope.liveResponseFields = fields; - }, - setUrlTemplates: (urlTemplates) => { - $scope.urlTemplates = urlTemplates; - }, - getWorkspace: () => { - return $scope.workspace; - }, - getSavedWorkspace: () => { - return $route.current.locals.savedWorkspace; - }, - notifications: coreStart.notifications, - http: coreStart.http, - overlays: coreStart.overlays, - savedObjectsClient, - showSaveModal, - setWorkspaceInitialized: () => { - $scope.workspaceInitialized = true; - }, - savePolicy: graphSavePolicy, - changeUrl: (newUrl) => { - $scope.$evalAsync(() => { - $location.url(newUrl); - }); - }, - notifyAngular: () => { - $scope.$digest(); - }, - chrome, - I18nContext: coreStart.i18n.Context, - }); - - // register things on scope passed down to react components - $scope.pluginDataStart = data; - $scope.storage = storage; - $scope.coreStart = coreStart; - $scope.loading = false; - $scope.reduxStore = store; - $scope.savedWorkspace = $route.current.locals.savedWorkspace; - - // register things for legacy angular UI - const allSavingDisabled = graphSavePolicy === 'none'; - $scope.spymode = 'request'; - $scope.colors = colorChoices; - $scope.isColorDark = (color) => isColorDark(...hexToRgb(color)); - $scope.nodeClick = function (n, $event) { - //Selection logic - shift key+click helps selects multiple nodes - // Without the shift key we deselect all prior selections (perhaps not - // a great idea for touch devices with no concept of shift key) - if (!$event.shiftKey) { - const prevSelection = n.isSelected; - $scope.workspace.selectNone(); - n.isSelected = prevSelection; - } - - if ($scope.workspace.toggleNodeSelection(n)) { - $scope.selectSelected(n); - } else { - $scope.detail = null; - } - }; - - $scope.clickEdge = function (edge) { - $scope.workspace.getAllIntersections($scope.handleMergeCandidatesCallback, [ - edge.topSrc, - edge.topTarget, - ]); - }; - - $scope.submit = function (searchTerm) { - $scope.workspaceInitialized = true; - const numHops = 2; - if (searchTerm.startsWith('{')) { - try { - const query = JSON.parse(searchTerm); - if (query.vertices) { - // Is a graph explore request - $scope.workspace.callElasticsearch(query); - } else { - // Is a regular query DSL query - $scope.workspace.search(query, $scope.liveResponseFields, numHops); - } - } catch (err) { - handleError(err); - } - return; - } - $scope.workspace.simpleSearch(searchTerm, $scope.liveResponseFields, numHops); - }; - - $scope.selectSelected = function (node) { - $scope.detail = { - latestNodeSelection: node, - }; - return ($scope.selectedSelectedVertex = node); - }; - - $scope.isSelectedSelected = function (node) { - return $scope.selectedSelectedVertex === node; - }; - - $scope.openUrlTemplate = function (template) { - const url = template.url; - const newUrl = url.replace(urlTemplateRegex, template.encoder.encode($scope.workspace)); - window.open(newUrl, '_blank'); - }; - - $scope.aceLoaded = (editor) => { - editor.$blockScrolling = Infinity; - }; - - $scope.setDetail = function (data) { - $scope.detail = data; - }; - - function canWipeWorkspace(callback, text, options) { - if (!hasFieldsSelector(store.getState())) { - callback(); - return; - } - const confirmModalOptions = { - confirmButtonText: i18n.translate('xpack.graph.leaveWorkspace.confirmButtonLabel', { - defaultMessage: 'Leave anyway', - }), - title: i18n.translate('xpack.graph.leaveWorkspace.modalTitle', { - defaultMessage: 'Unsaved changes', - }), - 'data-test-subj': 'confirmModal', - ...options, - }; - - overlays - .openConfirm( - text || - i18n.translate('xpack.graph.leaveWorkspace.confirmText', { - defaultMessage: 'If you leave now, you will lose unsaved changes.', - }), - confirmModalOptions - ) - .then((isConfirmed) => { - if (isConfirmed) { - callback(); - } - }); - } - $scope.confirmWipeWorkspace = canWipeWorkspace; - - $scope.performMerge = function (parentId, childId) { - let found = true; - while (found) { - found = false; - for (const i in $scope.detail.mergeCandidates) { - if ($scope.detail.mergeCandidates.hasOwnProperty(i)) { - const mc = $scope.detail.mergeCandidates[i]; - if (mc.id1 === childId || mc.id2 === childId) { - $scope.detail.mergeCandidates.splice(i, 1); - found = true; - break; - } - } - } - } - $scope.workspace.mergeIds(parentId, childId); - $scope.detail = null; - }; - - $scope.handleMergeCandidatesCallback = function (termIntersects) { - const mergeCandidates = []; - termIntersects.forEach((ti) => { - mergeCandidates.push({ - id1: ti.id1, - id2: ti.id2, - term1: ti.term1, - term2: ti.term2, - v1: ti.v1, - v2: ti.v2, - overlap: ti.overlap, - }); - }); - $scope.detail = { mergeCandidates }; - }; - - // ===== Menubar configuration ========= - $scope.setHeaderActionMenu = setHeaderActionMenu; - $scope.topNavMenu = []; - $scope.topNavMenu.push({ - key: 'new', - label: i18n.translate('xpack.graph.topNavMenu.newWorkspaceLabel', { - defaultMessage: 'New', - }), - description: i18n.translate('xpack.graph.topNavMenu.newWorkspaceAriaLabel', { - defaultMessage: 'New Workspace', - }), - tooltip: i18n.translate('xpack.graph.topNavMenu.newWorkspaceTooltip', { - defaultMessage: 'Create a new workspace', - }), - run: function () { - canWipeWorkspace(function () { - $scope.$evalAsync(() => { - if ($location.url() === '/workspace/') { - $route.reload(); - } else { - $location.url('/workspace/'); - } - }); - }); - }, - testId: 'graphNewButton', - }); - - // if saving is disabled using uiCapabilities, we don't want to render the save - // button so it's consistent with all of the other applications - if (capabilities.save) { - // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality - - $scope.topNavMenu.push({ - key: 'save', - label: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { - defaultMessage: 'Save', - }), - description: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { - defaultMessage: 'Save workspace', - }), - tooltip: () => { - if (allSavingDisabled) { - return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { - defaultMessage: - 'No changes to saved workspaces are permitted by the current save policy', - }); - } else { - return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { - defaultMessage: 'Save this workspace', - }); - } - }, - disableButton: function () { - return allSavingDisabled || !hasFieldsSelector(store.getState()); - }, - run: () => { - store.dispatch({ - type: 'x-pack/graph/SAVE_WORKSPACE', - payload: $route.current.locals.savedWorkspace, - }); - }, - testId: 'graphSaveButton', - }); - } - $scope.topNavMenu.push({ - key: 'inspect', - disableButton: function () { - return $scope.workspace === null; - }, - label: i18n.translate('xpack.graph.topNavMenu.inspectLabel', { - defaultMessage: 'Inspect', - }), - description: i18n.translate('xpack.graph.topNavMenu.inspectAriaLabel', { - defaultMessage: 'Inspect', - }), - run: () => { - $scope.$evalAsync(() => { - const curState = $scope.menus.showInspect; - $scope.closeMenus(); - $scope.menus.showInspect = !curState; - }); - }, - }); - - $scope.topNavMenu.push({ - key: 'settings', - disableButton: function () { - return datasourceSelector(store.getState()).type === 'none'; - }, - label: i18n.translate('xpack.graph.topNavMenu.settingsLabel', { - defaultMessage: 'Settings', - }), - description: i18n.translate('xpack.graph.topNavMenu.settingsAriaLabel', { - defaultMessage: 'Settings', - }), - run: () => { - const settingsObservable = asAngularSyncedObservable( - () => ({ - blocklistedNodes: $scope.workspace ? [...$scope.workspace.blocklistedNodes] : undefined, - unblocklistNode: $scope.workspace ? $scope.workspace.unblocklist : undefined, - canEditDrillDownUrls: canEditDrillDownUrls, - }), - $scope.$digest.bind($scope) - ); - coreStart.overlays.openFlyout( - toMountPoint( - - - - ), - { - size: 'm', - closeButtonAriaLabel: i18n.translate('xpack.graph.settings.closeLabel', { - defaultMessage: 'Close', - }), - 'data-test-subj': 'graphSettingsFlyout', - ownFocus: true, - className: 'gphSettingsFlyout', - maxWidth: 520, - } - ); - }, - }); - - // Allow URLs to include a user-defined text query - if ($route.current.params.query) { - $scope.initialQuery = $route.current.params.query; - const unbind = $scope.$watch('workspace', () => { - if (!$scope.workspace) { - return; - } - unbind(); - $scope.submit($route.current.params.query); - }); - } - - $scope.menus = { - showSettings: false, - }; - - $scope.closeMenus = () => { - _.forOwn($scope.menus, function (_, key) { - $scope.menus[key] = false; - }); - }; - - // Deal with situation of request to open saved workspace - if ($route.current.locals.savedWorkspace.id) { - store.dispatch({ - type: 'x-pack/graph/LOAD_WORKSPACE', - payload: $route.current.locals.savedWorkspace, - }); - } else { - $scope.noIndexPatterns = $route.current.locals.indexPatterns.length === 0; - } - }); - //End controller -} diff --git a/x-pack/plugins/graph/public/application.ts b/x-pack/plugins/graph/public/application.ts index 4d4b3c34de52..7461a7b5fc17 100644 --- a/x-pack/plugins/graph/public/application.ts +++ b/x-pack/plugins/graph/public/application.ts @@ -5,20 +5,8 @@ * 2.0. */ -// inner angular imports -// these are necessary to bootstrap the local angular. -// They can stay even after NP cutover -import angular from 'angular'; -import { i18nDirective, i18nFilter, I18nProvider } from '@kbn/i18n/angular'; +import { i18n } from '@kbn/i18n'; -import 'brace'; -import 'brace/mode/json'; - -// required for i18nIdDirective and `ngSanitize` angular module -import 'angular-sanitize'; -// required for ngRoute -import 'angular-route'; -// type imports import { ChromeStart, CoreStart, @@ -28,23 +16,21 @@ import { OverlayStart, AppMountParameters, IUiSettingsClient, + Capabilities, + ScopedHistory, } from 'kibana/public'; -// @ts-ignore -import { initGraphApp } from './app'; +import ReactDOM from 'react-dom'; import { DataPlugin, IndexPatternsContract } from '../../../../src/plugins/data/public'; import { LicensingPluginStart } from '../../licensing/public'; import { checkLicense } from '../common/check_license'; import { NavigationPublicPluginStart as NavigationStart } from '../../../../src/plugins/navigation/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; -import { - configureAppAngularModule, - createTopNavDirective, - createTopNavHelper, - KibanaLegacyStart, -} from '../../../../src/plugins/kibana_legacy/public'; +import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; import './index.scss'; import { SavedObjectsStart } from '../../../../src/plugins/saved_objects/public'; +import { GraphSavePolicy } from './types'; +import { graphRouter } from './router'; /** * These are dependencies of the Graph app besides the base dependencies @@ -58,7 +44,7 @@ export interface GraphDependencies { coreStart: CoreStart; element: HTMLElement; appBasePath: string; - capabilities: Record>; + capabilities: Capabilities; navigation: NavigationStart; licensing: LicensingPluginStart; chrome: ChromeStart; @@ -70,22 +56,32 @@ export interface GraphDependencies { getBasePath: () => string; storage: Storage; canEditDrillDownUrls: boolean; - graphSavePolicy: string; + graphSavePolicy: GraphSavePolicy; overlays: OverlayStart; savedObjects: SavedObjectsStart; kibanaLegacy: KibanaLegacyStart; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; uiSettings: IUiSettingsClient; + history: ScopedHistory; } -export const renderApp = ({ appBasePath, element, kibanaLegacy, ...deps }: GraphDependencies) => { +export type GraphServices = Omit; + +export const renderApp = ({ history, kibanaLegacy, element, ...deps }: GraphDependencies) => { + const { chrome, capabilities } = deps; kibanaLegacy.loadFontAwesome(); - const graphAngularModule = createLocalAngularModule(deps.navigation); - configureAppAngularModule( - graphAngularModule, - { core: deps.core, env: deps.pluginInitializerContext.env }, - true - ); + + if (!capabilities.graph.save) { + chrome.setBadge({ + text: i18n.translate('xpack.graph.badge.readOnly.text', { + defaultMessage: 'Read only', + }), + tooltip: i18n.translate('xpack.graph.badge.readOnly.tooltip', { + defaultMessage: 'Unable to save Graph workspaces', + }), + iconType: 'glasses', + }); + } const licenseSubscription = deps.licensing.license$.subscribe((license) => { const info = checkLicense(license); @@ -105,59 +101,19 @@ export const renderApp = ({ appBasePath, element, kibanaLegacy, ...deps }: Graph } }); - initGraphApp(graphAngularModule, deps); - const $injector = mountGraphApp(appBasePath, element); + // dispatch synthetic hash change event to update hash history objects + // this is necessary because hash updates triggered by using popState won't trigger this event naturally. + const unlistenParentHistory = history.listen(() => { + window.dispatchEvent(new HashChangeEvent('hashchange')); + }); + + const app = graphRouter(deps); + ReactDOM.render(app, element); + element.setAttribute('class', 'gphAppWrapper'); + return () => { licenseSubscription.unsubscribe(); - $injector.get('$rootScope').$destroy(); + unlistenParentHistory(); + ReactDOM.unmountComponentAtNode(element); }; }; - -const mainTemplate = (basePath: string) => `
- -
-`; - -const moduleName = 'app/graph'; - -const thirdPartyAngularDependencies = ['ngSanitize', 'ngRoute', 'react', 'ui.bootstrap']; - -function mountGraphApp(appBasePath: string, element: HTMLElement) { - const mountpoint = document.createElement('div'); - mountpoint.setAttribute('class', 'gphAppWrapper'); - // eslint-disable-next-line no-unsanitized/property - mountpoint.innerHTML = mainTemplate(appBasePath); - // bootstrap angular into detached element and attach it later to - // make angular-within-angular possible - const $injector = angular.bootstrap(mountpoint, [moduleName]); - element.appendChild(mountpoint); - element.setAttribute('class', 'gphAppWrapper'); - return $injector; -} - -function createLocalAngularModule(navigation: NavigationStart) { - createLocalI18nModule(); - createLocalTopNavModule(navigation); - - const graphAngularModule = angular.module(moduleName, [ - ...thirdPartyAngularDependencies, - 'graphI18n', - 'graphTopNav', - ]); - return graphAngularModule; -} - -function createLocalTopNavModule(navigation: NavigationStart) { - angular - .module('graphTopNav', ['react']) - .directive('kbnTopNav', createTopNavDirective) - .directive('kbnTopNavHelper', createTopNavHelper(navigation.ui)); -} - -function createLocalI18nModule() { - angular - .module('graphI18n', []) - .provider('i18n', I18nProvider) - .filter('i18n', i18nFilter) - .directive('i18nId', i18nDirective); -} diff --git a/x-pack/plugins/graph/public/components/listing.tsx b/x-pack/plugins/graph/public/apps/listing_route.tsx similarity index 64% rename from x-pack/plugins/graph/public/components/listing.tsx rename to x-pack/plugins/graph/public/apps/listing_route.tsx index 53fdab4a0288..e7457f18005e 100644 --- a/x-pack/plugins/graph/public/components/listing.tsx +++ b/x-pack/plugins/graph/public/apps/listing_route.tsx @@ -5,30 +5,72 @@ * 2.0. */ +import React, { Fragment, useCallback, useEffect } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage, I18nProvider } from '@kbn/i18n/react'; -import React, { Fragment } from 'react'; import { EuiEmptyPrompt, EuiLink, EuiButton } from '@elastic/eui'; - -import { CoreStart, ApplicationStart } from 'kibana/public'; +import { ApplicationStart } from 'kibana/public'; +import { useHistory, useLocation } from 'react-router-dom'; import { TableListView } from '../../../../../src/plugins/kibana_react/public'; +import { deleteSavedWorkspace, findSavedWorkspace } from '../helpers/saved_workspace_utils'; +import { getEditPath, getEditUrl, getNewPath, setBreadcrumbs } from '../services/url'; import { GraphWorkspaceSavedObject } from '../types'; +import { GraphServices } from '../application'; -export interface ListingProps { - coreStart: CoreStart; - createItem: () => void; - findItems: (query: string) => Promise<{ total: number; hits: GraphWorkspaceSavedObject[] }>; - deleteItems: (records: GraphWorkspaceSavedObject[]) => Promise; - editItem: (record: GraphWorkspaceSavedObject) => void; - getViewUrl: (record: GraphWorkspaceSavedObject) => string; - listingLimit: number; - hideWriteControls: boolean; - capabilities: { save: boolean; delete: boolean }; - initialFilter: string; - initialPageSize: number; +export interface ListingRouteProps { + deps: GraphServices; } -export function Listing(props: ListingProps) { +export function ListingRoute({ + deps: { chrome, savedObjects, savedObjectsClient, coreStart, capabilities, addBasePath }, +}: ListingRouteProps) { + const listingLimit = savedObjects.settings.getListingLimit(); + const initialPageSize = savedObjects.settings.getPerPage(); + const history = useHistory(); + const query = new URLSearchParams(useLocation().search); + const initialFilter = query.get('filter') || ''; + + useEffect(() => { + setBreadcrumbs({ chrome }); + }, [chrome]); + + const createItem = useCallback(() => { + history.push(getNewPath()); + }, [history]); + + const findItems = useCallback( + (search: string) => { + return findSavedWorkspace( + { savedObjectsClient, basePath: coreStart.http.basePath }, + search, + listingLimit + ); + }, + [coreStart.http.basePath, listingLimit, savedObjectsClient] + ); + + const editItem = useCallback( + (savedWorkspace: GraphWorkspaceSavedObject) => { + history.push(getEditPath(savedWorkspace)); + }, + [history] + ); + + const getViewUrl = useCallback( + (savedWorkspace: GraphWorkspaceSavedObject) => getEditUrl(addBasePath, savedWorkspace), + [addBasePath] + ); + + const deleteItems = useCallback( + async (savedWorkspaces: GraphWorkspaceSavedObject[]) => { + await deleteSavedWorkspace( + savedObjectsClient, + savedWorkspaces.map((cur) => cur.id!) + ); + }, + [savedObjectsClient] + ); + return ( { + /** + * It's temporary workaround, which should be removed after migration `workspace` to redux. + * Ref holds mutable `workspace` object. After each `workspace.methodName(...)` call + * (which might mutate `workspace` somehow), react state needs to be updated using + * `workspace.changeHandler()`. + */ + const workspaceRef = useRef(); + /** + * Providing `workspaceRef.current` to the hook dependencies or components itself + * will not leads to updates, therefore `renderCounter` is used to update react state. + */ + const [renderCounter, setRenderCounter] = useState(0); + const history = useHistory(); + const urlQuery = new URLSearchParams(useLocation().search).get('query'); + + const indexPatternProvider = useMemo( + () => createCachedIndexPatternProvider(getIndexPatternProvider.get), + [getIndexPatternProvider.get] + ); + + const { loading, callNodeProxy, callSearchNodeProxy, handleSearchQueryError } = useGraphLoader({ + toastNotifications, + coreStart, + }); + + const services = useMemo( + () => ({ + appName: 'graph', + storage, + data, + ...coreStart, + }), + [coreStart, data, storage] + ); + + const [store] = useState(() => + createGraphStore({ + basePath: getBasePath(), + addBasePath, + indexPatternProvider, + createWorkspace: (indexPattern, exploreControls) => { + const options = { + indexName: indexPattern, + vertex_fields: [], + // Here we have the opportunity to look up labels for nodes... + nodeLabeller() { + // console.log(newNodes); + }, + changeHandler: () => setRenderCounter((cur) => cur + 1), + graphExploreProxy: callNodeProxy, + searchProxy: callSearchNodeProxy, + exploreControls, + }; + const createdWorkspace = (workspaceRef.current = createWorkspace(options)); + return createdWorkspace; + }, + getWorkspace: () => workspaceRef.current, + notifications: coreStart.notifications, + http: coreStart.http, + overlays: coreStart.overlays, + savedObjectsClient, + showSaveModal, + savePolicy: graphSavePolicy, + changeUrl: (newUrl) => history.push(newUrl), + notifyReact: () => setRenderCounter((cur) => cur + 1), + chrome, + I18nContext: coreStart.i18n.Context, + handleSearchQueryError, + }) + ); + + const { savedWorkspace, indexPatterns } = useWorkspaceLoader({ + workspaceRef, + store, + savedObjectsClient, + toastNotifications, + }); + + if (!savedWorkspace || !indexPatterns) { + return null; + } + + return ( + + + + + + + + ); +}; diff --git a/x-pack/plugins/graph/public/badge.js b/x-pack/plugins/graph/public/badge.js deleted file mode 100644 index 128e30ee3f01..000000000000 --- a/x-pack/plugins/graph/public/badge.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export function getReadonlyBadge(uiCapabilities) { - if (uiCapabilities.graph.save) { - return null; - } - - return { - text: i18n.translate('xpack.graph.badge.readOnly.text', { - defaultMessage: 'Read only', - }), - tooltip: i18n.translate('xpack.graph.badge.readOnly.tooltip', { - defaultMessage: 'Unable to save Graph workspaces', - }), - iconType: 'glasses', - }; -} diff --git a/x-pack/plugins/graph/public/angular/templates/_graph.scss b/x-pack/plugins/graph/public/components/_graph.scss similarity index 75% rename from x-pack/plugins/graph/public/angular/templates/_graph.scss rename to x-pack/plugins/graph/public/components/_graph.scss index 5c2f5d5f7a88..706389304067 100644 --- a/x-pack/plugins/graph/public/angular/templates/_graph.scss +++ b/x-pack/plugins/graph/public/components/_graph.scss @@ -1,11 +1,3 @@ -@mixin gphSvgText() { - font-family: $euiFontFamily; - font-size: $euiSizeS; - line-height: $euiSizeM; - fill: $euiColorDarkShade; - color: $euiColorDarkShade; -} - /** * THE SVG Graph * 1. Calculated px values come from the open/closed state of the global nav sidebar diff --git a/x-pack/plugins/graph/public/components/_index.scss b/x-pack/plugins/graph/public/components/_index.scss index a06209e7e4d3..743c24c89642 100644 --- a/x-pack/plugins/graph/public/components/_index.scss +++ b/x-pack/plugins/graph/public/components/_index.scss @@ -7,3 +7,6 @@ @import './settings/index'; @import './legacy_icon/index'; @import './field_manager/index'; +@import './graph'; +@import './sidebar'; +@import './inspect'; diff --git a/x-pack/plugins/graph/public/angular/templates/_inspect.scss b/x-pack/plugins/graph/public/components/_inspect.scss similarity index 100% rename from x-pack/plugins/graph/public/angular/templates/_inspect.scss rename to x-pack/plugins/graph/public/components/_inspect.scss diff --git a/x-pack/plugins/graph/public/angular/templates/_sidebar.scss b/x-pack/plugins/graph/public/components/_sidebar.scss similarity index 82% rename from x-pack/plugins/graph/public/angular/templates/_sidebar.scss rename to x-pack/plugins/graph/public/components/_sidebar.scss index e784649b250f..831032231fe8 100644 --- a/x-pack/plugins/graph/public/angular/templates/_sidebar.scss +++ b/x-pack/plugins/graph/public/components/_sidebar.scss @@ -24,6 +24,10 @@ padding: $euiSizeXS; border-radius: $euiBorderRadius; margin-bottom: $euiSizeXS; + + & > span { + padding-right: $euiSizeXS; + } } .gphSidebar__panel { @@ -35,8 +39,9 @@ * Vertex Select */ -.gphVertexSelect__button { - margin: $euiSizeXS $euiSizeXS $euiSizeXS 0; +.vertexSelectionTypesBar { + margin-top: 0; + margin-bottom: 0; } /** @@ -68,15 +73,24 @@ background: $euiColorLightShade; } +/** + * Link summary + */ + +.gphDrillDownIconLinks { + margin-top: .5 * $euiSizeXS; + margin-bottom: .5 * $euiSizeXS; +} + /** * Link summary */ .gphLinkSummary__term--1 { - color:$euiColorDanger; + color: $euiColorDanger; } .gphLinkSummary__term--2 { - color:$euiColorPrimary; + color: $euiColorPrimary; } .gphLinkSummary__term--1-2 { color: mix($euiColorDanger, $euiColorPrimary); diff --git a/x-pack/plugins/graph/public/components/app.tsx b/x-pack/plugins/graph/public/components/app.tsx deleted file mode 100644 index fbe7f2d3ebe8..000000000000 --- a/x-pack/plugins/graph/public/components/app.tsx +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiSpacer } from '@elastic/eui'; - -import { DataPublicPluginStart } from 'src/plugins/data/public'; -import { Provider } from 'react-redux'; -import React, { useState } from 'react'; -import { I18nProvider } from '@kbn/i18n/react'; -import { CoreStart } from 'kibana/public'; -import { IStorageWrapper } from 'src/plugins/kibana_utils/public'; -import { FieldManager } from './field_manager'; -import { SearchBarProps, SearchBar } from './search_bar'; -import { GraphStore } from '../state_management'; -import { GuidancePanel } from './guidance_panel'; -import { GraphTitle } from './graph_title'; - -import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; - -export interface GraphAppProps extends SearchBarProps { - coreStart: CoreStart; - // This is not named dataStart because of Angular treating data- prefix differently - pluginDataStart: DataPublicPluginStart; - storage: IStorageWrapper; - reduxStore: GraphStore; - isInitialized: boolean; - noIndexPatterns: boolean; -} - -export function GraphApp(props: GraphAppProps) { - const [pickerOpen, setPickerOpen] = useState(false); - const { - coreStart, - pluginDataStart, - storage, - reduxStore, - noIndexPatterns, - ...searchBarProps - } = props; - - return ( - - - - <> - {props.isInitialized && } -
- - - -
- {!props.isInitialized && ( - { - setPickerOpen(true); - }} - /> - )} - -
-
-
- ); -} diff --git a/x-pack/plugins/graph/public/components/control_panel/control_panel.tsx b/x-pack/plugins/graph/public/components/control_panel/control_panel.tsx new file mode 100644 index 000000000000..2946bc8ad56f --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/control_panel.tsx @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { connect } from 'react-redux'; +import { + ControlType, + TermIntersect, + UrlTemplate, + Workspace, + WorkspaceField, + WorkspaceNode, +} from '../../types'; +import { urlTemplateRegex } from '../../helpers/url_template'; +import { SelectionToolBar } from './selection_tool_bar'; +import { ControlPanelToolBar } from './control_panel_tool_bar'; +import { SelectStyle } from './select_style'; +import { SelectedNodeEditor } from './selected_node_editor'; +import { MergeCandidates } from './merge_candidates'; +import { DrillDowns } from './drill_downs'; +import { DrillDownIconLinks } from './drill_down_icon_links'; +import { GraphState, liveResponseFieldsSelector, templatesSelector } from '../../state_management'; +import { SelectedNodeItem } from './selected_node_item'; + +export interface TargetOptions { + toFields: WorkspaceField[]; +} + +interface ControlPanelProps { + renderCounter: number; + workspace: Workspace; + control: ControlType; + selectedNode?: WorkspaceNode; + colors: string[]; + mergeCandidates: TermIntersect[]; + onSetControl: (control: ControlType) => void; + selectSelected: (node: WorkspaceNode) => void; +} + +interface ControlPanelStateProps { + urlTemplates: UrlTemplate[]; + liveResponseFields: WorkspaceField[]; +} + +const ControlPanelComponent = ({ + workspace, + liveResponseFields, + urlTemplates, + control, + selectedNode, + colors, + mergeCandidates, + onSetControl, + selectSelected, +}: ControlPanelProps & ControlPanelStateProps) => { + const hasNodes = workspace.nodes.length === 0; + + const openUrlTemplate = (template: UrlTemplate) => { + const url = template.url; + const newUrl = url.replace(urlTemplateRegex, template.encoder.encode(workspace!)); + window.open(newUrl, '_blank'); + }; + + const onSelectedFieldClick = (node: WorkspaceNode) => { + selectSelected(node); + workspace.changeHandler(); + }; + + const onDeselectNode = (node: WorkspaceNode) => { + workspace.deselectNode(node); + workspace.changeHandler(); + onSetControl('none'); + }; + + return ( + + ); +}; + +export const ControlPanel = connect((state: GraphState) => ({ + urlTemplates: templatesSelector(state), + liveResponseFields: liveResponseFieldsSelector(state), +}))(ControlPanelComponent); diff --git a/x-pack/plugins/graph/public/components/control_panel/control_panel_tool_bar.tsx b/x-pack/plugins/graph/public/components/control_panel/control_panel_tool_bar.tsx new file mode 100644 index 000000000000..37a9c003f768 --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/control_panel_tool_bar.tsx @@ -0,0 +1,230 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { ControlType, Workspace, WorkspaceField } from '../../types'; + +interface ControlPanelToolBarProps { + workspace: Workspace; + liveResponseFields: WorkspaceField[]; + onSetControl: (action: ControlType) => void; +} + +export const ControlPanelToolBar = ({ + workspace, + onSetControl, + liveResponseFields, +}: ControlPanelToolBarProps) => { + const haveNodes = workspace.nodes.length === 0; + + const undoButtonMsg = i18n.translate('xpack.graph.sidebar.topMenu.undoButtonTooltip', { + defaultMessage: 'Undo', + }); + const redoButtonMsg = i18n.translate('xpack.graph.sidebar.topMenu.redoButtonTooltip', { + defaultMessage: 'Redo', + }); + const expandButtonMsg = i18n.translate( + 'xpack.graph.sidebar.topMenu.expandSelectionButtonTooltip', + { + defaultMessage: 'Expand selection', + } + ); + const addLinksButtonMsg = i18n.translate('xpack.graph.sidebar.topMenu.addLinksButtonTooltip', { + defaultMessage: 'Add links between existing terms', + }); + const removeVerticesButtonMsg = i18n.translate( + 'xpack.graph.sidebar.topMenu.removeVerticesButtonTooltip', + { + defaultMessage: 'Remove vertices from workspace', + } + ); + const blocklistButtonMsg = i18n.translate('xpack.graph.sidebar.topMenu.blocklistButtonTooltip', { + defaultMessage: 'Block selection from appearing in workspace', + }); + const customStyleButtonMsg = i18n.translate( + 'xpack.graph.sidebar.topMenu.customStyleButtonTooltip', + { + defaultMessage: 'Custom style selected vertices', + } + ); + const drillDownButtonMsg = i18n.translate('xpack.graph.sidebar.topMenu.drillDownButtonTooltip', { + defaultMessage: 'Drill down', + }); + const runLayoutButtonMsg = i18n.translate('xpack.graph.sidebar.topMenu.runLayoutButtonTooltip', { + defaultMessage: 'Run layout', + }); + const pauseLayoutButtonMsg = i18n.translate( + 'xpack.graph.sidebar.topMenu.pauseLayoutButtonTooltip', + { + defaultMessage: 'Pause layout', + } + ); + + const onUndoClick = () => workspace.undo(); + const onRedoClick = () => workspace.redo(); + const onExpandButtonClick = () => { + onSetControl('none'); + workspace.expandSelecteds({ toFields: liveResponseFields }); + }; + const onAddLinksClick = () => workspace.fillInGraph(); + const onRemoveVerticesClick = () => { + onSetControl('none'); + workspace.deleteSelection(); + }; + const onBlockListClick = () => workspace.blocklistSelection(); + const onCustomStyleClick = () => onSetControl('style'); + const onDrillDownClick = () => onSetControl('drillDowns'); + const onRunLayoutClick = () => workspace.runLayout(); + const onPauseLayoutClick = () => { + workspace.stopLayout(); + workspace.changeHandler(); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {(workspace.nodes.length === 0 || workspace.force === null) && ( + + + + + + )} + + {workspace.force !== null && workspace.nodes.length > 0 && ( + + + + + + )} + + ); +}; diff --git a/x-pack/plugins/graph/public/components/control_panel/drill_down_icon_links.tsx b/x-pack/plugins/graph/public/components/control_panel/drill_down_icon_links.tsx new file mode 100644 index 000000000000..8d92d6ca0400 --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/drill_down_icon_links.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import React from 'react'; +import { UrlTemplate } from '../../types'; + +interface UrlTemplateButtonsProps { + urlTemplates: UrlTemplate[]; + hasNodes: boolean; + openUrlTemplate: (template: UrlTemplate) => void; +} + +export const DrillDownIconLinks = ({ + hasNodes, + urlTemplates, + openUrlTemplate, +}: UrlTemplateButtonsProps) => { + const drillDownsWithIcons = urlTemplates.filter( + ({ icon }: UrlTemplate) => icon && icon.class !== '' + ); + + if (drillDownsWithIcons.length === 0) { + return null; + } + + const drillDowns = drillDownsWithIcons.map((cur) => { + const onUrlTemplateClick = () => openUrlTemplate(cur); + + return ( + + + + + + ); + }); + + return ( + + {drillDowns} + + ); +}; diff --git a/x-pack/plugins/graph/public/components/control_panel/drill_downs.tsx b/x-pack/plugins/graph/public/components/control_panel/drill_downs.tsx new file mode 100644 index 000000000000..9d0dfdc7ba70 --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/drill_downs.tsx @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { UrlTemplate } from '../../types'; + +interface DrillDownsProps { + urlTemplates: UrlTemplate[]; + openUrlTemplate: (template: UrlTemplate) => void; +} + +export const DrillDowns = ({ urlTemplates, openUrlTemplate }: DrillDownsProps) => { + return ( +
+
+ + {i18n.translate('xpack.graph.sidebar.drillDownsTitle', { + defaultMessage: 'Drill-downs', + })} +
+ +
+ {urlTemplates.length === 0 && ( +

+ {i18n.translate('xpack.graph.sidebar.drillDowns.noDrillDownsHelpText', { + defaultMessage: 'Configure drill-downs from the settings menu', + })} +

+ )} + +
    + {urlTemplates.map((urlTemplate) => { + const onOpenUrlTemplate = () => openUrlTemplate(urlTemplate); + + return ( +
  • + {urlTemplate.icon && ( + {urlTemplate.icon?.code} + )} + +
  • + ); + })} +
+
+
+ ); +}; diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/index.js b/x-pack/plugins/graph/public/components/control_panel/index.ts similarity index 79% rename from x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/index.js rename to x-pack/plugins/graph/public/components/control_panel/index.ts index dcf009d0a280..7c3ab15baea2 100644 --- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/index.js +++ b/x-pack/plugins/graph/public/components/control_panel/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { KibanaRegionmapSource } from './kibana_regionmap_source'; +export * from './control_panel'; diff --git a/x-pack/plugins/graph/public/components/control_panel/merge_candidates.tsx b/x-pack/plugins/graph/public/components/control_panel/merge_candidates.tsx new file mode 100644 index 000000000000..cc380993ef99 --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/merge_candidates.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiToolTip } from '@elastic/eui'; +import { ControlType, TermIntersect, Workspace } from '../../types'; +import { VennDiagram } from '../venn_diagram'; + +interface MergeCandidatesProps { + workspace: Workspace; + mergeCandidates: TermIntersect[]; + onSetControl: (control: ControlType) => void; +} + +export const MergeCandidates = ({ + workspace, + mergeCandidates, + onSetControl, +}: MergeCandidatesProps) => { + const performMerge = (parentId: string, childId: string) => { + const tempMergeCandidates = [...mergeCandidates]; + let found = true; + while (found) { + found = false; + + for (let i = 0; i < tempMergeCandidates.length; i++) { + const term = tempMergeCandidates[i]; + if (term.id1 === childId || term.id2 === childId) { + tempMergeCandidates.splice(i, 1); + found = true; + break; + } + } + } + workspace.mergeIds(parentId, childId); + onSetControl('none'); + }; + + return ( +
+
+ + {i18n.translate('xpack.graph.sidebar.linkSummaryTitle', { + defaultMessage: 'Link summary', + })} +
+ {mergeCandidates.map((mc) => { + const mergeTerm1ToTerm2ButtonMsg = i18n.translate( + 'xpack.graph.sidebar.linkSummary.mergeTerm1ToTerm2ButtonTooltip', + { + defaultMessage: 'Merge {term1} into {term2}', + values: { term1: mc.term1, term2: mc.term2 }, + } + ); + const mergeTerm2ToTerm1ButtonMsg = i18n.translate( + 'xpack.graph.sidebar.linkSummary.mergeTerm2ToTerm1ButtonTooltip', + { + defaultMessage: 'Merge {term2} into {term1}', + values: { term1: mc.term1, term2: mc.term2 }, + } + ); + const leftTermCountMsg = i18n.translate( + 'xpack.graph.sidebar.linkSummary.leftTermCountTooltip', + { + defaultMessage: '{count} documents have term {term}', + values: { count: mc.v1, term: mc.term1 }, + } + ); + const bothTermsCountMsg = i18n.translate( + 'xpack.graph.sidebar.linkSummary.bothTermsCountTooltip', + { + defaultMessage: '{count} documents have both terms', + values: { count: mc.overlap }, + } + ); + const rightTermCountMsg = i18n.translate( + 'xpack.graph.sidebar.linkSummary.rightTermCountTooltip', + { + defaultMessage: '{count} documents have term {term}', + values: { count: mc.v2, term: mc.term2 }, + } + ); + + const onMergeTerm1ToTerm2Click = () => performMerge(mc.id2, mc.id1); + const onMergeTerm2ToTerm1Click = () => performMerge(mc.id1, mc.id2); + + return ( +
+ + + + + + {mc.term1} + {mc.term2} + + + + + + + + + + {mc.v1} + + +  ({mc.overlap})  + + + {mc.v2} + +
+ ); + })} +
+ ); +}; diff --git a/x-pack/plugins/graph/public/components/control_panel/select_style.tsx b/x-pack/plugins/graph/public/components/control_panel/select_style.tsx new file mode 100644 index 000000000000..2dbefc7d2445 --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/select_style.tsx @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { Workspace } from '../../types'; + +interface SelectStyleProps { + workspace: Workspace; + colors: string[]; +} + +export const SelectStyle = ({ colors, workspace }: SelectStyleProps) => { + return ( +
+
+ + {i18n.translate('xpack.graph.sidebar.styleVerticesTitle', { + defaultMessage: 'Style selected vertices', + })} +
+ +
+ {colors.map((c) => { + const onSelectColor = () => { + workspace.colorSelected(c); + workspace.changeHandler(); + }; + return ( +
+
+ ); +}; diff --git a/x-pack/plugins/graph/public/components/control_panel/selected_node_editor.tsx b/x-pack/plugins/graph/public/components/control_panel/selected_node_editor.tsx new file mode 100644 index 000000000000..a0eed56fac67 --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/selected_node_editor.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiToolTip } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { Workspace, WorkspaceNode } from '../../types'; + +interface SelectedNodeEditorProps { + workspace: Workspace; + selectedNode: WorkspaceNode; +} + +export const SelectedNodeEditor = ({ workspace, selectedNode }: SelectedNodeEditorProps) => { + const groupButtonMsg = i18n.translate('xpack.graph.sidebar.groupButtonTooltip', { + defaultMessage: 'group the currently selected items into {latestSelectionLabel}', + values: { latestSelectionLabel: selectedNode.label }, + }); + const ungroupButtonMsg = i18n.translate('xpack.graph.sidebar.ungroupButtonTooltip', { + defaultMessage: 'ungroup {latestSelectionLabel}', + values: { latestSelectionLabel: selectedNode.label }, + }); + + const onGroupButtonClick = () => { + workspace.groupSelections(selectedNode); + }; + const onClickUngroup = () => { + workspace.ungroup(selectedNode); + }; + const onChangeSelectedVertexLabel = (event: React.ChangeEvent) => { + selectedNode.label = event.target.value; + workspace.changeHandler(); + }; + + return ( +
+
+ {selectedNode.icon && } + {selectedNode.data.field} {selectedNode.data.term} +
+ + {(workspace.selectedNodes.length > 1 || + (workspace.selectedNodes.length > 0 && workspace.selectedNodes[0] !== selectedNode)) && ( + + + + )} + + {selectedNode.numChildren > 0 && ( + + + + )} + +
+
+ +
+ element && (element.value = selectedNode.label)} + type="text" + id="labelEdit" + className="form-control input-sm" + onChange={onChangeSelectedVertexLabel} + /> +
+ {i18n.translate('xpack.graph.sidebar.displayLabelHelpText', { + defaultMessage: 'Change the label for this vertex.', + })} +
+
+
+
+
+ ); +}; diff --git a/x-pack/plugins/graph/public/components/control_panel/selected_node_item.tsx b/x-pack/plugins/graph/public/components/control_panel/selected_node_item.tsx new file mode 100644 index 000000000000..11df3b5d5208 --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/selected_node_item.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { hexToRgb, isColorDark } from '@elastic/eui'; +import classNames from 'classnames'; +import React from 'react'; +import { WorkspaceNode } from '../../types'; + +const isHexColorDark = (color: string) => isColorDark(...hexToRgb(color)); + +interface SelectedNodeItemProps { + node: WorkspaceNode; + isHighlighted: boolean; + onDeselectNode: (node: WorkspaceNode) => void; + onSelectedFieldClick: (node: WorkspaceNode) => void; +} + +export const SelectedNodeItem = ({ + node, + isHighlighted, + onSelectedFieldClick, + onDeselectNode, +}: SelectedNodeItemProps) => { + const fieldClasses = classNames('gphSelectionList__field', { + ['gphSelectionList__field--selected']: isHighlighted, + }); + const fieldIconClasses = classNames('fa', 'gphNode__text', 'gphSelectionList__icon', { + ['gphNode__text--inverse']: isHexColorDark(node.color), + }); + + return ( + + ); +}; diff --git a/x-pack/plugins/graph/public/components/control_panel/selection_tool_bar.tsx b/x-pack/plugins/graph/public/components/control_panel/selection_tool_bar.tsx new file mode 100644 index 000000000000..e2e9771a8e9e --- /dev/null +++ b/x-pack/plugins/graph/public/components/control_panel/selection_tool_bar.tsx @@ -0,0 +1,136 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { ControlType, Workspace } from '../../types'; + +interface SelectionToolBarProps { + workspace: Workspace; + onSetControl: (data: ControlType) => void; +} + +export const SelectionToolBar = ({ workspace, onSetControl }: SelectionToolBarProps) => { + const haveNodes = workspace.nodes.length === 0; + + const selectAllButtonMsg = i18n.translate( + 'xpack.graph.sidebar.selections.selectAllButtonTooltip', + { + defaultMessage: 'Select all', + } + ); + const selectNoneButtonMsg = i18n.translate( + 'xpack.graph.sidebar.selections.selectNoneButtonTooltip', + { + defaultMessage: 'Select none', + } + ); + const invertSelectionButtonMsg = i18n.translate( + 'xpack.graph.sidebar.selections.invertSelectionButtonTooltip', + { + defaultMessage: 'Invert selection', + } + ); + const selectNeighboursButtonMsg = i18n.translate( + 'xpack.graph.sidebar.selections.selectNeighboursButtonTooltip', + { + defaultMessage: 'Select neighbours', + } + ); + + const onSelectAllClick = () => { + onSetControl('none'); + workspace.selectAll(); + workspace.changeHandler(); + }; + const onSelectNoneClick = () => { + onSetControl('none'); + workspace.selectNone(); + workspace.changeHandler(); + }; + const onInvertSelectionClick = () => { + onSetControl('none'); + workspace.selectInvert(); + workspace.changeHandler(); + }; + const onSelectNeighboursClick = () => { + onSetControl('none'); + workspace.selectNeighbours(); + workspace.changeHandler(); + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/graph/public/components/graph_visualization/_graph_visualization.scss b/x-pack/plugins/graph/public/components/graph_visualization/_graph_visualization.scss index caef2b6987dd..0853ab411459 100644 --- a/x-pack/plugins/graph/public/components/graph_visualization/_graph_visualization.scss +++ b/x-pack/plugins/graph/public/components/graph_visualization/_graph_visualization.scss @@ -1,3 +1,11 @@ +@mixin gphSvgText() { + font-family: $euiFontFamily; + font-size: $euiSizeS; + line-height: $euiSizeM; + fill: $euiColorDarkShade; + color: $euiColorDarkShade; +} + .gphVisualization { flex: 1; display: flex; diff --git a/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.test.tsx b/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.test.tsx index f49b5bfd32da..1ae556a79edc 100644 --- a/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.test.tsx +++ b/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.test.tsx @@ -7,15 +7,13 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { - GraphVisualization, - GroupAwareWorkspaceNode, - GroupAwareWorkspaceEdge, -} from './graph_visualization'; +import { GraphVisualization } from './graph_visualization'; +import { Workspace, WorkspaceEdge, WorkspaceNode } from '../../types'; describe('graph_visualization', () => { - const nodes: GroupAwareWorkspaceNode[] = [ + const nodes: WorkspaceNode[] = [ { + id: '1', color: 'black', data: { field: 'A', @@ -37,6 +35,7 @@ describe('graph_visualization', () => { y: 5, }, { + id: '2', color: 'red', data: { field: 'B', @@ -58,6 +57,7 @@ describe('graph_visualization', () => { y: 9, }, { + id: '3', color: 'yellow', data: { field: 'C', @@ -79,7 +79,7 @@ describe('graph_visualization', () => { y: 9, }, ]; - const edges: GroupAwareWorkspaceEdge[] = [ + const edges: WorkspaceEdge[] = [ { isSelected: true, label: '', @@ -101,9 +101,32 @@ describe('graph_visualization', () => { width: 2.2, }, ]; + const workspace = ({ + nodes, + edges, + selectNone: () => {}, + changeHandler: jest.fn(), + toggleNodeSelection: jest.fn().mockImplementation((node: WorkspaceNode) => { + return !node.isSelected; + }), + getAllIntersections: jest.fn(), + } as unknown) as jest.Mocked; + + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should render empty workspace without data', () => { - expect(shallow( {}} nodeClick={() => {}} />)) - .toMatchInlineSnapshot(` + expect( + shallow( + {}} + onSetControl={() => {}} + onSetMergeCandidates={() => {}} + /> + ) + ).toMatchInlineSnapshot(` { it('should render to svg elements', () => { expect( shallow( - {}} nodeClick={() => {}} nodes={nodes} edges={edges} /> + {}} + onSetControl={() => {}} + onSetMergeCandidates={() => {}} + /> ) ).toMatchSnapshot(); }); - it('should react to node click', () => { - const nodeClickSpy = jest.fn(); + it('should react to node selection', () => { + const selectSelectedMock = jest.fn(); + const instance = shallow( {}} - nodeClick={nodeClickSpy} - nodes={nodes} - edges={edges} + workspace={workspace} + selectSelected={selectSelectedMock} + onSetControl={() => {}} + onSetMergeCandidates={() => {}} /> ); + + instance.find('.gphNode').last().simulate('click', {}); + + expect(workspace.toggleNodeSelection).toHaveBeenCalledWith(nodes[2]); + expect(selectSelectedMock).toHaveBeenCalledWith(nodes[2]); + expect(workspace.changeHandler).toHaveBeenCalled(); + }); + + it('should react to node deselection', () => { + const onSetControlMock = jest.fn(); + const instance = shallow( + {}} + onSetControl={onSetControlMock} + onSetMergeCandidates={() => {}} + /> + ); + instance.find('.gphNode').first().simulate('click', {}); - expect(nodeClickSpy).toHaveBeenCalledWith(nodes[0], {}); + + expect(workspace.toggleNodeSelection).toHaveBeenCalledWith(nodes[0]); + expect(onSetControlMock).toHaveBeenCalledWith('none'); + expect(workspace.changeHandler).toHaveBeenCalled(); }); it('should react to edge click', () => { - const edgeClickSpy = jest.fn(); const instance = shallow( {}} - nodes={nodes} - edges={edges} + workspace={workspace} + selectSelected={() => {}} + onSetControl={() => {}} + onSetMergeCandidates={() => {}} /> ); + instance.find('.gphEdge').first().simulate('click'); - expect(edgeClickSpy).toHaveBeenCalledWith(edges[0]); + + expect(workspace.getAllIntersections).toHaveBeenCalled(); + expect(edges[0].topSrc).toEqual(workspace.getAllIntersections.mock.calls[0][1][0]); + expect(edges[0].topTarget).toEqual(workspace.getAllIntersections.mock.calls[0][1][1]); }); }); diff --git a/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.tsx b/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.tsx index 9b8dc98b84f4..26359101a9a5 100644 --- a/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.tsx +++ b/x-pack/plugins/graph/public/components/graph_visualization/graph_visualization.tsx @@ -9,31 +9,14 @@ import React, { useRef } from 'react'; import classNames from 'classnames'; import d3, { ZoomEvent } from 'd3'; import { isColorDark, hexToRgb } from '@elastic/eui'; -import { WorkspaceNode, WorkspaceEdge } from '../../types'; +import { Workspace, WorkspaceNode, TermIntersect, ControlType, WorkspaceEdge } from '../../types'; import { makeNodeId } from '../../services/persistence'; -/* - * The layouting algorithm sets a few extra properties on - * node objects to handle grouping. This will be moved to - * a separate data structure when the layouting is migrated - */ - -export interface GroupAwareWorkspaceNode extends WorkspaceNode { - kx: number; - ky: number; - numChildren: number; -} - -export interface GroupAwareWorkspaceEdge extends WorkspaceEdge { - topTarget: GroupAwareWorkspaceNode; - topSrc: GroupAwareWorkspaceNode; -} - export interface GraphVisualizationProps { - nodes?: GroupAwareWorkspaceNode[]; - edges?: GroupAwareWorkspaceEdge[]; - edgeClick: (edge: GroupAwareWorkspaceEdge) => void; - nodeClick: (node: GroupAwareWorkspaceNode, e: React.MouseEvent) => void; + workspace: Workspace; + onSetControl: (control: ControlType) => void; + selectSelected: (node: WorkspaceNode) => void; + onSetMergeCandidates: (terms: TermIntersect[]) => void; } function registerZooming(element: SVGSVGElement) { @@ -55,13 +38,39 @@ function registerZooming(element: SVGSVGElement) { } export function GraphVisualization({ - nodes, - edges, - edgeClick, - nodeClick, + workspace, + selectSelected, + onSetControl, + onSetMergeCandidates, }: GraphVisualizationProps) { const svgRoot = useRef(null); + const nodeClick = (n: WorkspaceNode, event: React.MouseEvent) => { + // Selection logic - shift key+click helps selects multiple nodes + // Without the shift key we deselect all prior selections (perhaps not + // a great idea for touch devices with no concept of shift key) + if (!event.shiftKey) { + const prevSelection = n.isSelected; + workspace.selectNone(); + n.isSelected = prevSelection; + } + if (workspace.toggleNodeSelection(n)) { + selectSelected(n); + } else { + onSetControl('none'); + } + workspace.changeHandler(); + }; + + const handleMergeCandidatesCallback = (termIntersects: TermIntersect[]) => { + const mergeCandidates: TermIntersect[] = [...termIntersects]; + onSetMergeCandidates(mergeCandidates); + onSetControl('mergeTerms'); + }; + + const edgeClick = (edge: WorkspaceEdge) => + workspace.getAllIntersections(handleMergeCandidatesCallback, [edge.topSrc, edge.topTarget]); + return ( - {edges && - edges.map((edge) => ( + {workspace.edges && + workspace.edges.map((edge) => ( ))} - {nodes && - nodes + {workspace.nodes && + workspace.nodes .filter((node) => !node.parent) .map((node) => ( {}; + +export const InspectPanel = ({ + showInspect, + lastRequest, + lastResponse, + indexPattern, +}: InspectPanelProps) => { + const [selectedTabId, setSelectedTabId] = useState('request'); + + const onRequestClick = () => setSelectedTabId('request'); + const onResponseClick = () => setSelectedTabId('response'); + + const editorContent = useMemo(() => (selectedTabId === 'request' ? lastRequest : lastResponse), [ + lastRequest, + lastResponse, + selectedTabId, + ]); + + if (showInspect) { + return ( +
+
+
+ +
+ +
+ + http://host:port/{indexPattern?.id}/_graph/explore + + + + + + + + + + +
+
+
+ ); + } + + return null; +}; diff --git a/x-pack/plugins/graph/public/components/inspect_panel/inspect_panel.tsx b/x-pack/plugins/graph/public/components/inspect_panel/inspect_panel.tsx deleted file mode 100644 index 2f29849bebce..000000000000 --- a/x-pack/plugins/graph/public/components/inspect_panel/inspect_panel.tsx +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React, { useMemo, useState } from 'react'; -import { EuiTab, EuiTabs, EuiText } from '@elastic/eui'; -import { monaco, XJsonLang } from '@kbn/monaco'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { IUiSettingsClient } from 'kibana/public'; -import { IndexPattern } from '../../../../../../src/plugins/data/public'; -import { - CodeEditor, - KibanaContextProvider, -} from '../../../../../../src/plugins/kibana_react/public'; - -interface InspectPanelProps { - showInspect?: boolean; - indexPattern?: IndexPattern; - uiSettings: IUiSettingsClient; - lastRequest?: string; - lastResponse?: string; -} - -const CODE_EDITOR_OPTIONS: monaco.editor.IStandaloneEditorConstructionOptions = { - automaticLayout: true, - fontSize: 12, - lineNumbers: 'on', - minimap: { - enabled: false, - }, - overviewRulerBorder: false, - readOnly: true, - scrollbar: { - alwaysConsumeMouseWheel: false, - }, - scrollBeyondLastLine: false, - wordWrap: 'on', - wrappingIndent: 'indent', -}; - -const dummyCallback = () => {}; - -export const InspectPanel = ({ - showInspect, - lastRequest, - lastResponse, - indexPattern, - uiSettings, -}: InspectPanelProps) => { - const [selectedTabId, setSelectedTabId] = useState('request'); - - const onRequestClick = () => setSelectedTabId('request'); - const onResponseClick = () => setSelectedTabId('response'); - - const services = useMemo(() => ({ uiSettings }), [uiSettings]); - - const editorContent = useMemo(() => (selectedTabId === 'request' ? lastRequest : lastResponse), [ - selectedTabId, - lastRequest, - lastResponse, - ]); - - if (showInspect) { - return ( - -
-
-
- -
- -
- - http://host:port/{indexPattern?.id}/_graph/explore - - - - - - - - - - -
-
-
-
- ); - } - - return null; -}; diff --git a/x-pack/plugins/graph/public/components/search_bar.test.tsx b/x-pack/plugins/graph/public/components/search_bar.test.tsx index 690fdf832c37..1b76cde1a62f 100644 --- a/x-pack/plugins/graph/public/components/search_bar.test.tsx +++ b/x-pack/plugins/graph/public/components/search_bar.test.tsx @@ -6,18 +6,18 @@ */ import { mountWithIntl } from '@kbn/test/jest'; -import { SearchBar, OuterSearchBarProps } from './search_bar'; -import React, { ReactElement } from 'react'; +import { SearchBar, SearchBarProps } from './search_bar'; +import React, { Component, ReactElement } from 'react'; import { CoreStart } from 'src/core/public'; import { act } from 'react-dom/test-utils'; import { IndexPattern, QueryStringInput } from '../../../../../src/plugins/data/public'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; -import { I18nProvider } from '@kbn/i18n/react'; +import { I18nProvider, InjectedIntl } from '@kbn/i18n/react'; import { openSourceModal } from '../services/source_modal'; -import { GraphStore, setDatasource } from '../state_management'; +import { GraphStore, setDatasource, submitSearchSaga } from '../state_management'; import { ReactWrapper } from 'enzyme'; import { createMockGraphStore } from '../state_management/mocks'; import { Provider } from 'react-redux'; @@ -26,7 +26,7 @@ jest.mock('../services/source_modal', () => ({ openSourceModal: jest.fn() })); const waitForIndexPatternFetch = () => new Promise((r) => setTimeout(r)); -function wrapSearchBarInContext(testProps: OuterSearchBarProps) { +function wrapSearchBarInContext(testProps: SearchBarProps) { const services = { uiSettings: { get: (key: string) => { @@ -67,21 +67,34 @@ function wrapSearchBarInContext(testProps: OuterSearchBarProps) { } describe('search_bar', () => { + let dispatchSpy: jest.Mock; + let instance: ReactWrapper< + SearchBarProps & { intl: InjectedIntl }, + Readonly<{}>, + Component<{}, {}, any> + >; + let store: GraphStore; const defaultProps = { isLoading: false, - onQuerySubmit: jest.fn(), indexPatternProvider: { get: jest.fn(() => Promise.resolve(({ fields: [] } as unknown) as IndexPattern)), }, confirmWipeWorkspace: (callback: () => void) => { callback(); }, + onIndexPatternChange: (indexPattern?: IndexPattern) => { + instance.setProps({ + ...defaultProps, + currentIndexPattern: indexPattern, + }); + }, }; - let instance: ReactWrapper; - let store: GraphStore; beforeEach(() => { - store = createMockGraphStore({}).store; + store = createMockGraphStore({ + sagas: [submitSearchSaga], + }).store; + store.dispatch( setDatasource({ type: 'indexpattern', @@ -89,14 +102,21 @@ describe('search_bar', () => { title: 'test-index', }) ); + + dispatchSpy = jest.fn(store.dispatch); + store.dispatch = dispatchSpy; }); async function mountSearchBar() { jest.clearAllMocks(); - const wrappedSearchBar = wrapSearchBarInContext({ ...defaultProps }); + const searchBarTestRoot = React.createElement((updatedProps: SearchBarProps) => ( + + {wrapSearchBarInContext({ ...defaultProps, ...updatedProps })} + + )); await act(async () => { - instance = mountWithIntl({wrappedSearchBar}); + instance = mountWithIntl(searchBarTestRoot); }); } @@ -119,7 +139,10 @@ describe('search_bar', () => { instance.find('form').simulate('submit', { preventDefault: () => {} }); }); - expect(defaultProps.onQuerySubmit).toHaveBeenCalledWith('testQuery'); + expect(dispatchSpy).toHaveBeenCalledWith({ + type: 'x-pack/graph/workspace/SUBMIT_SEARCH', + payload: 'testQuery', + }); }); it('should translate kql query into JSON dsl', async () => { @@ -135,7 +158,7 @@ describe('search_bar', () => { instance.find('form').simulate('submit', { preventDefault: () => {} }); }); - const parsedQuery = JSON.parse(defaultProps.onQuerySubmit.mock.calls[0][0]); + const parsedQuery = JSON.parse(dispatchSpy.mock.calls[0][0].payload); expect(parsedQuery).toEqual({ bool: { should: [{ match: { test: 'abc' } }], minimum_should_match: 1 }, }); diff --git a/x-pack/plugins/graph/public/components/search_bar.tsx b/x-pack/plugins/graph/public/components/search_bar.tsx index fdf198c76195..fc7e3be3d0d3 100644 --- a/x-pack/plugins/graph/public/components/search_bar.tsx +++ b/x-pack/plugins/graph/public/components/search_bar.tsx @@ -17,6 +17,7 @@ import { datasourceSelector, requestDatasource, IndexpatternDatasource, + submitSearch, } from '../state_management'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; @@ -28,11 +29,11 @@ import { esKuery, } from '../../../../../src/plugins/data/public'; -export interface OuterSearchBarProps { +export interface SearchBarProps { isLoading: boolean; - initialQuery?: string; - onQuerySubmit: (query: string) => void; - + urlQuery: string | null; + currentIndexPattern?: IndexPattern; + onIndexPatternChange: (indexPattern?: IndexPattern) => void; confirmWipeWorkspace: ( onConfirm: () => void, text?: string, @@ -41,9 +42,10 @@ export interface OuterSearchBarProps { indexPatternProvider: IndexPatternProvider; } -export interface SearchBarProps extends OuterSearchBarProps { +export interface SearchBarStateProps { currentDatasource?: IndexpatternDatasource; onIndexPatternSelected: (indexPattern: IndexPatternSavedObject) => void; + submit: (searchTerm: string) => void; } function queryToString(query: Query, indexPattern: IndexPattern) { @@ -65,31 +67,34 @@ function queryToString(query: Query, indexPattern: IndexPattern) { return JSON.stringify(query.query); } -export function SearchBarComponent(props: SearchBarProps) { +export function SearchBarComponent(props: SearchBarStateProps & SearchBarProps) { const { - currentDatasource, - onQuerySubmit, isLoading, - onIndexPatternSelected, - initialQuery, + urlQuery, + currentIndexPattern, + currentDatasource, indexPatternProvider, + submit, + onIndexPatternSelected, confirmWipeWorkspace, + onIndexPatternChange, } = props; - const [query, setQuery] = useState({ language: 'kuery', query: initialQuery || '' }); - const [currentIndexPattern, setCurrentIndexPattern] = useState( - undefined - ); + const [query, setQuery] = useState({ language: 'kuery', query: urlQuery || '' }); + + useEffect(() => setQuery((prev) => ({ language: prev.language, query: urlQuery || '' })), [ + urlQuery, + ]); useEffect(() => { async function fetchPattern() { if (currentDatasource) { - setCurrentIndexPattern(await indexPatternProvider.get(currentDatasource.id)); + onIndexPatternChange(await indexPatternProvider.get(currentDatasource.id)); } else { - setCurrentIndexPattern(undefined); + onIndexPatternChange(undefined); } } fetchPattern(); - }, [currentDatasource, indexPatternProvider]); + }, [currentDatasource, indexPatternProvider, onIndexPatternChange]); const kibana = useKibana(); const { services, overlays } = kibana; @@ -101,7 +106,7 @@ export function SearchBarComponent(props: SearchBarProps) { onSubmit={(e) => { e.preventDefault(); if (!isLoading && currentIndexPattern) { - onQuerySubmit(queryToString(query, currentIndexPattern)); + submit(queryToString(query, currentIndexPattern)); } }} > @@ -196,5 +201,8 @@ export const SearchBar = connect( }) ); }, + submit: (searchTerm: string) => { + dispatch(submitSearch(searchTerm)); + }, }) )(SearchBarComponent); diff --git a/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx b/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx index 10ee306cd48a..44ce606b0c1a 100644 --- a/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx +++ b/x-pack/plugins/graph/public/components/settings/advanced_settings_form.tsx @@ -8,8 +8,8 @@ import React, { useState, useEffect } from 'react'; import { EuiFormRow, EuiFieldNumber, EuiComboBox, EuiSwitch, EuiText } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SettingsProps } from './settings'; import { AdvancedSettings } from '../../types'; +import { SettingsStateProps } from './settings'; // Helper type to get all keys of an interface // that are of type number. @@ -26,9 +26,10 @@ export function AdvancedSettingsForm({ advancedSettings, updateSettings, allFields, -}: Pick) { +}: Pick) { // keep a local state during changes const [formState, updateFormState] = useState({ ...advancedSettings }); + // useEffect update localState only based on the main store useEffect(() => { updateFormState(advancedSettings); diff --git a/x-pack/plugins/graph/public/components/settings/blocklist_form.tsx b/x-pack/plugins/graph/public/components/settings/blocklist_form.tsx index 6f6b759f1ee1..8954e812bdb8 100644 --- a/x-pack/plugins/graph/public/components/settings/blocklist_form.tsx +++ b/x-pack/plugins/graph/public/components/settings/blocklist_form.tsx @@ -17,14 +17,15 @@ import { EuiCallOut, } from '@elastic/eui'; -import { SettingsProps } from './settings'; +import { SettingsWorkspaceProps } from './settings'; import { LegacyIcon } from '../legacy_icon'; import { useListKeys } from './use_list_keys'; export function BlocklistForm({ blocklistedNodes, - unblocklistNode, -}: Pick) { + unblockNode, + unblockAll, +}: Pick) { const getListKey = useListKeys(blocklistedNodes || []); return ( <> @@ -46,7 +47,7 @@ export function BlocklistForm({ /> )} - {blocklistedNodes && unblocklistNode && blocklistedNodes.length > 0 && ( + {blocklistedNodes && blocklistedNodes.length > 0 && ( <> {blocklistedNodes.map((node) => ( @@ -63,9 +64,7 @@ export function BlocklistForm({ defaultMessage: 'Delete', }), color: 'danger', - onClick: () => { - unblocklistNode(node); - }, + onClick: () => unblockNode(node), }} /> ))} @@ -77,11 +76,7 @@ export function BlocklistForm({ iconType="trash" size="s" fill - onClick={() => { - blocklistedNodes.forEach((node) => { - unblocklistNode(node); - }); - }} + onClick={() => unblockAll()} > {i18n.translate('xpack.graph.settings.blocklist.clearButtonLabel', { defaultMessage: 'Delete all', diff --git a/x-pack/plugins/graph/public/components/settings/settings.test.tsx b/x-pack/plugins/graph/public/components/settings/settings.test.tsx index f0d506cf4755..060b1e93fbdc 100644 --- a/x-pack/plugins/graph/public/components/settings/settings.test.tsx +++ b/x-pack/plugins/graph/public/components/settings/settings.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { EuiTab, EuiListGroupItem, EuiButton, EuiAccordion, EuiFieldText } from '@elastic/eui'; import * as Rx from 'rxjs'; import { mountWithIntl } from '@kbn/test/jest'; -import { Settings, AngularProps } from './settings'; +import { Settings, SettingsWorkspaceProps } from './settings'; import { act } from '@testing-library/react'; import { ReactWrapper } from 'enzyme'; import { UrlTemplateForm } from './url_template_form'; @@ -46,7 +46,7 @@ describe('settings', () => { isDefault: false, }; - const angularProps: jest.Mocked = { + const workspaceProps: jest.Mocked = { blocklistedNodes: [ { x: 0, @@ -83,11 +83,12 @@ describe('settings', () => { }, }, ], - unblocklistNode: jest.fn(), + unblockNode: jest.fn(), + unblockAll: jest.fn(), canEditDrillDownUrls: true, }; - let subject: Rx.BehaviorSubject>; + let subject: Rx.BehaviorSubject>; let instance: ReactWrapper; beforeEach(() => { @@ -137,7 +138,7 @@ describe('settings', () => { ); dispatchSpy = jest.fn(store.dispatch); store.dispatch = dispatchSpy; - subject = new Rx.BehaviorSubject(angularProps); + subject = new Rx.BehaviorSubject(workspaceProps); instance = mountWithIntl( @@ -217,7 +218,7 @@ describe('settings', () => { it('should update on new data', () => { act(() => { subject.next({ - ...angularProps, + ...workspaceProps, blocklistedNodes: [ { x: 0, @@ -250,14 +251,13 @@ describe('settings', () => { it('should delete node', () => { instance.find(EuiListGroupItem).at(0).prop('extraAction')!.onClick!({} as any); - expect(angularProps.unblocklistNode).toHaveBeenCalledWith(angularProps.blocklistedNodes![0]); + expect(workspaceProps.unblockNode).toHaveBeenCalledWith(workspaceProps.blocklistedNodes![0]); }); it('should delete all nodes', () => { instance.find('[data-test-subj="graphUnblocklistAll"]').find(EuiButton).simulate('click'); - expect(angularProps.unblocklistNode).toHaveBeenCalledWith(angularProps.blocklistedNodes![0]); - expect(angularProps.unblocklistNode).toHaveBeenCalledWith(angularProps.blocklistedNodes![1]); + expect(workspaceProps.unblockAll).toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/graph/public/components/settings/settings.tsx b/x-pack/plugins/graph/public/components/settings/settings.tsx index ab9cfdfe3807..d8f18add4f37 100644 --- a/x-pack/plugins/graph/public/components/settings/settings.tsx +++ b/x-pack/plugins/graph/public/components/settings/settings.tsx @@ -6,7 +6,7 @@ */ import { i18n } from '@kbn/i18n'; -import React, { useState, useEffect } from 'react'; +import React, { useEffect, useState } from 'react'; import { EuiFlyoutHeader, EuiTitle, EuiTabs, EuiFlyoutBody, EuiTab } from '@elastic/eui'; import * as Rx from 'rxjs'; import { connect } from 'react-redux'; @@ -14,7 +14,7 @@ import { bindActionCreators } from 'redux'; import { AdvancedSettingsForm } from './advanced_settings_form'; import { BlocklistForm } from './blocklist_form'; import { UrlTemplateList } from './url_template_list'; -import { WorkspaceNode, AdvancedSettings, UrlTemplate, WorkspaceField } from '../../types'; +import { AdvancedSettings, BlockListedNode, UrlTemplate, WorkspaceField } from '../../types'; import { GraphState, settingsSelector, @@ -47,16 +47,6 @@ const tabs = [ }, ]; -/** - * These props are wired in the angular scope and are passed in via observable - * to catch update outside updates - */ -export interface AngularProps { - blocklistedNodes: WorkspaceNode[]; - unblocklistNode: (node: WorkspaceNode) => void; - canEditDrillDownUrls: boolean; -} - export interface StateProps { advancedSettings: AdvancedSettings; urlTemplates: UrlTemplate[]; @@ -69,26 +59,43 @@ export interface DispatchProps { saveTemplate: (props: { index: number; template: UrlTemplate }) => void; } -interface AsObservable

{ +export interface SettingsWorkspaceProps { + blocklistedNodes: BlockListedNode[]; + unblockNode: (node: BlockListedNode) => void; + unblockAll: () => void; + canEditDrillDownUrls: boolean; +} + +export interface AsObservable

{ observable: Readonly>; } -export interface SettingsProps extends AngularProps, StateProps, DispatchProps {} +export interface SettingsStateProps extends StateProps, DispatchProps {} export function SettingsComponent({ observable, - ...props -}: AsObservable & StateProps & DispatchProps) { - const [angularProps, setAngularProps] = useState(undefined); + advancedSettings, + urlTemplates, + allFields, + saveTemplate: saveTemplateAction, + updateSettings: updateSettingsAction, + removeTemplate: removeTemplateAction, +}: AsObservable & SettingsStateProps) { + const [workspaceProps, setWorkspaceProps] = useState( + undefined + ); const [activeTab, setActiveTab] = useState(0); useEffect(() => { - observable.subscribe(setAngularProps); + observable.subscribe(setWorkspaceProps); }, [observable]); - if (!angularProps) return null; + if (!workspaceProps) { + return null; + } const ActiveTabContent = tabs[activeTab].component; + return ( <> @@ -97,7 +104,7 @@ export function SettingsComponent({ {tabs - .filter(({ id }) => id !== 'drillDowns' || angularProps.canEditDrillDownUrls) + .filter(({ id }) => id !== 'drillDowns' || workspaceProps.canEditDrillDownUrls) .map(({ title }, index) => ( - + ); } -export const Settings = connect, GraphState>( +export const Settings = connect< + StateProps, + DispatchProps, + AsObservable, + GraphState +>( (state: GraphState) => ({ advancedSettings: settingsSelector(state), urlTemplates: templatesSelector(state), diff --git a/x-pack/plugins/graph/public/components/settings/url_template_list.tsx b/x-pack/plugins/graph/public/components/settings/url_template_list.tsx index 24ce9dd267ad..d18a9adb9bc0 100644 --- a/x-pack/plugins/graph/public/components/settings/url_template_list.tsx +++ b/x-pack/plugins/graph/public/components/settings/url_template_list.tsx @@ -8,7 +8,7 @@ import React, { useState } from 'react'; import { EuiText, EuiSpacer, EuiTextAlign, EuiButton, htmlIdGenerator } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { SettingsProps } from './settings'; +import { SettingsStateProps } from './settings'; import { UrlTemplateForm } from './url_template_form'; import { useListKeys } from './use_list_keys'; @@ -18,7 +18,7 @@ export function UrlTemplateList({ removeTemplate, saveTemplate, urlTemplates, -}: Pick) { +}: Pick) { const [uncommittedForms, setUncommittedForms] = useState([]); const getListKey = useListKeys(urlTemplates); diff --git a/x-pack/plugins/graph/public/components/workspace_layout/index.ts b/x-pack/plugins/graph/public/components/workspace_layout/index.ts new file mode 100644 index 000000000000..9f753a5bad57 --- /dev/null +++ b/x-pack/plugins/graph/public/components/workspace_layout/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './workspace_layout'; diff --git a/x-pack/plugins/graph/public/components/workspace_layout/workspace_layout.tsx b/x-pack/plugins/graph/public/components/workspace_layout/workspace_layout.tsx new file mode 100644 index 000000000000..70e5b82ec652 --- /dev/null +++ b/x-pack/plugins/graph/public/components/workspace_layout/workspace_layout.tsx @@ -0,0 +1,234 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { Fragment, memo, useCallback, useRef, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiSpacer } from '@elastic/eui'; +import { connect } from 'react-redux'; +import { SearchBar } from '../search_bar'; +import { + GraphState, + hasFieldsSelector, + workspaceInitializedSelector, +} from '../../state_management'; +import { FieldManager } from '../field_manager'; +import { IndexPattern } from '../../../../../../src/plugins/data/public'; +import { + ControlType, + IndexPatternProvider, + IndexPatternSavedObject, + TermIntersect, + WorkspaceNode, +} from '../../types'; +import { WorkspaceTopNavMenu } from './workspace_top_nav_menu'; +import { InspectPanel } from '../inspect_panel'; +import { GuidancePanel } from '../guidance_panel'; +import { GraphTitle } from '../graph_title'; +import { GraphWorkspaceSavedObject, Workspace } from '../../types'; +import { GraphServices } from '../../application'; +import { ControlPanel } from '../control_panel'; +import { GraphVisualization } from '../graph_visualization'; +import { colorChoices } from '../../helpers/style_choices'; + +/** + * Each component, which depends on `worksapce` + * should not be memoized, since it will not get updates. + * This behaviour should be changed after migrating `worksapce` to redux + */ +const FieldManagerMemoized = memo(FieldManager); +const GuidancePanelMemoized = memo(GuidancePanel); + +type WorkspaceLayoutProps = Pick< + GraphServices, + | 'setHeaderActionMenu' + | 'graphSavePolicy' + | 'navigation' + | 'capabilities' + | 'coreStart' + | 'canEditDrillDownUrls' + | 'overlays' +> & { + renderCounter: number; + workspace?: Workspace; + loading: boolean; + indexPatterns: IndexPatternSavedObject[]; + savedWorkspace: GraphWorkspaceSavedObject; + indexPatternProvider: IndexPatternProvider; + urlQuery: string | null; +}; + +interface WorkspaceLayoutStateProps { + workspaceInitialized: boolean; + hasFields: boolean; +} + +const WorkspaceLayoutComponent = ({ + renderCounter, + workspace, + loading, + savedWorkspace, + hasFields, + overlays, + workspaceInitialized, + indexPatterns, + indexPatternProvider, + capabilities, + coreStart, + graphSavePolicy, + navigation, + canEditDrillDownUrls, + urlQuery, + setHeaderActionMenu, +}: WorkspaceLayoutProps & WorkspaceLayoutStateProps) => { + const [currentIndexPattern, setCurrentIndexPattern] = useState(); + const [showInspect, setShowInspect] = useState(false); + const [pickerOpen, setPickerOpen] = useState(false); + const [mergeCandidates, setMergeCandidates] = useState([]); + const [control, setControl] = useState('none'); + const selectedNode = useRef(undefined); + const isInitialized = Boolean(workspaceInitialized || savedWorkspace.id); + + const selectSelected = useCallback((node: WorkspaceNode) => { + selectedNode.current = node; + setControl('editLabel'); + }, []); + + const onSetControl = useCallback((newControl: ControlType) => { + selectedNode.current = undefined; + setControl(newControl); + }, []); + + const onIndexPatternChange = useCallback( + (indexPattern?: IndexPattern) => setCurrentIndexPattern(indexPattern), + [] + ); + + const onOpenFieldPicker = useCallback(() => { + setPickerOpen(true); + }, []); + + const confirmWipeWorkspace = useCallback( + ( + onConfirm: () => void, + text?: string, + options?: { confirmButtonText: string; title: string } + ) => { + if (!hasFields) { + onConfirm(); + return; + } + const confirmModalOptions = { + confirmButtonText: i18n.translate('xpack.graph.leaveWorkspace.confirmButtonLabel', { + defaultMessage: 'Leave anyway', + }), + title: i18n.translate('xpack.graph.leaveWorkspace.modalTitle', { + defaultMessage: 'Unsaved changes', + }), + 'data-test-subj': 'confirmModal', + ...options, + }; + + overlays + .openConfirm( + text || + i18n.translate('xpack.graph.leaveWorkspace.confirmText', { + defaultMessage: 'If you leave now, you will lose unsaved changes.', + }), + confirmModalOptions + ) + .then((isConfirmed) => { + if (isConfirmed) { + onConfirm(); + } + }); + }, + [hasFields, overlays] + ); + + const onSetMergeCandidates = useCallback( + (terms: TermIntersect[]) => setMergeCandidates(terms), + [] + ); + + return ( + + + + + + {isInitialized && } +

+ + + +
+ {!isInitialized && ( +
+ +
+ )} + + {isInitialized && workspace && ( +
+
+ +
+ + +
+ )} + + ); +}; + +export const WorkspaceLayout = connect( + (state: GraphState) => ({ + workspaceInitialized: workspaceInitializedSelector(state), + hasFields: hasFieldsSelector(state), + }) +)(WorkspaceLayoutComponent); diff --git a/x-pack/plugins/graph/public/components/workspace_layout/workspace_top_nav_menu.tsx b/x-pack/plugins/graph/public/components/workspace_layout/workspace_top_nav_menu.tsx new file mode 100644 index 000000000000..c5b10b9d9212 --- /dev/null +++ b/x-pack/plugins/graph/public/components/workspace_layout/workspace_top_nav_menu.tsx @@ -0,0 +1,175 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { Provider, useStore } from 'react-redux'; +import { AppMountParameters, Capabilities, CoreStart } from 'kibana/public'; +import { useHistory, useLocation } from 'react-router-dom'; +import { NavigationPublicPluginStart as NavigationStart } from '../../../../../../src/plugins/navigation/public'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; +import { datasourceSelector, hasFieldsSelector } from '../../state_management'; +import { GraphSavePolicy, GraphWorkspaceSavedObject, Workspace } from '../../types'; +import { AsObservable, Settings, SettingsWorkspaceProps } from '../settings'; +import { asSyncedObservable } from '../../helpers/as_observable'; + +interface WorkspaceTopNavMenuProps { + workspace: Workspace | undefined; + setShowInspect: React.Dispatch>; + confirmWipeWorkspace: ( + onConfirm: () => void, + text?: string, + options?: { confirmButtonText: string; title: string } + ) => void; + savedWorkspace: GraphWorkspaceSavedObject; + setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; + graphSavePolicy: GraphSavePolicy; + navigation: NavigationStart; + capabilities: Capabilities; + coreStart: CoreStart; + canEditDrillDownUrls: boolean; + isInitialized: boolean; +} + +export const WorkspaceTopNavMenu = (props: WorkspaceTopNavMenuProps) => { + const store = useStore(); + const location = useLocation(); + const history = useHistory(); + + // register things for legacy angular UI + const allSavingDisabled = props.graphSavePolicy === 'none'; + + // ===== Menubar configuration ========= + const { TopNavMenu } = props.navigation.ui; + const topNavMenu = []; + topNavMenu.push({ + key: 'new', + label: i18n.translate('xpack.graph.topNavMenu.newWorkspaceLabel', { + defaultMessage: 'New', + }), + description: i18n.translate('xpack.graph.topNavMenu.newWorkspaceAriaLabel', { + defaultMessage: 'New Workspace', + }), + tooltip: i18n.translate('xpack.graph.topNavMenu.newWorkspaceTooltip', { + defaultMessage: 'Create a new workspace', + }), + disableButton() { + return !props.isInitialized; + }, + run() { + props.confirmWipeWorkspace(() => { + if (location.pathname === '/workspace/') { + history.go(0); + } else { + history.push('/workspace/'); + } + }); + }, + testId: 'graphNewButton', + }); + + // if saving is disabled using uiCapabilities, we don't want to render the save + // button so it's consistent with all of the other applications + if (props.capabilities.graph.save) { + // allSavingDisabled is based on the xpack.graph.savePolicy, we'll maintain this functionality + + topNavMenu.push({ + key: 'save', + label: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledLabel', { + defaultMessage: 'Save', + }), + description: i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledAriaLabel', { + defaultMessage: 'Save workspace', + }), + tooltip: () => { + if (allSavingDisabled) { + return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.disabledTooltip', { + defaultMessage: + 'No changes to saved workspaces are permitted by the current save policy', + }); + } else { + return i18n.translate('xpack.graph.topNavMenu.saveWorkspace.enabledTooltip', { + defaultMessage: 'Save this workspace', + }); + } + }, + disableButton() { + return allSavingDisabled || !hasFieldsSelector(store.getState()); + }, + run: () => { + store.dispatch({ type: 'x-pack/graph/SAVE_WORKSPACE', payload: props.savedWorkspace }); + }, + testId: 'graphSaveButton', + }); + } + topNavMenu.push({ + key: 'inspect', + disableButton() { + return props.workspace === null; + }, + label: i18n.translate('xpack.graph.topNavMenu.inspectLabel', { + defaultMessage: 'Inspect', + }), + description: i18n.translate('xpack.graph.topNavMenu.inspectAriaLabel', { + defaultMessage: 'Inspect', + }), + run: () => { + props.setShowInspect((prevShowInspect) => !prevShowInspect); + }, + }); + + topNavMenu.push({ + key: 'settings', + disableButton() { + return datasourceSelector(store.getState()).current.type === 'none'; + }, + label: i18n.translate('xpack.graph.topNavMenu.settingsLabel', { + defaultMessage: 'Settings', + }), + description: i18n.translate('xpack.graph.topNavMenu.settingsAriaLabel', { + defaultMessage: 'Settings', + }), + run: () => { + // At this point workspace should be initialized, + // since settings button will be disabled only if workspace was set + const workspace = props.workspace as Workspace; + + const settingsObservable = (asSyncedObservable(() => ({ + blocklistedNodes: workspace.blocklistedNodes, + unblockNode: workspace.unblockNode, + unblockAll: workspace.unblockAll, + canEditDrillDownUrls: props.canEditDrillDownUrls, + })) as unknown) as AsObservable['observable']; + + props.coreStart.overlays.openFlyout( + toMountPoint( + + + + ), + { + size: 'm', + closeButtonAriaLabel: i18n.translate('xpack.graph.settings.closeLabel', { + defaultMessage: 'Close', + }), + 'data-test-subj': 'graphSettingsFlyout', + ownFocus: true, + className: 'gphSettingsFlyout', + maxWidth: 520, + } + ); + }, + }); + + return ( + + ); +}; diff --git a/x-pack/plugins/graph/public/helpers/as_observable.ts b/x-pack/plugins/graph/public/helpers/as_observable.ts index c1fa96364136..146161cceb46 100644 --- a/x-pack/plugins/graph/public/helpers/as_observable.ts +++ b/x-pack/plugins/graph/public/helpers/as_observable.ts @@ -12,19 +12,20 @@ interface Props { } /** - * This is a helper to tie state updates that happen somewhere else back to an angular scope. + * This is a helper to tie state updates that happen somewhere else back to an react state. * It is roughly comparable to `reactDirective`, but does not have to be used from within a * template. * - * This is a temporary solution until the state management is moved outside of Angular. + * This is a temporary solution until the state of Workspace internals is moved outside + * of mutable object to the redux state (at least blocklistedNodes, canEditDrillDownUrls and + * unblocklist action in this case). * * @param collectProps Function that collects properties from the scope that should be passed - * into the observable. All functions passed along will be wrapped to cause an angular digest cycle - * and refresh the observable afterwards with a new call to `collectProps`. By doing so, angular - * can react to changes made outside of it and the results are passed back via the observable - * @param angularDigest The `$digest` function of the scope. + * into the observable. All functions passed along will be wrapped to cause a react render + * and refresh the observable afterwards with a new call to `collectProps`. By doing so, react + * will receive an update outside of it local state and the results are passed back via the observable. */ -export function asAngularSyncedObservable(collectProps: () => Props, angularDigest: () => void) { +export function asSyncedObservable(collectProps: () => Props) { const boundCollectProps = () => { const collectedProps = collectProps(); Object.keys(collectedProps).forEach((key) => { @@ -32,7 +33,6 @@ export function asAngularSyncedObservable(collectProps: () => Props, angularDige if (typeof currentValue === 'function') { collectedProps[key] = (...args: unknown[]) => { currentValue(...args); - angularDigest(); subject$.next(boundCollectProps()); }; } diff --git a/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts b/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts index 1d8be0fe86b9..336708173d32 100644 --- a/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts +++ b/x-pack/plugins/graph/public/helpers/saved_workspace_utils.ts @@ -49,7 +49,7 @@ const defaultsProps = { const urlFor = (basePath: IBasePath, id: string) => basePath.prepend(`/app/graph#/workspace/${encodeURIComponent(id)}`); -function mapHits(hit: { id: string; attributes: Record }, url: string) { +function mapHits(hit: any, url: string): GraphWorkspaceSavedObject { const source = hit.attributes; source.id = hit.id; source.url = url; diff --git a/x-pack/plugins/graph/public/helpers/use_graph_loader.ts b/x-pack/plugins/graph/public/helpers/use_graph_loader.ts new file mode 100644 index 000000000000..c133f6bf260c --- /dev/null +++ b/x-pack/plugins/graph/public/helpers/use_graph_loader.ts @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useCallback, useState } from 'react'; +import { ToastsStart } from 'kibana/public'; +import { IHttpFetchError, CoreStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { ExploreRequest, GraphExploreCallback, GraphSearchCallback, SearchRequest } from '../types'; +import { formatHttpError } from './format_http_error'; + +interface UseGraphLoaderProps { + toastNotifications: ToastsStart; + coreStart: CoreStart; +} + +export const useGraphLoader = ({ toastNotifications, coreStart }: UseGraphLoaderProps) => { + const [loading, setLoading] = useState(false); + + const handleHttpError = useCallback( + (error: IHttpFetchError) => { + toastNotifications.addDanger(formatHttpError(error)); + }, + [toastNotifications] + ); + + const handleSearchQueryError = useCallback( + (err: Error | string) => { + const toastTitle = i18n.translate('xpack.graph.errorToastTitle', { + defaultMessage: 'Graph Error', + description: '"Graph" is a product name and should not be translated.', + }); + if (err instanceof Error) { + toastNotifications.addError(err, { + title: toastTitle, + }); + } else { + toastNotifications.addDanger({ + title: toastTitle, + text: String(err), + }); + } + }, + [toastNotifications] + ); + + // Replacement function for graphClientWorkspace's comms so + // that it works with Kibana. + const callNodeProxy = useCallback( + (indexName: string, query: ExploreRequest, responseHandler: GraphExploreCallback) => { + const request = { + body: JSON.stringify({ + index: indexName, + query, + }), + }; + setLoading(true); + return coreStart.http + .post('../api/graph/graphExplore', request) + .then(function (data) { + const response = data.resp; + if (response.timed_out) { + toastNotifications.addWarning( + i18n.translate('xpack.graph.exploreGraph.timedOutWarningText', { + defaultMessage: 'Exploration timed out', + }) + ); + } + responseHandler(response); + }) + .catch(handleHttpError) + .finally(() => setLoading(false)); + }, + [coreStart.http, handleHttpError, toastNotifications] + ); + + // Helper function for the graphClientWorkspace to perform a query + const callSearchNodeProxy = useCallback( + (indexName: string, query: SearchRequest, responseHandler: GraphSearchCallback) => { + const request = { + body: JSON.stringify({ + index: indexName, + body: query, + }), + }; + setLoading(true); + coreStart.http + .post('../api/graph/searchProxy', request) + .then(function (data) { + const response = data.resp; + responseHandler(response); + }) + .catch(handleHttpError) + .finally(() => setLoading(false)); + }, + [coreStart.http, handleHttpError] + ); + + return { + loading, + callNodeProxy, + callSearchNodeProxy, + handleSearchQueryError, + }; +}; diff --git a/x-pack/plugins/graph/public/helpers/use_workspace_loader.ts b/x-pack/plugins/graph/public/helpers/use_workspace_loader.ts new file mode 100644 index 000000000000..8b91546d5244 --- /dev/null +++ b/x-pack/plugins/graph/public/helpers/use_workspace_loader.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SavedObjectsClientContract, ToastsStart } from 'kibana/public'; +import { useEffect, useState } from 'react'; +import { useHistory, useLocation, useParams } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; +import { GraphStore } from '../state_management'; +import { GraphWorkspaceSavedObject, IndexPatternSavedObject, Workspace } from '../types'; +import { getSavedWorkspace } from './saved_workspace_utils'; + +interface UseWorkspaceLoaderProps { + store: GraphStore; + workspaceRef: React.MutableRefObject; + savedObjectsClient: SavedObjectsClientContract; + toastNotifications: ToastsStart; +} + +interface WorkspaceUrlParams { + id?: string; +} + +export const useWorkspaceLoader = ({ + workspaceRef, + store, + savedObjectsClient, + toastNotifications, +}: UseWorkspaceLoaderProps) => { + const [indexPatterns, setIndexPatterns] = useState(); + const [savedWorkspace, setSavedWorkspace] = useState(); + const history = useHistory(); + const location = useLocation(); + const { id } = useParams(); + + /** + * The following effect initializes workspace initially and reacts + * on changes in id parameter and URL query only. + */ + useEffect(() => { + const urlQuery = new URLSearchParams(location.search).get('query'); + + function loadWorkspace( + fetchedSavedWorkspace: GraphWorkspaceSavedObject, + fetchedIndexPatterns: IndexPatternSavedObject[] + ) { + store.dispatch({ + type: 'x-pack/graph/LOAD_WORKSPACE', + payload: { + savedWorkspace: fetchedSavedWorkspace, + indexPatterns: fetchedIndexPatterns, + urlQuery, + }, + }); + } + + function clearStore() { + store.dispatch({ type: 'x-pack/graph/RESET' }); + } + + async function fetchIndexPatterns() { + return await savedObjectsClient + .find<{ title: string }>({ + type: 'index-pattern', + fields: ['title', 'type'], + perPage: 10000, + }) + .then((response) => response.savedObjects); + } + + async function fetchSavedWorkspace() { + return (id + ? await getSavedWorkspace(savedObjectsClient, id).catch(function (e) { + toastNotifications.addError(e, { + title: i18n.translate('xpack.graph.missingWorkspaceErrorMessage', { + defaultMessage: "Couldn't load graph with ID", + }), + }); + history.replace('/home'); + // return promise that never returns to prevent the controller from loading + return new Promise(() => {}); + }) + : await getSavedWorkspace(savedObjectsClient)) as GraphWorkspaceSavedObject; + } + + async function initializeWorkspace() { + const fetchedIndexPatterns = await fetchIndexPatterns(); + const fetchedSavedWorkspace = await fetchSavedWorkspace(); + + /** + * Deal with situation of request to open saved workspace. Otherwise clean up store, + * when navigating to a new workspace from existing one. + */ + if (fetchedSavedWorkspace.id) { + loadWorkspace(fetchedSavedWorkspace, fetchedIndexPatterns); + } else if (workspaceRef.current) { + clearStore(); + } + + setIndexPatterns(fetchedIndexPatterns); + setSavedWorkspace(fetchedSavedWorkspace); + } + + initializeWorkspace(); + }, [ + id, + location, + store, + history, + savedObjectsClient, + setSavedWorkspace, + toastNotifications, + workspaceRef, + ]); + + return { savedWorkspace, indexPatterns }; +}; diff --git a/x-pack/plugins/graph/public/index.scss b/x-pack/plugins/graph/public/index.scss index f4e38de3e93a..4062864dd41e 100644 --- a/x-pack/plugins/graph/public/index.scss +++ b/x-pack/plugins/graph/public/index.scss @@ -10,5 +10,4 @@ @import './mixins'; @import './main'; -@import './angular/templates/index'; @import './components/index'; diff --git a/x-pack/plugins/graph/public/plugin.ts b/x-pack/plugins/graph/public/plugin.ts index 70671260ce5b..1ff9afe505a3 100644 --- a/x-pack/plugins/graph/public/plugin.ts +++ b/x-pack/plugins/graph/public/plugin.ts @@ -84,7 +84,6 @@ export class GraphPlugin updater$: this.appUpdater$, mount: async (params: AppMountParameters) => { const [coreStart, pluginsStart] = await core.getStartServices(); - await pluginsStart.kibanaLegacy.loadAngularBootstrap(); coreStart.chrome.docTitle.change( i18n.translate('xpack.graph.pageTitle', { defaultMessage: 'Graph' }) ); @@ -104,7 +103,7 @@ export class GraphPlugin canEditDrillDownUrls: config.canEditDrillDownUrls, graphSavePolicy: config.savePolicy, storage: new Storage(window.localStorage), - capabilities: coreStart.application.capabilities.graph, + capabilities: coreStart.application.capabilities, chrome: coreStart.chrome, toastNotifications: coreStart.notifications.toasts, indexPatterns: pluginsStart.data!.indexPatterns, diff --git a/x-pack/plugins/graph/public/router.tsx b/x-pack/plugins/graph/public/router.tsx new file mode 100644 index 000000000000..61a39bbbf63d --- /dev/null +++ b/x-pack/plugins/graph/public/router.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { createHashHistory } from 'history'; +import { Redirect, Route, Router, Switch } from 'react-router-dom'; +import { ListingRoute } from './apps/listing_route'; +import { GraphServices } from './application'; +import { WorkspaceRoute } from './apps/workspace_route'; + +export const graphRouter = (deps: GraphServices) => { + const history = createHashHistory(); + + return ( + + + + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts b/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts index 443d8581c435..31826c3b3a74 100644 --- a/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts +++ b/x-pack/plugins/graph/public/services/persistence/deserialize.test.ts @@ -7,7 +7,7 @@ import { GraphWorkspaceSavedObject, IndexPatternSavedObject, Workspace } from '../../types'; import { migrateLegacyIndexPatternRef, savedWorkspaceToAppState, mapFields } from './deserialize'; -import { createWorkspace } from '../../angular/graph_client_workspace'; +import { createWorkspace } from '../../services/workspace/graph_client_workspace'; import { outlinkEncoders } from '../../helpers/outlink_encoders'; import { IndexPattern } from '../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/graph/public/services/persistence/serialize.test.ts b/x-pack/plugins/graph/public/services/persistence/serialize.test.ts index 8213aac3fd62..2466582bc7b2 100644 --- a/x-pack/plugins/graph/public/services/persistence/serialize.test.ts +++ b/x-pack/plugins/graph/public/services/persistence/serialize.test.ts @@ -146,7 +146,7 @@ describe('serialize', () => { target: appState.workspace.nodes[0], weight: 5, width: 5, - }); + } as WorkspaceEdge); // C <-> E appState.workspace.edges.push({ @@ -155,7 +155,7 @@ describe('serialize', () => { target: appState.workspace.nodes[4], weight: 5, width: 5, - }); + } as WorkspaceEdge); }); it('should serialize given workspace', () => { diff --git a/x-pack/plugins/graph/public/services/persistence/serialize.ts b/x-pack/plugins/graph/public/services/persistence/serialize.ts index 65392b69b5a6..e1ec8db19a4c 100644 --- a/x-pack/plugins/graph/public/services/persistence/serialize.ts +++ b/x-pack/plugins/graph/public/services/persistence/serialize.ts @@ -6,7 +6,6 @@ */ import { - SerializedNode, WorkspaceNode, WorkspaceEdge, SerializedEdge, @@ -17,13 +16,15 @@ import { SerializedWorkspaceState, Workspace, AdvancedSettings, + SerializedNode, + BlockListedNode, } from '../../types'; import { IndexpatternDatasource } from '../../state_management'; function serializeNode( - { data, scaledSize, parent, x, y, label, color }: WorkspaceNode, + { data, scaledSize, parent, x, y, label, color }: BlockListedNode, allNodes: WorkspaceNode[] = [] -): SerializedNode { +) { return { x, y, diff --git a/x-pack/plugins/graph/public/services/save_modal.tsx b/x-pack/plugins/graph/public/services/save_modal.tsx index eff98ebeded4..f1603ed790d3 100644 --- a/x-pack/plugins/graph/public/services/save_modal.tsx +++ b/x-pack/plugins/graph/public/services/save_modal.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React from 'react'; +import React, { ReactElement } from 'react'; import { I18nStart, OverlayStart, SavedObjectsClientContract } from 'src/core/public'; import { SaveResult } from 'src/plugins/saved_objects/public'; import { GraphWorkspaceSavedObject, GraphSavePolicy } from '../types'; @@ -39,7 +39,7 @@ export function openSaveModal({ hasData: boolean; workspace: GraphWorkspaceSavedObject; saveWorkspace: SaveWorkspaceHandler; - showSaveModal: (el: React.ReactNode, I18nContext: I18nStart['Context']) => void; + showSaveModal: (el: ReactElement, I18nContext: I18nStart['Context']) => void; I18nContext: I18nStart['Context']; services: SaveWorkspaceServices; }) { diff --git a/x-pack/plugins/graph/public/angular/graph_client_workspace.d.ts b/x-pack/plugins/graph/public/services/workspace/graph_client_workspace.d.ts similarity index 100% rename from x-pack/plugins/graph/public/angular/graph_client_workspace.d.ts rename to x-pack/plugins/graph/public/services/workspace/graph_client_workspace.d.ts diff --git a/x-pack/plugins/graph/public/angular/graph_client_workspace.js b/x-pack/plugins/graph/public/services/workspace/graph_client_workspace.js similarity index 99% rename from x-pack/plugins/graph/public/angular/graph_client_workspace.js rename to x-pack/plugins/graph/public/services/workspace/graph_client_workspace.js index 07e4dfc2e874..c849a25cb19b 100644 --- a/x-pack/plugins/graph/public/angular/graph_client_workspace.js +++ b/x-pack/plugins/graph/public/services/workspace/graph_client_workspace.js @@ -631,10 +631,14 @@ function GraphWorkspace(options) { self.runLayout(); }; - this.unblocklist = function (node) { + this.unblockNode = function (node) { self.arrRemove(self.blocklistedNodes, node); }; + this.unblockAll = function () { + self.arrRemoveAll(self.blocklistedNodes, self.blocklistedNodes); + }; + this.blocklistSelection = function () { const selection = self.getAllSelectedNodes(); const danglingEdges = []; diff --git a/x-pack/plugins/graph/public/angular/graph_client_workspace.test.js b/x-pack/plugins/graph/public/services/workspace/graph_client_workspace.test.js similarity index 100% rename from x-pack/plugins/graph/public/angular/graph_client_workspace.test.js rename to x-pack/plugins/graph/public/services/workspace/graph_client_workspace.test.js diff --git a/x-pack/plugins/graph/public/state_management/advanced_settings.ts b/x-pack/plugins/graph/public/state_management/advanced_settings.ts index 82f1358dd416..68b9e002766e 100644 --- a/x-pack/plugins/graph/public/state_management/advanced_settings.ts +++ b/x-pack/plugins/graph/public/state_management/advanced_settings.ts @@ -43,14 +43,14 @@ export const settingsSelector = (state: GraphState) => state.advancedSettings; * * Won't be necessary once the workspace is moved to redux */ -export const syncSettingsSaga = ({ getWorkspace, notifyAngular }: GraphStoreDependencies) => { +export const syncSettingsSaga = ({ getWorkspace, notifyReact }: GraphStoreDependencies) => { function* syncSettings(action: Action): IterableIterator { const workspace = getWorkspace(); if (!workspace) { return; } workspace.options.exploreControls = action.payload; - notifyAngular(); + notifyReact(); } return function* () { diff --git a/x-pack/plugins/graph/public/state_management/datasource.sagas.ts b/x-pack/plugins/graph/public/state_management/datasource.sagas.ts index b185af28c348..9bfc7b3da0f9 100644 --- a/x-pack/plugins/graph/public/state_management/datasource.sagas.ts +++ b/x-pack/plugins/graph/public/state_management/datasource.sagas.ts @@ -30,7 +30,7 @@ export const datasourceSaga = ({ indexPatternProvider, notifications, createWorkspace, - notifyAngular, + notifyReact, }: GraphStoreDependencies) => { function* fetchFields(action: Action) { try { @@ -39,7 +39,7 @@ export const datasourceSaga = ({ yield put(datasourceLoaded()); const advancedSettings = settingsSelector(yield select()); createWorkspace(indexPattern.title, advancedSettings); - notifyAngular(); + notifyReact(); } catch (e) { // in case of errors, reset the datasource and show notification yield put(setDatasource({ type: 'none' })); diff --git a/x-pack/plugins/graph/public/state_management/fields.ts b/x-pack/plugins/graph/public/state_management/fields.ts index 051f5328091e..3a117fa6fe50 100644 --- a/x-pack/plugins/graph/public/state_management/fields.ts +++ b/x-pack/plugins/graph/public/state_management/fields.ts @@ -69,9 +69,9 @@ export const hasFieldsSelector = createSelector( * * Won't be necessary once the workspace is moved to redux */ -export const updateSaveButtonSaga = ({ notifyAngular }: GraphStoreDependencies) => { +export const updateSaveButtonSaga = ({ notifyReact }: GraphStoreDependencies) => { function* notify(): IterableIterator { - notifyAngular(); + notifyReact(); } return function* () { yield takeLatest(matchesOne(selectField, deselectField), notify); @@ -84,7 +84,7 @@ export const updateSaveButtonSaga = ({ notifyAngular }: GraphStoreDependencies) * * Won't be necessary once the workspace is moved to redux */ -export const syncFieldsSaga = ({ getWorkspace, setLiveResponseFields }: GraphStoreDependencies) => { +export const syncFieldsSaga = ({ getWorkspace }: GraphStoreDependencies) => { function* syncFields() { const workspace = getWorkspace(); if (!workspace) { @@ -93,7 +93,6 @@ export const syncFieldsSaga = ({ getWorkspace, setLiveResponseFields }: GraphSto const currentState = yield select(); workspace.options.vertex_fields = selectedFieldsSelector(currentState); - setLiveResponseFields(liveResponseFieldsSelector(currentState)); } return function* () { yield takeEvery( @@ -109,7 +108,7 @@ export const syncFieldsSaga = ({ getWorkspace, setLiveResponseFields }: GraphSto * * Won't be necessary once the workspace is moved to redux */ -export const syncNodeStyleSaga = ({ getWorkspace, notifyAngular }: GraphStoreDependencies) => { +export const syncNodeStyleSaga = ({ getWorkspace, notifyReact }: GraphStoreDependencies) => { function* syncNodeStyle(action: Action>) { const workspace = getWorkspace(); if (!workspace) { @@ -132,7 +131,7 @@ export const syncNodeStyleSaga = ({ getWorkspace, notifyAngular }: GraphStoreDep } }); } - notifyAngular(); + notifyReact(); const selectedFields = selectedFieldsSelector(yield select()); workspace.options.vertex_fields = selectedFields; diff --git a/x-pack/plugins/graph/public/state_management/legacy.test.ts b/x-pack/plugins/graph/public/state_management/legacy.test.ts index 1dbad39a918a..5a05efdc478f 100644 --- a/x-pack/plugins/graph/public/state_management/legacy.test.ts +++ b/x-pack/plugins/graph/public/state_management/legacy.test.ts @@ -77,13 +77,12 @@ describe('legacy sync sagas', () => { it('syncs templates with workspace', () => { env.store.dispatch(loadTemplates([])); - expect(env.mockedDeps.setUrlTemplates).toHaveBeenCalledWith([]); - expect(env.mockedDeps.notifyAngular).toHaveBeenCalled(); + expect(env.mockedDeps.notifyReact).toHaveBeenCalled(); }); it('notifies angular when fields are selected', () => { env.store.dispatch(selectField('field1')); - expect(env.mockedDeps.notifyAngular).toHaveBeenCalled(); + expect(env.mockedDeps.notifyReact).toHaveBeenCalled(); }); it('syncs field list with workspace', () => { @@ -99,9 +98,6 @@ describe('legacy sync sagas', () => { const workspace = env.mockedDeps.getWorkspace()!; expect(workspace.options.vertex_fields![0].name).toEqual('field1'); expect(workspace.options.vertex_fields![0].hopSize).toEqual(22); - expect(env.mockedDeps.setLiveResponseFields).toHaveBeenCalledWith([ - expect.objectContaining({ hopSize: 22 }), - ]); }); it('syncs styles with nodes', () => { diff --git a/x-pack/plugins/graph/public/state_management/mocks.ts b/x-pack/plugins/graph/public/state_management/mocks.ts index 74d980753a09..189875d04b01 100644 --- a/x-pack/plugins/graph/public/state_management/mocks.ts +++ b/x-pack/plugins/graph/public/state_management/mocks.ts @@ -15,7 +15,7 @@ import createSagaMiddleware from 'redux-saga'; import { createStore, applyMiddleware, AnyAction } from 'redux'; import { ChromeStart } from 'kibana/public'; import { GraphStoreDependencies, createRootReducer, GraphStore, GraphState } from './store'; -import { Workspace, GraphWorkspaceSavedObject, IndexPatternSavedObject } from '../types'; +import { Workspace } from '../types'; import { IndexPattern } from '../../../../../src/plugins/data/public'; export interface MockedGraphEnvironment { @@ -48,11 +48,8 @@ export function createMockGraphStore({ blocklistedNodes: [], } as unknown) as Workspace; - const savedWorkspace = ({ - save: jest.fn(), - } as unknown) as GraphWorkspaceSavedObject; - const mockedDeps: jest.Mocked = { + basePath: '', addBasePath: jest.fn((url: string) => url), changeUrl: jest.fn(), chrome: ({ @@ -60,15 +57,11 @@ export function createMockGraphStore({ } as unknown) as ChromeStart, createWorkspace: jest.fn(), getWorkspace: jest.fn(() => workspaceMock), - getSavedWorkspace: jest.fn(() => savedWorkspace), indexPatternProvider: { get: jest.fn(() => Promise.resolve(({ id: '123', title: 'test-pattern' } as unknown) as IndexPattern) ), }, - indexPatterns: [ - ({ id: '123', attributes: { title: 'test-pattern' } } as unknown) as IndexPatternSavedObject, - ], I18nContext: jest .fn() .mockImplementation(({ children }: { children: React.ReactNode }) => children), @@ -79,12 +72,9 @@ export function createMockGraphStore({ }, } as unknown) as NotificationsStart, http: {} as HttpStart, - notifyAngular: jest.fn(), + notifyReact: jest.fn(), savePolicy: 'configAndData', showSaveModal: jest.fn(), - setLiveResponseFields: jest.fn(), - setUrlTemplates: jest.fn(), - setWorkspaceInitialized: jest.fn(), overlays: ({ openModal: jest.fn(), } as unknown) as OverlayStart, @@ -92,6 +82,7 @@ export function createMockGraphStore({ find: jest.fn(), get: jest.fn(), } as unknown) as SavedObjectsClientContract, + handleSearchQueryError: jest.fn(), ...mockedDepsOverwrites, }; const sagaMiddleware = createSagaMiddleware(); diff --git a/x-pack/plugins/graph/public/state_management/persistence.test.ts b/x-pack/plugins/graph/public/state_management/persistence.test.ts index b0932c92c2d1..dc59869fafd4 100644 --- a/x-pack/plugins/graph/public/state_management/persistence.test.ts +++ b/x-pack/plugins/graph/public/state_management/persistence.test.ts @@ -6,8 +6,14 @@ */ import { createMockGraphStore, MockedGraphEnvironment } from './mocks'; -import { loadSavedWorkspace, loadingSaga, saveWorkspace, savingSaga } from './persistence'; -import { GraphWorkspaceSavedObject, UrlTemplate, AdvancedSettings, WorkspaceField } from '../types'; +import { + loadSavedWorkspace, + loadingSaga, + saveWorkspace, + savingSaga, + LoadSavedWorkspacePayload, +} from './persistence'; +import { UrlTemplate, AdvancedSettings, WorkspaceField, GraphWorkspaceSavedObject } from '../types'; import { IndexpatternDatasource, datasourceSelector } from './datasource'; import { fieldsSelector } from './fields'; import { metaDataSelector, updateMetaData } from './meta_data'; @@ -55,7 +61,9 @@ describe('persistence sagas', () => { }); it('should deserialize saved object and populate state', async () => { env.store.dispatch( - loadSavedWorkspace({ title: 'my workspace' } as GraphWorkspaceSavedObject) + loadSavedWorkspace({ + savedWorkspace: { title: 'my workspace' }, + } as LoadSavedWorkspacePayload) ); await waitForPromise(); const resultingState = env.store.getState(); @@ -70,7 +78,7 @@ describe('persistence sagas', () => { it('should warn with a toast and abort if index pattern is not found', async () => { (migrateLegacyIndexPatternRef as jest.Mock).mockReturnValueOnce({ success: false }); - env.store.dispatch(loadSavedWorkspace({} as GraphWorkspaceSavedObject)); + env.store.dispatch(loadSavedWorkspace({ savedWorkspace: {} } as LoadSavedWorkspacePayload)); await waitForPromise(); expect(env.mockedDeps.notifications.toasts.addDanger).toHaveBeenCalled(); const resultingState = env.store.getState(); @@ -96,11 +104,10 @@ describe('persistence sagas', () => { savePolicy: 'configAndDataWithConsent', }, }); - env.mockedDeps.getSavedWorkspace().id = '123'; }); it('should serialize saved object and save after confirmation', async () => { - env.store.dispatch(saveWorkspace()); + env.store.dispatch(saveWorkspace({ id: '123' } as GraphWorkspaceSavedObject)); (openSaveModal as jest.Mock).mock.calls[0][0].saveWorkspace({}, true); expect(appStateToSavedWorkspace).toHaveBeenCalled(); await waitForPromise(); @@ -112,7 +119,7 @@ describe('persistence sagas', () => { }); it('should not save data if user does not give consent in the modal', async () => { - env.store.dispatch(saveWorkspace()); + env.store.dispatch(saveWorkspace({} as GraphWorkspaceSavedObject)); (openSaveModal as jest.Mock).mock.calls[0][0].saveWorkspace({}, false); // serialize function is called with `canSaveData` set to false expect(appStateToSavedWorkspace).toHaveBeenCalledWith( @@ -123,9 +130,8 @@ describe('persistence sagas', () => { }); it('should not change url if it was just updating existing workspace', async () => { - env.mockedDeps.getSavedWorkspace().id = '123'; env.store.dispatch(updateMetaData({ savedObjectId: '123' })); - env.store.dispatch(saveWorkspace()); + env.store.dispatch(saveWorkspace({} as GraphWorkspaceSavedObject)); await waitForPromise(); expect(env.mockedDeps.changeUrl).not.toHaveBeenCalled(); }); diff --git a/x-pack/plugins/graph/public/state_management/persistence.ts b/x-pack/plugins/graph/public/state_management/persistence.ts index f815474fa6e5..6a99eaddb32e 100644 --- a/x-pack/plugins/graph/public/state_management/persistence.ts +++ b/x-pack/plugins/graph/public/state_management/persistence.ts @@ -8,8 +8,8 @@ import actionCreatorFactory, { Action } from 'typescript-fsa'; import { i18n } from '@kbn/i18n'; import { takeLatest, call, put, select, cps } from 'redux-saga/effects'; -import { GraphWorkspaceSavedObject, Workspace } from '../types'; -import { GraphStoreDependencies, GraphState } from '.'; +import { GraphWorkspaceSavedObject, IndexPatternSavedObject, Workspace } from '../types'; +import { GraphStoreDependencies, GraphState, submitSearch } from '.'; import { datasourceSelector } from './datasource'; import { setDatasource, IndexpatternDatasource } from './datasource'; import { loadFields, selectedFieldsSelector } from './fields'; @@ -26,10 +26,17 @@ import { openSaveModal, SaveWorkspaceHandler } from '../services/save_modal'; import { getEditPath } from '../services/url'; import { saveSavedWorkspace } from '../helpers/saved_workspace_utils'; +export interface LoadSavedWorkspacePayload { + indexPatterns: IndexPatternSavedObject[]; + savedWorkspace: GraphWorkspaceSavedObject; + urlQuery: string | null; +} + const actionCreator = actionCreatorFactory('x-pack/graph'); -export const loadSavedWorkspace = actionCreator('LOAD_WORKSPACE'); -export const saveWorkspace = actionCreator('SAVE_WORKSPACE'); +export const loadSavedWorkspace = actionCreator('LOAD_WORKSPACE'); +export const saveWorkspace = actionCreator('SAVE_WORKSPACE'); +export const fillWorkspace = actionCreator('FILL_WORKSPACE'); /** * Saga handling loading of a saved workspace. @@ -39,14 +46,12 @@ export const saveWorkspace = actionCreator('SAVE_WORKSPACE'); */ export const loadingSaga = ({ createWorkspace, - getWorkspace, - indexPatterns, notifications, indexPatternProvider, }: GraphStoreDependencies) => { - function* deserializeWorkspace(action: Action) { - const workspacePayload = action.payload; - const migrationStatus = migrateLegacyIndexPatternRef(workspacePayload, indexPatterns); + function* deserializeWorkspace(action: Action) { + const { indexPatterns, savedWorkspace, urlQuery } = action.payload; + const migrationStatus = migrateLegacyIndexPatternRef(savedWorkspace, indexPatterns); if (!migrationStatus.success) { notifications.toasts.addDanger( i18n.translate('xpack.graph.loadWorkspace.missingIndexPatternErrorMessage', { @@ -59,25 +64,24 @@ export const loadingSaga = ({ return; } - const selectedIndexPatternId = lookupIndexPatternId(workspacePayload); + const selectedIndexPatternId = lookupIndexPatternId(savedWorkspace); const indexPattern = yield call(indexPatternProvider.get, selectedIndexPatternId); const initialSettings = settingsSelector(yield select()); - createWorkspace(indexPattern.title, initialSettings); + const createdWorkspace = createWorkspace(indexPattern.title, initialSettings); const { urlTemplates, advancedSettings, allFields } = savedWorkspaceToAppState( - workspacePayload, + savedWorkspace, indexPattern, - // workspace won't be null because it's created in the same call stack - getWorkspace()! + createdWorkspace ); // put everything in the store yield put( updateMetaData({ - title: workspacePayload.title, - description: workspacePayload.description, - savedObjectId: workspacePayload.id, + title: savedWorkspace.title, + description: savedWorkspace.description, + savedObjectId: savedWorkspace.id, }) ); yield put( @@ -91,7 +95,11 @@ export const loadingSaga = ({ yield put(updateSettings(advancedSettings)); yield put(loadTemplates(urlTemplates)); - getWorkspace()!.runLayout(); + if (urlQuery) { + yield put(submitSearch(urlQuery)); + } + + createdWorkspace.runLayout(); } return function* () { @@ -105,8 +113,8 @@ export const loadingSaga = ({ * It will serialize everything and save it using the saved objects client */ export const savingSaga = (deps: GraphStoreDependencies) => { - function* persistWorkspace() { - const savedWorkspace = deps.getSavedWorkspace(); + function* persistWorkspace(action: Action) { + const savedWorkspace = action.payload; const state: GraphState = yield select(); const workspace = deps.getWorkspace(); const selectedDatasource = datasourceSelector(state).current; diff --git a/x-pack/plugins/graph/public/state_management/store.ts b/x-pack/plugins/graph/public/state_management/store.ts index 400736f7534b..ba9bff98b0ca 100644 --- a/x-pack/plugins/graph/public/state_management/store.ts +++ b/x-pack/plugins/graph/public/state_management/store.ts @@ -9,6 +9,7 @@ import createSagaMiddleware, { SagaMiddleware } from 'redux-saga'; import { combineReducers, createStore, Store, AnyAction, Dispatch, applyMiddleware } from 'redux'; import { ChromeStart, I18nStart, OverlayStart, SavedObjectsClientContract } from 'kibana/public'; import { CoreStart } from 'src/core/public'; +import { ReactElement } from 'react'; import { fieldsReducer, FieldsState, @@ -24,19 +25,10 @@ import { } from './advanced_settings'; import { DatasourceState, datasourceReducer } from './datasource'; import { datasourceSaga } from './datasource.sagas'; -import { - IndexPatternProvider, - Workspace, - IndexPatternSavedObject, - GraphSavePolicy, - GraphWorkspaceSavedObject, - AdvancedSettings, - WorkspaceField, - UrlTemplate, -} from '../types'; +import { IndexPatternProvider, Workspace, GraphSavePolicy, AdvancedSettings } from '../types'; import { loadingSaga, savingSaga } from './persistence'; import { metaDataReducer, MetaDataState, syncBreadcrumbSaga } from './meta_data'; -import { fillWorkspaceSaga } from './workspace'; +import { fillWorkspaceSaga, submitSearchSaga, workspaceReducer, WorkspaceState } from './workspace'; export interface GraphState { fields: FieldsState; @@ -44,28 +36,26 @@ export interface GraphState { advancedSettings: AdvancedSettingsState; datasource: DatasourceState; metaData: MetaDataState; + workspace: WorkspaceState; } export interface GraphStoreDependencies { addBasePath: (url: string) => string; indexPatternProvider: IndexPatternProvider; - indexPatterns: IndexPatternSavedObject[]; - createWorkspace: (index: string, advancedSettings: AdvancedSettings) => void; - getWorkspace: () => Workspace | null; - getSavedWorkspace: () => GraphWorkspaceSavedObject; + createWorkspace: (index: string, advancedSettings: AdvancedSettings) => Workspace; + getWorkspace: () => Workspace | undefined; notifications: CoreStart['notifications']; http: CoreStart['http']; overlays: OverlayStart; savedObjectsClient: SavedObjectsClientContract; - showSaveModal: (el: React.ReactNode, I18nContext: I18nStart['Context']) => void; + showSaveModal: (el: ReactElement, I18nContext: I18nStart['Context']) => void; savePolicy: GraphSavePolicy; changeUrl: (newUrl: string) => void; - notifyAngular: () => void; - setLiveResponseFields: (fields: WorkspaceField[]) => void; - setUrlTemplates: (templates: UrlTemplate[]) => void; - setWorkspaceInitialized: () => void; + notifyReact: () => void; chrome: ChromeStart; I18nContext: I18nStart['Context']; + basePath: string; + handleSearchQueryError: (err: Error | string) => void; } export function createRootReducer(addBasePath: (url: string) => string) { @@ -75,6 +65,7 @@ export function createRootReducer(addBasePath: (url: string) => string) { advancedSettings: advancedSettingsReducer, datasource: datasourceReducer, metaData: metaDataReducer, + workspace: workspaceReducer, }); } @@ -89,6 +80,7 @@ function registerSagas(sagaMiddleware: SagaMiddleware, deps: GraphStoreD sagaMiddleware.run(syncBreadcrumbSaga(deps)); sagaMiddleware.run(syncTemplatesSaga(deps)); sagaMiddleware.run(fillWorkspaceSaga(deps)); + sagaMiddleware.run(submitSearchSaga(deps)); } export const createGraphStore = (deps: GraphStoreDependencies) => { diff --git a/x-pack/plugins/graph/public/state_management/url_templates.ts b/x-pack/plugins/graph/public/state_management/url_templates.ts index e8f5308534e2..01b1a9296b0b 100644 --- a/x-pack/plugins/graph/public/state_management/url_templates.ts +++ b/x-pack/plugins/graph/public/state_management/url_templates.ts @@ -10,7 +10,7 @@ import { reducerWithInitialState } from 'typescript-fsa-reducers/dist'; import { i18n } from '@kbn/i18n'; import { modifyUrl } from '@kbn/std'; import rison from 'rison-node'; -import { takeEvery, select } from 'redux-saga/effects'; +import { takeEvery } from 'redux-saga/effects'; import { format, parse } from 'url'; import { GraphState, GraphStoreDependencies } from './store'; import { UrlTemplate } from '../types'; @@ -102,11 +102,9 @@ export const templatesSelector = (state: GraphState) => state.urlTemplates; * * Won't be necessary once the side bar is moved to redux */ -export const syncTemplatesSaga = ({ setUrlTemplates, notifyAngular }: GraphStoreDependencies) => { +export const syncTemplatesSaga = ({ notifyReact }: GraphStoreDependencies) => { function* syncTemplates() { - const templates = templatesSelector(yield select()); - setUrlTemplates(templates); - notifyAngular(); + notifyReact(); } return function* () { diff --git a/x-pack/plugins/graph/public/state_management/workspace.ts b/x-pack/plugins/graph/public/state_management/workspace.ts index 4e0e481a05c1..9e8cca488e4e 100644 --- a/x-pack/plugins/graph/public/state_management/workspace.ts +++ b/x-pack/plugins/graph/public/state_management/workspace.ts @@ -5,16 +5,41 @@ * 2.0. */ -import actionCreatorFactory from 'typescript-fsa'; +import actionCreatorFactory, { Action } from 'typescript-fsa'; import { i18n } from '@kbn/i18n'; -import { takeLatest, select, call } from 'redux-saga/effects'; -import { GraphStoreDependencies, GraphState } from '.'; +import { takeLatest, select, call, put } from 'redux-saga/effects'; +import { reducerWithInitialState } from 'typescript-fsa-reducers'; +import { createSelector } from 'reselect'; +import { GraphStoreDependencies, GraphState, fillWorkspace } from '.'; +import { reset } from './global'; import { datasourceSelector } from './datasource'; -import { selectedFieldsSelector } from './fields'; +import { liveResponseFieldsSelector, selectedFieldsSelector } from './fields'; import { fetchTopNodes } from '../services/fetch_top_nodes'; -const actionCreator = actionCreatorFactory('x-pack/graph'); +import { Workspace } from '../types'; -export const fillWorkspace = actionCreator('FILL_WORKSPACE'); +const actionCreator = actionCreatorFactory('x-pack/graph/workspace'); + +export interface WorkspaceState { + isInitialized: boolean; +} + +const initialWorkspaceState: WorkspaceState = { + isInitialized: false, +}; + +export const initializeWorkspace = actionCreator('INITIALIZE_WORKSPACE'); +export const submitSearch = actionCreator('SUBMIT_SEARCH'); + +export const workspaceReducer = reducerWithInitialState(initialWorkspaceState) + .case(reset, () => ({ isInitialized: false })) + .case(initializeWorkspace, () => ({ isInitialized: true })) + .build(); + +export const workspaceSelector = (state: GraphState) => state.workspace; +export const workspaceInitializedSelector = createSelector( + workspaceSelector, + (workspace: WorkspaceState) => workspace.isInitialized +); /** * Saga handling filling in top terms into workspace. @@ -23,8 +48,7 @@ export const fillWorkspace = actionCreator('FILL_WORKSPACE'); */ export const fillWorkspaceSaga = ({ getWorkspace, - setWorkspaceInitialized, - notifyAngular, + notifyReact, http, notifications, }: GraphStoreDependencies) => { @@ -47,8 +71,8 @@ export const fillWorkspaceSaga = ({ nodes: topTermNodes, edges: [], }); - setWorkspaceInitialized(); - notifyAngular(); + yield put(initializeWorkspace()); + notifyReact(); workspace.fillInGraph(fields.length * 10); } catch (e) { const message = 'body' in e ? e.body.message : e.message; @@ -65,3 +89,39 @@ export const fillWorkspaceSaga = ({ yield takeLatest(fillWorkspace.match, fetchNodes); }; }; + +export const submitSearchSaga = ({ + getWorkspace, + handleSearchQueryError, +}: GraphStoreDependencies) => { + function* submit(action: Action) { + const searchTerm = action.payload; + yield put(initializeWorkspace()); + + // type casting is safe, at this point workspace should be loaded + const workspace = getWorkspace() as Workspace; + const numHops = 2; + const liveResponseFields = liveResponseFieldsSelector(yield select()); + + if (searchTerm.startsWith('{')) { + try { + const query = JSON.parse(searchTerm); + if (query.vertices) { + // Is a graph explore request + workspace.callElasticsearch(query); + } else { + // Is a regular query DSL query + workspace.search(query, liveResponseFields, numHops); + } + } catch (err) { + handleSearchQueryError(err); + } + return; + } + workspace.simpleSearch(searchTerm, liveResponseFields, numHops); + } + + return function* () { + yield takeLatest(submitSearch.match, submit); + }; +}; diff --git a/x-pack/plugins/graph/public/types/persistence.ts b/x-pack/plugins/graph/public/types/persistence.ts index 46d711de0420..640348d96f6a 100644 --- a/x-pack/plugins/graph/public/types/persistence.ts +++ b/x-pack/plugins/graph/public/types/persistence.ts @@ -53,15 +53,15 @@ export interface SerializedField extends Omit { +export interface SerializedNode extends Pick { field: string; term: string; parent: number | null; size: number; } -export interface SerializedEdge extends Omit { +export interface SerializedEdge + extends Omit { source: number; target: number; } diff --git a/x-pack/plugins/graph/public/types/workspace_state.ts b/x-pack/plugins/graph/public/types/workspace_state.ts index 86f05376b952..bca94a7cfad6 100644 --- a/x-pack/plugins/graph/public/types/workspace_state.ts +++ b/x-pack/plugins/graph/public/types/workspace_state.ts @@ -6,10 +6,13 @@ */ import { JsonObject } from '@kbn/utility-types'; +import d3 from 'd3'; +import { TargetOptions } from '../components/control_panel'; import { FontawesomeIcon } from '../helpers/style_choices'; import { WorkspaceField, AdvancedSettings } from './app_state'; export interface WorkspaceNode { + id: string; x: number; y: number; label: string; @@ -21,9 +24,14 @@ export interface WorkspaceNode { scaledSize: number; parent: WorkspaceNode | null; color: string; + numChildren: number; isSelected?: boolean; + kx: number; + ky: number; } +export type BlockListedNode = Omit; + export interface WorkspaceEdge { weight: number; width: number; @@ -31,6 +39,8 @@ export interface WorkspaceEdge { source: WorkspaceNode; target: WorkspaceNode; isSelected?: boolean; + topTarget: WorkspaceNode; + topSrc: WorkspaceNode; } export interface ServerResultNode { @@ -58,13 +68,59 @@ export interface GraphData { nodes: ServerResultNode[]; edges: ServerResultEdge[]; } +export interface TermIntersect { + id1: string; + id2: string; + term1: string; + term2: string; + v1: number; + v2: number; + overlap: number; +} export interface Workspace { options: WorkspaceOptions; nodesMap: Record; nodes: WorkspaceNode[]; + selectedNodes: WorkspaceNode[]; edges: WorkspaceEdge[]; - blocklistedNodes: WorkspaceNode[]; + blocklistedNodes: BlockListedNode[]; + undoLog: string; + redoLog: string; + force: ReturnType; + lastRequest: string; + lastResponse: string; + + undo: () => void; + redo: () => void; + expandSelecteds: (targetOptions: TargetOptions) => {}; + deleteSelection: () => void; + blocklistSelection: () => void; + selectAll: () => void; + selectNone: () => void; + selectInvert: () => void; + selectNeighbours: () => void; + deselectNode: (node: WorkspaceNode) => void; + colorSelected: (color: string) => void; + groupSelections: (node: WorkspaceNode | undefined) => void; + ungroup: (node: WorkspaceNode | undefined) => void; + callElasticsearch: (request: any) => void; + search: (qeury: any, fieldsChoice: WorkspaceField[] | undefined, numHops: number) => void; + simpleSearch: ( + searchTerm: string, + fieldsChoice: WorkspaceField[] | undefined, + numHops: number + ) => void; + getAllIntersections: ( + callback: (termIntersects: TermIntersect[]) => void, + nodes: WorkspaceNode[] + ) => void; + toggleNodeSelection: (node: WorkspaceNode) => boolean; + mergeIds: (term1: string, term2: string) => void; + changeHandler: () => void; + unblockNode: (node: BlockListedNode) => void; + unblockAll: () => void; + clearGraph: () => void; getQuery(startNodes?: WorkspaceNode[], loose?: boolean): JsonObject; getSelectedOrAllNodes(): WorkspaceNode[]; @@ -96,6 +152,8 @@ export type ExploreRequest = any; export type SearchRequest = any; export type ExploreResults = any; export type SearchResults = any; +export type GraphExploreCallback = (data: ExploreResults) => void; +export type GraphSearchCallback = (data: SearchResults) => void; export type WorkspaceOptions = Partial<{ indexName: string; @@ -105,12 +163,14 @@ export type WorkspaceOptions = Partial<{ graphExploreProxy: ( indexPattern: string, request: ExploreRequest, - callback: (data: ExploreResults) => void + callback: GraphExploreCallback ) => void; searchProxy: ( indexPattern: string, request: SearchRequest, - callback: (data: SearchResults) => void + callback: GraphSearchCallback ) => void; exploreControls: AdvancedSettings; }>; + +export type ControlType = 'style' | 'drillDowns' | 'editLabel' | 'mergeTerms' | 'none'; diff --git a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx index de399e068be0..5a6d8bb878c3 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/index.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/index.tsx @@ -12,7 +12,7 @@ import { UnmountCallback } from 'src/core/public'; import { CloudSetup } from '../../../cloud/public'; import { ILicense } from '../../../licensing/public'; -import { KibanaContextProvider } from '../shared_imports'; +import { KibanaContextProvider, APP_WRAPPER_CLASS } from '../shared_imports'; import { App } from './app'; @@ -30,7 +30,7 @@ export const renderApp = ( ): UnmountCallback => { const { getUrlForApp } = application; render( - + diff --git a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts index 7e9aa7a0d422..e191c4bd799a 100644 --- a/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts +++ b/x-pack/plugins/index_lifecycle_management/public/shared_imports.ts @@ -43,4 +43,6 @@ export { attemptToURIDecode } from '../../../../src/plugins/es_ui_shared/public' export { KibanaContextProvider } from '../../../../src/plugins/kibana_react/public'; +export { APP_WRAPPER_CLASS } from '../../../../src/core/public'; + export const useKibana = () => _useKibana(); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx index a8ccb0f5119c..e7ace1aff310 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/use_state_listener.tsx @@ -94,17 +94,25 @@ export const useMappingsStateListener = ({ onChange, value }: Args) => { validate: async () => { const configurationFormValidator = state.configuration.submitForm !== undefined - ? new Promise(async (resolve) => { - const { isValid } = await state.configuration.submitForm!(); - resolve(isValid); + ? new Promise(async (resolve, reject) => { + try { + const { isValid } = await state.configuration.submitForm!(); + resolve(isValid); + } catch (error) { + reject(error); + } }) : Promise.resolve(true); const templatesFormValidator = state.templates.submitForm !== undefined - ? new Promise(async (resolve) => { - const { isValid } = await state.templates.submitForm!(); - resolve(isValid); + ? new Promise(async (resolve, reject) => { + try { + const { isValid } = await state.templates.submitForm!(); + resolve(isValid); + } catch (error) { + reject(error); + } }) : Promise.resolve(true); diff --git a/x-pack/plugins/lens/common/suffix_formatter/index.ts b/x-pack/plugins/lens/common/suffix_formatter/index.ts index 00ae005c38b1..4fa6457f0125 100644 --- a/x-pack/plugins/lens/common/suffix_formatter/index.ts +++ b/x-pack/plugins/lens/common/suffix_formatter/index.ts @@ -28,9 +28,11 @@ export const unitSuffixesLong: Record = { d: i18n.translate('xpack.lens.fieldFormats.longSuffix.d', { defaultMessage: 'per day' }), }; -export function getSuffixFormatter(formatFactory: FormatFactory): FieldFormatInstanceType { +export const suffixFormatterId = 'suffix'; + +export function getSuffixFormatter(getFormatFactory: () => FormatFactory): FieldFormatInstanceType { return class SuffixFormatter extends FieldFormat { - static id = 'suffix'; + static id = suffixFormatterId; static hidden = true; // Don't want this format to appear in index pattern editor static title = i18n.translate('xpack.lens.fieldFormats.suffix.title', { defaultMessage: 'Suffix', @@ -51,9 +53,10 @@ export function getSuffixFormatter(formatFactory: FormatFactory): FieldFormatIns const nestedFormatter = this.param('id'); const nestedParams = this.param('params'); - const formattedValue = formatFactory({ id: nestedFormatter, params: nestedParams }).convert( - val - ); + const formattedValue = getFormatFactory()({ + id: nestedFormatter, + params: nestedParams, + }).convert(val); // do not add suffixes to empty strings if (formattedValue === '') { diff --git a/x-pack/plugins/lens/common/suffix_formatter/suffix_formatter.test.ts b/x-pack/plugins/lens/common/suffix_formatter/suffix_formatter.test.ts index d08908ecde41..9ab76b73cbb6 100644 --- a/x-pack/plugins/lens/common/suffix_formatter/suffix_formatter.test.ts +++ b/x-pack/plugins/lens/common/suffix_formatter/suffix_formatter.test.ts @@ -12,7 +12,7 @@ describe('suffix formatter', () => { it('should call nested formatter and apply suffix', () => { const convertMock = jest.fn((x) => x); const formatFactory = jest.fn(() => ({ convert: convertMock })); - const SuffixFormatter = getSuffixFormatter((formatFactory as unknown) as FormatFactory); + const SuffixFormatter = getSuffixFormatter(() => (formatFactory as unknown) as FormatFactory); const nestedParams = { abc: 123 }; const formatterInstance = new SuffixFormatter({ unit: 'h', @@ -30,7 +30,7 @@ describe('suffix formatter', () => { it('should not add suffix to empty strings', () => { const convertMock = jest.fn((x) => ''); const formatFactory = jest.fn(() => ({ convert: convertMock })); - const SuffixFormatter = getSuffixFormatter((formatFactory as unknown) as FormatFactory); + const SuffixFormatter = getSuffixFormatter(() => (formatFactory as unknown) as FormatFactory); const nestedParams = { abc: 123 }; const formatterInstance = new SuffixFormatter({ unit: 'h', @@ -46,7 +46,7 @@ describe('suffix formatter', () => { it('should be a hidden formatter', () => { const convertMock = jest.fn((x) => ''); const formatFactory = jest.fn(() => ({ convert: convertMock })); - const SuffixFormatter = getSuffixFormatter((formatFactory as unknown) as FormatFactory); + const SuffixFormatter = getSuffixFormatter(() => (formatFactory as unknown) as FormatFactory); expect(SuffixFormatter.hidden).toBe(true); }); }); diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index 9565bf57a315..2ec7a1962da8 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -41,8 +41,8 @@ "fieldFormats" ], "owner": { - "name": "Kibana App", - "githubTeam": "kibana-app" + "name": "Vis Editors", + "githubTeam": "kibana-vis-editors" }, "description": "Visualization editor allowing to quickly and easily configure compelling visualizations to use on dashboards and canvas workpads. Exposes components to embed visualizations and link into the Lens editor from within other apps in Kibana." } diff --git a/x-pack/plugins/lens/public/app_plugin/app.test.tsx b/x-pack/plugins/lens/public/app_plugin/app.test.tsx index 22da62c616c4..8cb4a7c4c843 100644 --- a/x-pack/plugins/lens/public/app_plugin/app.test.tsx +++ b/x-pack/plugins/lens/public/app_plugin/app.test.tsx @@ -23,10 +23,10 @@ import { createMemoryHistory } from 'history'; import { esFilters, FilterManager, - IFieldType, IndexPattern, Query, } from '../../../../../src/plugins/data/public'; +import type { FieldSpec } from '../../../../../src/plugins/data/common'; import { TopNavMenuData } from '../../../../../src/plugins/navigation/public'; import { LensByValueInput } from '../embeddable/embeddable'; import { SavedObjectReference } from '../../../../../src/core/types'; @@ -146,7 +146,7 @@ describe('Lens App', () => { it('updates global filters with store state', async () => { const services = makeDefaultServices(sessionIdSubject); const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; - const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; + const pinnedField = ({ name: 'pinnedField' } as unknown) as FieldSpec; const pinnedFilter = esFilters.buildExistsFilter(pinnedField, indexPattern); services.data.query.filterManager.getFilters = jest.fn().mockImplementation(() => { return []; @@ -644,8 +644,8 @@ describe('Lens App', () => { it('saves app filters and does not save pinned filters', async () => { const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; - const field = ({ name: 'myfield' } as unknown) as IFieldType; - const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; + const field = ({ name: 'myfield' } as unknown) as FieldSpec; + const pinnedField = ({ name: 'pinnedField' } as unknown) as FieldSpec; const unpinned = esFilters.buildExistsFilter(field, indexPattern); const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); await act(async () => { @@ -857,7 +857,7 @@ describe('Lens App', () => { it('updates the filters when the user changes them', async () => { const { instance, services, lensStore } = await mountWith({}); const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; - const field = ({ name: 'myfield' } as unknown) as IFieldType; + const field = ({ name: 'myfield' } as unknown) as FieldSpec; expect(lensStore.getState()).toEqual({ lens: expect.objectContaining({ filters: [], @@ -912,7 +912,7 @@ describe('Lens App', () => { }), }); const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; - const field = ({ name: 'myfield' } as unknown) as IFieldType; + const field = ({ name: 'myfield' } as unknown) as FieldSpec; act(() => services.data.query.filterManager.setFilters([ esFilters.buildExistsFilter(field, indexPattern), @@ -1047,8 +1047,8 @@ describe('Lens App', () => { }) ); const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; - const field = ({ name: 'myfield' } as unknown) as IFieldType; - const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; + const field = ({ name: 'myfield' } as unknown) as FieldSpec; + const pinnedField = ({ name: 'pinnedField' } as unknown) as FieldSpec; const unpinned = esFilters.buildExistsFilter(field, indexPattern); const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); @@ -1104,8 +1104,8 @@ describe('Lens App', () => { }) ); const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; - const field = ({ name: 'myfield' } as unknown) as IFieldType; - const pinnedField = ({ name: 'pinnedField' } as unknown) as IFieldType; + const field = ({ name: 'myfield' } as unknown) as FieldSpec; + const pinnedField = ({ name: 'pinnedField' } as unknown) as FieldSpec; const unpinned = esFilters.buildExistsFilter(field, indexPattern); const pinned = esFilters.buildExistsFilter(pinnedField, indexPattern); FilterManager.setFiltersStore([pinned], esFilters.FilterStateStore.GLOBAL_STATE); diff --git a/x-pack/plugins/lens/public/app_plugin/mounter.tsx b/x-pack/plugins/lens/public/app_plugin/mounter.tsx index 15f72bed582e..1ac8833f0f51 100644 --- a/x-pack/plugins/lens/public/app_plugin/mounter.tsx +++ b/x-pack/plugins/lens/public/app_plugin/mounter.tsx @@ -37,6 +37,7 @@ import { navigateAway, LensRootStore, loadInitial, + LensAppState, LensState, } from '../state_management'; import { getPreloadedState } from '../state_management/lens_slice'; @@ -186,8 +187,9 @@ export async function mountApp( embeddableEditorIncomingState, initialContext, }; + const emptyState = getPreloadedState(storeDeps) as LensAppState; const lensStore: LensRootStore = makeConfigureStore(storeDeps, { - lens: getPreloadedState(storeDeps), + lens: emptyState, } as DeepPartial); const EditorRenderer = React.memo( @@ -200,7 +202,8 @@ export async function mountApp( ); trackUiEvent('loaded'); const initialInput = getInitialInput(props.id, props.editByValue); - lensStore.dispatch(loadInitial({ redirectCallback, initialInput })); + + lensStore.dispatch(loadInitial({ redirectCallback, initialInput, emptyState })); return ( diff --git a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx index 45d3f959274f..0f99902e0b10 100644 --- a/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx +++ b/x-pack/plugins/lens/public/app_plugin/save_modal_container.tsx @@ -10,18 +10,18 @@ import { i18n } from '@kbn/i18n'; import { METRIC_TYPE } from '@kbn/analytics'; import { partition } from 'lodash'; -import type { ChromeStart, NotificationsStart, SavedObjectReference } from 'kibana/public'; +import type { SavedObjectReference } from 'kibana/public'; import { SaveModal } from './save_modal'; import type { LensAppProps, LensAppServices } from './types'; import type { SaveProps } from './app'; import { Document, injectFilterReferences } from '../persistence'; import type { LensByReferenceInput, LensEmbeddableInput } from '../embeddable'; -import type { LensAttributeService } from '../lens_attribute_service'; -import { DataPublicPluginStart, esFilters } from '../../../../../src/plugins/data/public'; +import { esFilters } from '../../../../../src/plugins/data/public'; import { APP_ID, getFullPath, LENS_EMBEDDABLE_TYPE } from '../../common'; import { trackUiEvent } from '../lens_ui_telemetry'; import { checkForDuplicateTitle } from '../../../../../src/plugins/saved_objects/public'; import type { LensAppState } from '../state_management'; +import { getPersisted } from '../state_management/init_middleware/load_initial'; type ExtraProps = Pick & Partial>; @@ -51,54 +51,41 @@ export function SaveModalContainer({ redirectToOrigin, getAppNameFromId = () => undefined, isSaveable = true, - lastKnownDoc: initLastKnowDoc, + lastKnownDoc: initLastKnownDoc, lensServices, }: SaveModalContainerProps) { - const [lastKnownDoc, setLastKnownDoc] = useState(initLastKnowDoc); let title = ''; let description; let savedObjectId; + const [lastKnownDoc, setLastKnownDoc] = useState(initLastKnownDoc); if (lastKnownDoc) { title = lastKnownDoc.title; description = lastKnownDoc.description; savedObjectId = lastKnownDoc.savedObjectId; } - const { - attributeService, - notifications, - data, - chrome, - savedObjectsTagging, - application, - dashboardFeatureFlag, - } = lensServices; + const { attributeService, savedObjectsTagging, application, dashboardFeatureFlag } = lensServices; useEffect(() => { - setLastKnownDoc(initLastKnowDoc); - }, [initLastKnowDoc]); + setLastKnownDoc(initLastKnownDoc); + }, [initLastKnownDoc]); useEffect(() => { let isMounted = true; - async function loadPersistedDoc() { - if (initialInput) { - getPersistedDoc({ - data, - initialInput, - chrome, - notifications, - attributeService, - }).then((doc) => { - if (doc && isMounted) setLastKnownDoc(doc); - }); - } + + if (initialInput) { + getPersisted({ + initialInput, + lensServices, + }).then((persisted) => { + if (persisted?.doc && isMounted) setLastKnownDoc(persisted.doc); + }); } - loadPersistedDoc(); return () => { isMounted = false; }; - }, [chrome, data, initialInput, notifications, attributeService]); + }, [initialInput, lensServices]); const tagsIds = persistedDoc && savedObjectsTagging @@ -109,27 +96,25 @@ export function SaveModalContainer({ if (runSave) { // inside lens, we use the function that's passed to it runSave(saveProps, options); - } else { - if (attributeService && lastKnownDoc) { - runSaveLensVisualization( - { - ...lensServices, - lastKnownDoc, - initialInput, - attributeService, - redirectTo, - redirectToOrigin, - originatingApp, - getIsByValueMode: () => false, - onAppLeave: () => {}, - }, - saveProps, - options - ).then(() => { - onSave?.(); - onClose(); - }); - } + } else if (attributeService && lastKnownDoc) { + runSaveLensVisualization( + { + ...lensServices, + lastKnownDoc, + initialInput, + attributeService, + redirectTo, + redirectToOrigin, + originatingApp, + getIsByValueMode: () => false, + onAppLeave: () => {}, + }, + saveProps, + options + ).then(() => { + onSave?.(); + onClose(); + }); } }; @@ -384,51 +369,5 @@ export function getLastKnownDocWithoutPinnedFilters(doc?: Document) { : doc; } -export const getPersistedDoc = async ({ - initialInput, - attributeService, - data, - notifications, - chrome, -}: { - initialInput: LensEmbeddableInput; - attributeService: LensAttributeService; - data: DataPublicPluginStart; - notifications: NotificationsStart; - chrome: ChromeStart; -}): Promise => { - let doc: Document; - - try { - const attributes = await attributeService.unwrapAttributes(initialInput); - - doc = { - ...initialInput, - ...attributes, - type: LENS_EMBEDDABLE_TYPE, - }; - - if (attributeService.inputIsRefType(initialInput)) { - chrome.recentlyAccessed.add( - getFullPath(initialInput.savedObjectId), - attributes.title, - initialInput.savedObjectId - ); - } - - // Don't overwrite any pinned filters - data.query.filterManager.setAppFilters( - injectFilterReferences(doc.state.filters, doc.references) - ); - return doc; - } catch (e) { - notifications.toasts.addDanger( - i18n.translate('xpack.lens.app.docLoadingError', { - defaultMessage: 'Error loading saved document', - }) - ); - } -}; - // eslint-disable-next-line import/no-default-export export default SaveModalContainer; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx index fff9fe1372a2..7c5fd4f5b884 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.test.tsx @@ -230,7 +230,7 @@ describe('editor_frame', () => { await mountWithProvider(, { data: props.plugins.data, preloadedState: { - visualization: { activeId: 'testVis', state: null }, + visualization: { activeId: 'testVis', state: {} }, datasourceStates: { testDatasource: { isLoading: false, @@ -285,7 +285,7 @@ describe('editor_frame', () => { await mountWithProvider(, { data: props.plugins.data, preloadedState: { - visualization: { activeId: 'testVis', state: null }, + visualization: { activeId: 'testVis', state: {} }, datasourceStates: { testDatasource: { isLoading: false, diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx index 7700bc708fc1..3b55c4923f96 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/editor_frame.tsx @@ -45,7 +45,8 @@ export function EditorFrame(props: EditorFrameProps) { const activeDatasourceId = useLensSelector(selectActiveDatasourceId); const datasourceStates = useLensSelector(selectDatasourceStates); const visualization = useLensSelector(selectVisualization); - const allLoaded = useLensSelector(selectAreDatasourcesLoaded); + const areDatasourcesLoaded = useLensSelector(selectAreDatasourcesLoaded); + const isVisualizationLoaded = !!visualization.state; const framePublicAPI: FramePublicAPI = useLensSelector((state) => selectFramePublicAPI(state, datasourceMap) ); @@ -95,7 +96,7 @@ export function EditorFrame(props: EditorFrameProps) { /> } configPanel={ - allLoaded && ( + areDatasourcesLoaded && ( { expect(expressionRendererMock).toHaveBeenCalledTimes(1); const indexPattern = ({ id: 'index1' } as unknown) as IndexPattern; - const field = ({ name: 'myfield' } as unknown) as IFieldType; + const field = ({ name: 'myfield' } as unknown) as FieldSpec; await act(async () => { instance.setProps({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx index 59aebc517bf2..a6828bf9d587 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/datapanel.test.tsx @@ -664,7 +664,14 @@ describe('IndexPattern Data Panel', () => { ...props.indexPatterns['1'], fields: [ ...props.indexPatterns['1'].fields, - { name: '_id', displayName: '_id', meta: true, type: 'string' }, + { + name: '_id', + displayName: '_id', + meta: true, + type: 'string', + searchable: true, + aggregatable: true, + }, ], }, }} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts index 3c00241cd7ed..9ff80f51bea9 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/index.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/index.ts @@ -6,7 +6,7 @@ */ import type { CoreSetup } from 'kibana/public'; -import { Storage } from '../../../../../src/plugins/kibana_utils/public'; +import { createStartServicesGetter, Storage } from '../../../../../src/plugins/kibana_utils/public'; import type { ExpressionsSetup } from '../../../../../src/plugins/expressions/public'; import type { ChartsPluginSetup } from '../../../../../src/plugins/charts/public'; import type { IndexPatternFieldEditorStart } from '../../../../../src/plugins/index_pattern_field_editor/public'; @@ -14,7 +14,7 @@ import type { DataPublicPluginSetup, DataPublicPluginStart, } from '../../../../../src/plugins/data/public'; -import type { Datasource, EditorFrameSetup } from '../types'; +import type { EditorFrameSetup } from '../types'; import type { UiActionsStart } from '../../../../../src/plugins/ui_actions/public'; import type { FieldFormatsStart, @@ -57,29 +57,37 @@ export class IndexPatternDatasource { counterRate, getTimeScale, getSuffixFormatter, + suffixFormatterId, } = await import('../async_services'); - return core - .getStartServices() - .then(([coreStart, { indexPatternFieldEditor, uiActions, data, fieldFormats }]) => { - const suffixFormatter = getSuffixFormatter(fieldFormats.deserialize); - if (!fieldFormats.has(suffixFormatter.id)) { - // todo: this code should be executed on setup phase. - fieldFormatsSetup.register([suffixFormatter]); - } - expressions.registerFunction(getTimeScale(() => getTimeZone(core.uiSettings))); - expressions.registerFunction(counterRate); - expressions.registerFunction(renameColumns); - expressions.registerFunction(formatColumn); - return getIndexPatternDatasource({ - core: coreStart, - fieldFormats, - storage: new Storage(localStorage), - data, - charts, - indexPatternFieldEditor, - uiActions, - }); - }) as Promise; + + if (!fieldFormatsSetup.has(suffixFormatterId)) { + const startServices = createStartServicesGetter(core.getStartServices); + const suffixFormatter = getSuffixFormatter( + () => startServices().plugins.fieldFormats.deserialize + ); + + fieldFormatsSetup.register([suffixFormatter]); + } + + expressions.registerFunction(getTimeScale(() => getTimeZone(core.uiSettings))); + expressions.registerFunction(counterRate); + expressions.registerFunction(renameColumns); + expressions.registerFunction(formatColumn); + + const [ + coreStart, + { indexPatternFieldEditor, uiActions, data, fieldFormats }, + ] = await core.getStartServices(); + + return getIndexPatternDatasource({ + core: coreStart, + fieldFormats, + storage: new Storage(localStorage), + data, + charts, + indexPatternFieldEditor, + uiActions, + }); }); } } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index fffbf0cba34d..8f66bcf7fe49 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -76,7 +76,11 @@ export { counterRate, } from '../../common/expressions'; export { FormatColumnArgs, supportedFormats, formatColumn } from '../../common/expressions'; -export { getSuffixFormatter, unitSuffixesLong } from '../../common/suffix_formatter'; +export { + getSuffixFormatter, + unitSuffixesLong, + suffixFormatterId, +} from '../../common/suffix_formatter'; export { getTimeScale, TimeScaleArgs } from '../../common/expressions'; export { renameColumns } from '../../common/expressions'; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx index 118405baebc8..c4a88617c24b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/percentile.test.tsx @@ -81,6 +81,7 @@ describe('percentile', () => { displayName: 'bytes', type: 'number', esTypes: ['long'], + searchable: true, aggregatable: true, }) ).toEqual({ @@ -97,6 +98,7 @@ describe('percentile', () => { displayName: 'response_time', type: 'histogram', esTypes: ['histogram'], + searchable: true, aggregatable: true, }) ).toEqual({ @@ -113,6 +115,7 @@ describe('percentile', () => { displayName: 'origin', type: 'string', esTypes: ['keyword'], + searchable: true, aggregatable: true, }) ).toBeUndefined(); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx index 92565d1590ea..aef086a6ee28 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/ranges/ranges.test.tsx @@ -96,9 +96,23 @@ const defaultOptions = { id: '1', title: 'my_index_pattern', hasRestrictions: false, - fields: [{ name: sourceField, type: 'number', displayName: sourceField }], + fields: [ + { + name: sourceField, + type: 'number', + displayName: sourceField, + searchable: true, + aggregatable: true, + }, + ], getFieldByName: getFieldByNameFactory([ - { name: sourceField, type: 'number', displayName: sourceField }, + { + name: sourceField, + type: 'number', + displayName: sourceField, + searchable: true, + aggregatable: true, + }, ]), }, operationDefinitionMap: {}, diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx index cfe190261b53..6489a43cf2f8 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/index.tsx @@ -348,27 +348,19 @@ export const termsOperation: OperationDefinition - - { - updateLayer( - updateColumnParam({ - layer, - columnId, - paramName: 'size', - value, - }) - ); - }} - /> - + { + updateLayer( + updateColumnParam({ + layer, + columnId, + paramName: 'size', + value, + }) + ); + }} + /> diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.test.tsx index 4303695d6e29..ac7397fb582a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { shallow } from 'enzyme'; -import { EuiFieldNumber } from '@elastic/eui'; +import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; import { ValuesInput } from './values_input'; jest.mock('react-use/lib/useDebounce', () => (fn: () => void) => fn()); @@ -41,7 +41,7 @@ describe('Values', () => { expect(onChangeSpy.mock.calls[0][0]).toBe(7); }); - it('should not run onChange function on update when value is out of 1-100 range', () => { + it('should not run onChange function on update when value is out of 1-1000 range', () => { const onChangeSpy = jest.fn(); const instance = shallow(); act(() => { @@ -54,4 +54,56 @@ describe('Values', () => { expect(onChangeSpy.mock.calls.length).toBe(1); expect(onChangeSpy.mock.calls[0][0]).toBe(1000); }); + + it('should show an error message when the value is out of bounds', () => { + const instance = shallow(); + + expect(instance.find(EuiFieldNumber).prop('isInvalid')).toBeTruthy(); + expect(instance.find(EuiFormRow).prop('error')).toEqual( + expect.arrayContaining([expect.stringMatching('Value is lower')]) + ); + + act(() => { + instance.find(EuiFieldNumber).prop('onChange')!({ + currentTarget: { value: '1007' }, + } as React.ChangeEvent); + }); + instance.update(); + + expect(instance.find(EuiFieldNumber).prop('isInvalid')).toBeTruthy(); + expect(instance.find(EuiFormRow).prop('error')).toEqual( + expect.arrayContaining([expect.stringMatching('Value is higher')]) + ); + }); + + it('should fallback to last valid value on input blur', () => { + const instance = shallow(); + + function changeAndBlur(newValue: string) { + act(() => { + instance.find(EuiFieldNumber).prop('onChange')!({ + currentTarget: { value: newValue }, + } as React.ChangeEvent); + }); + instance.update(); + act(() => { + instance.find(EuiFieldNumber).prop('onBlur')!({} as React.FocusEvent); + }); + instance.update(); + } + + changeAndBlur('-5'); + + expect(instance.find(EuiFieldNumber).prop('isInvalid')).toBeFalsy(); + expect(instance.find(EuiFieldNumber).prop('value')).toBe('1'); + + changeAndBlur('5000'); + + expect(instance.find(EuiFieldNumber).prop('isInvalid')).toBeFalsy(); + expect(instance.find(EuiFieldNumber).prop('value')).toBe('1000'); + + changeAndBlur(''); + // as we're not handling the onChange state, it fallbacks to the value prop + expect(instance.find(EuiFieldNumber).prop('value')).toBe('123'); + }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.tsx index a4c0f8f1c50e..96b92686f762 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/terms/values_input.tsx @@ -7,7 +7,7 @@ import React, { useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFieldNumber } from '@elastic/eui'; +import { EuiFieldNumber, EuiFormRow } from '@elastic/eui'; import { useDebounceWithOptions } from '../../../../shared_components'; export const ValuesInput = ({ @@ -35,17 +35,63 @@ export const ValuesInput = ({ [inputValue] ); + const isEmptyString = inputValue === ''; + const isHigherThanMax = !isEmptyString && Number(inputValue) > MAX_NUMBER_OF_VALUES; + const isLowerThanMin = !isEmptyString && Number(inputValue) < MIN_NUMBER_OF_VALUES; + return ( - setInputValue(currentTarget.value)} - aria-label={i18n.translate('xpack.lens.indexPattern.terms.size', { + + display="columnCompressed" + fullWidth + isInvalid={isHigherThanMax || isLowerThanMin} + error={ + isHigherThanMax + ? [ + i18n.translate('xpack.lens.indexPattern.terms.sizeLimitMax', { + defaultMessage: + 'Value is higher than the maximum {max}, the maximum value is used instead.', + values: { + max: MAX_NUMBER_OF_VALUES, + }, + }), + ] + : isLowerThanMin + ? [ + i18n.translate('xpack.lens.indexPattern.terms.sizeLimitMin', { + defaultMessage: + 'Value is lower than the minimum {min}, the minimum value is used instead.', + values: { + min: MIN_NUMBER_OF_VALUES, + }, + }), + ] + : null + } + > + setInputValue(currentTarget.value)} + aria-label={i18n.translate('xpack.lens.indexPattern.terms.size', { + defaultMessage: 'Number of values', + })} + onBlur={() => { + if (inputValue === '') { + return setInputValue(String(value)); + } + const inputNumber = Number(inputValue); + setInputValue( + String(Math.min(MAX_NUMBER_OF_VALUES, Math.max(inputNumber, MIN_NUMBER_OF_VALUES))) + ); + }} + /> + ); }; diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts index 1a3451bdb403..72acc114ca4b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/types.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/types.ts @@ -5,10 +5,10 @@ * 2.0. */ -import { IFieldType } from 'src/plugins/data/common'; -import { IndexPatternColumn, IncompleteColumn } from './operations'; -import { IndexPatternAggRestrictions } from '../../../../../src/plugins/data/public'; -import { DragDropIdentifier } from '../drag_drop/providers'; +import type { IndexPatternColumn, IncompleteColumn } from './operations'; +import type { IndexPatternAggRestrictions } from '../../../../../src/plugins/data/public'; +import type { FieldSpec } from '../../../../../src/plugins/data/common'; +import type { DragDropIdentifier } from '../drag_drop/providers'; export { FieldBasedIndexPatternColumn, @@ -57,7 +57,7 @@ export interface IndexPattern { hasRestrictions: boolean; } -export type IndexPatternField = IFieldType & { +export type IndexPatternField = FieldSpec & { displayName: string; aggregationRestrictions?: Partial; meta?: boolean; diff --git a/x-pack/plugins/lens/public/mocks.tsx b/x-pack/plugins/lens/public/mocks.tsx index a88831dda7ba..b2c8d3948b28 100644 --- a/x-pack/plugins/lens/public/mocks.tsx +++ b/x-pack/plugins/lens/public/mocks.tsx @@ -208,12 +208,14 @@ export const defaultDoc = ({ savedObjectId: '1234', title: 'An extremely cool default document!', expression: 'definitely a valid expression', + visualizationType: 'testVis', state: { query: 'kuery', filters: [{ query: { match_phrase: { src: 'test' } } }], datasourceStates: { testDatasource: 'datasource', }, + visualization: {}, }, references: [{ type: 'index-pattern', id: '1', name: 'index-pattern-0' }], } as unknown) as Document; diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/index.ts b/x-pack/plugins/lens/public/state_management/init_middleware/index.ts index 854e08dfe83e..bf13ca69e82c 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/index.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/index.ts @@ -23,7 +23,8 @@ export const initMiddleware = (storeDeps: LensStoreDeps) => (store: MiddlewareAP store, storeDeps, action.payload.redirectCallback, - action.payload.initialInput + action.payload.initialInput, + action.payload.emptyState ); } else if (lensSlice.actions.navigateAway.match(action)) { return unsubscribeFromExternalContext(); diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx index 9f653c2ff9f8..79402b698af9 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.test.tsx @@ -15,6 +15,8 @@ import { import { act } from 'react-dom/test-utils'; import { loadInitial } from './load_initial'; import { LensEmbeddableInput } from '../../embeddable'; +import { getPreloadedState } from '../lens_slice'; +import { LensAppState } from '..'; const defaultSavedObjectId = '1234'; const preloadedState = { @@ -63,7 +65,6 @@ describe('Mounter', () => { it('should initialize initial datasource', async () => { const services = makeDefaultServices(); - const redirectCallback = jest.fn(); services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc); const lensStore = await makeLensStore({ @@ -78,7 +79,7 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - redirectCallback, + jest.fn(), ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput ); }); @@ -87,7 +88,6 @@ describe('Mounter', () => { it('should have initialized only the initial datasource and visualization', async () => { const services = makeDefaultServices(); - const redirectCallback = jest.fn(); services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc); const lensStore = await makeLensStore({ data: services.data, preloadedState }); @@ -99,7 +99,7 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - redirectCallback + jest.fn() ); }); expect(mockDatasource.initialize).toHaveBeenCalled(); @@ -121,7 +121,6 @@ describe('Mounter', () => { describe('loadInitial', () => { it('does not load a document if there is no initial input', async () => { const services = makeDefaultServices(); - const redirectCallback = jest.fn(); const lensStore = makeLensStore({ data: services.data, preloadedState }); await loadInitial( lensStore, @@ -130,14 +129,66 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - redirectCallback + jest.fn() ); expect(services.attributeService.unwrapAttributes).not.toHaveBeenCalled(); }); + it('cleans datasource and visualization state properly when reloading', async () => { + const services = makeDefaultServices(); + const storeDeps = { + lensServices: services, + datasourceMap, + visualizationMap, + }; + services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc); + const lensStore = await makeLensStore({ + data: services.data, + preloadedState: { + ...preloadedState, + visualization: { + activeId: 'testVis', + state: {}, + }, + datasourceStates: { testDatasource: { isLoading: false, state: {} } }, + }, + }); + + expect(lensStore.getState()).toEqual({ + lens: expect.objectContaining({ + visualization: { + activeId: 'testVis', + state: {}, + }, + activeDatasourceId: 'testDatasource', + datasourceStates: { + testDatasource: { isLoading: false, state: {} }, + }, + }), + }); + + const emptyState = getPreloadedState(storeDeps) as LensAppState; + services.attributeService.unwrapAttributes = jest.fn(); + await act(async () => { + await loadInitial(lensStore, storeDeps, jest.fn(), undefined, emptyState); + }); + + expect(lensStore.getState()).toEqual({ + lens: expect.objectContaining({ + visualization: { + activeId: 'testVis', + state: null, // resets to null + }, + activeDatasourceId: 'testDatasource2', // resets to first on the list + datasourceStates: { + testDatasource: { isLoading: false, state: undefined }, // state resets to undefined + }, + }), + }); + }); + it('loads a document and uses query and filters if initial input is provided', async () => { const services = makeDefaultServices(); - const redirectCallback = jest.fn(); services.attributeService.unwrapAttributes = jest.fn().mockResolvedValue(defaultDoc); const lensStore = await makeLensStore({ data: services.data, preloadedState }); @@ -149,7 +200,7 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - redirectCallback, + jest.fn(), ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput ); }); @@ -173,7 +224,6 @@ describe('Mounter', () => { }); it('does not load documents on sequential renders unless the id changes', async () => { - const redirectCallback = jest.fn(); const services = makeDefaultServices(); const lensStore = makeLensStore({ data: services.data, preloadedState }); @@ -185,7 +235,7 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - redirectCallback, + jest.fn(), ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput ); }); @@ -198,7 +248,7 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - redirectCallback, + jest.fn(), ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput ); }); @@ -213,7 +263,7 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - redirectCallback, + jest.fn(), ({ savedObjectId: '5678' } as unknown) as LensEmbeddableInput ); }); @@ -249,8 +299,6 @@ describe('Mounter', () => { }); it('adds to the recently accessed list on load', async () => { - const redirectCallback = jest.fn(); - const services = makeDefaultServices(); const lensStore = makeLensStore({ data: services.data, preloadedState }); await act(async () => { @@ -261,7 +309,7 @@ describe('Mounter', () => { datasourceMap, visualizationMap, }, - redirectCallback, + jest.fn(), ({ savedObjectId: defaultSavedObjectId } as unknown) as LensEmbeddableInput ); }); diff --git a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts index 5aeeec81e29b..0be2bc9cfc00 100644 --- a/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts +++ b/x-pack/plugins/lens/public/state_management/init_middleware/load_initial.ts @@ -7,7 +7,8 @@ import { MiddlewareAPI } from '@reduxjs/toolkit'; import { isEqual } from 'lodash'; -import { setState } from '..'; +import { i18n } from '@kbn/i18n'; +import { LensAppState, setState } from '..'; import { updateLayer, updateVisualizationState, LensStoreDeps } from '..'; import { LensEmbeddableInput, LensByReferenceInput } from '../../embeddable/embeddable'; import { getInitialDatasourceId } from '../../utils'; @@ -17,7 +18,40 @@ import { getVisualizeFieldSuggestions, switchToSuggestion, } from '../../editor_frame_service/editor_frame/suggestion_helpers'; -import { getPersistedDoc } from '../../app_plugin/save_modal_container'; +import { LensAppServices } from '../../app_plugin/types'; +import { getFullPath, LENS_EMBEDDABLE_TYPE } from '../../../common/constants'; +import { Document, injectFilterReferences } from '../../persistence'; + +export const getPersisted = async ({ + initialInput, + lensServices, +}: { + initialInput: LensEmbeddableInput; + lensServices: LensAppServices; +}): Promise<{ doc: Document } | undefined> => { + const { notifications, attributeService } = lensServices; + let doc: Document; + + try { + const attributes = await attributeService.unwrapAttributes(initialInput); + + doc = { + ...initialInput, + ...attributes, + type: LENS_EMBEDDABLE_TYPE, + }; + + return { + doc, + }; + } catch (e) { + notifications.toasts.addDanger( + i18n.translate('xpack.lens.app.docLoadingError', { + defaultMessage: 'Error loading saved document', + }) + ); + } +}; export function loadInitial( store: MiddlewareAPI, @@ -29,11 +63,13 @@ export function loadInitial( initialContext, }: LensStoreDeps, redirectCallback: (savedObjectId?: string) => void, - initialInput?: LensEmbeddableInput + initialInput?: LensEmbeddableInput, + emptyState?: LensAppState ) { const { getState, dispatch } = store; - const { attributeService, chrome, notifications, data, dashboardFeatureFlag } = lensServices; + const { attributeService, notifications, data, dashboardFeatureFlag } = lensServices; const { persistedDoc } = getState().lens; + if ( !initialInput || (attributeService.inputIsRefType(initialInput) && @@ -61,6 +97,7 @@ export function loadInitial( ); dispatch( setState({ + ...emptyState, datasourceStates, isLoading: false, }) @@ -109,17 +146,23 @@ export function loadInitial( redirectCallback(); }); } - getPersistedDoc({ - initialInput, - attributeService, - data, - chrome, - notifications, - }) + getPersisted({ initialInput, lensServices }) .then( - (doc) => { - if (doc) { - const currentSessionId = data.search.session.getSessionId(); + (persisted) => { + if (persisted) { + const { doc } = persisted; + if (attributeService.inputIsRefType(initialInput)) { + lensServices.chrome.recentlyAccessed.add( + getFullPath(initialInput.savedObjectId), + doc.title, + initialInput.savedObjectId + ); + } + // Don't overwrite any pinned filters + data.query.filterManager.setAppFilters( + injectFilterReferences(doc.state.filters, doc.references) + ); + const docDatasourceStates = Object.entries(doc.state.datasourceStates).reduce( (stateMap, [datasourceId, datasourceState]) => ({ ...stateMap, @@ -143,6 +186,8 @@ export function loadInitial( .then((result) => { const activeDatasourceId = getInitialDatasourceId(datasourceMap, doc); + const currentSessionId = data.search.session.getSessionId(); + dispatch( setState({ query: doc.state.query, diff --git a/x-pack/plugins/lens/public/state_management/lens_slice.ts b/x-pack/plugins/lens/public/state_management/lens_slice.ts index 21624ae99cef..85cb79f6ea5d 100644 --- a/x-pack/plugins/lens/public/state_management/lens_slice.ts +++ b/x-pack/plugins/lens/public/state_management/lens_slice.ts @@ -12,6 +12,7 @@ import { getInitialDatasourceId, getResolvedDateRange } from '../utils'; import { LensAppState, LensStoreDeps } from './types'; export const initialState: LensAppState = { + persistedDoc: undefined, searchSessionId: '', filters: [], query: { language: 'kuery', query: '' }, @@ -299,6 +300,7 @@ export const lensSlice = createSlice({ payload: PayloadAction<{ initialInput?: LensEmbeddableInput; redirectCallback: (savedObjectId?: string) => void; + emptyState: LensAppState; }> ) => state, }, diff --git a/x-pack/plugins/maps/common/constants.ts b/x-pack/plugins/maps/common/constants.ts index 1a3065cb4518..5cfed7d6a58b 100644 --- a/x-pack/plugins/maps/common/constants.ts +++ b/x-pack/plugins/maps/common/constants.ts @@ -92,7 +92,6 @@ export enum SOURCE_TYPES { EMS_XYZ = 'EMS_XYZ', // identifies a custom TMS source. EMS-prefix in the name is a little unfortunate :( WMS = 'WMS', KIBANA_TILEMAP = 'KIBANA_TILEMAP', - REGIONMAP_FILE = 'REGIONMAP_FILE', GEOJSON_FILE = 'GEOJSON_FILE', MVT_SINGLE_LAYER = 'MVT_SINGLE_LAYER', TABLE_SOURCE = 'TABLE_SOURCE', @@ -131,7 +130,7 @@ export enum ES_GEO_FIELD_TYPE { GEO_SHAPE = 'geo_shape', } -// Using strings instead of ES_GEO_FIELD_TYPE enum to avoid typeing errors where IFieldType.type is compared to value +// Using strings instead of ES_GEO_FIELD_TYPE enum to avoid typeing errors where IndexPatternField.type is compared to value export const ES_GEO_FIELD_TYPES = ['geo_point', 'geo_shape']; export enum ES_SPATIAL_RELATIONS { diff --git a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts index 06afe34bb73f..9a2af711ea2c 100644 --- a/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts +++ b/x-pack/plugins/maps/common/descriptor_types/source_descriptor_types.ts @@ -112,10 +112,6 @@ export type ESTermSourceDescriptor = AbstractESAggSourceDescriptor & { type: SOURCE_TYPES.ES_TERM_SOURCE; }; -export type KibanaRegionmapSourceDescriptor = AbstractSourceDescriptor & { - name: string; -}; - // This is for symmetry with other sources only. // It takes no additional configuration since all params are in the .yml. export type KibanaTilemapSourceDescriptor = AbstractSourceDescriptor; diff --git a/x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts b/x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts index cae5f6ee9f41..55600ca307ff 100644 --- a/x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts +++ b/x-pack/plugins/maps/common/elasticsearch_util/es_agg_utils.ts @@ -7,13 +7,13 @@ import { i18n } from '@kbn/i18n'; import _ from 'lodash'; -import { IndexPattern, IFieldType } from '../../../../../src/plugins/data/common'; +import type { IndexPattern, IndexPatternField } from 'src/plugins/data/common'; import { AGG_TYPE, JOIN_FIELD_NAME_PREFIX, TOP_TERM_PERCENTAGE_SUFFIX } from '../constants'; export type BucketProperties = Record; export type PropertiesMap = Map; -export function getField(indexPattern: IndexPattern, fieldName: string): IFieldType { +export function getField(indexPattern: IndexPattern, fieldName: string): IndexPatternField { const field = indexPattern.fields.getByName(fieldName); if (!field) { throw new Error( @@ -26,7 +26,7 @@ export function getField(indexPattern: IndexPattern, fieldName: string): IFieldT return field; } -export function addFieldToDSL(dsl: object, field: IFieldType) { +export function addFieldToDSL(dsl: object, field: IndexPatternField) { return !field.scripted ? { ...dsl, field: field.name } : { diff --git a/x-pack/plugins/maps/public/actions/map_actions.test.js b/x-pack/plugins/maps/public/actions/map_actions.test.ts similarity index 68% rename from x-pack/plugins/maps/public/actions/map_actions.test.js rename to x-pack/plugins/maps/public/actions/map_actions.test.ts index 763ce459dcc2..d222d8e5b046 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.test.js +++ b/x-pack/plugins/maps/public/actions/map_actions.test.ts @@ -5,6 +5,8 @@ * 2.0. */ +/* eslint @typescript-eslint/no-var-requires: 0 */ + jest.mock('../selectors/map_selectors', () => ({})); jest.mock('./data_request_actions', () => { return { @@ -30,7 +32,7 @@ describe('map_actions', () => { }); describe('mapExtentChanged', () => { - describe('store mapState is empty', () => { + describe('mapState.buffer is undefined', () => { beforeEach(() => { require('../selectors/map_selectors').getDataFilters = () => { return { @@ -43,20 +45,12 @@ describe('map_actions', () => { }; }); - it('should add newMapConstants to dispatch action mapState', async () => { - const action = mapExtentChanged({ zoom: 5 }); - await action(dispatchMock, getStoreMock); - - expect(dispatchMock).toHaveBeenCalledWith({ - mapState: { - zoom: 5, - }, - type: 'MAP_EXTENT_CHANGED', - }); - }); - - it('should add buffer to dispatch action mapState', async () => { + it('should set buffer', () => { const action = mapExtentChanged({ + center: { + lat: 7.5, + lon: 97.5, + }, extent: { maxLat: 10, maxLon: 100, @@ -65,30 +59,36 @@ describe('map_actions', () => { }, zoom: 5, }); - await action(dispatchMock, getStoreMock); + action(dispatchMock, getStoreMock); - expect(dispatchMock).toHaveBeenCalledWith({ - mapState: { - zoom: 5, - extent: { - maxLat: 10, - maxLon: 100, - minLat: 5, - minLon: 95, - }, - buffer: { - maxLat: 11.1784, - maxLon: 101.25, - minLat: 0, - minLon: 90, + expect(dispatchMock.mock.calls[0]).toEqual([ + { + mapViewContext: { + center: { + lat: 7.5, + lon: 97.5, + }, + zoom: 5, + extent: { + maxLat: 10, + maxLon: 100, + minLat: 5, + minLon: 95, + }, + buffer: { + maxLat: 11.1784, + maxLon: 101.25, + minLat: 0, + minLon: 90, + }, }, + type: 'MAP_EXTENT_CHANGED', }, - type: 'MAP_EXTENT_CHANGED', - }); + ]); }); }); - describe('store mapState is populated', () => { + describe('mapState.buffer is defined', () => { const initialZoom = 10; beforeEach(() => { require('../selectors/map_selectors').getDataFilters = () => { @@ -104,8 +104,12 @@ describe('map_actions', () => { }; }); - it('should not update buffer if extent is contained in existing buffer', async () => { + it('should not update buffer if extent is contained in existing buffer', () => { const action = mapExtentChanged({ + center: { + lat: 8.5, + lon: 98.5, + }, zoom: initialZoom, extent: { maxLat: 11, @@ -114,30 +118,40 @@ describe('map_actions', () => { minLon: 96, }, }); - await action(dispatchMock, getStoreMock); - - expect(dispatchMock).toHaveBeenCalledWith({ - mapState: { - zoom: 10, - extent: { - maxLat: 11, - maxLon: 101, - minLat: 6, - minLon: 96, - }, - buffer: { - maxLat: 12.5, - maxLon: 102.5, - minLat: 2.5, - minLon: 92.5, + action(dispatchMock, getStoreMock); + + expect(dispatchMock.mock.calls[0]).toEqual([ + { + mapViewContext: { + center: { + lat: 8.5, + lon: 98.5, + }, + zoom: 10, + extent: { + maxLat: 11, + maxLon: 101, + minLat: 6, + minLon: 96, + }, + buffer: { + maxLat: 12.5, + maxLon: 102.5, + minLat: 2.5, + minLon: 92.5, + }, }, + type: 'MAP_EXTENT_CHANGED', }, - type: 'MAP_EXTENT_CHANGED', - }); + ]); }); - it('should update buffer if extent is outside of existing buffer', async () => { + it('should update buffer if extent is outside of existing buffer', () => { const action = mapExtentChanged({ + center: { + lat: 2.5, + lon: 87.5, + }, zoom: initialZoom, extent: { maxLat: 5, @@ -146,30 +160,40 @@ describe('map_actions', () => { minLon: 85, }, }); - await action(dispatchMock, getStoreMock); - - expect(dispatchMock).toHaveBeenCalledWith({ - mapState: { - zoom: 10, - extent: { - maxLat: 5, - maxLon: 90, - minLat: 0, - minLon: 85, - }, - buffer: { - maxLat: 5.26601, - maxLon: 90.35156, - minLat: -0.35156, - minLon: 84.72656, + action(dispatchMock, getStoreMock); + + expect(dispatchMock.mock.calls[0]).toEqual([ + { + mapViewContext: { + center: { + lat: 2.5, + lon: 87.5, + }, + zoom: 10, + extent: { + maxLat: 5, + maxLon: 90, + minLat: 0, + minLon: 85, + }, + buffer: { + maxLat: 5.26601, + maxLon: 90.35156, + minLat: -0.35156, + minLon: 84.72656, + }, }, + type: 'MAP_EXTENT_CHANGED', }, - type: 'MAP_EXTENT_CHANGED', - }); + ]); }); - it('should update buffer when zoom changes', async () => { + it('should update buffer when zoom changes', () => { const action = mapExtentChanged({ + center: { + lat: 8.5, + lon: 98.5, + }, zoom: initialZoom + 1, extent: { maxLat: 11, @@ -178,26 +202,32 @@ describe('map_actions', () => { minLon: 96, }, }); - await action(dispatchMock, getStoreMock); - - expect(dispatchMock).toHaveBeenCalledWith({ - mapState: { - zoom: 11, - extent: { - maxLat: 11, - maxLon: 101, - minLat: 6, - minLon: 96, - }, - buffer: { - maxLat: 11.0059, - maxLon: 101.07422, - minLat: 5.96575, - minLon: 95.97656, + action(dispatchMock, getStoreMock); + + expect(dispatchMock.mock.calls[0]).toEqual([ + { + mapViewContext: { + center: { + lat: 8.5, + lon: 98.5, + }, + zoom: 11, + extent: { + maxLat: 11, + maxLon: 101, + minLat: 6, + minLon: 96, + }, + buffer: { + maxLat: 11.0059, + maxLon: 101.07422, + minLat: 5.96575, + minLon: 95.97656, + }, }, + type: 'MAP_EXTENT_CHANGED', }, - type: 'MAP_EXTENT_CHANGED', - }); + ]); }); }); }); @@ -262,13 +292,12 @@ describe('map_actions', () => { params: { query: 'png' }, }, query: { match_phrase: { extension: 'png' } }, - $state: { store: 'appState' }, }, ]; const searchSessionId = '1234'; beforeEach(() => { - //Mocks the "previous" state + // Mocks the "previous" state require('../selectors/map_selectors').getQuery = () => { return query; }; diff --git a/x-pack/plugins/maps/public/actions/map_actions.ts b/x-pack/plugins/maps/public/actions/map_actions.ts index af8d6cc6bedc..c1db14347460 100644 --- a/x-pack/plugins/maps/public/actions/map_actions.ts +++ b/x-pack/plugins/maps/public/actions/map_actions.ts @@ -6,6 +6,7 @@ */ import _ from 'lodash'; +import { i18n } from '@kbn/i18n'; import { AnyAction, Dispatch } from 'redux'; import { ThunkDispatch } from 'redux-thunk'; import turfBboxPolygon from '@turf/bbox-polygon'; @@ -13,6 +14,7 @@ import turfBooleanContains from '@turf/boolean-contains'; import { Filter, Query, TimeRange } from 'src/plugins/data/public'; import { Geometry, Position } from 'geojson'; import { DRAW_MODE, DRAW_SHAPE } from '../../common/constants'; +import type { MapExtentState, MapViewContext } from '../reducers/map/types'; import { MapStoreState } from '../reducers/store'; import { getDataFilters, @@ -52,24 +54,13 @@ import { import { autoFitToBounds, syncDataForAllLayers, syncDataForLayer } from './data_request_actions'; import { addLayer, addLayerWithoutDataSync } from './layer_actions'; import { MapSettings } from '../reducers/map'; -import { - DrawState, - MapCenter, - MapCenterAndZoom, - MapExtent, - Timeslice, -} from '../../common/descriptor_types'; +import { DrawState, MapCenterAndZoom, MapExtent, Timeslice } from '../../common/descriptor_types'; import { INITIAL_LOCATION } from '../../common/constants'; import { cleanTooltipStateForLayer } from './tooltip_actions'; import { VectorLayer } from '../classes/layers/vector_layer'; import { SET_DRAW_MODE } from './ui_actions'; import { expandToTileBoundaries } from '../../common/geo_tile_utils'; - -export interface MapExtentState { - zoom: number; - extent: MapExtent; - center: MapCenter; -} +import { getToasts } from '../kibana_services'; export function setMapInitError(errorMessage: string) { return { @@ -138,56 +129,50 @@ export function mapDestroyed() { } export function mapExtentChanged(mapExtentState: MapExtentState) { - return async ( + return ( dispatch: ThunkDispatch, getState: () => MapStoreState ) => { - const dataFilters = getDataFilters(getState()); - const { extent, zoom: newZoom } = mapExtentState; - const { buffer, zoom: currentZoom } = dataFilters; - - if (extent) { - let doesBufferContainExtent = false; - if (buffer) { - const bufferGeometry = turfBboxPolygon([ - buffer.minLon, - buffer.minLat, - buffer.maxLon, - buffer.maxLat, - ]); - const extentGeometry = turfBboxPolygon([ - extent.minLon, - extent.minLat, - extent.maxLon, - extent.maxLat, - ]); - - doesBufferContainExtent = turfBooleanContains(bufferGeometry, extentGeometry); - } - - if (!doesBufferContainExtent || currentZoom !== newZoom) { - // snap to the smallest tile-bounds, to avoid jitter in the bounds - dataFilters.buffer = expandToTileBoundaries(extent, Math.ceil(newZoom)); - } + const { extent, zoom: nextZoom } = mapExtentState; + const { buffer: prevBuffer, zoom: prevZoom } = getDataFilters(getState()); + + let doesPrevBufferContainNextExtent = true; + if (prevBuffer) { + const bufferGeometry = turfBboxPolygon([ + prevBuffer.minLon, + prevBuffer.minLat, + prevBuffer.maxLon, + prevBuffer.maxLat, + ]); + const extentGeometry = turfBboxPolygon([ + extent.minLon, + extent.minLat, + extent.maxLon, + extent.maxLat, + ]); + doesPrevBufferContainNextExtent = turfBooleanContains(bufferGeometry, extentGeometry); } dispatch({ type: MAP_EXTENT_CHANGED, - mapState: { - ...dataFilters, + mapViewContext: { ...mapExtentState, - }, + buffer: + !prevBuffer || !doesPrevBufferContainNextExtent || prevZoom !== nextZoom + ? expandToTileBoundaries(extent, Math.ceil(nextZoom)) + : prevBuffer, + } as MapViewContext, }); - if (currentZoom !== newZoom) { + if (prevZoom !== nextZoom) { getLayerList(getState()).map((layer) => { - if (!layer.showAtZoomLevel(newZoom)) { + if (!layer.showAtZoomLevel(nextZoom)) { dispatch(cleanTooltipStateForLayer(layer.getId())); } }); } - await dispatch(syncDataForAllLayers()); + dispatch(syncDataForAllLayers()); }; } @@ -384,8 +369,17 @@ export function addNewFeatureToIndex(geometry: Geometry | Position[]) { if (!layer || !(layer instanceof VectorLayer)) { return; } - await layer.addFeature(geometry); - await dispatch(syncDataForLayer(layer, true)); + + try { + await layer.addFeature(geometry); + await dispatch(syncDataForLayer(layer, true)); + } catch (e) { + getToasts().addError(e, { + title: i18n.translate('xpack.maps.mapActions.addFeatureError', { + defaultMessage: `Unable to add feature to index.`, + }), + }); + } }; } @@ -403,7 +397,15 @@ export function deleteFeatureFromIndex(featureId: string) { if (!layer || !(layer instanceof VectorLayer)) { return; } - await layer.deleteFeature(featureId); - await dispatch(syncDataForLayer(layer, true)); + try { + await layer.deleteFeature(featureId); + await dispatch(syncDataForLayer(layer, true)); + } catch (e) { + getToasts().addError(e, { + title: i18n.translate('xpack.maps.mapActions.removeFeatureError', { + defaultMessage: `Unable to remove feature from index.`, + }), + }); + } }; } diff --git a/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts b/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts index abdcf65a4ab1..afddb34d2d0e 100644 --- a/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts +++ b/x-pack/plugins/maps/public/classes/fields/es_doc_field.ts @@ -5,11 +5,11 @@ * 2.0. */ +import type { IndexPatternField } from 'src/plugins/data/public'; import { FIELD_ORIGIN } from '../../../common/constants'; import { ESTooltipProperty } from '../tooltips/es_tooltip_property'; import { ITooltipProperty, TooltipProperty } from '../tooltips/tooltip_property'; import { indexPatterns } from '../../../../../../src/plugins/data/public'; -import { IFieldType } from '../../../../../../src/plugins/data/public'; import { IField, AbstractField } from './field'; import { IESSource } from '../sources/es_source'; import { IVectorSource } from '../sources/vector_source'; @@ -42,7 +42,7 @@ export class ESDocField extends AbstractField implements IField { return this._source; } - async _getIndexPatternField(): Promise { + async _getIndexPatternField(): Promise { const indexPattern = await this._source.getIndexPattern(); const indexPatternField = indexPattern.fields.getByName(this.getName()); return indexPatternField && indexPatterns.isNestedField(indexPatternField) diff --git a/x-pack/plugins/maps/public/classes/fields/kibana_region_field.ts b/x-pack/plugins/maps/public/classes/fields/kibana_region_field.ts deleted file mode 100644 index a183d197b1db..000000000000 --- a/x-pack/plugins/maps/public/classes/fields/kibana_region_field.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { IField, AbstractField } from './field'; -import { KibanaRegionmapSource } from '../sources/kibana_regionmap_source/kibana_regionmap_source'; -import { FIELD_ORIGIN } from '../../../common/constants'; -import { IVectorSource } from '../sources/vector_source'; - -export class KibanaRegionField extends AbstractField implements IField { - private readonly _source: KibanaRegionmapSource; - - constructor({ - fieldName, - source, - origin, - }: { - fieldName: string; - source: KibanaRegionmapSource; - origin: FIELD_ORIGIN; - }) { - super({ fieldName, origin }); - this._source = source; - } - - getSource(): IVectorSource { - return this._source; - } - - async getLabel(): Promise { - const meta = await this._source.getVectorFileMeta(); - // TODO remove any and @ts-ignore when vectorFileMeta type defined - // @ts-ignore - const field: any = meta.fields.find((f) => f.name === this.getName()); - return field ? field.description : this.getName(); - } -} diff --git a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx index 82a741e7ccda..5bd2b68e61bc 100644 --- a/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx +++ b/x-pack/plugins/maps/public/classes/layers/choropleth_layer_wizard/layer_template.tsx @@ -18,7 +18,7 @@ import { EuiSpacer, EuiTitle, } from '@elastic/eui'; -import { IFieldType, IndexPattern } from 'src/plugins/data/public'; +import { IndexPatternField, IndexPattern } from 'src/plugins/data/public'; import { RenderWizardArguments } from '../layer_wizard_registry'; import { EMSFileSelect } from '../../../components/ems_file_select'; import { GeoIndexPatternSelect } from '../../../components/geo_index_pattern_select'; @@ -56,14 +56,14 @@ interface State { leftEmsFileId: string | null; leftEmsFields: Array>; leftIndexPattern: IndexPattern | null; - leftGeoFields: IFieldType[]; - leftJoinFields: IFieldType[]; + leftGeoFields: IndexPatternField[]; + leftJoinFields: IndexPatternField[]; leftGeoField: string | null; leftEmsJoinField: string | null; leftElasticsearchJoinField: string | null; rightIndexPatternId: string; rightIndexPatternTitle: string | null; - rightTermsFields: IFieldType[]; + rightTermsFields: IndexPatternField[]; rightJoinField: string | null; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/update_source_editor.tsx index 2688ef1d2968..66f3efe992f8 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_geo_line_source/update_source_editor.tsx @@ -9,11 +9,8 @@ import React, { Fragment, Component } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui'; -import { - IFieldType, - IndexPattern, - indexPatterns, -} from '../../../../../../../src/plugins/data/public'; +import type { IndexPatternField, IndexPattern } from 'src/plugins/data/public'; +import { indexPatterns } from '../../../../../../../src/plugins/data/public'; import { MetricsEditor } from '../../../components/metrics_editor'; import { getIndexPatternService } from '../../../kibana_services'; import { GeoLineForm } from './geo_line_form'; @@ -30,7 +27,7 @@ interface Props { interface State { indexPattern: IndexPattern | null; - fields: IFieldType[]; + fields: IndexPatternField[]; } export class UpdateSourceEditor extends Component { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx index 464ba024663e..1ca7ddb58629 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/es_search_source.tsx @@ -9,7 +9,7 @@ import _ from 'lodash'; import React, { ReactElement } from 'react'; import rison from 'rison-node'; import { i18n } from '@kbn/i18n'; -import type { Filter, IFieldType, IndexPattern } from 'src/plugins/data/public'; +import type { Filter, IndexPatternField, IndexPattern } from 'src/plugins/data/public'; import { GeoJsonProperties, Geometry, Position } from 'geojson'; import { esFilters } from '../../../../../../../src/plugins/data/public'; import { AbstractESSource } from '../es_source'; @@ -183,7 +183,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye async getFields(): Promise { try { const indexPattern = await this.getIndexPattern(); - const fields: IFieldType[] = indexPattern.fields.filter((field) => { + const fields: IndexPatternField[] = indexPattern.fields.filter((field) => { // Ensure fielddata is enabled for field. // Search does not request _source return field.aggregatable; @@ -300,7 +300,7 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye }; } - const topHitsSplitField: IFieldType = getField(indexPattern, topHitsSplitFieldName); + const topHitsSplitField: IndexPatternField = getField(indexPattern, topHitsSplitFieldName); const cardinalityAgg = { precision_threshold: 1 }; const termsAgg = { size: DEFAULT_MAX_BUCKETS_LIMIT, @@ -441,15 +441,23 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye return !!(scalingType === SCALING_TYPES.TOP_HITS && topHitsSplitField); } - async supportsFeatureEditing(): Promise { + async getSourceIndexList(): Promise { await this.getIndexPattern(); if (!(this.indexPattern && this.indexPattern.title)) { - return false; + return []; } - const { matchingIndexes } = await getMatchingIndexes(this.indexPattern.title); - if (!matchingIndexes) { - return false; + let success; + let matchingIndexes; + try { + ({ success, matchingIndexes } = await getMatchingIndexes(this.indexPattern.title)); + } catch (e) { + // Fail silently } + return success ? matchingIndexes : []; + } + + async supportsFeatureEditing(): Promise { + const matchingIndexes = await this.getSourceIndexList(); // For now we only support 1:1 index-pattern:index matches return matchingIndexes.length === 1; } @@ -749,17 +757,36 @@ export class ESSearchSource extends AbstractESSource implements ITiledSingleLaye return MVT_SOURCE_LAYER_NAME; } + async _getEditableIndex(): Promise { + const indexList = await this.getSourceIndexList(); + if (indexList.length === 0) { + throw new Error( + i18n.translate('xpack.maps.source.esSearch.indexZeroLengthEditError', { + defaultMessage: `Your index pattern doesn't point to any indices.`, + }) + ); + } + if (indexList.length > 1) { + throw new Error( + i18n.translate('xpack.maps.source.esSearch.indexOverOneLengthEditError', { + defaultMessage: `Your index pattern points to multiple indices. Only one index is allowed per index pattern.`, + }) + ); + } + return indexList[0]; + } + async addFeature( geometry: Geometry | Position[], defaultFields: Record> ) { - const indexPattern = await this.getIndexPattern(); - await addFeatureToIndex(indexPattern.title, geometry, this.getGeoFieldName(), defaultFields); + const index = await this._getEditableIndex(); + await addFeatureToIndex(index, geometry, this.getGeoFieldName(), defaultFields); } async deleteFeature(featureId: string) { - const indexPattern = await this.getIndexPattern(); - await deleteFeatureFromIndex(indexPattern.title, featureId); + const index = await this._getEditableIndex(); + await deleteFeatureFromIndex(index, featureId); } async getUrlTemplateWithMeta( diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx index 41c9076c1143..e71ee803d77e 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/create_source_editor.tsx @@ -8,16 +8,13 @@ import React, { Component } from 'react'; import { EuiPanel } from '@elastic/eui'; +import type { IndexPattern, IndexPatternField } from 'src/plugins/data/public'; import { SCALING_TYPES } from '../../../../../common/constants'; import { GeoFieldSelect } from '../../../../components/geo_field_select'; import { GeoIndexPatternSelect } from '../../../../components/geo_index_pattern_select'; import { getGeoFields, getTermsFields, getSortFields } from '../../../../index_pattern_util'; import { ESSearchSourceDescriptor } from '../../../../../common/descriptor_types'; -import { - IndexPattern, - IFieldType, - SortDirection, -} from '../../../../../../../../src/plugins/data/common'; +import { SortDirection } from '../../../../../../../../src/plugins/data/public'; import { TopHitsForm } from './top_hits_form'; import { OnSourceChangeArgs } from '../../source'; @@ -27,12 +24,12 @@ interface Props { interface State { indexPattern: IndexPattern | null; - geoFields: IFieldType[]; + geoFields: IndexPatternField[]; geoFieldName: string | null; sortField: string | null; - sortFields: IFieldType[]; + sortFields: IndexPatternField[]; sortOrder: SortDirection; - termFields: IFieldType[]; + termFields: IndexPatternField[]; topHitsSplitField: string | null; topHitsSize: number; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/top_hits_form.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/top_hits_form.tsx index 79d6039076f8..072c39954418 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/top_hits_form.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/top_hits_form.tsx @@ -8,6 +8,7 @@ import React, { ChangeEvent, Component, Fragment } from 'react'; import { EuiFormRow, EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import type { IndexPatternField } from 'src/plugins/data/public'; import { SingleFieldSelect } from '../../../../components/single_field_select'; import { getIndexPatternService } from '../../../../kibana_services'; // @ts-expect-error @@ -15,16 +16,16 @@ import { ValidatedRange } from '../../../../components/validated_range'; import { DEFAULT_MAX_INNER_RESULT_WINDOW } from '../../../../../common/constants'; import { loadIndexSettings } from '../util/load_index_settings'; import { OnSourceChangeArgs } from '../../source'; -import { IFieldType, SortDirection } from '../../../../../../../../src/plugins/data/public'; +import { SortDirection } from '../../../../../../../../src/plugins/data/public'; interface Props { indexPatternId: string; isColumnCompressed?: boolean; onChange: (args: OnSourceChangeArgs) => void; sortField: string; - sortFields: IFieldType[]; + sortFields: IndexPatternField[]; sortOrder: SortDirection; - termFields: IFieldType[]; + termFields: IndexPatternField[]; topHitsSplitField: string | null; topHitsSize: number; } diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/update_source_editor.tsx b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/update_source_editor.tsx index 6ca38f3be22f..33f0d3c1d984 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/update_source_editor.tsx +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/top_hits/update_source_editor.tsx @@ -9,12 +9,13 @@ import React, { Component, Fragment } from 'react'; import { EuiFormRow, EuiTitle, EuiPanel, EuiSpacer, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { IndexPatternField } from 'src/plugins/data/public'; import { FIELD_ORIGIN } from '../../../../../common/constants'; import { TooltipSelector } from '../../../../components/tooltip_selector'; import { getIndexPatternService } from '../../../../kibana_services'; import { getTermsFields, getSortFields, getSourceFields } from '../../../../index_pattern_util'; -import { SortDirection, IFieldType } from '../../../../../../../../src/plugins/data/public'; +import { SortDirection } from '../../../../../../../../src/plugins/data/public'; import { ESDocField } from '../../../fields/es_doc_field'; import { OnSourceChangeArgs } from '../../source'; import { TopHitsForm } from './top_hits_form'; @@ -36,8 +37,8 @@ interface Props { interface State { loadError?: string; sourceFields: IField[]; - termFields: IFieldType[]; - sortFields: IFieldType[]; + termFields: IndexPatternField[]; + sortFields: IndexPatternField[]; } export class TopHitsUpdateSourceEditor extends Component { diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts index c9a967bea3e2..08ba33a72363 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/feature_edit.ts @@ -43,8 +43,9 @@ export const deleteFeatureFromIndex = async (indexName: string, featureId: strin export const getMatchingIndexes = async (indexPattern: string) => { return await getHttp().fetch({ - path: `${GET_MATCHING_INDEXES_PATH}/${indexPattern}`, + path: GET_MATCHING_INDEXES_PATH, method: 'GET', + query: { indexPattern }, }); }; diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/get_docvalue_source_fields.test.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/get_docvalue_source_fields.test.ts index 0a24c140d735..a4429c663da8 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/get_docvalue_source_fields.test.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/get_docvalue_source_fields.test.ts @@ -6,10 +6,9 @@ */ import { getDocValueAndSourceFields } from './get_docvalue_source_fields'; -import { IndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns/index_patterns'; -import { IFieldType } from '../../../../../../../../src/plugins/data/common/index_patterns/fields'; +import type { IndexPatternField, IndexPattern } from 'src/plugins/data/public'; -function createMockIndexPattern(fields: IFieldType[]): IndexPattern { +function createMockIndexPattern(fields: IndexPatternField[]): IndexPattern { const indexPattern = { get fields() { return { @@ -29,9 +28,8 @@ describe('getDocValueAndSourceFields', () => { createMockIndexPattern([ { name: 'foobar', - // @ts-expect-error runtimeField not added yet to IFieldType. API tbd - runtimeField: {}, - }, + runtimeField: { type: 'keyword' }, + } as IndexPatternField, ]), ['foobar'], 'epoch_millis' diff --git a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/get_docvalue_source_fields.ts b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/get_docvalue_source_fields.ts index 78823f4631ca..949dc990c44f 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_search_source/util/get_docvalue_source_fields.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_search_source/util/get_docvalue_source_fields.ts @@ -34,9 +34,7 @@ export function getDocValueAndSourceFields( lang: field.lang || '', }, }; - } - // @ts-expect-error runtimeField has not been added to public API yet. exact shape of type TBD. - else if (field.readFromDocValues || field.runtimeField) { + } else if (field.readFromDocValues || field.runtimeField) { const docValueField = field.type === 'date' ? { diff --git a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts index 23bcd9baed8c..ce8991bb63ce 100644 --- a/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts +++ b/x-pack/plugins/maps/public/classes/sources/es_source/es_source.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import uuid from 'uuid/v4'; -import { Filter, IFieldType, IndexPattern, ISearchSource } from 'src/plugins/data/public'; +import { Filter, IndexPatternField, IndexPattern, ISearchSource } from 'src/plugins/data/public'; import { AbstractVectorSource, BoundsFilters } from '../vector_source'; import { getAutocompleteService, @@ -364,7 +364,7 @@ export class AbstractESSource extends AbstractVectorSource implements IESSource } } - async _getGeoField(): Promise { + async _getGeoField(): Promise { const indexPattern = await this.getIndexPattern(); const geoField = indexPattern.fields.getByName(this.getGeoFieldName()); if (!geoField) { diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/fetch_geojson.ts b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/fetch_geojson.ts deleted file mode 100644 index 329070632a94..000000000000 --- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/fetch_geojson.ts +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import _ from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { FeatureCollection } from 'geojson'; -import * as topojson from 'topojson-client'; -import { GeometryCollection } from 'topojson-specification'; -import fetch from 'node-fetch'; - -export enum FORMAT_TYPE { - GEOJSON = 'geojson', - TOPOJSON = 'topojson', -} - -export async function fetchGeoJson( - fetchUrl: string, - format: FORMAT_TYPE, - featureCollectionPath: string -): Promise { - let fetchedJson; - try { - const response = await fetch(fetchUrl); - if (!response.ok) { - throw new Error('Request failed'); - } - fetchedJson = await response.json(); - } catch (e) { - throw new Error( - i18n.translate('xpack.maps.util.requestFailedErrorMessage', { - defaultMessage: `Unable to fetch vector shapes from url: {fetchUrl}`, - values: { fetchUrl }, - }) - ); - } - - if (format === FORMAT_TYPE.GEOJSON) { - return fetchedJson; - } - - if (format === FORMAT_TYPE.TOPOJSON) { - const features = _.get(fetchedJson, `objects.${featureCollectionPath}`) as GeometryCollection; - return topojson.feature(fetchedJson, features); - } - - throw new Error( - i18n.translate('xpack.maps.util.formatErrorMessage', { - defaultMessage: `Unable to fetch vector shapes from url: {format}`, - values: { format }, - }) - ); -} diff --git a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts b/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts deleted file mode 100644 index f7f311d011d6..000000000000 --- a/x-pack/plugins/maps/public/classes/sources/kibana_regionmap_source/kibana_regionmap_source.ts +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { AbstractVectorSource, GeoJsonWithMeta } from '../vector_source'; -import { getRegionmapLayers } from '../../../kibana_services'; -import { getDataSourceLabel } from '../../../../common/i18n_getters'; -import { FIELD_ORIGIN, SOURCE_TYPES } from '../../../../common/constants'; -import { KibanaRegionField } from '../../fields/kibana_region_field'; -import { registerSource } from '../source_registry'; -import { KibanaRegionmapSourceDescriptor } from '../../../../common/descriptor_types'; -import { Adapters } from '../../../../../../../src/plugins/inspector/common/adapters'; -import { IField } from '../../fields/field'; -import type { LayerConfig } from '../../../../../../../src/plugins/maps_ems/public'; -import { fetchGeoJson, FORMAT_TYPE } from './fetch_geojson'; - -const sourceTitle = i18n.translate('xpack.maps.source.kbnRegionMapTitle', { - defaultMessage: 'Configured GeoJSON', -}); - -export class KibanaRegionmapSource extends AbstractVectorSource { - readonly _descriptor: KibanaRegionmapSourceDescriptor; - - static createDescriptor({ name }: { name: string }): KibanaRegionmapSourceDescriptor { - return { - type: SOURCE_TYPES.REGIONMAP_FILE, - name, - }; - } - - constructor(descriptor: KibanaRegionmapSourceDescriptor, inspectorAdapters?: Adapters) { - super(descriptor, inspectorAdapters); - this._descriptor = descriptor; - } - - createField({ fieldName }: { fieldName: string }): KibanaRegionField { - return new KibanaRegionField({ - fieldName, - source: this, - origin: FIELD_ORIGIN.SOURCE, - }); - } - - async getImmutableProperties() { - const vectorFileMeta = await this.getVectorFileMeta(); - return [ - { - label: getDataSourceLabel(), - value: sourceTitle, - }, - { - label: i18n.translate('xpack.maps.source.kbnRegionMap.vectorLayerLabel', { - defaultMessage: 'Vector layer', - }), - value: this._descriptor.name, - }, - { - label: i18n.translate('xpack.maps.source.kbnRegionMap.vectorLayerUrlLabel', { - defaultMessage: 'Vector layer url', - }), - value: vectorFileMeta.url, - }, - ]; - } - - async getVectorFileMeta(): Promise { - const regionList: LayerConfig[] = getRegionmapLayers(); - const layerConfig: LayerConfig | undefined = regionList.find( - (regionConfig: LayerConfig) => regionConfig.name === this._descriptor.name - ); - if (!layerConfig) { - throw new Error( - i18n.translate('xpack.maps.source.kbnRegionMap.noConfigErrorMessage', { - defaultMessage: `Unable to find map.regionmap configuration for {name}`, - values: { - name: this._descriptor.name, - }, - }) - ); - } - return layerConfig; - } - - async getGeoJsonWithMeta(): Promise { - const vectorFileMeta = await this.getVectorFileMeta(); - const featureCollection = await fetchGeoJson( - vectorFileMeta.url, - vectorFileMeta.format.type as FORMAT_TYPE, - vectorFileMeta.meta.feature_collection_path - ); - - return { - data: featureCollection, - meta: {}, - }; - } - - async getLeftJoinFields(): Promise { - const vectorFileMeta: LayerConfig = await this.getVectorFileMeta(); - return vectorFileMeta.fields.map( - (field): KibanaRegionField => { - return this.createField({ fieldName: field.name }); - } - ); - } - - async getDisplayName(): Promise { - return this._descriptor.name; - } - - hasTooltipProperties() { - return true; - } - - getSourceTooltipContent() { - return { - tooltipContent: i18n.translate('xpack.maps.source.kbnRegionMap.deprecationTooltipMessage', { - defaultMessage: `'Configured GeoJSON' layer is deprecated. 1) Use 'Upload GeoJSON' to upload '{vectorLayer}'. 2) Use Choropleth layer wizard to build a replacement layer. 3) Finally, delete this layer from your map.`, - values: { - vectorLayer: this._descriptor.name, - }, - }), - areResultsTrimmed: false, - isDeprecated: true, - }; - } -} - -registerSource({ - ConstructorFunction: KibanaRegionmapSource, - type: SOURCE_TYPES.REGIONMAP_FILE, -}); diff --git a/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.test.ts b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.test.ts index fbb416e7a761..b9021f189655 100644 --- a/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.test.ts +++ b/x-pack/plugins/maps/public/classes/tooltips/es_tooltip_property.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { IFieldType, IndexPattern } from '../../../../../../src/plugins/data/public'; +import type { IndexPatternField, IndexPattern } from 'src/plugins/data/public'; import { ESTooltipProperty } from './es_tooltip_property'; import { TooltipProperty } from './tooltip_property'; import { AbstractField } from '../fields/field'; @@ -25,7 +25,7 @@ const indexPatternField = { searchable: true, aggregatable: true, readFromDocValues: false, -} as IFieldType; +} as IndexPatternField; const featurePropertyField = new MockField({ fieldName: 'machine.os', @@ -41,7 +41,7 @@ const nonFilterableIndexPatternField = { searchable: true, aggregatable: true, readFromDocValues: false, -} as IFieldType; +} as IndexPatternField; const nonFilterableFeaturePropertyField = new MockField({ fieldName: 'location', @@ -51,7 +51,7 @@ const nonFilterableFeaturePropertyField = new MockField({ const indexPattern = { id: 'indexPatternId', fields: { - getByName: (name: string): IFieldType | null => { + getByName: (name: string): IndexPatternField | null => { if (name === 'machine.os') { return indexPatternField; } diff --git a/x-pack/plugins/maps/public/components/geo_field_select.tsx b/x-pack/plugins/maps/public/components/geo_field_select.tsx index 0b04ec714661..5ee81866784b 100644 --- a/x-pack/plugins/maps/public/components/geo_field_select.tsx +++ b/x-pack/plugins/maps/public/components/geo_field_select.tsx @@ -8,12 +8,12 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFormRow } from '@elastic/eui'; +import type { IndexPatternField } from 'src/plugins/data/public'; import { SingleFieldSelect } from './single_field_select'; -import { IFieldType } from '../../../../../src/plugins/data/common'; interface Props { value: string; - geoFields: IFieldType[]; + geoFields: IndexPatternField[]; onChange: (geoFieldName?: string) => void; } diff --git a/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx b/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx index b72a8d01d4a9..c3782d9be73d 100644 --- a/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx +++ b/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx @@ -11,15 +11,15 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonEmpty, EuiComboBoxOptionOption, EuiFieldText, EuiFormRow } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { IndexPatternField } from 'src/plugins/data/public'; import { MetricSelect } from './metric_select'; import { SingleFieldSelect } from '../single_field_select'; import { AggDescriptor } from '../../../common/descriptor_types'; import { AGG_TYPE, DEFAULT_PERCENTILE } from '../../../common/constants'; import { getTermsFields } from '../../index_pattern_util'; -import { IFieldType } from '../../../../../../src/plugins/data/public'; import { ValidatedNumberInput } from '../validated_number_input'; -function filterFieldsForAgg(fields: IFieldType[], aggType: AGG_TYPE) { +function filterFieldsForAgg(fields: IndexPatternField[], aggType: AGG_TYPE) { if (!fields) { return []; } @@ -40,7 +40,7 @@ function filterFieldsForAgg(fields: IFieldType[], aggType: AGG_TYPE) { interface Props { metric: AggDescriptor; - fields: IFieldType[]; + fields: IndexPatternField[]; onChange: (metric: AggDescriptor) => void; onRemove: () => void; metricsFilter?: (metricOption: EuiComboBoxOptionOption) => boolean; diff --git a/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx index 2124345e7e03..17baa7eb355d 100644 --- a/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx +++ b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx @@ -8,9 +8,9 @@ import React, { Component, Fragment } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButtonEmpty, EuiComboBoxOptionOption, EuiSpacer, EuiTextAlign } from '@elastic/eui'; +import type { IndexPatternField } from 'src/plugins/data/public'; import { MetricEditor } from './metric_editor'; import { DEFAULT_METRIC } from '../../classes/sources/es_agg_source'; -import { IFieldType } from '../../../../../../src/plugins/data/public'; import { AggDescriptor, FieldedAggDescriptor } from '../../../common/descriptor_types'; import { AGG_TYPE } from '../../../common/constants'; @@ -23,7 +23,7 @@ export function isMetricValid(aggDescriptor: AggDescriptor) { interface Props { allowMultipleMetrics: boolean; metrics: AggDescriptor[]; - fields: IFieldType[]; + fields: IndexPatternField[]; onChange: (metrics: AggDescriptor[]) => void; metricsFilter?: (metricOption: EuiComboBoxOptionOption) => boolean; } diff --git a/x-pack/plugins/maps/public/components/single_field_select.tsx b/x-pack/plugins/maps/public/components/single_field_select.tsx index 6727de35b1be..67594db11eb3 100644 --- a/x-pack/plugins/maps/public/components/single_field_select.tsx +++ b/x-pack/plugins/maps/public/components/single_field_select.tsx @@ -17,20 +17,20 @@ import { EuiFlexItem, EuiToolTip, } from '@elastic/eui'; -import { IFieldType } from 'src/plugins/data/public'; +import { IndexPatternField } from 'src/plugins/data/public'; import { FieldIcon } from '../../../../../src/plugins/kibana_react/public'; function fieldsToOptions( - fields?: IFieldType[], - isFieldDisabled?: (field: IFieldType) => boolean -): Array> { + fields?: IndexPatternField[], + isFieldDisabled?: (field: IndexPatternField) => boolean +): Array> { if (!fields) { return []; } return fields .map((field) => { - const option: EuiComboBoxOptionOption = { + const option: EuiComboBoxOptionOption = { value: field, label: field.displayName ? field.displayName : field.name, }; @@ -45,14 +45,14 @@ function fieldsToOptions( } type Props = Omit< - EuiComboBoxProps, + EuiComboBoxProps, 'isDisabled' | 'onChange' | 'options' | 'renderOption' | 'selectedOptions' | 'singleSelection' > & { - fields?: IFieldType[]; + fields?: IndexPatternField[]; onChange: (fieldName?: string) => void; value: string | null; // index pattern field name - isFieldDisabled?: (field: IFieldType) => boolean; - getFieldDisabledReason?: (field: IFieldType) => string | null; + isFieldDisabled?: (field: IndexPatternField) => boolean; + getFieldDisabledReason?: (field: IndexPatternField) => string | null; }; export function SingleFieldSelect({ @@ -64,7 +64,7 @@ export function SingleFieldSelect({ ...rest }: Props) { function renderOption( - option: EuiComboBoxOptionOption, + option: EuiComboBoxOptionOption, searchValue: string, contentClassName: string ) { @@ -91,13 +91,13 @@ export function SingleFieldSelect({ ); } - const onSelection = (selectedOptions: Array>) => { + const onSelection = (selectedOptions: Array>) => { onChange(_.get(selectedOptions, '0.value.name')); }; - const selectedOptions: Array> = []; + const selectedOptions: Array> = []; if (value && fields) { - const selectedField = fields.find((field: IFieldType) => { + const selectedField = fields.find((field: IndexPatternField) => { return field.name === value; }); if (selectedField) { diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join.tsx index 6962977627b9..c636047e2be3 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join.tsx @@ -9,7 +9,7 @@ import _ from 'lodash'; import React, { Component } from 'react'; import { EuiFlexItem, EuiFlexGroup, EuiButtonIcon } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import type { IFieldType, IndexPattern, Query } from 'src/plugins/data/public'; +import type { IndexPatternField, IndexPattern, Query } from 'src/plugins/data/public'; import { JoinExpression } from './join_expression'; import { MetricsExpression } from './metrics_expression'; import { WhereExpression } from './where_expression'; @@ -38,7 +38,7 @@ interface Props { } interface State { - rightFields: IFieldType[]; + rightFields: IndexPatternField[]; indexPattern?: IndexPattern; loadError?: string; } diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.tsx index f2073a9f6e65..19a47736b195 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/join_expression.tsx @@ -18,7 +18,7 @@ import { } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { IFieldType } from 'src/plugins/data/public'; +import { IndexPatternField } from 'src/plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; import { DEFAULT_MAX_BUCKETS_LIMIT } from '../../../../../common/constants'; import { SingleFieldSelect } from '../../../../components/single_field_select'; @@ -54,7 +54,7 @@ interface Props { // Right field props rightValue: string; rightSize?: number; - rightFields: IFieldType[]; + rightFields: IndexPatternField[]; onRightFieldChange: (term?: string) => void; onRightSizeChange: (size: number) => void; } diff --git a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.tsx b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.tsx index 899430f3c2f2..4c2e96498ee2 100644 --- a/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.tsx +++ b/x-pack/plugins/maps/public/connected_components/edit_layer_panel/join_editor/resources/metrics_expression.tsx @@ -15,7 +15,7 @@ import { EuiFormHelpText, } from '@elastic/eui'; -import { IFieldType } from 'src/plugins/data/public'; +import { IndexPatternField } from 'src/plugins/data/public'; import { FormattedMessage } from '@kbn/i18n/react'; import { MetricsEditor } from '../../../../components/metrics_editor'; import { AGG_TYPE } from '../../../../../common/constants'; @@ -23,7 +23,7 @@ import { AggDescriptor, FieldedAggDescriptor } from '../../../../../common/descr interface Props { metrics: AggDescriptor[]; - rightFields: IFieldType[]; + rightFields: IndexPatternField[]; onChange: (metrics: AggDescriptor[]) => void; } diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts index 3084d3b9c9f3..9936d412de9e 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/index.ts +++ b/x-pack/plugins/maps/public/connected_components/mb_map/index.ts @@ -14,7 +14,6 @@ import { clearMouseCoordinates, mapDestroyed, mapExtentChanged, - MapExtentState, mapReady, setAreTilesLoaded, setMapInitError, @@ -35,6 +34,7 @@ import { getInspectorAdapters } from '../../reducers/non_serializable_instances' import { MapStoreState } from '../../reducers/store'; import { DRAW_MODE } from '../../../common'; import { TileMetaFeature } from '../../../common/descriptor_types'; +import type { MapExtentState } from '../../reducers/map/types'; function mapStateToProps(state: MapStoreState) { return { diff --git a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx index a60aef95318f..053e410b8c71 100644 --- a/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx +++ b/x-pack/plugins/maps/public/connected_components/mb_map/mb_map.tsx @@ -49,10 +49,10 @@ import { } from './utils'; import { ResizeChecker } from '../../../../../../src/plugins/kibana_utils/public'; import { RenderToolTipContent } from '../../classes/tooltips/tooltip_property'; -import { MapExtentState } from '../../actions'; import { TileStatusTracker } from './tile_status_tracker'; import { DrawFeatureControl } from './draw_control/draw_feature_control'; import { TiledVectorLayer } from '../../classes/layers/tiled_vector_layer/tiled_vector_layer'; +import type { MapExtentState } from '../../reducers/map/types'; export interface Props { isMapReady: boolean; @@ -150,7 +150,7 @@ export class MbMap extends Component { } }, 256); - _getMapState() { + _getMapExtentState(): MapExtentState { const zoom = this.state.mbMap!.getZoom(); const mbCenter = this.state.mbMap!.getCenter(); const mbBounds = this.state.mbMap!.getBounds(); @@ -257,7 +257,7 @@ export class MbMap extends Component { this._loadMakiSprites(mbMap); this._initResizerChecker(); this._registerMapEventListeners(mbMap); - this.props.onMapReady(this._getMapState()); + this.props.onMapReady(this._getMapExtentState()); }); } @@ -269,7 +269,7 @@ export class MbMap extends Component { mbMap.on( 'moveend', _.debounce(() => { - this.props.extentChanged(this._getMapState()); + this.props.extentChanged(this._getMapExtentState()); }, 100) ); @@ -413,7 +413,7 @@ export class MbMap extends Component { // hack to update extent after zoom update finishes moving map. if (zoomRangeChanged) { setTimeout(() => { - this.props.extentChanged(this._getMapState()); + this.props.extentChanged(this._getMapExtentState()); }, 300); } } diff --git a/x-pack/plugins/maps/public/index_pattern_util.test.ts b/x-pack/plugins/maps/public/index_pattern_util.test.ts index 49ae16415e15..d1affe6ce4d8 100644 --- a/x-pack/plugins/maps/public/index_pattern_util.test.ts +++ b/x-pack/plugins/maps/public/index_pattern_util.test.ts @@ -14,6 +14,7 @@ import { supportsGeoTileAgg, } from './index_pattern_util'; import { ES_GEO_FIELD_TYPE } from '../common/constants'; +import { IndexPatternField } from 'src/plugins/data/public'; describe('getSourceFields', () => { test('Should remove multi fields from field list', () => { @@ -21,7 +22,7 @@ describe('getSourceFields', () => { { name: 'agent', type: 'string', - }, + } as IndexPatternField, { name: 'agent.keyword', subType: { @@ -30,7 +31,7 @@ describe('getSourceFields', () => { }, }, type: 'string', - }, + } as IndexPatternField, ]; const sourceFields = getSourceFields(fields); expect(sourceFields).toEqual([{ name: 'agent', type: 'string' }]); @@ -44,7 +45,7 @@ describe('Gold+ licensing', () => { name: 'location', type: 'geo_point', aggregatable: true, - }, + } as IndexPatternField, supportedInBasic: true, supportedInGold: true, }, @@ -53,7 +54,7 @@ describe('Gold+ licensing', () => { name: 'location', type: 'geo_shape', aggregatable: false, - }, + } as IndexPatternField, supportedInBasic: false, supportedInGold: false, }, @@ -62,7 +63,7 @@ describe('Gold+ licensing', () => { name: 'location', type: 'geo_shape', aggregatable: true, - }, + } as IndexPatternField, supportedInBasic: false, supportedInGold: true, }, diff --git a/x-pack/plugins/maps/public/index_pattern_util.ts b/x-pack/plugins/maps/public/index_pattern_util.ts index 3b1cb461c877..a2133f4f2521 100644 --- a/x-pack/plugins/maps/public/index_pattern_util.ts +++ b/x-pack/plugins/maps/public/index_pattern_util.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { IFieldType, IndexPattern } from 'src/plugins/data/public'; +import type { IndexPatternField, IndexPattern } from 'src/plugins/data/public'; import { i18n } from '@kbn/i18n'; import { getIndexPatternService } from './kibana_services'; import { indexPatterns } from '../../../../src/plugins/data/public'; import { ES_GEO_FIELD_TYPE, ES_GEO_FIELD_TYPES } from '../common/constants'; import { getIsGoldPlus } from './licensed_features'; -export function getGeoTileAggNotSupportedReason(field: IFieldType): string | null { +export function getGeoTileAggNotSupportedReason(field: IndexPatternField): string | null { if (!field.aggregatable) { return i18n.translate('xpack.maps.geoTileAgg.disabled.docValues', { defaultMessage: @@ -46,7 +46,7 @@ export async function getIndexPatternsFromIds( return await Promise.all(promises); } -export function getTermsFields(fields: IFieldType[]): IFieldType[] { +export function getTermsFields(fields: IndexPatternField[]): IndexPatternField[] { return fields.filter((field) => { return ( field.aggregatable && @@ -56,7 +56,7 @@ export function getTermsFields(fields: IFieldType[]): IFieldType[] { }); } -export function getSortFields(fields: IFieldType[]): IFieldType[] { +export function getSortFields(fields: IndexPatternField[]): IndexPatternField[] { return fields.filter((field) => { return field.sortable && !indexPatterns.isNestedField(field); }); @@ -70,23 +70,23 @@ export function getAggregatableGeoFieldTypes(): string[] { return aggregatableFieldTypes; } -export function getGeoFields(fields: IFieldType[]): IFieldType[] { +export function getGeoFields(fields: IndexPatternField[]): IndexPatternField[] { return fields.filter((field) => { return !indexPatterns.isNestedField(field) && ES_GEO_FIELD_TYPES.includes(field.type); }); } -export function getGeoPointFields(fields: IFieldType[]): IFieldType[] { +export function getGeoPointFields(fields: IndexPatternField[]): IndexPatternField[] { return fields.filter((field) => { return !indexPatterns.isNestedField(field) && ES_GEO_FIELD_TYPE.GEO_POINT === field.type; }); } -export function getFieldsWithGeoTileAgg(fields: IFieldType[]): IFieldType[] { +export function getFieldsWithGeoTileAgg(fields: IndexPatternField[]): IndexPatternField[] { return fields.filter(supportsGeoTileAgg); } -export function supportsGeoTileAgg(field?: IFieldType): boolean { +export function supportsGeoTileAgg(field?: IndexPatternField): boolean { return ( !!field && !!field.aggregatable && @@ -95,7 +95,7 @@ export function supportsGeoTileAgg(field?: IFieldType): boolean { ); } -export function getSourceFields(fields: IFieldType[]): IFieldType[] { +export function getSourceFields(fields: IndexPatternField[]): IndexPatternField[] { return fields.filter((field) => { // Multi fields are not stored in _source and only exist in index. const isMultiField = field.subType && field.subType.multi; diff --git a/x-pack/plugins/maps/public/kibana_services.ts b/x-pack/plugins/maps/public/kibana_services.ts index c4340a051a07..300fe07a841e 100644 --- a/x-pack/plugins/maps/public/kibana_services.ts +++ b/x-pack/plugins/maps/public/kibana_services.ts @@ -77,14 +77,6 @@ export const getEMSSettings = () => { export const getEmsTileLayerId = () => getKibanaCommonConfig().emsTileLayerId; -export const getRegionmapLayers = () => { - const config = getKibanaCommonConfig(); - if (config.regionmap && config.regionmap.layers) { - return config.regionmap.layers; - } else { - return []; - } -}; export const getTilemap = () => { const config = getKibanaCommonConfig(); if (config.tilemap) { diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts index 788e5938ee16..7157b145f828 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts @@ -70,36 +70,39 @@ export async function lazyLoadMapModules(): Promise { return loadModulesPromise; } - loadModulesPromise = new Promise(async (resolve) => { - const { - MapEmbeddable, - getIndexPatternService, - getMapsCapabilities, - renderApp, - createSecurityLayerDescriptors, - registerLayerWizard, - registerSource, - createTileMapLayerDescriptor, - createRegionMapLayerDescriptor, - createBasemapLayerDescriptor, - createESSearchSourceLayerDescriptor, - suggestEMSTermJoinConfig, - } = await import('./lazy'); - - resolve({ - MapEmbeddable, - getIndexPatternService, - getMapsCapabilities, - renderApp, - createSecurityLayerDescriptors, - registerLayerWizard, - registerSource, - createTileMapLayerDescriptor, - createRegionMapLayerDescriptor, - createBasemapLayerDescriptor, - createESSearchSourceLayerDescriptor, - suggestEMSTermJoinConfig, - }); + loadModulesPromise = new Promise(async (resolve, reject) => { + try { + const { + MapEmbeddable, + getIndexPatternService, + getMapsCapabilities, + renderApp, + createSecurityLayerDescriptors, + registerLayerWizard, + registerSource, + createTileMapLayerDescriptor, + createRegionMapLayerDescriptor, + createBasemapLayerDescriptor, + createESSearchSourceLayerDescriptor, + suggestEMSTermJoinConfig, + } = await import('./lazy'); + resolve({ + MapEmbeddable, + getIndexPatternService, + getMapsCapabilities, + renderApp, + createSecurityLayerDescriptors, + registerLayerWizard, + registerSource, + createTileMapLayerDescriptor, + createRegionMapLayerDescriptor, + createBasemapLayerDescriptor, + createESSearchSourceLayerDescriptor, + suggestEMSTermJoinConfig, + }); + } catch (error) { + reject(error); + } }); return loadModulesPromise; } diff --git a/x-pack/plugins/maps/public/reducers/map/map.ts b/x-pack/plugins/maps/public/reducers/map/map.ts index de74adf55ba9..30db4ef8120a 100644 --- a/x-pack/plugins/maps/public/reducers/map/map.ts +++ b/x-pack/plugins/maps/public/reducers/map/map.ts @@ -218,13 +218,7 @@ export function map(state: MapState = DEFAULT_MAP_STATE, action: Record & { scrollZoom: boolean; - buffer?: MapExtent; - extent?: MapExtent; mouseCoordinates?: { lat: number; lon: number; diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_layers_from_url_param.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_layers_from_url_param.ts index 712a6a1cae79..4dc4cbccb2f9 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_layers_from_url_param.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_layers_from_url_param.ts @@ -11,7 +11,6 @@ import '../../../classes/sources/wms_source'; import '../../../classes/sources/ems_file_source'; import '../../../classes/sources/es_search_source'; import '../../../classes/sources/es_pew_pew_source'; -import '../../../classes/sources/kibana_regionmap_source'; import '../../../classes/sources/es_geo_grid_source'; import '../../../classes/sources/xyz_tms_source'; import { getToasts } from '../../../kibana_services'; diff --git a/x-pack/plugins/maps/server/data_indexing/get_indexes_matching_pattern.ts b/x-pack/plugins/maps/server/data_indexing/get_indexes_matching_pattern.ts index c8b55ffe2e08..e09063f99ec8 100644 --- a/x-pack/plugins/maps/server/data_indexing/get_indexes_matching_pattern.ts +++ b/x-pack/plugins/maps/server/data_indexing/get_indexes_matching_pattern.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { IScopedClusterClient } from 'kibana/server'; -import { MatchingIndexesResp } from '../../common'; +import { IScopedClusterClient, KibanaResponseFactory, Logger } from 'kibana/server'; export async function getMatchingIndexes( indexPattern: string, - { asCurrentUser }: IScopedClusterClient -): Promise { + { asCurrentUser }: IScopedClusterClient, + response: KibanaResponseFactory, + logger: Logger +) { try { const { body: indexResults } = await asCurrentUser.cat.indices({ index: indexPattern, @@ -20,14 +21,20 @@ export async function getMatchingIndexes( const matchingIndexes = indexResults .map((indexRecord) => indexRecord.index) .filter((indexName) => !!indexName); - return { - success: true, - matchingIndexes: matchingIndexes as string[], - }; + return response.ok({ body: { success: true, matchingIndexes: matchingIndexes as string[] } }); } catch (error) { - return { - success: false, - error, - }; + const errorStatusCode = error.meta?.statusCode; + if (errorStatusCode === 404) { + return response.ok({ body: { success: true, matchingIndexes: [] } }); + } else { + logger.error(error); + return response.custom({ + body: { + success: false, + message: `Error accessing indexes: ${error.meta?.body?.error?.type}`, + }, + statusCode: 200, + }); + } } } diff --git a/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts b/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts index 52dd1c56d243..baba176286ee 100644 --- a/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts +++ b/x-pack/plugins/maps/server/data_indexing/indexing_routes.ts @@ -163,19 +163,20 @@ export function initIndexingRoutes({ router.get( { - path: `${GET_MATCHING_INDEXES_PATH}/{indexPattern}`, + path: GET_MATCHING_INDEXES_PATH, validate: { - params: schema.object({ + query: schema.object({ indexPattern: schema.string(), }), }, }, async (context, request, response) => { - const result = await getMatchingIndexes( - request.params.indexPattern, - context.core.elasticsearch.client + return await getMatchingIndexes( + request.query.indexPattern, + context.core.elasticsearch.client, + response, + logger ); - return response.ok({ body: result }); } ); diff --git a/x-pack/plugins/maps/server/index.ts b/x-pack/plugins/maps/server/index.ts index 6f0b9b39c40d..b57f9ec9c29b 100644 --- a/x-pack/plugins/maps/server/index.ts +++ b/x-pack/plugins/maps/server/index.ts @@ -51,37 +51,6 @@ export const config: PluginConfigDescriptor = { }); return completeConfig; }, - ( - completeConfig: Record, - rootPath: string, - addDeprecation: AddConfigDeprecation - ) => { - if (_.get(completeConfig, 'map.regionmap') === undefined) { - return completeConfig; - } - addDeprecation({ - message: i18n.translate('xpack.maps.deprecation.regionmap.message', { - defaultMessage: 'map.regionmap is deprecated and is no longer used', - }), - correctiveActions: { - manualSteps: [ - i18n.translate('xpack.maps.deprecation.regionmap.step1', { - defaultMessage: - 'Remove "map.regionmap" in the Kibana config file, CLI flag, or environment variable (in Docker only).', - }), - i18n.translate('xpack.maps.deprecation.regionmap.step2', { - defaultMessage: - 'Use "Upload GeoJSON" to upload each layer defined by "map.regionmap.layers".', - }), - i18n.translate('xpack.maps.deprecation.regionmap.step3', { - defaultMessage: - 'Update all maps with "Configured GeoJSON" layers. Use Choropleth layer wizard to build a replacement layer. Delete "Configured GeoJSON" layer from your map.', - }), - ], - }, - }); - return completeConfig; - }, ], }; diff --git a/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts b/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts index ee2b81716ca6..7dcfea6f267b 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/collectors/register.ts @@ -127,18 +127,6 @@ export function registerMapsUsageCollector(usageCollection: UsageCollectionSetup _meta: { description: 'total number of es track layers in cluster' }, }, }, - kbn_region: { - min: { type: 'long', _meta: { description: 'min number of kbn region layers per map' } }, - max: { type: 'long', _meta: { description: 'max number of kbn region layers per map' } }, - avg: { - type: 'float', - _meta: { description: 'avg number of kbn region layers per map' }, - }, - total: { - type: 'long', - _meta: { description: 'total number of kbn region layers in cluster' }, - }, - }, kbn_tms_raster: { min: { type: 'long', _meta: { description: 'min number of kbn tms layers per map' } }, max: { type: 'long', _meta: { description: 'max number of kbn tms layers per map' } }, diff --git a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts index b345c427b50b..5041cb997ff5 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/maps_telemetry.ts @@ -7,7 +7,7 @@ import _ from 'lodash'; import { SavedObject } from 'kibana/server'; -import { IFieldType } from 'src/plugins/data/public'; +import type { IndexPatternField } from 'src/plugins/data/public'; import { ES_GEO_FIELD_TYPE, LAYER_TYPE, @@ -136,7 +136,8 @@ async function isFieldGeoShape( return false; } return indexPattern.fields.some( - (fieldDescriptor: IFieldType) => fieldDescriptor.name && fieldDescriptor.name === geoField! + (fieldDescriptor: IndexPatternField) => + fieldDescriptor.name && fieldDescriptor.name === geoField! ); } diff --git a/x-pack/plugins/maps/server/maps_telemetry/util.ts b/x-pack/plugins/maps/server/maps_telemetry/util.ts index 24d211de659f..844486db89ab 100644 --- a/x-pack/plugins/maps/server/maps_telemetry/util.ts +++ b/x-pack/plugins/maps/server/maps_telemetry/util.ts @@ -29,7 +29,6 @@ export enum TELEMETRY_LAYER_TYPE { ES_AGG_HEATMAP = 'es_agg_heatmap', EMS_REGION = 'ems_region', EMS_BASEMAP = 'ems_basemap', - KBN_REGION = 'kbn_region', KBN_TMS_RASTER = 'kbn_tms_raster', UX_TMS_RASTER = 'ux_tms_raster', // configured in the UX layer wizard of Maps UX_TMS_MVT = 'ux_tms_mvt', // configured in the UX layer wizard of Maps @@ -111,10 +110,6 @@ export function getTelemetryLayerType( return TELEMETRY_LAYER_TYPE.KBN_TMS_RASTER; } - if (layerDescriptor.sourceDescriptor.type === SOURCE_TYPES.REGIONMAP_FILE) { - return TELEMETRY_LAYER_TYPE.KBN_REGION; - } - if (layerDescriptor.sourceDescriptor.type === SOURCE_TYPES.EMS_XYZ) { return TELEMETRY_LAYER_TYPE.UX_TMS_RASTER; } diff --git a/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/checkbox_showcharts.tsx b/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/checkbox_showcharts.tsx index 2fa81504f93c..73eb91ffd30a 100644 --- a/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/checkbox_showcharts.tsx +++ b/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/checkbox_showcharts.tsx @@ -8,34 +8,22 @@ import React, { FC, useCallback, useMemo } from 'react'; import { EuiCheckbox, htmlIdGenerator } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { useExplorerUrlState } from '../../../explorer/hooks/use_explorer_url_state'; -const SHOW_CHARTS_DEFAULT = true; - -export const useShowCharts = (): [boolean, (v: boolean) => void] => { - const [explorerUrlState, setExplorerUrlState] = useExplorerUrlState(); - - const showCharts = explorerUrlState?.mlShowCharts ?? SHOW_CHARTS_DEFAULT; - - const setShowCharts = useCallback( - (v: boolean) => { - setExplorerUrlState({ mlShowCharts: v }); - }, - [setExplorerUrlState] - ); - - return [showCharts, setShowCharts]; -}; +export interface CheckboxShowChartsProps { + showCharts: boolean; + setShowCharts: (update: boolean) => void; +} /* * React component for a checkbox element to toggle charts display. */ -export const CheckboxShowCharts: FC = () => { - const [showCharts, setShowCharts] = useShowCharts(); - - const onChange = (e: React.ChangeEvent) => { - setShowCharts(e.target.checked); - }; +export const CheckboxShowCharts: FC = ({ showCharts, setShowCharts }) => { + const onChange = useCallback( + (e: React.ChangeEvent) => { + setShowCharts(e.target.checked); + }, + [setShowCharts] + ); const id = useMemo(() => htmlIdGenerator()(), []); diff --git a/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/index.ts b/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/index.ts index 3ff95bf6e335..2099abb16828 100644 --- a/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/index.ts +++ b/x-pack/plugins/ml/public/application/components/controls/checkbox_showcharts/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { useShowCharts, CheckboxShowCharts } from './checkbox_showcharts'; +export { CheckboxShowCharts } from './checkbox_showcharts'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index ce91900f11e1..c02539fabfe4 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -13,7 +13,7 @@ import { extractErrorMessage } from '../../../../../../../common/util/errors'; import { DeepReadonly } from '../../../../../../../common/types/common'; import { ml } from '../../../../../services/ml_api_service'; import { useMlContext } from '../../../../../contexts/ml'; -import { DuplicateIndexPatternError } from '../../../../../../../../../../src/plugins/data/public'; +import { DuplicateDataViewError } from '../../../../../../../../../../src/plugins/data/public'; import { useRefreshAnalyticsList, DataFrameAnalyticsConfig } from '../../../../common'; import { extractCloningConfig, isAdvancedConfig } from '../../components/action_clone'; @@ -145,7 +145,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { ), }); } catch (e) { - if (e instanceof DuplicateIndexPatternError) { + if (e instanceof DuplicateDataViewError) { addRequestMessage({ error: i18n.translate( 'xpack.ml.dataframe.analytics.create.duplicateIndexPatternErrorMessageError', diff --git a/x-pack/plugins/ml/public/application/explorer/explorer.js b/x-pack/plugins/ml/public/application/explorer/explorer.js index c9365c4edbe5..daecf7585b3e 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer.js @@ -498,7 +498,10 @@ export class ExplorerUI extends React.Component { {chartsData.seriesToPlot.length > 0 && selectedCells !== undefined && ( - + )} diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts index d737c4733b9c..cd01de31e5e6 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_constants.ts @@ -34,6 +34,7 @@ export const EXPLORER_ACTION = { SET_VIEW_BY_PER_PAGE: 'setViewByPerPage', SET_VIEW_BY_FROM_PAGE: 'setViewByFromPage', SET_SWIM_LANE_SEVERITY: 'setSwimLaneSeverity', + SET_SHOW_CHARTS: 'setShowCharts', }; export const FILTER_ACTION = { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts index f858c40b3231..1d4a277af013 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts +++ b/x-pack/plugins/ml/public/application/explorer/explorer_dashboard_service.ts @@ -83,6 +83,10 @@ const explorerAppState$: Observable = explorerState$.pipe( appState.mlExplorerSwimlane.severity = state.swimLaneSeverity; } + if (state.showCharts !== undefined) { + appState.mlShowCharts = state.showCharts; + } + if (state.filterActive) { appState.mlExplorerFilter.influencersFilterQuery = state.influencersFilterQuery; appState.mlExplorerFilter.filterActive = state.filterActive; @@ -168,6 +172,9 @@ export const explorerService = { setSwimLaneSeverity: (payload: number) => { explorerAction$.next({ type: EXPLORER_ACTION.SET_SWIM_LANE_SEVERITY, payload }); }, + setShowCharts: (payload: boolean) => { + explorerAction$.next({ type: EXPLORER_ACTION.SET_SHOW_CHARTS, payload }); + }, }; export type ExplorerService = typeof explorerService; diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts index 74867af5f898..192699afc2cf 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/reducer.ts @@ -158,6 +158,13 @@ export const explorerReducer = (state: ExplorerState, nextAction: Action): Explo }; break; + case EXPLORER_ACTION.SET_SHOW_CHARTS: + nextState = { + ...state, + showCharts: payload, + }; + break; + default: nextState = state; } diff --git a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts index a06db20210c1..202a4389ef52 100644 --- a/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts +++ b/x-pack/plugins/ml/public/application/explorer/reducers/explorer_reducer/state.ts @@ -59,6 +59,7 @@ export interface ExplorerState { viewBySwimlaneOptions: string[]; swimlaneLimit?: number; swimLaneSeverity?: number; + showCharts: boolean; } function getDefaultIndexPattern() { @@ -112,5 +113,6 @@ export function getExplorerDefaultState(): ExplorerState { viewByPerPage: SWIM_LANE_DEFAULT_PAGE_SIZE, viewByFromPage: 1, swimlaneLimit: undefined, + showCharts: true, }; } diff --git a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx index 42927d9b4ef5..49e7857eee08 100644 --- a/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx +++ b/x-pack/plugins/ml/public/application/routing/routes/explorer.tsx @@ -26,7 +26,6 @@ import { useExplorerData } from '../../explorer/actions'; import { explorerService } from '../../explorer/explorer_dashboard_service'; import { getDateFormatTz } from '../../explorer/explorer_utils'; import { useJobSelection } from '../../components/job_selector/use_job_selection'; -import { useShowCharts } from '../../components/controls/checkbox_showcharts'; import { useTableInterval } from '../../components/controls/select_interval'; import { useTableSeverity } from '../../components/controls/select_severity'; import { useUrlState } from '../../util/url_state'; @@ -196,6 +195,10 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim if (severity !== undefined) { explorerService.setSwimLaneSeverity(severity); } + + if (explorerUrlState.mlShowCharts !== undefined) { + explorerService.setShowCharts(explorerUrlState.mlShowCharts); + } }, []); /** Sync URL state with {@link explorerService} state */ @@ -214,7 +217,6 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim } }, [explorerData]); - const [showCharts] = useShowCharts(); const [tableInterval] = useTableInterval(); const [tableSeverity] = useTableSeverity(); @@ -267,7 +269,11 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim } }, [JSON.stringify(loadExplorerDataConfig), selectedCells?.showTopFieldValues]); - if (explorerState === undefined || refresh === undefined || showCharts === undefined) { + if ( + explorerState === undefined || + refresh === undefined || + explorerAppState?.mlShowCharts === undefined + ) { return null; } @@ -277,7 +283,7 @@ const ExplorerUrlStateManager: FC = ({ jobsWithTim {...{ explorerState, setSelectedCells, - showCharts, + showCharts: explorerState.showCharts, severity: tableSeverity.val, stoppedPartitions, invalidTimeRangeError, diff --git a/x-pack/plugins/ml/public/application/services/new_job_capabilities/load_new_job_capabilities.ts b/x-pack/plugins/ml/public/application/services/new_job_capabilities/load_new_job_capabilities.ts index a99834353524..d3b407c2bb65 100644 --- a/x-pack/plugins/ml/public/application/services/new_job_capabilities/load_new_job_capabilities.ts +++ b/x-pack/plugins/ml/public/application/services/new_job_capabilities/load_new_job_capabilities.ts @@ -23,27 +23,34 @@ export function loadNewJobCapabilities( jobType: JobType ) { return new Promise(async (resolve, reject) => { - const serviceToUse = - jobType === ANOMALY_DETECTOR ? newJobCapsService : newJobCapsServiceAnalytics; - if (indexPatternId !== undefined) { - // index pattern is being used - const indexPattern: IIndexPattern = await indexPatterns.get(indexPatternId); - await serviceToUse.initializeFromIndexPattern(indexPattern); - resolve(serviceToUse.newJobCaps); - } else if (savedSearchId !== undefined) { - // saved search is being used - // load the index pattern from the saved search - const { indexPattern } = await getIndexPatternAndSavedSearch(savedSearchId); - if (indexPattern === null) { - // eslint-disable-next-line no-console - console.error('Cannot retrieve index pattern from saved search'); + try { + const serviceToUse = + jobType === ANOMALY_DETECTOR ? newJobCapsService : newJobCapsServiceAnalytics; + + if (indexPatternId !== undefined) { + // index pattern is being used + const indexPattern: IIndexPattern = await indexPatterns.get(indexPatternId); + await serviceToUse.initializeFromIndexPattern(indexPattern); + resolve(serviceToUse.newJobCaps); + } else if (savedSearchId !== undefined) { + // saved search is being used + // load the index pattern from the saved search + const { indexPattern } = await getIndexPatternAndSavedSearch(savedSearchId); + + if (indexPattern === null) { + // eslint-disable-next-line no-console + console.error('Cannot retrieve index pattern from saved search'); + reject(); + return; + } + + await serviceToUse.initializeFromIndexPattern(indexPattern); + resolve(serviceToUse.newJobCaps); + } else { reject(); - return; } - await serviceToUse.initializeFromIndexPattern(indexPattern); - resolve(serviceToUse.newJobCaps); - } else { - reject(); + } catch (error) { + reject(error); } }); } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx index eb39ba4ab29a..5090274ca738 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_charts/anomaly_charts_setup_flyout.tsx @@ -25,33 +25,34 @@ export async function resolveEmbeddableAnomalyChartsUserInput( const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http)); return new Promise(async (resolve, reject) => { - const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds); - - const title = input?.title ?? getDefaultExplorerChartsPanelTitle(jobIds); - const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise(); - const influencers = anomalyDetectorService.extractInfluencers(jobs); - influencers.push(VIEW_BY_JOB_LABEL); - - const modalSession = overlays.openModal( - toMountPoint( - { - modalSession.close(); - - resolve({ - jobIds, - title: panelTitle, - maxSeriesToPlot, - }); - }} - onCancel={() => { - modalSession.close(); - reject(); - }} - /> - ) - ); + try { + const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds); + const title = input?.title ?? getDefaultExplorerChartsPanelTitle(jobIds); + const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise(); + const influencers = anomalyDetectorService.extractInfluencers(jobs); + influencers.push(VIEW_BY_JOB_LABEL); + const modalSession = overlays.openModal( + toMountPoint( + { + modalSession.close(); + resolve({ + jobIds, + title: panelTitle, + maxSeriesToPlot, + }); + }} + onCancel={() => { + modalSession.close(); + reject(); + }} + /> + ) + ); + } catch (error) { + reject(error); + } }); } diff --git a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx index e183907def57..5027eb6783a6 100644 --- a/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx +++ b/x-pack/plugins/ml/public/embeddables/anomaly_swimlane/anomaly_swimlane_setup_flyout.tsx @@ -25,31 +25,36 @@ export async function resolveAnomalySwimlaneUserInput( const anomalyDetectorService = new AnomalyDetectorService(new HttpService(http)); return new Promise(async (resolve, reject) => { - const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds); - - const title = input?.title ?? getDefaultSwimlanePanelTitle(jobIds); - - const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise(); - - const influencers = anomalyDetectorService.extractInfluencers(jobs); - influencers.push(VIEW_BY_JOB_LABEL); - - const modalSession = overlays.openModal( - toMountPoint( - { - modalSession.close(); - resolve({ jobIds, title: panelTitle, swimlaneType, viewBy }); - }} - onCancel={() => { - modalSession.close(); - reject(); - }} - /> - ) - ); + try { + const { jobIds } = await resolveJobSelection(coreStart, input?.jobIds); + const title = input?.title ?? getDefaultSwimlanePanelTitle(jobIds); + const jobs = await anomalyDetectorService.getJobs$(jobIds).toPromise(); + const influencers = anomalyDetectorService.extractInfluencers(jobs); + influencers.push(VIEW_BY_JOB_LABEL); + const modalSession = overlays.openModal( + toMountPoint( + { + modalSession.close(); + resolve({ + jobIds, + title: panelTitle, + swimlaneType, + viewBy, + }); + }} + onCancel={() => { + modalSession.close(); + reject(); + }} + /> + ) + ); + } catch (error) { + reject(error); + } }); } diff --git a/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx index 183388344785..fbceeb7f7cf7 100644 --- a/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx +++ b/x-pack/plugins/ml/public/embeddables/common/resolve_job_selection.tsx @@ -38,56 +38,65 @@ export async function resolveJobSelection( } = coreStart; return new Promise(async (resolve, reject) => { - const maps = { - groupsMap: getInitialGroupsMap([]), - jobsMap: {}, - }; + try { + const maps = { + groupsMap: getInitialGroupsMap([]), + jobsMap: {}, + }; + const tzConfig = uiSettings.get('dateFormat:tz'); + const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess(); - const tzConfig = uiSettings.get('dateFormat:tz'); - const dateFormatTz = tzConfig !== 'Browser' ? tzConfig : moment.tz.guess(); + const onFlyoutClose = () => { + flyoutSession.close(); + reject(); + }; - const onFlyoutClose = () => { - flyoutSession.close(); - reject(); - }; + const onSelectionConfirmed = async ({ + jobIds, + groups, + }: { + jobIds: string[]; + groups: Array<{ + groupId: string; + jobIds: string[]; + }>; + }) => { + await flyoutSession.close(); + resolve({ + jobIds, + groups, + }); + }; - const onSelectionConfirmed = async ({ - jobIds, - groups, - }: { - jobIds: string[]; - groups: Array<{ groupId: string; jobIds: string[] }>; - }) => { - await flyoutSession.close(); - resolve({ jobIds, groups }); - }; - const flyoutSession = coreStart.overlays.openFlyout( - toMountPoint( - - - - ), - { - 'data-test-subj': 'mlFlyoutJobSelector', - ownFocus: true, - closeButtonAriaLabel: 'jobSelectorFlyout', - } - ); + const flyoutSession = coreStart.overlays.openFlyout( + toMountPoint( + + + + ), + { + 'data-test-subj': 'mlFlyoutJobSelector', + ownFocus: true, + closeButtonAriaLabel: 'jobSelectorFlyout', + } + ); // Close the flyout when user navigates out of the dashboard plugin - // Close the flyout when user navigates out of the dashboard plugin - currentAppId$.pipe(takeUntil(from(flyoutSession.onClose))).subscribe((appId) => { - if (appId !== DashboardConstants.DASHBOARDS_ID) { - flyoutSession.close(); - } - }); + currentAppId$.pipe(takeUntil(from(flyoutSession.onClose))).subscribe((appId) => { + if (appId !== DashboardConstants.DASHBOARDS_ID) { + flyoutSession.close(); + } + }); + } catch (error) { + reject(error); + } }); } diff --git a/x-pack/plugins/monitoring/public/application/external_config_context.tsx b/x-pack/plugins/monitoring/public/application/external_config_context.tsx new file mode 100644 index 000000000000..e710032ff1ae --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/external_config_context.tsx @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { createContext } from 'react'; + +export interface ExternalConfig { + minIntervalSeconds: number; + showLicenseExpiration: boolean; + showCgroupMetricsElasticsearch: boolean; + showCgroupMetricsLogstash: boolean; + renderReactApp: boolean; +} + +export const ExternalConfigContext = createContext({} as ExternalConfig); diff --git a/x-pack/plugins/monitoring/public/application/global_state_context.tsx b/x-pack/plugins/monitoring/public/application/global_state_context.tsx index e6e18e279bba..dc33316dbd9d 100644 --- a/x-pack/plugins/monitoring/public/application/global_state_context.tsx +++ b/x-pack/plugins/monitoring/public/application/global_state_context.tsx @@ -11,16 +11,20 @@ import { MonitoringStartPluginDependencies } from '../types'; interface GlobalStateProviderProps { query: MonitoringStartPluginDependencies['data']['query']; toasts: MonitoringStartPluginDependencies['core']['notifications']['toasts']; - children: React.ReactNode; } interface State { cluster_uuid?: string; + ccs?: any; } export const GlobalStateContext = createContext({} as State); -export const GlobalStateProvider = ({ query, toasts, children }: GlobalStateProviderProps) => { +export const GlobalStateProvider: React.FC = ({ + query, + toasts, + children, +}) => { // TODO: remove fakeAngularRootScope and fakeAngularLocation when angular is removed const fakeAngularRootScope: Partial = { $on: ( diff --git a/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts b/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts index b970d8c84b5b..e11317fd92bd 100644 --- a/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts +++ b/x-pack/plugins/monitoring/public/application/hooks/use_clusters.ts @@ -15,7 +15,7 @@ export function useClusters(clusterUuid?: string | null, ccs?: any, codePaths?: const [min] = useState(bounds.min.toISOString()); const [max] = useState(bounds.max.toISOString()); - const [clusters, setClusters] = useState([]); + const [clusters, setClusters] = useState([] as any); const [loaded, setLoaded] = useState(false); let url = '../api/monitoring/v1/clusters'; diff --git a/x-pack/plugins/monitoring/public/application/index.tsx b/x-pack/plugins/monitoring/public/application/index.tsx index ed74d342f7a8..e15ad995ca16 100644 --- a/x-pack/plugins/monitoring/public/application/index.tsx +++ b/x-pack/plugins/monitoring/public/application/index.tsx @@ -11,17 +11,24 @@ import ReactDOM from 'react-dom'; import { Route, Switch, Redirect, Router } from 'react-router-dom'; import { KibanaContextProvider } from '../../../../../src/plugins/kibana_react/public'; import { LoadingPage } from './pages/loading_page'; +import { ClusterOverview } from './pages/cluster/overview_page'; import { MonitoringStartPluginDependencies } from '../types'; import { GlobalStateProvider } from './global_state_context'; +import { ExternalConfigContext, ExternalConfig } from './external_config_context'; import { createPreserveQueryHistory } from './preserve_query_history'; import { RouteInit } from './route_init'; +import { MonitoringTimeContainer } from './pages/use_monitoring_time'; export const renderApp = ( core: CoreStart, plugins: MonitoringStartPluginDependencies, - { element }: AppMountParameters + { element }: AppMountParameters, + externalConfig: ExternalConfig ) => { - ReactDOM.render(, element); + ReactDOM.render( + , + element + ); return () => { ReactDOM.unmountComponentAtNode(element); @@ -31,38 +38,48 @@ export const renderApp = ( const MonitoringApp: React.FC<{ core: CoreStart; plugins: MonitoringStartPluginDependencies; -}> = ({ core, plugins }) => { + externalConfig: ExternalConfig; +}> = ({ core, plugins, externalConfig }) => { const history = createPreserveQueryHistory(); return ( - - - - - - - - - - - - + + + + + + + + + + + + + + + + ); }; @@ -75,10 +92,6 @@ const Home: React.FC<{}> = () => { return
Home page (Cluster listing)
; }; -const ClusterOverview: React.FC<{}> = () => { - return
Cluster overview page
; -}; - const License: React.FC<{}> = () => { return
License page
; }; diff --git a/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx new file mode 100644 index 000000000000..ddc097caea57 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/cluster/overview_page.tsx @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useContext } from 'react'; +import { i18n } from '@kbn/i18n'; +import { CODE_PATH_ALL } from '../../../../common/constants'; +import { PageTemplate } from '../page_template'; +import { useClusters } from '../../hooks/use_clusters'; +import { GlobalStateContext } from '../../global_state_context'; +import { TabMenuItem } from '../page_template'; +import { PageLoading } from '../../../components'; +import { Overview } from '../../../components/cluster/overview'; +import { ExternalConfigContext } from '../../external_config_context'; + +const CODE_PATHS = [CODE_PATH_ALL]; + +export const ClusterOverview: React.FC<{}> = () => { + // TODO: check how many requests with useClusters + const state = useContext(GlobalStateContext); + const externalConfig = useContext(ExternalConfigContext); + const { clusters, loaded } = useClusters(state.cluster_uuid, state.ccs, CODE_PATHS); + let tabs: TabMenuItem[] = []; + + const title = i18n.translate('xpack.monitoring.cluster.overviewTitle', { + defaultMessage: 'Overview', + }); + + const pageTitle = i18n.translate('xpack.monitoring.cluster.overview.pageTitle', { + defaultMessage: 'Cluster overview', + }); + + if (loaded) { + tabs = [ + { + id: 'clusterName', + label: clusters[0].cluster_name, + disabled: false, + description: clusters[0].cluster_name, + onClick: () => {}, + testSubj: 'clusterName', + }, + ]; + } + + return ( + + {loaded ? ( + + ) : ( + + )} + + ); +}; diff --git a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx index fb766af6c8cb..f40c2d3ec5e5 100644 --- a/x-pack/plugins/monitoring/public/application/pages/page_template.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/page_template.tsx @@ -5,16 +5,76 @@ * 2.0. */ +import { EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs, EuiTitle } from '@elastic/eui'; import React from 'react'; import { useTitle } from '../hooks/use_title'; +import { MonitoringToolbar } from '../../components/shared/toolbar'; +export interface TabMenuItem { + id: string; + label: string; + description: string; + disabled: boolean; + onClick: () => void; + testSubj: string; +} interface PageTemplateProps { title: string; - children: React.ReactNode; + pageTitle?: string; + tabs?: TabMenuItem[]; } -export const PageTemplate = ({ title, children }: PageTemplateProps) => { +export const PageTemplate: React.FC = ({ title, pageTitle, tabs, children }) => { useTitle('', title); - return
{children}
; + return ( +
+ + + + +
{/* HERE GOES THE SETUP BUTTON */}
+
+ + {pageTitle && ( +
+ +

{pageTitle}

+
+
+ )} +
+
+
+ + + + +
+ + {tabs && ( + + {tabs.map((item, idx) => { + return ( + + {item.label} + + ); + })} + + )} +
{children}
+
+ ); }; diff --git a/x-pack/plugins/monitoring/public/application/pages/use_monitoring_time.tsx b/x-pack/plugins/monitoring/public/application/pages/use_monitoring_time.tsx new file mode 100644 index 000000000000..f54d40ed29a0 --- /dev/null +++ b/x-pack/plugins/monitoring/public/application/pages/use_monitoring_time.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useCallback, useState } from 'react'; +import createContainer from 'constate'; + +interface TimeOptions { + from: string; + to: string; + interval: string; +} + +export const DEFAULT_TIMERANGE: TimeOptions = { + from: 'now-1h', + to: 'now', + interval: '>=10s', +}; + +export const useMonitoringTime = () => { + const defaultTimeRange = { + from: 'now-1h', + to: 'now', + interval: DEFAULT_TIMERANGE.interval, + }; + const [refreshInterval, setRefreshInterval] = useState(5000); + const [isPaused, setIsPaused] = useState(false); + const [currentTimerange, setTimeRange] = useState(defaultTimeRange); + + const handleTimeChange = useCallback( + (start: string, end: string) => { + setTimeRange({ ...currentTimerange, from: start, to: end }); + }, + [currentTimerange, setTimeRange] + ); + + return { + currentTimerange, + setTimeRange, + handleTimeChange, + setRefreshInterval, + refreshInterval, + setIsPaused, + isPaused, + }; +}; + +export const MonitoringTimeContainer = createContainer(useMonitoringTime); diff --git a/x-pack/plugins/monitoring/public/components/cluster/overview/index.d.ts b/x-pack/plugins/monitoring/public/components/cluster/overview/index.d.ts new file mode 100644 index 000000000000..2cfd37e8e27e --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/cluster/overview/index.d.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const Overview: FunctionComponent; diff --git a/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx b/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx new file mode 100644 index 000000000000..6e45d4d831ec --- /dev/null +++ b/x-pack/plugins/monitoring/public/components/shared/toolbar.tsx @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EuiFlexGroup, EuiFlexItem, EuiSuperDatePicker, OnRefreshChangeProps } from '@elastic/eui'; +import React, { useContext, useCallback } from 'react'; +import { MonitoringTimeContainer } from '../../application/pages/use_monitoring_time'; + +export const MonitoringToolbar = () => { + const { + currentTimerange, + handleTimeChange, + setRefreshInterval, + refreshInterval, + setIsPaused, + isPaused, + } = useContext(MonitoringTimeContainer.Context); + + const onTimeChange = useCallback( + (selectedTime: { start: string; end: string; isInvalid: boolean }) => { + if (selectedTime.isInvalid) { + return; + } + handleTimeChange(selectedTime.start, selectedTime.end); + }, + [handleTimeChange] + ); + + const onRefreshChange = useCallback( + ({ refreshInterval: ri, isPaused: isP }: OnRefreshChangeProps) => { + setRefreshInterval(ri); + setIsPaused(isP); + }, + [setRefreshInterval, setIsPaused] + ); + + return ( + + Setup Button + + {}} + isPaused={isPaused} + refreshInterval={refreshInterval} + onRefreshChange={onRefreshChange} + /> + + + ); +}; diff --git a/x-pack/plugins/monitoring/public/plugin.ts b/x-pack/plugins/monitoring/public/plugin.ts index f1ab86dbad76..6884dba760fc 100644 --- a/x-pack/plugins/monitoring/public/plugin.ts +++ b/x-pack/plugins/monitoring/public/plugin.ts @@ -127,7 +127,7 @@ export class MonitoringPlugin const config = Object.fromEntries(externalConfig); if (config.renderReactApp) { const { renderApp } = await import('./application'); - return renderApp(coreStart, pluginsStart, params); + return renderApp(coreStart, pluginsStart, params, config); } else { const monitoringApp = new AngularApp(deps); const removeHistoryListener = params.history.listen((location) => { diff --git a/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.js b/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.js index 32662ae0efa3..a4645edda73d 100644 --- a/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.js +++ b/x-pack/plugins/monitoring/server/lib/logstash/get_paginated_pipelines.js @@ -98,27 +98,39 @@ async function getPaginatedThroughputData(pipelines, req, lsIndexPattern, throug const metricSeriesData = Object.values( await Promise.all( pipelines.map((pipeline) => { - return new Promise(async (resolve) => { - const data = await getMetrics( - req, - lsIndexPattern, - [throughputMetric], - [ - { - bool: { - should: [ - { term: { type: 'logstash_stats' } }, - { term: { 'metricset.name': 'stats' } }, - ], + return new Promise(async (resolve, reject) => { + try { + const data = await getMetrics( + req, + lsIndexPattern, + [throughputMetric], + [ + { + bool: { + should: [ + { + term: { + type: 'logstash_stats', + }, + }, + { + term: { + 'metricset.name': 'stats', + }, + }, + ], + }, }, + ], + { + pipeline, }, - ], - { - pipeline, - }, - 2 - ); - resolve(reduceData(pipeline, data)); + 2 + ); + resolve(reduceData(pipeline, data)); + } catch (error) { + reject(error); + } }); }) ) @@ -184,27 +196,38 @@ async function getPipelines(req, lsIndexPattern, pipelines, throughputMetric, no async function getThroughputPipelines(req, lsIndexPattern, pipelines, throughputMetric) { const metricsResponse = await Promise.all( pipelines.map((pipeline) => { - return new Promise(async (resolve) => { - const data = await getMetrics( - req, - lsIndexPattern, - [throughputMetric], - [ - { - bool: { - should: [ - { term: { type: 'logstash_stats' } }, - { term: { 'metricset.name': 'stats' } }, - ], + return new Promise(async (resolve, reject) => { + try { + const data = await getMetrics( + req, + lsIndexPattern, + [throughputMetric], + [ + { + bool: { + should: [ + { + term: { + type: 'logstash_stats', + }, + }, + { + term: { + 'metricset.name': 'stats', + }, + }, + ], + }, }, - }, - ], - { - pipeline, - } - ); - - resolve(reduceData(pipeline, data)); + ], + { + pipeline, + } + ); + resolve(reduceData(pipeline, data)); + } catch (error) { + reject(error); + } }); }) ); diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx index f92f12c79a56..dc3db695a3fb 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.test.tsx @@ -10,16 +10,13 @@ import { mount } from 'enzyme'; import { EuiThemeProvider } from '../../../../../../../../src/plugins/kibana_react/common'; import { CreateCaseFlyout } from './flyout'; +import { render } from '@testing-library/react'; + +import { useKibana } from '../../../../utils/kibana_react'; +import { CASES_OWNER } from '../constants'; + +jest.mock('../../../../utils/kibana_react'); -jest.mock('../../../../utils/kibana_react', () => ({ - useKibana: () => ({ - services: { - cases: { - getCreateCase: jest.fn(), - }, - }, - }), -})); const onCloseFlyout = jest.fn(); const onSuccess = jest.fn(); const defaultProps = { @@ -28,8 +25,17 @@ const defaultProps = { }; describe('CreateCaseFlyout', () => { + const mockCreateCase = jest.fn(); + beforeEach(() => { jest.resetAllMocks(); + (useKibana as jest.Mock).mockReturnValue({ + services: { + cases: { + getCreateCase: mockCreateCase, + }, + }, + }); }); it('renders', () => { @@ -52,4 +58,22 @@ describe('CreateCaseFlyout', () => { wrapper.find(`[data-test-subj='euiFlyoutCloseButton']`).first().simulate('click'); expect(onCloseFlyout).toBeCalled(); }); + + it('does not show the sync alerts toggle', () => { + render( + + + + ); + + expect(mockCreateCase).toBeCalledTimes(1); + expect(mockCreateCase).toBeCalledWith({ + onCancel: onCloseFlyout, + onSuccess, + afterCaseCreated: undefined, + withSteps: false, + owner: [CASES_OWNER], + disableAlerts: true, + }); + }); }); diff --git a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx index df29d02e8d83..896bc27a9767 100644 --- a/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx +++ b/x-pack/plugins/observability/public/components/app/cases/create/flyout.tsx @@ -68,6 +68,7 @@ function CreateCaseFlyoutComponent({ onSuccess, withSteps: false, owner: [CASES_OWNER], + disableAlerts: true, })} diff --git a/x-pack/plugins/observability/public/hooks/use_alert_permission.ts b/x-pack/plugins/observability/public/hooks/use_alert_permission.ts index 2c2837c4bda8..d754c53f23f5 100644 --- a/x-pack/plugins/observability/public/hooks/use_alert_permission.ts +++ b/x-pack/plugins/observability/public/hooks/use_alert_permission.ts @@ -12,7 +12,7 @@ import { Capabilities } from '../../../../../src/core/types'; export interface UseGetUserAlertsPermissionsProps { crud: boolean; read: boolean; - loading: boolean; + loading?: boolean; featureId: string | null; } @@ -30,9 +30,12 @@ export const getAlertsPermissions = ( } return { - crud: uiCapabilities[featureId].save as boolean, - read: uiCapabilities[featureId].show as boolean, - loading: false, + crud: (featureId === 'apm' + ? uiCapabilities[featureId]['alerting:save'] + : uiCapabilities[featureId].save) as boolean, + read: (featureId === 'apm' + ? uiCapabilities[featureId]['alerting:show'] + : uiCapabilities[featureId].show) as boolean, featureId, }; }; diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_search_bar.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_search_bar.tsx index 01bb01857eaf..926f03acf01d 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_search_bar.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_search_bar.tsx @@ -46,7 +46,7 @@ export function AlertsSearchBar({ 75', + defaultMessage: 'Search alerts (e.g. kibana.alert.evaluation.threshold > 75)', })} query={{ query: query ?? '', language: queryLanguage }} timeHistory={timeHistory} diff --git a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx index 7cb7395acaa8..2d325b6f3f7c 100644 --- a/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/alerts_table_t_grid.tsx @@ -14,6 +14,7 @@ import { ALERT_DURATION as ALERT_DURATION_TYPED, ALERT_REASON as ALERT_REASON_TYPED, ALERT_RULE_CONSUMER, + ALERT_RULE_PRODUCER, ALERT_STATUS as ALERT_STATUS_TYPED, ALERT_WORKFLOW_STATUS as ALERT_WORKFLOW_STATUS_TYPED, } from '@kbn/rule-data-utils'; @@ -173,6 +174,9 @@ function ObservabilityActions({ const alertDataConsumer = useMemo(() => get(dataFieldEs, ALERT_RULE_CONSUMER, [''])[0], [ dataFieldEs, ]); + const alertDataProducer = useMemo(() => get(dataFieldEs, ALERT_RULE_PRODUCER, [''])[0], [ + dataFieldEs, + ]); const alert = parseObservabilityAlert(dataFieldEs); const { prepend } = core.http.basePath; @@ -204,7 +208,10 @@ function ObservabilityActions({ } }, [setActionsPopover, refetch]); - const alertPermissions = useGetUserAlertsPermissions(capabilities, alertDataConsumer); + const alertPermissions = useGetUserAlertsPermissions( + capabilities, + alertDataConsumer === 'alerts' ? alertDataProducer : alertDataConsumer + ); const statusActionItems = useStatusBulkActionItems({ eventIds: [eventId], @@ -298,8 +305,11 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { const casePermissions = useGetUserCasesPermissions(); const hasAlertsCrudPermissions = useCallback( - (featureId: string) => { - return getAlertsPermissions(capabilities, featureId).crud; + ({ ruleConsumer, ruleProducer }: { ruleConsumer: string; ruleProducer?: string }) => { + if (ruleConsumer === 'alerts' && ruleProducer) { + return getAlertsPermissions(capabilities, ruleProducer).crud; + } + return getAlertsPermissions(capabilities, ruleConsumer).crud; }, [capabilities] ); diff --git a/x-pack/plugins/observability/public/pages/alerts/index.tsx b/x-pack/plugins/observability/public/pages/alerts/index.tsx index 45a8dd842ee2..bba3b426598d 100644 --- a/x-pack/plugins/observability/public/pages/alerts/index.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/index.tsx @@ -8,7 +8,6 @@ import { EuiButtonEmpty, EuiCallOut, EuiFlexGroup, EuiFlexItem, EuiLink } from '@elastic/eui'; import { IndexPatternBase } from '@kbn/es-query'; import { i18n } from '@kbn/i18n'; -import { ALERT_STATUS, ALERT_STATUS_ACTIVE } from '@kbn/rule-data-utils'; import React, { useCallback, useRef } from 'react'; import { useHistory } from 'react-router-dom'; import useAsync from 'react-use/lib/useAsync'; @@ -43,12 +42,7 @@ export function AlertsPage({ routeParams }: AlertsPageProps) { const history = useHistory(); const refetch = useRef<() => void>(); const { - query: { - rangeFrom = 'now-15m', - rangeTo = 'now', - kuery = `${ALERT_STATUS}: "${ALERT_STATUS_ACTIVE}"`, - workflowStatus = 'open', - }, + query: { rangeFrom = 'now-15m', rangeTo = 'now', kuery = '', workflowStatus = 'open' }, } = routeParams; useBreadcrumbs([ @@ -177,13 +171,11 @@ export function AlertsPage({ routeParams }: AlertsPageProps) {

{i18n.translate('xpack.observability.alertsDisclaimerText', { defaultMessage: - 'This page shows an experimental alerting view. The data shown here will probably not be an accurate representation of alerts. A non-experimental list of alerts is available in the Alerts and Actions settings in Stack Management.', + 'This page shows an experimental list of alerts. The data might not be accurate. All alerts are available in the ', })} -

-

{i18n.translate('xpack.observability.alertsDisclaimerLinkText', { - defaultMessage: 'Alerts and Actions', + defaultMessage: 'Rules and Connectors settings.', })}

diff --git a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx index 0430c750c886..7e33b61c9b35 100644 --- a/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx +++ b/x-pack/plugins/observability/public/pages/alerts/render_cell_value.tsx @@ -93,7 +93,7 @@ export const getRenderCellValue = ({ case ALERT_STATUS_RECOVERED: return ( - + {i18n.translate('xpack.observability.alertsTGrid.statusRecoveredDescription', { defaultMessage: 'Recovered', })} diff --git a/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts b/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts index ecd7828cb828..15f1e48f1536 100644 --- a/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts +++ b/x-pack/plugins/osquery/public/agent_policies/use_agent_policy.ts @@ -36,10 +36,8 @@ export const useAgentPolicy = ({ policyId, skip, silent }: UseAgentPolicy) => { defaultMessage: 'Error while fetching agent policy details', }), }), - refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, - staleTime: Infinity, } ); }; diff --git a/x-pack/plugins/osquery/public/agents/use_agent_policy_agent_ids.ts b/x-pack/plugins/osquery/public/agents/use_agent_policy_agent_ids.ts index 42790e46e0a9..65a2520e07d0 100644 --- a/x-pack/plugins/osquery/public/agents/use_agent_policy_agent_ids.ts +++ b/x-pack/plugins/osquery/public/agents/use_agent_policy_agent_ids.ts @@ -50,7 +50,6 @@ export const useAgentPolicyAgentIds = ({ defaultMessage: 'Error while fetching agents', }), }), - refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, } diff --git a/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx b/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx index 7cc561ff7a73..1994ea348e30 100644 --- a/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx +++ b/x-pack/plugins/osquery/public/common/hooks/use_osquery_integration.tsx @@ -22,9 +22,7 @@ export const useOsqueryIntegrationStatus = () => { defaultMessage: 'Error while fetching osquery integration', }), }), - refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, - staleTime: Infinity, }); }; diff --git a/x-pack/plugins/osquery/public/common/schemas/ecs/v1.11.0.json b/x-pack/plugins/osquery/public/common/schemas/ecs/v1.11.0.json index 6d26f6000017..406999901961 100644 --- a/x-pack/plugins/osquery/public/common/schemas/ecs/v1.11.0.json +++ b/x-pack/plugins/osquery/public/common/schemas/ecs/v1.11.0.json @@ -1 +1 @@ -[{"field":"@timestamp","type":"date","description":"Date/time when the event originated."},{"field":"labels","type":"object","description":"Custom key/value pairs."},{"field":"message","type":"text","description":"Log message optimized for viewing in a log viewer."},{"field":"tags","type":"keyword","description":"List of keywords used to tag each event."},{"field":"agent.build.original","type":"keyword","description":"Extended build information for the agent."},{"field":"agent.ephemeral_id","type":"keyword","description":"Ephemeral identifier of this agent."},{"field":"agent.id","type":"keyword","description":"Unique identifier of this agent."},{"field":"agent.name","type":"keyword","description":"Custom name of the agent."},{"field":"agent.type","type":"keyword","description":"Type of the agent."},{"field":"agent.version","type":"keyword","description":"Version of the agent."},{"field":"client.address","type":"keyword","description":"Client network address."},{"field":"client.as.number","type":"long","description":"Unique number allocated to the autonomous system."},{"field":"client.as.organization.name","type":"keyword","description":"Organization name."},{"field":"client.as.organization.name.text","type":"text","description":"Organization name."},{"field":"client.bytes","type":"long","description":"Bytes sent from the client to the server."},{"field":"client.domain","type":"keyword","description":"Client domain."},{"field":"client.geo.city_name","type":"keyword","description":"City name."},{"field":"client.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"client.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"client.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"client.geo.country_name","type":"keyword","description":"Country name."},{"field":"client.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"client.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"client.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"client.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"client.geo.region_name","type":"keyword","description":"Region name."},{"field":"client.geo.timezone","type":"keyword","description":"Time zone."},{"field":"client.ip","type":"ip","description":"IP address of the client."},{"field":"client.mac","type":"keyword","description":"MAC address of the client."},{"field":"client.nat.ip","type":"ip","description":"Client NAT ip address"},{"field":"client.nat.port","type":"long","description":"Client NAT port"},{"field":"client.packets","type":"long","description":"Packets sent from the client to the server."},{"field":"client.port","type":"long","description":"Port of the client."},{"field":"client.registered_domain","type":"keyword","description":"The highest registered client domain, stripped of the subdomain."},{"field":"client.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"client.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"client.user.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"client.user.email","type":"keyword","description":"User email address."},{"field":"client.user.full_name","type":"keyword","description":"User's full name, if available."},{"field":"client.user.full_name.text","type":"text","description":"User's full name, if available."},{"field":"client.user.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"client.user.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"client.user.group.name","type":"keyword","description":"Name of the group."},{"field":"client.user.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"client.user.id","type":"keyword","description":"Unique identifier of the user."},{"field":"client.user.name","type":"keyword","description":"Short name or login of the user."},{"field":"client.user.name.text","type":"text","description":"Short name or login of the user."},{"field":"client.user.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"cloud.account.id","type":"keyword","description":"The cloud account or organization id."},{"field":"cloud.account.name","type":"keyword","description":"The cloud account name."},{"field":"cloud.availability_zone","type":"keyword","description":"Availability zone in which this host, resource, or service is located."},{"field":"cloud.instance.id","type":"keyword","description":"Instance ID of the host machine."},{"field":"cloud.instance.name","type":"keyword","description":"Instance name of the host machine."},{"field":"cloud.machine.type","type":"keyword","description":"Machine type of the host machine."},{"field":"cloud.project.id","type":"keyword","description":"The cloud project id."},{"field":"cloud.project.name","type":"keyword","description":"The cloud project name."},{"field":"cloud.provider","type":"keyword","description":"Name of the cloud provider."},{"field":"cloud.region","type":"keyword","description":"Region in which this host, resource, or service is located."},{"field":"cloud.service.name","type":"keyword","description":"The cloud service name."},{"field":"container.id","type":"keyword","description":"Unique container id."},{"field":"container.image.name","type":"keyword","description":"Name of the image the container was built on."},{"field":"container.image.tag","type":"keyword","description":"Container image tags."},{"field":"container.labels","type":"object","description":"Image labels."},{"field":"container.name","type":"keyword","description":"Container name."},{"field":"container.runtime","type":"keyword","description":"Runtime managing this container."},{"field":"data_stream.dataset","type":"constant_keyword","description":"The field can contain anything that makes sense to signify the source of the data."},{"field":"data_stream.namespace","type":"constant_keyword","description":"A user defined namespace. Namespaces are useful to allow grouping of data."},{"field":"data_stream.type","type":"constant_keyword","description":"An overarching type for the data stream."},{"field":"destination.address","type":"keyword","description":"Destination network address."},{"field":"destination.as.number","type":"long","description":"Unique number allocated to the autonomous system."},{"field":"destination.as.organization.name","type":"keyword","description":"Organization name."},{"field":"destination.as.organization.name.text","type":"text","description":"Organization name."},{"field":"destination.bytes","type":"long","description":"Bytes sent from the destination to the source."},{"field":"destination.domain","type":"keyword","description":"Destination domain."},{"field":"destination.geo.city_name","type":"keyword","description":"City name."},{"field":"destination.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"destination.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"destination.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"destination.geo.country_name","type":"keyword","description":"Country name."},{"field":"destination.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"destination.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"destination.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"destination.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"destination.geo.region_name","type":"keyword","description":"Region name."},{"field":"destination.geo.timezone","type":"keyword","description":"Time zone."},{"field":"destination.ip","type":"ip","description":"IP address of the destination."},{"field":"destination.mac","type":"keyword","description":"MAC address of the destination."},{"field":"destination.nat.ip","type":"ip","description":"Destination NAT ip"},{"field":"destination.nat.port","type":"long","description":"Destination NAT Port"},{"field":"destination.packets","type":"long","description":"Packets sent from the destination to the source."},{"field":"destination.port","type":"long","description":"Port of the destination."},{"field":"destination.registered_domain","type":"keyword","description":"The highest registered destination domain, stripped of the subdomain."},{"field":"destination.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"destination.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"destination.user.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"destination.user.email","type":"keyword","description":"User email address."},{"field":"destination.user.full_name","type":"keyword","description":"User's full name, if available."},{"field":"destination.user.full_name.text","type":"text","description":"User's full name, if available."},{"field":"destination.user.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"destination.user.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"destination.user.group.name","type":"keyword","description":"Name of the group."},{"field":"destination.user.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"destination.user.id","type":"keyword","description":"Unique identifier of the user."},{"field":"destination.user.name","type":"keyword","description":"Short name or login of the user."},{"field":"destination.user.name.text","type":"text","description":"Short name or login of the user."},{"field":"destination.user.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"dll.code_signature.exists","type":"boolean","description":"Boolean to capture if a signature is present."},{"field":"dll.code_signature.signing_id","type":"keyword","description":"The identifier used to sign the process."},{"field":"dll.code_signature.status","type":"keyword","description":"Additional information about the certificate status."},{"field":"dll.code_signature.subject_name","type":"keyword","description":"Subject name of the code signer"},{"field":"dll.code_signature.team_id","type":"keyword","description":"The team identifier used to sign the process."},{"field":"dll.code_signature.trusted","type":"boolean","description":"Stores the trust status of the certificate chain."},{"field":"dll.code_signature.valid","type":"boolean","description":"Boolean to capture if the digital signature is verified against the binary content."},{"field":"dll.hash.md5","type":"keyword","description":"MD5 hash."},{"field":"dll.hash.sha1","type":"keyword","description":"SHA1 hash."},{"field":"dll.hash.sha256","type":"keyword","description":"SHA256 hash."},{"field":"dll.hash.sha512","type":"keyword","description":"SHA512 hash."},{"field":"dll.hash.ssdeep","type":"keyword","description":"SSDEEP hash."},{"field":"dll.name","type":"keyword","description":"Name of the library."},{"field":"dll.path","type":"keyword","description":"Full file path of the library."},{"field":"dll.pe.architecture","type":"keyword","description":"CPU architecture target for the file."},{"field":"dll.pe.company","type":"keyword","description":"Internal company name of the file, provided at compile-time."},{"field":"dll.pe.description","type":"keyword","description":"Internal description of the file, provided at compile-time."},{"field":"dll.pe.file_version","type":"keyword","description":"Process name."},{"field":"dll.pe.imphash","type":"keyword","description":"A hash of the imports in a PE file."},{"field":"dll.pe.original_file_name","type":"keyword","description":"Internal name of the file, provided at compile-time."},{"field":"dll.pe.product","type":"keyword","description":"Internal product name of the file, provided at compile-time."},{"field":"dns.answers","type":"object","description":"Array of DNS answers."},{"field":"dns.answers.class","type":"keyword","description":"The class of DNS data contained in this resource record."},{"field":"dns.answers.data","type":"keyword","description":"The data describing the resource."},{"field":"dns.answers.name","type":"keyword","description":"The domain name to which this resource record pertains."},{"field":"dns.answers.ttl","type":"long","description":"The time interval in seconds that this resource record may be cached before it should be discarded."},{"field":"dns.answers.type","type":"keyword","description":"The type of data contained in this resource record."},{"field":"dns.header_flags","type":"keyword","description":"Array of DNS header flags."},{"field":"dns.id","type":"keyword","description":"The DNS packet identifier assigned by the program that generated the query. The identifier is copied to the response."},{"field":"dns.op_code","type":"keyword","description":"The DNS operation code that specifies the kind of query in the message."},{"field":"dns.question.class","type":"keyword","description":"The class of records being queried."},{"field":"dns.question.name","type":"keyword","description":"The name being queried."},{"field":"dns.question.registered_domain","type":"keyword","description":"The highest registered domain, stripped of the subdomain."},{"field":"dns.question.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"dns.question.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"dns.question.type","type":"keyword","description":"The type of record being queried."},{"field":"dns.resolved_ip","type":"ip","description":"Array containing all IPs seen in answers.data"},{"field":"dns.response_code","type":"keyword","description":"The DNS response code."},{"field":"dns.type","type":"keyword","description":"The type of DNS event captured, query or answer."},{"field":"ecs.version","type":"keyword","description":"ECS version this event conforms to."},{"field":"error.code","type":"keyword","description":"Error code describing the error."},{"field":"error.id","type":"keyword","description":"Unique identifier for the error."},{"field":"error.message","type":"text","description":"Error message."},{"field":"error.stack_trace","type":"keyword","description":"The stack trace of this error in plain text."},{"field":"error.stack_trace.text","type":"text","description":"The stack trace of this error in plain text."},{"field":"error.type","type":"keyword","description":"The type of the error, for example the class name of the exception."},{"field":"event.action","type":"keyword","description":"The action captured by the event."},{"field":"event.agent_id_status","type":"keyword","description":"Validation status of the event's agent.id field."},{"field":"event.category","type":"keyword","description":"Event category. The second categorization field in the hierarchy."},{"field":"event.code","type":"keyword","description":"Identification code for this event."},{"field":"event.created","type":"date","description":"Time when the event was first read by an agent or by your pipeline."},{"field":"event.dataset","type":"keyword","description":"Name of the dataset."},{"field":"event.duration","type":"long","description":"Duration of the event in nanoseconds."},{"field":"event.end","type":"date","description":"event.end contains the date when the event ended or when the activity was last observed."},{"field":"event.hash","type":"keyword","description":"Hash (perhaps logstash fingerprint) of raw field to be able to demonstrate log integrity."},{"field":"event.id","type":"keyword","description":"Unique ID to describe the event."},{"field":"event.ingested","type":"date","description":"Timestamp when an event arrived in the central data store."},{"field":"event.kind","type":"keyword","description":"The kind of the event. The highest categorization field in the hierarchy."},{"field":"event.module","type":"keyword","description":"Name of the module this data is coming from."},{"field":"event.original","type":"keyword","description":"Raw text message of entire event."},{"field":"event.outcome","type":"keyword","description":"The outcome of the event. The lowest level categorization field in the hierarchy."},{"field":"event.provider","type":"keyword","description":"Source of the event."},{"field":"event.reason","type":"keyword","description":"Reason why this event happened, according to the source"},{"field":"event.reference","type":"keyword","description":"Event reference URL"},{"field":"event.risk_score","type":"float","description":"Risk score or priority of the event (e.g. security solutions). Use your system's original value here."},{"field":"event.risk_score_norm","type":"float","description":"Normalized risk score or priority of the event (0-100)."},{"field":"event.sequence","type":"long","description":"Sequence number of the event."},{"field":"event.severity","type":"long","description":"Numeric severity of the event."},{"field":"event.start","type":"date","description":"event.start contains the date when the event started or when the activity was first observed."},{"field":"event.timezone","type":"keyword","description":"Event time zone."},{"field":"event.type","type":"keyword","description":"Event type. The third categorization field in the hierarchy."},{"field":"event.url","type":"keyword","description":"Event investigation URL"},{"field":"file.accessed","type":"date","description":"Last time the file was accessed."},{"field":"file.attributes","type":"keyword","description":"Array of file attributes."},{"field":"file.code_signature.exists","type":"boolean","description":"Boolean to capture if a signature is present."},{"field":"file.code_signature.signing_id","type":"keyword","description":"The identifier used to sign the process."},{"field":"file.code_signature.status","type":"keyword","description":"Additional information about the certificate status."},{"field":"file.code_signature.subject_name","type":"keyword","description":"Subject name of the code signer"},{"field":"file.code_signature.team_id","type":"keyword","description":"The team identifier used to sign the process."},{"field":"file.code_signature.trusted","type":"boolean","description":"Stores the trust status of the certificate chain."},{"field":"file.code_signature.valid","type":"boolean","description":"Boolean to capture if the digital signature is verified against the binary content."},{"field":"file.created","type":"date","description":"File creation time."},{"field":"file.ctime","type":"date","description":"Last time the file attributes or metadata changed."},{"field":"file.device","type":"keyword","description":"Device that is the source of the file."},{"field":"file.directory","type":"keyword","description":"Directory where the file is located."},{"field":"file.drive_letter","type":"keyword","description":"Drive letter where the file is located."},{"field":"file.elf.architecture","type":"keyword","description":"Machine architecture of the ELF file."},{"field":"file.elf.byte_order","type":"keyword","description":"Byte sequence of ELF file."},{"field":"file.elf.cpu_type","type":"keyword","description":"CPU type of the ELF file."},{"field":"file.elf.creation_date","type":"date","description":"Build or compile date."},{"field":"file.elf.exports","type":"flattened","description":"List of exported element names and types."},{"field":"file.elf.header.abi_version","type":"keyword","description":"Version of the ELF Application Binary Interface (ABI)."},{"field":"file.elf.header.class","type":"keyword","description":"Header class of the ELF file."},{"field":"file.elf.header.data","type":"keyword","description":"Data table of the ELF header."},{"field":"file.elf.header.entrypoint","type":"long","description":"Header entrypoint of the ELF file."},{"field":"file.elf.header.object_version","type":"keyword","description":"0x1\" for original ELF files."},{"field":"file.elf.header.os_abi","type":"keyword","description":"Application Binary Interface (ABI) of the Linux OS."},{"field":"file.elf.header.type","type":"keyword","description":"Header type of the ELF file."},{"field":"file.elf.header.version","type":"keyword","description":"Version of the ELF header."},{"field":"file.elf.imports","type":"flattened","description":"List of imported element names and types."},{"field":"file.elf.sections","type":"nested","description":"Section information of the ELF file."},{"field":"file.elf.sections.chi2","type":"long","description":"Chi-square probability distribution of the section."},{"field":"file.elf.sections.entropy","type":"long","description":"Shannon entropy calculation from the section."},{"field":"file.elf.sections.flags","type":"keyword","description":"ELF Section List flags."},{"field":"file.elf.sections.name","type":"keyword","description":"ELF Section List name."},{"field":"file.elf.sections.physical_offset","type":"keyword","description":"ELF Section List offset."},{"field":"file.elf.sections.physical_size","type":"long","description":"ELF Section List physical size."},{"field":"file.elf.sections.type","type":"keyword","description":"ELF Section List type."},{"field":"file.elf.sections.virtual_address","type":"long","description":"ELF Section List virtual address."},{"field":"file.elf.sections.virtual_size","type":"long","description":"ELF Section List virtual size."},{"field":"file.elf.segments","type":"nested","description":"ELF object segment list."},{"field":"file.elf.segments.sections","type":"keyword","description":"ELF object segment sections."},{"field":"file.elf.segments.type","type":"keyword","description":"ELF object segment type."},{"field":"file.elf.shared_libraries","type":"keyword","description":"List of shared libraries used by this ELF object."},{"field":"file.elf.telfhash","type":"keyword","description":"telfhash hash for ELF file."},{"field":"file.extension","type":"keyword","description":"File extension, excluding the leading dot."},{"field":"file.gid","type":"keyword","description":"Primary group ID (GID) of the file."},{"field":"file.group","type":"keyword","description":"Primary group name of the file."},{"field":"file.hash.md5","type":"keyword","description":"MD5 hash."},{"field":"file.hash.sha1","type":"keyword","description":"SHA1 hash."},{"field":"file.hash.sha256","type":"keyword","description":"SHA256 hash."},{"field":"file.hash.sha512","type":"keyword","description":"SHA512 hash."},{"field":"file.hash.ssdeep","type":"keyword","description":"SSDEEP hash."},{"field":"file.inode","type":"keyword","description":"Inode representing the file in the filesystem."},{"field":"file.mime_type","type":"keyword","description":"Media type of file, document, or arrangement of bytes."},{"field":"file.mode","type":"keyword","description":"Mode of the file in octal representation."},{"field":"file.mtime","type":"date","description":"Last time the file content was modified."},{"field":"file.name","type":"keyword","description":"Name of the file including the extension, without the directory."},{"field":"file.owner","type":"keyword","description":"File owner's username."},{"field":"file.path","type":"keyword","description":"Full path to the file, including the file name."},{"field":"file.path.text","type":"text","description":"Full path to the file, including the file name."},{"field":"file.pe.architecture","type":"keyword","description":"CPU architecture target for the file."},{"field":"file.pe.company","type":"keyword","description":"Internal company name of the file, provided at compile-time."},{"field":"file.pe.description","type":"keyword","description":"Internal description of the file, provided at compile-time."},{"field":"file.pe.file_version","type":"keyword","description":"Process name."},{"field":"file.pe.imphash","type":"keyword","description":"A hash of the imports in a PE file."},{"field":"file.pe.original_file_name","type":"keyword","description":"Internal name of the file, provided at compile-time."},{"field":"file.pe.product","type":"keyword","description":"Internal product name of the file, provided at compile-time."},{"field":"file.size","type":"long","description":"File size in bytes."},{"field":"file.target_path","type":"keyword","description":"Target path for symlinks."},{"field":"file.target_path.text","type":"text","description":"Target path for symlinks."},{"field":"file.type","type":"keyword","description":"File type (file, dir, or symlink)."},{"field":"file.uid","type":"keyword","description":"The user ID (UID) or security identifier (SID) of the file owner."},{"field":"file.x509.alternative_names","type":"keyword","description":"List of subject alternative names (SAN)."},{"field":"file.x509.issuer.common_name","type":"keyword","description":"List of common name (CN) of issuing certificate authority."},{"field":"file.x509.issuer.country","type":"keyword","description":"List of country (C) codes"},{"field":"file.x509.issuer.distinguished_name","type":"keyword","description":"Distinguished name (DN) of issuing certificate authority."},{"field":"file.x509.issuer.locality","type":"keyword","description":"List of locality names (L)"},{"field":"file.x509.issuer.organization","type":"keyword","description":"List of organizations (O) of issuing certificate authority."},{"field":"file.x509.issuer.organizational_unit","type":"keyword","description":"List of organizational units (OU) of issuing certificate authority."},{"field":"file.x509.issuer.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"file.x509.not_after","type":"date","description":"Time at which the certificate is no longer considered valid."},{"field":"file.x509.not_before","type":"date","description":"Time at which the certificate is first considered valid."},{"field":"file.x509.public_key_algorithm","type":"keyword","description":"Algorithm used to generate the public key."},{"field":"file.x509.public_key_curve","type":"keyword","description":"The curve used by the elliptic curve public key algorithm. This is algorithm specific."},{"field":"file.x509.public_key_exponent","type":"long","description":"Exponent used to derive the public key. This is algorithm specific."},{"field":"file.x509.public_key_size","type":"long","description":"The size of the public key space in bits."},{"field":"file.x509.serial_number","type":"keyword","description":"Unique serial number issued by the certificate authority."},{"field":"file.x509.signature_algorithm","type":"keyword","description":"Identifier for certificate signature algorithm."},{"field":"file.x509.subject.common_name","type":"keyword","description":"List of common names (CN) of subject."},{"field":"file.x509.subject.country","type":"keyword","description":"List of country (C) code"},{"field":"file.x509.subject.distinguished_name","type":"keyword","description":"Distinguished name (DN) of the certificate subject entity."},{"field":"file.x509.subject.locality","type":"keyword","description":"List of locality names (L)"},{"field":"file.x509.subject.organization","type":"keyword","description":"List of organizations (O) of subject."},{"field":"file.x509.subject.organizational_unit","type":"keyword","description":"List of organizational units (OU) of subject."},{"field":"file.x509.subject.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"file.x509.version_number","type":"keyword","description":"Version of x509 format."},{"field":"group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"group.name","type":"keyword","description":"Name of the group."},{"field":"host.architecture","type":"keyword","description":"Operating system architecture."},{"field":"host.cpu.usage","type":"scaled_float","description":"Percent CPU used, between 0 and 1."},{"field":"host.disk.read.bytes","type":"long","description":"The number of bytes read by all disks."},{"field":"host.disk.write.bytes","type":"long","description":"The number of bytes written on all disks."},{"field":"host.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"host.geo.city_name","type":"keyword","description":"City name."},{"field":"host.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"host.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"host.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"host.geo.country_name","type":"keyword","description":"Country name."},{"field":"host.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"host.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"host.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"host.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"host.geo.region_name","type":"keyword","description":"Region name."},{"field":"host.geo.timezone","type":"keyword","description":"Time zone."},{"field":"host.hostname","type":"keyword","description":"Hostname of the host."},{"field":"host.id","type":"keyword","description":"Unique host id."},{"field":"host.ip","type":"ip","description":"Host ip addresses."},{"field":"host.mac","type":"keyword","description":"Host MAC addresses."},{"field":"host.name","type":"keyword","description":"Name of the host."},{"field":"host.network.egress.bytes","type":"long","description":"The number of bytes sent on all network interfaces."},{"field":"host.network.egress.packets","type":"long","description":"The number of packets sent on all network interfaces."},{"field":"host.network.ingress.bytes","type":"long","description":"The number of bytes received on all network interfaces."},{"field":"host.network.ingress.packets","type":"long","description":"The number of packets received on all network interfaces."},{"field":"host.os.family","type":"keyword","description":"OS family (such as redhat, debian, freebsd, windows)."},{"field":"host.os.full","type":"keyword","description":"Operating system name, including the version or code name."},{"field":"host.os.full.text","type":"text","description":"Operating system name, including the version or code name."},{"field":"host.os.kernel","type":"keyword","description":"Operating system kernel version as a raw string."},{"field":"host.os.name","type":"keyword","description":"Operating system name, without the version."},{"field":"host.os.name.text","type":"text","description":"Operating system name, without the version."},{"field":"host.os.platform","type":"keyword","description":"Operating system platform (such centos, ubuntu, windows)."},{"field":"host.os.type","type":"keyword","description":"Which commercial OS family (one of: linux, macos, unix or windows)."},{"field":"host.os.version","type":"keyword","description":"Operating system version as a raw string."},{"field":"host.type","type":"keyword","description":"Type of host."},{"field":"host.uptime","type":"long","description":"Seconds the host has been up."},{"field":"host.user.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"host.user.email","type":"keyword","description":"User email address."},{"field":"host.user.full_name","type":"keyword","description":"User's full name, if available."},{"field":"host.user.full_name.text","type":"text","description":"User's full name, if available."},{"field":"host.user.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"host.user.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"host.user.group.name","type":"keyword","description":"Name of the group."},{"field":"host.user.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"host.user.id","type":"keyword","description":"Unique identifier of the user."},{"field":"host.user.name","type":"keyword","description":"Short name or login of the user."},{"field":"host.user.name.text","type":"text","description":"Short name or login of the user."},{"field":"host.user.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"http.request.body.bytes","type":"long","description":"Size in bytes of the request body."},{"field":"http.request.body.content","type":"keyword","description":"The full HTTP request body."},{"field":"http.request.body.content.text","type":"text","description":"The full HTTP request body."},{"field":"http.request.bytes","type":"long","description":"Total size in bytes of the request (body and headers)."},{"field":"http.request.id","type":"keyword","description":"HTTP request ID."},{"field":"http.request.method","type":"keyword","description":"HTTP request method."},{"field":"http.request.mime_type","type":"keyword","description":"Mime type of the body of the request."},{"field":"http.request.referrer","type":"keyword","description":"Referrer for this HTTP request."},{"field":"http.response.body.bytes","type":"long","description":"Size in bytes of the response body."},{"field":"http.response.body.content","type":"keyword","description":"The full HTTP response body."},{"field":"http.response.body.content.text","type":"text","description":"The full HTTP response body."},{"field":"http.response.bytes","type":"long","description":"Total size in bytes of the response (body and headers)."},{"field":"http.response.mime_type","type":"keyword","description":"Mime type of the body of the response."},{"field":"http.response.status_code","type":"long","description":"HTTP response status code."},{"field":"http.version","type":"keyword","description":"HTTP version."},{"field":"log.file.path","type":"keyword","description":"Full path to the log file this event came from."},{"field":"log.level","type":"keyword","description":"Log level of the log event."},{"field":"log.logger","type":"keyword","description":"Name of the logger."},{"field":"log.origin.file.line","type":"integer","description":"The line number of the file which originated the log event."},{"field":"log.origin.file.name","type":"keyword","description":"The code file which originated the log event."},{"field":"log.origin.function","type":"keyword","description":"The function which originated the log event."},{"field":"log.original","type":"keyword","description":"Deprecated original log message with light interpretation only (encoding, newlines)."},{"field":"log.syslog","type":"object","description":"Syslog metadata"},{"field":"log.syslog.facility.code","type":"long","description":"Syslog numeric facility of the event."},{"field":"log.syslog.facility.name","type":"keyword","description":"Syslog text-based facility of the event."},{"field":"log.syslog.priority","type":"long","description":"Syslog priority of the event."},{"field":"log.syslog.severity.code","type":"long","description":"Syslog numeric severity of the event."},{"field":"log.syslog.severity.name","type":"keyword","description":"Syslog text-based severity of the event."},{"field":"network.application","type":"keyword","description":"Application level protocol name."},{"field":"network.bytes","type":"long","description":"Total bytes transferred in both directions."},{"field":"network.community_id","type":"keyword","description":"A hash of source and destination IPs and ports."},{"field":"network.direction","type":"keyword","description":"Direction of the network traffic."},{"field":"network.forwarded_ip","type":"ip","description":"Host IP address when the source IP address is the proxy."},{"field":"network.iana_number","type":"keyword","description":"IANA Protocol Number."},{"field":"network.inner","type":"object","description":"Inner VLAN tag information"},{"field":"network.inner.vlan.id","type":"keyword","description":"VLAN ID as reported by the observer."},{"field":"network.inner.vlan.name","type":"keyword","description":"Optional VLAN name as reported by the observer."},{"field":"network.name","type":"keyword","description":"Name given by operators to sections of their network."},{"field":"network.packets","type":"long","description":"Total packets transferred in both directions."},{"field":"network.protocol","type":"keyword","description":"L7 Network protocol name."},{"field":"network.transport","type":"keyword","description":"Protocol Name corresponding to the field `iana_number`."},{"field":"network.type","type":"keyword","description":"In the OSI Model this would be the Network Layer. ipv4, ipv6, ipsec, pim, etc"},{"field":"network.vlan.id","type":"keyword","description":"VLAN ID as reported by the observer."},{"field":"network.vlan.name","type":"keyword","description":"Optional VLAN name as reported by the observer."},{"field":"observer.egress","type":"object","description":"Object field for egress information"},{"field":"observer.egress.interface.alias","type":"keyword","description":"Interface alias"},{"field":"observer.egress.interface.id","type":"keyword","description":"Interface ID"},{"field":"observer.egress.interface.name","type":"keyword","description":"Interface name"},{"field":"observer.egress.vlan.id","type":"keyword","description":"VLAN ID as reported by the observer."},{"field":"observer.egress.vlan.name","type":"keyword","description":"Optional VLAN name as reported by the observer."},{"field":"observer.egress.zone","type":"keyword","description":"Observer Egress zone"},{"field":"observer.geo.city_name","type":"keyword","description":"City name."},{"field":"observer.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"observer.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"observer.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"observer.geo.country_name","type":"keyword","description":"Country name."},{"field":"observer.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"observer.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"observer.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"observer.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"observer.geo.region_name","type":"keyword","description":"Region name."},{"field":"observer.geo.timezone","type":"keyword","description":"Time zone."},{"field":"observer.hostname","type":"keyword","description":"Hostname of the observer."},{"field":"observer.ingress","type":"object","description":"Object field for ingress information"},{"field":"observer.ingress.interface.alias","type":"keyword","description":"Interface alias"},{"field":"observer.ingress.interface.id","type":"keyword","description":"Interface ID"},{"field":"observer.ingress.interface.name","type":"keyword","description":"Interface name"},{"field":"observer.ingress.vlan.id","type":"keyword","description":"VLAN ID as reported by the observer."},{"field":"observer.ingress.vlan.name","type":"keyword","description":"Optional VLAN name as reported by the observer."},{"field":"observer.ingress.zone","type":"keyword","description":"Observer ingress zone"},{"field":"observer.ip","type":"ip","description":"IP addresses of the observer."},{"field":"observer.mac","type":"keyword","description":"MAC addresses of the observer."},{"field":"observer.name","type":"keyword","description":"Custom name of the observer."},{"field":"observer.os.family","type":"keyword","description":"OS family (such as redhat, debian, freebsd, windows)."},{"field":"observer.os.full","type":"keyword","description":"Operating system name, including the version or code name."},{"field":"observer.os.full.text","type":"text","description":"Operating system name, including the version or code name."},{"field":"observer.os.kernel","type":"keyword","description":"Operating system kernel version as a raw string."},{"field":"observer.os.name","type":"keyword","description":"Operating system name, without the version."},{"field":"observer.os.name.text","type":"text","description":"Operating system name, without the version."},{"field":"observer.os.platform","type":"keyword","description":"Operating system platform (such centos, ubuntu, windows)."},{"field":"observer.os.type","type":"keyword","description":"Which commercial OS family (one of: linux, macos, unix or windows)."},{"field":"observer.os.version","type":"keyword","description":"Operating system version as a raw string."},{"field":"observer.product","type":"keyword","description":"The product name of the observer."},{"field":"observer.serial_number","type":"keyword","description":"Observer serial number."},{"field":"observer.type","type":"keyword","description":"The type of the observer the data is coming from."},{"field":"observer.vendor","type":"keyword","description":"Vendor name of the observer."},{"field":"observer.version","type":"keyword","description":"Observer version."},{"field":"orchestrator.api_version","type":"keyword","description":"API version being used to carry out the action"},{"field":"orchestrator.cluster.name","type":"keyword","description":"Name of the cluster."},{"field":"orchestrator.cluster.url","type":"keyword","description":"URL of the API used to manage the cluster."},{"field":"orchestrator.cluster.version","type":"keyword","description":"The version of the cluster."},{"field":"orchestrator.namespace","type":"keyword","description":"Namespace in which the action is taking place."},{"field":"orchestrator.organization","type":"keyword","description":"Organization affected by the event (for multi-tenant orchestrator setups)."},{"field":"orchestrator.resource.name","type":"keyword","description":"Name of the resource being acted upon."},{"field":"orchestrator.resource.type","type":"keyword","description":"Type of resource being acted upon."},{"field":"orchestrator.type","type":"keyword","description":"Orchestrator cluster type (e.g. kubernetes, nomad or cloudfoundry)."},{"field":"organization.id","type":"keyword","description":"Unique identifier for the organization."},{"field":"organization.name","type":"keyword","description":"Organization name."},{"field":"organization.name.text","type":"text","description":"Organization name."},{"field":"package.architecture","type":"keyword","description":"Package architecture."},{"field":"package.build_version","type":"keyword","description":"Build version information"},{"field":"package.checksum","type":"keyword","description":"Checksum of the installed package for verification."},{"field":"package.description","type":"keyword","description":"Description of the package."},{"field":"package.install_scope","type":"keyword","description":"Indicating how the package was installed, e.g. user-local, global."},{"field":"package.installed","type":"date","description":"Time when package was installed."},{"field":"package.license","type":"keyword","description":"Package license"},{"field":"package.name","type":"keyword","description":"Package name"},{"field":"package.path","type":"keyword","description":"Path where the package is installed."},{"field":"package.reference","type":"keyword","description":"Package home page or reference URL"},{"field":"package.size","type":"long","description":"Package size in bytes."},{"field":"package.type","type":"keyword","description":"Package type"},{"field":"package.version","type":"keyword","description":"Package version"},{"field":"process.args","type":"keyword","description":"Array of process arguments."},{"field":"process.args_count","type":"long","description":"Length of the process.args array."},{"field":"process.code_signature.exists","type":"boolean","description":"Boolean to capture if a signature is present."},{"field":"process.code_signature.signing_id","type":"keyword","description":"The identifier used to sign the process."},{"field":"process.code_signature.status","type":"keyword","description":"Additional information about the certificate status."},{"field":"process.code_signature.subject_name","type":"keyword","description":"Subject name of the code signer"},{"field":"process.code_signature.team_id","type":"keyword","description":"The team identifier used to sign the process."},{"field":"process.code_signature.trusted","type":"boolean","description":"Stores the trust status of the certificate chain."},{"field":"process.code_signature.valid","type":"boolean","description":"Boolean to capture if the digital signature is verified against the binary content."},{"field":"process.command_line","type":"keyword","description":"Full command line that started the process."},{"field":"process.command_line.text","type":"text","description":"Full command line that started the process."},{"field":"process.elf.architecture","type":"keyword","description":"Machine architecture of the ELF file."},{"field":"process.elf.byte_order","type":"keyword","description":"Byte sequence of ELF file."},{"field":"process.elf.cpu_type","type":"keyword","description":"CPU type of the ELF file."},{"field":"process.elf.creation_date","type":"date","description":"Build or compile date."},{"field":"process.elf.exports","type":"flattened","description":"List of exported element names and types."},{"field":"process.elf.header.abi_version","type":"keyword","description":"Version of the ELF Application Binary Interface (ABI)."},{"field":"process.elf.header.class","type":"keyword","description":"Header class of the ELF file."},{"field":"process.elf.header.data","type":"keyword","description":"Data table of the ELF header."},{"field":"process.elf.header.entrypoint","type":"long","description":"Header entrypoint of the ELF file."},{"field":"process.elf.header.object_version","type":"keyword","description":"0x1\" for original ELF files."},{"field":"process.elf.header.os_abi","type":"keyword","description":"Application Binary Interface (ABI) of the Linux OS."},{"field":"process.elf.header.type","type":"keyword","description":"Header type of the ELF file."},{"field":"process.elf.header.version","type":"keyword","description":"Version of the ELF header."},{"field":"process.elf.imports","type":"flattened","description":"List of imported element names and types."},{"field":"process.elf.sections","type":"nested","description":"Section information of the ELF file."},{"field":"process.elf.sections.chi2","type":"long","description":"Chi-square probability distribution of the section."},{"field":"process.elf.sections.entropy","type":"long","description":"Shannon entropy calculation from the section."},{"field":"process.elf.sections.flags","type":"keyword","description":"ELF Section List flags."},{"field":"process.elf.sections.name","type":"keyword","description":"ELF Section List name."},{"field":"process.elf.sections.physical_offset","type":"keyword","description":"ELF Section List offset."},{"field":"process.elf.sections.physical_size","type":"long","description":"ELF Section List physical size."},{"field":"process.elf.sections.type","type":"keyword","description":"ELF Section List type."},{"field":"process.elf.sections.virtual_address","type":"long","description":"ELF Section List virtual address."},{"field":"process.elf.sections.virtual_size","type":"long","description":"ELF Section List virtual size."},{"field":"process.elf.segments","type":"nested","description":"ELF object segment list."},{"field":"process.elf.segments.sections","type":"keyword","description":"ELF object segment sections."},{"field":"process.elf.segments.type","type":"keyword","description":"ELF object segment type."},{"field":"process.elf.shared_libraries","type":"keyword","description":"List of shared libraries used by this ELF object."},{"field":"process.elf.telfhash","type":"keyword","description":"telfhash hash for ELF file."},{"field":"process.entity_id","type":"keyword","description":"Unique identifier for the process."},{"field":"process.executable","type":"keyword","description":"Absolute path to the process executable."},{"field":"process.executable.text","type":"text","description":"Absolute path to the process executable."},{"field":"process.exit_code","type":"long","description":"The exit code of the process."},{"field":"process.hash.md5","type":"keyword","description":"MD5 hash."},{"field":"process.hash.sha1","type":"keyword","description":"SHA1 hash."},{"field":"process.hash.sha256","type":"keyword","description":"SHA256 hash."},{"field":"process.hash.sha512","type":"keyword","description":"SHA512 hash."},{"field":"process.hash.ssdeep","type":"keyword","description":"SSDEEP hash."},{"field":"process.name","type":"keyword","description":"Process name."},{"field":"process.name.text","type":"text","description":"Process name."},{"field":"process.parent.args","type":"keyword","description":"Array of process arguments."},{"field":"process.parent.args_count","type":"long","description":"Length of the process.args array."},{"field":"process.parent.code_signature.exists","type":"boolean","description":"Boolean to capture if a signature is present."},{"field":"process.parent.code_signature.signing_id","type":"keyword","description":"The identifier used to sign the process."},{"field":"process.parent.code_signature.status","type":"keyword","description":"Additional information about the certificate status."},{"field":"process.parent.code_signature.subject_name","type":"keyword","description":"Subject name of the code signer"},{"field":"process.parent.code_signature.team_id","type":"keyword","description":"The team identifier used to sign the process."},{"field":"process.parent.code_signature.trusted","type":"boolean","description":"Stores the trust status of the certificate chain."},{"field":"process.parent.code_signature.valid","type":"boolean","description":"Boolean to capture if the digital signature is verified against the binary content."},{"field":"process.parent.command_line","type":"keyword","description":"Full command line that started the process."},{"field":"process.parent.command_line.text","type":"text","description":"Full command line that started the process."},{"field":"process.parent.elf.architecture","type":"keyword","description":"Machine architecture of the ELF file."},{"field":"process.parent.elf.byte_order","type":"keyword","description":"Byte sequence of ELF file."},{"field":"process.parent.elf.cpu_type","type":"keyword","description":"CPU type of the ELF file."},{"field":"process.parent.elf.creation_date","type":"date","description":"Build or compile date."},{"field":"process.parent.elf.exports","type":"flattened","description":"List of exported element names and types."},{"field":"process.parent.elf.header.abi_version","type":"keyword","description":"Version of the ELF Application Binary Interface (ABI)."},{"field":"process.parent.elf.header.class","type":"keyword","description":"Header class of the ELF file."},{"field":"process.parent.elf.header.data","type":"keyword","description":"Data table of the ELF header."},{"field":"process.parent.elf.header.entrypoint","type":"long","description":"Header entrypoint of the ELF file."},{"field":"process.parent.elf.header.object_version","type":"keyword","description":"0x1\" for original ELF files."},{"field":"process.parent.elf.header.os_abi","type":"keyword","description":"Application Binary Interface (ABI) of the Linux OS."},{"field":"process.parent.elf.header.type","type":"keyword","description":"Header type of the ELF file."},{"field":"process.parent.elf.header.version","type":"keyword","description":"Version of the ELF header."},{"field":"process.parent.elf.imports","type":"flattened","description":"List of imported element names and types."},{"field":"process.parent.elf.sections","type":"nested","description":"Section information of the ELF file."},{"field":"process.parent.elf.sections.chi2","type":"long","description":"Chi-square probability distribution of the section."},{"field":"process.parent.elf.sections.entropy","type":"long","description":"Shannon entropy calculation from the section."},{"field":"process.parent.elf.sections.flags","type":"keyword","description":"ELF Section List flags."},{"field":"process.parent.elf.sections.name","type":"keyword","description":"ELF Section List name."},{"field":"process.parent.elf.sections.physical_offset","type":"keyword","description":"ELF Section List offset."},{"field":"process.parent.elf.sections.physical_size","type":"long","description":"ELF Section List physical size."},{"field":"process.parent.elf.sections.type","type":"keyword","description":"ELF Section List type."},{"field":"process.parent.elf.sections.virtual_address","type":"long","description":"ELF Section List virtual address."},{"field":"process.parent.elf.sections.virtual_size","type":"long","description":"ELF Section List virtual size."},{"field":"process.parent.elf.segments","type":"nested","description":"ELF object segment list."},{"field":"process.parent.elf.segments.sections","type":"keyword","description":"ELF object segment sections."},{"field":"process.parent.elf.segments.type","type":"keyword","description":"ELF object segment type."},{"field":"process.parent.elf.shared_libraries","type":"keyword","description":"List of shared libraries used by this ELF object."},{"field":"process.parent.elf.telfhash","type":"keyword","description":"telfhash hash for ELF file."},{"field":"process.parent.entity_id","type":"keyword","description":"Unique identifier for the process."},{"field":"process.parent.executable","type":"keyword","description":"Absolute path to the process executable."},{"field":"process.parent.executable.text","type":"text","description":"Absolute path to the process executable."},{"field":"process.parent.exit_code","type":"long","description":"The exit code of the process."},{"field":"process.parent.hash.md5","type":"keyword","description":"MD5 hash."},{"field":"process.parent.hash.sha1","type":"keyword","description":"SHA1 hash."},{"field":"process.parent.hash.sha256","type":"keyword","description":"SHA256 hash."},{"field":"process.parent.hash.sha512","type":"keyword","description":"SHA512 hash."},{"field":"process.parent.hash.ssdeep","type":"keyword","description":"SSDEEP hash."},{"field":"process.parent.name","type":"keyword","description":"Process name."},{"field":"process.parent.name.text","type":"text","description":"Process name."},{"field":"process.parent.pe.architecture","type":"keyword","description":"CPU architecture target for the file."},{"field":"process.parent.pe.company","type":"keyword","description":"Internal company name of the file, provided at compile-time."},{"field":"process.parent.pe.description","type":"keyword","description":"Internal description of the file, provided at compile-time."},{"field":"process.parent.pe.file_version","type":"keyword","description":"Process name."},{"field":"process.parent.pe.imphash","type":"keyword","description":"A hash of the imports in a PE file."},{"field":"process.parent.pe.original_file_name","type":"keyword","description":"Internal name of the file, provided at compile-time."},{"field":"process.parent.pe.product","type":"keyword","description":"Internal product name of the file, provided at compile-time."},{"field":"process.parent.pgid","type":"long","description":"Identifier of the group of processes the process belongs to."},{"field":"process.parent.pid","type":"long","description":"Process id."},{"field":"process.parent.ppid","type":"long","description":"Parent process' pid."},{"field":"process.parent.start","type":"date","description":"The time the process started."},{"field":"process.parent.thread.id","type":"long","description":"Thread ID."},{"field":"process.parent.thread.name","type":"keyword","description":"Thread name."},{"field":"process.parent.title","type":"keyword","description":"Process title."},{"field":"process.parent.title.text","type":"text","description":"Process title."},{"field":"process.parent.uptime","type":"long","description":"Seconds the process has been up."},{"field":"process.parent.working_directory","type":"keyword","description":"The working directory of the process."},{"field":"process.parent.working_directory.text","type":"text","description":"The working directory of the process."},{"field":"process.pe.architecture","type":"keyword","description":"CPU architecture target for the file."},{"field":"process.pe.company","type":"keyword","description":"Internal company name of the file, provided at compile-time."},{"field":"process.pe.description","type":"keyword","description":"Internal description of the file, provided at compile-time."},{"field":"process.pe.file_version","type":"keyword","description":"Process name."},{"field":"process.pe.imphash","type":"keyword","description":"A hash of the imports in a PE file."},{"field":"process.pe.original_file_name","type":"keyword","description":"Internal name of the file, provided at compile-time."},{"field":"process.pe.product","type":"keyword","description":"Internal product name of the file, provided at compile-time."},{"field":"process.pgid","type":"long","description":"Identifier of the group of processes the process belongs to."},{"field":"process.pid","type":"long","description":"Process id."},{"field":"process.ppid","type":"long","description":"Parent process' pid."},{"field":"process.start","type":"date","description":"The time the process started."},{"field":"process.thread.id","type":"long","description":"Thread ID."},{"field":"process.thread.name","type":"keyword","description":"Thread name."},{"field":"process.title","type":"keyword","description":"Process title."},{"field":"process.title.text","type":"text","description":"Process title."},{"field":"process.uptime","type":"long","description":"Seconds the process has been up."},{"field":"process.working_directory","type":"keyword","description":"The working directory of the process."},{"field":"process.working_directory.text","type":"text","description":"The working directory of the process."},{"field":"registry.data.bytes","type":"keyword","description":"Original bytes written with base64 encoding."},{"field":"registry.data.strings","type":"keyword","description":"List of strings representing what was written to the registry."},{"field":"registry.data.type","type":"keyword","description":"Standard registry type for encoding contents"},{"field":"registry.hive","type":"keyword","description":"Abbreviated name for the hive."},{"field":"registry.key","type":"keyword","description":"Hive-relative path of keys."},{"field":"registry.path","type":"keyword","description":"Full path, including hive, key and value"},{"field":"registry.value","type":"keyword","description":"Name of the value written."},{"field":"related.hash","type":"keyword","description":"All the hashes seen on your event."},{"field":"related.hosts","type":"keyword","description":"All the host identifiers seen on your event."},{"field":"related.ip","type":"ip","description":"All of the IPs seen on your event."},{"field":"related.user","type":"keyword","description":"All the user names or other user identifiers seen on the event."},{"field":"rule.author","type":"keyword","description":"Rule author"},{"field":"rule.category","type":"keyword","description":"Rule category"},{"field":"rule.description","type":"keyword","description":"Rule description"},{"field":"rule.id","type":"keyword","description":"Rule ID"},{"field":"rule.license","type":"keyword","description":"Rule license"},{"field":"rule.name","type":"keyword","description":"Rule name"},{"field":"rule.reference","type":"keyword","description":"Rule reference URL"},{"field":"rule.ruleset","type":"keyword","description":"Rule ruleset"},{"field":"rule.uuid","type":"keyword","description":"Rule UUID"},{"field":"rule.version","type":"keyword","description":"Rule version"},{"field":"server.address","type":"keyword","description":"Server network address."},{"field":"server.as.number","type":"long","description":"Unique number allocated to the autonomous system."},{"field":"server.as.organization.name","type":"keyword","description":"Organization name."},{"field":"server.as.organization.name.text","type":"text","description":"Organization name."},{"field":"server.bytes","type":"long","description":"Bytes sent from the server to the client."},{"field":"server.domain","type":"keyword","description":"Server domain."},{"field":"server.geo.city_name","type":"keyword","description":"City name."},{"field":"server.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"server.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"server.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"server.geo.country_name","type":"keyword","description":"Country name."},{"field":"server.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"server.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"server.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"server.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"server.geo.region_name","type":"keyword","description":"Region name."},{"field":"server.geo.timezone","type":"keyword","description":"Time zone."},{"field":"server.ip","type":"ip","description":"IP address of the server."},{"field":"server.mac","type":"keyword","description":"MAC address of the server."},{"field":"server.nat.ip","type":"ip","description":"Server NAT ip"},{"field":"server.nat.port","type":"long","description":"Server NAT port"},{"field":"server.packets","type":"long","description":"Packets sent from the server to the client."},{"field":"server.port","type":"long","description":"Port of the server."},{"field":"server.registered_domain","type":"keyword","description":"The highest registered server domain, stripped of the subdomain."},{"field":"server.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"server.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"server.user.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"server.user.email","type":"keyword","description":"User email address."},{"field":"server.user.full_name","type":"keyword","description":"User's full name, if available."},{"field":"server.user.full_name.text","type":"text","description":"User's full name, if available."},{"field":"server.user.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"server.user.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"server.user.group.name","type":"keyword","description":"Name of the group."},{"field":"server.user.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"server.user.id","type":"keyword","description":"Unique identifier of the user."},{"field":"server.user.name","type":"keyword","description":"Short name or login of the user."},{"field":"server.user.name.text","type":"text","description":"Short name or login of the user."},{"field":"server.user.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"service.ephemeral_id","type":"keyword","description":"Ephemeral identifier of this service."},{"field":"service.id","type":"keyword","description":"Unique identifier of the running service."},{"field":"service.name","type":"keyword","description":"Name of the service."},{"field":"service.node.name","type":"keyword","description":"Name of the service node."},{"field":"service.state","type":"keyword","description":"Current state of the service."},{"field":"service.type","type":"keyword","description":"The type of the service."},{"field":"service.version","type":"keyword","description":"Version of the service."},{"field":"source.address","type":"keyword","description":"Source network address."},{"field":"source.as.number","type":"long","description":"Unique number allocated to the autonomous system."},{"field":"source.as.organization.name","type":"keyword","description":"Organization name."},{"field":"source.as.organization.name.text","type":"text","description":"Organization name."},{"field":"source.bytes","type":"long","description":"Bytes sent from the source to the destination."},{"field":"source.domain","type":"keyword","description":"Source domain."},{"field":"source.geo.city_name","type":"keyword","description":"City name."},{"field":"source.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"source.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"source.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"source.geo.country_name","type":"keyword","description":"Country name."},{"field":"source.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"source.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"source.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"source.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"source.geo.region_name","type":"keyword","description":"Region name."},{"field":"source.geo.timezone","type":"keyword","description":"Time zone."},{"field":"source.ip","type":"ip","description":"IP address of the source."},{"field":"source.mac","type":"keyword","description":"MAC address of the source."},{"field":"source.nat.ip","type":"ip","description":"Source NAT ip"},{"field":"source.nat.port","type":"long","description":"Source NAT port"},{"field":"source.packets","type":"long","description":"Packets sent from the source to the destination."},{"field":"source.port","type":"long","description":"Port of the source."},{"field":"source.registered_domain","type":"keyword","description":"The highest registered source domain, stripped of the subdomain."},{"field":"source.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"source.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"source.user.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"source.user.email","type":"keyword","description":"User email address."},{"field":"source.user.full_name","type":"keyword","description":"User's full name, if available."},{"field":"source.user.full_name.text","type":"text","description":"User's full name, if available."},{"field":"source.user.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"source.user.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"source.user.group.name","type":"keyword","description":"Name of the group."},{"field":"source.user.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"source.user.id","type":"keyword","description":"Unique identifier of the user."},{"field":"source.user.name","type":"keyword","description":"Short name or login of the user."},{"field":"source.user.name.text","type":"text","description":"Short name or login of the user."},{"field":"source.user.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"span.id","type":"keyword","description":"Unique identifier of the span within the scope of its trace."},{"field":"threat.enrichments","type":"nested","description":"List of objects containing indicators enriching the event."},{"field":"threat.enrichments.indicator","type":"object","description":"Object containing indicators enriching the event."},{"field":"threat.enrichments.indicator.as.number","type":"long","description":"Unique number allocated to the autonomous system."},{"field":"threat.enrichments.indicator.as.organization.name","type":"keyword","description":"Organization name."},{"field":"threat.enrichments.indicator.as.organization.name.text","type":"text","description":"Organization name."},{"field":"threat.enrichments.indicator.confidence","type":"keyword","description":"Indicator confidence rating"},{"field":"threat.enrichments.indicator.description","type":"keyword","description":"Indicator description"},{"field":"threat.enrichments.indicator.email.address","type":"keyword","description":"Indicator email address"},{"field":"threat.enrichments.indicator.file.accessed","type":"date","description":"Last time the file was accessed."},{"field":"threat.enrichments.indicator.file.attributes","type":"keyword","description":"Array of file attributes."},{"field":"threat.enrichments.indicator.file.code_signature.exists","type":"boolean","description":"Boolean to capture if a signature is present."},{"field":"threat.enrichments.indicator.file.code_signature.signing_id","type":"keyword","description":"The identifier used to sign the process."},{"field":"threat.enrichments.indicator.file.code_signature.status","type":"keyword","description":"Additional information about the certificate status."},{"field":"threat.enrichments.indicator.file.code_signature.subject_name","type":"keyword","description":"Subject name of the code signer"},{"field":"threat.enrichments.indicator.file.code_signature.team_id","type":"keyword","description":"The team identifier used to sign the process."},{"field":"threat.enrichments.indicator.file.code_signature.trusted","type":"boolean","description":"Stores the trust status of the certificate chain."},{"field":"threat.enrichments.indicator.file.code_signature.valid","type":"boolean","description":"Boolean to capture if the digital signature is verified against the binary content."},{"field":"threat.enrichments.indicator.file.created","type":"date","description":"File creation time."},{"field":"threat.enrichments.indicator.file.ctime","type":"date","description":"Last time the file attributes or metadata changed."},{"field":"threat.enrichments.indicator.file.device","type":"keyword","description":"Device that is the source of the file."},{"field":"threat.enrichments.indicator.file.directory","type":"keyword","description":"Directory where the file is located."},{"field":"threat.enrichments.indicator.file.drive_letter","type":"keyword","description":"Drive letter where the file is located."},{"field":"threat.enrichments.indicator.file.elf.architecture","type":"keyword","description":"Machine architecture of the ELF file."},{"field":"threat.enrichments.indicator.file.elf.byte_order","type":"keyword","description":"Byte sequence of ELF file."},{"field":"threat.enrichments.indicator.file.elf.cpu_type","type":"keyword","description":"CPU type of the ELF file."},{"field":"threat.enrichments.indicator.file.elf.creation_date","type":"date","description":"Build or compile date."},{"field":"threat.enrichments.indicator.file.elf.exports","type":"flattened","description":"List of exported element names and types."},{"field":"threat.enrichments.indicator.file.elf.header.abi_version","type":"keyword","description":"Version of the ELF Application Binary Interface (ABI)."},{"field":"threat.enrichments.indicator.file.elf.header.class","type":"keyword","description":"Header class of the ELF file."},{"field":"threat.enrichments.indicator.file.elf.header.data","type":"keyword","description":"Data table of the ELF header."},{"field":"threat.enrichments.indicator.file.elf.header.entrypoint","type":"long","description":"Header entrypoint of the ELF file."},{"field":"threat.enrichments.indicator.file.elf.header.object_version","type":"keyword","description":"0x1\" for original ELF files."},{"field":"threat.enrichments.indicator.file.elf.header.os_abi","type":"keyword","description":"Application Binary Interface (ABI) of the Linux OS."},{"field":"threat.enrichments.indicator.file.elf.header.type","type":"keyword","description":"Header type of the ELF file."},{"field":"threat.enrichments.indicator.file.elf.header.version","type":"keyword","description":"Version of the ELF header."},{"field":"threat.enrichments.indicator.file.elf.imports","type":"flattened","description":"List of imported element names and types."},{"field":"threat.enrichments.indicator.file.elf.sections","type":"nested","description":"Section information of the ELF file."},{"field":"threat.enrichments.indicator.file.elf.sections.chi2","type":"long","description":"Chi-square probability distribution of the section."},{"field":"threat.enrichments.indicator.file.elf.sections.entropy","type":"long","description":"Shannon entropy calculation from the section."},{"field":"threat.enrichments.indicator.file.elf.sections.flags","type":"keyword","description":"ELF Section List flags."},{"field":"threat.enrichments.indicator.file.elf.sections.name","type":"keyword","description":"ELF Section List name."},{"field":"threat.enrichments.indicator.file.elf.sections.physical_offset","type":"keyword","description":"ELF Section List offset."},{"field":"threat.enrichments.indicator.file.elf.sections.physical_size","type":"long","description":"ELF Section List physical size."},{"field":"threat.enrichments.indicator.file.elf.sections.type","type":"keyword","description":"ELF Section List type."},{"field":"threat.enrichments.indicator.file.elf.sections.virtual_address","type":"long","description":"ELF Section List virtual address."},{"field":"threat.enrichments.indicator.file.elf.sections.virtual_size","type":"long","description":"ELF Section List virtual size."},{"field":"threat.enrichments.indicator.file.elf.segments","type":"nested","description":"ELF object segment list."},{"field":"threat.enrichments.indicator.file.elf.segments.sections","type":"keyword","description":"ELF object segment sections."},{"field":"threat.enrichments.indicator.file.elf.segments.type","type":"keyword","description":"ELF object segment type."},{"field":"threat.enrichments.indicator.file.elf.shared_libraries","type":"keyword","description":"List of shared libraries used by this ELF object."},{"field":"threat.enrichments.indicator.file.elf.telfhash","type":"keyword","description":"telfhash hash for ELF file."},{"field":"threat.enrichments.indicator.file.extension","type":"keyword","description":"File extension, excluding the leading dot."},{"field":"threat.enrichments.indicator.file.gid","type":"keyword","description":"Primary group ID (GID) of the file."},{"field":"threat.enrichments.indicator.file.group","type":"keyword","description":"Primary group name of the file."},{"field":"threat.enrichments.indicator.file.inode","type":"keyword","description":"Inode representing the file in the filesystem."},{"field":"threat.enrichments.indicator.file.mime_type","type":"keyword","description":"Media type of file, document, or arrangement of bytes."},{"field":"threat.enrichments.indicator.file.mode","type":"keyword","description":"Mode of the file in octal representation."},{"field":"threat.enrichments.indicator.file.mtime","type":"date","description":"Last time the file content was modified."},{"field":"threat.enrichments.indicator.file.name","type":"keyword","description":"Name of the file including the extension, without the directory."},{"field":"threat.enrichments.indicator.file.owner","type":"keyword","description":"File owner's username."},{"field":"threat.enrichments.indicator.file.path","type":"keyword","description":"Full path to the file, including the file name."},{"field":"threat.enrichments.indicator.file.path.text","type":"text","description":"Full path to the file, including the file name."},{"field":"threat.enrichments.indicator.file.size","type":"long","description":"File size in bytes."},{"field":"threat.enrichments.indicator.file.target_path","type":"keyword","description":"Target path for symlinks."},{"field":"threat.enrichments.indicator.file.target_path.text","type":"text","description":"Target path for symlinks."},{"field":"threat.enrichments.indicator.file.type","type":"keyword","description":"File type (file, dir, or symlink)."},{"field":"threat.enrichments.indicator.file.uid","type":"keyword","description":"The user ID (UID) or security identifier (SID) of the file owner."},{"field":"threat.enrichments.indicator.first_seen","type":"date","description":"Date/time indicator was first reported."},{"field":"threat.enrichments.indicator.geo.city_name","type":"keyword","description":"City name."},{"field":"threat.enrichments.indicator.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"threat.enrichments.indicator.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"threat.enrichments.indicator.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"threat.enrichments.indicator.geo.country_name","type":"keyword","description":"Country name."},{"field":"threat.enrichments.indicator.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"threat.enrichments.indicator.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"threat.enrichments.indicator.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"threat.enrichments.indicator.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"threat.enrichments.indicator.geo.region_name","type":"keyword","description":"Region name."},{"field":"threat.enrichments.indicator.geo.timezone","type":"keyword","description":"Time zone."},{"field":"threat.enrichments.indicator.hash.md5","type":"keyword","description":"MD5 hash."},{"field":"threat.enrichments.indicator.hash.sha1","type":"keyword","description":"SHA1 hash."},{"field":"threat.enrichments.indicator.hash.sha256","type":"keyword","description":"SHA256 hash."},{"field":"threat.enrichments.indicator.hash.sha512","type":"keyword","description":"SHA512 hash."},{"field":"threat.enrichments.indicator.hash.ssdeep","type":"keyword","description":"SSDEEP hash."},{"field":"threat.enrichments.indicator.ip","type":"ip","description":"Indicator IP address"},{"field":"threat.enrichments.indicator.last_seen","type":"date","description":"Date/time indicator was last reported."},{"field":"threat.enrichments.indicator.marking.tlp","type":"keyword","description":"Indicator TLP marking"},{"field":"threat.enrichments.indicator.modified_at","type":"date","description":"Date/time indicator was last updated."},{"field":"threat.enrichments.indicator.pe.architecture","type":"keyword","description":"CPU architecture target for the file."},{"field":"threat.enrichments.indicator.pe.company","type":"keyword","description":"Internal company name of the file, provided at compile-time."},{"field":"threat.enrichments.indicator.pe.description","type":"keyword","description":"Internal description of the file, provided at compile-time."},{"field":"threat.enrichments.indicator.pe.file_version","type":"keyword","description":"Process name."},{"field":"threat.enrichments.indicator.pe.imphash","type":"keyword","description":"A hash of the imports in a PE file."},{"field":"threat.enrichments.indicator.pe.original_file_name","type":"keyword","description":"Internal name of the file, provided at compile-time."},{"field":"threat.enrichments.indicator.pe.product","type":"keyword","description":"Internal product name of the file, provided at compile-time."},{"field":"threat.enrichments.indicator.port","type":"long","description":"Indicator port"},{"field":"threat.enrichments.indicator.provider","type":"keyword","description":"Indicator provider"},{"field":"threat.enrichments.indicator.reference","type":"keyword","description":"Indicator reference URL"},{"field":"threat.enrichments.indicator.registry.data.bytes","type":"keyword","description":"Original bytes written with base64 encoding."},{"field":"threat.enrichments.indicator.registry.data.strings","type":"keyword","description":"List of strings representing what was written to the registry."},{"field":"threat.enrichments.indicator.registry.data.type","type":"keyword","description":"Standard registry type for encoding contents"},{"field":"threat.enrichments.indicator.registry.hive","type":"keyword","description":"Abbreviated name for the hive."},{"field":"threat.enrichments.indicator.registry.key","type":"keyword","description":"Hive-relative path of keys."},{"field":"threat.enrichments.indicator.registry.path","type":"keyword","description":"Full path, including hive, key and value"},{"field":"threat.enrichments.indicator.registry.value","type":"keyword","description":"Name of the value written."},{"field":"threat.enrichments.indicator.scanner_stats","type":"long","description":"Scanner statistics"},{"field":"threat.enrichments.indicator.sightings","type":"long","description":"Number of times indicator observed"},{"field":"threat.enrichments.indicator.type","type":"keyword","description":"Type of indicator"},{"field":"threat.enrichments.indicator.url.domain","type":"keyword","description":"Domain of the url."},{"field":"threat.enrichments.indicator.url.extension","type":"keyword","description":"File extension from the request url, excluding the leading dot."},{"field":"threat.enrichments.indicator.url.fragment","type":"keyword","description":"Portion of the url after the `#`."},{"field":"threat.enrichments.indicator.url.full","type":"keyword","description":"Full unparsed URL."},{"field":"threat.enrichments.indicator.url.full.text","type":"text","description":"Full unparsed URL."},{"field":"threat.enrichments.indicator.url.original","type":"keyword","description":"Unmodified original url as seen in the event source."},{"field":"threat.enrichments.indicator.url.original.text","type":"text","description":"Unmodified original url as seen in the event source."},{"field":"threat.enrichments.indicator.url.password","type":"keyword","description":"Password of the request."},{"field":"threat.enrichments.indicator.url.path","type":"keyword","description":"Path of the request, such as \"/search\"."},{"field":"threat.enrichments.indicator.url.port","type":"long","description":"Port of the request, such as 443."},{"field":"threat.enrichments.indicator.url.query","type":"keyword","description":"Query string of the request."},{"field":"threat.enrichments.indicator.url.registered_domain","type":"keyword","description":"The highest registered url domain, stripped of the subdomain."},{"field":"threat.enrichments.indicator.url.scheme","type":"keyword","description":"Scheme of the url."},{"field":"threat.enrichments.indicator.url.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"threat.enrichments.indicator.url.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"threat.enrichments.indicator.url.username","type":"keyword","description":"Username of the request."},{"field":"threat.enrichments.indicator.x509.alternative_names","type":"keyword","description":"List of subject alternative names (SAN)."},{"field":"threat.enrichments.indicator.x509.issuer.common_name","type":"keyword","description":"List of common name (CN) of issuing certificate authority."},{"field":"threat.enrichments.indicator.x509.issuer.country","type":"keyword","description":"List of country (C) codes"},{"field":"threat.enrichments.indicator.x509.issuer.distinguished_name","type":"keyword","description":"Distinguished name (DN) of issuing certificate authority."},{"field":"threat.enrichments.indicator.x509.issuer.locality","type":"keyword","description":"List of locality names (L)"},{"field":"threat.enrichments.indicator.x509.issuer.organization","type":"keyword","description":"List of organizations (O) of issuing certificate authority."},{"field":"threat.enrichments.indicator.x509.issuer.organizational_unit","type":"keyword","description":"List of organizational units (OU) of issuing certificate authority."},{"field":"threat.enrichments.indicator.x509.issuer.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"threat.enrichments.indicator.x509.not_after","type":"date","description":"Time at which the certificate is no longer considered valid."},{"field":"threat.enrichments.indicator.x509.not_before","type":"date","description":"Time at which the certificate is first considered valid."},{"field":"threat.enrichments.indicator.x509.public_key_algorithm","type":"keyword","description":"Algorithm used to generate the public key."},{"field":"threat.enrichments.indicator.x509.public_key_curve","type":"keyword","description":"The curve used by the elliptic curve public key algorithm. This is algorithm specific."},{"field":"threat.enrichments.indicator.x509.public_key_exponent","type":"long","description":"Exponent used to derive the public key. This is algorithm specific."},{"field":"threat.enrichments.indicator.x509.public_key_size","type":"long","description":"The size of the public key space in bits."},{"field":"threat.enrichments.indicator.x509.serial_number","type":"keyword","description":"Unique serial number issued by the certificate authority."},{"field":"threat.enrichments.indicator.x509.signature_algorithm","type":"keyword","description":"Identifier for certificate signature algorithm."},{"field":"threat.enrichments.indicator.x509.subject.common_name","type":"keyword","description":"List of common names (CN) of subject."},{"field":"threat.enrichments.indicator.x509.subject.country","type":"keyword","description":"List of country (C) code"},{"field":"threat.enrichments.indicator.x509.subject.distinguished_name","type":"keyword","description":"Distinguished name (DN) of the certificate subject entity."},{"field":"threat.enrichments.indicator.x509.subject.locality","type":"keyword","description":"List of locality names (L)"},{"field":"threat.enrichments.indicator.x509.subject.organization","type":"keyword","description":"List of organizations (O) of subject."},{"field":"threat.enrichments.indicator.x509.subject.organizational_unit","type":"keyword","description":"List of organizational units (OU) of subject."},{"field":"threat.enrichments.indicator.x509.subject.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"threat.enrichments.indicator.x509.version_number","type":"keyword","description":"Version of x509 format."},{"field":"threat.enrichments.matched.atomic","type":"keyword","description":"Matched indicator value"},{"field":"threat.enrichments.matched.field","type":"keyword","description":"Matched indicator field"},{"field":"threat.enrichments.matched.id","type":"keyword","description":"Matched indicator identifier"},{"field":"threat.enrichments.matched.index","type":"keyword","description":"Matched indicator index"},{"field":"threat.enrichments.matched.type","type":"keyword","description":"Type of indicator match"},{"field":"threat.framework","type":"keyword","description":"Threat classification framework."},{"field":"threat.group.alias","type":"keyword","description":"Alias of the group."},{"field":"threat.group.id","type":"keyword","description":"ID of the group."},{"field":"threat.group.name","type":"keyword","description":"Name of the group."},{"field":"threat.group.reference","type":"keyword","description":"Reference URL of the group."},{"field":"threat.indicator.as.number","type":"long","description":"Unique number allocated to the autonomous system."},{"field":"threat.indicator.as.organization.name","type":"keyword","description":"Organization name."},{"field":"threat.indicator.as.organization.name.text","type":"text","description":"Organization name."},{"field":"threat.indicator.confidence","type":"keyword","description":"Indicator confidence rating"},{"field":"threat.indicator.description","type":"keyword","description":"Indicator description"},{"field":"threat.indicator.email.address","type":"keyword","description":"Indicator email address"},{"field":"threat.indicator.file.accessed","type":"date","description":"Last time the file was accessed."},{"field":"threat.indicator.file.attributes","type":"keyword","description":"Array of file attributes."},{"field":"threat.indicator.file.code_signature.exists","type":"boolean","description":"Boolean to capture if a signature is present."},{"field":"threat.indicator.file.code_signature.signing_id","type":"keyword","description":"The identifier used to sign the process."},{"field":"threat.indicator.file.code_signature.status","type":"keyword","description":"Additional information about the certificate status."},{"field":"threat.indicator.file.code_signature.subject_name","type":"keyword","description":"Subject name of the code signer"},{"field":"threat.indicator.file.code_signature.team_id","type":"keyword","description":"The team identifier used to sign the process."},{"field":"threat.indicator.file.code_signature.trusted","type":"boolean","description":"Stores the trust status of the certificate chain."},{"field":"threat.indicator.file.code_signature.valid","type":"boolean","description":"Boolean to capture if the digital signature is verified against the binary content."},{"field":"threat.indicator.file.created","type":"date","description":"File creation time."},{"field":"threat.indicator.file.ctime","type":"date","description":"Last time the file attributes or metadata changed."},{"field":"threat.indicator.file.device","type":"keyword","description":"Device that is the source of the file."},{"field":"threat.indicator.file.directory","type":"keyword","description":"Directory where the file is located."},{"field":"threat.indicator.file.drive_letter","type":"keyword","description":"Drive letter where the file is located."},{"field":"threat.indicator.file.elf.architecture","type":"keyword","description":"Machine architecture of the ELF file."},{"field":"threat.indicator.file.elf.byte_order","type":"keyword","description":"Byte sequence of ELF file."},{"field":"threat.indicator.file.elf.cpu_type","type":"keyword","description":"CPU type of the ELF file."},{"field":"threat.indicator.file.elf.creation_date","type":"date","description":"Build or compile date."},{"field":"threat.indicator.file.elf.exports","type":"flattened","description":"List of exported element names and types."},{"field":"threat.indicator.file.elf.header.abi_version","type":"keyword","description":"Version of the ELF Application Binary Interface (ABI)."},{"field":"threat.indicator.file.elf.header.class","type":"keyword","description":"Header class of the ELF file."},{"field":"threat.indicator.file.elf.header.data","type":"keyword","description":"Data table of the ELF header."},{"field":"threat.indicator.file.elf.header.entrypoint","type":"long","description":"Header entrypoint of the ELF file."},{"field":"threat.indicator.file.elf.header.object_version","type":"keyword","description":"0x1\" for original ELF files."},{"field":"threat.indicator.file.elf.header.os_abi","type":"keyword","description":"Application Binary Interface (ABI) of the Linux OS."},{"field":"threat.indicator.file.elf.header.type","type":"keyword","description":"Header type of the ELF file."},{"field":"threat.indicator.file.elf.header.version","type":"keyword","description":"Version of the ELF header."},{"field":"threat.indicator.file.elf.imports","type":"flattened","description":"List of imported element names and types."},{"field":"threat.indicator.file.elf.sections","type":"nested","description":"Section information of the ELF file."},{"field":"threat.indicator.file.elf.sections.chi2","type":"long","description":"Chi-square probability distribution of the section."},{"field":"threat.indicator.file.elf.sections.entropy","type":"long","description":"Shannon entropy calculation from the section."},{"field":"threat.indicator.file.elf.sections.flags","type":"keyword","description":"ELF Section List flags."},{"field":"threat.indicator.file.elf.sections.name","type":"keyword","description":"ELF Section List name."},{"field":"threat.indicator.file.elf.sections.physical_offset","type":"keyword","description":"ELF Section List offset."},{"field":"threat.indicator.file.elf.sections.physical_size","type":"long","description":"ELF Section List physical size."},{"field":"threat.indicator.file.elf.sections.type","type":"keyword","description":"ELF Section List type."},{"field":"threat.indicator.file.elf.sections.virtual_address","type":"long","description":"ELF Section List virtual address."},{"field":"threat.indicator.file.elf.sections.virtual_size","type":"long","description":"ELF Section List virtual size."},{"field":"threat.indicator.file.elf.segments","type":"nested","description":"ELF object segment list."},{"field":"threat.indicator.file.elf.segments.sections","type":"keyword","description":"ELF object segment sections."},{"field":"threat.indicator.file.elf.segments.type","type":"keyword","description":"ELF object segment type."},{"field":"threat.indicator.file.elf.shared_libraries","type":"keyword","description":"List of shared libraries used by this ELF object."},{"field":"threat.indicator.file.elf.telfhash","type":"keyword","description":"telfhash hash for ELF file."},{"field":"threat.indicator.file.extension","type":"keyword","description":"File extension, excluding the leading dot."},{"field":"threat.indicator.file.gid","type":"keyword","description":"Primary group ID (GID) of the file."},{"field":"threat.indicator.file.group","type":"keyword","description":"Primary group name of the file."},{"field":"threat.indicator.file.inode","type":"keyword","description":"Inode representing the file in the filesystem."},{"field":"threat.indicator.file.mime_type","type":"keyword","description":"Media type of file, document, or arrangement of bytes."},{"field":"threat.indicator.file.mode","type":"keyword","description":"Mode of the file in octal representation."},{"field":"threat.indicator.file.mtime","type":"date","description":"Last time the file content was modified."},{"field":"threat.indicator.file.name","type":"keyword","description":"Name of the file including the extension, without the directory."},{"field":"threat.indicator.file.owner","type":"keyword","description":"File owner's username."},{"field":"threat.indicator.file.path","type":"keyword","description":"Full path to the file, including the file name."},{"field":"threat.indicator.file.path.text","type":"text","description":"Full path to the file, including the file name."},{"field":"threat.indicator.file.size","type":"long","description":"File size in bytes."},{"field":"threat.indicator.file.target_path","type":"keyword","description":"Target path for symlinks."},{"field":"threat.indicator.file.target_path.text","type":"text","description":"Target path for symlinks."},{"field":"threat.indicator.file.type","type":"keyword","description":"File type (file, dir, or symlink)."},{"field":"threat.indicator.file.uid","type":"keyword","description":"The user ID (UID) or security identifier (SID) of the file owner."},{"field":"threat.indicator.first_seen","type":"date","description":"Date/time indicator was first reported."},{"field":"threat.indicator.geo.city_name","type":"keyword","description":"City name."},{"field":"threat.indicator.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"threat.indicator.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"threat.indicator.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"threat.indicator.geo.country_name","type":"keyword","description":"Country name."},{"field":"threat.indicator.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"threat.indicator.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"threat.indicator.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"threat.indicator.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"threat.indicator.geo.region_name","type":"keyword","description":"Region name."},{"field":"threat.indicator.geo.timezone","type":"keyword","description":"Time zone."},{"field":"threat.indicator.hash.md5","type":"keyword","description":"MD5 hash."},{"field":"threat.indicator.hash.sha1","type":"keyword","description":"SHA1 hash."},{"field":"threat.indicator.hash.sha256","type":"keyword","description":"SHA256 hash."},{"field":"threat.indicator.hash.sha512","type":"keyword","description":"SHA512 hash."},{"field":"threat.indicator.hash.ssdeep","type":"keyword","description":"SSDEEP hash."},{"field":"threat.indicator.ip","type":"ip","description":"Indicator IP address"},{"field":"threat.indicator.last_seen","type":"date","description":"Date/time indicator was last reported."},{"field":"threat.indicator.marking.tlp","type":"keyword","description":"Indicator TLP marking"},{"field":"threat.indicator.modified_at","type":"date","description":"Date/time indicator was last updated."},{"field":"threat.indicator.pe.architecture","type":"keyword","description":"CPU architecture target for the file."},{"field":"threat.indicator.pe.company","type":"keyword","description":"Internal company name of the file, provided at compile-time."},{"field":"threat.indicator.pe.description","type":"keyword","description":"Internal description of the file, provided at compile-time."},{"field":"threat.indicator.pe.file_version","type":"keyword","description":"Process name."},{"field":"threat.indicator.pe.imphash","type":"keyword","description":"A hash of the imports in a PE file."},{"field":"threat.indicator.pe.original_file_name","type":"keyword","description":"Internal name of the file, provided at compile-time."},{"field":"threat.indicator.pe.product","type":"keyword","description":"Internal product name of the file, provided at compile-time."},{"field":"threat.indicator.port","type":"long","description":"Indicator port"},{"field":"threat.indicator.provider","type":"keyword","description":"Indicator provider"},{"field":"threat.indicator.reference","type":"keyword","description":"Indicator reference URL"},{"field":"threat.indicator.registry.data.bytes","type":"keyword","description":"Original bytes written with base64 encoding."},{"field":"threat.indicator.registry.data.strings","type":"keyword","description":"List of strings representing what was written to the registry."},{"field":"threat.indicator.registry.data.type","type":"keyword","description":"Standard registry type for encoding contents"},{"field":"threat.indicator.registry.hive","type":"keyword","description":"Abbreviated name for the hive."},{"field":"threat.indicator.registry.key","type":"keyword","description":"Hive-relative path of keys."},{"field":"threat.indicator.registry.path","type":"keyword","description":"Full path, including hive, key and value"},{"field":"threat.indicator.registry.value","type":"keyword","description":"Name of the value written."},{"field":"threat.indicator.scanner_stats","type":"long","description":"Scanner statistics"},{"field":"threat.indicator.sightings","type":"long","description":"Number of times indicator observed"},{"field":"threat.indicator.type","type":"keyword","description":"Type of indicator"},{"field":"threat.indicator.url.domain","type":"keyword","description":"Domain of the url."},{"field":"threat.indicator.url.extension","type":"keyword","description":"File extension from the request url, excluding the leading dot."},{"field":"threat.indicator.url.fragment","type":"keyword","description":"Portion of the url after the `#`."},{"field":"threat.indicator.url.full","type":"keyword","description":"Full unparsed URL."},{"field":"threat.indicator.url.full.text","type":"text","description":"Full unparsed URL."},{"field":"threat.indicator.url.original","type":"keyword","description":"Unmodified original url as seen in the event source."},{"field":"threat.indicator.url.original.text","type":"text","description":"Unmodified original url as seen in the event source."},{"field":"threat.indicator.url.password","type":"keyword","description":"Password of the request."},{"field":"threat.indicator.url.path","type":"keyword","description":"Path of the request, such as \"/search\"."},{"field":"threat.indicator.url.port","type":"long","description":"Port of the request, such as 443."},{"field":"threat.indicator.url.query","type":"keyword","description":"Query string of the request."},{"field":"threat.indicator.url.registered_domain","type":"keyword","description":"The highest registered url domain, stripped of the subdomain."},{"field":"threat.indicator.url.scheme","type":"keyword","description":"Scheme of the url."},{"field":"threat.indicator.url.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"threat.indicator.url.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"threat.indicator.url.username","type":"keyword","description":"Username of the request."},{"field":"threat.indicator.x509.alternative_names","type":"keyword","description":"List of subject alternative names (SAN)."},{"field":"threat.indicator.x509.issuer.common_name","type":"keyword","description":"List of common name (CN) of issuing certificate authority."},{"field":"threat.indicator.x509.issuer.country","type":"keyword","description":"List of country (C) codes"},{"field":"threat.indicator.x509.issuer.distinguished_name","type":"keyword","description":"Distinguished name (DN) of issuing certificate authority."},{"field":"threat.indicator.x509.issuer.locality","type":"keyword","description":"List of locality names (L)"},{"field":"threat.indicator.x509.issuer.organization","type":"keyword","description":"List of organizations (O) of issuing certificate authority."},{"field":"threat.indicator.x509.issuer.organizational_unit","type":"keyword","description":"List of organizational units (OU) of issuing certificate authority."},{"field":"threat.indicator.x509.issuer.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"threat.indicator.x509.not_after","type":"date","description":"Time at which the certificate is no longer considered valid."},{"field":"threat.indicator.x509.not_before","type":"date","description":"Time at which the certificate is first considered valid."},{"field":"threat.indicator.x509.public_key_algorithm","type":"keyword","description":"Algorithm used to generate the public key."},{"field":"threat.indicator.x509.public_key_curve","type":"keyword","description":"The curve used by the elliptic curve public key algorithm. This is algorithm specific."},{"field":"threat.indicator.x509.public_key_exponent","type":"long","description":"Exponent used to derive the public key. This is algorithm specific."},{"field":"threat.indicator.x509.public_key_size","type":"long","description":"The size of the public key space in bits."},{"field":"threat.indicator.x509.serial_number","type":"keyword","description":"Unique serial number issued by the certificate authority."},{"field":"threat.indicator.x509.signature_algorithm","type":"keyword","description":"Identifier for certificate signature algorithm."},{"field":"threat.indicator.x509.subject.common_name","type":"keyword","description":"List of common names (CN) of subject."},{"field":"threat.indicator.x509.subject.country","type":"keyword","description":"List of country (C) code"},{"field":"threat.indicator.x509.subject.distinguished_name","type":"keyword","description":"Distinguished name (DN) of the certificate subject entity."},{"field":"threat.indicator.x509.subject.locality","type":"keyword","description":"List of locality names (L)"},{"field":"threat.indicator.x509.subject.organization","type":"keyword","description":"List of organizations (O) of subject."},{"field":"threat.indicator.x509.subject.organizational_unit","type":"keyword","description":"List of organizational units (OU) of subject."},{"field":"threat.indicator.x509.subject.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"threat.indicator.x509.version_number","type":"keyword","description":"Version of x509 format."},{"field":"threat.software.id","type":"keyword","description":"ID of the software"},{"field":"threat.software.name","type":"keyword","description":"Name of the software."},{"field":"threat.software.platforms","type":"keyword","description":"Platforms of the software."},{"field":"threat.software.reference","type":"keyword","description":"Software reference URL."},{"field":"threat.software.type","type":"keyword","description":"Software type."},{"field":"threat.tactic.id","type":"keyword","description":"Threat tactic id."},{"field":"threat.tactic.name","type":"keyword","description":"Threat tactic."},{"field":"threat.tactic.reference","type":"keyword","description":"Threat tactic URL reference."},{"field":"threat.technique.id","type":"keyword","description":"Threat technique id."},{"field":"threat.technique.name","type":"keyword","description":"Threat technique name."},{"field":"threat.technique.name.text","type":"text","description":"Threat technique name."},{"field":"threat.technique.reference","type":"keyword","description":"Threat technique URL reference."},{"field":"threat.technique.subtechnique.id","type":"keyword","description":"Threat subtechnique id."},{"field":"threat.technique.subtechnique.name","type":"keyword","description":"Threat subtechnique name."},{"field":"threat.technique.subtechnique.name.text","type":"text","description":"Threat subtechnique name."},{"field":"threat.technique.subtechnique.reference","type":"keyword","description":"Threat subtechnique URL reference."},{"field":"tls.cipher","type":"keyword","description":"String indicating the cipher used during the current connection."},{"field":"tls.client.certificate","type":"keyword","description":"PEM-encoded stand-alone certificate offered by the client."},{"field":"tls.client.certificate_chain","type":"keyword","description":"Array of PEM-encoded certificates that make up the certificate chain offered by the client."},{"field":"tls.client.hash.md5","type":"keyword","description":"Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the client."},{"field":"tls.client.hash.sha1","type":"keyword","description":"Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the client."},{"field":"tls.client.hash.sha256","type":"keyword","description":"Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the client."},{"field":"tls.client.issuer","type":"keyword","description":"Distinguished name of subject of the issuer of the x.509 certificate presented by the client."},{"field":"tls.client.ja3","type":"keyword","description":"A hash that identifies clients based on how they perform an SSL/TLS handshake."},{"field":"tls.client.not_after","type":"date","description":"Date/Time indicating when client certificate is no longer considered valid."},{"field":"tls.client.not_before","type":"date","description":"Date/Time indicating when client certificate is first considered valid."},{"field":"tls.client.server_name","type":"keyword","description":"Hostname the client is trying to connect to. Also called the SNI."},{"field":"tls.client.subject","type":"keyword","description":"Distinguished name of subject of the x.509 certificate presented by the client."},{"field":"tls.client.supported_ciphers","type":"keyword","description":"Array of ciphers offered by the client during the client hello."},{"field":"tls.client.x509.alternative_names","type":"keyword","description":"List of subject alternative names (SAN)."},{"field":"tls.client.x509.issuer.common_name","type":"keyword","description":"List of common name (CN) of issuing certificate authority."},{"field":"tls.client.x509.issuer.country","type":"keyword","description":"List of country (C) codes"},{"field":"tls.client.x509.issuer.distinguished_name","type":"keyword","description":"Distinguished name (DN) of issuing certificate authority."},{"field":"tls.client.x509.issuer.locality","type":"keyword","description":"List of locality names (L)"},{"field":"tls.client.x509.issuer.organization","type":"keyword","description":"List of organizations (O) of issuing certificate authority."},{"field":"tls.client.x509.issuer.organizational_unit","type":"keyword","description":"List of organizational units (OU) of issuing certificate authority."},{"field":"tls.client.x509.issuer.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"tls.client.x509.not_after","type":"date","description":"Time at which the certificate is no longer considered valid."},{"field":"tls.client.x509.not_before","type":"date","description":"Time at which the certificate is first considered valid."},{"field":"tls.client.x509.public_key_algorithm","type":"keyword","description":"Algorithm used to generate the public key."},{"field":"tls.client.x509.public_key_curve","type":"keyword","description":"The curve used by the elliptic curve public key algorithm. This is algorithm specific."},{"field":"tls.client.x509.public_key_exponent","type":"long","description":"Exponent used to derive the public key. This is algorithm specific."},{"field":"tls.client.x509.public_key_size","type":"long","description":"The size of the public key space in bits."},{"field":"tls.client.x509.serial_number","type":"keyword","description":"Unique serial number issued by the certificate authority."},{"field":"tls.client.x509.signature_algorithm","type":"keyword","description":"Identifier for certificate signature algorithm."},{"field":"tls.client.x509.subject.common_name","type":"keyword","description":"List of common names (CN) of subject."},{"field":"tls.client.x509.subject.country","type":"keyword","description":"List of country (C) code"},{"field":"tls.client.x509.subject.distinguished_name","type":"keyword","description":"Distinguished name (DN) of the certificate subject entity."},{"field":"tls.client.x509.subject.locality","type":"keyword","description":"List of locality names (L)"},{"field":"tls.client.x509.subject.organization","type":"keyword","description":"List of organizations (O) of subject."},{"field":"tls.client.x509.subject.organizational_unit","type":"keyword","description":"List of organizational units (OU) of subject."},{"field":"tls.client.x509.subject.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"tls.client.x509.version_number","type":"keyword","description":"Version of x509 format."},{"field":"tls.curve","type":"keyword","description":"String indicating the curve used for the given cipher, when applicable."},{"field":"tls.established","type":"boolean","description":"Boolean flag indicating if the TLS negotiation was successful and transitioned to an encrypted tunnel."},{"field":"tls.next_protocol","type":"keyword","description":"String indicating the protocol being tunneled."},{"field":"tls.resumed","type":"boolean","description":"Boolean flag indicating if this TLS connection was resumed from an existing TLS negotiation."},{"field":"tls.server.certificate","type":"keyword","description":"PEM-encoded stand-alone certificate offered by the server."},{"field":"tls.server.certificate_chain","type":"keyword","description":"Array of PEM-encoded certificates that make up the certificate chain offered by the server."},{"field":"tls.server.hash.md5","type":"keyword","description":"Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the server."},{"field":"tls.server.hash.sha1","type":"keyword","description":"Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the server."},{"field":"tls.server.hash.sha256","type":"keyword","description":"Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the server."},{"field":"tls.server.issuer","type":"keyword","description":"Subject of the issuer of the x.509 certificate presented by the server."},{"field":"tls.server.ja3s","type":"keyword","description":"A hash that identifies servers based on how they perform an SSL/TLS handshake."},{"field":"tls.server.not_after","type":"date","description":"Timestamp indicating when server certificate is no longer considered valid."},{"field":"tls.server.not_before","type":"date","description":"Timestamp indicating when server certificate is first considered valid."},{"field":"tls.server.subject","type":"keyword","description":"Subject of the x.509 certificate presented by the server."},{"field":"tls.server.x509.alternative_names","type":"keyword","description":"List of subject alternative names (SAN)."},{"field":"tls.server.x509.issuer.common_name","type":"keyword","description":"List of common name (CN) of issuing certificate authority."},{"field":"tls.server.x509.issuer.country","type":"keyword","description":"List of country (C) codes"},{"field":"tls.server.x509.issuer.distinguished_name","type":"keyword","description":"Distinguished name (DN) of issuing certificate authority."},{"field":"tls.server.x509.issuer.locality","type":"keyword","description":"List of locality names (L)"},{"field":"tls.server.x509.issuer.organization","type":"keyword","description":"List of organizations (O) of issuing certificate authority."},{"field":"tls.server.x509.issuer.organizational_unit","type":"keyword","description":"List of organizational units (OU) of issuing certificate authority."},{"field":"tls.server.x509.issuer.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"tls.server.x509.not_after","type":"date","description":"Time at which the certificate is no longer considered valid."},{"field":"tls.server.x509.not_before","type":"date","description":"Time at which the certificate is first considered valid."},{"field":"tls.server.x509.public_key_algorithm","type":"keyword","description":"Algorithm used to generate the public key."},{"field":"tls.server.x509.public_key_curve","type":"keyword","description":"The curve used by the elliptic curve public key algorithm. This is algorithm specific."},{"field":"tls.server.x509.public_key_exponent","type":"long","description":"Exponent used to derive the public key. This is algorithm specific."},{"field":"tls.server.x509.public_key_size","type":"long","description":"The size of the public key space in bits."},{"field":"tls.server.x509.serial_number","type":"keyword","description":"Unique serial number issued by the certificate authority."},{"field":"tls.server.x509.signature_algorithm","type":"keyword","description":"Identifier for certificate signature algorithm."},{"field":"tls.server.x509.subject.common_name","type":"keyword","description":"List of common names (CN) of subject."},{"field":"tls.server.x509.subject.country","type":"keyword","description":"List of country (C) code"},{"field":"tls.server.x509.subject.distinguished_name","type":"keyword","description":"Distinguished name (DN) of the certificate subject entity."},{"field":"tls.server.x509.subject.locality","type":"keyword","description":"List of locality names (L)"},{"field":"tls.server.x509.subject.organization","type":"keyword","description":"List of organizations (O) of subject."},{"field":"tls.server.x509.subject.organizational_unit","type":"keyword","description":"List of organizational units (OU) of subject."},{"field":"tls.server.x509.subject.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"tls.server.x509.version_number","type":"keyword","description":"Version of x509 format."},{"field":"tls.version","type":"keyword","description":"Numeric part of the version parsed from the original string."},{"field":"tls.version_protocol","type":"keyword","description":"Normalized lowercase protocol name parsed from original string."},{"field":"trace.id","type":"keyword","description":"Unique identifier of the trace."},{"field":"transaction.id","type":"keyword","description":"Unique identifier of the transaction within the scope of its trace."},{"field":"url.domain","type":"keyword","description":"Domain of the url."},{"field":"url.extension","type":"keyword","description":"File extension from the request url, excluding the leading dot."},{"field":"url.fragment","type":"keyword","description":"Portion of the url after the `#`."},{"field":"url.full","type":"keyword","description":"Full unparsed URL."},{"field":"url.full.text","type":"text","description":"Full unparsed URL."},{"field":"url.original","type":"keyword","description":"Unmodified original url as seen in the event source."},{"field":"url.original.text","type":"text","description":"Unmodified original url as seen in the event source."},{"field":"url.password","type":"keyword","description":"Password of the request."},{"field":"url.path","type":"keyword","description":"Path of the request, such as \"/search\"."},{"field":"url.port","type":"long","description":"Port of the request, such as 443."},{"field":"url.query","type":"keyword","description":"Query string of the request."},{"field":"url.registered_domain","type":"keyword","description":"The highest registered url domain, stripped of the subdomain."},{"field":"url.scheme","type":"keyword","description":"Scheme of the url."},{"field":"url.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"url.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"url.username","type":"keyword","description":"Username of the request."},{"field":"user.changes.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"user.changes.email","type":"keyword","description":"User email address."},{"field":"user.changes.full_name","type":"keyword","description":"User's full name, if available."},{"field":"user.changes.full_name.text","type":"text","description":"User's full name, if available."},{"field":"user.changes.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"user.changes.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"user.changes.group.name","type":"keyword","description":"Name of the group."},{"field":"user.changes.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"user.changes.id","type":"keyword","description":"Unique identifier of the user."},{"field":"user.changes.name","type":"keyword","description":"Short name or login of the user."},{"field":"user.changes.name.text","type":"text","description":"Short name or login of the user."},{"field":"user.changes.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"user.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"user.effective.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"user.effective.email","type":"keyword","description":"User email address."},{"field":"user.effective.full_name","type":"keyword","description":"User's full name, if available."},{"field":"user.effective.full_name.text","type":"text","description":"User's full name, if available."},{"field":"user.effective.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"user.effective.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"user.effective.group.name","type":"keyword","description":"Name of the group."},{"field":"user.effective.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"user.effective.id","type":"keyword","description":"Unique identifier of the user."},{"field":"user.effective.name","type":"keyword","description":"Short name or login of the user."},{"field":"user.effective.name.text","type":"text","description":"Short name or login of the user."},{"field":"user.effective.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"user.email","type":"keyword","description":"User email address."},{"field":"user.full_name","type":"keyword","description":"User's full name, if available."},{"field":"user.full_name.text","type":"text","description":"User's full name, if available."},{"field":"user.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"user.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"user.group.name","type":"keyword","description":"Name of the group."},{"field":"user.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"user.id","type":"keyword","description":"Unique identifier of the user."},{"field":"user.name","type":"keyword","description":"Short name or login of the user."},{"field":"user.name.text","type":"text","description":"Short name or login of the user."},{"field":"user.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"user.target.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"user.target.email","type":"keyword","description":"User email address."},{"field":"user.target.full_name","type":"keyword","description":"User's full name, if available."},{"field":"user.target.full_name.text","type":"text","description":"User's full name, if available."},{"field":"user.target.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"user.target.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"user.target.group.name","type":"keyword","description":"Name of the group."},{"field":"user.target.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"user.target.id","type":"keyword","description":"Unique identifier of the user."},{"field":"user.target.name","type":"keyword","description":"Short name or login of the user."},{"field":"user.target.name.text","type":"text","description":"Short name or login of the user."},{"field":"user.target.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"user_agent.device.name","type":"keyword","description":"Name of the device."},{"field":"user_agent.name","type":"keyword","description":"Name of the user agent."},{"field":"user_agent.original","type":"keyword","description":"Unparsed user_agent string."},{"field":"user_agent.original.text","type":"text","description":"Unparsed user_agent string."},{"field":"user_agent.os.family","type":"keyword","description":"OS family (such as redhat, debian, freebsd, windows)."},{"field":"user_agent.os.full","type":"keyword","description":"Operating system name, including the version or code name."},{"field":"user_agent.os.full.text","type":"text","description":"Operating system name, including the version or code name."},{"field":"user_agent.os.kernel","type":"keyword","description":"Operating system kernel version as a raw string."},{"field":"user_agent.os.name","type":"keyword","description":"Operating system name, without the version."},{"field":"user_agent.os.name.text","type":"text","description":"Operating system name, without the version."},{"field":"user_agent.os.platform","type":"keyword","description":"Operating system platform (such centos, ubuntu, windows)."},{"field":"user_agent.os.type","type":"keyword","description":"Which commercial OS family (one of: linux, macos, unix or windows)."},{"field":"user_agent.os.version","type":"keyword","description":"Operating system version as a raw string."},{"field":"user_agent.version","type":"keyword","description":"Version of the user agent."},{"field":"vulnerability.category","type":"keyword","description":"Category of a vulnerability."},{"field":"vulnerability.classification","type":"keyword","description":"Classification of the vulnerability."},{"field":"vulnerability.description","type":"keyword","description":"Description of the vulnerability."},{"field":"vulnerability.description.text","type":"text","description":"Description of the vulnerability."},{"field":"vulnerability.enumeration","type":"keyword","description":"Identifier of the vulnerability."},{"field":"vulnerability.id","type":"keyword","description":"ID of the vulnerability."},{"field":"vulnerability.reference","type":"keyword","description":"Reference of the vulnerability."},{"field":"vulnerability.report_id","type":"keyword","description":"Scan identification number."},{"field":"vulnerability.scanner.vendor","type":"keyword","description":"Name of the scanner vendor."},{"field":"vulnerability.score.base","type":"float","description":"Vulnerability Base score."},{"field":"vulnerability.score.environmental","type":"float","description":"Vulnerability Environmental score."},{"field":"vulnerability.score.temporal","type":"float","description":"Vulnerability Temporal score."},{"field":"vulnerability.score.version","type":"keyword","description":"CVSS version."},{"field":"vulnerability.severity","type":"keyword","description":"Severity of the vulnerability."}] \ No newline at end of file +[{"field":"labels","type":"object","description":"Custom key/value pairs."},{"field":"message","type":"text","description":"Log message optimized for viewing in a log viewer."},{"field":"tags","type":"keyword","description":"List of keywords used to tag each event."},{"field":"agent.build.original","type":"keyword","description":"Extended build information for the agent."},{"field":"client.address","type":"keyword","description":"Client network address."},{"field":"client.as.number","type":"long","description":"Unique number allocated to the autonomous system."},{"field":"client.as.organization.name","type":"keyword","description":"Organization name."},{"field":"client.as.organization.name.text","type":"text","description":"Organization name."},{"field":"client.bytes","type":"long","description":"Bytes sent from the client to the server."},{"field":"client.domain","type":"keyword","description":"Client domain."},{"field":"client.geo.city_name","type":"keyword","description":"City name."},{"field":"client.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"client.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"client.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"client.geo.country_name","type":"keyword","description":"Country name."},{"field":"client.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"client.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"client.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"client.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"client.geo.region_name","type":"keyword","description":"Region name."},{"field":"client.geo.timezone","type":"keyword","description":"Time zone."},{"field":"client.ip","type":"ip","description":"IP address of the client."},{"field":"client.mac","type":"keyword","description":"MAC address of the client."},{"field":"client.nat.ip","type":"ip","description":"Client NAT ip address"},{"field":"client.nat.port","type":"long","description":"Client NAT port"},{"field":"client.packets","type":"long","description":"Packets sent from the client to the server."},{"field":"client.port","type":"long","description":"Port of the client."},{"field":"client.registered_domain","type":"keyword","description":"The highest registered client domain, stripped of the subdomain."},{"field":"client.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"client.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"client.user.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"client.user.email","type":"keyword","description":"User email address."},{"field":"client.user.full_name","type":"keyword","description":"User's full name, if available."},{"field":"client.user.full_name.text","type":"text","description":"User's full name, if available."},{"field":"client.user.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"client.user.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"client.user.group.name","type":"keyword","description":"Name of the group."},{"field":"client.user.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"client.user.id","type":"keyword","description":"Unique identifier of the user."},{"field":"client.user.name","type":"keyword","description":"Short name or login of the user."},{"field":"client.user.name.text","type":"text","description":"Short name or login of the user."},{"field":"client.user.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"cloud.account.id","type":"keyword","description":"The cloud account or organization id."},{"field":"cloud.account.name","type":"keyword","description":"The cloud account name."},{"field":"cloud.availability_zone","type":"keyword","description":"Availability zone in which this host, resource, or service is located."},{"field":"cloud.instance.id","type":"keyword","description":"Instance ID of the host machine."},{"field":"cloud.instance.name","type":"keyword","description":"Instance name of the host machine."},{"field":"cloud.machine.type","type":"keyword","description":"Machine type of the host machine."},{"field":"cloud.project.id","type":"keyword","description":"The cloud project id."},{"field":"cloud.project.name","type":"keyword","description":"The cloud project name."},{"field":"cloud.provider","type":"keyword","description":"Name of the cloud provider."},{"field":"cloud.region","type":"keyword","description":"Region in which this host, resource, or service is located."},{"field":"cloud.service.name","type":"keyword","description":"The cloud service name."},{"field":"container.id","type":"keyword","description":"Unique container id."},{"field":"container.image.name","type":"keyword","description":"Name of the image the container was built on."},{"field":"container.image.tag","type":"keyword","description":"Container image tags."},{"field":"container.labels","type":"object","description":"Image labels."},{"field":"container.name","type":"keyword","description":"Container name."},{"field":"container.runtime","type":"keyword","description":"Runtime managing this container."},{"field":"data_stream.dataset","type":"constant_keyword","description":"The field can contain anything that makes sense to signify the source of the data."},{"field":"data_stream.namespace","type":"constant_keyword","description":"A user defined namespace. Namespaces are useful to allow grouping of data."},{"field":"data_stream.type","type":"constant_keyword","description":"An overarching type for the data stream."},{"field":"destination.address","type":"keyword","description":"Destination network address."},{"field":"destination.as.number","type":"long","description":"Unique number allocated to the autonomous system."},{"field":"destination.as.organization.name","type":"keyword","description":"Organization name."},{"field":"destination.as.organization.name.text","type":"text","description":"Organization name."},{"field":"destination.bytes","type":"long","description":"Bytes sent from the destination to the source."},{"field":"destination.domain","type":"keyword","description":"Destination domain."},{"field":"destination.geo.city_name","type":"keyword","description":"City name."},{"field":"destination.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"destination.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"destination.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"destination.geo.country_name","type":"keyword","description":"Country name."},{"field":"destination.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"destination.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"destination.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"destination.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"destination.geo.region_name","type":"keyword","description":"Region name."},{"field":"destination.geo.timezone","type":"keyword","description":"Time zone."},{"field":"destination.ip","type":"ip","description":"IP address of the destination."},{"field":"destination.mac","type":"keyword","description":"MAC address of the destination."},{"field":"destination.nat.ip","type":"ip","description":"Destination NAT ip"},{"field":"destination.nat.port","type":"long","description":"Destination NAT Port"},{"field":"destination.packets","type":"long","description":"Packets sent from the destination to the source."},{"field":"destination.port","type":"long","description":"Port of the destination."},{"field":"destination.registered_domain","type":"keyword","description":"The highest registered destination domain, stripped of the subdomain."},{"field":"destination.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"destination.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"destination.user.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"destination.user.email","type":"keyword","description":"User email address."},{"field":"destination.user.full_name","type":"keyword","description":"User's full name, if available."},{"field":"destination.user.full_name.text","type":"text","description":"User's full name, if available."},{"field":"destination.user.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"destination.user.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"destination.user.group.name","type":"keyword","description":"Name of the group."},{"field":"destination.user.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"destination.user.id","type":"keyword","description":"Unique identifier of the user."},{"field":"destination.user.name","type":"keyword","description":"Short name or login of the user."},{"field":"destination.user.name.text","type":"text","description":"Short name or login of the user."},{"field":"destination.user.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"dll.code_signature.exists","type":"boolean","description":"Boolean to capture if a signature is present."},{"field":"dll.code_signature.signing_id","type":"keyword","description":"The identifier used to sign the process."},{"field":"dll.code_signature.status","type":"keyword","description":"Additional information about the certificate status."},{"field":"dll.code_signature.subject_name","type":"keyword","description":"Subject name of the code signer"},{"field":"dll.code_signature.team_id","type":"keyword","description":"The team identifier used to sign the process."},{"field":"dll.code_signature.trusted","type":"boolean","description":"Stores the trust status of the certificate chain."},{"field":"dll.code_signature.valid","type":"boolean","description":"Boolean to capture if the digital signature is verified against the binary content."},{"field":"dll.hash.md5","type":"keyword","description":"MD5 hash."},{"field":"dll.hash.sha1","type":"keyword","description":"SHA1 hash."},{"field":"dll.hash.sha256","type":"keyword","description":"SHA256 hash."},{"field":"dll.hash.sha512","type":"keyword","description":"SHA512 hash."},{"field":"dll.hash.ssdeep","type":"keyword","description":"SSDEEP hash."},{"field":"dll.name","type":"keyword","description":"Name of the library."},{"field":"dll.path","type":"keyword","description":"Full file path of the library."},{"field":"dll.pe.architecture","type":"keyword","description":"CPU architecture target for the file."},{"field":"dll.pe.company","type":"keyword","description":"Internal company name of the file, provided at compile-time."},{"field":"dll.pe.description","type":"keyword","description":"Internal description of the file, provided at compile-time."},{"field":"dll.pe.file_version","type":"keyword","description":"Process name."},{"field":"dll.pe.imphash","type":"keyword","description":"A hash of the imports in a PE file."},{"field":"dll.pe.original_file_name","type":"keyword","description":"Internal name of the file, provided at compile-time."},{"field":"dll.pe.product","type":"keyword","description":"Internal product name of the file, provided at compile-time."},{"field":"dns.answers","type":"object","description":"Array of DNS answers."},{"field":"dns.answers.class","type":"keyword","description":"The class of DNS data contained in this resource record."},{"field":"dns.answers.data","type":"keyword","description":"The data describing the resource."},{"field":"dns.answers.name","type":"keyword","description":"The domain name to which this resource record pertains."},{"field":"dns.answers.ttl","type":"long","description":"The time interval in seconds that this resource record may be cached before it should be discarded."},{"field":"dns.answers.type","type":"keyword","description":"The type of data contained in this resource record."},{"field":"dns.header_flags","type":"keyword","description":"Array of DNS header flags."},{"field":"dns.id","type":"keyword","description":"The DNS packet identifier assigned by the program that generated the query. The identifier is copied to the response."},{"field":"dns.op_code","type":"keyword","description":"The DNS operation code that specifies the kind of query in the message."},{"field":"dns.question.class","type":"keyword","description":"The class of records being queried."},{"field":"dns.question.name","type":"keyword","description":"The name being queried."},{"field":"dns.question.registered_domain","type":"keyword","description":"The highest registered domain, stripped of the subdomain."},{"field":"dns.question.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"dns.question.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"dns.question.type","type":"keyword","description":"The type of record being queried."},{"field":"dns.resolved_ip","type":"ip","description":"Array containing all IPs seen in answers.data"},{"field":"dns.response_code","type":"keyword","description":"The DNS response code."},{"field":"dns.type","type":"keyword","description":"The type of DNS event captured, query or answer."},{"field":"error.code","type":"keyword","description":"Error code describing the error."},{"field":"error.id","type":"keyword","description":"Unique identifier for the error."},{"field":"error.message","type":"text","description":"Error message."},{"field":"error.stack_trace","type":"keyword","description":"The stack trace of this error in plain text."},{"field":"error.stack_trace.text","type":"text","description":"The stack trace of this error in plain text."},{"field":"error.type","type":"keyword","description":"The type of the error, for example the class name of the exception."},{"field":"event.action","type":"keyword","description":"The action captured by the event."},{"field":"event.category","type":"keyword","description":"Event category. The second categorization field in the hierarchy."},{"field":"event.code","type":"keyword","description":"Identification code for this event."},{"field":"event.created","type":"date","description":"Time when the event was first read by an agent or by your pipeline."},{"field":"event.dataset","type":"keyword","description":"Name of the dataset."},{"field":"event.duration","type":"long","description":"Duration of the event in nanoseconds."},{"field":"event.end","type":"date","description":"event.end contains the date when the event ended or when the activity was last observed."},{"field":"event.hash","type":"keyword","description":"Hash (perhaps logstash fingerprint) of raw field to be able to demonstrate log integrity."},{"field":"event.id","type":"keyword","description":"Unique ID to describe the event."},{"field":"event.kind","type":"keyword","description":"The kind of the event. The highest categorization field in the hierarchy."},{"field":"event.original","type":"keyword","description":"Raw text message of entire event."},{"field":"event.outcome","type":"keyword","description":"The outcome of the event. The lowest level categorization field in the hierarchy."},{"field":"event.provider","type":"keyword","description":"Source of the event."},{"field":"event.reason","type":"keyword","description":"Reason why this event happened, according to the source"},{"field":"event.reference","type":"keyword","description":"Event reference URL"},{"field":"event.risk_score","type":"float","description":"Risk score or priority of the event (e.g. security solutions). Use your system's original value here."},{"field":"event.risk_score_norm","type":"float","description":"Normalized risk score or priority of the event (0-100)."},{"field":"event.sequence","type":"long","description":"Sequence number of the event."},{"field":"event.severity","type":"long","description":"Numeric severity of the event."},{"field":"event.start","type":"date","description":"event.start contains the date when the event started or when the activity was first observed."},{"field":"event.timezone","type":"keyword","description":"Event time zone."},{"field":"event.type","type":"keyword","description":"Event type. The third categorization field in the hierarchy."},{"field":"event.url","type":"keyword","description":"Event investigation URL"},{"field":"file.accessed","type":"date","description":"Last time the file was accessed."},{"field":"file.attributes","type":"keyword","description":"Array of file attributes."},{"field":"file.code_signature.exists","type":"boolean","description":"Boolean to capture if a signature is present."},{"field":"file.code_signature.signing_id","type":"keyword","description":"The identifier used to sign the process."},{"field":"file.code_signature.status","type":"keyword","description":"Additional information about the certificate status."},{"field":"file.code_signature.subject_name","type":"keyword","description":"Subject name of the code signer"},{"field":"file.code_signature.team_id","type":"keyword","description":"The team identifier used to sign the process."},{"field":"file.code_signature.trusted","type":"boolean","description":"Stores the trust status of the certificate chain."},{"field":"file.code_signature.valid","type":"boolean","description":"Boolean to capture if the digital signature is verified against the binary content."},{"field":"file.created","type":"date","description":"File creation time."},{"field":"file.ctime","type":"date","description":"Last time the file attributes or metadata changed."},{"field":"file.device","type":"keyword","description":"Device that is the source of the file."},{"field":"file.directory","type":"keyword","description":"Directory where the file is located."},{"field":"file.drive_letter","type":"keyword","description":"Drive letter where the file is located."},{"field":"file.elf.architecture","type":"keyword","description":"Machine architecture of the ELF file."},{"field":"file.elf.byte_order","type":"keyword","description":"Byte sequence of ELF file."},{"field":"file.elf.cpu_type","type":"keyword","description":"CPU type of the ELF file."},{"field":"file.elf.creation_date","type":"date","description":"Build or compile date."},{"field":"file.elf.exports","type":"flattened","description":"List of exported element names and types."},{"field":"file.elf.header.abi_version","type":"keyword","description":"Version of the ELF Application Binary Interface (ABI)."},{"field":"file.elf.header.class","type":"keyword","description":"Header class of the ELF file."},{"field":"file.elf.header.data","type":"keyword","description":"Data table of the ELF header."},{"field":"file.elf.header.entrypoint","type":"long","description":"Header entrypoint of the ELF file."},{"field":"file.elf.header.object_version","type":"keyword","description":"0x1\" for original ELF files."},{"field":"file.elf.header.os_abi","type":"keyword","description":"Application Binary Interface (ABI) of the Linux OS."},{"field":"file.elf.header.type","type":"keyword","description":"Header type of the ELF file."},{"field":"file.elf.header.version","type":"keyword","description":"Version of the ELF header."},{"field":"file.elf.imports","type":"flattened","description":"List of imported element names and types."},{"field":"file.elf.sections","type":"nested","description":"Section information of the ELF file."},{"field":"file.elf.sections.chi2","type":"long","description":"Chi-square probability distribution of the section."},{"field":"file.elf.sections.entropy","type":"long","description":"Shannon entropy calculation from the section."},{"field":"file.elf.sections.flags","type":"keyword","description":"ELF Section List flags."},{"field":"file.elf.sections.name","type":"keyword","description":"ELF Section List name."},{"field":"file.elf.sections.physical_offset","type":"keyword","description":"ELF Section List offset."},{"field":"file.elf.sections.physical_size","type":"long","description":"ELF Section List physical size."},{"field":"file.elf.sections.type","type":"keyword","description":"ELF Section List type."},{"field":"file.elf.sections.virtual_address","type":"long","description":"ELF Section List virtual address."},{"field":"file.elf.sections.virtual_size","type":"long","description":"ELF Section List virtual size."},{"field":"file.elf.segments","type":"nested","description":"ELF object segment list."},{"field":"file.elf.segments.sections","type":"keyword","description":"ELF object segment sections."},{"field":"file.elf.segments.type","type":"keyword","description":"ELF object segment type."},{"field":"file.elf.shared_libraries","type":"keyword","description":"List of shared libraries used by this ELF object."},{"field":"file.elf.telfhash","type":"keyword","description":"telfhash hash for ELF file."},{"field":"file.extension","type":"keyword","description":"File extension, excluding the leading dot."},{"field":"file.gid","type":"keyword","description":"Primary group ID (GID) of the file."},{"field":"file.group","type":"keyword","description":"Primary group name of the file."},{"field":"file.hash.md5","type":"keyword","description":"MD5 hash."},{"field":"file.hash.sha1","type":"keyword","description":"SHA1 hash."},{"field":"file.hash.sha256","type":"keyword","description":"SHA256 hash."},{"field":"file.hash.sha512","type":"keyword","description":"SHA512 hash."},{"field":"file.hash.ssdeep","type":"keyword","description":"SSDEEP hash."},{"field":"file.inode","type":"keyword","description":"Inode representing the file in the filesystem."},{"field":"file.mime_type","type":"keyword","description":"Media type of file, document, or arrangement of bytes."},{"field":"file.mode","type":"keyword","description":"Mode of the file in octal representation."},{"field":"file.mtime","type":"date","description":"Last time the file content was modified."},{"field":"file.name","type":"keyword","description":"Name of the file including the extension, without the directory."},{"field":"file.owner","type":"keyword","description":"File owner's username."},{"field":"file.path","type":"keyword","description":"Full path to the file, including the file name."},{"field":"file.path.text","type":"text","description":"Full path to the file, including the file name."},{"field":"file.pe.architecture","type":"keyword","description":"CPU architecture target for the file."},{"field":"file.pe.company","type":"keyword","description":"Internal company name of the file, provided at compile-time."},{"field":"file.pe.description","type":"keyword","description":"Internal description of the file, provided at compile-time."},{"field":"file.pe.file_version","type":"keyword","description":"Process name."},{"field":"file.pe.imphash","type":"keyword","description":"A hash of the imports in a PE file."},{"field":"file.pe.original_file_name","type":"keyword","description":"Internal name of the file, provided at compile-time."},{"field":"file.pe.product","type":"keyword","description":"Internal product name of the file, provided at compile-time."},{"field":"file.size","type":"long","description":"File size in bytes."},{"field":"file.target_path","type":"keyword","description":"Target path for symlinks."},{"field":"file.target_path.text","type":"text","description":"Target path for symlinks."},{"field":"file.type","type":"keyword","description":"File type (file, dir, or symlink)."},{"field":"file.uid","type":"keyword","description":"The user ID (UID) or security identifier (SID) of the file owner."},{"field":"file.x509.alternative_names","type":"keyword","description":"List of subject alternative names (SAN)."},{"field":"file.x509.issuer.common_name","type":"keyword","description":"List of common name (CN) of issuing certificate authority."},{"field":"file.x509.issuer.country","type":"keyword","description":"List of country (C) codes"},{"field":"file.x509.issuer.distinguished_name","type":"keyword","description":"Distinguished name (DN) of issuing certificate authority."},{"field":"file.x509.issuer.locality","type":"keyword","description":"List of locality names (L)"},{"field":"file.x509.issuer.organization","type":"keyword","description":"List of organizations (O) of issuing certificate authority."},{"field":"file.x509.issuer.organizational_unit","type":"keyword","description":"List of organizational units (OU) of issuing certificate authority."},{"field":"file.x509.issuer.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"file.x509.not_after","type":"date","description":"Time at which the certificate is no longer considered valid."},{"field":"file.x509.not_before","type":"date","description":"Time at which the certificate is first considered valid."},{"field":"file.x509.public_key_algorithm","type":"keyword","description":"Algorithm used to generate the public key."},{"field":"file.x509.public_key_curve","type":"keyword","description":"The curve used by the elliptic curve public key algorithm. This is algorithm specific."},{"field":"file.x509.public_key_exponent","type":"long","description":"Exponent used to derive the public key. This is algorithm specific."},{"field":"file.x509.public_key_size","type":"long","description":"The size of the public key space in bits."},{"field":"file.x509.serial_number","type":"keyword","description":"Unique serial number issued by the certificate authority."},{"field":"file.x509.signature_algorithm","type":"keyword","description":"Identifier for certificate signature algorithm."},{"field":"file.x509.subject.common_name","type":"keyword","description":"List of common names (CN) of subject."},{"field":"file.x509.subject.country","type":"keyword","description":"List of country (C) code"},{"field":"file.x509.subject.distinguished_name","type":"keyword","description":"Distinguished name (DN) of the certificate subject entity."},{"field":"file.x509.subject.locality","type":"keyword","description":"List of locality names (L)"},{"field":"file.x509.subject.organization","type":"keyword","description":"List of organizations (O) of subject."},{"field":"file.x509.subject.organizational_unit","type":"keyword","description":"List of organizational units (OU) of subject."},{"field":"file.x509.subject.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"file.x509.version_number","type":"keyword","description":"Version of x509 format."},{"field":"group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"group.name","type":"keyword","description":"Name of the group."},{"field":"host.cpu.usage","type":"scaled_float","description":"Percent CPU used, between 0 and 1."},{"field":"host.disk.read.bytes","type":"long","description":"The number of bytes read by all disks."},{"field":"host.disk.write.bytes","type":"long","description":"The number of bytes written on all disks."},{"field":"host.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"host.geo.city_name","type":"keyword","description":"City name."},{"field":"host.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"host.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"host.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"host.geo.country_name","type":"keyword","description":"Country name."},{"field":"host.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"host.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"host.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"host.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"host.geo.region_name","type":"keyword","description":"Region name."},{"field":"host.geo.timezone","type":"keyword","description":"Time zone."},{"field":"host.name","type":"keyword","description":"Name of the host."},{"field":"host.network.egress.bytes","type":"long","description":"The number of bytes sent on all network interfaces."},{"field":"host.network.egress.packets","type":"long","description":"The number of packets sent on all network interfaces."},{"field":"host.network.ingress.bytes","type":"long","description":"The number of bytes received on all network interfaces."},{"field":"host.network.ingress.packets","type":"long","description":"The number of packets received on all network interfaces."},{"field":"host.os.full","type":"keyword","description":"Operating system name, including the version or code name."},{"field":"host.os.full.text","type":"text","description":"Operating system name, including the version or code name."},{"field":"host.os.name.text","type":"text","description":"Operating system name, without the version."},{"field":"host.os.platform","type":"keyword","description":"Operating system platform (such centos, ubuntu, windows)."},{"field":"host.type","type":"keyword","description":"Type of host."},{"field":"host.uptime","type":"long","description":"Seconds the host has been up."},{"field":"host.user.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"host.user.email","type":"keyword","description":"User email address."},{"field":"host.user.full_name","type":"keyword","description":"User's full name, if available."},{"field":"host.user.full_name.text","type":"text","description":"User's full name, if available."},{"field":"host.user.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"host.user.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"host.user.group.name","type":"keyword","description":"Name of the group."},{"field":"host.user.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"host.user.id","type":"keyword","description":"Unique identifier of the user."},{"field":"host.user.name","type":"keyword","description":"Short name or login of the user."},{"field":"host.user.name.text","type":"text","description":"Short name or login of the user."},{"field":"host.user.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"http.request.body.bytes","type":"long","description":"Size in bytes of the request body."},{"field":"http.request.body.content","type":"keyword","description":"The full HTTP request body."},{"field":"http.request.body.content.text","type":"text","description":"The full HTTP request body."},{"field":"http.request.bytes","type":"long","description":"Total size in bytes of the request (body and headers)."},{"field":"http.request.id","type":"keyword","description":"HTTP request ID."},{"field":"http.request.method","type":"keyword","description":"HTTP request method."},{"field":"http.request.mime_type","type":"keyword","description":"Mime type of the body of the request."},{"field":"http.request.referrer","type":"keyword","description":"Referrer for this HTTP request."},{"field":"http.response.body.bytes","type":"long","description":"Size in bytes of the response body."},{"field":"http.response.body.content","type":"keyword","description":"The full HTTP response body."},{"field":"http.response.body.content.text","type":"text","description":"The full HTTP response body."},{"field":"http.response.bytes","type":"long","description":"Total size in bytes of the response (body and headers)."},{"field":"http.response.mime_type","type":"keyword","description":"Mime type of the body of the response."},{"field":"http.response.status_code","type":"long","description":"HTTP response status code."},{"field":"http.version","type":"keyword","description":"HTTP version."},{"field":"log.file.path","type":"keyword","description":"Full path to the log file this event came from."},{"field":"log.level","type":"keyword","description":"Log level of the log event."},{"field":"log.logger","type":"keyword","description":"Name of the logger."},{"field":"log.origin.file.line","type":"integer","description":"The line number of the file which originated the log event."},{"field":"log.origin.file.name","type":"keyword","description":"The code file which originated the log event."},{"field":"log.origin.function","type":"keyword","description":"The function which originated the log event."},{"field":"log.original","type":"keyword","description":"Deprecated original log message with light interpretation only (encoding, newlines)."},{"field":"log.syslog","type":"object","description":"Syslog metadata"},{"field":"log.syslog.facility.code","type":"long","description":"Syslog numeric facility of the event."},{"field":"log.syslog.facility.name","type":"keyword","description":"Syslog text-based facility of the event."},{"field":"log.syslog.priority","type":"long","description":"Syslog priority of the event."},{"field":"log.syslog.severity.code","type":"long","description":"Syslog numeric severity of the event."},{"field":"log.syslog.severity.name","type":"keyword","description":"Syslog text-based severity of the event."},{"field":"network.application","type":"keyword","description":"Application level protocol name."},{"field":"network.bytes","type":"long","description":"Total bytes transferred in both directions."},{"field":"network.community_id","type":"keyword","description":"A hash of source and destination IPs and ports."},{"field":"network.direction","type":"keyword","description":"Direction of the network traffic."},{"field":"network.forwarded_ip","type":"ip","description":"Host IP address when the source IP address is the proxy."},{"field":"network.iana_number","type":"keyword","description":"IANA Protocol Number."},{"field":"network.inner","type":"object","description":"Inner VLAN tag information"},{"field":"network.inner.vlan.id","type":"keyword","description":"VLAN ID as reported by the observer."},{"field":"network.inner.vlan.name","type":"keyword","description":"Optional VLAN name as reported by the observer."},{"field":"network.name","type":"keyword","description":"Name given by operators to sections of their network."},{"field":"network.packets","type":"long","description":"Total packets transferred in both directions."},{"field":"network.protocol","type":"keyword","description":"L7 Network protocol name."},{"field":"network.transport","type":"keyword","description":"Protocol Name corresponding to the field `iana_number`."},{"field":"network.type","type":"keyword","description":"In the OSI Model this would be the Network Layer. ipv4, ipv6, ipsec, pim, etc"},{"field":"network.vlan.id","type":"keyword","description":"VLAN ID as reported by the observer."},{"field":"network.vlan.name","type":"keyword","description":"Optional VLAN name as reported by the observer."},{"field":"observer.egress","type":"object","description":"Object field for egress information"},{"field":"observer.egress.interface.alias","type":"keyword","description":"Interface alias"},{"field":"observer.egress.interface.id","type":"keyword","description":"Interface ID"},{"field":"observer.egress.interface.name","type":"keyword","description":"Interface name"},{"field":"observer.egress.vlan.id","type":"keyword","description":"VLAN ID as reported by the observer."},{"field":"observer.egress.vlan.name","type":"keyword","description":"Optional VLAN name as reported by the observer."},{"field":"observer.egress.zone","type":"keyword","description":"Observer Egress zone"},{"field":"observer.geo.city_name","type":"keyword","description":"City name."},{"field":"observer.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"observer.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"observer.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"observer.geo.country_name","type":"keyword","description":"Country name."},{"field":"observer.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"observer.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"observer.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"observer.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"observer.geo.region_name","type":"keyword","description":"Region name."},{"field":"observer.geo.timezone","type":"keyword","description":"Time zone."},{"field":"observer.hostname","type":"keyword","description":"Hostname of the observer."},{"field":"observer.ingress","type":"object","description":"Object field for ingress information"},{"field":"observer.ingress.interface.alias","type":"keyword","description":"Interface alias"},{"field":"observer.ingress.interface.id","type":"keyword","description":"Interface ID"},{"field":"observer.ingress.interface.name","type":"keyword","description":"Interface name"},{"field":"observer.ingress.vlan.id","type":"keyword","description":"VLAN ID as reported by the observer."},{"field":"observer.ingress.vlan.name","type":"keyword","description":"Optional VLAN name as reported by the observer."},{"field":"observer.ingress.zone","type":"keyword","description":"Observer ingress zone"},{"field":"observer.ip","type":"ip","description":"IP addresses of the observer."},{"field":"observer.mac","type":"keyword","description":"MAC addresses of the observer."},{"field":"observer.name","type":"keyword","description":"Custom name of the observer."},{"field":"observer.os.family","type":"keyword","description":"OS family (such as redhat, debian, freebsd, windows)."},{"field":"observer.os.full","type":"keyword","description":"Operating system name, including the version or code name."},{"field":"observer.os.full.text","type":"text","description":"Operating system name, including the version or code name."},{"field":"observer.os.kernel","type":"keyword","description":"Operating system kernel version as a raw string."},{"field":"observer.os.name","type":"keyword","description":"Operating system name, without the version."},{"field":"observer.os.name.text","type":"text","description":"Operating system name, without the version."},{"field":"observer.os.platform","type":"keyword","description":"Operating system platform (such centos, ubuntu, windows)."},{"field":"observer.os.type","type":"keyword","description":"Which commercial OS family (one of: linux, macos, unix or windows)."},{"field":"observer.os.version","type":"keyword","description":"Operating system version as a raw string."},{"field":"observer.product","type":"keyword","description":"The product name of the observer."},{"field":"observer.serial_number","type":"keyword","description":"Observer serial number."},{"field":"observer.type","type":"keyword","description":"The type of the observer the data is coming from."},{"field":"observer.vendor","type":"keyword","description":"Vendor name of the observer."},{"field":"observer.version","type":"keyword","description":"Observer version."},{"field":"orchestrator.api_version","type":"keyword","description":"API version being used to carry out the action"},{"field":"orchestrator.cluster.name","type":"keyword","description":"Name of the cluster."},{"field":"orchestrator.cluster.url","type":"keyword","description":"URL of the API used to manage the cluster."},{"field":"orchestrator.cluster.version","type":"keyword","description":"The version of the cluster."},{"field":"orchestrator.namespace","type":"keyword","description":"Namespace in which the action is taking place."},{"field":"orchestrator.organization","type":"keyword","description":"Organization affected by the event (for multi-tenant orchestrator setups)."},{"field":"orchestrator.resource.name","type":"keyword","description":"Name of the resource being acted upon."},{"field":"orchestrator.resource.type","type":"keyword","description":"Type of resource being acted upon."},{"field":"orchestrator.type","type":"keyword","description":"Orchestrator cluster type (e.g. kubernetes, nomad or cloudfoundry)."},{"field":"organization.id","type":"keyword","description":"Unique identifier for the organization."},{"field":"organization.name","type":"keyword","description":"Organization name."},{"field":"organization.name.text","type":"text","description":"Organization name."},{"field":"package.architecture","type":"keyword","description":"Package architecture."},{"field":"package.build_version","type":"keyword","description":"Build version information"},{"field":"package.checksum","type":"keyword","description":"Checksum of the installed package for verification."},{"field":"package.description","type":"keyword","description":"Description of the package."},{"field":"package.install_scope","type":"keyword","description":"Indicating how the package was installed, e.g. user-local, global."},{"field":"package.installed","type":"date","description":"Time when package was installed."},{"field":"package.license","type":"keyword","description":"Package license"},{"field":"package.name","type":"keyword","description":"Package name"},{"field":"package.path","type":"keyword","description":"Path where the package is installed."},{"field":"package.reference","type":"keyword","description":"Package home page or reference URL"},{"field":"package.size","type":"long","description":"Package size in bytes."},{"field":"package.type","type":"keyword","description":"Package type"},{"field":"package.version","type":"keyword","description":"Package version"},{"field":"process.args","type":"keyword","description":"Array of process arguments."},{"field":"process.args_count","type":"long","description":"Length of the process.args array."},{"field":"process.code_signature.exists","type":"boolean","description":"Boolean to capture if a signature is present."},{"field":"process.code_signature.signing_id","type":"keyword","description":"The identifier used to sign the process."},{"field":"process.code_signature.status","type":"keyword","description":"Additional information about the certificate status."},{"field":"process.code_signature.subject_name","type":"keyword","description":"Subject name of the code signer"},{"field":"process.code_signature.team_id","type":"keyword","description":"The team identifier used to sign the process."},{"field":"process.code_signature.trusted","type":"boolean","description":"Stores the trust status of the certificate chain."},{"field":"process.code_signature.valid","type":"boolean","description":"Boolean to capture if the digital signature is verified against the binary content."},{"field":"process.command_line","type":"keyword","description":"Full command line that started the process."},{"field":"process.command_line.text","type":"text","description":"Full command line that started the process."},{"field":"process.elf.architecture","type":"keyword","description":"Machine architecture of the ELF file."},{"field":"process.elf.byte_order","type":"keyword","description":"Byte sequence of ELF file."},{"field":"process.elf.cpu_type","type":"keyword","description":"CPU type of the ELF file."},{"field":"process.elf.creation_date","type":"date","description":"Build or compile date."},{"field":"process.elf.exports","type":"flattened","description":"List of exported element names and types."},{"field":"process.elf.header.abi_version","type":"keyword","description":"Version of the ELF Application Binary Interface (ABI)."},{"field":"process.elf.header.class","type":"keyword","description":"Header class of the ELF file."},{"field":"process.elf.header.data","type":"keyword","description":"Data table of the ELF header."},{"field":"process.elf.header.entrypoint","type":"long","description":"Header entrypoint of the ELF file."},{"field":"process.elf.header.object_version","type":"keyword","description":"0x1\" for original ELF files."},{"field":"process.elf.header.os_abi","type":"keyword","description":"Application Binary Interface (ABI) of the Linux OS."},{"field":"process.elf.header.type","type":"keyword","description":"Header type of the ELF file."},{"field":"process.elf.header.version","type":"keyword","description":"Version of the ELF header."},{"field":"process.elf.imports","type":"flattened","description":"List of imported element names and types."},{"field":"process.elf.sections","type":"nested","description":"Section information of the ELF file."},{"field":"process.elf.sections.chi2","type":"long","description":"Chi-square probability distribution of the section."},{"field":"process.elf.sections.entropy","type":"long","description":"Shannon entropy calculation from the section."},{"field":"process.elf.sections.flags","type":"keyword","description":"ELF Section List flags."},{"field":"process.elf.sections.name","type":"keyword","description":"ELF Section List name."},{"field":"process.elf.sections.physical_offset","type":"keyword","description":"ELF Section List offset."},{"field":"process.elf.sections.physical_size","type":"long","description":"ELF Section List physical size."},{"field":"process.elf.sections.type","type":"keyword","description":"ELF Section List type."},{"field":"process.elf.sections.virtual_address","type":"long","description":"ELF Section List virtual address."},{"field":"process.elf.sections.virtual_size","type":"long","description":"ELF Section List virtual size."},{"field":"process.elf.segments","type":"nested","description":"ELF object segment list."},{"field":"process.elf.segments.sections","type":"keyword","description":"ELF object segment sections."},{"field":"process.elf.segments.type","type":"keyword","description":"ELF object segment type."},{"field":"process.elf.shared_libraries","type":"keyword","description":"List of shared libraries used by this ELF object."},{"field":"process.elf.telfhash","type":"keyword","description":"telfhash hash for ELF file."},{"field":"process.entity_id","type":"keyword","description":"Unique identifier for the process."},{"field":"process.executable","type":"keyword","description":"Absolute path to the process executable."},{"field":"process.executable.text","type":"text","description":"Absolute path to the process executable."},{"field":"process.exit_code","type":"long","description":"The exit code of the process."},{"field":"process.hash.md5","type":"keyword","description":"MD5 hash."},{"field":"process.hash.sha1","type":"keyword","description":"SHA1 hash."},{"field":"process.hash.sha256","type":"keyword","description":"SHA256 hash."},{"field":"process.hash.sha512","type":"keyword","description":"SHA512 hash."},{"field":"process.hash.ssdeep","type":"keyword","description":"SSDEEP hash."},{"field":"process.name","type":"keyword","description":"Process name."},{"field":"process.name.text","type":"text","description":"Process name."},{"field":"process.parent.args","type":"keyword","description":"Array of process arguments."},{"field":"process.parent.args_count","type":"long","description":"Length of the process.args array."},{"field":"process.parent.code_signature.exists","type":"boolean","description":"Boolean to capture if a signature is present."},{"field":"process.parent.code_signature.signing_id","type":"keyword","description":"The identifier used to sign the process."},{"field":"process.parent.code_signature.status","type":"keyword","description":"Additional information about the certificate status."},{"field":"process.parent.code_signature.subject_name","type":"keyword","description":"Subject name of the code signer"},{"field":"process.parent.code_signature.team_id","type":"keyword","description":"The team identifier used to sign the process."},{"field":"process.parent.code_signature.trusted","type":"boolean","description":"Stores the trust status of the certificate chain."},{"field":"process.parent.code_signature.valid","type":"boolean","description":"Boolean to capture if the digital signature is verified against the binary content."},{"field":"process.parent.command_line","type":"keyword","description":"Full command line that started the process."},{"field":"process.parent.command_line.text","type":"text","description":"Full command line that started the process."},{"field":"process.parent.elf.architecture","type":"keyword","description":"Machine architecture of the ELF file."},{"field":"process.parent.elf.byte_order","type":"keyword","description":"Byte sequence of ELF file."},{"field":"process.parent.elf.cpu_type","type":"keyword","description":"CPU type of the ELF file."},{"field":"process.parent.elf.creation_date","type":"date","description":"Build or compile date."},{"field":"process.parent.elf.exports","type":"flattened","description":"List of exported element names and types."},{"field":"process.parent.elf.header.abi_version","type":"keyword","description":"Version of the ELF Application Binary Interface (ABI)."},{"field":"process.parent.elf.header.class","type":"keyword","description":"Header class of the ELF file."},{"field":"process.parent.elf.header.data","type":"keyword","description":"Data table of the ELF header."},{"field":"process.parent.elf.header.entrypoint","type":"long","description":"Header entrypoint of the ELF file."},{"field":"process.parent.elf.header.object_version","type":"keyword","description":"0x1\" for original ELF files."},{"field":"process.parent.elf.header.os_abi","type":"keyword","description":"Application Binary Interface (ABI) of the Linux OS."},{"field":"process.parent.elf.header.type","type":"keyword","description":"Header type of the ELF file."},{"field":"process.parent.elf.header.version","type":"keyword","description":"Version of the ELF header."},{"field":"process.parent.elf.imports","type":"flattened","description":"List of imported element names and types."},{"field":"process.parent.elf.sections","type":"nested","description":"Section information of the ELF file."},{"field":"process.parent.elf.sections.chi2","type":"long","description":"Chi-square probability distribution of the section."},{"field":"process.parent.elf.sections.entropy","type":"long","description":"Shannon entropy calculation from the section."},{"field":"process.parent.elf.sections.flags","type":"keyword","description":"ELF Section List flags."},{"field":"process.parent.elf.sections.name","type":"keyword","description":"ELF Section List name."},{"field":"process.parent.elf.sections.physical_offset","type":"keyword","description":"ELF Section List offset."},{"field":"process.parent.elf.sections.physical_size","type":"long","description":"ELF Section List physical size."},{"field":"process.parent.elf.sections.type","type":"keyword","description":"ELF Section List type."},{"field":"process.parent.elf.sections.virtual_address","type":"long","description":"ELF Section List virtual address."},{"field":"process.parent.elf.sections.virtual_size","type":"long","description":"ELF Section List virtual size."},{"field":"process.parent.elf.segments","type":"nested","description":"ELF object segment list."},{"field":"process.parent.elf.segments.sections","type":"keyword","description":"ELF object segment sections."},{"field":"process.parent.elf.segments.type","type":"keyword","description":"ELF object segment type."},{"field":"process.parent.elf.shared_libraries","type":"keyword","description":"List of shared libraries used by this ELF object."},{"field":"process.parent.elf.telfhash","type":"keyword","description":"telfhash hash for ELF file."},{"field":"process.parent.entity_id","type":"keyword","description":"Unique identifier for the process."},{"field":"process.parent.executable","type":"keyword","description":"Absolute path to the process executable."},{"field":"process.parent.executable.text","type":"text","description":"Absolute path to the process executable."},{"field":"process.parent.exit_code","type":"long","description":"The exit code of the process."},{"field":"process.parent.hash.md5","type":"keyword","description":"MD5 hash."},{"field":"process.parent.hash.sha1","type":"keyword","description":"SHA1 hash."},{"field":"process.parent.hash.sha256","type":"keyword","description":"SHA256 hash."},{"field":"process.parent.hash.sha512","type":"keyword","description":"SHA512 hash."},{"field":"process.parent.hash.ssdeep","type":"keyword","description":"SSDEEP hash."},{"field":"process.parent.name","type":"keyword","description":"Process name."},{"field":"process.parent.name.text","type":"text","description":"Process name."},{"field":"process.parent.pe.architecture","type":"keyword","description":"CPU architecture target for the file."},{"field":"process.parent.pe.company","type":"keyword","description":"Internal company name of the file, provided at compile-time."},{"field":"process.parent.pe.description","type":"keyword","description":"Internal description of the file, provided at compile-time."},{"field":"process.parent.pe.file_version","type":"keyword","description":"Process name."},{"field":"process.parent.pe.imphash","type":"keyword","description":"A hash of the imports in a PE file."},{"field":"process.parent.pe.original_file_name","type":"keyword","description":"Internal name of the file, provided at compile-time."},{"field":"process.parent.pe.product","type":"keyword","description":"Internal product name of the file, provided at compile-time."},{"field":"process.parent.pgid","type":"long","description":"Identifier of the group of processes the process belongs to."},{"field":"process.parent.pid","type":"long","description":"Process id."},{"field":"process.parent.ppid","type":"long","description":"Parent process' pid."},{"field":"process.parent.start","type":"date","description":"The time the process started."},{"field":"process.parent.thread.id","type":"long","description":"Thread ID."},{"field":"process.parent.thread.name","type":"keyword","description":"Thread name."},{"field":"process.parent.title","type":"keyword","description":"Process title."},{"field":"process.parent.title.text","type":"text","description":"Process title."},{"field":"process.parent.uptime","type":"long","description":"Seconds the process has been up."},{"field":"process.parent.working_directory","type":"keyword","description":"The working directory of the process."},{"field":"process.parent.working_directory.text","type":"text","description":"The working directory of the process."},{"field":"process.pe.architecture","type":"keyword","description":"CPU architecture target for the file."},{"field":"process.pe.company","type":"keyword","description":"Internal company name of the file, provided at compile-time."},{"field":"process.pe.description","type":"keyword","description":"Internal description of the file, provided at compile-time."},{"field":"process.pe.file_version","type":"keyword","description":"Process name."},{"field":"process.pe.imphash","type":"keyword","description":"A hash of the imports in a PE file."},{"field":"process.pe.original_file_name","type":"keyword","description":"Internal name of the file, provided at compile-time."},{"field":"process.pe.product","type":"keyword","description":"Internal product name of the file, provided at compile-time."},{"field":"process.pgid","type":"long","description":"Identifier of the group of processes the process belongs to."},{"field":"process.pid","type":"long","description":"Process id."},{"field":"process.ppid","type":"long","description":"Parent process' pid."},{"field":"process.start","type":"date","description":"The time the process started."},{"field":"process.thread.id","type":"long","description":"Thread ID."},{"field":"process.thread.name","type":"keyword","description":"Thread name."},{"field":"process.title","type":"keyword","description":"Process title."},{"field":"process.title.text","type":"text","description":"Process title."},{"field":"process.uptime","type":"long","description":"Seconds the process has been up."},{"field":"process.working_directory","type":"keyword","description":"The working directory of the process."},{"field":"process.working_directory.text","type":"text","description":"The working directory of the process."},{"field":"registry.data.bytes","type":"keyword","description":"Original bytes written with base64 encoding."},{"field":"registry.data.strings","type":"keyword","description":"List of strings representing what was written to the registry."},{"field":"registry.data.type","type":"keyword","description":"Standard registry type for encoding contents"},{"field":"registry.hive","type":"keyword","description":"Abbreviated name for the hive."},{"field":"registry.key","type":"keyword","description":"Hive-relative path of keys."},{"field":"registry.path","type":"keyword","description":"Full path, including hive, key and value"},{"field":"registry.value","type":"keyword","description":"Name of the value written."},{"field":"related.hash","type":"keyword","description":"All the hashes seen on your event."},{"field":"related.hosts","type":"keyword","description":"All the host identifiers seen on your event."},{"field":"related.ip","type":"ip","description":"All of the IPs seen on your event."},{"field":"related.user","type":"keyword","description":"All the user names or other user identifiers seen on the event."},{"field":"rule.author","type":"keyword","description":"Rule author"},{"field":"rule.category","type":"keyword","description":"Rule category"},{"field":"rule.description","type":"keyword","description":"Rule description"},{"field":"rule.id","type":"keyword","description":"Rule ID"},{"field":"rule.license","type":"keyword","description":"Rule license"},{"field":"rule.name","type":"keyword","description":"Rule name"},{"field":"rule.reference","type":"keyword","description":"Rule reference URL"},{"field":"rule.ruleset","type":"keyword","description":"Rule ruleset"},{"field":"rule.uuid","type":"keyword","description":"Rule UUID"},{"field":"rule.version","type":"keyword","description":"Rule version"},{"field":"server.address","type":"keyword","description":"Server network address."},{"field":"server.as.number","type":"long","description":"Unique number allocated to the autonomous system."},{"field":"server.as.organization.name","type":"keyword","description":"Organization name."},{"field":"server.as.organization.name.text","type":"text","description":"Organization name."},{"field":"server.bytes","type":"long","description":"Bytes sent from the server to the client."},{"field":"server.domain","type":"keyword","description":"Server domain."},{"field":"server.geo.city_name","type":"keyword","description":"City name."},{"field":"server.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"server.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"server.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"server.geo.country_name","type":"keyword","description":"Country name."},{"field":"server.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"server.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"server.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"server.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"server.geo.region_name","type":"keyword","description":"Region name."},{"field":"server.geo.timezone","type":"keyword","description":"Time zone."},{"field":"server.ip","type":"ip","description":"IP address of the server."},{"field":"server.mac","type":"keyword","description":"MAC address of the server."},{"field":"server.nat.ip","type":"ip","description":"Server NAT ip"},{"field":"server.nat.port","type":"long","description":"Server NAT port"},{"field":"server.packets","type":"long","description":"Packets sent from the server to the client."},{"field":"server.port","type":"long","description":"Port of the server."},{"field":"server.registered_domain","type":"keyword","description":"The highest registered server domain, stripped of the subdomain."},{"field":"server.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"server.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"server.user.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"server.user.email","type":"keyword","description":"User email address."},{"field":"server.user.full_name","type":"keyword","description":"User's full name, if available."},{"field":"server.user.full_name.text","type":"text","description":"User's full name, if available."},{"field":"server.user.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"server.user.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"server.user.group.name","type":"keyword","description":"Name of the group."},{"field":"server.user.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"server.user.id","type":"keyword","description":"Unique identifier of the user."},{"field":"server.user.name","type":"keyword","description":"Short name or login of the user."},{"field":"server.user.name.text","type":"text","description":"Short name or login of the user."},{"field":"server.user.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"service.ephemeral_id","type":"keyword","description":"Ephemeral identifier of this service."},{"field":"service.id","type":"keyword","description":"Unique identifier of the running service."},{"field":"service.name","type":"keyword","description":"Name of the service."},{"field":"service.node.name","type":"keyword","description":"Name of the service node."},{"field":"service.state","type":"keyword","description":"Current state of the service."},{"field":"service.type","type":"keyword","description":"The type of the service."},{"field":"service.version","type":"keyword","description":"Version of the service."},{"field":"source.address","type":"keyword","description":"Source network address."},{"field":"source.as.number","type":"long","description":"Unique number allocated to the autonomous system."},{"field":"source.as.organization.name","type":"keyword","description":"Organization name."},{"field":"source.as.organization.name.text","type":"text","description":"Organization name."},{"field":"source.bytes","type":"long","description":"Bytes sent from the source to the destination."},{"field":"source.domain","type":"keyword","description":"Source domain."},{"field":"source.geo.city_name","type":"keyword","description":"City name."},{"field":"source.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"source.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"source.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"source.geo.country_name","type":"keyword","description":"Country name."},{"field":"source.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"source.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"source.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"source.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"source.geo.region_name","type":"keyword","description":"Region name."},{"field":"source.geo.timezone","type":"keyword","description":"Time zone."},{"field":"source.ip","type":"ip","description":"IP address of the source."},{"field":"source.mac","type":"keyword","description":"MAC address of the source."},{"field":"source.nat.ip","type":"ip","description":"Source NAT ip"},{"field":"source.nat.port","type":"long","description":"Source NAT port"},{"field":"source.packets","type":"long","description":"Packets sent from the source to the destination."},{"field":"source.port","type":"long","description":"Port of the source."},{"field":"source.registered_domain","type":"keyword","description":"The highest registered source domain, stripped of the subdomain."},{"field":"source.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"source.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"source.user.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"source.user.email","type":"keyword","description":"User email address."},{"field":"source.user.full_name","type":"keyword","description":"User's full name, if available."},{"field":"source.user.full_name.text","type":"text","description":"User's full name, if available."},{"field":"source.user.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"source.user.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"source.user.group.name","type":"keyword","description":"Name of the group."},{"field":"source.user.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"source.user.id","type":"keyword","description":"Unique identifier of the user."},{"field":"source.user.name","type":"keyword","description":"Short name or login of the user."},{"field":"source.user.name.text","type":"text","description":"Short name or login of the user."},{"field":"source.user.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"span.id","type":"keyword","description":"Unique identifier of the span within the scope of its trace."},{"field":"threat.enrichments","type":"nested","description":"List of objects containing indicators enriching the event."},{"field":"threat.enrichments.indicator","type":"object","description":"Object containing indicators enriching the event."},{"field":"threat.enrichments.indicator.as.number","type":"long","description":"Unique number allocated to the autonomous system."},{"field":"threat.enrichments.indicator.as.organization.name","type":"keyword","description":"Organization name."},{"field":"threat.enrichments.indicator.as.organization.name.text","type":"text","description":"Organization name."},{"field":"threat.enrichments.indicator.confidence","type":"keyword","description":"Indicator confidence rating"},{"field":"threat.enrichments.indicator.description","type":"keyword","description":"Indicator description"},{"field":"threat.enrichments.indicator.email.address","type":"keyword","description":"Indicator email address"},{"field":"threat.enrichments.indicator.file.accessed","type":"date","description":"Last time the file was accessed."},{"field":"threat.enrichments.indicator.file.attributes","type":"keyword","description":"Array of file attributes."},{"field":"threat.enrichments.indicator.file.code_signature.exists","type":"boolean","description":"Boolean to capture if a signature is present."},{"field":"threat.enrichments.indicator.file.code_signature.signing_id","type":"keyword","description":"The identifier used to sign the process."},{"field":"threat.enrichments.indicator.file.code_signature.status","type":"keyword","description":"Additional information about the certificate status."},{"field":"threat.enrichments.indicator.file.code_signature.subject_name","type":"keyword","description":"Subject name of the code signer"},{"field":"threat.enrichments.indicator.file.code_signature.team_id","type":"keyword","description":"The team identifier used to sign the process."},{"field":"threat.enrichments.indicator.file.code_signature.trusted","type":"boolean","description":"Stores the trust status of the certificate chain."},{"field":"threat.enrichments.indicator.file.code_signature.valid","type":"boolean","description":"Boolean to capture if the digital signature is verified against the binary content."},{"field":"threat.enrichments.indicator.file.created","type":"date","description":"File creation time."},{"field":"threat.enrichments.indicator.file.ctime","type":"date","description":"Last time the file attributes or metadata changed."},{"field":"threat.enrichments.indicator.file.device","type":"keyword","description":"Device that is the source of the file."},{"field":"threat.enrichments.indicator.file.directory","type":"keyword","description":"Directory where the file is located."},{"field":"threat.enrichments.indicator.file.drive_letter","type":"keyword","description":"Drive letter where the file is located."},{"field":"threat.enrichments.indicator.file.elf.architecture","type":"keyword","description":"Machine architecture of the ELF file."},{"field":"threat.enrichments.indicator.file.elf.byte_order","type":"keyword","description":"Byte sequence of ELF file."},{"field":"threat.enrichments.indicator.file.elf.cpu_type","type":"keyword","description":"CPU type of the ELF file."},{"field":"threat.enrichments.indicator.file.elf.creation_date","type":"date","description":"Build or compile date."},{"field":"threat.enrichments.indicator.file.elf.exports","type":"flattened","description":"List of exported element names and types."},{"field":"threat.enrichments.indicator.file.elf.header.abi_version","type":"keyword","description":"Version of the ELF Application Binary Interface (ABI)."},{"field":"threat.enrichments.indicator.file.elf.header.class","type":"keyword","description":"Header class of the ELF file."},{"field":"threat.enrichments.indicator.file.elf.header.data","type":"keyword","description":"Data table of the ELF header."},{"field":"threat.enrichments.indicator.file.elf.header.entrypoint","type":"long","description":"Header entrypoint of the ELF file."},{"field":"threat.enrichments.indicator.file.elf.header.object_version","type":"keyword","description":"0x1\" for original ELF files."},{"field":"threat.enrichments.indicator.file.elf.header.os_abi","type":"keyword","description":"Application Binary Interface (ABI) of the Linux OS."},{"field":"threat.enrichments.indicator.file.elf.header.type","type":"keyword","description":"Header type of the ELF file."},{"field":"threat.enrichments.indicator.file.elf.header.version","type":"keyword","description":"Version of the ELF header."},{"field":"threat.enrichments.indicator.file.elf.imports","type":"flattened","description":"List of imported element names and types."},{"field":"threat.enrichments.indicator.file.elf.sections","type":"nested","description":"Section information of the ELF file."},{"field":"threat.enrichments.indicator.file.elf.sections.chi2","type":"long","description":"Chi-square probability distribution of the section."},{"field":"threat.enrichments.indicator.file.elf.sections.entropy","type":"long","description":"Shannon entropy calculation from the section."},{"field":"threat.enrichments.indicator.file.elf.sections.flags","type":"keyword","description":"ELF Section List flags."},{"field":"threat.enrichments.indicator.file.elf.sections.name","type":"keyword","description":"ELF Section List name."},{"field":"threat.enrichments.indicator.file.elf.sections.physical_offset","type":"keyword","description":"ELF Section List offset."},{"field":"threat.enrichments.indicator.file.elf.sections.physical_size","type":"long","description":"ELF Section List physical size."},{"field":"threat.enrichments.indicator.file.elf.sections.type","type":"keyword","description":"ELF Section List type."},{"field":"threat.enrichments.indicator.file.elf.sections.virtual_address","type":"long","description":"ELF Section List virtual address."},{"field":"threat.enrichments.indicator.file.elf.sections.virtual_size","type":"long","description":"ELF Section List virtual size."},{"field":"threat.enrichments.indicator.file.elf.segments","type":"nested","description":"ELF object segment list."},{"field":"threat.enrichments.indicator.file.elf.segments.sections","type":"keyword","description":"ELF object segment sections."},{"field":"threat.enrichments.indicator.file.elf.segments.type","type":"keyword","description":"ELF object segment type."},{"field":"threat.enrichments.indicator.file.elf.shared_libraries","type":"keyword","description":"List of shared libraries used by this ELF object."},{"field":"threat.enrichments.indicator.file.elf.telfhash","type":"keyword","description":"telfhash hash for ELF file."},{"field":"threat.enrichments.indicator.file.extension","type":"keyword","description":"File extension, excluding the leading dot."},{"field":"threat.enrichments.indicator.file.gid","type":"keyword","description":"Primary group ID (GID) of the file."},{"field":"threat.enrichments.indicator.file.group","type":"keyword","description":"Primary group name of the file."},{"field":"threat.enrichments.indicator.file.inode","type":"keyword","description":"Inode representing the file in the filesystem."},{"field":"threat.enrichments.indicator.file.mime_type","type":"keyword","description":"Media type of file, document, or arrangement of bytes."},{"field":"threat.enrichments.indicator.file.mode","type":"keyword","description":"Mode of the file in octal representation."},{"field":"threat.enrichments.indicator.file.mtime","type":"date","description":"Last time the file content was modified."},{"field":"threat.enrichments.indicator.file.name","type":"keyword","description":"Name of the file including the extension, without the directory."},{"field":"threat.enrichments.indicator.file.owner","type":"keyword","description":"File owner's username."},{"field":"threat.enrichments.indicator.file.path","type":"keyword","description":"Full path to the file, including the file name."},{"field":"threat.enrichments.indicator.file.path.text","type":"text","description":"Full path to the file, including the file name."},{"field":"threat.enrichments.indicator.file.size","type":"long","description":"File size in bytes."},{"field":"threat.enrichments.indicator.file.target_path","type":"keyword","description":"Target path for symlinks."},{"field":"threat.enrichments.indicator.file.target_path.text","type":"text","description":"Target path for symlinks."},{"field":"threat.enrichments.indicator.file.type","type":"keyword","description":"File type (file, dir, or symlink)."},{"field":"threat.enrichments.indicator.file.uid","type":"keyword","description":"The user ID (UID) or security identifier (SID) of the file owner."},{"field":"threat.enrichments.indicator.first_seen","type":"date","description":"Date/time indicator was first reported."},{"field":"threat.enrichments.indicator.geo.city_name","type":"keyword","description":"City name."},{"field":"threat.enrichments.indicator.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"threat.enrichments.indicator.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"threat.enrichments.indicator.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"threat.enrichments.indicator.geo.country_name","type":"keyword","description":"Country name."},{"field":"threat.enrichments.indicator.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"threat.enrichments.indicator.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"threat.enrichments.indicator.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"threat.enrichments.indicator.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"threat.enrichments.indicator.geo.region_name","type":"keyword","description":"Region name."},{"field":"threat.enrichments.indicator.geo.timezone","type":"keyword","description":"Time zone."},{"field":"threat.enrichments.indicator.hash.md5","type":"keyword","description":"MD5 hash."},{"field":"threat.enrichments.indicator.hash.sha1","type":"keyword","description":"SHA1 hash."},{"field":"threat.enrichments.indicator.hash.sha256","type":"keyword","description":"SHA256 hash."},{"field":"threat.enrichments.indicator.hash.sha512","type":"keyword","description":"SHA512 hash."},{"field":"threat.enrichments.indicator.hash.ssdeep","type":"keyword","description":"SSDEEP hash."},{"field":"threat.enrichments.indicator.ip","type":"ip","description":"Indicator IP address"},{"field":"threat.enrichments.indicator.last_seen","type":"date","description":"Date/time indicator was last reported."},{"field":"threat.enrichments.indicator.marking.tlp","type":"keyword","description":"Indicator TLP marking"},{"field":"threat.enrichments.indicator.modified_at","type":"date","description":"Date/time indicator was last updated."},{"field":"threat.enrichments.indicator.pe.architecture","type":"keyword","description":"CPU architecture target for the file."},{"field":"threat.enrichments.indicator.pe.company","type":"keyword","description":"Internal company name of the file, provided at compile-time."},{"field":"threat.enrichments.indicator.pe.description","type":"keyword","description":"Internal description of the file, provided at compile-time."},{"field":"threat.enrichments.indicator.pe.file_version","type":"keyword","description":"Process name."},{"field":"threat.enrichments.indicator.pe.imphash","type":"keyword","description":"A hash of the imports in a PE file."},{"field":"threat.enrichments.indicator.pe.original_file_name","type":"keyword","description":"Internal name of the file, provided at compile-time."},{"field":"threat.enrichments.indicator.pe.product","type":"keyword","description":"Internal product name of the file, provided at compile-time."},{"field":"threat.enrichments.indicator.port","type":"long","description":"Indicator port"},{"field":"threat.enrichments.indicator.provider","type":"keyword","description":"Indicator provider"},{"field":"threat.enrichments.indicator.reference","type":"keyword","description":"Indicator reference URL"},{"field":"threat.enrichments.indicator.registry.data.bytes","type":"keyword","description":"Original bytes written with base64 encoding."},{"field":"threat.enrichments.indicator.registry.data.strings","type":"keyword","description":"List of strings representing what was written to the registry."},{"field":"threat.enrichments.indicator.registry.data.type","type":"keyword","description":"Standard registry type for encoding contents"},{"field":"threat.enrichments.indicator.registry.hive","type":"keyword","description":"Abbreviated name for the hive."},{"field":"threat.enrichments.indicator.registry.key","type":"keyword","description":"Hive-relative path of keys."},{"field":"threat.enrichments.indicator.registry.path","type":"keyword","description":"Full path, including hive, key and value"},{"field":"threat.enrichments.indicator.registry.value","type":"keyword","description":"Name of the value written."},{"field":"threat.enrichments.indicator.scanner_stats","type":"long","description":"Scanner statistics"},{"field":"threat.enrichments.indicator.sightings","type":"long","description":"Number of times indicator observed"},{"field":"threat.enrichments.indicator.type","type":"keyword","description":"Type of indicator"},{"field":"threat.enrichments.indicator.url.domain","type":"keyword","description":"Domain of the url."},{"field":"threat.enrichments.indicator.url.extension","type":"keyword","description":"File extension from the request url, excluding the leading dot."},{"field":"threat.enrichments.indicator.url.fragment","type":"keyword","description":"Portion of the url after the `#`."},{"field":"threat.enrichments.indicator.url.full","type":"keyword","description":"Full unparsed URL."},{"field":"threat.enrichments.indicator.url.full.text","type":"text","description":"Full unparsed URL."},{"field":"threat.enrichments.indicator.url.original","type":"keyword","description":"Unmodified original url as seen in the event source."},{"field":"threat.enrichments.indicator.url.original.text","type":"text","description":"Unmodified original url as seen in the event source."},{"field":"threat.enrichments.indicator.url.password","type":"keyword","description":"Password of the request."},{"field":"threat.enrichments.indicator.url.path","type":"keyword","description":"Path of the request, such as \"/search\"."},{"field":"threat.enrichments.indicator.url.port","type":"long","description":"Port of the request, such as 443."},{"field":"threat.enrichments.indicator.url.query","type":"keyword","description":"Query string of the request."},{"field":"threat.enrichments.indicator.url.registered_domain","type":"keyword","description":"The highest registered url domain, stripped of the subdomain."},{"field":"threat.enrichments.indicator.url.scheme","type":"keyword","description":"Scheme of the url."},{"field":"threat.enrichments.indicator.url.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"threat.enrichments.indicator.url.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"threat.enrichments.indicator.url.username","type":"keyword","description":"Username of the request."},{"field":"threat.enrichments.indicator.x509.alternative_names","type":"keyword","description":"List of subject alternative names (SAN)."},{"field":"threat.enrichments.indicator.x509.issuer.common_name","type":"keyword","description":"List of common name (CN) of issuing certificate authority."},{"field":"threat.enrichments.indicator.x509.issuer.country","type":"keyword","description":"List of country (C) codes"},{"field":"threat.enrichments.indicator.x509.issuer.distinguished_name","type":"keyword","description":"Distinguished name (DN) of issuing certificate authority."},{"field":"threat.enrichments.indicator.x509.issuer.locality","type":"keyword","description":"List of locality names (L)"},{"field":"threat.enrichments.indicator.x509.issuer.organization","type":"keyword","description":"List of organizations (O) of issuing certificate authority."},{"field":"threat.enrichments.indicator.x509.issuer.organizational_unit","type":"keyword","description":"List of organizational units (OU) of issuing certificate authority."},{"field":"threat.enrichments.indicator.x509.issuer.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"threat.enrichments.indicator.x509.not_after","type":"date","description":"Time at which the certificate is no longer considered valid."},{"field":"threat.enrichments.indicator.x509.not_before","type":"date","description":"Time at which the certificate is first considered valid."},{"field":"threat.enrichments.indicator.x509.public_key_algorithm","type":"keyword","description":"Algorithm used to generate the public key."},{"field":"threat.enrichments.indicator.x509.public_key_curve","type":"keyword","description":"The curve used by the elliptic curve public key algorithm. This is algorithm specific."},{"field":"threat.enrichments.indicator.x509.public_key_exponent","type":"long","description":"Exponent used to derive the public key. This is algorithm specific."},{"field":"threat.enrichments.indicator.x509.public_key_size","type":"long","description":"The size of the public key space in bits."},{"field":"threat.enrichments.indicator.x509.serial_number","type":"keyword","description":"Unique serial number issued by the certificate authority."},{"field":"threat.enrichments.indicator.x509.signature_algorithm","type":"keyword","description":"Identifier for certificate signature algorithm."},{"field":"threat.enrichments.indicator.x509.subject.common_name","type":"keyword","description":"List of common names (CN) of subject."},{"field":"threat.enrichments.indicator.x509.subject.country","type":"keyword","description":"List of country (C) code"},{"field":"threat.enrichments.indicator.x509.subject.distinguished_name","type":"keyword","description":"Distinguished name (DN) of the certificate subject entity."},{"field":"threat.enrichments.indicator.x509.subject.locality","type":"keyword","description":"List of locality names (L)"},{"field":"threat.enrichments.indicator.x509.subject.organization","type":"keyword","description":"List of organizations (O) of subject."},{"field":"threat.enrichments.indicator.x509.subject.organizational_unit","type":"keyword","description":"List of organizational units (OU) of subject."},{"field":"threat.enrichments.indicator.x509.subject.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"threat.enrichments.indicator.x509.version_number","type":"keyword","description":"Version of x509 format."},{"field":"threat.enrichments.matched.atomic","type":"keyword","description":"Matched indicator value"},{"field":"threat.enrichments.matched.field","type":"keyword","description":"Matched indicator field"},{"field":"threat.enrichments.matched.id","type":"keyword","description":"Matched indicator identifier"},{"field":"threat.enrichments.matched.index","type":"keyword","description":"Matched indicator index"},{"field":"threat.enrichments.matched.type","type":"keyword","description":"Type of indicator match"},{"field":"threat.framework","type":"keyword","description":"Threat classification framework."},{"field":"threat.group.alias","type":"keyword","description":"Alias of the group."},{"field":"threat.group.id","type":"keyword","description":"ID of the group."},{"field":"threat.group.name","type":"keyword","description":"Name of the group."},{"field":"threat.group.reference","type":"keyword","description":"Reference URL of the group."},{"field":"threat.indicator.as.number","type":"long","description":"Unique number allocated to the autonomous system."},{"field":"threat.indicator.as.organization.name","type":"keyword","description":"Organization name."},{"field":"threat.indicator.as.organization.name.text","type":"text","description":"Organization name."},{"field":"threat.indicator.confidence","type":"keyword","description":"Indicator confidence rating"},{"field":"threat.indicator.description","type":"keyword","description":"Indicator description"},{"field":"threat.indicator.email.address","type":"keyword","description":"Indicator email address"},{"field":"threat.indicator.file.accessed","type":"date","description":"Last time the file was accessed."},{"field":"threat.indicator.file.attributes","type":"keyword","description":"Array of file attributes."},{"field":"threat.indicator.file.code_signature.exists","type":"boolean","description":"Boolean to capture if a signature is present."},{"field":"threat.indicator.file.code_signature.signing_id","type":"keyword","description":"The identifier used to sign the process."},{"field":"threat.indicator.file.code_signature.status","type":"keyword","description":"Additional information about the certificate status."},{"field":"threat.indicator.file.code_signature.subject_name","type":"keyword","description":"Subject name of the code signer"},{"field":"threat.indicator.file.code_signature.team_id","type":"keyword","description":"The team identifier used to sign the process."},{"field":"threat.indicator.file.code_signature.trusted","type":"boolean","description":"Stores the trust status of the certificate chain."},{"field":"threat.indicator.file.code_signature.valid","type":"boolean","description":"Boolean to capture if the digital signature is verified against the binary content."},{"field":"threat.indicator.file.created","type":"date","description":"File creation time."},{"field":"threat.indicator.file.ctime","type":"date","description":"Last time the file attributes or metadata changed."},{"field":"threat.indicator.file.device","type":"keyword","description":"Device that is the source of the file."},{"field":"threat.indicator.file.directory","type":"keyword","description":"Directory where the file is located."},{"field":"threat.indicator.file.drive_letter","type":"keyword","description":"Drive letter where the file is located."},{"field":"threat.indicator.file.elf.architecture","type":"keyword","description":"Machine architecture of the ELF file."},{"field":"threat.indicator.file.elf.byte_order","type":"keyword","description":"Byte sequence of ELF file."},{"field":"threat.indicator.file.elf.cpu_type","type":"keyword","description":"CPU type of the ELF file."},{"field":"threat.indicator.file.elf.creation_date","type":"date","description":"Build or compile date."},{"field":"threat.indicator.file.elf.exports","type":"flattened","description":"List of exported element names and types."},{"field":"threat.indicator.file.elf.header.abi_version","type":"keyword","description":"Version of the ELF Application Binary Interface (ABI)."},{"field":"threat.indicator.file.elf.header.class","type":"keyword","description":"Header class of the ELF file."},{"field":"threat.indicator.file.elf.header.data","type":"keyword","description":"Data table of the ELF header."},{"field":"threat.indicator.file.elf.header.entrypoint","type":"long","description":"Header entrypoint of the ELF file."},{"field":"threat.indicator.file.elf.header.object_version","type":"keyword","description":"0x1\" for original ELF files."},{"field":"threat.indicator.file.elf.header.os_abi","type":"keyword","description":"Application Binary Interface (ABI) of the Linux OS."},{"field":"threat.indicator.file.elf.header.type","type":"keyword","description":"Header type of the ELF file."},{"field":"threat.indicator.file.elf.header.version","type":"keyword","description":"Version of the ELF header."},{"field":"threat.indicator.file.elf.imports","type":"flattened","description":"List of imported element names and types."},{"field":"threat.indicator.file.elf.sections","type":"nested","description":"Section information of the ELF file."},{"field":"threat.indicator.file.elf.sections.chi2","type":"long","description":"Chi-square probability distribution of the section."},{"field":"threat.indicator.file.elf.sections.entropy","type":"long","description":"Shannon entropy calculation from the section."},{"field":"threat.indicator.file.elf.sections.flags","type":"keyword","description":"ELF Section List flags."},{"field":"threat.indicator.file.elf.sections.name","type":"keyword","description":"ELF Section List name."},{"field":"threat.indicator.file.elf.sections.physical_offset","type":"keyword","description":"ELF Section List offset."},{"field":"threat.indicator.file.elf.sections.physical_size","type":"long","description":"ELF Section List physical size."},{"field":"threat.indicator.file.elf.sections.type","type":"keyword","description":"ELF Section List type."},{"field":"threat.indicator.file.elf.sections.virtual_address","type":"long","description":"ELF Section List virtual address."},{"field":"threat.indicator.file.elf.sections.virtual_size","type":"long","description":"ELF Section List virtual size."},{"field":"threat.indicator.file.elf.segments","type":"nested","description":"ELF object segment list."},{"field":"threat.indicator.file.elf.segments.sections","type":"keyword","description":"ELF object segment sections."},{"field":"threat.indicator.file.elf.segments.type","type":"keyword","description":"ELF object segment type."},{"field":"threat.indicator.file.elf.shared_libraries","type":"keyword","description":"List of shared libraries used by this ELF object."},{"field":"threat.indicator.file.elf.telfhash","type":"keyword","description":"telfhash hash for ELF file."},{"field":"threat.indicator.file.extension","type":"keyword","description":"File extension, excluding the leading dot."},{"field":"threat.indicator.file.gid","type":"keyword","description":"Primary group ID (GID) of the file."},{"field":"threat.indicator.file.group","type":"keyword","description":"Primary group name of the file."},{"field":"threat.indicator.file.inode","type":"keyword","description":"Inode representing the file in the filesystem."},{"field":"threat.indicator.file.mime_type","type":"keyword","description":"Media type of file, document, or arrangement of bytes."},{"field":"threat.indicator.file.mode","type":"keyword","description":"Mode of the file in octal representation."},{"field":"threat.indicator.file.mtime","type":"date","description":"Last time the file content was modified."},{"field":"threat.indicator.file.name","type":"keyword","description":"Name of the file including the extension, without the directory."},{"field":"threat.indicator.file.owner","type":"keyword","description":"File owner's username."},{"field":"threat.indicator.file.path","type":"keyword","description":"Full path to the file, including the file name."},{"field":"threat.indicator.file.path.text","type":"text","description":"Full path to the file, including the file name."},{"field":"threat.indicator.file.size","type":"long","description":"File size in bytes."},{"field":"threat.indicator.file.target_path","type":"keyword","description":"Target path for symlinks."},{"field":"threat.indicator.file.target_path.text","type":"text","description":"Target path for symlinks."},{"field":"threat.indicator.file.type","type":"keyword","description":"File type (file, dir, or symlink)."},{"field":"threat.indicator.file.uid","type":"keyword","description":"The user ID (UID) or security identifier (SID) of the file owner."},{"field":"threat.indicator.first_seen","type":"date","description":"Date/time indicator was first reported."},{"field":"threat.indicator.geo.city_name","type":"keyword","description":"City name."},{"field":"threat.indicator.geo.continent_code","type":"keyword","description":"Continent code."},{"field":"threat.indicator.geo.continent_name","type":"keyword","description":"Name of the continent."},{"field":"threat.indicator.geo.country_iso_code","type":"keyword","description":"Country ISO code."},{"field":"threat.indicator.geo.country_name","type":"keyword","description":"Country name."},{"field":"threat.indicator.geo.location","type":"geo_point","description":"Longitude and latitude."},{"field":"threat.indicator.geo.name","type":"keyword","description":"User-defined description of a location."},{"field":"threat.indicator.geo.postal_code","type":"keyword","description":"Postal code."},{"field":"threat.indicator.geo.region_iso_code","type":"keyword","description":"Region ISO code."},{"field":"threat.indicator.geo.region_name","type":"keyword","description":"Region name."},{"field":"threat.indicator.geo.timezone","type":"keyword","description":"Time zone."},{"field":"threat.indicator.hash.md5","type":"keyword","description":"MD5 hash."},{"field":"threat.indicator.hash.sha1","type":"keyword","description":"SHA1 hash."},{"field":"threat.indicator.hash.sha256","type":"keyword","description":"SHA256 hash."},{"field":"threat.indicator.hash.sha512","type":"keyword","description":"SHA512 hash."},{"field":"threat.indicator.hash.ssdeep","type":"keyword","description":"SSDEEP hash."},{"field":"threat.indicator.ip","type":"ip","description":"Indicator IP address"},{"field":"threat.indicator.last_seen","type":"date","description":"Date/time indicator was last reported."},{"field":"threat.indicator.marking.tlp","type":"keyword","description":"Indicator TLP marking"},{"field":"threat.indicator.modified_at","type":"date","description":"Date/time indicator was last updated."},{"field":"threat.indicator.pe.architecture","type":"keyword","description":"CPU architecture target for the file."},{"field":"threat.indicator.pe.company","type":"keyword","description":"Internal company name of the file, provided at compile-time."},{"field":"threat.indicator.pe.description","type":"keyword","description":"Internal description of the file, provided at compile-time."},{"field":"threat.indicator.pe.file_version","type":"keyword","description":"Process name."},{"field":"threat.indicator.pe.imphash","type":"keyword","description":"A hash of the imports in a PE file."},{"field":"threat.indicator.pe.original_file_name","type":"keyword","description":"Internal name of the file, provided at compile-time."},{"field":"threat.indicator.pe.product","type":"keyword","description":"Internal product name of the file, provided at compile-time."},{"field":"threat.indicator.port","type":"long","description":"Indicator port"},{"field":"threat.indicator.provider","type":"keyword","description":"Indicator provider"},{"field":"threat.indicator.reference","type":"keyword","description":"Indicator reference URL"},{"field":"threat.indicator.registry.data.bytes","type":"keyword","description":"Original bytes written with base64 encoding."},{"field":"threat.indicator.registry.data.strings","type":"keyword","description":"List of strings representing what was written to the registry."},{"field":"threat.indicator.registry.data.type","type":"keyword","description":"Standard registry type for encoding contents"},{"field":"threat.indicator.registry.hive","type":"keyword","description":"Abbreviated name for the hive."},{"field":"threat.indicator.registry.key","type":"keyword","description":"Hive-relative path of keys."},{"field":"threat.indicator.registry.path","type":"keyword","description":"Full path, including hive, key and value"},{"field":"threat.indicator.registry.value","type":"keyword","description":"Name of the value written."},{"field":"threat.indicator.scanner_stats","type":"long","description":"Scanner statistics"},{"field":"threat.indicator.sightings","type":"long","description":"Number of times indicator observed"},{"field":"threat.indicator.type","type":"keyword","description":"Type of indicator"},{"field":"threat.indicator.url.domain","type":"keyword","description":"Domain of the url."},{"field":"threat.indicator.url.extension","type":"keyword","description":"File extension from the request url, excluding the leading dot."},{"field":"threat.indicator.url.fragment","type":"keyword","description":"Portion of the url after the `#`."},{"field":"threat.indicator.url.full","type":"keyword","description":"Full unparsed URL."},{"field":"threat.indicator.url.full.text","type":"text","description":"Full unparsed URL."},{"field":"threat.indicator.url.original","type":"keyword","description":"Unmodified original url as seen in the event source."},{"field":"threat.indicator.url.original.text","type":"text","description":"Unmodified original url as seen in the event source."},{"field":"threat.indicator.url.password","type":"keyword","description":"Password of the request."},{"field":"threat.indicator.url.path","type":"keyword","description":"Path of the request, such as \"/search\"."},{"field":"threat.indicator.url.port","type":"long","description":"Port of the request, such as 443."},{"field":"threat.indicator.url.query","type":"keyword","description":"Query string of the request."},{"field":"threat.indicator.url.registered_domain","type":"keyword","description":"The highest registered url domain, stripped of the subdomain."},{"field":"threat.indicator.url.scheme","type":"keyword","description":"Scheme of the url."},{"field":"threat.indicator.url.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"threat.indicator.url.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"threat.indicator.url.username","type":"keyword","description":"Username of the request."},{"field":"threat.indicator.x509.alternative_names","type":"keyword","description":"List of subject alternative names (SAN)."},{"field":"threat.indicator.x509.issuer.common_name","type":"keyword","description":"List of common name (CN) of issuing certificate authority."},{"field":"threat.indicator.x509.issuer.country","type":"keyword","description":"List of country (C) codes"},{"field":"threat.indicator.x509.issuer.distinguished_name","type":"keyword","description":"Distinguished name (DN) of issuing certificate authority."},{"field":"threat.indicator.x509.issuer.locality","type":"keyword","description":"List of locality names (L)"},{"field":"threat.indicator.x509.issuer.organization","type":"keyword","description":"List of organizations (O) of issuing certificate authority."},{"field":"threat.indicator.x509.issuer.organizational_unit","type":"keyword","description":"List of organizational units (OU) of issuing certificate authority."},{"field":"threat.indicator.x509.issuer.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"threat.indicator.x509.not_after","type":"date","description":"Time at which the certificate is no longer considered valid."},{"field":"threat.indicator.x509.not_before","type":"date","description":"Time at which the certificate is first considered valid."},{"field":"threat.indicator.x509.public_key_algorithm","type":"keyword","description":"Algorithm used to generate the public key."},{"field":"threat.indicator.x509.public_key_curve","type":"keyword","description":"The curve used by the elliptic curve public key algorithm. This is algorithm specific."},{"field":"threat.indicator.x509.public_key_exponent","type":"long","description":"Exponent used to derive the public key. This is algorithm specific."},{"field":"threat.indicator.x509.public_key_size","type":"long","description":"The size of the public key space in bits."},{"field":"threat.indicator.x509.serial_number","type":"keyword","description":"Unique serial number issued by the certificate authority."},{"field":"threat.indicator.x509.signature_algorithm","type":"keyword","description":"Identifier for certificate signature algorithm."},{"field":"threat.indicator.x509.subject.common_name","type":"keyword","description":"List of common names (CN) of subject."},{"field":"threat.indicator.x509.subject.country","type":"keyword","description":"List of country (C) code"},{"field":"threat.indicator.x509.subject.distinguished_name","type":"keyword","description":"Distinguished name (DN) of the certificate subject entity."},{"field":"threat.indicator.x509.subject.locality","type":"keyword","description":"List of locality names (L)"},{"field":"threat.indicator.x509.subject.organization","type":"keyword","description":"List of organizations (O) of subject."},{"field":"threat.indicator.x509.subject.organizational_unit","type":"keyword","description":"List of organizational units (OU) of subject."},{"field":"threat.indicator.x509.subject.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"threat.indicator.x509.version_number","type":"keyword","description":"Version of x509 format."},{"field":"threat.software.id","type":"keyword","description":"ID of the software"},{"field":"threat.software.name","type":"keyword","description":"Name of the software."},{"field":"threat.software.platforms","type":"keyword","description":"Platforms of the software."},{"field":"threat.software.reference","type":"keyword","description":"Software reference URL."},{"field":"threat.software.type","type":"keyword","description":"Software type."},{"field":"threat.tactic.id","type":"keyword","description":"Threat tactic id."},{"field":"threat.tactic.name","type":"keyword","description":"Threat tactic."},{"field":"threat.tactic.reference","type":"keyword","description":"Threat tactic URL reference."},{"field":"threat.technique.id","type":"keyword","description":"Threat technique id."},{"field":"threat.technique.name","type":"keyword","description":"Threat technique name."},{"field":"threat.technique.name.text","type":"text","description":"Threat technique name."},{"field":"threat.technique.reference","type":"keyword","description":"Threat technique URL reference."},{"field":"threat.technique.subtechnique.id","type":"keyword","description":"Threat subtechnique id."},{"field":"threat.technique.subtechnique.name","type":"keyword","description":"Threat subtechnique name."},{"field":"threat.technique.subtechnique.name.text","type":"text","description":"Threat subtechnique name."},{"field":"threat.technique.subtechnique.reference","type":"keyword","description":"Threat subtechnique URL reference."},{"field":"tls.cipher","type":"keyword","description":"String indicating the cipher used during the current connection."},{"field":"tls.client.certificate","type":"keyword","description":"PEM-encoded stand-alone certificate offered by the client."},{"field":"tls.client.certificate_chain","type":"keyword","description":"Array of PEM-encoded certificates that make up the certificate chain offered by the client."},{"field":"tls.client.hash.md5","type":"keyword","description":"Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the client."},{"field":"tls.client.hash.sha1","type":"keyword","description":"Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the client."},{"field":"tls.client.hash.sha256","type":"keyword","description":"Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the client."},{"field":"tls.client.issuer","type":"keyword","description":"Distinguished name of subject of the issuer of the x.509 certificate presented by the client."},{"field":"tls.client.ja3","type":"keyword","description":"A hash that identifies clients based on how they perform an SSL/TLS handshake."},{"field":"tls.client.not_after","type":"date","description":"Date/Time indicating when client certificate is no longer considered valid."},{"field":"tls.client.not_before","type":"date","description":"Date/Time indicating when client certificate is first considered valid."},{"field":"tls.client.server_name","type":"keyword","description":"Hostname the client is trying to connect to. Also called the SNI."},{"field":"tls.client.subject","type":"keyword","description":"Distinguished name of subject of the x.509 certificate presented by the client."},{"field":"tls.client.supported_ciphers","type":"keyword","description":"Array of ciphers offered by the client during the client hello."},{"field":"tls.client.x509.alternative_names","type":"keyword","description":"List of subject alternative names (SAN)."},{"field":"tls.client.x509.issuer.common_name","type":"keyword","description":"List of common name (CN) of issuing certificate authority."},{"field":"tls.client.x509.issuer.country","type":"keyword","description":"List of country (C) codes"},{"field":"tls.client.x509.issuer.distinguished_name","type":"keyword","description":"Distinguished name (DN) of issuing certificate authority."},{"field":"tls.client.x509.issuer.locality","type":"keyword","description":"List of locality names (L)"},{"field":"tls.client.x509.issuer.organization","type":"keyword","description":"List of organizations (O) of issuing certificate authority."},{"field":"tls.client.x509.issuer.organizational_unit","type":"keyword","description":"List of organizational units (OU) of issuing certificate authority."},{"field":"tls.client.x509.issuer.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"tls.client.x509.not_after","type":"date","description":"Time at which the certificate is no longer considered valid."},{"field":"tls.client.x509.not_before","type":"date","description":"Time at which the certificate is first considered valid."},{"field":"tls.client.x509.public_key_algorithm","type":"keyword","description":"Algorithm used to generate the public key."},{"field":"tls.client.x509.public_key_curve","type":"keyword","description":"The curve used by the elliptic curve public key algorithm. This is algorithm specific."},{"field":"tls.client.x509.public_key_exponent","type":"long","description":"Exponent used to derive the public key. This is algorithm specific."},{"field":"tls.client.x509.public_key_size","type":"long","description":"The size of the public key space in bits."},{"field":"tls.client.x509.serial_number","type":"keyword","description":"Unique serial number issued by the certificate authority."},{"field":"tls.client.x509.signature_algorithm","type":"keyword","description":"Identifier for certificate signature algorithm."},{"field":"tls.client.x509.subject.common_name","type":"keyword","description":"List of common names (CN) of subject."},{"field":"tls.client.x509.subject.country","type":"keyword","description":"List of country (C) code"},{"field":"tls.client.x509.subject.distinguished_name","type":"keyword","description":"Distinguished name (DN) of the certificate subject entity."},{"field":"tls.client.x509.subject.locality","type":"keyword","description":"List of locality names (L)"},{"field":"tls.client.x509.subject.organization","type":"keyword","description":"List of organizations (O) of subject."},{"field":"tls.client.x509.subject.organizational_unit","type":"keyword","description":"List of organizational units (OU) of subject."},{"field":"tls.client.x509.subject.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"tls.client.x509.version_number","type":"keyword","description":"Version of x509 format."},{"field":"tls.curve","type":"keyword","description":"String indicating the curve used for the given cipher, when applicable."},{"field":"tls.established","type":"boolean","description":"Boolean flag indicating if the TLS negotiation was successful and transitioned to an encrypted tunnel."},{"field":"tls.next_protocol","type":"keyword","description":"String indicating the protocol being tunneled."},{"field":"tls.resumed","type":"boolean","description":"Boolean flag indicating if this TLS connection was resumed from an existing TLS negotiation."},{"field":"tls.server.certificate","type":"keyword","description":"PEM-encoded stand-alone certificate offered by the server."},{"field":"tls.server.certificate_chain","type":"keyword","description":"Array of PEM-encoded certificates that make up the certificate chain offered by the server."},{"field":"tls.server.hash.md5","type":"keyword","description":"Certificate fingerprint using the MD5 digest of DER-encoded version of certificate offered by the server."},{"field":"tls.server.hash.sha1","type":"keyword","description":"Certificate fingerprint using the SHA1 digest of DER-encoded version of certificate offered by the server."},{"field":"tls.server.hash.sha256","type":"keyword","description":"Certificate fingerprint using the SHA256 digest of DER-encoded version of certificate offered by the server."},{"field":"tls.server.issuer","type":"keyword","description":"Subject of the issuer of the x.509 certificate presented by the server."},{"field":"tls.server.ja3s","type":"keyword","description":"A hash that identifies servers based on how they perform an SSL/TLS handshake."},{"field":"tls.server.not_after","type":"date","description":"Timestamp indicating when server certificate is no longer considered valid."},{"field":"tls.server.not_before","type":"date","description":"Timestamp indicating when server certificate is first considered valid."},{"field":"tls.server.subject","type":"keyword","description":"Subject of the x.509 certificate presented by the server."},{"field":"tls.server.x509.alternative_names","type":"keyword","description":"List of subject alternative names (SAN)."},{"field":"tls.server.x509.issuer.common_name","type":"keyword","description":"List of common name (CN) of issuing certificate authority."},{"field":"tls.server.x509.issuer.country","type":"keyword","description":"List of country (C) codes"},{"field":"tls.server.x509.issuer.distinguished_name","type":"keyword","description":"Distinguished name (DN) of issuing certificate authority."},{"field":"tls.server.x509.issuer.locality","type":"keyword","description":"List of locality names (L)"},{"field":"tls.server.x509.issuer.organization","type":"keyword","description":"List of organizations (O) of issuing certificate authority."},{"field":"tls.server.x509.issuer.organizational_unit","type":"keyword","description":"List of organizational units (OU) of issuing certificate authority."},{"field":"tls.server.x509.issuer.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"tls.server.x509.not_after","type":"date","description":"Time at which the certificate is no longer considered valid."},{"field":"tls.server.x509.not_before","type":"date","description":"Time at which the certificate is first considered valid."},{"field":"tls.server.x509.public_key_algorithm","type":"keyword","description":"Algorithm used to generate the public key."},{"field":"tls.server.x509.public_key_curve","type":"keyword","description":"The curve used by the elliptic curve public key algorithm. This is algorithm specific."},{"field":"tls.server.x509.public_key_exponent","type":"long","description":"Exponent used to derive the public key. This is algorithm specific."},{"field":"tls.server.x509.public_key_size","type":"long","description":"The size of the public key space in bits."},{"field":"tls.server.x509.serial_number","type":"keyword","description":"Unique serial number issued by the certificate authority."},{"field":"tls.server.x509.signature_algorithm","type":"keyword","description":"Identifier for certificate signature algorithm."},{"field":"tls.server.x509.subject.common_name","type":"keyword","description":"List of common names (CN) of subject."},{"field":"tls.server.x509.subject.country","type":"keyword","description":"List of country (C) code"},{"field":"tls.server.x509.subject.distinguished_name","type":"keyword","description":"Distinguished name (DN) of the certificate subject entity."},{"field":"tls.server.x509.subject.locality","type":"keyword","description":"List of locality names (L)"},{"field":"tls.server.x509.subject.organization","type":"keyword","description":"List of organizations (O) of subject."},{"field":"tls.server.x509.subject.organizational_unit","type":"keyword","description":"List of organizational units (OU) of subject."},{"field":"tls.server.x509.subject.state_or_province","type":"keyword","description":"List of state or province names (ST, S, or P)"},{"field":"tls.server.x509.version_number","type":"keyword","description":"Version of x509 format."},{"field":"tls.version","type":"keyword","description":"Numeric part of the version parsed from the original string."},{"field":"tls.version_protocol","type":"keyword","description":"Normalized lowercase protocol name parsed from original string."},{"field":"trace.id","type":"keyword","description":"Unique identifier of the trace."},{"field":"transaction.id","type":"keyword","description":"Unique identifier of the transaction within the scope of its trace."},{"field":"url.domain","type":"keyword","description":"Domain of the url."},{"field":"url.extension","type":"keyword","description":"File extension from the request url, excluding the leading dot."},{"field":"url.fragment","type":"keyword","description":"Portion of the url after the `#`."},{"field":"url.full","type":"keyword","description":"Full unparsed URL."},{"field":"url.full.text","type":"text","description":"Full unparsed URL."},{"field":"url.original","type":"keyword","description":"Unmodified original url as seen in the event source."},{"field":"url.original.text","type":"text","description":"Unmodified original url as seen in the event source."},{"field":"url.password","type":"keyword","description":"Password of the request."},{"field":"url.path","type":"keyword","description":"Path of the request, such as \"/search\"."},{"field":"url.port","type":"long","description":"Port of the request, such as 443."},{"field":"url.query","type":"keyword","description":"Query string of the request."},{"field":"url.registered_domain","type":"keyword","description":"The highest registered url domain, stripped of the subdomain."},{"field":"url.scheme","type":"keyword","description":"Scheme of the url."},{"field":"url.subdomain","type":"keyword","description":"The subdomain of the domain."},{"field":"url.top_level_domain","type":"keyword","description":"The effective top level domain (com, org, net, co.uk)."},{"field":"url.username","type":"keyword","description":"Username of the request."},{"field":"user.changes.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"user.changes.email","type":"keyword","description":"User email address."},{"field":"user.changes.full_name","type":"keyword","description":"User's full name, if available."},{"field":"user.changes.full_name.text","type":"text","description":"User's full name, if available."},{"field":"user.changes.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"user.changes.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"user.changes.group.name","type":"keyword","description":"Name of the group."},{"field":"user.changes.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"user.changes.id","type":"keyword","description":"Unique identifier of the user."},{"field":"user.changes.name","type":"keyword","description":"Short name or login of the user."},{"field":"user.changes.name.text","type":"text","description":"Short name or login of the user."},{"field":"user.changes.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"user.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"user.effective.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"user.effective.email","type":"keyword","description":"User email address."},{"field":"user.effective.full_name","type":"keyword","description":"User's full name, if available."},{"field":"user.effective.full_name.text","type":"text","description":"User's full name, if available."},{"field":"user.effective.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"user.effective.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"user.effective.group.name","type":"keyword","description":"Name of the group."},{"field":"user.effective.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"user.effective.id","type":"keyword","description":"Unique identifier of the user."},{"field":"user.effective.name","type":"keyword","description":"Short name or login of the user."},{"field":"user.effective.name.text","type":"text","description":"Short name or login of the user."},{"field":"user.effective.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"user.email","type":"keyword","description":"User email address."},{"field":"user.full_name","type":"keyword","description":"User's full name, if available."},{"field":"user.full_name.text","type":"text","description":"User's full name, if available."},{"field":"user.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"user.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"user.group.name","type":"keyword","description":"Name of the group."},{"field":"user.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"user.id","type":"keyword","description":"Unique identifier of the user."},{"field":"user.name","type":"keyword","description":"Short name or login of the user."},{"field":"user.name.text","type":"text","description":"Short name or login of the user."},{"field":"user.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"user.target.domain","type":"keyword","description":"Name of the directory the user is a member of."},{"field":"user.target.email","type":"keyword","description":"User email address."},{"field":"user.target.full_name","type":"keyword","description":"User's full name, if available."},{"field":"user.target.full_name.text","type":"text","description":"User's full name, if available."},{"field":"user.target.group.domain","type":"keyword","description":"Name of the directory the group is a member of."},{"field":"user.target.group.id","type":"keyword","description":"Unique identifier for the group on the system/platform."},{"field":"user.target.group.name","type":"keyword","description":"Name of the group."},{"field":"user.target.hash","type":"keyword","description":"Unique user hash to correlate information for a user in anonymized form."},{"field":"user.target.id","type":"keyword","description":"Unique identifier of the user."},{"field":"user.target.name","type":"keyword","description":"Short name or login of the user."},{"field":"user.target.name.text","type":"text","description":"Short name or login of the user."},{"field":"user.target.roles","type":"keyword","description":"Array of user roles at the time of the event."},{"field":"user_agent.device.name","type":"keyword","description":"Name of the device."},{"field":"user_agent.name","type":"keyword","description":"Name of the user agent."},{"field":"user_agent.original","type":"keyword","description":"Unparsed user_agent string."},{"field":"user_agent.original.text","type":"text","description":"Unparsed user_agent string."},{"field":"user_agent.os.family","type":"keyword","description":"OS family (such as redhat, debian, freebsd, windows)."},{"field":"user_agent.os.full","type":"keyword","description":"Operating system name, including the version or code name."},{"field":"user_agent.os.full.text","type":"text","description":"Operating system name, including the version or code name."},{"field":"user_agent.os.kernel","type":"keyword","description":"Operating system kernel version as a raw string."},{"field":"user_agent.os.name","type":"keyword","description":"Operating system name, without the version."},{"field":"user_agent.os.name.text","type":"text","description":"Operating system name, without the version."},{"field":"user_agent.os.platform","type":"keyword","description":"Operating system platform (such centos, ubuntu, windows)."},{"field":"user_agent.os.type","type":"keyword","description":"Which commercial OS family (one of: linux, macos, unix or windows)."},{"field":"user_agent.os.version","type":"keyword","description":"Operating system version as a raw string."},{"field":"user_agent.version","type":"keyword","description":"Version of the user agent."},{"field":"vulnerability.category","type":"keyword","description":"Category of a vulnerability."},{"field":"vulnerability.classification","type":"keyword","description":"Classification of the vulnerability."},{"field":"vulnerability.description","type":"keyword","description":"Description of the vulnerability."},{"field":"vulnerability.description.text","type":"text","description":"Description of the vulnerability."},{"field":"vulnerability.enumeration","type":"keyword","description":"Identifier of the vulnerability."},{"field":"vulnerability.id","type":"keyword","description":"ID of the vulnerability."},{"field":"vulnerability.reference","type":"keyword","description":"Reference of the vulnerability."},{"field":"vulnerability.report_id","type":"keyword","description":"Scan identification number."},{"field":"vulnerability.scanner.vendor","type":"keyword","description":"Name of the scanner vendor."},{"field":"vulnerability.score.base","type":"float","description":"Vulnerability Base score."},{"field":"vulnerability.score.environmental","type":"float","description":"Vulnerability Environmental score."},{"field":"vulnerability.score.temporal","type":"float","description":"Vulnerability Temporal score."},{"field":"vulnerability.score.version","type":"keyword","description":"CVSS version."},{"field":"vulnerability.severity","type":"keyword","description":"Severity of the vulnerability."}] \ No newline at end of file diff --git a/x-pack/plugins/osquery/public/components/app.tsx b/x-pack/plugins/osquery/public/components/app.tsx index df61b116a564..44407139ab49 100644 --- a/x-pack/plugins/osquery/public/components/app.tsx +++ b/x-pack/plugins/osquery/public/components/app.tsx @@ -26,12 +26,6 @@ const OsqueryAppComponent = () => { - {/* - - */} ( - + diff --git a/x-pack/plugins/osquery/public/live_queries/index.tsx b/x-pack/plugins/osquery/public/live_queries/index.tsx index 9feb05bc0520..8d87d59828ee 100644 --- a/x-pack/plugins/osquery/public/live_queries/index.tsx +++ b/x-pack/plugins/osquery/public/live_queries/index.tsx @@ -7,6 +7,7 @@ import { EuiCode, EuiLoadingContent, EuiEmptyPrompt } from '@elastic/eui'; import React, { useMemo } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; import { LiveQueryForm } from './form'; import { useActionResultsPrivileges } from '../action_results/use_action_privileges'; @@ -52,13 +53,26 @@ const LiveQueryComponent: React.FC = ({ return ( } - title={

Permission denied

} + title={ +

+ +

+ } titleSize="xs" body={

- To view query results, ask your administrator to update your user role to have index{' '} - read privileges on the{' '} - logs-{OSQUERY_INTEGRATION_NAME}.result* index. + read, + logs: logs-{OSQUERY_INTEGRATION_NAME}.result*, + }} + />

} /> diff --git a/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx b/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx index 401966460a7e..abed9fc1bce4 100644 --- a/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx +++ b/x-pack/plugins/osquery/public/routes/saved_queries/edit/index.tsx @@ -125,16 +125,33 @@ const EditSavedQueryPageComponent = () => { )} {isDeleteModalVisible ? ( + } onCancel={handleCloseDeleteConfirmationModal} onConfirm={handleDeleteConfirmClick} - cancelButtonText="No, don't do it" - confirmButtonText="Yes, do it" + cancelButtonText={ + + } + confirmButtonText={ + + } buttonColor="danger" defaultFocusedButton="confirm" > -

You’re about to delete this query.

-

Are you sure you want to do this?

+
) : null} diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx index 5bc4bf32bcef..685960ecd202 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/index.tsx @@ -14,12 +14,11 @@ import { EuiButton, EuiDescribedFormGroup, EuiSpacer, - EuiAccordion, EuiBottomBar, EuiHorizontalRule, } from '@elastic/eui'; import React, { useCallback, useMemo, useState } from 'react'; -import { useMutation } from 'react-query'; +import { useMutation, useQueryClient } from 'react-query'; import { produce } from 'immer'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; @@ -64,6 +63,7 @@ const ScheduledQueryGroupFormComponent: React.FC = packageInfo, editMode = false, }) => { + const queryClient = useQueryClient(); const { application: { navigateToApp }, http, @@ -112,6 +112,10 @@ const ScheduledQueryGroupFormComponent: React.FC = return; } + queryClient.invalidateQueries([ + 'scheduledQueryGroup', + { scheduledQueryGroupId: data.item.id }, + ]); setErrorToast(); navigateToApp(PLUGIN_ID, { path: `scheduled_query_groups/${data.item.id}` }); toasts.addSuccess( @@ -328,16 +332,7 @@ const ScheduledQueryGroupFormComponent: React.FC = agentPoliciesById={agentPoliciesById} /> - - - - + diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/pack_uploader.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/pack_uploader.tsx index 83e64ed6e6f3..6c3d90473c80 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/pack_uploader.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/pack_uploader.tsx @@ -61,7 +61,7 @@ const OsqueryPackUploaderComponent: React.FC = ({ onCh return; } - onChange(parsedContent?.queries, packName.current); + onChange(parsedContent, packName.current); // @ts-expect-error update types filePickerRef.current?.removeFiles(new Event('fake')); }; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx index 079b9ddacc9d..15acfcf4fbdc 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/form/queries_field.tsx @@ -211,7 +211,7 @@ const QueriesFieldComponent: React.FC = ({ }, [setValue, tableSelectedItems]); const handlePackUpload = useCallback( - (newQueries, packName) => { + (parsedContent, packName) => { /* Osquery scheduled packs are supported since osquery_manager@0.5.0 */ const isOsqueryPackSupported = integrationPackageVersion ? satisfies(integrationPackageVersion, '>=0.5.0') @@ -219,14 +219,14 @@ const QueriesFieldComponent: React.FC = ({ setValue( produce((draft) => { - forEach(newQueries, (newQuery, newQueryId) => { + forEach(parsedContent.queries, (newQuery, newQueryId) => { draft[0].streams.push( getNewStream({ id: isOsqueryPackSupported ? newQueryId : `pack_${packName}_${newQueryId}`, - interval: newQuery.interval, + interval: newQuery.interval ?? parsedContent.interval, query: newQuery.query, - version: newQuery.version, - platform: getSupportedPlatforms(newQuery.platform), + version: newQuery.version ?? parsedContent.version, + platform: getSupportedPlatforms(newQuery.platform ?? parsedContent.platform), scheduledQueryGroupId, }) ); diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/constants.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/constants.ts index bedca9d5ef8d..cbd52fb41885 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/queries/constants.ts +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/queries/constants.ts @@ -6,6 +6,9 @@ */ export const ALL_OSQUERY_VERSIONS_OPTIONS = [ + { + label: '4.9.0', + }, { label: '4.8.0', }, diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_status_table.tsx b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_status_table.tsx index fe54a46df8c0..35344f2503eb 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_status_table.tsx +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/scheduled_query_group_queries_status_table.tsx @@ -60,6 +60,7 @@ interface ViewResultsInDiscoverActionProps { buttonType: ViewResultsActionButtonType; endDate?: string; startDate?: string; + mode?: string; } function getLensAttributes( @@ -200,6 +201,7 @@ const ViewResultsInLensActionComponent: React.FC { const lensService = useKibana().services.lens; @@ -215,7 +217,7 @@ const ViewResultsInLensActionComponent: React.FC ), [agentIds, scheduledQueryGroupName] @@ -545,12 +550,15 @@ const ScheduledQueryGroupQueriesStatusTableComponent: React.FC ( ), - [agentIds] + [agentIds, scheduledQueryGroupName] ); const getItemId = useCallback( diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts index 6b6acc30036c..c3458698dd51 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group.ts @@ -33,10 +33,8 @@ export const useScheduledQueryGroup = ({ keepPreviousData: true, enabled: !skip || !scheduledQueryGroupId, select: (response) => response.item, - refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, - staleTime: Infinity, } ); }; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_errors.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_errors.ts index 31d98ee6204e..64e5a20de68c 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_errors.ts +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_errors.ts @@ -82,10 +82,8 @@ export const useScheduledQueryGroupQueryErrors = ({ keepPreviousData: true, enabled: !!(!skip && actionId && interval && agentIds?.length), select: (response) => response.rawResponse.hits ?? [], - refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, - staleTime: Infinity, } ); }; diff --git a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_last_results.ts b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_last_results.ts index 21117a4a0f83..f972640e2598 100644 --- a/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_last_results.ts +++ b/x-pack/plugins/osquery/public/scheduled_query_groups/use_scheduled_query_group_query_last_results.ts @@ -79,10 +79,8 @@ export const useScheduledQueryGroupQueryLastResults = ({ enabled: !!(!skip && actionId && interval && agentIds?.length), // @ts-expect-error update types select: (response) => response.rawResponse.aggregations?.runs?.buckets[0] ?? [], - refetchOnMount: false, refetchOnReconnect: false, refetchOnWindowFocus: false, - staleTime: Infinity, } ); }; diff --git a/x-pack/plugins/osquery/scripts/schema_formatter/ecs_formatter.ts b/x-pack/plugins/osquery/scripts/schema_formatter/ecs_formatter.ts index 7a37fcc4cd80..e53750080ef7 100644 --- a/x-pack/plugins/osquery/scripts/schema_formatter/ecs_formatter.ts +++ b/x-pack/plugins/osquery/scripts/schema_formatter/ecs_formatter.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { map, partialRight, pick } from 'lodash'; +import { filter, map, partialRight, pick } from 'lodash'; import { promises as fs } from 'fs'; import path from 'path'; @@ -13,13 +13,42 @@ import { run } from '@kbn/dev-utils'; const ECS_COLUMN_SCHEMA_FIELDS = ['field', 'type', 'description']; +const RESTRICTED_FIELDS = [ + 'agent.name', + 'agent.id', + 'agent.ephemeral_id', + 'agent.type', + 'agent.version', + 'ecs.version', + 'event.agent_id_status', + 'event.ingested', + 'event.module', + 'host.hostname', + 'host.os.build', + 'host.os.kernel', + 'host.os.name', + 'host.os.family', + 'host.os.type', + 'host.os.version', + 'host.platform', + 'host.ip', + 'host.id', + 'host.mac', + 'host.architecture', + '@timestamp', +]; + run( async ({ flags }) => { - const schemaPath = path.resolve(`../public/common/schemas/ecs/`); + const schemaPath = path.resolve(`public/common/schemas/ecs/`); const schemaFile = path.join(schemaPath, flags.schema_version as string); const schemaData = await require(schemaFile); - const formattedSchema = map(schemaData, partialRight(pick, ECS_COLUMN_SCHEMA_FIELDS)); + const filteredSchemaData = filter( + schemaData, + (field) => !RESTRICTED_FIELDS.includes(field.field) + ); + const formattedSchema = map(filteredSchemaData, partialRight(pick, ECS_COLUMN_SCHEMA_FIELDS)); await fs.writeFile( path.join(schemaPath, `v${flags.schema_version}-formatted.json`), diff --git a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts index e35e776cb195..08b5b4314f1f 100644 --- a/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts +++ b/x-pack/plugins/osquery/server/routes/fleet_wrapper/get_agent_policies.ts @@ -5,7 +5,9 @@ * 2.0. */ +import bluebird from 'bluebird'; import { schema } from '@kbn/config-schema'; +import { GetAgentPoliciesResponseItem, AGENT_SAVED_OBJECT_TYPE } from '../../../../fleet/common'; import { PLUGIN_ID } from '../../../common'; import { IRouter } from '../../../../../../src/core/server'; import { OsqueryAppContext } from '../../lib/osquery_app_context_services'; @@ -22,13 +24,32 @@ export const getAgentPoliciesRoute = (router: IRouter, osqueryContext: OsqueryAp }, async (context, request, response) => { const soClient = context.core.savedObjects.client; + const esClient = context.core.elasticsearch.client.asInternalUser; - const agentPolicies = await osqueryContext.service.getAgentPolicyService()?.list(soClient, { + // TODO: Use getAgentPoliciesHandler from x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts + const body = await osqueryContext.service.getAgentPolicyService()?.list(soClient, { ...(request.query || {}), perPage: 100, }); - return response.ok({ body: agentPolicies }); + if (body?.items) { + await bluebird.map( + body.items, + (agentPolicy: GetAgentPoliciesResponseItem) => + osqueryContext.service + .getAgentService() + ?.listAgents(esClient, { + showInactive: false, + perPage: 0, + page: 1, + kuery: `${AGENT_SAVED_OBJECT_TYPE}.policy_id:${agentPolicy.id}`, + }) + .then(({ total: agentTotal }) => (agentPolicy.agents = agentTotal)), + { concurrency: 10 } + ); + } + + return response.ok({ body }); } ); }; diff --git a/x-pack/plugins/reporting/common/poller.ts b/x-pack/plugins/reporting/common/poller.ts index 13ded0576bdf..3778454c3a4c 100644 --- a/x-pack/plugins/reporting/common/poller.ts +++ b/x-pack/plugins/reporting/common/poller.ts @@ -8,20 +8,19 @@ import _ from 'lodash'; interface PollerOptions { - functionToPoll: () => Promise; + functionToPoll: () => Promise; pollFrequencyInMillis: number; trailing?: boolean; continuePollingOnError?: boolean; pollFrequencyErrorMultiplier?: number; - successFunction?: (...args: any) => any; - errorFunction?: (error: Error) => any; + successFunction?: (...args: unknown[]) => void; + errorFunction?: (error: Error) => void; } -// @TODO Maybe move to observables someday export class Poller { - private readonly functionToPoll: () => Promise; - private readonly successFunction: (...args: any) => any; - private readonly errorFunction: (error: Error) => any; + private readonly functionToPoll: () => Promise; + private readonly successFunction: (...args: unknown[]) => void; + private readonly errorFunction: (error: Error) => void; private _isRunning: boolean; private _timeoutId: NodeJS.Timeout | null; private pollFrequencyInMillis: number; diff --git a/x-pack/plugins/reporting/public/management/report_listing.tsx b/x-pack/plugins/reporting/public/management/report_listing.tsx index 4e183380a6b4..c3a05042681c 100644 --- a/x-pack/plugins/reporting/public/management/report_listing.tsx +++ b/x-pack/plugins/reporting/public/management/report_listing.tsx @@ -51,7 +51,7 @@ class ReportListingUi extends Component { private isInitialJobsFetch: boolean; private licenseSubscription?: Subscription; private mounted?: boolean; - private poller?: any; + private poller?: Poller; constructor(props: Props) { super(props); @@ -119,7 +119,7 @@ class ReportListingUi extends Component { public componentWillUnmount() { this.mounted = false; - this.poller.stop(); + this.poller?.stop(); if (this.licenseSubscription) { this.licenseSubscription.unsubscribe(); diff --git a/x-pack/plugins/reporting/public/notifier/job_completion_notifications.ts b/x-pack/plugins/reporting/public/notifier/job_completion_notifications.ts index e764f94105b7..c4addfa3eede 100644 --- a/x-pack/plugins/reporting/public/notifier/job_completion_notifications.ts +++ b/x-pack/plugins/reporting/public/notifier/job_completion_notifications.ts @@ -9,11 +9,11 @@ import { JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY } from '../../common/constants type JobId = string; -const set = (jobs: any) => { +const set = (jobs: string[]) => { sessionStorage.setItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY, JSON.stringify(jobs)); }; -const getAll = () => { +const getAll = (): string[] => { const sessionValue = sessionStorage.getItem(JOB_COMPLETION_NOTIFICATIONS_SESSION_KEY); return sessionValue ? JSON.parse(sessionValue) : []; }; diff --git a/x-pack/plugins/reporting/server/browsers/safe_child_process.ts b/x-pack/plugins/reporting/server/browsers/safe_child_process.ts index 9265dae23b89..70e45bf10803 100644 --- a/x-pack/plugins/reporting/server/browsers/safe_child_process.ts +++ b/x-pack/plugins/reporting/server/browsers/safe_child_process.ts @@ -10,7 +10,7 @@ import { take, share, mapTo, delay, tap } from 'rxjs/operators'; import { LevelLogger } from '../lib'; interface IChild { - kill: (signal: string) => Promise; + kill: (signal: string) => Promise; } // Our process can get sent various signals, and when these occur we wish to diff --git a/x-pack/plugins/reporting/server/deprecations/migrage_existing_indices_ilm_policy.test.ts b/x-pack/plugins/reporting/server/deprecations/migrage_existing_indices_ilm_policy.test.ts index 485c4e62a208..766e23a90293 100644 --- a/x-pack/plugins/reporting/server/deprecations/migrage_existing_indices_ilm_policy.test.ts +++ b/x-pack/plugins/reporting/server/deprecations/migrage_existing_indices_ilm_policy.test.ts @@ -66,6 +66,7 @@ describe("Migrate existing indices' ILM policy deprecations", () => { }, "level": "warning", "message": "New reporting indices will be managed by the \\"kibana-reporting\\" provisioned ILM policy. You must edit this policy to manage the report lifecycle. This change targets all indices prefixed with \\".reporting-*\\".", + "title": "Found reporting indices managed by custom ILM policy.", }, ] `); diff --git a/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.ts b/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.ts index a3dd4205b9e6..9e3304e78e00 100644 --- a/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.ts +++ b/x-pack/plugins/reporting/server/deprecations/migrate_existing_indices_ilm_policy.ts @@ -30,6 +30,9 @@ export const getDeprecationsInfo = async ( if (migrationStatus !== 'ok') { return [ { + title: i18n.translate('xpack.reporting.deprecations.migrateIndexIlmPolicyActionTitle', { + defaultMessage: 'Found reporting indices managed by custom ILM policy.', + }), level: 'warning', message: i18n.translate('xpack.reporting.deprecations.migrateIndexIlmPolicyActionMessage', { defaultMessage: `New reporting indices will be managed by the "{reportingIlmPolicy}" provisioned ILM policy. You must edit this policy to manage the report lifecycle. This change targets all indices prefixed with "{indexPattern}".`, diff --git a/x-pack/plugins/reporting/server/deprecations/reporting_role.test.ts b/x-pack/plugins/reporting/server/deprecations/reporting_role.test.ts index b52d51d3e931..d8ab0ac0c568 100644 --- a/x-pack/plugins/reporting/server/deprecations/reporting_role.test.ts +++ b/x-pack/plugins/reporting/server/deprecations/reporting_role.test.ts @@ -53,6 +53,7 @@ test('logs a plain message when only a reporting_user role issue is found', asyn "documentationUrl": "https://www.elastic.co/guide/en/kibana/current/secure-reporting.html", "level": "critical", "message": "The deprecated \\"reporting_user\\" role has been found for 1 user(s): \\"reportron\\"", + "title": "Found deprecated reporting role", }, ] `); @@ -82,6 +83,7 @@ test('logs multiple entries when multiple reporting_user role issues are found', "documentationUrl": "https://www.elastic.co/guide/en/kibana/current/secure-reporting.html", "level": "critical", "message": "The deprecated \\"reporting_user\\" role has been found for 2 user(s): \\"reportron\\", \\"supercooluser\\"", + "title": "Found deprecated reporting role", }, ] `); @@ -112,6 +114,7 @@ test('logs an expanded message when a config issue and a reporting_user role iss "documentationUrl": "https://www.elastic.co/guide/en/kibana/current/secure-reporting.html", "level": "critical", "message": "The deprecated \\"reporting_user\\" role has been found for 1 user(s): \\"reportron\\"", + "title": "Found deprecated reporting role", }, ] `); diff --git a/x-pack/plugins/reporting/server/deprecations/reporting_role.ts b/x-pack/plugins/reporting/server/deprecations/reporting_role.ts index d5138043060a..6e08169727d1 100644 --- a/x-pack/plugins/reporting/server/deprecations/reporting_role.ts +++ b/x-pack/plugins/reporting/server/deprecations/reporting_role.ts @@ -6,6 +6,7 @@ */ import type { GetDeprecationsContext, DeprecationsDetails } from 'src/core/server'; +import { i18n } from '@kbn/i18n'; import { ReportingCore } from '..'; const deprecatedRole = 'reporting_user'; @@ -24,21 +25,41 @@ export const getDeprecationsInfo = async ( const { body: users } = await esClient.asCurrentUser.security.getUser(); const reportingUsers = Object.entries(users) - .filter(([username, user]) => user.roles.includes(deprecatedRole)) + .filter(([, user]) => user.roles.includes(deprecatedRole)) .map(([, user]) => user.username); + const numReportingUsers = reportingUsers.length; if (numReportingUsers > 0) { - const usernames = reportingUsers.join('", "'); deprecations.push({ - message: `The deprecated "${deprecatedRole}" role has been found for ${numReportingUsers} user(s): "${usernames}"`, + title: i18n.translate('xpack.reporting.deprecations.reportingRoleTitle', { + defaultMessage: 'Found deprecated reporting role', + }), + message: i18n.translate('xpack.reporting.deprecations.reportingRoleMessage', { + defaultMessage: + 'The deprecated "{deprecatedRole}" role has been found for {numReportingUsers} user(s): "{usernames}"', + values: { deprecatedRole, numReportingUsers, usernames: reportingUsers.join('", "') }, + }), documentationUrl: 'https://www.elastic.co/guide/en/kibana/current/secure-reporting.html', level: 'critical', correctiveActions: { manualSteps: [ - ...(usingDeprecatedConfig ? [`Set "${upgradableConfig}" in kibana.yml`] : []), - `Create one or more custom roles that provide Kibana application privileges to reporting features in **Management > Security > Roles**.`, - `Assign the custom role(s) as desired, and remove the "${deprecatedRole}" role from the user(s).`, + ...(usingDeprecatedConfig + ? [ + i18n.translate('xpack.reporting.deprecations.reportingRole.manualStepOneMessage', { + defaultMessage: 'Set "{upgradableConfig}" in kibana.yml', + values: { upgradableConfig }, + }), + ] + : []), + i18n.translate('xpack.reporting.deprecations.reportingRole.manualStepTwoMessage', { + defaultMessage: `Create one or more custom roles that provide Kibana application privileges to reporting features in **Management > Security > Roles**.`, + }), + i18n.translate('xpack.reporting.deprecations.reportingRole.manualStepThreeMessage', { + defaultMessage: + 'Assign the custom role(s) as desired, and remove the "{deprecatedRole}" role from the user(s).', + values: { deprecatedRole }, + }), ], }, }); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts index f806b8a7e5bc..342e1fc7d85d 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/create_job/compatibility_shim.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { KibanaRequest } from 'kibana/server'; +import type { KibanaRequest, SavedObjectsClientContract } from 'kibana/server'; import { url as urlUtils } from '../../../../../../../src/plugins/kibana_utils/server'; import type { LevelLogger } from '../../../lib'; import type { CreateJobFn, ReportingRequestHandlerContext } from '../../../types'; @@ -20,9 +20,9 @@ function isLegacyJob( const getSavedObjectTitle = async ( objectType: string, savedObjectId: string, - savedObjectsClient: any + savedObjectsClient: SavedObjectsClientContract ) => { - const savedObject = await savedObjectsClient.get(objectType, savedObjectId); + const savedObject = await savedObjectsClient.get<{ title: string }>(objectType, savedObjectId); return savedObject.attributes.title; }; diff --git a/x-pack/plugins/reporting/server/lib/content_stream.test.ts b/x-pack/plugins/reporting/server/lib/content_stream.test.ts index 34b8982a5257..da55b4728d10 100644 --- a/x-pack/plugins/reporting/server/lib/content_stream.test.ts +++ b/x-pack/plugins/reporting/server/lib/content_stream.test.ts @@ -9,21 +9,26 @@ import { set } from 'lodash'; import { elasticsearchServiceMock } from 'src/core/server/mocks'; import { createMockLevelLogger } from '../test_helpers'; import { ContentStream } from './content_stream'; -import { ExportTypesRegistry } from './export_types_registry'; describe('ContentStream', () => { let client: ReturnType['asInternalUser']; - let exportTypesRegistry: jest.Mocked; let logger: ReturnType; let stream: ContentStream; + let base64Stream: ContentStream; beforeEach(() => { client = elasticsearchServiceMock.createClusterClient().asInternalUser; - exportTypesRegistry = ({ - get: jest.fn(() => ({})), - } as unknown) as typeof exportTypesRegistry; logger = createMockLevelLogger(); - stream = new ContentStream(client, exportTypesRegistry, logger, { + stream = new ContentStream( + client, + logger, + { + id: 'something', + index: 'somewhere', + }, + { encoding: 'raw' } + ); + base64Stream = new ContentStream(client, logger, { id: 'something', index: 'somewhere', }); @@ -79,9 +84,6 @@ describe('ContentStream', () => { }); it('should decode base64 encoded content', async () => { - exportTypesRegistry.get.mockReturnValueOnce({ jobContentEncoding: 'base64' } as ReturnType< - typeof exportTypesRegistry.get - >); client.search.mockResolvedValueOnce( set( {}, @@ -89,7 +91,7 @@ describe('ContentStream', () => { Buffer.from('encoded content').toString('base64') ) ); - const data = await new Promise((resolve) => stream.once('data', resolve)); + const data = await new Promise((resolve) => base64Stream.once('data', resolve)); expect(data).toEqual(Buffer.from('encoded content')); }); @@ -184,9 +186,6 @@ describe('ContentStream', () => { }); it('should decode every chunk separately', async () => { - exportTypesRegistry.get.mockReturnValueOnce({ jobContentEncoding: 'base64' } as ReturnType< - typeof exportTypesRegistry.get - >); client.search.mockResolvedValueOnce( set({}, 'body.hits.hits.0._source', { jobtype: 'pdf', @@ -211,7 +210,7 @@ describe('ContentStream', () => { ) ); let data = ''; - for await (const chunk of stream) { + for await (const chunk of base64Stream) { data += chunk; } @@ -275,12 +274,8 @@ describe('ContentStream', () => { }); it('should encode using base64', async () => { - exportTypesRegistry.get.mockReturnValueOnce({ jobContentEncoding: 'base64' } as ReturnType< - typeof exportTypesRegistry.get - >); - - stream.end('12345'); - await new Promise((resolve) => stream.once('finish', resolve)); + base64Stream.end('12345'); + await new Promise((resolve) => base64Stream.once('finish', resolve)); expect(client.update).toHaveBeenCalledTimes(1); @@ -347,14 +342,11 @@ describe('ContentStream', () => { }); it('should encode every chunk separately', async () => { - exportTypesRegistry.get.mockReturnValueOnce({ jobContentEncoding: 'base64' } as ReturnType< - typeof exportTypesRegistry.get - >); client.cluster.getSettings.mockResolvedValueOnce( set({}, 'body.defaults.http.max_content_length', 1028) ); - stream.end('12345678'); - await new Promise((resolve) => stream.once('finish', resolve)); + base64Stream.end('12345678'); + await new Promise((resolve) => base64Stream.once('finish', resolve)); expect(client.update).toHaveBeenCalledTimes(1); expect(client.update).toHaveBeenCalledWith( diff --git a/x-pack/plugins/reporting/server/lib/content_stream.ts b/x-pack/plugins/reporting/server/lib/content_stream.ts index 1d8ebf2bdfdb..79ff9a681213 100644 --- a/x-pack/plugins/reporting/server/lib/content_stream.ts +++ b/x-pack/plugins/reporting/server/lib/content_stream.ts @@ -12,7 +12,6 @@ import { ByteSizeValue } from '@kbn/config-schema'; import type { ElasticsearchClient } from 'src/core/server'; import { ReportingCore } from '..'; import { ReportSource } from '../../common/types'; -import { ExportTypesRegistry } from './export_types_registry'; import { LevelLogger } from './level_logger'; /** @@ -42,6 +41,15 @@ interface ChunkSource { output: ChunkOutput; } +type ContentStreamEncoding = 'base64' | 'raw'; + +interface ContentStreamParameters { + /** + * Content encoding. By default, it is Base64. + */ + encoding?: ContentStreamEncoding; +} + export class ContentStream extends Duplex { /** * @see https://en.wikipedia.org/wiki/Base64#Output_padding @@ -62,10 +70,9 @@ export class ContentStream extends Duplex { private bytesRead = 0; private chunksRead = 0; private chunksWritten = 0; - private jobContentEncoding?: string; private jobSize?: number; - private jobType?: string; private maxChunkSize?: number; + private parameters: Required; private puid = new Puid(); private primaryTerm?: number; private seqNo?: number; @@ -78,60 +85,20 @@ export class ContentStream extends Duplex { constructor( private client: ElasticsearchClient, - private exportTypesRegistry: ExportTypesRegistry, private logger: LevelLogger, - private document: ContentStreamDocument + private document: ContentStreamDocument, + { encoding = 'base64' }: ContentStreamParameters = {} ) { super(); - } - - private async getJobType() { - if (!this.jobType) { - const { id, index } = this.document; - const body: SearchRequest['body'] = { - _source: { includes: ['jobtype'] }, - query: { - constant_score: { - filter: { - bool: { - must: [{ term: { _id: id } }], - }, - }, - }, - }, - size: 1, - }; - - const response = await this.client.search({ body, index }); - const hits = response?.body.hits?.hits?.[0]; - this.jobType = hits?._source?.jobtype; - } - - return this.jobType; - } - - private async getJobContentEncoding() { - if (!this.jobContentEncoding) { - const jobType = await this.getJobType(); - - ({ jobContentEncoding: this.jobContentEncoding } = this.exportTypesRegistry.get( - ({ jobType: item }) => item === jobType - )); - } - - return this.jobContentEncoding; + this.parameters = { encoding }; } private async decode(content: string) { - const contentEncoding = await this.getJobContentEncoding(); - - return Buffer.from(content, contentEncoding === 'base64' ? 'base64' : undefined); + return Buffer.from(content, this.parameters.encoding === 'base64' ? 'base64' : undefined); } private async encode(buffer: Buffer) { - const contentEncoding = await this.getJobContentEncoding(); - - return buffer.toString(contentEncoding === 'base64' ? 'base64' : undefined); + return buffer.toString(this.parameters.encoding === 'base64' ? 'base64' : undefined); } private async getMaxContentSize() { @@ -146,10 +113,9 @@ export class ContentStream extends Duplex { private async getMaxChunkSize() { if (!this.maxChunkSize) { const maxContentSize = (await this.getMaxContentSize()) - REQUEST_SPAN_SIZE_IN_BYTES; - const jobContentEncoding = await this.getJobContentEncoding(); this.maxChunkSize = - jobContentEncoding === 'base64' + this.parameters.encoding === 'base64' ? ContentStream.getMaxBase64EncodedSize(maxContentSize) : ContentStream.getMaxJsonEscapedSize(maxContentSize); @@ -180,7 +146,6 @@ export class ContentStream extends Duplex { const response = await this.client.search({ body, index }); const hits = response?.body.hits?.hits?.[0]; - this.jobType = hits?._source?.jobtype; this.jobSize = hits?._source?.output?.size; return hits?._source?.output?.content; @@ -341,15 +306,18 @@ export class ContentStream extends Duplex { } } -export async function getContentStream(reporting: ReportingCore, document: ContentStreamDocument) { +export async function getContentStream( + reporting: ReportingCore, + document: ContentStreamDocument, + parameters?: ContentStreamParameters +) { const { asInternalUser: client } = await reporting.getEsClient(); - const exportTypesRegistry = reporting.getExportTypesRegistry(); const { logger } = reporting.getPluginSetupDeps(); return new ContentStream( client, - exportTypesRegistry, logger.clone(['content_stream', document.id]), - document + document, + parameters ); } diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts index 0602c45b8016..1e29efd9cce0 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts @@ -22,7 +22,7 @@ import { CancellationToken } from '../../../common'; import { durationToNumber, numberToDuration } from '../../../common/schema_utils'; import { ReportOutput } from '../../../common/types'; import { ReportingConfigType } from '../../config'; -import { BasePayload, RunTaskFn } from '../../types'; +import { BasePayload, ExportTypeDefinition, RunTaskFn } from '../../types'; import { Report, ReportDocument, ReportingStore, SavedReport } from '../store'; import { ReportFailedFields, ReportProcessingFields } from '../store/store'; import { @@ -43,6 +43,10 @@ interface ReportingExecuteTaskInstance { runAt?: Date; } +interface TaskExecutor extends Pick { + jobExecutor: RunTaskFn; +} + function isOutput(output: any): output is CompletedReportOutput { return output?.size != null; } @@ -56,7 +60,7 @@ export class ExecuteReportTask implements ReportingTask { private logger: LevelLogger; private taskManagerStart?: TaskManagerStartContract; - private taskExecutors?: Map>; + private taskExecutors?: Map; private kibanaId?: string; private kibanaName?: string; private store?: ReportingStore; @@ -78,13 +82,16 @@ export class ExecuteReportTask implements ReportingTask { const { reporting } = this; const exportTypesRegistry = reporting.getExportTypesRegistry(); - const executors = new Map>(); + const executors = new Map(); for (const exportType of exportTypesRegistry.getAll()) { const exportTypeLogger = this.logger.clone([exportType.id]); const jobExecutor = exportType.runTaskFnFactory(reporting, exportTypeLogger); // The task will run the function with the job type as a param. // This allows us to retrieve the specific export type runFn when called to run an export - executors.set(exportType.jobType, jobExecutor); + executors.set(exportType.jobType, { + jobExecutor, + jobContentEncoding: exportType.jobContentEncoding, + }); } this.taskExecutors = executors; @@ -113,6 +120,10 @@ export class ExecuteReportTask implements ReportingTask { return this.taskManagerStart; } + private getJobContentEncoding(jobType: string) { + return this.taskExecutors?.get(jobType)?.jobContentEncoding; + } + public async _claimJob(task: ReportTaskParams): Promise { if (this.kibanaId == null) { throw new Error(`Kibana instance ID is undefined!`); @@ -241,7 +252,7 @@ export class ExecuteReportTask implements ReportingTask { // run the report // if workerFn doesn't finish before timeout, call the cancellationToken and throw an error const queueTimeout = durationToNumber(this.config.queue.timeout); - return Rx.from(runner(task.id, task.payload, cancellationToken, stream)) + return Rx.from(runner.jobExecutor(task.id, task.payload, cancellationToken, stream)) .pipe(timeout(queueTimeout)) // throw an error if a value is not emitted before timeout .toPromise(); } @@ -323,12 +334,19 @@ export class ExecuteReportTask implements ReportingTask { this.logger.debug(`Reports running: ${this.reporting.countConcurrentReports()}.`); try { - const stream = await getContentStream(this.reporting, { - id: report._id, - index: report._index, - if_primary_term: report._primary_term, - if_seq_no: report._seq_no, - }); + const jobContentEncoding = this.getJobContentEncoding(jobType); + const stream = await getContentStream( + this.reporting, + { + id: report._id, + index: report._index, + if_primary_term: report._primary_term, + if_seq_no: report._seq_no, + }, + { + encoding: jobContentEncoding === 'base64' ? 'base64' : 'raw', + } + ); const output = await this._performJob(task, cancellationToken, stream); stream.end(); diff --git a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts index 0d7a249daa5a..c083849686ff 100644 --- a/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts +++ b/x-pack/plugins/reporting/server/routes/lib/get_document_payload.ts @@ -51,15 +51,18 @@ const getReportingHeaders = (output: TaskRunResult, exportType: ExportTypeDefini export function getDocumentPayloadFactory(reporting: ReportingCore) { const exportTypesRegistry = reporting.getExportTypesRegistry(); - async function getCompleted( - output: TaskRunResult, - jobType: string, - title: string, - content: Stream - ): Promise { + async function getCompleted({ + id, + index, + output, + jobtype: jobType, + payload: { title }, + }: Required): Promise { const exportType = exportTypesRegistry.get( (item: ExportTypeDefinition) => item.jobType === jobType ); + const encoding = exportType.jobContentEncoding === 'base64' ? 'base64' : 'raw'; + const content = await getContentStream(reporting, { id, index }, { encoding }); const filename = getTitle(exportType, title); const headers = getReportingHeaders(output, exportType); @@ -77,18 +80,21 @@ export function getDocumentPayloadFactory(reporting: ReportingCore) { // @TODO: These should be semantic HTTP codes as 500/503's indicate // error then these are really operating properly. - function getFailure(content: string): Payload { + async function getFailure({ id }: ReportApiJSON): Promise { + const jobsQuery = jobsQueryFactory(reporting); + const error = await jobsQuery.getError(id); + return { statusCode: 500, content: { - message: `Reporting generation failed: ${content}`, + message: `Reporting generation failed: ${error}`, }, contentType: 'application/json', headers: {}, }; } - function getIncomplete(status: string) { + function getIncomplete({ status }: ReportApiJSON): Payload { return { statusCode: 503, content: status, @@ -97,30 +103,18 @@ export function getDocumentPayloadFactory(reporting: ReportingCore) { }; } - return async function getDocumentPayload({ - id, - index, - output, - status, - jobtype: jobType, - payload: { title }, - }: ReportApiJSON): Promise { - if (output) { - if (status === statuses.JOB_STATUS_COMPLETED || status === statuses.JOB_STATUS_WARNINGS) { - const stream = await getContentStream(reporting, { id, index }); - - return getCompleted(output, jobType, title, stream); + return async function getDocumentPayload(report: ReportApiJSON): Promise { + if (report.output) { + if ([statuses.JOB_STATUS_COMPLETED, statuses.JOB_STATUS_WARNINGS].includes(report.status)) { + return getCompleted(report as Required); } - if (status === statuses.JOB_STATUS_FAILED) { - const jobsQuery = jobsQueryFactory(reporting); - const error = await jobsQuery.getError(id); - - return getFailure(error); + if (statuses.JOB_STATUS_FAILED === report.status) { + return getFailure(report); } } // send a 503 indicating that the report isn't completed yet - return getIncomplete(status); + return getIncomplete(report); }; } diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index 7fc638211e87..406beb2a56b6 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -63,7 +63,7 @@ export { BaseParams, BasePayload }; export type CreateJobFn = ( jobParams: JobParamsType, context: ReportingRequestHandlerContext, - request: KibanaRequest + request: KibanaRequest ) => Promise; // default fn type for RunTaskFnFactory diff --git a/x-pack/plugins/rule_registry/server/config.ts b/x-pack/plugins/rule_registry/server/config.ts index 830762c9b374..8f98ceb2dd8d 100644 --- a/x-pack/plugins/rule_registry/server/config.ts +++ b/x-pack/plugins/rule_registry/server/config.ts @@ -17,6 +17,9 @@ export const config = { legacyMultiTenancy: schema.object({ enabled: schema.boolean({ defaultValue: false }), }), + indexUpgrade: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + }), }), }), }; diff --git a/x-pack/plugins/rule_registry/server/plugin.ts b/x-pack/plugins/rule_registry/server/plugin.ts index a4122e3a1ffc..2329b90898ca 100644 --- a/x-pack/plugins/rule_registry/server/plugin.ts +++ b/x-pack/plugins/rule_registry/server/plugin.ts @@ -103,6 +103,7 @@ export class RuleRegistryPlugin logger, kibanaVersion, isWriteEnabled: isWriteEnabled(this.config, this.legacyConfig), + isIndexUpgradeEnabled: this.config.unsafe.indexUpgrade.enabled, getClusterClient: async () => { const deps = await startDependencies; return deps.core.elasticsearch.client.asInternalUser; diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts index 73651ec298c3..d683cc95065e 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.ts @@ -29,6 +29,7 @@ interface ConstructorOptions { getClusterClient: () => Promise; logger: Logger; isWriteEnabled: boolean; + isIndexUpgradeEnabled: boolean; } export class ResourceInstaller { @@ -115,6 +116,7 @@ export class ResourceInstaller { public async installIndexLevelResources(indexInfo: IndexInfo): Promise { await this.installWithTimeout(`resources for index ${indexInfo.baseName}`, async () => { const { componentTemplates, ilmPolicy } = indexInfo.indexOptions; + const { isIndexUpgradeEnabled } = this.options; if (ilmPolicy != null) { await this.createOrUpdateLifecyclePolicy({ @@ -138,9 +140,11 @@ export class ResourceInstaller { }) ); - // TODO: Update all existing namespaced index templates matching this index' base name + if (isIndexUpgradeEnabled) { + // TODO: Update all existing namespaced index templates matching this index' base name - await this.updateIndexMappings(indexInfo); + await this.updateIndexMappings(indexInfo); + } }); } diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts index ed3d5340756e..c69677b091c9 100644 --- a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.ts @@ -22,6 +22,7 @@ interface ConstructorOptions { logger: Logger; kibanaVersion: string; isWriteEnabled: boolean; + isIndexUpgradeEnabled: boolean; } /** @@ -43,6 +44,7 @@ export class RuleDataPluginService { getClusterClient: options.getClusterClient, logger: options.logger, isWriteEnabled: options.isWriteEnabled, + isIndexUpgradeEnabled: options.isIndexUpgradeEnabled, }); this.installCommonResources = Promise.resolve(right('ok')); diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts index c1a4fccaf205..ad0922858063 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.test.ts @@ -17,6 +17,7 @@ import { ALERT_STATUS, ALERT_STATUS_ACTIVE, ALERT_STATUS_RECOVERED, + ALERT_WORKFLOW_STATUS, ALERT_UUID, EVENT_ACTION, EVENT_KIND, @@ -118,7 +119,7 @@ describe('createLifecycleExecutor', () => { ); }); - it('overwrites existing documents for repeatedly firing alerts', async () => { + it('updates existing documents for repeatedly firing alerts', async () => { const logger = loggerMock.create(); const ruleDataClientMock = createRuleDataClientMock(); ruleDataClientMock.getReader().search.mockResolvedValue({ @@ -126,17 +127,35 @@ describe('createLifecycleExecutor', () => { hits: [ { fields: { + '@timestamp': '', [ALERT_ID]: 'TEST_ALERT_0', + [ALERT_UUID]: 'ALERT_0_UUID', + [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_NAME]: 'NAME', + [ALERT_RULE_PRODUCER]: 'PRODUCER', [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', - labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc + [ALERT_RULE_UUID]: 'RULE_UUID', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [ALERT_WORKFLOW_STATUS]: 'closed', + [SPACE_IDS]: ['fake-space-id'], + labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must show up in the written doc }, }, { fields: { + '@timestamp': '', [ALERT_ID]: 'TEST_ALERT_1', + [ALERT_UUID]: 'ALERT_1_UUID', + [ALERT_RULE_CATEGORY]: 'RULE_TYPE_NAME', [ALERT_RULE_CONSUMER]: 'CONSUMER', + [ALERT_RULE_NAME]: 'NAME', + [ALERT_RULE_PRODUCER]: 'PRODUCER', [ALERT_RULE_TYPE_ID]: 'RULE_TYPE_ID', + [ALERT_RULE_UUID]: 'RULE_UUID', + [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [ALERT_WORKFLOW_STATUS]: 'open', + [SPACE_IDS]: ['fake-space-id'], labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, // this must not show up in the written doc }, }, @@ -188,14 +207,19 @@ describe('createLifecycleExecutor', () => { { index: { _id: 'TEST_ALERT_0_UUID' } }, expect.objectContaining({ [ALERT_ID]: 'TEST_ALERT_0', + [ALERT_WORKFLOW_STATUS]: 'closed', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + labels: { LABEL_0_KEY: 'LABEL_0_VALUE' }, + [EVENT_ACTION]: 'active', [EVENT_KIND]: 'signal', }), { index: { _id: 'TEST_ALERT_1_UUID' } }, expect.objectContaining({ [ALERT_ID]: 'TEST_ALERT_1', + [ALERT_WORKFLOW_STATUS]: 'open', [ALERT_STATUS]: ALERT_STATUS_ACTIVE, + [EVENT_ACTION]: 'active', [EVENT_KIND]: 'signal', }), @@ -216,8 +240,6 @@ describe('createLifecycleExecutor', () => { }); it('updates existing documents for recovered alerts', async () => { - // NOTE: the documents should actually also be updated for recurring, - // active alerts (see elastic/kibana#108670) const logger = loggerMock.create(); const ruleDataClientMock = createRuleDataClientMock(); ruleDataClientMock.getReader().search.mockResolvedValue({ diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts index 97337e3a5e09..231d3060a2c5 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_executor.ts @@ -182,19 +182,17 @@ export const createLifecycleExecutor = ( const allAlertIds = [...new Set(currentAlertIds.concat(trackedAlertIds))]; - const trackedAlertStatesOfRecovered = Object.values(state.trackedAlerts).filter( - (trackedAlertState) => !currentAlerts[trackedAlertState.alertId] - ); + const trackedAlertStates = Object.values(state.trackedAlerts); logger.debug( - `Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStatesOfRecovered.length} recovered)` + `Tracking ${allAlertIds.length} alerts (${newAlertIds.length} new, ${trackedAlertStates.length} previous)` ); const alertsDataMap: Record> = { ...currentAlerts, }; - if (trackedAlertStatesOfRecovered.length) { + if (trackedAlertStates.length) { const { hits } = await ruleDataClient.getReader().search({ body: { query: { @@ -207,7 +205,7 @@ export const createLifecycleExecutor = ( }, { terms: { - [ALERT_UUID]: trackedAlertStatesOfRecovered.map( + [ALERT_UUID]: trackedAlertStates.map( (trackedAlertState) => trackedAlertState.alertUuid ), }, @@ -215,7 +213,7 @@ export const createLifecycleExecutor = ( ], }, }, - size: trackedAlertStatesOfRecovered.length, + size: trackedAlertStates.length, collapse: { field: ALERT_UUID, }, diff --git a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts index 2b138ae72330..b2cd69be7fda 100644 --- a/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts +++ b/x-pack/plugins/rule_registry/server/utils/create_lifecycle_rule_type.test.ts @@ -263,6 +263,36 @@ describe('createLifecycleRuleTypeFactory', () => { }, ]); + // TODO mock the resolved value before calling alertWithLifecycle again + const lastOpbeansNodeDoc = helpers.ruleDataClientMock + .getWriter() + .bulk.mock.calls[0][0].body?.concat() + .reverse() + .find( + (doc: any) => !('index' in doc) && doc['service.name'] === 'opbeans-node' + ) as Record; + + const stored = mapValues(lastOpbeansNodeDoc, (val) => { + return castArray(val); + }); + + helpers.ruleDataClientMock.getReader().search.mockResolvedValueOnce({ + hits: { + hits: [{ fields: stored } as any], + total: { + value: 1, + relation: 'eq', + }, + }, + took: 0, + timed_out: false, + _shards: { + failed: 0, + successful: 1, + total: 1, + }, + }); + await helpers.alertWithLifecycle([ { id: 'opbeans-java', @@ -274,6 +304,7 @@ describe('createLifecycleRuleTypeFactory', () => { id: 'opbeans-node', fields: { 'service.name': 'opbeans-node', + 'kibana.alert.workflow_status': 'closed', }, }, ]); @@ -281,7 +312,6 @@ describe('createLifecycleRuleTypeFactory', () => { it('writes the correct alerts', () => { expect(helpers.ruleDataClientMock.getWriter().bulk).toHaveBeenCalledTimes(2); - const body = helpers.ruleDataClientMock.getWriter().bulk.mock.calls[1][0].body!; const documents = body.filter((op: any) => !('index' in op)) as any[]; diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts index bd8b21358e3e..ad6f81eaeeff 100644 --- a/x-pack/plugins/security/server/config_deprecations.test.ts +++ b/x-pack/plugins/security/server/config_deprecations.test.ts @@ -49,7 +49,7 @@ describe('Config Deprecations', () => { expect(migrated.xpack.security.session.idleTimeout).toEqual(123); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"xpack.security.sessionTimeout\\" is deprecated and has been replaced by \\"xpack.security.session.idleTimeout\\"", + "Setting \\"xpack.security.sessionTimeout\\" has been replaced by \\"xpack.security.session.idleTimeout\\"", ] `); }); @@ -71,7 +71,7 @@ describe('Config Deprecations', () => { expect(migrated.xpack.security.audit.appender.type).toEqual('console'); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"xpack.security.audit.appender.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.type\\"", + "Setting \\"xpack.security.audit.appender.kind\\" has been replaced by \\"xpack.security.audit.appender.type\\"", ] `); }); @@ -93,7 +93,7 @@ describe('Config Deprecations', () => { expect(migrated.xpack.security.audit.appender.layout.type).toEqual('pattern'); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"xpack.security.audit.appender.layout.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.layout.type\\"", + "Setting \\"xpack.security.audit.appender.layout.kind\\" has been replaced by \\"xpack.security.audit.appender.layout.type\\"", ] `); }); @@ -115,7 +115,7 @@ describe('Config Deprecations', () => { expect(migrated.xpack.security.audit.appender.policy.type).toEqual('time-interval'); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"xpack.security.audit.appender.policy.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.policy.type\\"", + "Setting \\"xpack.security.audit.appender.policy.kind\\" has been replaced by \\"xpack.security.audit.appender.policy.type\\"", ] `); }); @@ -137,7 +137,7 @@ describe('Config Deprecations', () => { expect(migrated.xpack.security.audit.appender.strategy.type).toEqual('numeric'); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"xpack.security.audit.appender.strategy.kind\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.strategy.type\\"", + "Setting \\"xpack.security.audit.appender.strategy.kind\\" has been replaced by \\"xpack.security.audit.appender.strategy.type\\"", ] `); }); @@ -160,7 +160,7 @@ describe('Config Deprecations', () => { expect(migrated.xpack.security.audit.appender.fileName).toEqual('./audit.log'); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"xpack.security.audit.appender.path\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.fileName\\"", + "Setting \\"xpack.security.audit.appender.path\\" has been replaced by \\"xpack.security.audit.appender.fileName\\"", ] `); }); @@ -222,7 +222,7 @@ describe('Config Deprecations', () => { expect(migrated.xpack.security.audit.appender.fileName).toEqual('./audit.log'); expect(messages).toMatchInlineSnapshot(` Array [ - "\\"xpack.security.audit.appender.path\\" is deprecated and has been replaced by \\"xpack.security.audit.appender.fileName\\"", + "Setting \\"xpack.security.audit.appender.path\\" has been replaced by \\"xpack.security.audit.appender.fileName\\"", ] `); }); @@ -242,7 +242,7 @@ describe('Config Deprecations', () => { const { messages } = applyConfigDeprecations(cloneDeep(config)); expect(messages).toMatchInlineSnapshot(` Array [ - "xpack.security.authorization.legacyFallback.enabled is deprecated and is no longer used", + "You no longer need to configure \\"xpack.security.authorization.legacyFallback.enabled\\".", ] `); }); @@ -262,7 +262,7 @@ describe('Config Deprecations', () => { const { messages } = applyConfigDeprecations(cloneDeep(config)); expect(messages).toMatchInlineSnapshot(` Array [ - "xpack.security.authc.saml.maxRedirectURLSize is deprecated and is no longer used", + "You no longer need to configure \\"xpack.security.authc.saml.maxRedirectURLSize\\".", ] `); }); @@ -286,7 +286,7 @@ describe('Config Deprecations', () => { const { messages } = applyConfigDeprecations(cloneDeep(config)); expect(messages).toMatchInlineSnapshot(` Array [ - "\`xpack.security.authc.providers.saml..maxRedirectURLSize\` is deprecated and is no longer used", + "\\"xpack.security.authc.providers.saml..maxRedirectURLSize\\" is is no longer used.", ] `); }); @@ -305,7 +305,7 @@ describe('Config Deprecations', () => { expect(migrated).toEqual(config); expect(messages).toMatchInlineSnapshot(` Array [ - "Defining \\"xpack.security.authc.providers\\" as an array of provider types is deprecated. Use extended \\"object\\" format instead.", + "\\"xpack.security.authc.providers\\" accepts an extended \\"object\\" format instead of an array of provider types.", ] `); }); @@ -324,8 +324,8 @@ describe('Config Deprecations', () => { expect(migrated).toEqual(config); expect(messages).toMatchInlineSnapshot(` Array [ - "Defining \\"xpack.security.authc.providers\\" as an array of provider types is deprecated. Use extended \\"object\\" format instead.", - "Enabling both \`basic\` and \`token\` authentication providers in \`xpack.security.authc.providers\` is deprecated. Login page will only use \`token\` provider.", + "\\"xpack.security.authc.providers\\" accepts an extended \\"object\\" format instead of an array of provider types.", + "Enabling both \\"basic\\" and \\"token\\" authentication providers in \\"xpack.security.authc.providers\\" is deprecated. Login page will only use \\"token\\" provider.", ] `); }); @@ -341,9 +341,9 @@ describe('Config Deprecations', () => { const { messages, migrated } = applyConfigDeprecations(cloneDeep(config)); expect(migrated).toEqual(config); expect(messages).toMatchInlineSnapshot(` - Array [ - "Disabling the security plugin (\`xpack.security.enabled\`) will not be supported in the next major version (8.0). To turn off security features, disable them in Elasticsearch instead.", - ] + Array [ + "Disabling the security plugin \\"xpack.security.enabled\\" will only be supported by disable security in Elasticsearch.", + ] `); }); diff --git a/x-pack/plugins/security/server/config_deprecations.ts b/x-pack/plugins/security/server/config_deprecations.ts index 808a192f8320..f68112760632 100644 --- a/x-pack/plugins/security/server/config_deprecations.ts +++ b/x-pack/plugins/security/server/config_deprecations.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { i18n } from '@kbn/i18n'; import type { ConfigDeprecationProvider } from 'src/core/server'; export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ @@ -27,27 +28,45 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ const legacyAuditLoggerEnabled = !settings?.xpack?.security?.audit?.appender; if (auditLoggingEnabled && legacyAuditLoggerEnabled) { addDeprecation({ - message: `The legacy audit logger is deprecated in favor of the new ECS-compliant audit logger.`, + title: i18n.translate('xpack.security.deprecations.auditLoggerTitle', { + defaultMessage: 'The legacy audit logger is deprecated', + }), + message: i18n.translate('xpack.security.deprecations.auditLoggerMessage', { + defaultMessage: + 'The legacy audit logger is deprecated in favor of the new ECS-compliant audit logger.', + }), documentationUrl: 'https://www.elastic.co/guide/en/kibana/current/security-settings-kb.html#audit-logging-settings', correctiveActions: { manualSteps: [ - `Declare an audit logger "appender" via "xpack.security.audit.appender" to enable the ECS audit logger.`, + i18n.translate('xpack.security.deprecations.auditLogger.manualStepOneMessage', { + defaultMessage: + 'Declare an audit logger "appender" via "xpack.security.audit.appender" to enable the ECS audit logger.', + }), ], }, }); } }, + // Deprecation warning for the old array-based format of `xpack.security.authc.providers`. (settings, fromPath, addDeprecation) => { if (Array.isArray(settings?.xpack?.security?.authc?.providers)) { addDeprecation({ - message: - `Defining "xpack.security.authc.providers" as an array of provider types is deprecated. ` + - `Use extended "object" format instead.`, + title: i18n.translate('xpack.security.deprecations.authcProvidersTitle', { + defaultMessage: + 'Defining "xpack.security.authc.providers" as an array of provider types is deprecated', + }), + message: i18n.translate('xpack.security.deprecations.authcProvidersMessage', { + defaultMessage: + '"xpack.security.authc.providers" accepts an extended "object" format instead of an array of provider types.', + }), correctiveActions: { manualSteps: [ - `Use the extended object format for "xpack.security.authc.providers" in your Kibana configuration.`, + i18n.translate('xpack.security.deprecations.authcProviders.manualStepOneMessage', { + defaultMessage: + 'Use the extended object format for "xpack.security.authc.providers" in your Kibana configuration.', + }), ], }, }); @@ -67,11 +86,23 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ if (hasProviderType('basic') && hasProviderType('token')) { addDeprecation({ - message: - 'Enabling both `basic` and `token` authentication providers in `xpack.security.authc.providers` is deprecated. Login page will only use `token` provider.', + title: i18n.translate('xpack.security.deprecations.basicAndTokenProvidersTitle', { + defaultMessage: + 'Both "basic" and "token" authentication providers are enabled in "xpack.security.authc.providers"', + }), + message: i18n.translate('xpack.security.deprecations.basicAndTokenProvidersMessage', { + defaultMessage: + 'Enabling both "basic" and "token" authentication providers in "xpack.security.authc.providers" is deprecated. Login page will only use "token" provider.', + }), correctiveActions: { manualSteps: [ - 'Remove either the `basic` or `token` auth provider in "xpack.security.authc.providers" from your Kibana configuration.', + i18n.translate( + 'xpack.security.deprecations.basicAndTokenProviders.manualStepOneMessage', + { + defaultMessage: + 'Remove either the "basic" or "token" auth provider in "xpack.security.authc.providers" from your Kibana configuration.', + } + ), ], }, }); @@ -84,11 +115,20 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ >; if (Object.values(samlProviders).find((provider) => !!provider.maxRedirectURLSize)) { addDeprecation({ - message: - '`xpack.security.authc.providers.saml..maxRedirectURLSize` is deprecated and is no longer used', + title: i18n.translate('xpack.security.deprecations.maxRedirectURLSizeTitle', { + defaultMessage: + '"xpack.security.authc.providers.saml..maxRedirectURLSize" is deprecated', + }), + message: i18n.translate('xpack.security.deprecations.maxRedirectURLSizeMessage', { + defaultMessage: + '"xpack.security.authc.providers.saml..maxRedirectURLSize" is is no longer used.', + }), correctiveActions: { manualSteps: [ - `Remove "xpack.security.authc.providers.saml..maxRedirectURLSize" from your Kibana configuration.`, + i18n.translate('xpack.security.deprecations.maxRedirectURLSize.manualStepOneMessage', { + defaultMessage: + 'Remove "xpack.security.authc.providers.saml..maxRedirectURLSize" from your Kibana configuration.', + }), ], }, }); @@ -97,13 +137,21 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ (settings, fromPath, addDeprecation) => { if (settings?.xpack?.security?.enabled === false) { addDeprecation({ - message: - 'Disabling the security plugin (`xpack.security.enabled`) will not be supported in the next major version (8.0). ' + - 'To turn off security features, disable them in Elasticsearch instead.', + title: i18n.translate('xpack.security.deprecations.enabledTitle', { + defaultMessage: 'Disabling the security plugin "xpack.security.enabled" is deprecated', + }), + message: i18n.translate('xpack.security.deprecations.enabledMessage', { + defaultMessage: + 'Disabling the security plugin "xpack.security.enabled" will only be supported by disable security in Elasticsearch.', + }), correctiveActions: { manualSteps: [ - `Remove "xpack.security.enabled" from your Kibana configuration.`, - `To turn off security features, disable them in Elasticsearch instead.`, + i18n.translate('xpack.security.deprecations.enabled.manualStepOneMessage', { + defaultMessage: `Remove "xpack.security.enabled" from your Kibana configuration.`, + }), + i18n.translate('xpack.security.deprecations.enabled.manualStepTwoMessage', { + defaultMessage: `To turn off security features, disable them in Elasticsearch instead.`, + }), ], }, }); diff --git a/x-pack/plugins/security/server/session_management/session_index.ts b/x-pack/plugins/security/server/session_management/session_index.ts index 9093d5d2e0db..f1a9296177d9 100644 --- a/x-pack/plugins/security/server/session_management/session_index.ts +++ b/x-pack/plugins/security/server/session_management/session_index.ts @@ -317,69 +317,73 @@ export class SessionIndex { const sessionIndexTemplateName = `${this.options.kibanaIndexName}_security_session_index_template_${SESSION_INDEX_TEMPLATE_VERSION}`; return (this.indexInitialization = new Promise(async (resolve, reject) => { - // Check if required index template exists. - let indexTemplateExists = false; try { - indexTemplateExists = ( - await this.options.elasticsearchClient.indices.existsTemplate({ - name: sessionIndexTemplateName, - }) - ).body; - } catch (err) { - this.options.logger.error( - `Failed to check if session index template exists: ${err.message}` - ); - return reject(err); - } - - // Create index template if it doesn't exist. - if (indexTemplateExists) { - this.options.logger.debug('Session index template already exists.'); - } else { + // Check if required index template exists. + let indexTemplateExists = false; try { - await this.options.elasticsearchClient.indices.putTemplate({ - name: sessionIndexTemplateName, - body: getSessionIndexTemplate(this.indexName), - }); - this.options.logger.debug('Successfully created session index template.'); + indexTemplateExists = ( + await this.options.elasticsearchClient.indices.existsTemplate({ + name: sessionIndexTemplateName, + }) + ).body; } catch (err) { - this.options.logger.error(`Failed to create session index template: ${err.message}`); + this.options.logger.error( + `Failed to check if session index template exists: ${err.message}` + ); return reject(err); } - } - // Check if required index exists. We cannot be sure that automatic creation of indices is - // always enabled, so we create session index explicitly. - let indexExists = false; - try { - indexExists = ( - await this.options.elasticsearchClient.indices.exists({ index: this.indexName }) - ).body; - } catch (err) { - this.options.logger.error(`Failed to check if session index exists: ${err.message}`); - return reject(err); - } + // Create index template if it doesn't exist. + if (indexTemplateExists) { + this.options.logger.debug('Session index template already exists.'); + } else { + try { + await this.options.elasticsearchClient.indices.putTemplate({ + name: sessionIndexTemplateName, + body: getSessionIndexTemplate(this.indexName), + }); + this.options.logger.debug('Successfully created session index template.'); + } catch (err) { + this.options.logger.error(`Failed to create session index template: ${err.message}`); + return reject(err); + } + } - // Create index if it doesn't exist. - if (indexExists) { - this.options.logger.debug('Session index already exists.'); - } else { + // Check if required index exists. We cannot be sure that automatic creation of indices is + // always enabled, so we create session index explicitly. + let indexExists = false; try { - await this.options.elasticsearchClient.indices.create({ index: this.indexName }); - this.options.logger.debug('Successfully created session index.'); + indexExists = ( + await this.options.elasticsearchClient.indices.exists({ index: this.indexName }) + ).body; } catch (err) { - // There can be a race condition if index is created by another Kibana instance. - if (err?.body?.error?.type === 'resource_already_exists_exception') { - this.options.logger.debug('Session index already exists.'); - } else { - this.options.logger.error(`Failed to create session index: ${err.message}`); - return reject(err); + this.options.logger.error(`Failed to check if session index exists: ${err.message}`); + return reject(err); + } + + // Create index if it doesn't exist. + if (indexExists) { + this.options.logger.debug('Session index already exists.'); + } else { + try { + await this.options.elasticsearchClient.indices.create({ index: this.indexName }); + this.options.logger.debug('Successfully created session index.'); + } catch (err) { + // There can be a race condition if index is created by another Kibana instance. + if (err?.body?.error?.type === 'resource_already_exists_exception') { + this.options.logger.debug('Session index already exists.'); + } else { + this.options.logger.error(`Failed to create session index: ${err.message}`); + return reject(err); + } } } - } - // Notify any consumers that are awaiting on this promise and immediately reset it. - resolve(); + // Notify any consumers that are awaiting on this promise and immediately reset it. + resolve(); + } catch (error) { + reject(error); + } }).finally(() => { this.indexInitialization = undefined; })); diff --git a/x-pack/plugins/security_solution/common/ecs/event/index.ts b/x-pack/plugins/security_solution/common/ecs/event/index.ts index 14f38480f90c..9e2ebb059b3b 100644 --- a/x-pack/plugins/security_solution/common/ecs/event/index.ts +++ b/x-pack/plugins/security_solution/common/ecs/event/index.ts @@ -53,7 +53,7 @@ export enum EventCode { // Memory Protection alert MEMORY_SIGNATURE = 'memory_signature', // Memory Protection alert - MALICIOUS_THREAD = 'malicious_thread', + SHELLCODE_THREAD = 'shellcode_thread', // behavior BEHAVIOR = 'behavior', } diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index afe85e1abaa5..8f985db732b6 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -678,7 +678,7 @@ export class EndpointDocGenerator extends BaseDataGenerator { action: 'start', kind: 'alert', category: 'malware', - code: isShellcode ? 'malicious_thread' : 'memory_signature', + code: isShellcode ? 'shellcode_thread' : 'memory_signature', id: this.seededUUIDv4(), dataset: 'endpoint', module: 'endpoint', diff --git a/x-pack/plugins/security_solution/common/types/timeline/store.ts b/x-pack/plugins/security_solution/common/types/timeline/store.ts index 01fc9db7c8e1..03cf0c39378e 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/store.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/store.ts @@ -48,6 +48,7 @@ export interface TimelinePersistInput { expandedDetail?: TimelineExpandedDetail; filters?: Filter[]; columns: ColumnHeaderOptions[]; + defaultColumns?: ColumnHeaderOptions[]; itemsPerPage?: number; indexNames: string[]; kqlQuery?: { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index f5cbc65effd8..674114188632 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -54,7 +54,7 @@ describe('Alert details with unmapped fields', () => { it('Displays the unmapped field on the table', () => { const expectedUnmmappedField = { - row: 90, + row: 86, field: 'unmapped', text: 'This is the unmapped field', }; diff --git a/x-pack/plugins/security_solution/cypress/objects/rule.ts b/x-pack/plugins/security_solution/cypress/objects/rule.ts index 41027258f0bf..c3eab5cc2a93 100644 --- a/x-pack/plugins/security_solution/cypress/objects/rule.ts +++ b/x-pack/plugins/security_solution/cypress/objects/rule.ts @@ -164,7 +164,7 @@ const getRunsEvery = (): Interval => ({ }); const getLookBack = (): Interval => ({ - interval: '17520', + interval: '50000', timeType: 'Hours', type: 'h', }); @@ -382,5 +382,5 @@ export const getEditedRule = (): CustomRule => ({ export const expectedExportedRule = (ruleResponse: Cypress.Response): string => { const jsonrule = ruleResponse.body; - return `{"id":"${jsonrule.id}","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","created_at":"${jsonrule.created_at}","created_by":"elastic","name":"${jsonrule.name}","tags":[],"interval":"100m","enabled":false,"description":"${jsonrule.description}","risk_score":${jsonrule.risk_score},"severity":"${jsonrule.severity}","output_index":".siem-signals-default","author":[],"false_positives":[],"from":"now-17520h","rule_id":"rule_testing","max_signals":100,"risk_score_mapping":[],"severity_mapping":[],"threat":[],"to":"now","references":[],"version":1,"exceptions_list":[],"immutable":false,"type":"query","language":"kuery","index":["exceptions-*"],"query":"${jsonrule.query}","throttle":"no_actions","actions":[]}\n{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n`; + return `{"id":"${jsonrule.id}","updated_at":"${jsonrule.updated_at}","updated_by":"elastic","created_at":"${jsonrule.created_at}","created_by":"elastic","name":"${jsonrule.name}","tags":[],"interval":"100m","enabled":false,"description":"${jsonrule.description}","risk_score":${jsonrule.risk_score},"severity":"${jsonrule.severity}","output_index":".siem-signals-default","author":[],"false_positives":[],"from":"now-50000h","rule_id":"rule_testing","max_signals":100,"risk_score_mapping":[],"severity_mapping":[],"threat":[],"to":"now","references":[],"version":1,"exceptions_list":[],"immutable":false,"type":"query","language":"kuery","index":["exceptions-*"],"query":"${jsonrule.query}","throttle":"no_actions","actions":[]}\n{"exported_count":1,"missing_rules":[],"missing_rules_count":0}\n`; }; diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index fe7bf959fe05..675a25641a2b 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -41,8 +41,6 @@ export const ACKNOWLEDGED_ALERTS_FILTER_BTN = '[data-test-subj="acknowledgedAler export const LOADING_ALERTS_PANEL = '[data-test-subj="loading-alerts-panel"]'; -export const LOADING_SPINNER = '[data-test-subj="LoadingPanelTimeline"]'; - export const MANAGE_ALERT_DETECTION_RULES_BTN = '[data-test-subj="manage-alert-detection-rules"]'; export const MARK_ALERT_ACKNOWLEDGED_BTN = '[data-test-subj="acknowledged-alert-status"]'; diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts index 1520a88ec31b..871ef0ca51ce 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts.ts @@ -63,7 +63,12 @@ export const closeAlerts = () => { }; export const expandFirstAlert = () => { - cy.get(EXPAND_ALERT_BTN).should('exist').first().click({ force: true }); + cy.get(EXPAND_ALERT_BTN).should('exist'); + + cy.get(EXPAND_ALERT_BTN) + .first() + .pipe(($el) => $el.trigger('click')) + .should('exist'); }; export const viewThreatIntelTab = () => cy.get(THREAT_INTEL_TAB).click(); diff --git a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts index b4e4941ff7f9..33bd8a06b998 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/api_calls/rules.ts @@ -19,7 +19,7 @@ export const createCustomRule = (rule: CustomRule, ruleId = 'rule_testing', inte name: rule.name, severity: rule.severity.toLocaleLowerCase(), type: 'query', - from: 'now-17520h', + from: 'now-50000h', index: ['exceptions-*'], query: rule.customQuery, language: 'kuery', @@ -59,7 +59,7 @@ export const createCustomIndicatorRule = (rule: ThreatIndicatorRule, ruleId = 'r threat_filters: [], threat_index: rule.indicatorIndexPattern, threat_indicator_path: '', - from: 'now-17520h', + from: 'now-50000h', index: rule.index, query: rule.customQuery || '*:*', language: 'kuery', @@ -86,7 +86,7 @@ export const createCustomRuleActivated = ( name: rule.name, severity: rule.severity.toLocaleLowerCase(), type: 'query', - from: 'now-17520h', + from: 'now-50000h', index: rule.index, query: rule.customQuery, language: 'kuery', 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 98f9b3455841..e2d27a11ed71 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 @@ -14,7 +14,6 @@ import { ThreatIndicatorRule, ThresholdRule, } from '../objects/rule'; -import { LOADING_SPINNER } from '../screens/alerts'; import { ABOUT_CONTINUE_BTN, ABOUT_EDIT_TAB, @@ -94,6 +93,7 @@ import { EMAIL_CONNECTOR_USER_INPUT, EMAIL_CONNECTOR_PASSWORD_INPUT, } from '../screens/create_new_rule'; +import { LOADING_INDICATOR } from '../screens/security_header'; import { TOAST_ERROR } from '../screens/shared'; import { SERVER_SIDE_EVENT_COUNT } from '../screens/timeline'; import { TIMELINE } from '../screens/timelines'; @@ -532,7 +532,7 @@ export const waitForAlertsToPopulate = async (alertCountThreshold = 1) => { cy.waitUntil( () => { refreshPage(); - cy.get(LOADING_SPINNER).should('not.exist'); + cy.get(LOADING_INDICATOR).should('not.exist'); return cy .get(SERVER_SIDE_EVENT_COUNT) .invoke('text') diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 3a0eb1a5458a..5fda8730d5e9 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -14,7 +14,7 @@ "cypress:run": "yarn cypress:run:reporter --browser chrome --headless --spec './cypress/integration/**/*.spec.ts'; status=$?; yarn junit:merge && exit $status", "cypress:run:firefox": "yarn cypress:run:reporter --browser firefox --headless --spec './cypress/integration/**/*.spec.ts'; status=$?; yarn junit:merge && exit $status", "cypress:run:reporter": "yarn cypress run --config-file ./cypress/cypress.json --reporter ../../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json", - "cypress:run:ccs": "yarn cypress:run:reporter --browser chrome --headless --config integrationFolder=./cypress/ccs_integration", + "cypress:run:ccs": "yarn cypress:run:reporter --browser chrome --headless --config integrationFolder=./cypress/ccs_integration; status=$?; yarn junit:merge && exit $status", "cypress:run-as-ci": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/security_solution_cypress/cli_config.ts", "cypress:run-as-ci:firefox": "node --max-old-space-size=2048 ../../../scripts/functional_tests --config ../../test/security_solution_cypress/config.firefox.ts", "cypress:run:upgrade": "yarn cypress:run:reporter --browser chrome --headless --config integrationFolder=./cypress/upgrade_integration", diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts index 4df49b957ad9..59af6737e495 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.test.ts @@ -101,68 +101,4 @@ describe('public search functions', () => { }); expect(deepLinks.some((l) => l.id === SecurityPageName.ueba)).toBeTruthy(); }); - - describe('Detections Alerts deep links', () => { - it('should return alerts link for basic license with only read_alerts capabilities', () => { - const basicLicense = 'basic'; - const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ - siem: { read_alerts: true, crud_alerts: false }, - } as unknown) as Capabilities); - - const detectionsDeepLinks = - basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? []; - - expect( - detectionsDeepLinks.length && - detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts) - ).toBeTruthy(); - }); - - it('should return alerts link with for basic license with crud_alerts capabilities', () => { - const basicLicense = 'basic'; - const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ - siem: { read_alerts: true, crud_alerts: true }, - } as unknown) as Capabilities); - - const detectionsDeepLinks = - basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? []; - - expect( - detectionsDeepLinks.length && - detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts) - ).toBeTruthy(); - }); - - it('should NOT return alerts link for basic license with NO read_alerts capabilities', () => { - const basicLicense = 'basic'; - const basicLinks = getDeepLinks(mockGlobalState.app.enableExperimental, basicLicense, ({ - siem: { read_alerts: false, crud_alerts: false }, - } as unknown) as Capabilities); - - const detectionsDeepLinks = - basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? []; - - expect( - detectionsDeepLinks.length && - detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts) - ).toBeFalsy(); - }); - - it('should return alerts link for basic license with undefined capabilities', () => { - const basicLicense = 'basic'; - const basicLinks = getDeepLinks( - mockGlobalState.app.enableExperimental, - basicLicense, - undefined - ); - - const detectionsDeepLinks = - basicLinks.find((l) => l.id === SecurityPageName.detections)?.deepLinks ?? []; - - expect( - detectionsDeepLinks.length && - detectionsDeepLinks.some((l) => l.id === SecurityPageName.alerts) - ).toBeTruthy(); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/app/deep_links/index.ts b/x-pack/plugins/security_solution/public/app/deep_links/index.ts index bafab2dd659f..9f13a8be0e13 100644 --- a/x-pack/plugins/security_solution/public/app/deep_links/index.ts +++ b/x-pack/plugins/security_solution/public/app/deep_links/index.ts @@ -368,16 +368,7 @@ export function getDeepLinks( deepLinks: [], }; } - if ( - deepLinkId === SecurityPageName.detections && - capabilities != null && - capabilities.siem.read_alerts === false - ) { - return { - ...deepLink, - deepLinks: baseDeepLinks.filter(({ id }) => id !== SecurityPageName.alerts), - }; - } + if (isPremiumLicense(licenseType) && subPluginDeepLinks?.premium) { return { ...deepLink, diff --git a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx index 3ec616127f24..7041cc426450 100644 --- a/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx +++ b/x-pack/plugins/security_solution/public/cases/components/case_view/index.tsx @@ -59,7 +59,7 @@ const TimelineDetailsPanel = () => { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx index db5eb2d882c6..2b399a057117 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.test.tsx @@ -86,8 +86,8 @@ describe('AlertSummaryView', () => { return { category: 'event', field: 'event.code', - values: ['malicious_thread'], - originalValue: ['malicious_thread'], + values: ['shellcode_thread'], + originalValue: ['shellcode_thread'], }; } return item; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx index d8c1cc7fbfa6..da6c091ab069 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/alert_summary_view.tsx @@ -157,7 +157,7 @@ function getEventFieldsToDisplay({ }): EventSummaryField[] { switch (eventCode) { // memory protection fields - case EventCode.MALICIOUS_THREAD: + case EventCode.SHELLCODE_THREAD: return memoryShellCodeAlertFields; case EventCode.MEMORY_SIGNATURE: return memorySignatureAlertFields; diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx index 29ba8fc0bd54..7b7a1ead5d70 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/events_viewer.test.tsx @@ -33,14 +33,6 @@ import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_fe import { defaultCellActions } from '../../lib/cell_actions/default_cell_actions'; import { mockTimelines } from '../../mock/mock_timelines_plugin'; -jest.mock('@kbn/alerts', () => ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); - jest.mock('../../lib/kibana', () => ({ useKibana: () => ({ services: { diff --git a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx index 70fd80a13555..c91b646aba96 100644 --- a/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/events_viewer/index.tsx @@ -5,8 +5,8 @@ * 2.0. */ -import React, { useMemo, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import React, { useCallback, useMemo, useEffect } from 'react'; +import { connect, ConnectedProps, useDispatch } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import styled from 'styled-components'; @@ -78,6 +78,7 @@ type Props = OwnProps & PropsFromRedux; const StatefulEventsViewerComponent: React.FC = ({ createTimeline, columns, + defaultColumns, dataProviders, defaultCellActions, deletedEventIds, @@ -108,6 +109,7 @@ const StatefulEventsViewerComponent: React.FC = ({ hasAlertsCrud = false, unit, }) => { + const dispatch = useDispatch(); const { timelines: timelinesUi } = useKibana().services; const { browserFields, @@ -127,6 +129,7 @@ const StatefulEventsViewerComponent: React.FC = ({ createTimeline({ id, columns, + defaultColumns, excludedRowRendererIds, indexNames: selectedPatterns, sort, @@ -149,6 +152,13 @@ const StatefulEventsViewerComponent: React.FC = ({ ) : null, [graphEventId, id] ); + const setQuery = useCallback( + (inspect, loading, refetch) => { + dispatch(inputsActions.setQuery({ id, inputId: 'global', inspect, loading, refetch })); + }, + [dispatch, id] + ); + return ( <> @@ -180,6 +190,7 @@ const StatefulEventsViewerComponent: React.FC = ({ onRuleChange, renderCellValue, rowRenderers, + setQuery, start, sort, additionalFilters, @@ -241,6 +252,7 @@ const makeMapStateToProps = () => { const timeline: TimelineModel = getTimeline(state, id) ?? defaultModel; const { columns, + defaultColumns, dataProviders, deletedEventIds, excludedRowRendererIds, @@ -254,6 +266,7 @@ const makeMapStateToProps = () => { return { columns, + defaultColumns, dataProviders, deletedEventIds, excludedRowRendererIds, diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json index d46b39b90fe5..043ea11a51fd 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/exceptionable_endpoint_fields.json @@ -19,13 +19,13 @@ "Target.process.pe.original_file_name", "Target.process.pe.product", "Target.process.pgid", - "Target.process.thread.Ext.start_address_details.allocation_type", + "Target.process.Ext.memory_region.allocation_type", "Target.process.thread.Ext.start_address_bytes_disasm_hash", "Target.process.thread.Ext.start_address_allocation_offset", - "Target.process.thread.Ext.start_address_details.allocation_size", - "Target.process.thread.Ext.start_address_details.region_size", - "Target.process.thread.Ext.start_address_details.region_protection", - "Target.process.thread.Ext.start_address_details.memory_pe.imphash", + "Target.process.Ext.memory_region.allocation_size", + "Target.process.Ext.memory_region.region_size", + "Target.process.Ext.memory_region.region_protection", + "Target.process.Ext.memory_region.memory_pe.imphash", "Target.process.thread.Ext.start_address_bytes", "agent.id", "agent.type", @@ -82,6 +82,8 @@ "process.Ext.services", "process.Ext.user", "process.Ext.code_signature", + "process.Ext.token.integrity_level_name", + "process.Ext.memory_region.malware_signature.all_names", "process.executable", "process.hash.md5", "process.hash.sha1", diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx index 9696604ddf22..209d7d8fa273 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.test.tsx @@ -1031,7 +1031,7 @@ describe('Exception helpers', () => { ]); }); - test('it should return pre-populated memory shellcode items for event code `malicious_thread`', () => { + test('it should return pre-populated memory shellcode items for event code `shellcode_thread`', () => { const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { _id: '123', process: { @@ -1049,7 +1049,7 @@ describe('Exception helpers', () => { self_injection: true, }, event: { - code: 'malicious_thread', + code: 'shellcode_thread', }, Target: { process: { @@ -1108,52 +1108,10 @@ describe('Exception helpers', () => { value: 'high', id: '123', }, - { - field: 'Target.process.thread.Ext.start_address_details', - type: 'nested', - entries: [ - { - field: 'allocation_type', - operator: 'included', - type: 'match', - value: 'PRIVATE', - id: '123', - }, - { - field: 'allocation_size', - operator: 'included', - type: 'match', - value: '4000', - id: '123', - }, - { - field: 'region_size', - operator: 'included', - type: 'match', - value: '4000', - id: '123', - }, - { - field: 'region_protection', - operator: 'included', - type: 'match', - value: 'RWX', - id: '123', - }, - { - field: 'memory_pe.imphash', - operator: 'included', - type: 'match', - value: 'a hash', - id: '123', - }, - ], - id: '123', - }, ]); }); - test('it should return pre-populated memory shellcode items for event code `malicious_thread` and skip empty', () => { + test('it should return pre-populated memory shellcode items for event code `shellcode_thread` and skip empty', () => { const defaultItems = defaultEndpointExceptionItems('list_id', 'my_rule', { _id: '123', process: { @@ -1171,7 +1129,7 @@ describe('Exception helpers', () => { self_injection: true, }, event: { - code: 'malicious_thread', + code: 'shellcode_thread', }, Target: { process: { @@ -1217,41 +1175,6 @@ describe('Exception helpers', () => { value: 'high', id: '123', }, - { - field: 'Target.process.thread.Ext.start_address_details', - type: 'nested', - entries: [ - { - field: 'allocation_size', - operator: 'included', - type: 'match', - value: '4000', - id: '123', - }, - { - field: 'region_size', - operator: 'included', - type: 'match', - value: '4000', - id: '123', - }, - { - field: 'region_protection', - operator: 'included', - type: 'match', - value: 'RWX', - id: '123', - }, - { - field: 'memory_pe.imphash', - operator: 'included', - type: 'match', - value: 'a hash', - id: '123', - }, - ], - id: '123', - }, ]); }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx index 3d219b90a2fc..58da977fcb8f 100644 --- a/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/exceptions/helpers.tsx @@ -577,7 +577,7 @@ export const getPrepopulatedMemoryShellcodeException = ({ eventCode: string; alertEcsData: Flattened; }): ExceptionsBuilderExceptionItem => { - const { process, Target } = alertEcsData; + const { process } = alertEcsData; const entries = filterEmptyExceptionEntries([ { field: 'Memory_protection.feature', @@ -609,44 +609,6 @@ export const getPrepopulatedMemoryShellcodeException = ({ type: 'match' as const, value: process?.Ext?.token?.integrity_level_name ?? '', }, - { - field: 'Target.process.thread.Ext.start_address_details', - type: 'nested' as const, - entries: [ - { - field: 'allocation_type', - operator: 'included' as const, - type: 'match' as const, - value: Target?.process?.thread?.Ext?.start_address_details?.allocation_type ?? '', - }, - { - field: 'allocation_size', - operator: 'included' as const, - type: 'match' as const, - value: String(Target?.process?.thread?.Ext?.start_address_details?.allocation_size) ?? '', - }, - { - field: 'region_size', - operator: 'included' as const, - type: 'match' as const, - value: String(Target?.process?.thread?.Ext?.start_address_details?.region_size) ?? '', - }, - { - field: 'region_protection', - operator: 'included' as const, - type: 'match' as const, - value: - String(Target?.process?.thread?.Ext?.start_address_details?.region_protection) ?? '', - }, - { - field: 'memory_pe.imphash', - operator: 'included' as const, - type: 'match' as const, - value: - String(Target?.process?.thread?.Ext?.start_address_details?.memory_pe?.imphash) ?? '', - }, - ], - }, ]); return { @@ -845,7 +807,7 @@ export const defaultEndpointExceptionItems = ( alertEcsData, }), ]; - case 'malicious_thread': + case 'shellcode_thread': return [ getPrepopulatedMemoryShellcodeException({ listId, diff --git a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx index 46d05d971222..94abbca1da90 100644 --- a/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/header_page/index.tsx @@ -84,27 +84,29 @@ export interface HeaderPageProps extends HeaderProps { titleNode?: React.ReactElement; } -const HeaderLinkBack: React.FC<{ backOptions: BackOptions }> = React.memo(({ backOptions }) => { - const { navigateToUrl } = useKibana().services.application; - const { formatUrl } = useFormatUrl(backOptions.pageId); +export const HeaderLinkBack: React.FC<{ backOptions: BackOptions }> = React.memo( + ({ backOptions }) => { + const { navigateToUrl } = useKibana().services.application; + const { formatUrl } = useFormatUrl(backOptions.pageId); - const backUrl = formatUrl(backOptions.path ?? ''); - return ( - - { - ev.preventDefault(); - navigateToUrl(backUrl); - }} - href={backUrl} - iconType="arrowLeft" - > - {backOptions.text} - - - ); -}); + const backUrl = formatUrl(backOptions.path ?? ''); + return ( + + { + ev.preventDefault(); + navigateToUrl(backUrl); + }} + href={backUrl} + iconType="arrowLeft" + > + {backOptions.text} + + + ); + } +); HeaderLinkBack.displayName = 'HeaderLinkBack'; const HeaderPageComponent: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx index 1f98d3b82612..b488000ac873 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/index.test.tsx @@ -4,7 +4,6 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { useGetUserAlertsPermissions } from '@kbn/alerts'; import { renderHook } from '@testing-library/react-hooks'; import { KibanaPageTemplateProps } from '../../../../../../../../src/plugins/kibana_react/public'; @@ -24,7 +23,6 @@ jest.mock('../../../lib/kibana'); jest.mock('../../../hooks/use_selector'); jest.mock('../../../hooks/use_experimental_features'); jest.mock('../../../utils/route/use_route_spy'); -jest.mock('@kbn/alerts'); describe('useSecuritySolutionNavigation', () => { const mockUrlState = { [CONSTANTS.appQuery]: { query: 'host.name:"security-solution-es"', language: 'kuery' }, @@ -76,11 +74,6 @@ describe('useSecuritySolutionNavigation', () => { (useIsExperimentalFeatureEnabled as jest.Mock).mockReturnValue(false); (useDeepEqualSelector as jest.Mock).mockReturnValue({ urlState: mockUrlState }); (useRouteSpy as jest.Mock).mockReturnValue(mockRouteSpy); - (useGetUserAlertsPermissions as jest.Mock).mockReturnValue({ - loading: false, - crud: true, - read: true, - }); (useKibana as jest.Mock).mockReturnValue({ services: { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx index ca574a587276..1630bc47fd0c 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx +++ b/x-pack/plugins/security_solution/public/common/components/navigation/use_security_solution_navigation/use_navigation_items.tsx @@ -7,15 +7,13 @@ import React, { useCallback, useMemo } from 'react'; import { EuiSideNavItemType } from '@elastic/eui/src/components/side_nav/side_nav_types'; -import { useGetUserAlertsPermissions } from '@kbn/alerts'; import { securityNavGroup } from '../../../../app/home/home_navigations'; import { getSearch } from '../helpers'; import { PrimaryNavigationItemsProps } from './types'; -import { useGetUserCasesPermissions, useKibana } from '../../../lib/kibana'; +import { useGetUserCasesPermissions } from '../../../lib/kibana'; import { useNavigation } from '../../../lib/kibana/hooks'; import { NavTab } from '../types'; -import { SERVER_APP_ID } from '../../../../../common/constants'; export const usePrimaryNavigationItems = ({ navTabs, @@ -63,9 +61,7 @@ export const usePrimaryNavigationItems = ({ }; function usePrimaryNavigationItemsToDisplay(navTabs: Record) { - const uiCapabilities = useKibana().services.application.capabilities; const hasCasesReadPermissions = useGetUserCasesPermissions()?.read; - const hasAlertsReadPermissions = useGetUserAlertsPermissions(uiCapabilities, SERVER_APP_ID); return useMemo( () => [ { @@ -75,9 +71,7 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { }, { ...securityNavGroup.detect, - items: hasAlertsReadPermissions.read - ? [navTabs.alerts, navTabs.rules, navTabs.exceptions] - : [navTabs.rules, navTabs.exceptions], + items: [navTabs.alerts, navTabs.rules, navTabs.exceptions], }, { ...securityNavGroup.explore, @@ -92,6 +86,6 @@ function usePrimaryNavigationItemsToDisplay(navTabs: Record) { items: [navTabs.endpoints, navTabs.trusted_apps, navTabs.event_filters], }, ], - [navTabs, hasCasesReadPermissions, hasAlertsReadPermissions] + [navTabs, hasCasesReadPermissions] ); } diff --git a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx index fa9de895f7d0..028473f5c200 100644 --- a/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/user_privileges/index.tsx @@ -5,9 +5,8 @@ * 2.0. */ -import React, { createContext, useContext } from 'react'; +import React, { createContext, useContext, useEffect, useState } from 'react'; import { DeepReadonly } from 'utility-types'; -import { useGetUserAlertsPermissions } from '@kbn/alerts'; import { Capabilities } from '../../../../../../../src/core/public'; import { useFetchDetectionEnginePrivileges } from '../../../detections/components/user_privileges/use_fetch_detection_engine_privileges'; @@ -19,14 +18,14 @@ export interface UserPrivilegesState { listPrivileges: ReturnType; detectionEnginePrivileges: ReturnType; endpointPrivileges: EndpointPrivileges; - alertsPrivileges: ReturnType; + kibanaSecuritySolutionsPrivileges: { crud: boolean; read: boolean }; } export const initialUserPrivilegesState = (): UserPrivilegesState => ({ listPrivileges: { loading: false, error: undefined, result: undefined }, detectionEnginePrivileges: { loading: false, error: undefined, result: undefined }, endpointPrivileges: { loading: true, canAccessEndpointManagement: false, canAccessFleet: false }, - alertsPrivileges: { loading: false, read: false, crud: false }, + kibanaSecuritySolutionsPrivileges: { crud: false, read: false }, }); const UserPrivilegesContext = createContext(initialUserPrivilegesState()); @@ -43,14 +42,29 @@ export const UserPrivilegesProvider = ({ const listPrivileges = useFetchListPrivileges(); const detectionEnginePrivileges = useFetchDetectionEnginePrivileges(); const endpointPrivileges = useEndpointPrivileges(); - const alertsPrivileges = useGetUserAlertsPermissions(kibanaCapabilities, SERVER_APP_ID); + const [kibanaSecuritySolutionsPrivileges, setKibanaSecuritySolutionsPrivileges] = useState({ + crud: false, + read: false, + }); + const crud: boolean = kibanaCapabilities[SERVER_APP_ID].crud === true; + const read: boolean = kibanaCapabilities[SERVER_APP_ID].show === true; + + useEffect(() => { + setKibanaSecuritySolutionsPrivileges((currPrivileges) => { + if (currPrivileges.read !== read || currPrivileges.crud !== crud) { + return { read, crud }; + } + return currPrivileges; + }); + }, [crud, read]); + return ( {children} diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx index 9cc844a80b03..6bd902658c8e 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_count_panel/index.tsx @@ -14,8 +14,6 @@ import { HeaderSection } from '../../../../common/components/header_section'; import { useQueryAlerts } from '../../../containers/detection_engine/alerts/use_query'; import { InspectButtonContainer } from '../../../../common/components/inspect'; -import { fetchQueryRuleRegistryAlerts } from '../../../containers/detection_engine/alerts/api'; - import { getAlertsCountQuery } from './helpers'; import * as i18n from './translations'; import { AlertsCount } from './alerts_count'; @@ -49,7 +47,8 @@ export const AlertsCountPanel = memo( // ? fetchQueryRuleRegistryAlerts // : fetchQueryAlerts; - const fetchMethod = fetchQueryRuleRegistryAlerts; + // Disabling the fecth method in useQueryAlerts since it is defaulted to the old one + // const fetchMethod = fetchQueryRuleRegistryAlerts; const additionalFilters = useMemo(() => { try { @@ -73,7 +72,6 @@ export const AlertsCountPanel = memo( request, refetch, } = useQueryAlerts<{}, AlertsCountAggregation>({ - fetchMethod, query: getAlertsCountQuery(selectedStackByOption, from, to, additionalFilters), indexName: signalIndexName, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx index b296371bae58..2182ed7da0c4 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/alerts_histogram_panel/index.tsx @@ -43,7 +43,6 @@ import type { AlertsStackByField } from '../common/types'; import { KpiPanel, StackBySelect } from '../common/components'; import { useInspectButton } from '../common/hooks'; -import { fetchQueryRuleRegistryAlerts } from '../../../containers/detection_engine/alerts/api'; const defaultTotalAlertsObj: AlertsTotal = { value: 0, @@ -117,16 +116,12 @@ export const AlertsHistogramPanel = memo( request, refetch, } = useQueryAlerts<{}, AlertsAggregation>({ - fetchMethod: fetchQueryRuleRegistryAlerts, - query: { - index: signalIndexName, - ...getAlertsHistogramQuery( - selectedStackByOption, - from, - to, - buildCombinedQueries(combinedQueries) - ), - }, + query: getAlertsHistogramQuery( + selectedStackByOption, + from, + to, + buildCombinedQueries(combinedQueries) + ), indexName: signalIndexName, }); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx index 4b3c792319cd..e179c0298746 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/index.tsx @@ -381,7 +381,7 @@ export const AlertsTableComponent: React.FC = ({ pageFilters={defaultFiltersMemo} defaultCellActions={defaultCellActions} defaultModel={defaultTimelineModel} - entityType="alerts" + entityType="events" end={to} currentFilter={filterGroup} id={timelineId} @@ -392,7 +392,7 @@ export const AlertsTableComponent: React.FC = ({ start={from} utilityBar={utilityBarCallback} additionalFilters={additionalFiltersComponent} - hasAlertsCrud={hasIndexWrite} + hasAlertsCrud={hasIndexWrite && hasIndexMaintenance} /> ); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index eb31a59f0ca8..9568f9c894e2 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -13,14 +13,6 @@ import React from 'react'; import { Ecs } from '../../../../../common/ecs'; import { mockTimelines } from '../../../../common/mock/mock_timelines_plugin'; -jest.mock('@kbn/alerts', () => ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); - const ecsRowData: Ecs = { _id: '1', agent: { type: ['blah'] } }; const props = { diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx index 3568972aef2e..8da4ce1c3ed7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/use_alerts_actions.tsx @@ -7,15 +7,12 @@ import { useCallback } from 'react'; import { useDispatch } from 'react-redux'; -import { useGetUserAlertsPermissions } from '@kbn/alerts'; import { useStatusBulkActionItems } from '../../../../../../timelines/public'; import { Status } from '../../../../../common/detection_engine/schemas/common/schemas'; import { timelineActions } from '../../../../timelines/store/timeline'; +import { useAlertsPrivileges } from '../../../containers/detection_engine/alerts/use_alerts_privileges'; import { SetEventsDeletedProps, SetEventsLoadingProps } from '../types'; - -import { useKibana } from '../../../../common/lib/kibana'; -import { SERVER_APP_ID } from '../../../../../common/constants'; interface Props { alertStatus?: Status; closePopover: () => void; @@ -34,8 +31,7 @@ export const useAlertsActions = ({ refetch, }: Props) => { const dispatch = useDispatch(); - const uiCapabilities = useKibana().services.application.capabilities; - const alertsPrivileges = useGetUserAlertsPermissions(uiCapabilities, SERVER_APP_ID); + const { hasIndexWrite, hasKibanaCRUD } = useAlertsPrivileges(); const onStatusUpdate = useCallback(() => { closePopover(); @@ -66,9 +62,10 @@ export const useAlertsActions = ({ setEventsDeleted, onUpdateSuccess: onStatusUpdate, onUpdateFailure: onStatusUpdate, + timelineId, }); return { - actionItems: alertsPrivileges.crud ? actionItems : [], + actionItems: hasIndexWrite && hasKibanaCRUD ? actionItems : [], }; }; diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/translations.tsx b/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/translations.tsx index 3509ad73001e..0d628d89c092 100644 --- a/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/translations.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/translations.tsx @@ -9,10 +9,6 @@ import { EuiCode } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import React from 'react'; -import { - DetectionsRequirementsLink, - SecuritySolutionRequirementsLink, -} from '../../../../common/components/links_to_docs'; import { DEFAULT_ITEMS_INDEX, DEFAULT_LISTS_INDEX, @@ -21,6 +17,10 @@ import { } from '../../../../../common/constants'; import { CommaSeparatedValues } from './comma_separated_values'; import { MissingPrivileges } from './use_missing_privileges'; +import { + DetectionsRequirementsLink, + SecuritySolutionRequirementsLink, +} from '../../../../common/components/links_to_docs'; export const MISSING_PRIVILEGES_CALLOUT_TITLE = i18n.translate( 'xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageTitle', @@ -46,17 +46,17 @@ const CANNOT_EDIT_LISTS = i18n.translate( const CANNOT_EDIT_ALERTS = i18n.translate( 'xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.cannotEditAlerts', { - defaultMessage: 'Without these privileges, you cannot open or close alerts.', + defaultMessage: 'Without these privileges, you cannot view or change status of alerts.', } ); export const missingPrivilegesCallOutBody = ({ indexPrivileges, - featurePrivileges, + featurePrivileges = [], }: MissingPrivileges) => ( @@ -77,23 +77,30 @@ export const missingPrivilegesCallOutBody = ({ {indexPrivileges.map(([index, missingPrivileges]) => (
  • {missingIndexPrivileges(index, missingPrivileges)}
  • ))} - - - ) : null, - featurePrivileges: - featurePrivileges.length > 0 ? ( - <> - -
      - {featurePrivileges.map(([feature, missingPrivileges]) => ( + { + // TODO: Uncomment once RBAC for alerts is reenabled + /* {featurePrivileges.map(([feature, missingPrivileges]) => (
    • {missingFeaturePrivileges(feature, missingPrivileges)}
    • - ))} + ))} */ + }
    ) : null, + // TODO: Uncomment once RBAC for alerts is reenabled + // featurePrivileges: + // featurePrivileges.length > 0 ? ( + // <> + // + //
      + // {featurePrivileges.map(([feature, missingPrivileges]) => ( + //
    • {missingFeaturePrivileges(feature, missingPrivileges)}
    • + // ))} + //
    + // + // ) : null, docs: (
    • @@ -152,14 +159,15 @@ const missingIndexPrivileges = (index: string, privileges: string[]) => ( /> ); -const missingFeaturePrivileges = (feature: string, privileges: string[]) => ( - , - index: {feature}, - explanation: getPrivilegesExplanation(privileges, feature), - }} - /> -); +// TODO: Uncomment once RBAC for alerts is reenabled +// const missingFeaturePrivileges = (feature: string, privileges: string[]) => ( +// , +// index: {feature}, +// explanation: getPrivilegesExplanation(privileges, feature), +// }} +// /> +// ); diff --git a/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts b/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts index ea2b081239fd..eec9bd1f0905 100644 --- a/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts +++ b/x-pack/plugins/security_solution/public/detections/components/callouts/missing_privileges_callout/use_missing_privileges.ts @@ -40,14 +40,18 @@ export interface MissingPrivileges { } export const useMissingPrivileges = (): MissingPrivileges => { - const { listPrivileges } = useUserPrivileges(); + const { detectionEnginePrivileges, listPrivileges } = useUserPrivileges(); const [{ canUserCRUD }] = useUserData(); return useMemo(() => { const featurePrivileges: MissingFeaturePrivileges[] = []; const indexPrivileges: MissingIndexPrivileges[] = []; - if (canUserCRUD == null || listPrivileges.result == null) { + if ( + canUserCRUD == null || + listPrivileges.result == null || + detectionEnginePrivileges.result == null + ) { /** * Do not check privileges till we get all the data. That helps to reduce * subsequent layout shift while loading and skip unneeded re-renders. @@ -72,9 +76,16 @@ export const useMissingPrivileges = (): MissingPrivileges => { indexPrivileges.push(missingListsPrivileges); } + const missingDetectionPrivileges = getMissingIndexPrivileges( + detectionEnginePrivileges.result.index + ); + if (missingDetectionPrivileges) { + indexPrivileges.push(missingDetectionPrivileges); + } + return { featurePrivileges, indexPrivileges, }; - }, [canUserCRUD, listPrivileges]); + }, [canUserCRUD, listPrivileges, detectionEnginePrivileges]); }; diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx index 76c0017f6fa9..bba652bcdd03 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx @@ -24,14 +24,14 @@ jest.mock('../../../common/lib/kibana', () => ({ useKibana: jest.fn(), useGetUserCasesPermissions: jest.fn().mockReturnValue({ crud: true }), })); +jest.mock('../../containers/detection_engine/alerts/use_alerts_privileges', () => ({ + useAlertsPrivileges: jest.fn().mockReturnValue({ hasIndexWrite: true, hasKibanaCRUD: true }), +})); jest.mock('../../../cases/components/use_insert_timeline'); jest.mock('../../../common/hooks/use_experimental_features', () => ({ useIsExperimentalFeatureEnabled: jest.fn().mockReturnValue(true), })); -jest.mock('@kbn/alerts', () => { - return { useGetUserAlertsPermissions: jest.fn().mockReturnValue({ crud: true }) }; -}); jest.mock('../../../common/utils/endpoint_alert_check', () => { return { endpointAlertCheck: jest.fn().mockReturnValue(true) }; diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx index 9972233dce35..67863f05c7d8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.test.tsx @@ -43,6 +43,7 @@ describe('useUserInfo', () => { expect(result.all).toHaveLength(1); expect(result.current).toEqual({ canUserCRUD: null, + canUserREAD: null, hasEncryptionKey: null, hasIndexManage: null, hasIndexMaintenance: null, diff --git a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx index da6df631d951..9c81b51445f6 100644 --- a/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/user_info/index.tsx @@ -10,11 +10,11 @@ import React, { useEffect, useReducer, Dispatch, createContext, useContext } fro import { useAlertsPrivileges } from '../../containers/detection_engine/alerts/use_alerts_privileges'; import { useSignalIndex } from '../../containers/detection_engine/alerts/use_signal_index'; -import { useKibana } from '../../../common/lib/kibana'; import { useCreateTransforms } from '../../../transforms/containers/use_create_transforms'; export interface State { canUserCRUD: boolean | null; + canUserREAD: boolean | null; hasIndexManage: boolean | null; hasIndexMaintenance: boolean | null; hasIndexWrite: boolean | null; @@ -30,6 +30,7 @@ export interface State { export const initialState: State = { canUserCRUD: null, + canUserREAD: null, hasIndexManage: null, hasIndexMaintenance: null, hasIndexWrite: null, @@ -77,10 +78,6 @@ export type Action = type: 'updateHasEncryptionKey'; hasEncryptionKey: boolean | null; } - | { - type: 'updateCanUserCRUD'; - canUserCRUD: boolean | null; - } | { type: 'updateSignalIndexName'; signalIndexName: string | null; @@ -88,6 +85,14 @@ export type Action = | { type: 'updateSignalIndexMappingOutdated'; signalIndexMappingOutdated: boolean | null; + } + | { + type: 'updateCanUserCRUD'; + canUserCRUD: boolean | null; + } + | { + type: 'updateCanUserREAD'; + canUserREAD: boolean | null; }; export const userInfoReducer = (state: State, action: Action): State => { @@ -146,12 +151,6 @@ export const userInfoReducer = (state: State, action: Action): State => { hasEncryptionKey: action.hasEncryptionKey, }; } - case 'updateCanUserCRUD': { - return { - ...state, - canUserCRUD: action.canUserCRUD, - }; - } case 'updateSignalIndexName': { return { ...state, @@ -164,6 +163,18 @@ export const userInfoReducer = (state: State, action: Action): State => { signalIndexMappingOutdated: action.signalIndexMappingOutdated, }; } + case 'updateCanUserCRUD': { + return { + ...state, + canUserCRUD: action.canUserCRUD, + }; + } + case 'updateCanUserREAD': { + return { + ...state, + canUserREAD: action.canUserREAD, + }; + } default: return state; } @@ -187,6 +198,7 @@ export const useUserInfo = (): State => { const [ { canUserCRUD, + canUserREAD, hasIndexManage, hasIndexMaintenance, hasIndexWrite, @@ -210,6 +222,8 @@ export const useUserInfo = (): State => { hasIndexUpdateDelete: hasApiIndexUpdateDelete, hasIndexWrite: hasApiIndexWrite, hasIndexRead: hasApiIndexRead, + hasKibanaCRUD, + hasKibanaREAD, } = useAlertsPrivileges(); const { loading: indexNameLoading, @@ -221,8 +235,17 @@ export const useUserInfo = (): State => { const { createTransforms } = useCreateTransforms(); - const uiCapabilities = useKibana().services.application.capabilities; - const capabilitiesCanUserCRUD: boolean = uiCapabilities.siem.crud === true; + useEffect(() => { + if (!loading && canUserCRUD !== hasKibanaCRUD) { + dispatch({ type: 'updateCanUserCRUD', canUserCRUD: hasKibanaCRUD }); + } + }, [dispatch, loading, canUserCRUD, hasKibanaCRUD]); + + useEffect(() => { + if (!loading && canUserREAD !== hasKibanaREAD) { + dispatch({ type: 'updateCanUserREAD', canUserREAD: hasKibanaREAD }); + } + }, [dispatch, loading, canUserREAD, hasKibanaREAD]); useEffect(() => { if (loading !== (privilegeLoading || indexNameLoading)) { @@ -293,12 +316,6 @@ export const useUserInfo = (): State => { } }, [dispatch, loading, hasEncryptionKey, isApiEncryptionKey]); - useEffect(() => { - if (!loading && canUserCRUD !== capabilitiesCanUserCRUD) { - dispatch({ type: 'updateCanUserCRUD', canUserCRUD: capabilitiesCanUserCRUD }); - } - }, [dispatch, loading, canUserCRUD, capabilitiesCanUserCRUD]); - useEffect(() => { if (!loading && signalIndexName !== apiSignalIndexName && apiSignalIndexName != null) { dispatch({ type: 'updateSignalIndexName', signalIndexName: apiSignalIndexName }); @@ -351,6 +368,7 @@ export const useUserInfo = (): State => { isAuthenticated, hasEncryptionKey, canUserCRUD, + canUserREAD, hasIndexManage, hasIndexMaintenance, hasIndexWrite, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx index 64d9db80316a..cbab24835c1a 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.test.tsx @@ -87,10 +87,10 @@ const userPrivilegesInitial: ReturnType = { error: undefined, }, endpointPrivileges: { loading: true, canAccessEndpointManagement: false, canAccessFleet: false }, - alertsPrivileges: { loading: true, crud: false, read: false }, + kibanaSecuritySolutionsPrivileges: { crud: true, read: true }, }; -describe('usePrivilegeUser', () => { +describe('useAlertsPrivileges', () => { let appToastsMock: jest.Mocked>; beforeEach(() => { @@ -113,13 +113,15 @@ describe('usePrivilegeUser', () => { hasIndexMaintenance: null, hasIndexWrite: null, hasIndexUpdateDelete: null, + hasKibanaCRUD: false, + hasKibanaREAD: false, isAuthenticated: null, loading: false, }); }); }); - test('if there is an error when fetching user privilege, we should get back false for every properties', async () => { + test('if there is an error when fetching user privilege, we should get back false for all index related properties', async () => { const userPrivileges = produce(userPrivilegesInitial, (draft) => { draft.detectionEnginePrivileges.error = new Error('Something went wrong'); }); @@ -137,6 +139,8 @@ describe('usePrivilegeUser', () => { hasIndexRead: false, hasIndexWrite: false, hasIndexUpdateDelete: false, + hasKibanaCRUD: true, + hasKibanaREAD: true, isAuthenticated: false, loading: false, }); @@ -162,9 +166,11 @@ describe('usePrivilegeUser', () => { hasEncryptionKey: true, hasIndexManage: false, hasIndexMaintenance: true, - hasIndexRead: false, - hasIndexWrite: false, + hasIndexRead: true, + hasIndexWrite: true, hasIndexUpdateDelete: true, + hasKibanaCRUD: true, + hasKibanaREAD: true, isAuthenticated: true, loading: false, }); @@ -187,9 +193,67 @@ describe('usePrivilegeUser', () => { hasEncryptionKey: true, hasIndexManage: true, hasIndexMaintenance: true, - hasIndexRead: false, - hasIndexWrite: false, + hasIndexRead: true, + hasIndexWrite: true, + hasIndexUpdateDelete: true, + hasKibanaCRUD: true, + hasKibanaREAD: true, + isAuthenticated: true, + loading: false, + }); + }); + }); + + test('returns "hasKibanaCRUD" as false if user does not have SIEM Kibana "all" privileges', async () => { + const userPrivileges = produce(userPrivilegesInitial, (draft) => { + draft.detectionEnginePrivileges.result = privilege; + draft.kibanaSecuritySolutionsPrivileges = { crud: false, read: true }; + }); + useUserPrivilegesMock.mockReturnValue(userPrivileges); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useAlertsPrivileges() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + hasEncryptionKey: true, + hasIndexManage: true, + hasIndexMaintenance: true, + hasIndexRead: true, + hasIndexWrite: true, + hasIndexUpdateDelete: true, + hasKibanaCRUD: false, + hasKibanaREAD: true, + isAuthenticated: true, + loading: false, + }); + }); + }); + + test('returns "hasKibanaREAD" as false if user does not have at least SIEM Kibana "read" privileges', async () => { + const userPrivileges = produce(userPrivilegesInitial, (draft) => { + draft.detectionEnginePrivileges.result = privilege; + draft.kibanaSecuritySolutionsPrivileges = { crud: false, read: false }; + }); + useUserPrivilegesMock.mockReturnValue(userPrivileges); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useAlertsPrivileges() + ); + await waitForNextUpdate(); + await waitForNextUpdate(); + expect(result.current).toEqual({ + hasEncryptionKey: true, + hasIndexManage: true, + hasIndexMaintenance: true, + hasIndexRead: true, + hasIndexWrite: true, hasIndexUpdateDelete: true, + hasKibanaCRUD: false, + hasKibanaREAD: false, isAuthenticated: true, loading: false, }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx index 1d9b8228b507..b377eda49d0c 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_alerts_privileges.tsx @@ -20,6 +20,8 @@ export interface AlertsPrivelegesState { hasIndexUpdateDelete: boolean | null; hasIndexMaintenance: boolean | null; hasIndexRead: boolean | null; + hasKibanaCRUD: boolean; + hasKibanaREAD: boolean; } /** * Hook to get user privilege from @@ -34,8 +36,13 @@ export const useAlertsPrivileges = (): UseAlertsPrivelegesReturn => { hasIndexWrite: null, hasIndexUpdateDelete: null, hasIndexMaintenance: null, + hasKibanaCRUD: false, + hasKibanaREAD: false, }); - const { detectionEnginePrivileges, alertsPrivileges } = useUserPrivileges(); + const { + detectionEnginePrivileges, + kibanaSecuritySolutionsPrivileges: { crud: hasKibanaCRUD, read: hasKibanaREAD }, + } = useUserPrivileges(); useEffect(() => { if (detectionEnginePrivileges.error != null) { @@ -47,9 +54,11 @@ export const useAlertsPrivileges = (): UseAlertsPrivelegesReturn => { hasIndexWrite: false, hasIndexUpdateDelete: false, hasIndexMaintenance: false, + hasKibanaCRUD, + hasKibanaREAD, }); } - }, [detectionEnginePrivileges.error]); + }, [detectionEnginePrivileges.error, hasKibanaCRUD, hasKibanaREAD]); useEffect(() => { if (detectionEnginePrivileges.result != null) { @@ -62,13 +71,19 @@ export const useAlertsPrivileges = (): UseAlertsPrivelegesReturn => { hasEncryptionKey: privilege.has_encryption_key, hasIndexManage: privilege.index[indexName].manage && privilege.cluster.manage, hasIndexMaintenance: privilege.index[indexName].maintenance, - hasIndexRead: alertsPrivileges.read, - hasIndexWrite: alertsPrivileges.crud, + hasIndexRead: privilege.index[indexName].read, + hasIndexWrite: + privilege.index[indexName].create || + privilege.index[indexName].create_doc || + privilege.index[indexName].index || + privilege.index[indexName].write, hasIndexUpdateDelete: privilege.index[indexName].write, + hasKibanaCRUD, + hasKibanaREAD, }); } } - }, [detectionEnginePrivileges.result, alertsPrivileges]); + }, [detectionEnginePrivileges.result, hasKibanaCRUD, hasKibanaREAD]); return { loading: detectionEnginePrivileges.loading, ...privileges }; }; diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx index 6d68dae37586..ade83fed4fd6 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/alerts/use_signal_index.test.tsx @@ -14,13 +14,6 @@ import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; jest.mock('./api'); jest.mock('../../../../common/hooks/use_app_toasts'); jest.mock('../../../../common/components/user_privileges/use_endpoint_privileges'); -jest.mock('@kbn/alerts', () => ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); describe('useSignalIndex', () => { let appToastsMock: jest.Mocked>; diff --git a/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx index dbd59d251023..18952feee528 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/alerts/index.tsx @@ -7,15 +7,15 @@ import React, { useEffect } from 'react'; import { Route, Switch } from 'react-router-dom'; -import { useGetUserAlertsPermissions } from '@kbn/alerts'; -import { ALERTS_PATH, SecurityPageName, SERVER_APP_ID } from '../../../../common/constants'; +import { ALERTS_PATH, SecurityPageName } from '../../../../common/constants'; import { NotFoundPage } from '../../../app/404'; import * as i18n from './translations'; import { TrackApplicationView } from '../../../../../../../src/plugins/usage_collection/public'; import { DetectionEnginePage } from '../../pages/detection_engine/detection_engine'; import { useKibana } from '../../../common/lib/kibana'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; +import { useAlertsPrivileges } from '../../containers/detection_engine/alerts/use_alerts_privileges'; const AlertsRoute = () => ( @@ -25,15 +25,12 @@ const AlertsRoute = () => ( ); const AlertsContainerComponent: React.FC = () => { - const { - chrome, - application: { capabilities }, - } = useKibana().services; - const userPermissions = useGetUserAlertsPermissions(capabilities, SERVER_APP_ID); + const { chrome } = useKibana().services; + const { hasIndexRead, hasIndexWrite } = useAlertsPrivileges(); useEffect(() => { // if the user is read only then display the glasses badge in the global navigation header - if (userPermissions != null && !userPermissions.crud && userPermissions.read) { + if (!hasIndexWrite && hasIndexRead) { chrome.setBadge({ text: i18n.READ_ONLY_BADGE_TEXT, tooltip: i18n.READ_ONLY_BADGE_TOOLTIP, @@ -45,7 +42,7 @@ const AlertsContainerComponent: React.FC = () => { return () => { chrome.setBadge(); }; - }, [userPermissions, chrome]); + }, [chrome, hasIndexRead, hasIndexWrite]); return ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx index a92f4d706dc7..0d0c51bc540b 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.test.tsx @@ -80,7 +80,7 @@ jest.mock('../../../common/lib/kibana', () => { docLinks: { links: { siem: { - gettingStarted: 'link', + privileges: 'link', }, }, }, @@ -107,6 +107,7 @@ describe('DetectionEnginePageComponent', () => { (useUserData as jest.Mock).mockReturnValue([ { hasIndexRead: true, + canUserREAD: true, }, ]); (useSourcererScope as jest.Mock).mockReturnValue({ diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx index d6531198c188..71542e693148 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/detection_engine.tsx @@ -5,6 +5,10 @@ * 2.0. */ +// No bueno, I know! Encountered when reverting RBAC work post initial BCs +// Don't want to include large amounts of refactor in this temporary workaround +// TODO: Refactor code - component can be broken apart +/* eslint-disable complexity */ import { EuiFlexGroup, EuiFlexItem, @@ -17,7 +21,6 @@ import { noop } from 'lodash/fp'; import React, { useCallback, useMemo, useRef, useState } from 'react'; import { connect, ConnectedProps, useDispatch } from 'react-redux'; import { Dispatch } from 'redux'; -import { AlertsFeatureNoPermissions } from '@kbn/alerts'; import { Status } from '../../../../common/detection_engine/schemas/common/schemas'; import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; @@ -73,6 +76,7 @@ import { AlertsTableFilterGroup, FILTER_OPEN, } from '../../components/alerts_table/alerts_filter_group'; +import { EmptyPage } from '../../../common/components/empty_page'; /** * Need a 100% height here to account for the graph/analyze tool, which sets no explicit height parameters, but fills the available space. @@ -117,7 +121,10 @@ const DetectionEnginePageComponent: React.FC = ({ isAuthenticated: isUserAuthenticated, hasEncryptionKey, signalIndexName, - hasIndexWrite, + hasIndexWrite = false, + hasIndexMaintenance = false, + canUserCRUD = false, + canUserREAD, hasIndexRead, }, ] = useUserData(); @@ -249,6 +256,18 @@ const DetectionEnginePageComponent: React.FC = ({ [containerElement, onSkipFocusBeforeEventsTable, onSkipFocusAfterEventsTable] ); + const emptyPageActions = useMemo( + () => ({ + feature: { + icon: 'documents', + label: i18n.GO_TO_DOCUMENTATION, + url: `${docLinks.links.siem.privileges}`, + target: '_blank', + }, + }), + [docLinks] + ); + if (isUserAuthenticated != null && !isUserAuthenticated && !loading) { return ( @@ -275,92 +294,89 @@ const DetectionEnginePageComponent: React.FC = ({ {hasEncryptionKey != null && !hasEncryptionKey && } - {indicesExist ? ( + {indicesExist && (hasIndexRead === false || canUserREAD === false) ? ( + + ) : indicesExist && hasIndexRead && canUserREAD ? ( - {hasIndexRead ? ( - <> - - - - - - - - {i18n.BUTTON_MANAGE_RULES} - - - - - - - - - {timelinesUi.getLastUpdated({ - updatedAt: updatedAt || 0, - showUpdating: loading, - })} - - - - - - - + + + + + + + + {i18n.BUTTON_MANAGE_RULES} + + + + + + + + + {timelinesUi.getLastUpdated({ + updatedAt: updatedAt || 0, + showUpdating: loading, + })} + + + + + + + - - - - + + + + - - + + - - - - ) : ( - - )} + ) : ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx index c1d674ce456f..0c67a19e59e3 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.test.tsx @@ -45,13 +45,6 @@ jest.mock('../../../../../common/containers/use_global_time', () => ({ setQuery: jest.fn(), }), })); -jest.mock('@kbn/alerts', () => ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); jest.mock('react-router-dom', () => { const originalModule = jest.requireActual('react-router-dom'); diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts index 96e423aff165..fedf11902530 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/translations.ts @@ -143,3 +143,18 @@ export const ML_RULES_UNAVAILABLE = (totalRules: number) => defaultMessage: '{totalRules} {totalRules, plural, =1 {rule requires} other {rules require}} Machine Learning to enable.', }); + +export const FEATURE_NO_PERMISSIONS_TITLE = i18n.translate( + 'xpack.securitySolution.detectionEngine.noPermissionsTitle', + { + defaultMessage: 'Privileges required', + } +); + +export const ALERTS_FEATURE_NO_PERMISSIONS_MSG = i18n.translate( + 'xpack.securitySolution.detectionEngine.noPermissionsMessage', + { + defaultMessage: + 'To view alerts, you must update privileges. For more information, contact your Kibana administrator.', + } +); diff --git a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx index 22192a8a349d..581df61efc3e 100644 --- a/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx +++ b/x-pack/plugins/security_solution/public/management/components/administration_list_page.tsx @@ -9,6 +9,8 @@ import React, { FC, memo, useMemo } from 'react'; import { CommonProps, EuiPageHeader, + EuiPageContent, + EuiPageContentBody, EuiFlexGroup, EuiFlexItem, EuiTitle, @@ -16,16 +18,26 @@ import { } from '@elastic/eui'; import { SecurityPageName } from '../../../common/constants'; import { SpyRoute } from '../../common/utils/route/spy_routes'; +import { useTestIdGenerator } from './hooks/use_test_id_generator'; interface AdministrationListPageProps { title: React.ReactNode; - subtitle: React.ReactNode; + subtitle?: React.ReactNode; actions?: React.ReactNode; + restrictWidth?: boolean | number; headerBackComponent?: React.ReactNode; } export const AdministrationListPage: FC = memo( - ({ title, subtitle, actions, children, headerBackComponent, ...otherProps }) => { + ({ + title, + subtitle, + actions, + children, + restrictWidth = false, + headerBackComponent, + ...otherProps + }) => { const header = useMemo(() => { return ( @@ -42,24 +54,34 @@ export const AdministrationListPage: FC { - return {subtitle}; + return subtitle ? {subtitle} : undefined; }, [subtitle]); + const getTestId = useTestIdGenerator(otherProps['data-test-subj']); + return ( - <> +
      - {children} + + {children} + - +
      ); } ); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index 17d836695bcf..e51fe15e7130 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -286,22 +286,15 @@ describe('endpoint list middleware', () => { it('should set ActivityLog state to loading', async () => { dispatchUserChangedUrl(); - dispatchGetActivityLogLoading(); const loadingDispatched = waitForAction('endpointDetailsActivityLogChanged', { validate(action) { return isLoadingResourceState(action.payload); }, }); + dispatchGetActivityLogLoading(); const loadingDispatchedResponse = await loadingDispatched; - expect(mockedApis.responseProvider.activityLogResponse).toHaveBeenCalledWith({ - path: expect.any(String), - query: { - page: 1, - page_size: 50, - }, - }); expect(loadingDispatchedResponse.payload.type).toEqual('LoadingResourceState'); }); @@ -343,6 +336,25 @@ describe('endpoint list middleware', () => { expect(failedAction.error).toBe(apiError); }); + it('should not call API again if it fails', async () => { + dispatchUserChangedUrl(); + + const apiError = new Error('oh oh'); + const failedDispatched = waitForAction('endpointDetailsActivityLogChanged', { + validate(action) { + return isFailedResourceState(action.payload); + }, + }); + + mockedApis.responseProvider.activityLogResponse.mockImplementation(() => { + throw apiError; + }); + + await failedDispatched; + + expect(mockedApis.responseProvider.activityLogResponse).toHaveBeenCalledTimes(1); + }); + it('should not fetch Activity Log with invalid date ranges', async () => { dispatchUserChangedUrl(); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 1be9ff5be55e..df4361a6048a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -33,11 +33,13 @@ import { getActivityLogData, getActivityLogDataPaging, getLastLoadedActivityLogData, + getActivityLogError, detailsData, getIsEndpointPackageInfoUninitialized, getIsOnEndpointDetailsActivityLog, getMetadataTransformStats, isMetadataTransformStatsLoading, + getActivityLogIsUninitializedOrHasSubsequentAPIError, } from './selectors'; import { AgentIdsPendingActions, @@ -124,7 +126,8 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory { - const pagingOptions = + const updatedActivityLog = action.payload.type === 'LoadedResourceState' ? { ...state.endpointDetails.activityLog, @@ -53,7 +53,7 @@ const handleEndpointDetailsActivityLogChanged: CaseReducer +) => boolean = createSelector(getActivityLogData, (activityLog) => + isUninitialisedResourceState(activityLog) +); + export const getActivityLogRequestLoading: ( state: Immutable ) => boolean = createSelector(getActivityLogData, (activityLog) => @@ -413,6 +419,19 @@ export const getActivityLogError: ( } }); +// returns a true if either lgo is uninitialised +// or if it has failed an api call after having fetched a non empty log list earlier +export const getActivityLogIsUninitializedOrHasSubsequentAPIError: ( + state: Immutable +) => boolean = createSelector( + getActivityLogUninitialized, + getLastLoadedActivityLogData, + getActivityLogError, + (isUninitialized, lastLoadedLogData, isAPIError) => { + return isUninitialized || (!isAPIError && !!lastLoadedLogData?.data.length); + } +); + export const getIsEndpointHostIsolated = createSelector(detailsData, (details) => { return (details && isEndpointHostIsolated(details)) || false; }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx index a6e571dbd7df..8f044959f4c9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/components/endpoint_details_tabs.tsx @@ -5,13 +5,10 @@ * 2.0. */ -import { useDispatch } from 'react-redux'; -import React, { memo, useCallback, useMemo } from 'react'; -import { EuiTab, EuiTabs, EuiFlyoutBody, EuiTabbedContentTab, EuiSpacer } from '@elastic/eui'; +import React, { memo, useMemo } from 'react'; +import { EuiTab, EuiTabs, EuiFlyoutBody, EuiSpacer } from '@elastic/eui'; import { EndpointIndexUIQueryParams } from '../../../types'; -import { EndpointAction } from '../../../store/action'; -import { useEndpointSelector } from '../../hooks'; -import { getActivityLogDataPaging } from '../../../store/selectors'; + import { EndpointDetailsFlyoutHeader } from './flyout_header'; import { useNavigateByRouterEventHandler } from '../../../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; import { useAppUrl } from '../../../../../../common/lib/kibana'; @@ -33,17 +30,9 @@ interface EndpointDetailsTabs { } const EndpointDetailsTab = memo( - ({ - tab, - isSelected, - handleTabClick, - }: { - tab: EndpointDetailsTabs; - isSelected: boolean; - handleTabClick: () => void; - }) => { + ({ tab, isSelected }: { tab: EndpointDetailsTabs; isSelected: boolean }) => { const { getAppUrl } = useAppUrl(); - const onClick = useNavigateByRouterEventHandler(tab.route, handleTabClick); + const onClick = useNavigateByRouterEventHandler(tab.route); return ( { - const dispatch = useDispatch<(action: EndpointAction) => void>(); - const { pageSize } = useEndpointSelector(getActivityLogDataPaging); - - const handleTabClick = useCallback( - (tab: EuiTabbedContentTab) => { - if (tab.id === EndpointDetailsTabsTypes.activityLog) { - dispatch({ - type: 'endpointDetailsActivityLogUpdatePaging', - payload: { - disabled: false, - page: 1, - pageSize, - startDate: undefined, - endDate: undefined, - }, - }); - } - }, - [dispatch, pageSize] - ); - const selectedTab = useMemo(() => tabs.find((tab) => tab.id === show), [tabs, show]); const renderTabs = tabs.map((tab) => ( - handleTabClick(tab)} - isSelected={tab.id === selectedTab?.id} - /> + )); return ( diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx index 121f23fdb3a9..5172b59450e0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/details/endpoint_activity_log.tsx @@ -30,6 +30,7 @@ import { getActivityLogRequestLoaded, getLastLoadedActivityLogData, getActivityLogRequestLoading, + getActivityLogUninitialized, } from '../../store/selectors'; const StyledEuiFlexGroup = styled(EuiFlexGroup)<{ isShorter: boolean }>` @@ -42,6 +43,7 @@ const LoadMoreTrigger = styled.div` export const EndpointActivityLog = memo( ({ activityLog }: { activityLog: AsyncResourceState> }) => { + const activityLogUninitialized = useEndpointSelector(getActivityLogUninitialized); const activityLogLoading = useEndpointSelector(getActivityLogRequestLoading); const activityLogLoaded = useEndpointSelector(getActivityLogRequestLoaded); const activityLastLogData = useEndpointSelector(getLastLoadedActivityLogData); @@ -96,7 +98,9 @@ export const EndpointActivityLog = memo( return ( <> - {showEmptyState ? ( + {(activityLogLoading && !activityLastLogData?.data.length) || activityLogUninitialized ? ( + + ) : showEmptyState ? ( ((props) => { title: i18n.translate( 'xpack.securitySolution.endpoint.policyDetails.agentsSummary.totalTitle', { - defaultMessage: 'Agents', + defaultMessage: 'Total agents', } ), health: '', @@ -73,20 +74,33 @@ export const AgentsSummary = memo((props) => { ]; }, []); + const theme = useContext(ThemeContext); + return ( - + {stats.map(({ key, title, health }) => { return ( - + - {health && } + {health && ( + + )} ), diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx index d0a246291fbe..f589ec628bef 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/config_form/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC, ReactNode, memo } from 'react'; +import React, { FC, ReactNode, memo, useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { @@ -20,6 +20,7 @@ import { EuiIconTip, } from '@elastic/eui'; +import { ThemeContext } from 'styled-components'; import { OperatingSystem } from '../../../../../../../common/endpoint/types'; import { OS_TITLES } from '../../../../../common/translations'; @@ -28,7 +29,7 @@ const TITLES = { defaultMessage: 'Type', }), os: i18n.translate('xpack.securitySolution.endpoint.policyDetailOS', { - defaultMessage: 'Operating System', + defaultMessage: 'Operating system', }), }; @@ -56,55 +57,63 @@ export const ConfigFormHeading: FC = memo(({ children }) => ( ConfigFormHeading.displayName = 'ConfigFormHeading'; export const ConfigForm: FC = memo( - ({ type, supportedOss, osRestriction, dataTestSubj, rightCorner, children }) => ( - - - - {TITLES.type} - {type} - - - {TITLES.os} - - - {supportedOss.map((os) => OS_TITLES[os]).join(', ')} - - {osRestriction && ( + ({ type, supportedOss, osRestriction, dataTestSubj, rightCorner, children }) => { + const paddingSize = useContext(ThemeContext).eui.euiPanelPaddingModifiers.paddingMedium; + return ( + + + + {TITLES.type} + {type} + + + {TITLES.os} + - - - - - - - - - - + {supportedOss.map((os) => OS_TITLES[os]).join(', ')} - )} - - - - - - {rightCorner} + {osRestriction && ( + + + + + + + + + + + + + )} - - - {rightCorner} - - + + + + {rightCorner} + + + + + {rightCorner} + + - + - {children} - - ) +
      {children}
      + + ); + } ); ConfigForm.displayName = 'ConfigForm'; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx index 1f4d6a6571ae..9bdc5aa49393 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/components/events_form/index.tsx @@ -54,7 +54,7 @@ export const EventsForm = ({ }: EventsFormProps) => ( { expect(backToListLink.prop('href')).toBe(`/app/security${endpointListPath}`); expect(backToListLink.text()).toBe('Back to endpoint hosts'); - const pageTitle = policyView.find('h1[data-test-subj="header-page-title"]'); + const pageTitle = policyView.find('span[data-test-subj="header-page-title"]'); expect(pageTitle).toHaveLength(1); expect(pageTitle.text()).toEqual(policyPackagePolicy.name); }); @@ -150,7 +150,7 @@ describe('Policy Details', () => { const agentsSummary = policyView.find('EuiFlexGroup[data-test-subj="policyAgentsSummary"]'); expect(agentsSummary).toHaveLength(1); - expect(agentsSummary.text()).toBe('Agents5Healthy3Unhealthy1Offline1'); + expect(agentsSummary.text()).toBe('Total agents5Healthy3Unhealthy1Offline1'); }); it('should display cancel button', async () => { await asyncActions; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx index 8c587858a1f2..c8b34f97ee6d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.tsx @@ -5,8 +5,7 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo, useState, useContext } from 'react'; -import styled, { ThemeContext } from 'styled-components'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiFlexGroup, EuiFlexItem, @@ -42,26 +41,9 @@ import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/ import { APP_ID } from '../../../../../common/constants'; import { PolicyDetailsRouteState } from '../../../../../common/endpoint/types'; import { SecuritySolutionPageWrapper } from '../../../../common/components/page_wrapper'; -import { HeaderPage } from '../../../../common/components/header_page'; +import { HeaderLinkBack } from '../../../../common/components/header_page'; import { PolicyDetailsForm } from './policy_details_form'; - -const maxFormWidth = '770px'; -const PolicyDetailsHeader = styled.div` - padding: ${(props) => props.theme.eui.paddingSizes.xl} 0; - border-bottom: 1px solid #d3dae6; - .securitySolutionHeaderPage { - max-width: ${maxFormWidth}; - margin: 0 auto; - } -`; - -const PolicyDetailsFormDiv = styled.div` - background-color: ${(props) => props.theme.eui.euiHeaderBackgroundColor}; - padding: ${(props) => props.theme.eui.paddingSizes.l} 0; - max-width: ${maxFormWidth}; - flex: 1; - align-self: center; -`; +import { AdministrationListPage } from '../../../components/administration_list_page'; export const PolicyDetails = React.memo(() => { const dispatch = useDispatch<(action: AppAction) => void>(); @@ -84,8 +66,8 @@ export const PolicyDetails = React.memo(() => { const [showConfirm, setShowConfirm] = useState(false); const [routeState, setRouteState] = useState(); const policyName = policyItem?.name ?? ''; + const policyDescription = policyItem?.description ?? undefined; const hostListRouterPath = getEndpointListPath({ name: 'endpointList' }); - const theme = useContext(ThemeContext); // Handle showing update statuses useEffect(() => { @@ -178,6 +160,18 @@ export const PolicyDetails = React.memo(() => { /> ); + const backToEndpointList = ( + + ); + return ( <> {showConfirm && ( @@ -187,69 +181,47 @@ export const PolicyDetails = React.memo(() => { onConfirm={handleSaveConfirmation} /> )} - - - - {headerRightContent} - - - - - - + - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx index e21e7117762e..c9d1b3b7882a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details_form.tsx @@ -58,17 +58,17 @@ export const PolicyDetailsForm = memo(() => { - + - + {isPlatinumPlus ? : } - + {isPlatinumPlus ? ( ) : ( )} - + {isPlatinumPlus ? ( ) : ( @@ -85,13 +85,13 @@ export const PolicyDetailsForm = memo(() => { - + - + - + - + diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/radio_buttons.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/radio_buttons.tsx index 793c24a0c4d0..acdbb7e97909 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/radio_buttons.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/radio_buttons.tsx @@ -64,12 +64,12 @@ export const RadioButtons = React.memo( - + @@ -112,15 +112,15 @@ export const UserNotification = React.memo( checked={userNotificationSelected} disabled={selected === ProtectionModes.off} label={i18n.translate('xpack.securitySolution.endpoint.policyDetail.notifyUser', { - defaultMessage: 'Notify User', + defaultMessage: 'Notify user', })} /> {userNotificationSelected && ( <> - - + + - +

      { const protectionLabel = i18n.translate( 'xpack.securitySolution.endpoint.policy.protections.behavior', { - defaultMessage: 'Behavior protections', + defaultMessage: 'Behavior', } ); return ( { const protectionLabel = i18n.translate( 'xpack.securitySolution.endpoint.policy.protections.memory', { - defaultMessage: 'Memory Manipulation Protection', + defaultMessage: 'Memory manipulation', } ); return ( { }, [addMessage]); const { endpointPrivileges: { canAccessFleet }, - alertsPrivileges, } = useUserPrivileges(); + const { hasIndexRead, hasKibanaREAD } = useAlertsPrivileges(); const isThreatIntelModuleEnabled = useIsThreatIntelModuleEnabled(); return ( <> @@ -98,7 +99,7 @@ const OverviewComponent = () => { - {alertsPrivileges?.read && ( + {hasIndexRead && hasKibanaREAD && ( <> diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 4a951dfff45d..93fa70ddd9bf 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -33,7 +33,6 @@ import { import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { initTelemetry } from './common/lib/telemetry'; import { KibanaServices } from './common/lib/kibana/services'; -import { BASE_RAC_ALERTS_API_PATH } from '../../rule_registry/common/constants'; import { APP_ID, @@ -42,7 +41,7 @@ import { APP_PATH, DEFAULT_INDEX_KEY, APP_ICON_SOLUTION, - SERVER_APP_ID, + DETECTION_ENGINE_INDEX_URL, } from '../common/constants'; import { getDeepLinks, updateGlobalNavigation } from './app/deep_links'; @@ -354,14 +353,17 @@ export class Plugin implements IPlugin ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); - describe('Details Panel Component', () => { const state: State = { ...mockGlobalState }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index cad6648cd1f3..5ed9398a621e 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -29,14 +29,6 @@ jest.mock( }) ); -jest.mock('@kbn/alerts', () => ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); - jest.mock('../../../../../common/lib/kibana', () => ({ useKibana: () => ({ services: { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx index d20c62348f07..404127893b11 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/events/event_column_view.test.tsx @@ -24,13 +24,6 @@ import { mockTimelines } from '../../../../../common/mock/mock_timelines_plugin' jest.mock('../../../../../common/hooks/use_experimental_features'); const useIsExperimentalFeatureEnabledMock = useIsExperimentalFeatureEnabled as jest.Mock; -jest.mock('@kbn/alerts', () => ({ - useGetUserAlertsPermissions: () => ({ - loading: false, - crud: true, - read: true, - }), -})); jest.mock('../../../../../common/hooks/use_selector'); jest.mock('../../../../../common/lib/kibana', () => ({ useKibana: () => ({ diff --git a/x-pack/plugins/security_solution/server/features.ts b/x-pack/plugins/security_solution/server/features.ts index 5494270d9ad8..cff1e2482a1e 100644 --- a/x-pack/plugins/security_solution/server/features.ts +++ b/x-pack/plugins/security_solution/server/features.ts @@ -114,7 +114,7 @@ export const getKibanaPrivilegesFeaturePrivileges = (ruleTypes: string[]): Kiban }, alerting: ruleTypes, cases: [APP_ID], - subFeatures: [{ ...CASES_SUB_FEATURE }, { ...getAlertsSubFeature(ruleTypes) }], + subFeatures: [{ ...CASES_SUB_FEATURE } /* , { ...getAlertsSubFeature(ruleTypes) } */], privileges: { all: { app: [APP_ID, 'kibana'], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap index 833a9084fdac..3c065ab0ac10 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/__snapshots__/get_signals_template.test.ts.snap @@ -6,11 +6,6 @@ Object { "test-index-*", ], "template": Object { - "aliases": Object { - ".alerts-security.alerts-space-id": Object { - "is_write_index": false, - }, - }, "mappings": Object { "_meta": Object { "aliases_version": 1, @@ -1810,10 +1805,6 @@ Object { "path": "signal.rule.building_block_type", "type": "alias", }, - "kibana.alert.rule.consumer": Object { - "type": "constant_keyword", - "value": "siem", - }, "kibana.alert.rule.created_at": Object { "path": "signal.rule.created_at", "type": "alias", @@ -1870,10 +1861,6 @@ Object { "path": "signal.rule.note", "type": "alias", }, - "kibana.alert.rule.producer": Object { - "type": "constant_keyword", - "value": "siem", - }, "kibana.alert.rule.query": Object { "path": "signal.rule.query", "type": "alias", @@ -1902,10 +1889,6 @@ Object { "path": "signal.rule.rule_name_override", "type": "alias", }, - "kibana.alert.rule.rule_type_id": Object { - "type": "constant_keyword", - "value": "siem.signals", - }, "kibana.alert.rule.saved_id": Object { "path": "signal.rule.saved_id", "type": "alias", @@ -2070,10 +2053,6 @@ Object { "path": "signal.status", "type": "alias", }, - "kibana.space_ids": Object { - "type": "constant_keyword", - "value": "space-id", - }, "labels": Object { "type": "object", }, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts index ab7ff26d9d87..d65a1ad87b41 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts @@ -25,7 +25,6 @@ import { buildSiemResponse } from '../utils'; import { createSignalsFieldAliases, getSignalsTemplate, - getRbacRequiredFields, SIGNALS_TEMPLATE_VERSION, SIGNALS_FIELD_ALIASES_VERSION, ALIAS_VERSION_FIELD, @@ -89,7 +88,7 @@ export const createDetectionIndex = async ( ruleDataService: RuleDataPluginService, ruleRegistryEnabled: boolean ): Promise => { - const esClient = context.core.elasticsearch.client.asInternalUser; + const esClient = context.core.elasticsearch.client.asCurrentUser; const spaceId = siemClient.getSpaceId(); if (!siemClient) { @@ -132,11 +131,11 @@ export const createDetectionIndex = async ( // for BOTH the index AND alias name. However, through 7.14 admins only needed permissions for .siem-signals (the index) // and not .alerts-security.alerts (the alias). From the security solution perspective, all .siem-signals--* // indices should have an alias to .alerts-security.alerts- so it's safe to add those aliases as the internal user. - await addIndexAliases({ - esClient: context.core.elasticsearch.client.asInternalUser, - index, - aadIndexAliasName, - }); + // await addIndexAliases({ + // esClient: context.core.elasticsearch.client.asInternalUser, + // index, + // aadIndexAliasName, + // }); const indexVersion = await getIndexVersion(esClient, index); if (isOutdated({ current: indexVersion, target: SIGNALS_TEMPLATE_VERSION })) { await esClient.indices.rollover({ alias: index }); @@ -166,7 +165,7 @@ const addFieldAliasesToIndices = async ({ properties: { ...signalExtraFields, ...fieldAliases, - ...getRbacRequiredFields(spaceId), + // ...getRbacRequiredFields(spaceId), }, _meta: { version: currentVersion, @@ -181,26 +180,26 @@ const addFieldAliasesToIndices = async ({ } }; -const addIndexAliases = async ({ - esClient, - index, - aadIndexAliasName, -}: { - esClient: ElasticsearchClient; - index: string; - aadIndexAliasName: string; -}) => { - const { body: indices } = await esClient.indices.getAlias({ name: index }); - const aliasActions = { - actions: Object.keys(indices).map((concreteIndexName) => { - return { - add: { - index: concreteIndexName, - alias: aadIndexAliasName, - is_write_index: false, - }, - }; - }), - }; - await esClient.indices.updateAliases({ body: aliasActions }); -}; +// const addIndexAliases = async ({ +// esClient, +// index, +// aadIndexAliasName, +// }: { +// esClient: ElasticsearchClient; +// index: string; +// aadIndexAliasName: string; +// }) => { +// const { body: indices } = await esClient.indices.getAlias({ name: index }); +// const aliasActions = { +// actions: Object.keys(indices).map((concreteIndexName) => { +// return { +// add: { +// index: concreteIndexName, +// alias: aadIndexAliasName, +// is_write_index: false, +// }, +// }; +// }), +// }; +// await esClient.indices.updateAliases({ body: aliasActions }); +// }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts index 3355b0659f28..bb67dd1fca6d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.test.ts @@ -107,12 +107,13 @@ describe('get_signals_template', () => { } }, []); const constantKeywordsFound = recursiveConstantKeywordFound('', template); - expect(constantKeywordsFound).toEqual([ - 'template.mappings.properties.kibana.space_ids', - 'template.mappings.properties.kibana.alert.rule.consumer', - 'template.mappings.properties.kibana.alert.rule.producer', - 'template.mappings.properties.kibana.alert.rule.rule_type_id', - ]); + expect(constantKeywordsFound).toEqual([]); + // expect(constantKeywordsFound).toEqual([ + // 'template.mappings.properties.kibana.space_ids', + // 'template.mappings.properties.kibana.alert.rule.consumer', + // 'template.mappings.properties.kibana.alert.rule.producer', + // 'template.mappings.properties.kibana.alert.rule.rule_type_id', + // ]); }); test('it should match snapshot', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts index 38a3612e5861..3470f955dbdb 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/get_signals_template.ts @@ -48,11 +48,11 @@ export const getSignalsTemplate = (index: string, spaceId: string, aadIndexAlias const template = { index_patterns: [`${index}-*`], template: { - aliases: { - [aadIndexAliasName]: { - is_write_index: false, - }, - }, + // aliases: { + // [aadIndexAliasName]: { + // is_write_index: false, + // }, + // }, settings: { index: { lifecycle: { @@ -72,7 +72,7 @@ export const getSignalsTemplate = (index: string, spaceId: string, aadIndexAlias ...ecsMapping.mappings.properties, ...otherMapping.mappings.properties, ...fieldAliases, - ...getRbacRequiredFields(spaceId), + // ...getRbacRequiredFields(spaceId), signal: signalsMapping.mappings.properties.signal, }, _meta: { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts index c36dade4bb9d..4cfedd5dcaa0 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/read_index_route.ts @@ -30,7 +30,7 @@ export const readIndexRoute = (router: SecuritySolutionPluginRouter, config: Con const siemResponse = buildSiemResponse(response); try { - const esClient = context.core.elasticsearch.client.asInternalUser; + const esClient = context.core.elasticsearch.client.asCurrentUser; const siemClient = context.securitySolution?.getAppClient(); if (!siemClient) { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts index 53bebf340c26..d3193900859f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/import_rules_route.ts @@ -128,208 +128,225 @@ export const importRulesRoute = ( const batchParseObjects = chunkParseObjects.shift() ?? []; const newImportRuleResponse = await Promise.all( batchParseObjects.reduce>>((accum, parsedRule) => { - const importsWorkerPromise = new Promise(async (resolve) => { - if (parsedRule instanceof Error) { - // If the JSON object had a validation or parse error then we return - // early with the error and an (unknown) for the ruleId - resolve( - createBulkErrorObject({ - statusCode: 400, - message: parsedRule.message, - }) - ); - return null; - } - const { - anomaly_threshold: anomalyThreshold, - author, - building_block_type: buildingBlockType, - description, - enabled, - event_category_override: eventCategoryOverride, - false_positives: falsePositives, - from, - immutable, - query: queryOrUndefined, - language: languageOrUndefined, - license, - machine_learning_job_id: machineLearningJobId, - output_index: outputIndex, - saved_id: savedId, - meta, - filters: filtersRest, - rule_id: ruleId, - index, - interval, - max_signals: maxSignals, - risk_score: riskScore, - risk_score_mapping: riskScoreMapping, - rule_name_override: ruleNameOverride, - name, - severity, - severity_mapping: severityMapping, - tags, - threat, - threat_filters: threatFilters, - threat_index: threatIndex, - threat_query: threatQuery, - threat_mapping: threatMapping, - threat_language: threatLanguage, - threat_indicator_path: threatIndicatorPath, - concurrent_searches: concurrentSearches, - items_per_search: itemsPerSearch, - threshold, - timestamp_override: timestampOverride, - to, - type, - references, - note, - timeline_id: timelineId, - timeline_title: timelineTitle, - throttle, - version, - exceptions_list: exceptionsList, - } = parsedRule; - - try { - const query = !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined; - - const language = - !isMlRule(type) && languageOrUndefined == null ? 'kuery' : languageOrUndefined; - - // TODO: Fix these either with an is conversion or by better typing them within io-ts - const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; + const importsWorkerPromise = new Promise( + async (resolve, reject) => { + try { + if (parsedRule instanceof Error) { + // If the JSON object had a validation or parse error then we return + // early with the error and an (unknown) for the ruleId + resolve( + createBulkErrorObject({ + statusCode: 400, + message: parsedRule.message, + }) + ); + return null; + } - throwHttpError(await mlAuthz.validateRuleType(type)); - - const rule = await readRules({ rulesClient, ruleId, id: undefined }); - if (rule == null) { - await createRules({ - rulesClient, - anomalyThreshold, + const { + anomaly_threshold: anomalyThreshold, author, - buildingBlockType, + building_block_type: buildingBlockType, description, enabled, - eventCategoryOverride, - falsePositives, + event_category_override: eventCategoryOverride, + false_positives: falsePositives, from, immutable, - query, - language, + query: queryOrUndefined, + language: languageOrUndefined, license, - machineLearningJobId, - outputIndex: signalsIndex, - savedId, - timelineId, - timelineTitle, + machine_learning_job_id: machineLearningJobId, + output_index: outputIndex, + saved_id: savedId, meta, - filters, - ruleId, + filters: filtersRest, + rule_id: ruleId, index, interval, - maxSignals, + max_signals: maxSignals, + risk_score: riskScore, + risk_score_mapping: riskScoreMapping, + rule_name_override: ruleNameOverride, name, - riskScore, - riskScoreMapping, - ruleNameOverride, severity, - severityMapping, + severity_mapping: severityMapping, tags, - throttle, - to, - type, threat, + threat_filters: threatFilters, + threat_index: threatIndex, + threat_query: threatQuery, + threat_mapping: threatMapping, + threat_language: threatLanguage, + threat_indicator_path: threatIndicatorPath, + concurrent_searches: concurrentSearches, + items_per_search: itemsPerSearch, threshold, - threatFilters, - threatIndex, - threatIndicatorPath, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, - timestampOverride, - references, - note, - version, - exceptionsList, - actions: [], // Actions are not imported nor exported at this time - }); - resolve({ rule_id: ruleId, status_code: 200 }); - } else if (rule != null && request.query.overwrite) { - await patchRules({ - rulesClient, - author, - buildingBlockType, - spaceId: context.securitySolution.getSpaceId(), - ruleStatusClient, - description, - enabled, - eventCategoryOverride, - falsePositives, - from, - query, - language, - license, - outputIndex, - savedId, - timelineId, - timelineTitle, - meta, - filters, - rule, - index, - interval, - maxSignals, - riskScore, - riskScoreMapping, - ruleNameOverride, - name, - severity, - severityMapping, - tags, - timestampOverride, - throttle, + timestamp_override: timestampOverride, to, type, - threat, - threshold, - threatFilters, - threatIndex, - threatQuery, - threatMapping, - threatLanguage, - concurrentSearches, - itemsPerSearch, references, note, + timeline_id: timelineId, + timeline_title: timelineTitle, + throttle, version, - exceptionsList, - anomalyThreshold, - machineLearningJobId, - actions: undefined, - }); - resolve({ rule_id: ruleId, status_code: 200 }); - } else if (rule != null) { - resolve( - createBulkErrorObject({ + exceptions_list: exceptionsList, + } = parsedRule; + + try { + const query = + !isMlRule(type) && queryOrUndefined == null ? '' : queryOrUndefined; + const language = + !isMlRule(type) && languageOrUndefined == null + ? 'kuery' + : languageOrUndefined; // TODO: Fix these either with an is conversion or by better typing them within io-ts + + const filters: PartialFilter[] | undefined = filtersRest as PartialFilter[]; + throwHttpError(await mlAuthz.validateRuleType(type)); + const rule = await readRules({ + rulesClient, ruleId, - statusCode: 409, - message: `rule_id: "${ruleId}" already exists`, - }) - ); + id: undefined, + }); + + if (rule == null) { + await createRules({ + rulesClient, + anomalyThreshold, + author, + buildingBlockType, + description, + enabled, + eventCategoryOverride, + falsePositives, + from, + immutable, + query, + language, + license, + machineLearningJobId, + outputIndex: signalsIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + ruleId, + index, + interval, + maxSignals, + name, + riskScore, + riskScoreMapping, + ruleNameOverride, + severity, + severityMapping, + tags, + throttle, + to, + type, + threat, + threshold, + threatFilters, + threatIndex, + threatIndicatorPath, + threatQuery, + threatMapping, + threatLanguage, + concurrentSearches, + itemsPerSearch, + timestampOverride, + references, + note, + version, + exceptionsList, + actions: [], // Actions are not imported nor exported at this time + }); + resolve({ + rule_id: ruleId, + status_code: 200, + }); + } else if (rule != null && request.query.overwrite) { + await patchRules({ + rulesClient, + author, + buildingBlockType, + spaceId: context.securitySolution.getSpaceId(), + ruleStatusClient, + description, + enabled, + eventCategoryOverride, + falsePositives, + from, + query, + language, + license, + outputIndex, + savedId, + timelineId, + timelineTitle, + meta, + filters, + rule, + index, + interval, + maxSignals, + riskScore, + riskScoreMapping, + ruleNameOverride, + name, + severity, + severityMapping, + tags, + timestampOverride, + throttle, + to, + type, + threat, + threshold, + threatFilters, + threatIndex, + threatQuery, + threatMapping, + threatLanguage, + concurrentSearches, + itemsPerSearch, + references, + note, + version, + exceptionsList, + anomalyThreshold, + machineLearningJobId, + actions: undefined, + }); + resolve({ + rule_id: ruleId, + status_code: 200, + }); + } else if (rule != null) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: 409, + message: `rule_id: "${ruleId}" already exists`, + }) + ); + } + } catch (err) { + resolve( + createBulkErrorObject({ + ruleId, + statusCode: err.statusCode ?? 400, + message: err.message, + }) + ); + } + } catch (error) { + reject(error); } - } catch (err) { - resolve( - createBulkErrorObject({ - ruleId, - statusCode: err.statusCode ?? 400, - message: err.message, - }) - ); } - }); + ); return [...accum, importsWorkerPromise]; }, []) ); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts index bf21f9de037f..e54cc94b886f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/signals/open_close_signals_route.ts @@ -6,6 +6,7 @@ */ import { transformError } from '@kbn/securitysolution-es-utils'; +import { ALERT_WORKFLOW_STATUS } from '@kbn/rule-data-utils'; import { setSignalStatusValidateTypeDependents } from '../../../../../common/detection_engine/schemas/request/set_signal_status_type_dependents'; import { SetSignalsStatusSchemaDecoded, @@ -66,7 +67,12 @@ export const setSignalsStatusRoute = (router: SecuritySolutionPluginRouter) => { refresh: true, body: { script: { - source: `ctx._source.signal.status = '${status}'`, + source: `if (ctx._source['${ALERT_WORKFLOW_STATUS}'] != null) { + ctx._source['${ALERT_WORKFLOW_STATUS}'] = '${status}' + } + if (ctx._source.signal != null && ctx._source.signal.status != null) { + ctx._source.signal.status = '${status}' + }`, lang: 'painless', }, query: queryObject, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts index d56344b7707d..1b867507905a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/__mocks__/rule_type.ts @@ -10,6 +10,7 @@ import { v4 } from 'uuid'; import { Logger, SavedObject } from 'kibana/server'; import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; +import { mlPluginServerMock } from '../../../../../../ml/server/mocks'; import type { IRuleDataClient } from '../../../../../../rule_registry/server'; import { ruleRegistryMocks } from '../../../../../../rule_registry/server/mocks'; @@ -84,6 +85,7 @@ export const createRuleTypeMocks = ( config$: mockedConfig$, lists: listMock.createSetup(), logger: loggerMock, + ml: mlPluginServerMock.createSetupContract(), ruleDataClient: ruleRegistryMocks.createRuleDataClient( '.alerts-security.alerts' ) as IRuleDataClient, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts index 75252cc3d47a..0fde90c991e4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/index.ts @@ -7,3 +7,4 @@ export { createQueryAlertType } from './query/create_query_alert_type'; export { createIndicatorMatchAlertType } from './indicator_match/create_indicator_match_alert_type'; +export { createMlAlertType } from './ml/create_ml_alert_type'; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.test.ts new file mode 100644 index 000000000000..40566ffa04e6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { mlPluginServerMock } from '../../../../../../ml/server/mocks'; + +import { allowedExperimentalValues } from '../../../../../common/experimental_features'; +import { bulkCreateMlSignals } from '../../signals/bulk_create_ml_signals'; + +import { createRuleTypeMocks } from '../__mocks__/rule_type'; +import { createMlAlertType } from './create_ml_alert_type'; + +import { RuleParams } from '../../schemas/rule_schemas'; + +jest.mock('../../signals/bulk_create_ml_signals'); + +jest.mock('../utils/get_list_client', () => ({ + getListClient: jest.fn().mockReturnValue({ + listClient: { + getListItemIndex: jest.fn(), + }, + exceptionsClient: jest.fn(), + }), +})); + +jest.mock('../../rule_execution_log/rule_execution_log_client'); + +jest.mock('../../signals/filters/filter_events_against_list', () => ({ + filterEventsAgainstList: jest.fn().mockReturnValue({ + _shards: { + failures: [], + }, + hits: { + hits: [ + { + is_interim: false, + }, + ], + }, + }), +})); + +let jobsSummaryMock: jest.Mock; +let mlMock: ReturnType; + +describe('Machine Learning Alerts', () => { + beforeEach(() => { + jobsSummaryMock = jest.fn(); + jobsSummaryMock.mockResolvedValue([ + { + id: 'test-ml-job', + jobState: 'started', + datafeedState: 'started', + }, + ]); + mlMock = mlPluginServerMock.createSetupContract(); + mlMock.jobServiceProvider.mockReturnValue({ + jobsSummary: jobsSummaryMock, + }); + + (bulkCreateMlSignals as jest.Mock).mockResolvedValue({ + success: true, + bulkCreateDuration: 0, + createdItemsCount: 1, + createdItems: [ + { + _id: '897234565234', + _index: 'test-index', + anomalyScore: 23, + }, + ], + errors: [], + }); + }); + + const params: Partial = { + anomalyThreshold: 23, + from: 'now-45m', + machineLearningJobId: ['test-ml-job'], + to: 'now', + type: 'machine_learning', + }; + + it('does not send an alert when no anomalies found', async () => { + jobsSummaryMock.mockResolvedValue([ + { + id: 'test-ml-job', + jobState: 'started', + datafeedState: 'started', + }, + ]); + const { dependencies, executor } = createRuleTypeMocks('machine_learning', params); + const mlAlertType = createMlAlertType({ + experimentalFeatures: allowedExperimentalValues, + lists: dependencies.lists, + logger: dependencies.logger, + mergeStrategy: 'allFields', + ml: mlMock, + ruleDataClient: dependencies.ruleDataClient, + ruleDataService: dependencies.ruleDataService, + version: '1.0.0', + }); + + dependencies.alerting.registerType(mlAlertType); + + await executor({ params }); + expect(dependencies.ruleDataClient.getWriter).not.toBeCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts new file mode 100644 index 000000000000..1d872df35de3 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/ml/create_ml_alert_type.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { validateNonExact } from '@kbn/securitysolution-io-ts-utils'; +import { PersistenceServices } from '../../../../../../rule_registry/server'; +import { ML_ALERT_TYPE_ID } from '../../../../../common/constants'; +import { machineLearningRuleParams, MachineLearningRuleParams } from '../../schemas/rule_schemas'; +import { mlExecutor } from '../../signals/executors/ml'; +import { createSecurityRuleTypeFactory } from '../create_security_rule_type_factory'; +import { CreateRuleOptions } from '../types'; + +export const createMlAlertType = (createOptions: CreateRuleOptions) => { + const { lists, logger, mergeStrategy, ml, ruleDataClient, ruleDataService } = createOptions; + const createSecurityRuleType = createSecurityRuleTypeFactory({ + lists, + logger, + mergeStrategy, + ruleDataClient, + ruleDataService, + }); + return createSecurityRuleType({ + id: ML_ALERT_TYPE_ID, + name: 'Machine Learning Rule', + validate: { + params: { + validate: (object: unknown) => { + const [validated, errors] = validateNonExact(object, machineLearningRuleParams); + if (errors != null) { + throw new Error(errors); + } + if (validated == null) { + throw new Error('Validation of rule params failed'); + } + return validated; + }, + }, + }, + actionGroups: [ + { + id: 'default', + name: 'Default', + }, + ], + defaultActionGroupId: 'default', + actionVariables: { + context: [{ name: 'server', description: 'the server' }], + }, + minimumLicenseRequired: 'basic', + isExportable: false, + producer: 'security-solution', + async executor(execOptions) { + const { + runOpts: { + buildRuleMessage, + bulkCreate, + exceptionItems, + listClient, + rule, + tuple, + wrapHits, + }, + services, + state, + } = execOptions; + + const result = await mlExecutor({ + buildRuleMessage, + bulkCreate, + exceptionItems, + listClient, + logger, + ml, + rule, + services, + tuple, + wrapHits, + }); + return { ...result, state }; + }, + }); +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_indicator_match.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_indicator_match.sh old mode 100644 new mode 100755 diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_ml.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_ml.sh new file mode 100755 index 000000000000..8ce3d56cd017 --- /dev/null +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/scripts/create_rule_ml.sh @@ -0,0 +1,53 @@ +#!/bin/sh +# +# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +# or more contributor license agreements. Licensed under the Elastic License +# 2.0; you may not use this file except in compliance with the Elastic License +# 2.0. +# + +curl -X POST ${KIBANA_URL}${SPACE_URL}/api/alerts/alert \ + -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ + -H 'kbn-xsrf: true' \ + -H 'Content-Type: application/json' \ + --verbose \ + -d ' +{ + "params":{ + "anomalyThreshold": 23, + "author": [], + "description": "Basic Machine Learning Rule", + "exceptionsList": [], + "falsePositives": [], + "from": "now-45m", + "immutable": false, + "machineLearningJobId": ["test-ml-job"], + "maxSignals": 101, + "outputIndex": "", + "references": [], + "riskScore": 23, + "riskScoreMapping": [], + "ruleId": "1781d055-5c66-4adf-9c59-fc0fa58336a5", + "severity": "high", + "severityMapping": [], + "threat": [], + "to": "now", + "type": "machine_learning", + "version": 1 + }, + "consumer":"alerts", + "alertTypeId":"siem.mlRule", + "schedule":{ + "interval":"15m" + }, + "actions":[], + "tags":[ + "custom", + "ml", + "persistence" + ], + "notifyWhen":"onActionGroupChange", + "name":"Basic Machine Learning Rule" +}' + + diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts index e781bfc50bee..f061240c4a6e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/types.ts @@ -99,7 +99,7 @@ export type CreateSecurityRuleTypeFactory = (options: { ruleDataClient: IRuleDataClient; ruleDataService: IRuleDataPluginService; }) => < - TParams extends RuleParams & { index: string[] | undefined }, + TParams extends RuleParams & { index?: string[] | undefined }, TAlertInstanceContext extends AlertInstanceContext, TServices extends PersistenceServices, TState extends AlertTypeState @@ -124,6 +124,7 @@ export interface CreateRuleOptions { lists: SetupPlugins['lists']; logger: Logger; mergeStrategy: ConfigType['alertMergeStrategy']; + ml?: SetupPlugins['ml']; ruleDataClient: IRuleDataClient; version: string; ruleDataService: IRuleDataPluginService; diff --git a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/helpers.ts b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/helpers.ts index 70d93d7552b1..7e35c2163df7 100644 --- a/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/helpers.ts +++ b/x-pack/plugins/security_solution/server/lib/timeline/routes/timelines/import_timelines/helpers.ts @@ -106,133 +106,132 @@ export const importTimelines = async ( batchParseObjects.reduce>>((accum, parsedTimeline) => { const importsWorkerPromise = new Promise( async (resolve, reject) => { - if (parsedTimeline instanceof Error) { - // If the JSON object had a validation or parse error then we return - // early with the error and an (unknown) for the ruleId - resolve( - createBulkErrorObject({ - statusCode: 400, - message: parsedTimeline.message, - }) - ); - - return null; - } - - const { - savedObjectId, - pinnedEventIds, - globalNotes, - eventNotes, - status, - templateTimelineId, - templateTimelineVersion, - title, - timelineType, - version, - } = parsedTimeline; - - const parsedTimelineObject = omit(timelineSavedObjectOmittedFields, parsedTimeline); - let newTimeline = null; try { - const compareTimelinesStatus = new CompareTimelinesStatus({ + if (parsedTimeline instanceof Error) { + // If the JSON object had a validation or parse error then we return + // early with the error and an (unknown) for the ruleId + resolve( + createBulkErrorObject({ + statusCode: 400, + message: parsedTimeline.message, + }) + ); + return null; + } + + const { + savedObjectId, + pinnedEventIds, + globalNotes, + eventNotes, status, - timelineType, + templateTimelineId, + templateTimelineVersion, title, - timelineInput: { - id: savedObjectId, - version, - }, - templateTimelineInput: { - id: templateTimelineId, - version: templateTimelineVersion, - }, - frameworkRequest, - }); - await compareTimelinesStatus.init(); - const isTemplateTimeline = compareTimelinesStatus.isHandlingTemplateTimeline; - if (compareTimelinesStatus.isCreatableViaImport) { - // create timeline / timeline template - newTimeline = await createTimelines({ - frameworkRequest, - timeline: setTimeline(parsedTimelineObject, parsedTimeline, isTemplateTimeline), - pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds, - notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes], - isImmutable, - overrideNotesOwner: false, - }); + timelineType, + version, + } = parsedTimeline; + const parsedTimelineObject = omit(timelineSavedObjectOmittedFields, parsedTimeline); + let newTimeline = null; - resolve({ - timeline_id: newTimeline.timeline.savedObjectId, - status_code: 200, - action: TimelineStatusActions.createViaImport, + try { + const compareTimelinesStatus = new CompareTimelinesStatus({ + status, + timelineType, + title, + timelineInput: { + id: savedObjectId, + version, + }, + templateTimelineInput: { + id: templateTimelineId, + version: templateTimelineVersion, + }, + frameworkRequest, }); - } + await compareTimelinesStatus.init(); + const isTemplateTimeline = compareTimelinesStatus.isHandlingTemplateTimeline; - if (!compareTimelinesStatus.isHandlingTemplateTimeline) { - const errorMessage = compareTimelinesStatus.checkIsFailureCases( - TimelineStatusActions.createViaImport - ); - const message = errorMessage?.body ?? DEFAULT_ERROR; - - resolve( - createBulkErrorObject({ - id: savedObjectId ?? 'unknown', - statusCode: 409, - message, - }) - ); - } else { - if (compareTimelinesStatus.isUpdatableViaImport) { - // update timeline template + if (compareTimelinesStatus.isCreatableViaImport) { + // create timeline / timeline template newTimeline = await createTimelines({ frameworkRequest, - timeline: parsedTimelineObject, - timelineSavedObjectId: compareTimelinesStatus.timelineId, - timelineVersion: compareTimelinesStatus.timelineVersion, - notes: globalNotes, - existingNoteIds: compareTimelinesStatus.timelineInput.data?.noteIds, + timeline: setTimeline(parsedTimelineObject, parsedTimeline, isTemplateTimeline), + pinnedEventIds: isTemplateTimeline ? null : pinnedEventIds, + notes: isTemplateTimeline ? globalNotes : [...globalNotes, ...eventNotes], isImmutable, overrideNotesOwner: false, }); - resolve({ timeline_id: newTimeline.timeline.savedObjectId, status_code: 200, - action: TimelineStatusActions.updateViaImport, + action: TimelineStatusActions.createViaImport, }); - } else { + } + + if (!compareTimelinesStatus.isHandlingTemplateTimeline) { const errorMessage = compareTimelinesStatus.checkIsFailureCases( - TimelineStatusActions.updateViaImport + TimelineStatusActions.createViaImport ); - const message = errorMessage?.body ?? DEFAULT_ERROR; - resolve( createBulkErrorObject({ - id: - savedObjectId ?? - (templateTimelineId - ? `(template_timeline_id) ${templateTimelineId}` - : 'unknown'), + id: savedObjectId ?? 'unknown', statusCode: 409, message, }) ); + } else { + if (compareTimelinesStatus.isUpdatableViaImport) { + // update timeline template + newTimeline = await createTimelines({ + frameworkRequest, + timeline: parsedTimelineObject, + timelineSavedObjectId: compareTimelinesStatus.timelineId, + timelineVersion: compareTimelinesStatus.timelineVersion, + notes: globalNotes, + existingNoteIds: compareTimelinesStatus.timelineInput.data?.noteIds, + isImmutable, + overrideNotesOwner: false, + }); + resolve({ + timeline_id: newTimeline.timeline.savedObjectId, + status_code: 200, + action: TimelineStatusActions.updateViaImport, + }); + } else { + const errorMessage = compareTimelinesStatus.checkIsFailureCases( + TimelineStatusActions.updateViaImport + ); + const message = errorMessage?.body ?? DEFAULT_ERROR; + resolve( + createBulkErrorObject({ + id: + savedObjectId ?? + (templateTimelineId + ? `(template_timeline_id) ${templateTimelineId}` + : 'unknown'), + statusCode: 409, + message, + }) + ); + } } + } catch (err) { + resolve( + createBulkErrorObject({ + id: + savedObjectId ?? + (templateTimelineId + ? `(template_timeline_id) ${templateTimelineId}` + : 'unknown'), + statusCode: 400, + message: err.message, + }) + ); } - } catch (err) { - resolve( - createBulkErrorObject({ - id: - savedObjectId ?? - (templateTimelineId - ? `(template_timeline_id) ${templateTimelineId}` - : 'unknown'), - statusCode: 400, - message: err.message, - }) - ); + } catch (error) { + reject(error); } } ); diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 734ccc4d5ba8..040ebb659abc 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -49,6 +49,7 @@ import { ILicense, LicensingPluginStart } from '../../licensing/server'; import { FleetStartContract } from '../../fleet/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { createQueryAlertType } from './lib/detection_engine/rule_types'; +import { createMlAlertType } from './lib/detection_engine/rule_types/ml/create_ml_alert_type'; import { initRoutes } from './routes'; import { isAlertExecutor } from './lib/detection_engine/signals/types'; import { signalRulesAlertType } from './lib/detection_engine/signals/signal_rule_alert_type'; @@ -65,6 +66,7 @@ import { QUERY_ALERT_TYPE_ID, DEFAULT_SPACE_ID, INDICATOR_ALERT_TYPE_ID, + ML_ALERT_TYPE_ID, } from '../common/constants'; import { registerEndpointRoutes } from './endpoint/routes/metadata'; import { registerLimitedConcurrencyRoutes } from './endpoint/routes/limited_concurrency'; @@ -129,6 +131,7 @@ export interface PluginSetup {} // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface PluginStart {} + export class Plugin implements IPlugin { private readonly logger: Logger; private readonly config: ConfigType; @@ -246,6 +249,7 @@ export class Plugin implements IPlugin { }; return new Promise(async (resolve, reject) => { - createConfigurationAggregator(configuration, managedConfig) - .pipe(take(3), bufferCount(3)) - .subscribe(([initial, updatedWorkers, updatedInterval]) => { - expect(initial.value).toEqual({ - max_workers: 10, - poll_interval: 6000000, - max_poll_inactivity_cycles: 10, - request_capacity: 1000, - monitored_aggregated_stats_refresh_rate: 5000, - monitored_stats_running_average_window: 50, - monitored_task_execution_thresholds: { - default: { - error_threshold: 90, - warn_threshold: 80, + try { + createConfigurationAggregator(configuration, managedConfig) + .pipe(take(3), bufferCount(3)) + .subscribe(([initial, updatedWorkers, updatedInterval]) => { + expect(initial.value).toEqual({ + max_workers: 10, + poll_interval: 6000000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, }, - custom: {}, - }, - }); - - expect(updatedWorkers.value).toEqual({ - max_workers: 8, - poll_interval: 6000000, - max_poll_inactivity_cycles: 10, - request_capacity: 1000, - monitored_aggregated_stats_refresh_rate: 5000, - monitored_stats_running_average_window: 50, - monitored_task_execution_thresholds: { - default: { - error_threshold: 90, - warn_threshold: 80, + }); + expect(updatedWorkers.value).toEqual({ + max_workers: 8, + poll_interval: 6000000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, }, - custom: {}, - }, - }); - - expect(updatedInterval.value).toEqual({ - max_workers: 8, - poll_interval: 3000, - max_poll_inactivity_cycles: 10, - request_capacity: 1000, - monitored_aggregated_stats_refresh_rate: 5000, - monitored_stats_running_average_window: 50, - monitored_task_execution_thresholds: { - default: { - error_threshold: 90, - warn_threshold: 80, + }); + expect(updatedInterval.value).toEqual({ + max_workers: 8, + poll_interval: 3000, + max_poll_inactivity_cycles: 10, + request_capacity: 1000, + monitored_aggregated_stats_refresh_rate: 5000, + monitored_stats_running_average_window: 50, + monitored_task_execution_thresholds: { + default: { + error_threshold: 90, + warn_threshold: 80, + }, + custom: {}, }, - custom: {}, - }, - }); - resolve(); - }, reject); - - managedConfig.maxWorkersConfiguration$.next(8); - - managedConfig.pollIntervalConfiguration$.next(3000); + }); + resolve(); + }, reject); + managedConfig.maxWorkersConfiguration$.next(8); + managedConfig.pollIntervalConfiguration$.next(3000); + } catch (error) { + reject(error); + } }); }); }); diff --git a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts index 9125bca8f5b0..d24931646128 100644 --- a/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts +++ b/x-pack/plugins/task_manager/server/monitoring/workload_statistics.test.ts @@ -328,27 +328,44 @@ describe('Workload Statistics Aggregator', () => { loggingSystemMock.create().get() ); - return new Promise(async (resolve) => { - workloadAggregator.pipe(first()).subscribe((result) => { - expect(result.key).toEqual('workload'); - expect(result.value).toMatchObject({ - count: 4, - task_types: { - actions_telemetry: { count: 2, status: { idle: 2 } }, - alerting_telemetry: { count: 1, status: { idle: 1 } }, - session_cleanup: { count: 1, status: { idle: 1 } }, - }, + return new Promise(async (resolve, reject) => { + try { + workloadAggregator.pipe(first()).subscribe((result) => { + expect(result.key).toEqual('workload'); + expect(result.value).toMatchObject({ + count: 4, + task_types: { + actions_telemetry: { + count: 2, + status: { + idle: 2, + }, + }, + alerting_telemetry: { + count: 1, + status: { + idle: 1, + }, + }, + session_cleanup: { + count: 1, + status: { + idle: 1, + }, + }, + }, + }); + resolve(); }); - resolve(); - }); - - availability$.next(false); - - await sleep(10); - expect(taskStore.aggregate).not.toHaveBeenCalled(); - await sleep(10); - expect(taskStore.aggregate).not.toHaveBeenCalled(); - availability$.next(true); + availability$.next(false); + await sleep(10); + expect(taskStore.aggregate).not.toHaveBeenCalled(); + await sleep(10); + expect(taskStore.aggregate).not.toHaveBeenCalled(); + availability$.next(true); + } catch (error) { + reject(error); + } }); }); diff --git a/x-pack/plugins/task_manager/server/saved_objects/index.ts b/x-pack/plugins/task_manager/server/saved_objects/index.ts index d2d079c7747b..abbd1af73b55 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/index.ts @@ -8,7 +8,7 @@ import type { SavedObjectsServiceSetup, SavedObjectsTypeMappingDefinition } from 'kibana/server'; import { estypes } from '@elastic/elasticsearch'; import mappings from './mappings.json'; -import { migrations } from './migrations'; +import { getMigrations } from './migrations'; import { TaskManagerConfig } from '../config.js'; import { getOldestIdleActionTask } from '../queries/oldest_idle_action_task'; @@ -22,7 +22,7 @@ export function setupSavedObjects( hidden: true, convertToAliasScript: `ctx._id = ctx._source.type + ':' + ctx._id; ctx._source.remove("kibana")`, mappings: mappings.task as SavedObjectsTypeMappingDefinition, - migrations, + migrations: getMigrations(), indexPattern: config.index, excludeOnUpgrade: async ({ readonlyEsClient }) => { const oldestNeededActionParams = await getOldestIdleActionTask( diff --git a/x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts b/x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts new file mode 100644 index 000000000000..73141479d908 --- /dev/null +++ b/x-pack/plugins/task_manager/server/saved_objects/migrations.test.ts @@ -0,0 +1,169 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import uuid from 'uuid'; +import { getMigrations } from './migrations'; +import { SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { migrationMocks } from 'src/core/server/mocks'; +import { TaskInstanceWithDeprecatedFields } from '../task'; + +const migrationContext = migrationMocks.createContext(); + +describe('successful migrations', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + describe('7.4.0', () => { + test('extend task instance with updated_at', () => { + const migration740 = getMigrations()['7.4.0']; + const taskInstance = getMockData({}); + expect(migration740(taskInstance, migrationContext).attributes.updated_at).not.toBeNull(); + }); + }); + + describe('7.6.0', () => { + test('rename property Internal to Schedule', () => { + const migration760 = getMigrations()['7.6.0']; + const taskInstance = getMockData({}); + expect(migration760(taskInstance, migrationContext)).toEqual({ + ...taskInstance, + attributes: { + ...taskInstance.attributes, + schedule: taskInstance.attributes.schedule, + }, + }); + }); + }); + + describe('8.0.0', () => { + test('transforms actionsTasksLegacyIdToSavedObjectIds', () => { + const migration800 = getMigrations()['8.0.0']; + const taskInstance = getMockData({ + taskType: 'actions:123456', + params: JSON.stringify({ spaceId: 'user1', actionTaskParamsId: '123456' }), + }); + + expect(migration800(taskInstance, migrationContext)).toEqual({ + ...taskInstance, + attributes: { + ...taskInstance.attributes, + params: '{"spaceId":"user1","actionTaskParamsId":"800f81f8-980e-58ca-b710-d1b0644adea2"}', + }, + }); + }); + + test('it is only applicable for saved objects that live in a custom space', () => { + const migration800 = getMigrations()['8.0.0']; + const taskInstance = getMockData({ + taskType: 'actions:123456', + params: JSON.stringify({ spaceId: 'default', actionTaskParamsId: '123456' }), + }); + + expect(migration800(taskInstance, migrationContext)).toEqual(taskInstance); + }); + + test('it is only applicable for saved objects that live in a custom space even if spaces are disabled', () => { + const migration800 = getMigrations()['8.0.0']; + const taskInstance = getMockData({ + taskType: 'actions:123456', + params: JSON.stringify({ actionTaskParamsId: '123456' }), + }); + + expect(migration800(taskInstance, migrationContext)).toEqual(taskInstance); + }); + + test('transforms alertingTaskLegacyIdToSavedObjectIds', () => { + const migration800 = getMigrations()['8.0.0']; + const taskInstance = getMockData({ + taskType: 'alerting:123456', + params: JSON.stringify({ spaceId: 'user1', alertId: '123456' }), + }); + + expect(migration800(taskInstance, migrationContext)).toEqual({ + ...taskInstance, + attributes: { + ...taskInstance.attributes, + params: '{"spaceId":"user1","alertId":"1a4f9206-e25f-58e6-bad5-3ff21e90648e"}', + }, + }); + }); + + test('skip transformation for defult space scenario', () => { + const migration800 = getMigrations()['8.0.0']; + const taskInstance = getMockData({ + taskType: 'alerting:123456', + params: JSON.stringify({ spaceId: 'default', alertId: '123456' }), + }); + + expect(migration800(taskInstance, migrationContext)).toEqual({ + ...taskInstance, + attributes: { + ...taskInstance.attributes, + params: '{"spaceId":"default","alertId":"123456"}', + }, + }); + }); + }); +}); + +describe('handles errors during migrations', () => { + describe('8.0.0 throws if migration fails', () => { + test('should throw the exception if task instance params format is wrong', () => { + const migration800 = getMigrations()['8.0.0']; + const taskInstance = getMockData({ + taskType: 'alerting:123456', + params: `{ spaceId: 'user1', customId: '123456' }`, + }); + expect(() => { + migration800(taskInstance, migrationContext); + }).toThrowError(); + expect(migrationContext.log.error).toHaveBeenCalledWith( + `savedObject 8.0.0 migration failed for task instance ${taskInstance.id} with error: Unexpected token s in JSON at position 2`, + { + migrations: { + taskInstanceDocument: { + ...taskInstance, + attributes: { + ...taskInstance.attributes, + }, + }, + }, + } + ); + }); + }); +}); + +function getUpdatedAt(): string { + const updatedAt = new Date(); + updatedAt.setHours(updatedAt.getHours() + 2); + return updatedAt.toISOString(); +} + +function getMockData( + overwrites: Record = {} +): SavedObjectUnsanitizedDoc> { + return { + attributes: { + scheduledAt: new Date(), + state: { runs: 0, total_cleaned_up: 0 }, + runAt: new Date(), + startedAt: new Date(), + retryAt: new Date(), + ownerId: '234', + taskType: 'foo', + schedule: { interval: '10s' }, + params: { + bar: true, + }, + ...overwrites, + }, + updated_at: getUpdatedAt(), + id: uuid.v4(), + type: 'task', + }; +} diff --git a/x-pack/plugins/task_manager/server/saved_objects/migrations.ts b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts index 879fca2ae4f6..a2ed91dba273 100644 --- a/x-pack/plugins/task_manager/server/saved_objects/migrations.ts +++ b/x-pack/plugins/task_manager/server/saved_objects/migrations.ts @@ -5,16 +5,123 @@ * 2.0. */ -import { SavedObjectMigrationMap, SavedObjectUnsanitizedDoc } from '../../../../../src/core/server'; +import { + LogMeta, + SavedObjectMigrationContext, + SavedObjectMigrationFn, + SavedObjectMigrationMap, + SavedObjectsUtils, + SavedObjectUnsanitizedDoc, +} from '../../../../../src/core/server'; import { TaskInstance, TaskInstanceWithDeprecatedFields } from '../task'; -export const migrations: SavedObjectMigrationMap = { - '7.4.0': (doc) => ({ - ...doc, - updated_at: new Date().toISOString(), - }), - '7.6.0': moveIntervalIntoSchedule, -}; +interface TaskInstanceLogMeta extends LogMeta { + migrations: { taskInstanceDocument: SavedObjectUnsanitizedDoc }; +} + +type TaskInstanceMigration = ( + doc: SavedObjectUnsanitizedDoc +) => SavedObjectUnsanitizedDoc; + +export function getMigrations(): SavedObjectMigrationMap { + return { + '7.4.0': executeMigrationWithErrorHandling( + (doc) => ({ + ...doc, + updated_at: new Date().toISOString(), + }), + '7.4.0' + ), + '7.6.0': executeMigrationWithErrorHandling(moveIntervalIntoSchedule, '7.6.0'), + '8.0.0': executeMigrationWithErrorHandling( + pipeMigrations(alertingTaskLegacyIdToSavedObjectIds, actionsTasksLegacyIdToSavedObjectIds), + '8.0.0' + ), + }; +} + +function executeMigrationWithErrorHandling( + migrationFunc: SavedObjectMigrationFn< + TaskInstanceWithDeprecatedFields, + TaskInstanceWithDeprecatedFields + >, + version: string +) { + return ( + doc: SavedObjectUnsanitizedDoc, + context: SavedObjectMigrationContext + ) => { + try { + return migrationFunc(doc, context); + } catch (ex) { + context.log.error( + `savedObject ${version} migration failed for task instance ${doc.id} with error: ${ex.message}`, + { + migrations: { + taskInstanceDocument: doc, + }, + } + ); + throw ex; + } + }; +} + +function alertingTaskLegacyIdToSavedObjectIds( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + if (doc.attributes.taskType.startsWith('alerting:')) { + let params: { spaceId?: string; alertId?: string } = {}; + params = JSON.parse((doc.attributes.params as unknown) as string); + + if (params.alertId && params.spaceId && params.spaceId !== 'default') { + const newId = SavedObjectsUtils.getConvertedObjectId(params.spaceId, 'alert', params.alertId); + return { + ...doc, + attributes: { + ...doc.attributes, + params: JSON.stringify({ + ...params, + alertId: newId, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any, + }, + }; + } + } + + return doc; +} + +function actionsTasksLegacyIdToSavedObjectIds( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + if (doc.attributes.taskType.startsWith('actions:')) { + let params: { spaceId?: string; actionTaskParamsId?: string } = {}; + params = JSON.parse((doc.attributes.params as unknown) as string); + + if (params.actionTaskParamsId && params.spaceId && params.spaceId !== 'default') { + const newId = SavedObjectsUtils.getConvertedObjectId( + params.spaceId, + 'action_task_params', + params.actionTaskParamsId + ); + return { + ...doc, + attributes: { + ...doc.attributes, + params: JSON.stringify({ + ...params, + actionTaskParamsId: newId, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any, + }, + }; + } + } + + return doc; +} function moveIntervalIntoSchedule({ attributes: { interval, ...attributes }, @@ -34,3 +141,8 @@ function moveIntervalIntoSchedule({ }, }; } + +function pipeMigrations(...migrations: TaskInstanceMigration[]): TaskInstanceMigration { + return (doc: SavedObjectUnsanitizedDoc) => + migrations.reduce((migratedDoc, nextMigration) => nextMigration(migratedDoc), doc); +} diff --git a/x-pack/plugins/task_manager/server/task_scheduling.ts b/x-pack/plugins/task_manager/server/task_scheduling.ts index 88176b25680c..a89f66d9c772 100644 --- a/x-pack/plugins/task_manager/server/task_scheduling.ts +++ b/x-pack/plugins/task_manager/server/task_scheduling.ts @@ -113,11 +113,18 @@ export class TaskScheduling { */ public async runNow(taskId: string): Promise { return new Promise(async (resolve, reject) => { - this.awaitTaskRunResult(taskId) - // don't expose state on runNow - .then(({ id }) => resolve({ id })) - .catch(reject); - this.taskPollingLifecycle.attemptToRun(taskId); + try { + this.awaitTaskRunResult(taskId) // don't expose state on runNow + .then(({ id }) => + resolve({ + id, + }) + ) + .catch(reject); + this.taskPollingLifecycle.attemptToRun(taskId); + } catch (error) { + reject(error); + } }); } @@ -137,39 +144,42 @@ export class TaskScheduling { taskInstance: task, }); return new Promise(async (resolve, reject) => { - // The actual promise returned from this function is resolved after the awaitTaskRunResult promise resolves. - // However, we do not wait to await this promise, as we want later execution to happen in parallel. - // The awaitTaskRunResult promise is resolved once the ephemeral task is successfully executed (technically, when a TaskEventType.TASK_RUN is emitted with the same id). - // However, the ephemeral task won't even get into the queue until the subsequent this.ephemeralTaskLifecycle.attemptToRun is called (which puts it in the queue). - - // The reason for all this confusion? Timing. - - // In the this.ephemeralTaskLifecycle.attemptToRun, it's possible that the ephemeral task is put into the queue and processed before this function call returns anything. - // If that happens, putting the awaitTaskRunResult after would just hang because the task already completed. We need to listen for the completion before we add it to the queue to avoid this possibility. - const { cancel, resolveOnCancel } = cancellablePromise(); - this.awaitTaskRunResult(id, resolveOnCancel) - .then((arg: RunNowResult) => { - resolve(arg); - }) - .catch((err: Error) => { - reject(err); + try { + // The actual promise returned from this function is resolved after the awaitTaskRunResult promise resolves. + // However, we do not wait to await this promise, as we want later execution to happen in parallel. + // The awaitTaskRunResult promise is resolved once the ephemeral task is successfully executed (technically, when a TaskEventType.TASK_RUN is emitted with the same id). + // However, the ephemeral task won't even get into the queue until the subsequent this.ephemeralTaskLifecycle.attemptToRun is called (which puts it in the queue). + // The reason for all this confusion? Timing. + // In the this.ephemeralTaskLifecycle.attemptToRun, it's possible that the ephemeral task is put into the queue and processed before this function call returns anything. + // If that happens, putting the awaitTaskRunResult after would just hang because the task already completed. We need to listen for the completion before we add it to the queue to avoid this possibility. + const { cancel, resolveOnCancel } = cancellablePromise(); + this.awaitTaskRunResult(id, resolveOnCancel) + .then((arg: RunNowResult) => { + resolve(arg); + }) + .catch((err: Error) => { + reject(err); + }); + const attemptToRunResult = this.ephemeralTaskLifecycle.attemptToRun({ + id, + scheduledAt: new Date(), + runAt: new Date(), + status: TaskStatus.Idle, + ownerId: this.taskManagerId, + ...modifiedTask, }); - const attemptToRunResult = this.ephemeralTaskLifecycle.attemptToRun({ - id, - scheduledAt: new Date(), - runAt: new Date(), - status: TaskStatus.Idle, - ownerId: this.taskManagerId, - ...modifiedTask, - }); - if (isErr(attemptToRunResult)) { - cancel(); - reject( - new EphemeralTaskRejectedDueToCapacityError( - `Ephemeral Task of type ${task.taskType} was rejected`, - task - ) - ); + + if (isErr(attemptToRunResult)) { + cancel(); + reject( + new EphemeralTaskRejectedDueToCapacityError( + `Ephemeral Task of type ${task.taskType} was rejected`, + task + ) + ); + } + } catch (error) { + reject(error); } }); } 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 b364277def21..41af9b075484 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -3221,34 +3221,6 @@ } } }, - "kbn_region": { - "properties": { - "min": { - "type": "long", - "_meta": { - "description": "min number of kbn region layers per map" - } - }, - "max": { - "type": "long", - "_meta": { - "description": "max number of kbn region layers per map" - } - }, - "avg": { - "type": "float", - "_meta": { - "description": "avg number of kbn region layers per map" - } - }, - "total": { - "type": "long", - "_meta": { - "description": "total number of kbn region layers in cluster" - } - } - } - }, "kbn_tms_raster": { "properties": { "min": { diff --git a/x-pack/plugins/timelines/common/constants.ts b/x-pack/plugins/timelines/common/constants.ts index 0c03682cc833..262ab841492e 100644 --- a/x-pack/plugins/timelines/common/constants.ts +++ b/x-pack/plugins/timelines/common/constants.ts @@ -21,3 +21,4 @@ export const FILTER_IN_PROGRESS: AlertStatus = 'in-progress'; export const FILTER_ACKNOWLEDGED: AlertStatus = 'acknowledged'; export const RAC_ALERTS_BULK_UPDATE_URL = '/internal/rac/alerts/bulk_update'; +export const DETECTION_ENGINE_SIGNALS_STATUS_URL = '/api/detection_engine/signals/status'; diff --git a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts index 281a1fcc9179..e85f2eaa12d7 100644 --- a/x-pack/plugins/timelines/common/types/timeline/actions/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/actions/index.ts @@ -61,6 +61,7 @@ export interface StatusBulkActionsProps { setEventsDeleted: SetEventsDeleted; onUpdateSuccess?: OnUpdateAlertStatusSuccess; onUpdateFailure?: OnUpdateAlertStatusError; + timelineId?: string; } export interface HeaderActionProps { width: number; diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx index b6d581f52cbe..73be0c13faf5 100644 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx +++ b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx @@ -26,6 +26,7 @@ export interface AddToCaseActionProps { } | null; appId: string; onClose?: Function; + disableAlerts?: boolean; } const AddToCaseActionComponent: React.FC = ({ @@ -35,6 +36,7 @@ const AddToCaseActionComponent: React.FC = ({ casePermissions, appId, onClose, + disableAlerts, }) => { const eventId = event?.ecs._id ?? ''; const eventIndex = event?.ecs._index ?? ''; @@ -104,6 +106,7 @@ const AddToCaseActionComponent: React.FC = ({ onSuccess={onCaseSuccess} useInsertTimeline={useInsertTimeline} appId={appId} + disableAlerts={disableAlerts} /> )} {isAllCaseModalOpen && cases.getAllCasesSelectorModal(getAllCasesSelectorModalProps)} diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/create/flyout.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/create/flyout.tsx index 826b9cd8dc4a..a817bae996aa 100644 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/create/flyout.tsx +++ b/x-pack/plugins/timelines/public/components/actions/timeline/cases/create/flyout.tsx @@ -20,6 +20,7 @@ export interface CreateCaseModalProps { onSuccess: (theCase: Case) => Promise; useInsertTimeline?: Function; appId: string; + disableAlerts?: boolean; } const StyledFlyout = styled(EuiFlyout)` @@ -53,6 +54,7 @@ const CreateCaseFlyoutComponent: React.FC = ({ onCloseFlyout, onSuccess, appId, + disableAlerts, }) => { const { cases } = useKibana().services; const createCaseProps = useMemo(() => { @@ -62,8 +64,9 @@ const CreateCaseFlyoutComponent: React.FC = ({ onSuccess, withSteps: false, owner: [appId], + disableAlerts, }; - }, [afterCaseCreated, onCloseFlyout, onSuccess, appId]); + }, [afterCaseCreated, onCloseFlyout, onSuccess, appId, disableAlerts]); return ( diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/height_hack.ts b/x-pack/plugins/timelines/public/components/t_grid/body/height_hack.ts index 542be06578d6..47cd1ed92d66 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/height_hack.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/body/height_hack.ts @@ -40,15 +40,17 @@ export const useDataGridHeightHack = (pageSize: number, rowCount: number) => { gridVirtualized && gridVirtualized.children[0].clientHeight !== gridVirtualized.clientHeight // check if it has vertical scroll ) { - setHeight( - height + + setHeight((currHeight) => { + return ( + currHeight + gridVirtualized.children[0].clientHeight - gridVirtualized.clientHeight + MAGIC_GAP - ); + ); + }); } }, TIME_INTERVAL); - }, [pageSize, rowCount, height]); + }, [pageSize, rowCount]); return height; }; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx index 09e773fff47a..8781a88c630d 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; +import { ALERT_RULE_CONSUMER, ALERT_RULE_PRODUCER } from '@kbn/rule-data-utils'; import { isEmpty } from 'lodash/fp'; import { EuiDataGridCellValueElementProps } from '@elastic/eui'; @@ -42,16 +42,26 @@ export const getEventIdToDataMapping = ( eventIds: string[], fieldsToKeep: string[], hasAlertsCrud: boolean, - hasAlertsCrudPermissionsByFeatureId?: (featureId: string) => boolean + hasAlertsCrudPermissionsByRule?: ({ + ruleConsumer, + ruleProducer, + }: { + ruleConsumer: string; + ruleProducer?: string; + }) => boolean ): Record => timelineData.reduce((acc, v) => { // FUTURE DEVELOPER // We only have one featureId for security solution therefore we can just use hasAlertsCrud // but for o11y we can multiple featureIds so we need to check every consumer // of the alert to see if they have the permission to update the alert - const alertConsumers = v.data.find((d) => d.field === ALERT_RULE_CONSUMER)?.value ?? []; - const hasPermissions = hasAlertsCrudPermissionsByFeatureId - ? alertConsumers.some((consumer) => hasAlertsCrudPermissionsByFeatureId(consumer)) + const ruleConsumers = v.data.find((d) => d.field === ALERT_RULE_CONSUMER)?.value ?? []; + const ruleProducers = v.data.find((d) => d.field === ALERT_RULE_PRODUCER)?.value ?? []; + const hasPermissions = hasAlertsCrudPermissionsByRule + ? hasAlertsCrudPermissionsByRule({ + ruleConsumer: ruleConsumers.length > 0 ? ruleConsumers[0] : '', + ruleProducer: ruleProducers.length > 0 ? ruleProducers[0] : undefined, + }) : hasAlertsCrud; const fvm = diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index 74d5d54e9652..24f2718fbc1c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -32,7 +32,7 @@ import React, { import { connect, ConnectedProps, useDispatch } from 'react-redux'; import styled, { ThemeContext } from 'styled-components'; -import { ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; +import { ALERT_RULE_CONSUMER, ALERT_RULE_PRODUCER } from '@kbn/rule-data-utils'; import { TGridCellAction, BulkActionsProp, @@ -105,7 +105,13 @@ interface OwnProps { trailingControlColumns?: ControlColumnProps[]; unit?: (total: number) => React.ReactNode; hasAlertsCrud?: boolean; - hasAlertsCrudPermissions?: (featureId: string) => boolean; + hasAlertsCrudPermissions?: ({ + ruleConsumer, + ruleProducer, + }: { + ruleConsumer: string; + ruleProducer?: string; + }) => boolean; totalSelectAllAlerts?: number; } @@ -180,7 +186,13 @@ const transformControlColumns = ({ theme: EuiTheme; setEventsLoading: SetEventsLoading; setEventsDeleted: SetEventsDeleted; - hasAlertsCrudPermissions?: (featureId: string) => boolean; + hasAlertsCrudPermissions?: ({ + ruleConsumer, + ruleProducer, + }: { + ruleConsumer: string; + ruleProducer?: string; + }) => boolean; }): EuiDataGridControlColumn[] => controlColumns.map( ({ id: columnId, headerCellRender = EmptyHeaderCellRender, rowCellRender, width }, i) => ({ @@ -208,7 +220,6 @@ const transformControlColumns = ({ ); }, - // eslint-disable-next-line react/display-name rowCellRender: ({ isDetails, @@ -224,9 +235,15 @@ const transformControlColumns = ({ if (rowData) { addBuildingBlockStyle(rowData.ecs, theme, setCellProps); if (columnId === 'checkbox-control-column' && hasAlertsCrudPermissions != null) { - const alertConsumers = + // FUTURE ENGINEER, the assumption here is you can only have one producer and consumer at this time + const ruleConsumers = rowData.data.find((d) => d.field === ALERT_RULE_CONSUMER)?.value ?? []; - disabled = alertConsumers.some((consumer) => !hasAlertsCrudPermissions(consumer)); + const ruleProducers = + rowData.data.find((d) => d.field === ALERT_RULE_PRODUCER)?.value ?? []; + disabled = !hasAlertsCrudPermissions({ + ruleConsumer: ruleConsumers.length > 0 ? ruleConsumers[0] : '', + ruleProducer: ruleProducers.length > 0 ? ruleProducers[0] : undefined, + }); } } else { // disable the cell when it has no data @@ -766,7 +783,6 @@ export const BodyComponent = React.memo( pageSize={pageSize} pageSizeOptions={itemsPerPageOptions} rowRenderers={rowRenderers} - timelineId={id} totalItemCount={totalItems} /> )} diff --git a/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.test.tsx new file mode 100644 index 000000000000..ca812e2c122d --- /dev/null +++ b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.test.tsx @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { eventRenderedProps, TestProviders } from '../../../mock'; +import { EventRenderedView } from './'; + +describe('event_rendered_view', () => { + test('it renders the timestamp correctly', () => { + render( + + + + ); + expect(screen.queryAllByTestId('moment-date')[0].textContent).toEqual( + '2018-11-05T14:03:25-05:00' + ); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx index 119bb32180ce..c04cc58f453c 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx @@ -15,7 +15,7 @@ import { EuiHorizontalRule, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { ALERT_RULE_NAME, TIMESTAMP } from '@kbn/rule-data-utils'; +import { ALERT_RULE_NAME } from '@kbn/rule-data-utils'; import { get } from 'lodash'; import moment from 'moment'; import React, { ComponentType, useCallback, useMemo } from 'react'; @@ -58,7 +58,7 @@ const StyledEuiBasicTable = styled(EuiBasicTable as BasicTableType)` } `; -interface EventRenderedViewProps { +export interface EventRenderedViewProps { alertToolbar: React.ReactNode; browserFields: BrowserFields; events: TimelineItem[]; @@ -69,13 +69,14 @@ interface EventRenderedViewProps { pageSize: number; pageSizeOptions: number[]; rowRenderers: RowRenderer[]; - timelineId: string; totalItemCount: number; } const PreferenceFormattedDateComponent = ({ value }: { value: Date }) => { const tz = useUiSetting('dateFormat:tz'); const dateFormat = useUiSetting('dateFormat'); - return <>{moment.tz(value, tz).format(dateFormat)}; + const zone: string = moment.tz.zone(tz)?.name ?? moment.tz.guess(); + + return {moment.tz(value, zone).format(dateFormat)}; }; export const PreferenceFormattedDate = React.memo(PreferenceFormattedDateComponent); @@ -90,7 +91,6 @@ const EventRenderedViewComponent = ({ pageSize, pageSizeOptions, rowRenderers, - timelineId, totalItemCount, }: EventRenderedViewProps) => { const ActionTitle = useMemo( @@ -143,7 +143,7 @@ const EventRenderedViewComponent = ({ width: '120px', }, { - field: 'ecs.@timestamp', + field: 'ecs.timestamp', name: i18n.translate('xpack.timelines.alerts.EventRenderedView.timestamp.column', { defaultMessage: 'Timestamp', }), @@ -151,7 +151,7 @@ const EventRenderedViewComponent = ({ hideForMobile: false, // eslint-disable-next-line react/display-name render: (name: unknown, item: TimelineItem) => { - const timestamp = get(item, `ecs.${TIMESTAMP}`); + const timestamp = get(item, `ecs.timestamp`); return ; }, }, @@ -239,6 +239,7 @@ const EventRenderedViewComponent = ({ compressed items={events} columns={columns} + data-test-subj="event-rendered-view" pagination={pagination} onChange={handleTableChange} rowProps={({ ecs }: TimelineItem) => diff --git a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx index 779fddcad256..e98d9fff04a0 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/integrated/index.tsx @@ -41,7 +41,7 @@ import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { defaultHeaders } from '../body/column_headers/default_headers'; import { buildCombinedQuery, getCombinedFilterQuery, resolverIsShowing } from '../helpers'; import { tGridActions, tGridSelectors } from '../../../store/t_grid'; -import { useTimelineEvents } from '../../../container'; +import { useTimelineEvents, InspectResponse, Refetch } from '../../../container'; import { StatefulBody } from '../body'; import { SELECTOR_TIMELINE_GLOBAL_CONTAINER, UpdatedFlexGroup, UpdatedFlexItem } from '../styles'; import { Sort } from '../body/sort'; @@ -114,6 +114,7 @@ export interface TGridIntegratedProps { query: Query; renderCellValue: (props: CellValueElementProps) => React.ReactNode; rowRenderers: RowRenderer[]; + setQuery: (inspect: InspectResponse, loading: boolean, refetch: Refetch) => void; sort: Sort[]; start: string; tGridEventRenderedViewEnabled: boolean; @@ -150,6 +151,7 @@ const TGridIntegratedComponent: React.FC = ({ query, renderCellValue, rowRenderers, + setQuery, sort, start, tGridEventRenderedViewEnabled, @@ -269,6 +271,10 @@ const TGridIntegratedComponent: React.FC = ({ } }, [loading]); + useEffect(() => { + setQuery(inspect, loading, refetch); + }, [inspect, loading, refetch, setQuery]); + return ( = ({ {!resolverIsShowing(graphEventId) && additionalFilters} - {tGridEventRenderedViewEnabled && entityType === 'alerts' && ( - - - - )} + {tGridEventRenderedViewEnabled && + ['detections-page', 'detections-rules-details-page'].includes(id) && ( + + + + )} {!graphEventId && graphOverlay == null && ( diff --git a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx index ad77ad177a9f..ee9b7be48df6 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/standalone/index.tsx @@ -94,7 +94,13 @@ export interface TGridStandaloneProps { filters: Filter[]; footerText: React.ReactNode; filterStatus: AlertStatus; - hasAlertsCrudPermissions: (featureId: string) => boolean; + hasAlertsCrudPermissions: ({ + ruleConsumer, + ruleProducer, + }: { + ruleConsumer: string; + ruleProducer?: string; + }) => boolean; height?: number; indexNames: string[]; itemsPerPageOptions: number[]; @@ -229,8 +235,8 @@ const TGridStandaloneComponent: React.FC = ({ hasAlertsCrud: boolean; totalSelectAllAlerts: number; }>( - (acc, [featureId, nbrAlerts]) => { - const featureHasPermission = hasAlertsCrudPermissions(featureId); + (acc, [ruleConsumer, nbrAlerts]) => { + const featureHasPermission = hasAlertsCrudPermissions({ ruleConsumer }); return { hasAlertsCrud: featureHasPermission || acc.hasAlertsCrud, totalSelectAllAlerts: featureHasPermission @@ -405,7 +411,7 @@ const TGridStandaloneComponent: React.FC = ({ ) : null} - + ); diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx index e4ccf1b72529..be4a75e44349 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx @@ -120,6 +120,7 @@ export const AlertStatusBulkActionsComponent = React.memo { expect(onCategorySelected).toHaveBeenCalledWith(notTheSelectedCategoryId); }); }); + +describe('ViewAllButton', () => { + it(`should update fields with the timestamp and category fields`, () => { + const onUpdateColumns = jest.fn(); + + const wrapper = mount( + + + + ); + + wrapper.find(`.${VIEW_ALL_BUTTON_CLASS_NAME}`).first().simulate('click'); + + expect(onUpdateColumns).toHaveBeenCalledWith( + expect.arrayContaining([ + expect.objectContaining({ id: '@timestamp' }), + expect.objectContaining({ id: 'agent.ephemeral_id' }), + expect.objectContaining({ id: 'agent.hostname' }), + expect.objectContaining({ id: 'agent.id' }), + expect.objectContaining({ id: 'agent.name' }), + ]) + ); + }); +}); diff --git a/x-pack/plugins/timelines/public/components/utils/helpers.ts b/x-pack/plugins/timelines/public/components/utils/helpers.ts index 0c531a9c483b..f02c86aecc10 100644 --- a/x-pack/plugins/timelines/public/components/utils/helpers.ts +++ b/x-pack/plugins/timelines/public/components/utils/helpers.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { get, getOr, isEmpty, uniqBy } from 'lodash/fp'; +import { getOr, isEmpty, uniqBy } from 'lodash/fp'; import { BrowserField, BrowserFields, ColumnHeaderOptions } from '../../../common'; -import { DEFAULT_COLUMN_MIN_WIDTH, DEFAULT_DATE_COLUMN_MIN_WIDTH } from '../t_grid/body/constants'; +import { defaultHeaders } from '../t_grid/body/column_headers/default_headers'; +import { DEFAULT_COLUMN_MIN_WIDTH } from '../t_grid/body/constants'; export const getColumnHeaderFromBrowserField = ({ browserField, @@ -39,17 +40,14 @@ export const getColumnsWithTimestamp = ({ category: string; }): ColumnHeaderOptions[] => { const emptyFields: Record> = {}; - const timestamp = get('base.fields.@timestamp', browserFields); + const timestamp = defaultHeaders.find(({ id }) => id === '@timestamp'); const categoryFields: Array> = [ ...Object.values(getOr(emptyFields, `${category}.fields`, browserFields)), ]; - return timestamp != null && categoryFields.length + return timestamp != null ? uniqBy('id', [ - getColumnHeaderFromBrowserField({ - browserField: timestamp, - width: DEFAULT_DATE_COLUMN_MIN_WIDTH, - }), + timestamp, ...categoryFields.map((f) => getColumnHeaderFromBrowserField({ browserField: f })), ]) : []; diff --git a/x-pack/plugins/timelines/public/container/index.tsx b/x-pack/plugins/timelines/public/container/index.tsx index cdc31ce28e38..30245ea9f17a 100644 --- a/x-pack/plugins/timelines/public/container/index.tsx +++ b/x-pack/plugins/timelines/public/container/index.tsx @@ -41,14 +41,14 @@ import { useAppToasts } from '../hooks/use_app_toasts'; import { TimelineId } from '../store/t_grid/types'; import * as i18n from './translations'; -type InspectResponse = Inspect & { response: string[] }; +export type InspectResponse = Inspect & { response: string[] }; export const detectionsTimelineIds = [ TimelineId.detectionsPage, TimelineId.detectionsRulesDetailsPage, ]; -type Refetch = () => void; +export type Refetch = () => void; export interface TimelineArgs { consumers: Record; diff --git a/x-pack/plugins/timelines/public/container/use_update_alerts.ts b/x-pack/plugins/timelines/public/container/use_update_alerts.ts index 7cce40b59632..7f42ddc6e821 100644 --- a/x-pack/plugins/timelines/public/container/use_update_alerts.ts +++ b/x-pack/plugins/timelines/public/container/use_update_alerts.ts @@ -10,7 +10,10 @@ import { CoreStart } from '../../../../../src/core/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { AlertStatus } from '../../../timelines/common'; -import { RAC_ALERTS_BULK_UPDATE_URL } from '../../common/constants'; +import { + DETECTION_ENGINE_SIGNALS_STATUS_URL, + RAC_ALERTS_BULK_UPDATE_URL, +} from '../../common/constants'; /** * Update alert status by query @@ -18,25 +21,35 @@ import { RAC_ALERTS_BULK_UPDATE_URL } from '../../common/constants'; * @param status to update to('open' / 'closed' / 'acknowledged') * @param index index to be updated * @param query optional query object to update alerts by query. - * @param ids optional array of alert ids to update. Ignored if query passed. + * * @throws An error if response is not OK */ -export const useUpdateAlertsStatus = (): { +export const useUpdateAlertsStatus = ( + timelineId: string +): { updateAlertStatus: (params: { status: AlertStatus; index: string; - ids?: string[]; - query?: object; + query: object; }) => Promise; } => { const { http } = useKibana().services; return { - updateAlertStatus: async ({ status, index, ids, query }) => { - const { body } = await http.post(RAC_ALERTS_BULK_UPDATE_URL, { - body: JSON.stringify({ index, status, ...(query ? { query } : { ids }) }), - }); - return body; + updateAlertStatus: async ({ status, index, query }) => { + if (['detections-page', 'detections-rules-details-page'].includes(timelineId)) { + return http!.fetch(DETECTION_ENGINE_SIGNALS_STATUS_URL, { + method: 'POST', + body: JSON.stringify({ status, query }), + }); + } else { + const { body } = await http.post(RAC_ALERTS_BULK_UPDATE_URL, { + body: JSON.stringify({ index, status, query }), + }); + return body; + } }, }; }; + +// diff --git a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx index 8fd637767a38..c9269436646e 100644 --- a/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx +++ b/x-pack/plugins/timelines/public/hooks/use_status_bulk_action_items.tsx @@ -26,8 +26,9 @@ export const useStatusBulkActionItems = ({ setEventsDeleted, onUpdateSuccess, onUpdateFailure, + timelineId, }: StatusBulkActionsProps) => { - const { updateAlertStatus } = useUpdateAlertsStatus(); + const { updateAlertStatus } = useUpdateAlertsStatus(timelineId ?? ''); const { addSuccess, addError, addWarning } = useAppToasts(); const onAlertStatusUpdateSuccess = useCallback( diff --git a/x-pack/plugins/timelines/public/mock/t_grid.ts b/x-pack/plugins/timelines/public/mock/t_grid.tsx similarity index 84% rename from x-pack/plugins/timelines/public/mock/t_grid.ts rename to x-pack/plugins/timelines/public/mock/t_grid.tsx index 550082466e69..3ae1a1d53c20 100644 --- a/x-pack/plugins/timelines/public/mock/t_grid.ts +++ b/x-pack/plugins/timelines/public/mock/t_grid.tsx @@ -5,12 +5,16 @@ * 2.0. */ +import React from 'react'; import { ALERT_START, ALERT_STATUS } from '@kbn/rule-data-utils'; import { TGridIntegratedProps } from '../components/t_grid/integrated'; import { mockBrowserFields, mockDocValueFields } from './browser_fields'; import { mockDataProviders } from './mock_data_providers'; +import { mockTimelineData } from './mock_timeline_data'; import { ColumnHeaderOptions, TimelineId } from '../../common'; import { mockIndexNames, mockIndexPattern } from './index_pattern'; +import { EventRenderedViewProps } from '../components/t_grid/event_rendered_view'; + const columnHeaders: ColumnHeaderOptions[] = [ { columnHeaderType: 'not-filtered', @@ -110,6 +114,7 @@ export const tGridIntegratedProps: TGridIntegratedProps = { }, renderCellValue: () => null, rowRenderers: [], + setQuery: () => null, sort: [ { columnId: '@timestamp', @@ -121,3 +126,17 @@ export const tGridIntegratedProps: TGridIntegratedProps = { tGridEventRenderedViewEnabled: true, trailingControlColumns: [], }; + +export const eventRenderedProps: EventRenderedViewProps = { + alertToolbar: <>, + browserFields: mockBrowserFields, + events: mockTimelineData, + leadingControlColumns: [], + onChangePage: () => null, + onChangeItemsPerPage: () => null, + pageIndex: 0, + pageSize: 10, + pageSizeOptions: [10, 25, 50, 100], + rowRenderers: [], + totalItemCount: 100, +}; diff --git a/x-pack/plugins/timelines/public/plugin.ts b/x-pack/plugins/timelines/public/plugin.ts index 74e1f2b32844..4b383ce39214 100644 --- a/x-pack/plugins/timelines/public/plugin.ts +++ b/x-pack/plugins/timelines/public/plugin.ts @@ -48,6 +48,13 @@ export class TimelinesPlugin implements Plugin { return getHoverActions(this._store!); }, getTGrid: (props: TGridProps) => { + if (props.type === 'standalone' && this._store) { + const { getState } = this._store; + const state = getState(); + if (state && state.app) { + this._store = undefined; + } + } return getTGridLazy(props, { store: this._store, storage: this._storage, diff --git a/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts b/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts index e20d76bdaf62..907907e97812 100644 --- a/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts @@ -65,7 +65,7 @@ export const requestIndexFieldSearch = async ( }); return get(searchResponse, 'body.hits.total.value', 0) > 0; } else { - if (index.startsWith('.alerts-security.alerts')) { + if (index.startsWith('.alerts-observability')) { return indexPatternsFetcherAsInternalUser.getFieldsForWildcard({ pattern: index, }); diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts index 8f4861ab43b4..3ab924440152 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/index.ts @@ -38,7 +38,7 @@ export const timelineEventsAll: TimelineFactory = { let { fieldRequested, ...queryOptions } = cloneDeep(options); queryOptions.fields = buildFieldsRequest(fieldRequested, queryOptions.excludeEcsData); const { activePage, querySize } = options.pagination; - const buckets = getOr([], 'aggregations.consumers.buckets', response.rawResponse); + const producerBuckets = getOr([], 'aggregations.producers.buckets', response.rawResponse); const totalCount = response.rawResponse.hits.total || 0; const hits = response.rawResponse.hits.hits; @@ -62,7 +62,7 @@ export const timelineEventsAll: TimelineFactory = { ) ); - const consumers = buckets.reduce( + const consumers: Record = producerBuckets.reduce( (acc: Record, b: { key: string; doc_count: number }) => ({ ...acc, [b.key]: b.doc_count, diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts index e9261e8b116b..72a7d6e2692e 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/query.events_all.dsl.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ALERT_RULE_CONSUMER } from '@kbn/rule-data-utils'; +import { ALERT_RULE_PRODUCER } from '@kbn/rule-data-utils'; import { isEmpty } from 'lodash/fp'; import { @@ -69,8 +69,8 @@ export const buildTimelineEventsAllQuery = ({ body: { ...(!isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), aggregations: { - consumers: { - terms: { field: ALERT_RULE_CONSUMER }, + producers: { + terms: { field: ALERT_RULE_PRODUCER, exclude: ['alerts'] }, }, }, query: { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index 214167fe6d72..6bf1abc2cc61 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -45,7 +45,7 @@ import { useApi } from '../../../../hooks/use_api'; import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { RedirectToTransformManagement } from '../../../../common/navigation'; import { ToastNotificationText } from '../../../../components'; -import { DuplicateIndexPatternError } from '../../../../../../../../../src/plugins/data/public'; +import { DuplicateDataViewError } from '../../../../../../../../../src/plugins/data/public'; import { PutTransformsLatestRequestSchema, PutTransformsPivotRequestSchema, @@ -257,7 +257,7 @@ export const StepCreateForm: FC = React.memo( setLoading(false); return true; } catch (e) { - if (e instanceof DuplicateIndexPatternError) { + if (e instanceof DuplicateDataViewError) { toastNotifications.addDanger( i18n.translate('xpack.transform.stepCreateForm.duplicateIndexPatternErrorMessage', { defaultMessage: diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx index 6249e77ce31d..55576c3f3ee7 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_clone/use_clone_action.tsx @@ -42,8 +42,8 @@ export const useCloneAction = (forceDisable: boolean, transformNodes: number) => toastNotifications.addDanger( i18n.translate('xpack.transform.clone.noIndexPatternErrorPromptText', { defaultMessage: - 'Unable to clone the transform . No index pattern exists for {indexPattern}.', - values: { indexPattern: indexPatternTitle }, + 'Unable to clone the transform {transformId}. No index pattern exists for {indexPattern}.', + values: { indexPattern: indexPatternTitle, transformId: item.id }, }) ); } else { @@ -52,11 +52,11 @@ export const useCloneAction = (forceDisable: boolean, transformNodes: number) => ); } } catch (e) { - toastNotifications.addDanger( - i18n.translate('xpack.transform.clone.errorPromptText', { + toastNotifications.addError(e, { + title: i18n.translate('xpack.transform.clone.errorPromptText', { defaultMessage: 'An error occurred checking if source index pattern exists', - }) - ); + }), + }); } }, [ diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx index b84b309c478f..03e45b827195 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx @@ -5,25 +5,63 @@ * 2.0. */ -import React, { useContext, useMemo, useState } from 'react'; +import React, { useCallback, useContext, useMemo, useState } from 'react'; -import { TransformConfigUnion } from '../../../../../../common/types/transform'; +import { i18n } from '@kbn/i18n'; import { TransformListAction, TransformListRow } from '../../../../common'; import { AuthorizationContext } from '../../../../lib/authorization'; import { editActionNameText, EditActionName } from './edit_action_name'; +import { useSearchItems } from '../../../../hooks/use_search_items'; +import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; +import { TransformConfigUnion } from '../../../../../../common/types/transform'; export const useEditAction = (forceDisable: boolean, transformNodes: number) => { const { canCreateTransform } = useContext(AuthorizationContext).capabilities; const [config, setConfig] = useState(); const [isFlyoutVisible, setIsFlyoutVisible] = useState(false); + const [indexPatternId, setIndexPatternId] = useState(); + const closeFlyout = () => setIsFlyoutVisible(false); - const showFlyout = (newConfig: TransformConfigUnion) => { - setConfig(newConfig); - setIsFlyoutVisible(true); - }; + + const { getIndexPatternIdByTitle } = useSearchItems(undefined); + const toastNotifications = useToastNotifications(); + const appDeps = useAppDependencies(); + const indexPatterns = appDeps.data.indexPatterns; + + const clickHandler = useCallback( + async (item: TransformListRow) => { + try { + const indexPatternTitle = Array.isArray(item.config.source.index) + ? item.config.source.index.join(',') + : item.config.source.index; + const currentIndexPatternId = getIndexPatternIdByTitle(indexPatternTitle); + + if (currentIndexPatternId === undefined) { + toastNotifications.addWarning( + i18n.translate('xpack.transform.edit.noIndexPatternErrorPromptText', { + defaultMessage: + 'Unable to get index pattern the transform {transformId}. No index pattern exists for {indexPattern}.', + values: { indexPattern: indexPatternTitle, transformId: item.id }, + }) + ); + } + setIndexPatternId(currentIndexPatternId); + setConfig(item.config); + setIsFlyoutVisible(true); + } catch (e) { + toastNotifications.addError(e, { + title: i18n.translate('xpack.transform.edit.errorPromptText', { + defaultMessage: 'An error occurred checking if source index pattern exists', + }), + }); + } + }, + // eslint-disable-next-line react-hooks/exhaustive-deps + [indexPatterns, toastNotifications, getIndexPatternIdByTitle] + ); const action: TransformListAction = useMemo( () => ({ @@ -32,10 +70,10 @@ export const useEditAction = (forceDisable: boolean, transformNodes: number) => description: editActionNameText, icon: 'pencil', type: 'icon', - onClick: (item: TransformListRow) => showFlyout(item.config), + onClick: (item: TransformListRow) => clickHandler(item), 'data-test-subj': 'transformActionEdit', }), - [canCreateTransform, forceDisable, transformNodes] + [canCreateTransform, clickHandler, forceDisable, transformNodes] ); return { @@ -43,6 +81,6 @@ export const useEditAction = (forceDisable: boolean, transformNodes: number) => config, closeFlyout, isFlyoutVisible, - showFlyout, + indexPatternId, }; }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx index faa304678c0f..55225e0ff45c 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx @@ -30,7 +30,6 @@ import { getErrorMessage } from '../../../../../../common/utils/errors'; import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../../../../common'; import { useToastNotifications } from '../../../../app_dependencies'; - import { useApi } from '../../../../hooks/use_api'; import { EditTransformFlyoutCallout } from './edit_transform_flyout_callout'; @@ -43,9 +42,14 @@ import { interface EditTransformFlyoutProps { closeFlyout: () => void; config: TransformConfigUnion; + indexPatternId?: string; } -export const EditTransformFlyout: FC = ({ closeFlyout, config }) => { +export const EditTransformFlyout: FC = ({ + closeFlyout, + config, + indexPatternId, +}) => { const api = useApi(); const toastNotifications = useToastNotifications(); @@ -96,7 +100,10 @@ export const EditTransformFlyout: FC = ({ closeFlyout, }> - + {errorMessage !== undefined && ( <> diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx index d434e2e719f5..40ccd6872440 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout_form.tsx @@ -5,23 +5,58 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useEffect, useMemo, useState } from 'react'; -import { EuiForm, EuiAccordion, EuiSpacer } from '@elastic/eui'; +import { EuiForm, EuiAccordion, EuiSpacer, EuiSelect, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { EditTransformFlyoutFormTextInput } from './edit_transform_flyout_form_text_input'; import { UseEditTransformFlyoutReturnType } from './use_edit_transform_flyout'; +import { useAppDependencies } from '../../../../app_dependencies'; +import { KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common'; interface EditTransformFlyoutFormProps { editTransformFlyout: UseEditTransformFlyoutReturnType; + indexPatternId?: string; } export const EditTransformFlyoutForm: FC = ({ editTransformFlyout: [state, dispatch], + indexPatternId, }) => { const formFields = state.formFields; + const [dateFieldNames, setDateFieldNames] = useState([]); + + const appDeps = useAppDependencies(); + const indexPatternsClient = appDeps.data.indexPatterns; + + useEffect( + function getDateFields() { + let unmounted = false; + if (indexPatternId !== undefined) { + indexPatternsClient.get(indexPatternId).then((indexPattern) => { + if (indexPattern) { + const dateTimeFields = indexPattern.fields + .filter((f) => f.type === KBN_FIELD_TYPES.DATE) + .map((f) => f.name) + .sort(); + if (!unmounted) { + setDateFieldNames(dateTimeFields); + } + } + }); + return () => { + unmounted = true; + }; + } + }, + [indexPatternId, indexPatternsClient] + ); + + const retentionDateFieldOptions = useMemo(() => { + return Array.isArray(dateFieldNames) ? dateFieldNames.map((text: string) => ({ text })) : []; + }, [dateFieldNames]); return ( @@ -112,19 +147,57 @@ export const EditTransformFlyoutForm: FC = ({ paddingSize="s" >
      - {' '} - dispatch({ field: 'retentionPolicyField', value })} - value={formFields.retentionPolicyField.value} - /> + { + // If index pattern or date fields info not available + // gracefully defaults to text input + indexPatternId ? ( + 0} + error={formFields.retentionPolicyField.errorMessages} + helpText={i18n.translate( + 'xpack.transform.transformList.editFlyoutFormRetentionPolicyDateFieldHelpText', + { + defaultMessage: + 'Select the date field that can be used to identify out of date documents in the destination index.', + } + )} + > + + dispatch({ field: 'retentionPolicyField', value: e.target.value }) + } + /> + + ) : ( + dispatch({ field: 'retentionPolicyField', value })} + value={formFields.retentionPolicyField.value} + /> + ) + } {startAction.isModalVisible && } {editAction.config && editAction.isFlyoutVisible && ( - + )} {deleteAction.isModalVisible && } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 6115e07189ed..082b24d3a9c0 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5580,9 +5580,6 @@ "xpack.apm.rum.visitorBreakdown.operatingSystem": "オペレーティングシステム", "xpack.apm.rum.visitorBreakdownMap.avgPageLoadDuration": "平均ページ読み込み時間", "xpack.apm.rum.visitorBreakdownMap.pageLoadDurationByRegion": "地域別ページ読み込み時間 (平均) ", - "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description": "ブラウザーの開発者ツールを開き、API応答を確認すると、すべてのElasticsearchクエリを検査できます。この設定はKibanaの{advancedSettingsLink}で無効にでkます", - "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description.advancedSettings": "高度な設定", - "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.title": "調査可能なESクエリ (`apm:enableInspectEsQueries`) ", "xpack.apm.searchInput.filter": "フィルター...", "xpack.apm.selectPlaceholder": "オプションを選択:", "xpack.apm.serviceDetails.errorsTabLabel": "エラー", @@ -5660,7 +5657,6 @@ "xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningTitle": "JVM を特定できませんでした", "xpack.apm.serviceNodeNameMissing": " (空) ", "xpack.apm.serviceOveriew.errorsTableOccurrences": "{occurrencesCount} occ.", - "xpack.apm.serviceOverview.dependenciesTableColumnBackend": "バックエンド", "xpack.apm.serviceOverview.dependenciesTableTitle": "依存関係", "xpack.apm.serviceOverview.errorsTableColumnLastSeen": "前回の認識", "xpack.apm.serviceOverview.errorsTableColumnName": "名前", @@ -13863,10 +13859,6 @@ "xpack.maps.deprecation.proxyEMS.message": "map.proxyElasticMapsServiceInMapsは廃止予定であり、8.0で削除されます。", "xpack.maps.deprecation.proxyEMS.step1": "Kibana構成ファイル、CLIフラグ、または環境変数 (Dockerのみ) で「map.proxyElasticMapsServiceInMaps」を削除します。", "xpack.maps.deprecation.proxyEMS.step2": "Elastic Maps Serviceをローカルでホストします。", - "xpack.maps.deprecation.regionmap.message": "map.regionmapは廃止予定であり、8.0 では削除されます。", - "xpack.maps.deprecation.regionmap.step1": "Kibana構成ファイル、CLIフラグ、または環境変数 (Dockerのみ) で「map.regionmap」を削除します。", - "xpack.maps.deprecation.regionmap.step2": "[GeoJSONのアップロード]を使用して、「map.regionmap.layers」で定義された各レイヤーをアップロードします。", - "xpack.maps.deprecation.regionmap.step3": "「構成されたGeoJSON」レイヤーですべてのマップを更新します。Choroplethレイヤーウィザードを使用して、置換レイヤーを構築します。「構成されたGeoJSON」レイヤーをマップから削除します。", "xpack.maps.discover.visualizeFieldLabel": "Mapsで可視化", "xpack.maps.distanceFilterForm.filterLabelLabel": "ラベルでフィルタリング", "xpack.maps.drawFeatureControl.invalidGeometry": "無効なジオメトリが検出されました", @@ -14233,11 +14225,6 @@ "xpack.maps.source.esTopHitsSearch.sortFieldLabel": "並べ替えフィールド", "xpack.maps.source.esTopHitsSearch.sortOrderLabel": "並べ替え順", "xpack.maps.source.geofieldLabel": "地理空間フィールド", - "xpack.maps.source.kbnRegionMap.deprecationTooltipMessage": "「構成されたGeoJSON」レイヤーは廃止予定です。1) [GeoJSONのアップロード]を使用して、'{vectorLayer}'をアップロードします。2) Choroplethレイヤーウィザードを使用して、置換レイヤーを構築します。3) 最後にこのレイヤーをマップから削除します。", - "xpack.maps.source.kbnRegionMap.noConfigErrorMessage": "{name} の map.regionmap 構成が見つかりません", - "xpack.maps.source.kbnRegionMap.vectorLayerLabel": "ベクターレイヤー", - "xpack.maps.source.kbnRegionMap.vectorLayerUrlLabel": "ベクターレイヤーURL", - "xpack.maps.source.kbnRegionMapTitle": "カスタムベクターシェイプ", "xpack.maps.source.kbnTMS.kbnTMS.urlLabel": "タイルマップ URL", "xpack.maps.source.kbnTMS.noConfigErrorMessage": "kibana.yml に map.tilemap.url 構成が見つかりません", "xpack.maps.source.kbnTMS.noLayerAvailableHelptext": "タイルマップレイヤーが利用できません。システム管理者に、kibana.yml で「map.tilemap.url」を設定するよう依頼してください。", @@ -14420,8 +14407,6 @@ "xpack.maps.tutorials.ems.shortDescription": "Elastic Maps Service からの管理ベクターシェイプ。", "xpack.maps.tutorials.ems.uploadStepText": "1.[Maps] ({newMapUrl}) を開きます。\n2.[Add layer]をクリックしてから[Upload GeoJSON]を選択します。\n3.GeoJSON ファイルをアップロードして[Import file]をクリックします。", "xpack.maps.tutorials.ems.uploadStepTitle": "Elastic Maps Service境界のインデックス作成", - "xpack.maps.util.formatErrorMessage": "URL からベクターシェイプを取得できません:{format}", - "xpack.maps.util.requestFailedErrorMessage": "URL からベクターシェイプを取得できません:{fetchUrl}", "xpack.maps.validatedNumberInput.invalidClampErrorMessage": "{min} と {max} の間でなければなりません", "xpack.maps.validatedRange.rangeErrorMessage": "{min} と {max} の間でなければなりません", "xpack.maps.vector.dualSize.unitLabel": "px", @@ -18455,7 +18440,6 @@ "xpack.osquery.scheduledQueryDetails.pageTitle": "{queryName}詳細", "xpack.osquery.scheduledQueryDetails.viewAllScheduledQueriesListTitle": "すべてのスケジュールされたクエリグループを表示", "xpack.osquery.scheduledQueryDetailsPage.editQueryButtonLabel": "編集", - "xpack.osquery.scheduledQueryGroup.form.advancedSectionToggleButtonLabel": "高度な設定", "xpack.osquery.scheduledQueryGroup.form.agentPolicyFieldLabel": "エージェントポリシー", "xpack.osquery.scheduledQueryGroup.form.cancelButtonLabel": "キャンセル", "xpack.osquery.scheduledQueryGroup.form.createSuccessToastMessageText": "正常に{scheduledQueryGroupName}をスケジュールしました", @@ -20272,7 +20256,6 @@ "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.cannotEditLists": "これらの権限がない場合は、値リストを作成したり編集したりできません。", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.cannotEditRules": "その権限がない場合、検出エンジンルールを作製したり編集したりできません。", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.essenceDescription": "この機能のすべてにアクセスするには、次の権限が必要です。サポートについては、管理者にお問い合わせください。", - "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.missingFeaturePrivileges": "{index}機能の{privileges}権限が不足しています。{explanation}", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.missingIndexPrivileges": "{index}インデックスの{privileges}権限が不足しています。{explanation}", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageTitle": "権限が不十分です", "xpack.securitySolution.detectionEngine.mitreAttack.addSubtechniqueTitle": "サブ手法を追加", @@ -23581,7 +23564,6 @@ "xpack.transform.clone.errorPromptText": "ソースインデックスパターンが存在するかどうかを確認するときにエラーが発生しました", "xpack.transform.clone.errorPromptTitle": "変換構成の取得中にエラーが発生しました。", "xpack.transform.clone.fetchErrorPromptText": "KibanaインデックスパターンIDを取得できませんでした。", - "xpack.transform.clone.noIndexPatternErrorPromptText": "変換を複製できません。{indexPattern}のインデックスパターンが存在しません。", "xpack.transform.cloneTransform.breadcrumbTitle": "クローン変換", "xpack.transform.createTransform.breadcrumbTitle": "変換の作成", "xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage": "ディスティネーションインデックス{destinationIndex}の削除中にエラーが発生しました", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index c7aff32f16db..83d99af18507 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5608,9 +5608,6 @@ "xpack.apm.rum.visitorBreakdown.operatingSystem": "操作系统", "xpack.apm.rum.visitorBreakdownMap.avgPageLoadDuration": "页面加载平均持续时间", "xpack.apm.rum.visitorBreakdownMap.pageLoadDurationByRegion": "按区域列出的页面加载持续时间(平均值)", - "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description": "现在可以通过打开浏览器的开发工具和查看 API 响应,来检查各个 Elasticsearch 查询。该设置可以在 Kibana 的“{advancedSettingsLink}”中禁用", - "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.description.advancedSettings": "高级设置", - "xpack.apm.searchBar.inspectEsQueriesEnabled.callout.title": "可检查的 ES 查询 (`apm:enableInspectEsQueries`)", "xpack.apm.searchInput.filter": "筛选...", "xpack.apm.selectPlaceholder": "选择选项:", "xpack.apm.serviceDetails.errorsTabLabel": "错误", @@ -5688,7 +5685,6 @@ "xpack.apm.serviceNodeMetrics.unidentifiedServiceNodesWarningTitle": "找不到 JVM", "xpack.apm.serviceNodeNameMissing": "(空)", "xpack.apm.serviceOveriew.errorsTableOccurrences": "{occurrencesCount} 次", - "xpack.apm.serviceOverview.dependenciesTableColumnBackend": "后端", "xpack.apm.serviceOverview.dependenciesTableTitle": "依赖项", "xpack.apm.serviceOverview.errorsTableColumnLastSeen": "最后看到时间", "xpack.apm.serviceOverview.errorsTableColumnName": "名称", @@ -14225,10 +14221,6 @@ "xpack.maps.deprecation.proxyEMS.message": "map.proxyElasticMapsServiceInMaps 已弃用,将在 8.0 中移除。", "xpack.maps.deprecation.proxyEMS.step1": "在 Kibana 配置文件、CLI 标志或环境变量中中移除“map.proxyElasticMapsServiceInMaps”(仅适用于 Docker)。", "xpack.maps.deprecation.proxyEMS.step2": "本地托管 Elastic Maps Service。", - "xpack.maps.deprecation.regionmap.message": "map.regionmap 已过时,将在 8.0 中移除。", - "xpack.maps.deprecation.regionmap.step1": "在 Kibana 配置文件、CLI 标志或环境变量中中移除“map.regionmap”(仅适用于 Docker)。", - "xpack.maps.deprecation.regionmap.step2": "使用“上传 GeoJSON”上传“map.regionmap.layers”定义的每个图层。", - "xpack.maps.deprecation.regionmap.step3": "使用“已配置 GeoJSON”图层更新所有地图。使用 Choropleth 图层向导构建替代图层。从您的图层中删除“已配置 GeoJSON”图层。", "xpack.maps.discover.visualizeFieldLabel": "在 Maps 中可视化", "xpack.maps.distanceFilterForm.filterLabelLabel": "筛选标签", "xpack.maps.drawFeatureControl.invalidGeometry": "检测到无效的几何形状", @@ -14596,11 +14588,6 @@ "xpack.maps.source.esTopHitsSearch.sortFieldLabel": "排序字段", "xpack.maps.source.esTopHitsSearch.sortOrderLabel": "排序顺序", "xpack.maps.source.geofieldLabel": "地理空间字段", - "xpack.maps.source.kbnRegionMap.deprecationTooltipMessage": "“已配置 GeoJSON”图层已弃用。1) 请使用“上传 GeoJSON”上传“{vectorLayer}”。2) 请使用 Choropleth 图层向导构建替代图层。3) 最后,请从地图中删除此图层。", - "xpack.maps.source.kbnRegionMap.noConfigErrorMessage": "找不到 {name} 的 map.regionmap 配置", - "xpack.maps.source.kbnRegionMap.vectorLayerLabel": "矢量图层", - "xpack.maps.source.kbnRegionMap.vectorLayerUrlLabel": "矢量图层 URL", - "xpack.maps.source.kbnRegionMapTitle": "定制矢量形状", "xpack.maps.source.kbnTMS.kbnTMS.urlLabel": "磁贴地图 URL", "xpack.maps.source.kbnTMS.noConfigErrorMessage": "在 kibana.yml 中找不到 map.tilemap.url 配置", "xpack.maps.source.kbnTMS.noLayerAvailableHelptext": "没有可用的磁贴地图图层。让您的系统管理员在 kibana.yml 中设置“map.tilemap.url”。", @@ -14783,8 +14770,6 @@ "xpack.maps.tutorials.ems.shortDescription": "来自 Elastic Maps Service 的管理边界。", "xpack.maps.tutorials.ems.uploadStepText": "1.打开 [Maps]({newMapUrl}).\n2.单击`添加图层`,然后选择`上传 GeoJSON`。\n3.上传 GeoJSON 文件,然后单击`导入文件`。", "xpack.maps.tutorials.ems.uploadStepTitle": "索引 Elastic Maps Service 边界", - "xpack.maps.util.formatErrorMessage": "无法从以下 URL 获取矢量形状:{format}", - "xpack.maps.util.requestFailedErrorMessage": "无法从以下 URL 获取矢量形状:{fetchUrl}", "xpack.maps.validatedNumberInput.invalidClampErrorMessage": "必须介于 {min} 和 {max} 之间", "xpack.maps.validatedRange.rangeErrorMessage": "必须介于 {min} 和 {max} 之间", "xpack.maps.vector.dualSize.unitLabel": "px", @@ -18880,7 +18865,6 @@ "xpack.osquery.scheduledQueryDetails.pageTitle": "{queryName} 详细信息", "xpack.osquery.scheduledQueryDetails.viewAllScheduledQueriesListTitle": "查看所有已计划查询组", "xpack.osquery.scheduledQueryDetailsPage.editQueryButtonLabel": "编辑", - "xpack.osquery.scheduledQueryGroup.form.advancedSectionToggleButtonLabel": "高级", "xpack.osquery.scheduledQueryGroup.form.agentPolicyFieldLabel": "代理策略", "xpack.osquery.scheduledQueryGroup.form.cancelButtonLabel": "取消", "xpack.osquery.scheduledQueryGroup.form.createSuccessToastMessageText": "已成功计划 {scheduledQueryGroupName}", @@ -20738,7 +20722,6 @@ "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.cannotEditLists": "没有这些权限,将无法创建或编辑值列表。", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.cannotEditRules": "没有该权限,将无法创建或编辑检测引擎规则。", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.essenceDescription": "您需要以下权限,才能完全使用此功能。有关进一步帮助,请联系您的管理员。", - "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.missingFeaturePrivileges": "缺失 {privileges} 权限,无法使用 {index} 功能。{explanation}", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageBody.missingIndexPrivileges": "缺失 {privileges} 权限,无法使用 {index} 索引。{explanation}", "xpack.securitySolution.detectionEngine.missingPrivilegesCallOut.messageTitle": "权限不足", "xpack.securitySolution.detectionEngine.mitreAttack.addSubtechniqueTitle": "添加子技术", @@ -24132,7 +24115,6 @@ "xpack.transform.clone.errorPromptText": "检查源索引模式是否存在时发生错误", "xpack.transform.clone.errorPromptTitle": "获取转换配置时发生错误。", "xpack.transform.clone.fetchErrorPromptText": "无法提取 Kibana 索引模式 ID。", - "xpack.transform.clone.noIndexPatternErrorPromptText": "无法克隆转换。对于 {indexPattern},不存在索引模式。", "xpack.transform.cloneTransform.breadcrumbTitle": "克隆转换", "xpack.transform.createTransform.breadcrumbTitle": "创建转换", "xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage": "删除目标索引 {destinationIndex} 时发生错误", diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana.test.ts index 5de290e325fe..ffac7a14804a 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana.test.ts @@ -22,6 +22,7 @@ describe('Kibana deprecations', () => { describe('With deprecations', () => { const kibanaDeprecationsMockResponse: DomainDeprecationDetails[] = [ { + title: 'mock-deprecation-title', correctiveActions: { manualSteps: ['Step 1', 'Step 2', 'Step 3'], api: { @@ -192,6 +193,7 @@ describe('Kibana deprecations', () => { const kibanaDeprecationsMockResponse: DomainDeprecationDetails[] = [ { domainId, + title: `Failed to fetch deprecations for ${domainId}`, message: `Failed to get deprecations info for plugin "${domainId}".`, level: 'fetch_error', correctiveActions: { diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/mocked_responses.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/mocked_responses.ts index 0bf9f9932b8a..f62bb0a4eeeb 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/mocked_responses.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/overview/review_logs_step/mocked_responses.ts @@ -42,6 +42,7 @@ export const esDeprecationsEmpty: ESUpgradeStatus = { export const kibanaDeprecations: DomainDeprecationDetails[] = [ { + title: 'mock-deprecation-title', correctiveActions: { manualSteps: ['test-step'] }, domainId: 'xpack.spaces', level: 'critical', @@ -49,6 +50,7 @@ export const kibanaDeprecations: DomainDeprecationDetails[] = [ 'Disabling the Spaces plugin (xpack.spaces.enabled) will not be supported in the next major version (8.0)', }, { + title: 'mock-deprecation-title', correctiveActions: { manualSteps: ['test-step'] }, domainId: 'xpack.spaces', level: 'warning', diff --git a/x-pack/plugins/upgrade_assistant/server/lib/kibana_status.test.ts b/x-pack/plugins/upgrade_assistant/server/lib/kibana_status.test.ts index c1bfbf6c4cbf..63532543a418 100644 --- a/x-pack/plugins/upgrade_assistant/server/lib/kibana_status.test.ts +++ b/x-pack/plugins/upgrade_assistant/server/lib/kibana_status.test.ts @@ -13,6 +13,7 @@ import { getKibanaUpgradeStatus } from './kibana_status'; const mockKibanaDeprecations: DomainDeprecationDetails[] = [ { + title: 'mock-deprecation-title', correctiveActions: { manualSteps: [ 'Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.', diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts b/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts index 722b1625f023..fd90c53dac12 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/formatters.ts @@ -15,7 +15,7 @@ export const browserFormatters: BrowserFormatMap = { [ConfigKeys.SOURCE_ZIP_USERNAME]: null, [ConfigKeys.SOURCE_ZIP_PASSWORD]: null, [ConfigKeys.SOURCE_ZIP_FOLDER]: null, - [ConfigKeys.SOURCE_INLINE]: null, + [ConfigKeys.SOURCE_INLINE]: (fields) => JSON.stringify(fields[ConfigKeys.SOURCE_INLINE]), [ConfigKeys.PARAMS]: null, [ConfigKeys.SCREENSHOTS]: null, [ConfigKeys.SYNTHETICS_ARGS]: (fields) => diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts index 2b742a188782..53bbf611d490 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/normalizers.ts @@ -10,7 +10,7 @@ import { Normalizer, commonNormalizers, getNormalizer, - getJsonToArrayOrObjectNormalizer, + getJsonToJavascriptNormalizer, } from '../common/normalizers'; import { defaultBrowserSimpleFields, defaultBrowserAdvancedFields } from '../contexts'; @@ -26,8 +26,8 @@ export const getBrowserNormalizer = (key: ConfigKeys) => { return getNormalizer(key, defaultBrowserFields); }; -export const getBrowserJsonToArrayOrObjectNormalizer = (key: ConfigKeys) => { - return getJsonToArrayOrObjectNormalizer(key, defaultBrowserFields); +export const getBrowserJsonToJavascriptNormalizer = (key: ConfigKeys) => { + return getJsonToJavascriptNormalizer(key, defaultBrowserFields); }; export const browserNormalizers: BrowserNormalizerMap = { @@ -35,9 +35,9 @@ export const browserNormalizers: BrowserNormalizerMap = { [ConfigKeys.SOURCE_ZIP_USERNAME]: getBrowserNormalizer(ConfigKeys.SOURCE_ZIP_USERNAME), [ConfigKeys.SOURCE_ZIP_PASSWORD]: getBrowserNormalizer(ConfigKeys.SOURCE_ZIP_PASSWORD), [ConfigKeys.SOURCE_ZIP_FOLDER]: getBrowserNormalizer(ConfigKeys.SOURCE_ZIP_FOLDER), - [ConfigKeys.SOURCE_INLINE]: getBrowserNormalizer(ConfigKeys.SOURCE_INLINE), + [ConfigKeys.SOURCE_INLINE]: getBrowserJsonToJavascriptNormalizer(ConfigKeys.SOURCE_INLINE), [ConfigKeys.PARAMS]: getBrowserNormalizer(ConfigKeys.PARAMS), [ConfigKeys.SCREENSHOTS]: getBrowserNormalizer(ConfigKeys.SCREENSHOTS), - [ConfigKeys.SYNTHETICS_ARGS]: getBrowserJsonToArrayOrObjectNormalizer(ConfigKeys.SYNTHETICS_ARGS), + [ConfigKeys.SYNTHETICS_ARGS]: getBrowserJsonToJavascriptNormalizer(ConfigKeys.SYNTHETICS_ARGS), ...commonNormalizers, }; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.test.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.test.ts index 055e829858a1..caeeac915a91 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.test.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { cronToSecondsNormalizer, jsonToArrayOrObjectNormalizer } from './normalizers'; +import { cronToSecondsNormalizer, jsonToJavascriptNormalizer } from './normalizers'; describe('normalizers', () => { describe('cronToSecondsNormalizer', () => { @@ -14,15 +14,15 @@ describe('normalizers', () => { }); }); - describe('jsonToArrayOrObjectNormalizer', () => { + describe('jsonToJavascriptNormalizer', () => { it('takes a json object string and returns an object', () => { - expect(jsonToArrayOrObjectNormalizer('{\n "key": "value"\n}')).toEqual({ + expect(jsonToJavascriptNormalizer('{\n "key": "value"\n}')).toEqual({ key: 'value', }); }); it('takes a json array string and returns an array', () => { - expect(jsonToArrayOrObjectNormalizer('["tag1","tag2"]')).toEqual(['tag1', 'tag2']); + expect(jsonToJavascriptNormalizer('["tag1","tag2"]')).toEqual(['tag1', 'tag2']); }); }); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts index 69121ca4bd70..57c4904d711c 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts @@ -23,19 +23,19 @@ export type CommonNormalizerMap = Record value ? value.slice(0, value.length - 1) : null; -export const jsonToArrayOrObjectNormalizer = (value: string) => (value ? JSON.parse(value) : null); +export const jsonToJavascriptNormalizer = (value: string) => (value ? JSON.parse(value) : null); export function getNormalizer(key: string, defaultValues: Fields): Normalizer { return (fields: NewPackagePolicyInput['vars']) => fields?.[key]?.value ?? defaultValues[key as keyof Fields]; } -export function getJsonToArrayOrObjectNormalizer( +export function getJsonToJavascriptNormalizer( key: string, defaultValues: Fields ): Normalizer { return (fields: NewPackagePolicyInput['vars']) => - jsonToArrayOrObjectNormalizer(fields?.[key]?.value) ?? defaultValues[key as keyof Fields]; + jsonToJavascriptNormalizer(fields?.[key]?.value) ?? defaultValues[key as keyof Fields]; } export function getCronNormalizer(key: string, defaultValues: Fields): Normalizer { @@ -47,8 +47,8 @@ export const getCommonNormalizer = (key: ConfigKeys) => { return getNormalizer(key, commonDefaultValues); }; -export const getCommonJsonToArrayOrObjectNormalizer = (key: ConfigKeys) => { - return getJsonToArrayOrObjectNormalizer(key, commonDefaultValues); +export const getCommonjsonToJavascriptNormalizer = (key: ConfigKeys) => { + return getJsonToJavascriptNormalizer(key, commonDefaultValues); }; export const getCommonCronToSecondsNormalizer = (key: ConfigKeys) => { @@ -74,6 +74,6 @@ export const commonNormalizers: CommonNormalizerMap = { } }, [ConfigKeys.APM_SERVICE_NAME]: getCommonNormalizer(ConfigKeys.APM_SERVICE_NAME), - [ConfigKeys.TAGS]: getCommonJsonToArrayOrObjectNormalizer(ConfigKeys.TAGS), + [ConfigKeys.TAGS]: getCommonjsonToJavascriptNormalizer(ConfigKeys.TAGS), [ConfigKeys.TIMEOUT]: getCommonCronToSecondsNormalizer(ConfigKeys.TIMEOUT), }; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/http/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/http/normalizers.ts index 10c52c295c9c..ca86fd5bdc35 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/http/normalizers.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/http/normalizers.ts @@ -10,7 +10,7 @@ import { Normalizer, commonNormalizers, getNormalizer, - getJsonToArrayOrObjectNormalizer, + getJsonToJavascriptNormalizer, } from '../common/normalizers'; import { tlsNormalizers } from '../tls/normalizers'; import { defaultHTTPSimpleFields, defaultHTTPAdvancedFields } from '../contexts'; @@ -26,8 +26,8 @@ export const getHTTPNormalizer = (key: ConfigKeys) => { return getNormalizer(key, defaultHTTPValues); }; -export const getHTTPJsonToArrayOrObjectNormalizer = (key: ConfigKeys) => { - return getJsonToArrayOrObjectNormalizer(key, defaultHTTPValues); +export const getHTTPJsonToJavascriptNormalizer = (key: ConfigKeys) => { + return getJsonToJavascriptNormalizer(key, defaultHTTPValues); }; export const httpNormalizers: HTTPNormalizerMap = { @@ -36,18 +36,18 @@ export const httpNormalizers: HTTPNormalizerMap = { [ConfigKeys.USERNAME]: getHTTPNormalizer(ConfigKeys.USERNAME), [ConfigKeys.PASSWORD]: getHTTPNormalizer(ConfigKeys.PASSWORD), [ConfigKeys.PROXY_URL]: getHTTPNormalizer(ConfigKeys.PROXY_URL), - [ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]: getHTTPJsonToArrayOrObjectNormalizer( + [ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE]: getHTTPJsonToJavascriptNormalizer( ConfigKeys.RESPONSE_BODY_CHECK_NEGATIVE ), - [ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]: getHTTPJsonToArrayOrObjectNormalizer( + [ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE]: getHTTPJsonToJavascriptNormalizer( ConfigKeys.RESPONSE_BODY_CHECK_POSITIVE ), [ConfigKeys.RESPONSE_BODY_INDEX]: getHTTPNormalizer(ConfigKeys.RESPONSE_BODY_INDEX), - [ConfigKeys.RESPONSE_HEADERS_CHECK]: getHTTPJsonToArrayOrObjectNormalizer( + [ConfigKeys.RESPONSE_HEADERS_CHECK]: getHTTPJsonToJavascriptNormalizer( ConfigKeys.RESPONSE_HEADERS_CHECK ), [ConfigKeys.RESPONSE_HEADERS_INDEX]: getHTTPNormalizer(ConfigKeys.RESPONSE_HEADERS_INDEX), - [ConfigKeys.RESPONSE_STATUS_CHECK]: getHTTPJsonToArrayOrObjectNormalizer( + [ConfigKeys.RESPONSE_STATUS_CHECK]: getHTTPJsonToJavascriptNormalizer( ConfigKeys.RESPONSE_STATUS_CHECK ), [ConfigKeys.REQUEST_BODY_CHECK]: (fields) => { @@ -76,7 +76,7 @@ export const httpNormalizers: HTTPNormalizerMap = { return defaultHTTPAdvancedFields[ConfigKeys.REQUEST_BODY_CHECK]; } }, - [ConfigKeys.REQUEST_HEADERS_CHECK]: getHTTPJsonToArrayOrObjectNormalizer( + [ConfigKeys.REQUEST_HEADERS_CHECK]: getHTTPJsonToJavascriptNormalizer( ConfigKeys.REQUEST_HEADERS_CHECK ), [ConfigKeys.REQUEST_METHOD_CHECK]: getHTTPNormalizer(ConfigKeys.REQUEST_METHOD_CHECK), diff --git a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx index d57a69860311..05d45da8d38a 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/use_update_policy.test.tsx @@ -655,7 +655,9 @@ describe('useBarChartsHooks', () => { config[ConfigKeys.SOURCE_ZIP_PASSWORD] ); expect(vars?.[ConfigKeys.SOURCE_ZIP_URL].value).toEqual(config[ConfigKeys.SOURCE_ZIP_URL]); - expect(vars?.[ConfigKeys.SOURCE_INLINE].value).toEqual(config[ConfigKeys.SOURCE_INLINE]); + expect(vars?.[ConfigKeys.SOURCE_INLINE].value).toEqual( + JSON.stringify(config[ConfigKeys.SOURCE_INLINE]) + ); expect(vars?.[ConfigKeys.SOURCE_ZIP_PASSWORD].value).toEqual( config[ConfigKeys.SOURCE_ZIP_PASSWORD] ); diff --git a/x-pack/test/accessibility/apps/kibana_overview.ts b/x-pack/test/accessibility/apps/kibana_overview.ts index 9d21f08a900c..9f5d91e5b4d5 100644 --- a/x-pack/test/accessibility/apps/kibana_overview.ts +++ b/x-pack/test/accessibility/apps/kibana_overview.ts @@ -11,7 +11,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const PageObjects = getPageObjects(['common', 'home']); const a11y = getService('a11y'); - describe('Kibana overview', () => { + // FLAKY: https://github.com/elastic/kibana/issues/98463 + describe.skip('Kibana overview', () => { const esArchiver = getService('esArchiver'); before(async () => { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts index 7bc335389859..e84eaf2cea04 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/alerting/rbac_legacy.ts @@ -10,6 +10,7 @@ import { UserAtSpaceScenarios, Superuser } from '../../scenarios'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; import { ESTestIndexTool, getUrlPrefix, ObjectRemover, AlertUtils } from '../../../common/lib'; import { setupSpacesAndUsers } from '..'; +import { SavedObjectsUtils } from '../../../../../../src/core/server/saved_objects'; // eslint-disable-next-line import/no-default-export export default function alertTests({ getService }: FtrProviderContext) { @@ -20,13 +21,38 @@ export default function alertTests({ getService }: FtrProviderContext) { const supertestWithoutAuth = getService('supertestWithoutAuth'); const esTestIndexTool = new ESTestIndexTool(es, retry); - const MIGRATED_ACTION_ID = '17f38826-5a8d-4a76-975a-b496e7fffe0b'; + const MIGRATED_ACTION_ID = SavedObjectsUtils.getConvertedObjectId( + 'space1', + 'action', + '17f38826-5a8d-4a76-975a-b496e7fffe0b' + ); + const MIGRATED_ALERT_ID: Record = { - space_1_all_alerts_none_actions: '6ee9630a-a20e-44af-9465-217a3717d2ab', - space_1_all_with_restricted_fixture: '5cc59319-74ee-4edc-8646-a79ea91067cd', - space_1_all: 'd41a6abb-b93b-46df-a80a-926221ea847c', - global_read: '362e362b-a137-4aa2-9434-43e3d0d84a34', - superuser: 'b384be60-ec53-4b26-857e-0253ee55b277', + space_1_all_alerts_none_actions: SavedObjectsUtils.getConvertedObjectId( + 'space1', + 'alert', + '6ee9630a-a20e-44af-9465-217a3717d2ab' + ), + space_1_all_with_restricted_fixture: SavedObjectsUtils.getConvertedObjectId( + 'space1', + 'alert', + '5cc59319-74ee-4edc-8646-a79ea91067cd' + ), + space_1_all: SavedObjectsUtils.getConvertedObjectId( + 'space1', + 'alert', + 'd41a6abb-b93b-46df-a80a-926221ea847c' + ), + global_read: SavedObjectsUtils.getConvertedObjectId( + 'space1', + 'alert', + '362e362b-a137-4aa2-9434-43e3d0d84a34' + ), + superuser: SavedObjectsUtils.getConvertedObjectId( + 'space1', + 'alert', + 'b384be60-ec53-4b26-857e-0253ee55b277' + ), }; describe('alerts', () => { diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts index 99a12dc3437d..f45ad28e2cdc 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/create.ts @@ -193,7 +193,7 @@ export default function createAlertTests({ getService }: FtrProviderContext) { const esResponse = await es.get>({ index: '.kibana', - id: `${Spaces.space1.id}:alert:${response.body.id}`, + id: `alert:${response.body.id}`, }); expect(esResponse.statusCode).to.eql(200); const rawActions = (esResponse.body._source as any)?.alert.actions ?? []; diff --git a/x-pack/test/api_integration/apis/maps/get_indexes_matching_pattern.js b/x-pack/test/api_integration/apis/maps/get_indexes_matching_pattern.js index 4129fdd02b72..0ff491d8fd67 100644 --- a/x-pack/test/api_integration/apis/maps/get_indexes_matching_pattern.js +++ b/x-pack/test/api_integration/apis/maps/get_indexes_matching_pattern.js @@ -13,7 +13,7 @@ export default function ({ getService }) { describe('get matching index patterns', () => { it('should return an array containing indexes matching pattern', async () => { const resp = await supertest - .get(`/api/maps/getMatchingIndexes/geo_shapes`) + .get(`/api/maps/getMatchingIndexes?indexPattern=geo_shapes`) .set('kbn-xsrf', 'kibana') .send() .expect(200); @@ -24,12 +24,13 @@ export default function ({ getService }) { it('should return an empty array when no indexes match pattern', async () => { const resp = await supertest - .get(`/api/maps/getMatchingIndexes/notAnIndex`) + .get(`/api/maps/getMatchingIndexes?indexPattern=notAnIndex`) .set('kbn-xsrf', 'kibana') .send() .expect(200); - expect(resp.body.success).to.be(false); + expect(resp.body.success).to.be(true); + expect(resp.body.matchingIndexes.length).to.be(0); }); }); } diff --git a/x-pack/test/api_integration/apis/security/privileges.ts b/x-pack/test/api_integration/apis/security/privileges.ts index 40a451ffb5cf..bbb0fc60cb3c 100644 --- a/x-pack/test/api_integration/apis/security/privileges.ts +++ b/x-pack/test/api_integration/apis/security/privileges.ts @@ -32,16 +32,7 @@ export default function ({ getService }: FtrProviderContext) { actions: ['all', 'read'], stackAlerts: ['all', 'read'], ml: ['all', 'read'], - siem: [ - 'all', - 'read', - 'minimal_all', - 'minimal_read', - 'cases_all', - 'cases_read', - 'alerts_all', - 'alerts_read', - ], + siem: ['all', 'read', 'minimal_all', 'minimal_read', 'cases_all', 'cases_read'], observabilityCases: ['all', 'read'], uptime: ['all', 'read'], infrastructure: ['all', 'read'], diff --git a/x-pack/test/apm_api_integration/tests/inspect/inspect.ts b/x-pack/test/apm_api_integration/tests/inspect/inspect.ts index 77ceedaeb68b..c2a4dfb77d0e 100644 --- a/x-pack/test/apm_api_integration/tests/inspect/inspect.ts +++ b/x-pack/test/apm_api_integration/tests/inspect/inspect.ts @@ -53,11 +53,13 @@ export default function customLinksTests({ getService }: FtrProviderContext) { // @ts-expect-error expect(Object.keys(body._inspect[0])).to.eql([ - 'operationName', + 'id', + 'json', + 'name', 'response', - 'duration', - 'requestType', - 'requestParams', + 'startTime', + 'stats', + 'status', ]); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts index 8eacd4231a92..4748e39cd3a4 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_index.ts @@ -82,15 +82,15 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.eql({ message: 'index for this space does not exist', status_code: 404 }); }); - it('should be able to create a signal index when it has not been created yet', async () => { + it('should NOT be able to create a signal index when it has not been created yet. Should return a 403 and error that the user is unauthorized', async () => { const { body } = await supertestWithoutAuth .post(DETECTION_ENGINE_INDEX_URL) .set('kbn-xsrf', 'true') .auth(role, 'changeme') .send() - .expect(200); - - expect(body).to.eql({ acknowledged: true }); + .expect(403); + expect(body.message).to.match(/^security_exception/); + expect(body.status_code).to.eql(403); }); it('should be able to read the index name and status as not being outdated', async () => { @@ -103,7 +103,7 @@ export default ({ getService }: FtrProviderContext) => { .send() .expect(200); expect(body).to.eql({ - index_mapping_outdated: false, + index_mapping_outdated: null, name: `${DEFAULT_SIGNALS_INDEX}-default`, }); }); @@ -129,15 +129,15 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.eql({ message: 'index for this space does not exist', status_code: 404 }); }); - it('should be able to create a signal index when it has not been created yet.', async () => { + it('should NOT be able to create a signal index when it has not been created yet. Should return a 403 and error that the user is unauthorized', async () => { const { body } = await supertestWithoutAuth .post(DETECTION_ENGINE_INDEX_URL) .set('kbn-xsrf', 'true') .auth(role, 'changeme') .send() - .expect(200); - - expect(body).to.eql({ acknowledged: true }); + .expect(403); + expect(body.message).to.match(/^security_exception/); + expect(body.status_code).to.eql(403); }); it('should be able to read the index name and status as not being outdated', async () => { @@ -150,7 +150,7 @@ export default ({ getService }: FtrProviderContext) => { .send() .expect(200); expect(body).to.eql({ - index_mapping_outdated: false, + index_mapping_outdated: null, name: `${DEFAULT_SIGNALS_INDEX}-default`, }); }); @@ -226,15 +226,15 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.eql({ message: 'index for this space does not exist', status_code: 404 }); }); - it('should be able to create a signal index when it has not been created yet', async () => { + it('should NOT be able to create a signal index when it has not been created yet. Should return a 403 and error that the user is unauthorized', async () => { const { body } = await supertestWithoutAuth .post(DETECTION_ENGINE_INDEX_URL) .set('kbn-xsrf', 'true') .auth(role, 'changeme') .send() - .expect(200); - - expect(body).to.eql({ acknowledged: true }); + .expect(403); + expect(body.message).to.match(/^security_exception/); + expect(body.status_code).to.eql(403); }); it('should be able to read the index name and status as not being outdated', async () => { @@ -272,16 +272,16 @@ export default ({ getService }: FtrProviderContext) => { .expect(404); expect(body).to.eql({ message: 'index for this space does not exist', status_code: 404 }); }); - // here - it('should be able to create a signal index when it has not been created yet', async () => { + + it('should NOT be able to create a signal index when it has not been created yet. Should return a 403 and error that the user is unauthorized', async () => { const { body } = await supertestWithoutAuth .post(DETECTION_ENGINE_INDEX_URL) .set('kbn-xsrf', 'true') .auth(role, 'changeme') .send() - .expect(200); - - expect(body).to.eql({ acknowledged: true }); + .expect(403); + expect(body.message).to.match(/^security_exception/); + expect(body.status_code).to.eql(403); }); it('should be able to read the index name and status as not being outdated', async () => { @@ -294,7 +294,7 @@ export default ({ getService }: FtrProviderContext) => { .send() .expect(200); expect(body).to.eql({ - index_mapping_outdated: false, + index_mapping_outdated: null, name: `${DEFAULT_SIGNALS_INDEX}-default`, }); }); @@ -370,14 +370,15 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.eql({ message: 'index for this space does not exist', status_code: 404 }); }); - it('should be able to create a signal index when it has not been created yet', async () => { + it('should NOT be able to create a signal index when it has not been created yet. Should return a 401 unauthorized', async () => { const { body } = await supertestWithoutAuth .post(DETECTION_ENGINE_INDEX_URL) .set('kbn-xsrf', 'true') .auth(role, 'changeme') .send() - .expect(200); - expect(body).to.eql({ acknowledged: true }); + .expect(403); + expect(body.message).to.match(/^security_exception/); + expect(body.status_code).to.eql(403); }); it('should be able to read the index name and status as being outdated.', async () => { @@ -416,14 +417,15 @@ export default ({ getService }: FtrProviderContext) => { expect(body).to.eql({ message: 'index for this space does not exist', status_code: 404 }); }); - it('should be able to create a signal index when it has not been created yet', async () => { + it('should NOT be able to create a signal index when it has not been created yet. Should return a 401 unauthorized', async () => { const { body } = await supertestWithoutAuth .post(DETECTION_ENGINE_INDEX_URL) .set('kbn-xsrf', 'true') .auth(role, 'changeme') .send() - .expect(200); - expect(body).to.eql({ acknowledged: true }); + .expect(403); + expect(body.message).to.match(/^security_exception/); + expect(body.status_code).to.eql(403); }); it('should be able to read the index name and status as being outdated.', async () => { diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/agent/stream/stream.yml.hbs b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/agent/stream/stream.yml.hbs new file mode 100644 index 000000000000..2870385f21f9 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/agent/stream/stream.yml.hbs @@ -0,0 +1 @@ +config.version: "2" diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/fields/fields.yml new file mode 100644 index 000000000000..6e003ed0ad14 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/manifest.yml new file mode 100644 index 000000000000..461d4fa94170 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/data_stream/test_stream/manifest.yml @@ -0,0 +1,4 @@ +title: Test stream +type: logs +streams: + - input: test_input diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/docs/README.md b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/docs/README.md new file mode 100644 index 000000000000..0b9b18421c9d --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing automated upgrades for package policies diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/manifest.yml new file mode 100644 index 000000000000..346ea4d2bcfa --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.7.0-add-stream-with-no-vars/manifest.yml @@ -0,0 +1,23 @@ +format_version: 1.0.0 +name: package_policy_upgrade +title: Tests package policy upgrades +description: This is a test package for upgrading package policies +version: 0.7.0-add-stream-with-no-vars +categories: [] +release: beta +type: integration +license: basic +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' +policy_templates: + - name: package_policy_upgrade_new + title: Package Policy Upgrade New + description: Test Package for Upgrading Package Policies + inputs: + - type: test_input + title: Test Input + description: Test Input + enabled: true diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/agent/stream/stream.yml.hbs b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/agent/stream/stream.yml.hbs new file mode 100644 index 000000000000..2870385f21f9 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/agent/stream/stream.yml.hbs @@ -0,0 +1 @@ +config.version: "2" diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/fields/fields.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/fields/fields.yml new file mode 100644 index 000000000000..6e003ed0ad14 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/fields/fields.yml @@ -0,0 +1,16 @@ +- name: data_stream.type + type: constant_keyword + description: > + Data stream type. +- name: data_stream.dataset + type: constant_keyword + description: > + Data stream dataset. +- name: data_stream.namespace + type: constant_keyword + description: > + Data stream namespace. +- name: '@timestamp' + type: date + description: > + Event timestamp. diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/manifest.yml new file mode 100644 index 000000000000..8b8ea1987ccc --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/data_stream/test_stream/manifest.yml @@ -0,0 +1,17 @@ +title: Test stream +type: logs +streams: + - input: test_input + vars: + - name: test_var_new + type: text + title: Test Var New + default: Test Var New + required: true + show_user: true + - name: test_var_new_2 + type: text + title: Test Var New 2 + default: Test Var New 2 + required: true + show_user: true diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/docs/README.md b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/docs/README.md new file mode 100644 index 000000000000..0b9b18421c9d --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/docs/README.md @@ -0,0 +1,3 @@ +# Test package + +This is a test package for testing automated upgrades for package policies diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/manifest.yml new file mode 100644 index 000000000000..bd61453fdaac --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/package_policy_upgrade/0.8.0-add-vars-to-stream-with-no-vars/manifest.yml @@ -0,0 +1,23 @@ +format_version: 1.0.0 +name: package_policy_upgrade +title: Tests package policy upgrades +description: This is a test package for upgrading package policies +version: 0.8.0-add-vars-to-stream-with-no-vars +categories: [] +release: beta +type: integration +license: basic +requirement: + elasticsearch: + versions: '>7.7.0' + kibana: + versions: '>7.7.0' +policy_templates: + - name: package_policy_upgrade_new + title: Package Policy Upgrade New + description: Test Package for Upgrading Package Policies + inputs: + - type: test_input + title: Test Input + description: Test Input + enabled: true diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts index e75bcfaf7514..3a7d6f5d6b19 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/upgrade.ts @@ -803,6 +803,126 @@ export default function (providerContext: FtrProviderContext) { }); }); + describe('when upgrading to a version where an input with no variables has variables added', function () { + withTestPackageVersion('0.8.0-add-vars-to-stream-with-no-vars'); + + beforeEach(async function () { + const { body: agentPolicyResponse } = await supertest + .post(`/api/fleet/agent_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'Test policy', + namespace: 'default', + }) + .expect(200); + + agentPolicyId = agentPolicyResponse.item.id; + + const { body: packagePolicyResponse } = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'package_policy_upgrade_1', + description: '', + namespace: 'default', + policy_id: agentPolicyId, + enabled: true, + output_id: '', + inputs: [ + { + policy_template: 'package_policy_upgrade', + type: 'test_input', + enabled: true, + streams: [ + { + id: 'test-package_policy_upgrade-xxxx', + enabled: true, + data_stream: { + type: 'test_stream', + dataset: 'package_policy_upgrade.test_stream', + }, + }, + ], + }, + ], + package: { + name: 'package_policy_upgrade', + title: 'This is a test package for upgrading package policies', + version: '0.7.0-add-stream-with-no-vars', + }, + }); + + packagePolicyId = packagePolicyResponse.item.id; + }); + + afterEach(async function () { + await supertest + .post(`/api/fleet/package_policies/delete`) + .set('kbn-xsrf', 'xxxx') + .send({ packagePolicyIds: [packagePolicyId] }) + .expect(200); + + await supertest + .post('/api/fleet/agent_policies/delete') + .set('kbn-xsrf', 'xxxx') + .send({ agentPolicyId }) + .expect(200); + }); + + describe('dry run', function () { + it('returns a valid diff', async function () { + const { body }: { body: UpgradePackagePolicyDryRunResponse } = await supertest + .post(`/api/fleet/package_policies/upgrade`) + .set('kbn-xsrf', 'xxxx') + .send({ + packagePolicyIds: [packagePolicyId], + dryRun: true, + }) + .expect(200); + + expect(body[0].hasErrors).to.be(false); + + const oldInput = body[0].diff?.[0].inputs.find((input) => input.type === 'test_input'); + const oldStream = oldInput?.streams.find( + (stream) => stream.data_stream.dataset === 'package_policy_upgrade.test_stream' + ); + + expect(oldStream?.vars).to.be(undefined); + + const newInput = body[0].diff?.[1].inputs.find((input) => input.type === 'test_input'); + const newStream = newInput?.streams.find( + (stream) => stream.data_stream.dataset === 'package_policy_upgrade.test_stream' + ); + + expect(newStream?.vars).to.eql({ + test_var_new: { + value: 'Test Var New', + type: 'text', + }, + test_var_new_2: { + value: 'Test Var New 2', + type: 'text', + }, + }); + }); + }); + + describe('upgrade', function () { + it('successfully upgrades package policy', async function () { + const { body }: { body: UpgradePackagePolicyResponse } = await supertest + .post(`/api/fleet/package_policies/upgrade`) + .set('kbn-xsrf', 'xxxx') + .send({ + packagePolicyIds: [packagePolicyId], + dryRun: false, + }) + .expect(200); + + expect(body[0].success).to.be(true); + }); + }); + }); + describe('when package policy is not found', function () { it('should return an 200 with errors when "dryRun:true" is provided', async function () { const { body }: { body: UpgradePackagePolicyDryRunResponse } = await supertest diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts index 3542abf9ea86..c245b4591749 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_spaces.ts @@ -176,6 +176,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await PageObjects.common.navigateToUrl('discover', '', { basePath: '/s/custom_space_no_index_patterns', ensureCurrentUrl: false, + shouldUseHashForSubUrl: false, }); await testSubjects.existOrFail('homeApp', { timeout: config.get('timeouts.waitFor') }); }); diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts index ff6103f16e49..8a1021abb433 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts @@ -38,7 +38,8 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await esArchiver.unload('x-pack/test/functional/es_archives/logstash_functional'); }); - describe('global timelion all privileges', () => { + // FLAKY: https://github.com/elastic/kibana/issues/110396 + describe.skip('global timelion all privileges', () => { before(async () => { await security.role.create('global_timelion_all_role', { elasticsearch: { diff --git a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts index d50943fad991..5f74b2da213b 100644 --- a/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts +++ b/x-pack/test/functional/apps/transform/permissions/full_transform_access.ts @@ -158,10 +158,7 @@ export default function ({ getService }: FtrProviderContext) { 'should have the retention policy inputs enabled' ); await transform.editFlyout.openTransformEditAccordionRetentionPolicySettings(); - await transform.editFlyout.assertTransformEditFlyoutInputEnabled( - 'RetentionPolicyField', - true - ); + await transform.editFlyout.assertTransformEditFlyoutRetentionPolicySelectEnabled(true); await transform.editFlyout.assertTransformEditFlyoutInputEnabled( 'RetentionPolicyMaxAge', true diff --git a/x-pack/test/functional/es_archives/task_manager_tasks/data.json b/x-pack/test/functional/es_archives/task_manager_tasks/data.json new file mode 100644 index 000000000000..b59abd341a7a --- /dev/null +++ b/x-pack/test/functional/es_archives/task_manager_tasks/data.json @@ -0,0 +1,61 @@ +{ + "type": "doc", + "value": { + "id": "task:be7e1250-3322-11eb-94c1-db6995e84f6a", + "index": ".kibana_task_manager_1", + "source": { + "migrationVersion": { + "task": "7.16.0" + }, + "references": [ + ], + "task": { + "attempts": 0, + "params": "{\"spaceId\":\"user1\",\"alertId\":\"0359d7fcc04da9878ee9aadbda38ba55\"}", + "retryAt": "2020-11-30T15:43:39.626Z", + "runAt": "2020-11-30T15:43:08.277Z", + "scheduledAt": "2020-11-30T15:43:08.277Z", + "scope": [ + "testing" + ], + "startedAt": null, + "state": "{}", + "status": "idle", + "taskType": "alerting:0359d7fcc04da9878ee9aadbda38ba55" + }, + "type": "task", + "updated_at": "2020-11-30T15:43:08.277Z" + } + } +} + +{ + "type": "doc", + "value": { + "id": "task:be7e1250-3322-11eb-94c1-db6995e8389f", + "index": ".kibana_task_manager_1", + "source": { + "migrationVersion": { + "task": "7.16.0" + }, + "references": [ + ], + "task": { + "attempts": 0, + "params": "{\"spaceId\":\"user1\",\"actionTaskParamsId\":\"6e96ac5e648f57523879661ea72525b7\"}", + "retryAt": "2020-11-30T15:43:39.626Z", + "runAt": "2020-11-30T15:43:08.277Z", + "scheduledAt": "2020-11-30T15:43:08.277Z", + "scope": [ + "testing" + ], + "startedAt": null, + "state": "{}", + "status": "idle", + "taskType": "actions:6e96ac5e648f57523879661ea72525b7" + }, + "type": "task", + "updated_at": "2020-11-30T15:43:08.277Z" + } + } +} \ No newline at end of file diff --git a/x-pack/test/functional/es_archives/task_manager_tasks/mappings.json b/x-pack/test/functional/es_archives/task_manager_tasks/mappings.json new file mode 100644 index 000000000000..6ec81326d1ca --- /dev/null +++ b/x-pack/test/functional/es_archives/task_manager_tasks/mappings.json @@ -0,0 +1,225 @@ +{ + "type": "index", + "value": { + "aliases": { + ".kibana": { + } + }, + "index": ".kibana_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "action": "6e96ac5e648f57523879661ea72525b7", + "action_task_params": "a9d49f184ee89641044be0ca2950fa3a", + "alert": "0359d7fcc04da9878ee9aadbda38ba55", + "api_key_pending_invalidation": "16f515278a295f6245149ad7c5ddedb7", + "apm-indices": "9bb9b2bf1fa636ed8619cbab5ce6a1dd", + "apm-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "app_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_daily": "43b8830d5d0df85a6823d290885fc9fd", + "application_usage_totals": "3d1b76c39bfb2cc8296b024d73854724", + "application_usage_transactional": "3d1b76c39bfb2cc8296b024d73854724", + "search-session": "721df406dbb7e35ac22e4df6c3ad2b2a", + "canvas-element": "7390014e1091044523666d97247392fc", + "canvas-workpad": "b0a1706d356228dbdcb4a17e6b9eb231", + "canvas-workpad-template": "ae2673f678281e2c055d764b153e9715", + "cases": "477f214ff61acc3af26a7b7818e380c1", + "cases-comments": "8a50736330e953bca91747723a319593", + "cases-configure": "387c5f3a3bda7e0ae0dd4e106f914a69", + "cases-user-actions": "32277330ec6b721abe3b846cfd939a71", + "config": "c63748b75f39d0c54de12d12c1ccbc20", + "dashboard": "40554caf09725935e2c02e02563a2d07", + "endpoint:user-artifact": "4a11183eee21e6fbad864f7a30b39ad0", + "endpoint:user-artifact-manifest": "a0d7b04ad405eed54d76e279c3727862", + "enterprise_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "epm-packages": "2b83397e3eaaaa8ef15e38813f3721c3", + "event_log_test": "bef808d4a9c27f204ffbda3359233931", + "exception-list": "67f055ab8c10abd7b2ebfd969b836788", + "exception-list-agnostic": "67f055ab8c10abd7b2ebfd969b836788", + "file-upload-telemetry": "0ed4d3e1983d1217a30982630897092e", + "fleet-agent-actions": "9511b565b1cc6441a42033db3d5de8e9", + "fleet-agent-events": "e20a508b6e805189356be381dbfac8db", + "fleet-agents": "cb661e8ede2b640c42c8e5ef99db0683", + "fleet-enrollment-api-keys": "a69ef7ae661dab31561d6c6f052ef2a7", + "graph-workspace": "cd7ba1330e6682e9cc00b78850874be1", + "index-pattern": "45915a1ad866812242df474eb0479052", + "infrastructure-ui-source": "3d1b76c39bfb2cc8296b024d73854724", + "ingest-agent-policies": "8b0733cce189659593659dad8db426f0", + "ingest-outputs": "8854f34453a47e26f86a29f8f3b80b4e", + "ingest-package-policies": "c91ca97b1ff700f0fc64dc6b13d65a85", + "ingest_manager_settings": "02a03095f0e05b7a538fa801b88a217f", + "inventory-view": "3d1b76c39bfb2cc8296b024d73854724", + "kql-telemetry": "d12a98a6f19a2d273696597547e064ee", + "lens": "52346cfec69ff7b47d5f0c12361a2797", + "lens-ui-telemetry": "509bfa5978586998e05f9e303c07a327", + "map": "4a05b35c3a3a58fbc72dd0202dc3487f", + "maps-telemetry": "5ef305b18111b77789afefbd36b66171", + "metrics-explorer-view": "3d1b76c39bfb2cc8296b024d73854724", + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "ml-job": "3bb64c31915acf93fc724af137a0891b", + "ml-telemetry": "257fd1d4b4fdbb9cb4b8a3b27da201e9", + "monitoring-telemetry": "2669d5ec15e82391cf58df4294ee9c68", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "query": "11aaeb7f5f7fa5bb43f25e18ce26e7d9", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "sample-data-telemetry": "7d3cfeb915303c9641c59681967ffeb4", + "search": "43012c7ebc4cb57054e0a490e4b43023", + "search-telemetry": "3d1b76c39bfb2cc8296b024d73854724", + "siem-detection-engine-rule-actions": "6569b288c169539db10cb262bf79de18", + "siem-detection-engine-rule-status": "ae783f41c6937db6b7a2ef5c93a9e9b0", + "siem-ui-timeline": "d12c5474364d737d17252acf1dc4585c", + "siem-ui-timeline-note": "8874706eedc49059d4cf0f5094559084", + "siem-ui-timeline-pinned-event": "20638091112f0e14f0e443d512301c29", + "space": "c5ca8acafa0beaa4d08d014a97b6bc6b", + "tag": "83d55da58f6530f7055415717ec06474", + "telemetry": "36a616f7026dfa617d6655df850fe16d", + "timelion-sheet": "9a2a2748877c7a7b582fef201ab1d4cf", + "tsvb-validation-telemetry": "3a37ef6c8700ae6fc97d5c7da00e9215", + "type": "2f4316de49999235636386fe51dc06c1", + "ui-metric": "0d409297dc5ebe1e3a1da691c6ee32e3", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0", + "upgrade-assistant-reindex-operation": "215107c281839ea9b3ad5f6419819763", + "upgrade-assistant-telemetry": "56702cec857e0a9dacfb696655b4ff7b", + "uptime-dynamic-settings": "3d1b76c39bfb2cc8296b024d73854724", + "url": "c7f66a0df8b1b52f17c28c4adb111105", + "visualization": "f819cf6636b75c9e76ba733a0c6ef355", + "workplace_search_telemetry": "3d1b76c39bfb2cc8296b024d73854724" + } + }, + "dynamic": "strict", + "properties": { + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} + +{ + "type": "index", + "value": { + "aliases": { + ".kibana_task_manager": { + } + }, + "index": ".kibana_task_manager_1", + "mappings": { + "_meta": { + "migrationMappingPropertyHashes": { + "migrationVersion": "4a1746014a75ade3a714e1db5763276f", + "namespace": "2f4316de49999235636386fe51dc06c1", + "namespaces": "2f4316de49999235636386fe51dc06c1", + "originId": "2f4316de49999235636386fe51dc06c1", + "references": "7997cf5a56cc02bdc9c93361bde732b0", + "task": "235412e52d09e7165fac8a67a43ad6b4", + "type": "2f4316de49999235636386fe51dc06c1", + "updated_at": "00da57df13e94e9d98437d13ace4bfe0" + } + }, + "dynamic": "strict", + "properties": { + "migrationVersion": { + "dynamic": "true", + "properties": { + "task": { + "fields": { + "keyword": { + "ignore_above": 256, + "type": "keyword" + } + }, + "type": "text" + } + } + }, + "references": { + "properties": { + "id": { + "type": "keyword" + }, + "name": { + "type": "keyword" + }, + "type": { + "type": "keyword" + } + }, + "type": "nested" + }, + "task": { + "properties": { + "attempts": { + "type": "integer" + }, + "ownerId": { + "type": "keyword" + }, + "params": { + "type": "text" + }, + "retryAt": { + "type": "date" + }, + "runAt": { + "type": "date" + }, + "schedule": { + "properties": { + "interval": { + "type": "keyword" + } + } + }, + "scheduledAt": { + "type": "date" + }, + "scope": { + "type": "keyword" + }, + "startedAt": { + "type": "date" + }, + "state": { + "type": "text" + }, + "status": { + "type": "keyword" + }, + "taskType": { + "type": "keyword" + }, + "user": { + "type": "keyword" + } + } + }, + "type": { + "type": "keyword" + }, + "updated_at": { + "type": "date" + } + } + }, + "settings": { + "index": { + "auto_expand_replicas": "0-1", + "number_of_replicas": "0", + "number_of_shards": "1" + } + } + } +} diff --git a/x-pack/test/functional/services/transform/edit_flyout.ts b/x-pack/test/functional/services/transform/edit_flyout.ts index fcb87fc9bec5..cc230e2c38fc 100644 --- a/x-pack/test/functional/services/transform/edit_flyout.ts +++ b/x-pack/test/functional/services/transform/edit_flyout.ts @@ -37,6 +37,21 @@ export function TransformEditFlyoutProvider({ getService }: FtrProviderContext) ); }, + async assertTransformEditFlyoutRetentionPolicySelectEnabled(expectedValue: boolean) { + await testSubjects.existOrFail(`transformEditFlyoutRetentionPolicyFieldSelect`, { + timeout: 1000, + }); + const isEnabled = await testSubjects.isEnabled( + `transformEditFlyoutRetentionPolicyFieldSelect` + ); + expect(isEnabled).to.eql( + expectedValue, + `Expected 'transformEditFlyoutRetentionPolicyFieldSelect' input to be '${ + expectedValue ? 'enabled' : 'disabled' + }' (got '${isEnabled ? 'enabled' : 'disabled'}')` + ); + }, + async assertTransformEditFlyoutInputEnabled(input: string, expectedValue: boolean) { await testSubjects.existOrFail(`transformEditFlyout${input}Input`, { timeout: 1000 }); const isEnabled = await testSubjects.isEnabled(`transformEditFlyout${input}Input`); diff --git a/x-pack/test/functional_enterprise_search/services/app_search_client.ts b/x-pack/test/functional_enterprise_search/services/app_search_client.ts index 8e829b97e9dd..457523bccf8c 100644 --- a/x-pack/test/functional_enterprise_search/services/app_search_client.ts +++ b/x-pack/test/functional_enterprise_search/services/app_search_client.ts @@ -105,14 +105,20 @@ const search = async (engineName: string): Promise => { // Since the App Search API does not issue document receipts, the only way to tell whether or not documents // are fully indexed is to poll the search endpoint. export const waitForIndexedDocs = (engineName: string) => { - return new Promise(async function (resolve) { - let isReady = false; - while (!isReady) { - const response = await search(engineName); - if (response.results && response.results.length > 0) { - isReady = true; - resolve(); + return new Promise(async function (resolve, reject) { + try { + let isReady = false; + + while (!isReady) { + const response = await search(engineName); + + if (response.results && response.results.length > 0) { + isReady = true; + resolve(); + } } + } catch (error) { + reject(error); } }); }; diff --git a/x-pack/test/lists_api_integration/utils.ts b/x-pack/test/lists_api_integration/utils.ts index 8975feb6fbe0..b3816ad7563b 100644 --- a/x-pack/test/lists_api_integration/utils.ts +++ b/x-pack/test/lists_api_integration/utils.ts @@ -116,21 +116,29 @@ export const waitFor = async ( timeoutWait: number = 10 ) => { await new Promise(async (resolve, reject) => { - let found = false; - let numberOfTries = 0; - while (!found && numberOfTries < Math.floor(maxTimeout / timeoutWait)) { - const itPasses = await functionToTest(); - if (itPasses) { - found = true; + try { + let found = false; + let numberOfTries = 0; + + while (!found && numberOfTries < Math.floor(maxTimeout / timeoutWait)) { + const itPasses = await functionToTest(); + + if (itPasses) { + found = true; + } else { + numberOfTries++; + } + + await new Promise((resolveTimeout) => setTimeout(resolveTimeout, timeoutWait)); + } + + if (found) { + resolve(); } else { - numberOfTries++; + reject(new Error(`timed out waiting for function ${functionName} condition to be true`)); } - await new Promise((resolveTimeout) => setTimeout(resolveTimeout, timeoutWait)); - } - if (found) { - resolve(); - } else { - reject(new Error(`timed out waiting for function ${functionName} condition to be true`)); + } catch (error) { + reject(error); } }); }; diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts index bab34312c363..b1de32fdcc93 100644 --- a/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/index.ts @@ -13,5 +13,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./health_route')); loadTestFile(require.resolve('./task_management')); loadTestFile(require.resolve('./task_management_removed_types')); + + loadTestFile(require.resolve('./migrations')); }); } diff --git a/x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts b/x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts new file mode 100644 index 000000000000..caf62a1d364c --- /dev/null +++ b/x-pack/test/plugin_api_integration/test_suites/task_manager/migrations.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { ApiResponse, estypes } from '@elastic/elasticsearch'; +import { TaskInstanceWithDeprecatedFields } from '../../../../plugins/task_manager/server/task'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { SavedObjectsUtils } from '../../../../../src/core/server/saved_objects'; + +export default function createGetTests({ getService }: FtrProviderContext) { + const es = getService('es'); + const esArchiver = getService('esArchiver'); + const ALERT_ID = '0359d7fcc04da9878ee9aadbda38ba55'; + const ACTION_TASK_PARAMS_ID = '6e96ac5e648f57523879661ea72525b7'; + + describe('migrations', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/task_manager_tasks'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/task_manager_tasks'); + }); + + it('8.0.0 migrates actions tasks with legacy id to saved object ids', async () => { + // NOTE: We hae to use elastic search directly against the ".kibana" index because alerts do not expose the references which we want to test exists + const response = await es.get<{ task: TaskInstanceWithDeprecatedFields }>({ + index: '.kibana_task_manager', + id: 'task:be7e1250-3322-11eb-94c1-db6995e84f6a', + }); + expect(response.statusCode).to.eql(200); + expect(response.body._source?.task.params).to.eql( + `{"spaceId":"user1","alertId":"${SavedObjectsUtils.getConvertedObjectId( + 'user1', + 'alert', + ALERT_ID + )}"}` + ); + }); + + it('8.0.0 migrates actions tasks from legacy id to saved object ids', async () => { + const searchResult: ApiResponse< + estypes.SearchResponse<{ task: TaskInstanceWithDeprecatedFields }> + > = await es.search({ + index: '.kibana_task_manager', + body: { + query: { + term: { + _id: 'task:be7e1250-3322-11eb-94c1-db6995e8389f', + }, + }, + }, + }); + expect(searchResult.statusCode).to.equal(200); + expect((searchResult.body.hits.total as estypes.SearchTotalHits).value).to.equal(1); + const hit = searchResult.body.hits.hits[0]; + expect(hit!._source!.task.params!).to.equal( + `{"spaceId":"user1","actionTaskParamsId":"${SavedObjectsUtils.getConvertedObjectId( + 'user1', + 'action_task_params', + ACTION_TASK_PARAMS_ID + )}"}` + ); + }); + }); +} diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts b/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts index fd49e2b23721..b312ba676927 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/ilm_migration_apis.ts @@ -20,7 +20,8 @@ export default function ({ getService }: FtrProviderContext) { const reportingAPI = getService('reportingAPI'); const security = getService('security'); - describe('ILM policy migration APIs', () => { + // FAILING ES PROMOTION: https://github.com/elastic/kibana/issues/110483 + describe.skip('ILM policy migration APIs', () => { before(async () => { await esArchiver.load('x-pack/test/functional/es_archives/reporting/logs'); await esArchiver.load('x-pack/test/functional/es_archives/logstash_functional'); diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts index ba046a081b6d..06398fdcd965 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/policy_details.ts @@ -21,8 +21,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const policyTestResources = getService('policyTestResources'); - // FLAKY: https://github.com/elastic/kibana/issues/100296 - describe.skip('When on the Endpoint Policy Details Page', function () { + describe('When on the Endpoint Policy Details Page', function () { describe('with an invalid policy id', () => { it('should display an error', async () => { await pageObjects.policy.navigateToPolicyDetails('invalid-id'); diff --git a/yarn.lock b/yarn.lock index fd917bdccfd1..80c66de7e355 100644 --- a/yarn.lock +++ b/yarn.lock @@ -110,6 +110,15 @@ jsesc "^2.5.1" source-map "^0.5.0" +"@babel/generator@^7.12.11": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.14.5.tgz#848d7b9f031caca9d0cd0af01b063f226f52d785" + integrity sha512-y3rlP+/G25OIX3mYKKIOlQRcqj7YgrvHxOLbVmyLJ9bPmi5ttvUmpydVjcFjZphOktWuA7ovbx91ECloWTfjIA== + dependencies: + "@babel/types" "^7.14.5" + jsesc "^2.5.1" + source-map "^0.5.0" + "@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.10.4", "@babel/helper-annotate-as-pure@^7.12.10": version "7.12.10" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.12.10.tgz#54ab9b000e60a93644ce17b3f37d313aaf1d115d" @@ -300,6 +309,11 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.12.11.tgz#c9a1f021917dcb5ccf0d4e453e399022981fc9ed" integrity sha512-np/lG3uARFybkoHokJUmf1QfEvRVCPbmQeUQpKow5cQ3xWrV9i3rUHodKDJPQfTVX61qKi+UdYk8kik84n7XOw== +"@babel/helper-validator-identifier@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.5.tgz#d0f0e277c512e0c938277faa85a3968c9a44c0e8" + integrity sha512-5lsetuxCLilmVGyiLEfoHBRX8UCFD+1m2x3Rj97WrW3V7H3u4RWRXA4evMjImCsin2J2YT0QaVDGf+z8ondbAg== + "@babel/helper-validator-option@^7.12.1", "@babel/helper-validator-option@^7.12.11": version "7.12.11" resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.11.tgz#d66cb8b7a3e7fe4c6962b32020a131ecf0847f4f" @@ -1217,6 +1231,14 @@ lodash "^4.17.19" to-fast-properties "^2.0.0" +"@babel/types@^7.14.5": + version "7.14.5" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.14.5.tgz#3bb997ba829a2104cedb20689c4a5b8121d383ff" + integrity sha512-M/NzBpEL95I5Hh4dwhin5JlE7EzO5PHMAuzjxss3tiOBD46KfQvVedN/3jEPZvdRvtsK2222XfdHogNIttFgcg== + dependencies: + "@babel/helper-validator-identifier" "^7.14.5" + to-fast-properties "^2.0.0" + "@base2/pretty-print-object@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@base2/pretty-print-object/-/pretty-print-object-1.0.0.tgz#860ce718b0b73f4009e153541faff2cb6b85d047" @@ -1432,10 +1454,10 @@ dependencies: "@elastic/ecs-helpers" "^1.1.0" -"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@^8.0.0-canary.18": - version "8.0.0-canary.18" - resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.0.0-canary.18.tgz#d17e3c74809079c7272bb5ee0f8f96c16d723bae" - integrity sha512-YxFeoOWLWRMOLLSv1Zp5TyGe6hwc6FQtrLZT1TjsXucm9LVnxQSpg9tUPnYUfUNDwoY3FZwthUxH3+gb3Lqedw== +"@elastic/elasticsearch@npm:@elastic/elasticsearch-canary@^8.0.0-canary.19": + version "8.0.0-canary.19" + resolved "https://registry.yarnpkg.com/@elastic/elasticsearch-canary/-/elasticsearch-canary-8.0.0-canary.19.tgz#07e5f57a361d38b3eb747564a278d3d3bbf4882d" + integrity sha512-LVtnCPTC6bdI/r5Yh4TGVa0frzyoHBhQz3q72qvnmlkXW8pHlvaDQ46+46M017CERZxTK8BBYb1/TYs5puWRWQ== dependencies: debug "^4.3.1" hpagent "^0.1.1" @@ -4617,6 +4639,11 @@ dependencies: "@babel/runtime" "^7.12.5" +"@tootallnate/once@1": + version "1.1.2" + resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" + integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== + "@ts-morph/common@~0.7.0": version "0.7.3" resolved "https://registry.yarnpkg.com/@ts-morph/common/-/common-0.7.3.tgz#380020c278e4aa6cecedf362a1157591d1003267" @@ -5524,13 +5551,6 @@ resolved "https://registry.yarnpkg.com/@types/lz-string/-/lz-string-1.3.34.tgz#69bfadde419314b4a374bf2c8e58659c035ed0a5" integrity sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow== -"@types/mapbox-gl@1.13.1": - version "1.13.1" - resolved "https://registry.yarnpkg.com/@types/mapbox-gl/-/mapbox-gl-1.13.1.tgz#bd8108f912f32c895117e2970b6d4fbbecbe42a1" - integrity sha512-Yqv1eFAzG2gdecc94higNC8KE+BR6t8QhFgbQGGEpKr3OgSVVtr2qaBNBPaGlIAtCoKDF6JGB2haOhvijYC4Bg== - dependencies: - "@types/geojson" "*" - "@types/markdown-it@^0.0.7": version "0.0.7" resolved "https://registry.yarnpkg.com/@types/markdown-it/-/markdown-it-0.0.7.tgz#75070485a3d8ad11e7deb8287f4430be15bf4d39" @@ -6437,6 +6457,11 @@ resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.14.1.tgz#b3d2eb91dafd0fd8b3fce7c61512ac66bd0364aa" integrity sha512-SkhzHdI/AllAgQSxXM89XwS1Tkic7csPdndUuTKabEwRcEfR8uQ/iPA3Dgio1rqsV3jtqZhY0QQni8rLswJM2w== +"@typescript-eslint/types@4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.28.3.tgz#8fffd436a3bada422c2c1da56060a0566a9506c7" + integrity sha512-kQFaEsQBQVtA9VGVyciyTbIg7S3WoKHNuOp/UF5RG40900KtGqfoiETWD/v0lzRXc+euVE9NXmfer9dLkUJrkA== + "@typescript-eslint/types@4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.3.0.tgz#1f0b2d5e140543e2614f06d48fb3ae95193c6ddf" @@ -6470,6 +6495,19 @@ semver "^7.3.2" tsutils "^3.17.1" +"@typescript-eslint/typescript-estree@^4.14.1": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.28.3.tgz#253d7088100b2a38aefe3c8dd7bd1f8232ec46fb" + integrity sha512-YAb1JED41kJsqCQt1NcnX5ZdTA93vKFCMP4lQYG6CFxd0VzDJcKttRlMrlG+1qiWAw8+zowmHU1H0OzjWJzR2w== + dependencies: + "@typescript-eslint/types" "4.28.3" + "@typescript-eslint/visitor-keys" "4.28.3" + debug "^4.3.1" + globby "^11.0.3" + is-glob "^4.0.1" + semver "^7.3.5" + tsutils "^3.21.0" + "@typescript-eslint/visitor-keys@4.14.1": version "4.14.1" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.14.1.tgz#e93c2ff27f47ee477a929b970ca89d60a117da91" @@ -6478,6 +6516,14 @@ "@typescript-eslint/types" "4.14.1" eslint-visitor-keys "^2.0.0" +"@typescript-eslint/visitor-keys@4.28.3": + version "4.28.3" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.28.3.tgz#26ac91e84b23529968361045829da80a4e5251c4" + integrity sha512-ri1OzcLnk1HH4gORmr1dllxDzzrN6goUIz/P4MHFV0YZJDCADPR3RvYNp0PW2SetKTThar6wlbFTL00hV2Q+fg== + dependencies: + "@typescript-eslint/types" "4.28.3" + eslint-visitor-keys "^2.0.0" + "@typescript-eslint/visitor-keys@4.3.0": version "4.3.0" resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.3.0.tgz#0e5ab0a09552903edeae205982e8521e17635ae0" @@ -6788,6 +6834,13 @@ agent-base@6: dependencies: debug "4" +agent-base@^6.0.2: + version "6.0.2" + resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77" + integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ== + dependencies: + debug "4" + agentkeepalive@^3.4.1: version "3.4.1" resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-3.4.1.tgz#aa95aebc3a749bca5ed53e3880a09f5235b48f0c" @@ -6795,6 +6848,15 @@ agentkeepalive@^3.4.1: dependencies: humanize-ms "^1.2.1" +agentkeepalive@^4.1.3: + version "4.1.4" + resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.1.4.tgz#d928028a4862cb11718e55227872e842a44c945b" + integrity sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ== + dependencies: + debug "^4.1.0" + depd "^1.1.2" + humanize-ms "^1.2.1" + aggregate-error@^3.0.0: version "3.0.1" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.0.1.tgz#db2fe7246e536f40d9b5442a39e117d7dd6a24e0" @@ -7309,11 +7371,6 @@ array-filter@^1.0.0: resolved "https://registry.yarnpkg.com/array-filter/-/array-filter-1.0.0.tgz#baf79e62e6ef4c2a4c0b831232daffec251f9d83" integrity sha1-uveeYubvTCpMC4MSMtr/7CUfnYM= -array-find-index@^1.0.1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/array-find-index/-/array-find-index-1.0.2.tgz#df010aa1287e164bbda6f9723b0a96a1ec4187a1" - integrity sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E= - array-find@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/array-find/-/array-find-1.0.0.tgz#6c8e286d11ed768327f8e62ecee87353ca3e78b8" @@ -8402,13 +8459,6 @@ blob-util@2.0.2: resolved "https://registry.yarnpkg.com/blob-util/-/blob-util-2.0.2.tgz#3b4e3c281111bb7f11128518006cdc60b403a1eb" integrity sha512-T7JQa+zsXXEa6/8ZhHcQEW1UFfVM49Ts65uBkFL6fz2QmrElqmbajIDJvuA0tEhRe5eIjpV9ZF+0RfZR9voJFQ== -block-stream@*: - version "0.0.9" - resolved "https://registry.yarnpkg.com/block-stream/-/block-stream-0.0.9.tgz#13ebfe778a03205cfe03751481ebb4b3300c126a" - integrity sha1-E+v+d4oDIFz+A3UUgeu0szAMEmo= - dependencies: - inherits "~2.0.0" - bluebird-retry@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/bluebird-retry/-/bluebird-retry-0.11.0.tgz#1289ab22cbbc3a02587baad35595351dd0c1c047" @@ -9177,14 +9227,6 @@ camelcase-css@2.0.1: resolved "https://registry.yarnpkg.com/camelcase-css/-/camelcase-css-2.0.1.tgz#ee978f6947914cc30c6b44741b6ed1df7f043fd5" integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA== -camelcase-keys@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-2.1.0.tgz#308beeaffdf28119051efa1d932213c91b8f92e7" - integrity sha1-MIvur/3ygRkFHvodkyITyRuPkuc= - dependencies: - camelcase "^2.0.0" - map-obj "^1.0.0" - camelcase-keys@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" @@ -9199,7 +9241,7 @@ camelcase@^1.0.2: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-1.2.1.tgz#9bb5304d2e0b56698b2c758b08a3eaa9daa58a39" integrity sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk= -camelcase@^2.0.0, camelcase@^2.0.1: +camelcase@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= @@ -9710,15 +9752,6 @@ clone-buffer@^1.0.0: resolved "https://registry.yarnpkg.com/clone-buffer/-/clone-buffer-1.0.0.tgz#e3e25b207ac4e701af721e2cb5a16792cac3dc58" integrity sha1-4+JbIHrE5wGvch4staFnksrD3Fg= -clone-deep@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" - integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== - dependencies: - is-plain-object "^2.0.4" - kind-of "^6.0.2" - shallow-clone "^3.0.0" - clone-regexp@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/clone-regexp/-/clone-regexp-2.2.0.tgz#7d65e00885cd8796405c35a737e7a86b7429e36f" @@ -10507,7 +10540,7 @@ cross-env@^6.0.3: dependencies: cross-spawn "^7.0.0" -cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2: +cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -10516,14 +10549,6 @@ cross-spawn@7.0.3, cross-spawn@^7.0.0, cross-spawn@^7.0.2: shebang-command "^2.0.0" which "^2.0.1" -cross-spawn@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-3.0.1.tgz#1256037ecb9f0c5f79e3d6ef135e30770184b982" - integrity sha1-ElYDfsufDF9549bvE14wdwGEuYI= - dependencies: - lru-cache "^4.0.1" - which "^1.2.9" - cross-spawn@^6.0.0, cross-spawn@^6.0.5: version "6.0.5" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" @@ -10925,13 +10950,6 @@ cucumber@^4.2.1: util-arity "^1.0.2" verror "^1.9.0" -currently-unhandled@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/currently-unhandled/-/currently-unhandled-0.4.1.tgz#988df33feab191ef799a61369dd76c17adf957ea" - integrity sha1-mI3zP+qxke95mmE2nddsF635V+o= - dependencies: - array-find-index "^1.0.1" - cwise-compiler@^1.0.0, cwise-compiler@^1.1.2: version "1.1.3" resolved "https://registry.yarnpkg.com/cwise-compiler/-/cwise-compiler-1.1.3.tgz#f4d667410e850d3a313a7d2db7b1e505bb034cc5" @@ -11491,7 +11509,7 @@ decamelize-keys@^1.1.0: decamelize "^1.1.0" map-obj "^1.0.0" -decamelize@^1.0.0, decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: +decamelize@^1.0.0, decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -11762,7 +11780,7 @@ delete-empty@^2.0.0: relative "^3.0.2" rimraf "^2.6.2" -depd@~1.1.2: +depd@^1.1.2, depd@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= @@ -12513,6 +12531,13 @@ encodeurl@~1.0.2: resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= +encoding@^0.1.12: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s= + dependencies: + iconv-lite "~0.4.13" + end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1, end-of-stream@^1.4.4: version "1.4.4" resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0" @@ -12640,6 +12665,11 @@ enzyme@^3.11.0: rst-selector-parser "^2.2.3" string.prototype.trim "^1.2.1" +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== + errlop@^1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/errlop/-/errlop-1.1.2.tgz#a99a48f37aa264d614e342ffdbbaa49eec9220e0" @@ -13116,6 +13146,11 @@ eslint-scope@^5.0.0: esrecurse "^4.1.0" estraverse "^4.1.1" +eslint-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/eslint-traverse/-/eslint-traverse-1.0.0.tgz#108d360a171a6e6334e1af0cee905a93bd0dcc53" + integrity sha512-bSp37rQs93LF8rZ409EI369DGCI4tELbFVmFNxI6QbuveS7VRxYVyUhwDafKN/enMyUh88HQQ7ZoGUHtPuGdcw== + eslint-utils@^1.4.3: version "1.4.3" resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.3.tgz#74fec7c54d0776b6f67e0251040b5806564e981f" @@ -14379,16 +14414,6 @@ fsevents@^2.1.2, fsevents@~2.1.2: resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.2.tgz#4c0a1fb34bc68e543b4b82a9ec392bfbda840805" integrity sha512-R4wDiBwZ0KzpgOWetKDug1FZcYhqYnUYKtfZYt4mD5SBz76q0KR4Q9o7GIPamsVPGmW3EYPPJ0dOOjvx32ldZA== -fstream@^1.0.0, fstream@^1.0.12: - version "1.0.12" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" - integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== - dependencies: - graceful-fs "^4.1.2" - inherits "~2.0.0" - mkdirp ">=0.5 0" - rimraf "2" - fsu@^1.0.2: version "1.1.1" resolved "https://registry.yarnpkg.com/fsu/-/fsu-1.1.1.tgz#bd36d3579907c59d85b257a75b836aa9e0c31834" @@ -15017,11 +15042,16 @@ got@^9.6.0: to-readable-stream "^1.0.0" url-parse-lax "^3.0.0" -graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.3, graceful-fs@^4.2.4: +graceful-fs@4.X, graceful-fs@^4.0.0, graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.4, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.0, graceful-fs@^4.2.2, graceful-fs@^4.2.4: version "4.2.4" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +graceful-fs@^4.2.3, graceful-fs@^4.2.6: + version "4.2.6" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== + graphlib@^2.1.8: version "2.1.8" resolved "https://registry.yarnpkg.com/graphlib/-/graphlib-2.1.8.tgz#5761d414737870084c92ec7b5dbcb0592c9d35da" @@ -15804,7 +15834,7 @@ htmlparser2@~3.3.0: domutils "1.1" readable-stream "1.0" -http-cache-semantics@^4.0.0: +http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== @@ -15855,6 +15885,15 @@ http-proxy-agent@^2.1.0: agent-base "4" debug "3.1.0" +http-proxy-agent@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" + integrity sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg== + dependencies: + "@tootallnate/once" "1" + agent-base "6" + debug "4" + http-proxy-middleware@0.19.1: version "0.19.1" resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a" @@ -16102,18 +16141,6 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= -in-publish@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/in-publish/-/in-publish-2.0.0.tgz#e20ff5e3a2afc2690320b6dc552682a9c7fadf51" - integrity sha1-4g/146KvwmkDILbcVSaCqcf631E= - -indent-string@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" - integrity sha1-ji1INIdCEhtKghi3oTfppSBJ3IA= - dependencies: - repeating "^2.0.0" - indent-string@^3.0.0, indent-string@^3.1.0, indent-string@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" @@ -16147,7 +16174,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -16234,10 +16261,10 @@ insert-module-globals@^7.0.0: undeclared-identifiers "^1.1.2" xtend "^4.0.0" -install-artifact-from-github@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.0.2.tgz#e1e478dd29880b9112ecd684a84029603e234a9d" - integrity sha512-yuMFBSVIP3vD0SDBGUqeIpgOAIlFx8eQFknQObpkYEM5gsl9hy6R9Ms3aV+Vw9MMyYsoPMeex0XDnfgY7uzc+Q== +install-artifact-from-github@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/install-artifact-from-github/-/install-artifact-from-github-1.2.0.tgz#adcbd123c16a4337ec44ea76d0ebf253cc16b074" + integrity sha512-3OxCPcY55XlVM3kkfIpeCgmoSKnMsz2A3Dbhsq0RXpIknKQmrX1YiznCeW9cD2ItFmDxziA3w6Eg8d80AoL3oA== internal-ip@^4.3.0: version "4.3.0" @@ -16649,6 +16676,11 @@ is-interactive@^1.0.0: resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= + is-map@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" @@ -18237,6 +18269,11 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +klona@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.4.tgz#7bb1e3affb0cb8624547ef7e8f6708ea2e39dfc0" + integrity sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA== + known-css-properties@^0.20.0: version "0.20.0" resolved "https://registry.yarnpkg.com/known-css-properties/-/known-css-properties-0.20.0.tgz#0570831661b47dd835293218381166090ff60e96" @@ -18926,14 +18963,6 @@ loose-envify@^1.0.0, loose-envify@^1.1.0, loose-envify@^1.2.0, loose-envify@^1.3 dependencies: js-tokens "^3.0.0 || ^4.0.0" -loud-rejection@^1.0.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/loud-rejection/-/loud-rejection-1.6.0.tgz#5b46f80147edee578870f086d04821cf998e551f" - integrity sha1-W0b4AUft7leIcPCG0Eghz5mOVR8= - dependencies: - currently-unhandled "^0.4.1" - signal-exit "^3.0.0" - lower-case@^1.1.1: version "1.1.4" resolved "https://registry.yarnpkg.com/lower-case/-/lower-case-1.1.4.tgz#9a2cabd1b9e8e0ae993a4bf7d5875c39c42e8eac" @@ -18964,7 +18993,7 @@ lowlight@^1.14.0: fault "^1.0.0" highlight.js "~10.4.0" -lru-cache@^4.0.0, lru-cache@^4.0.1, lru-cache@^4.1.5: +lru-cache@^4.0.0, 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== @@ -19025,6 +19054,27 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0: dependencies: semver "^6.0.0" +make-fetch-happen@^8.0.14: + version "8.0.14" + resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz#aaba73ae0ab5586ad8eaa68bd83332669393e222" + integrity sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ== + dependencies: + agentkeepalive "^4.1.3" + cacache "^15.0.5" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + promise-retry "^2.0.1" + socks-proxy-agent "^5.0.0" + ssri "^8.0.0" + make-iterator@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/make-iterator/-/make-iterator-1.0.0.tgz#57bef5dc85d23923ba23767324d8e8f8f3d9694b" @@ -19044,7 +19094,7 @@ map-cache@^0.2.0, map-cache@^0.2.2: resolved "https://registry.yarnpkg.com/map-cache/-/map-cache-0.2.2.tgz#c32abd0bd6525d9b051645bb4f26ac5dc98a0dbf" integrity sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8= -map-obj@^1.0.0, map-obj@^1.0.1: +map-obj@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= @@ -19071,10 +19121,15 @@ mapbox-gl-draw-rectangle-mode@1.0.4: resolved "https://registry.yarnpkg.com/mapbox-gl-draw-rectangle-mode/-/mapbox-gl-draw-rectangle-mode-1.0.4.tgz#42987d68872a5fb5cc5d76d3375ee20cd8bab8f7" integrity sha512-BdF6nwEK2p8n9LQoMPzBO8LhddW1fe+d5vK8HQIei+4VcRnUbKNsEj7Z15FsJxCHzsc2BQKXbESx5GaE8x0imQ== -mapbox-gl@1.13.1: - version "1.13.1" - resolved "https://registry.yarnpkg.com/mapbox-gl/-/mapbox-gl-1.13.1.tgz#322efe75ab4c764fc4c776da1506aad58d5a5b9d" - integrity sha512-GSyubcoSF5MyaP8z+DasLu5v7KmDK2pp4S5+VQ5WdVQUOaAqQY4jwl4JpcdNho3uWm2bIKs7x1l7q3ynGmW60g== +mapcap@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/mapcap/-/mapcap-1.0.0.tgz#e8e29d04a160eaf8c92ec4bcbd2c5d07ed037e5a" + integrity sha512-KcNlZSlFPx+r1jYZmxEbTVymG+dIctf10WmWkuhrhrblM+KMoF77HelwihL5cxYlORye79KoR4IlOOk99lUJ0g== + +maplibre-gl@1.15.2: + version "1.15.2" + resolved "https://registry.yarnpkg.com/maplibre-gl/-/maplibre-gl-1.15.2.tgz#7fb47868b62455af916c090903f2154394450f9c" + integrity sha512-uPeV530apb4JfX3cRFfE+awFnbcJTOnCv2QvY4mw4huiInbybElWYkNzTs324YLSADq0f4bidRoYcR81ho3aLA== dependencies: "@mapbox/geojson-rewind" "^0.5.0" "@mapbox/geojson-types" "^1.0.2" @@ -19100,11 +19155,6 @@ mapbox-gl@1.13.1: tinyqueue "^2.0.3" vt-pbf "^3.1.1" -mapcap@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/mapcap/-/mapcap-1.0.0.tgz#e8e29d04a160eaf8c92ec4bcbd2c5d07ed037e5a" - integrity sha512-KcNlZSlFPx+r1jYZmxEbTVymG+dIctf10WmWkuhrhrblM+KMoF77HelwihL5cxYlORye79KoR4IlOOk99lUJ0g== - marge@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/marge/-/marge-1.0.1.tgz#52d6026911e62e1dd1cf60a07313dde285a8370c" @@ -19352,22 +19402,6 @@ memory-fs@^0.5.0: errno "^0.1.3" readable-stream "^2.0.1" -meow@^3.7.0: - version "3.7.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-3.7.0.tgz#72cb668b425228290abbfa856892587308a801fb" - integrity sha1-cstmi0JSKCkKu/qFaJJYcwioAfs= - dependencies: - camelcase-keys "^2.0.0" - decamelize "^1.1.2" - loud-rejection "^1.0.0" - map-obj "^1.0.1" - minimist "^1.1.3" - normalize-package-data "^2.3.4" - object-assign "^4.0.1" - read-pkg-up "^1.0.1" - redent "^1.0.0" - trim-newlines "^1.0.0" - meow@^6.1.1: version "6.1.1" resolved "https://registry.yarnpkg.com/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467" @@ -19421,6 +19455,24 @@ meow@^8.0.0: type-fest "^0.18.0" yargs-parser "^20.2.3" +meow@^9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-9.0.0.tgz#cd9510bc5cac9dee7d03c73ee1f9ad959f4ea364" + integrity sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize "^1.2.0" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^3.0.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.18.0" + yargs-parser "^20.2.3" + merge-descriptors@1.0.1, merge-descriptors@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -19615,6 +19667,17 @@ minipass-collect@^1.0.2: dependencies: minipass "^3.0.0" +minipass-fetch@^1.3.2: + version "1.3.4" + resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-1.3.4.tgz#63f5af868a38746ca7b33b03393ddf8c291244fe" + integrity sha512-TielGogIzbUEtd1LsjZFs47RWuHHfhl6TiCx1InVxApBAmQ8bL0dL5ilkLGcRvuyW/A9nE+Lvn855Ewz8S0PnQ== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + minipass-flush@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" @@ -19629,6 +19692,20 @@ minipass-pipeline@^1.2.2: dependencies: minipass "^3.0.0" +minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + minipass@^2.2.1, minipass@^2.8.6, minipass@^2.9.0: version "2.9.0" resolved "https://registry.yarnpkg.com/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" @@ -19644,6 +19721,13 @@ minipass@^3.0.0, minipass@^3.1.1: dependencies: yallist "^4.0.0" +minipass@^3.1.0, minipass@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + minizlib@^1.2.1: version "1.3.3" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" @@ -19651,6 +19735,14 @@ minizlib@^1.2.1: dependencies: minipass "^2.9.0" +minizlib@^2.0.0, minizlib@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" + integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== + dependencies: + minipass "^3.0.0" + yallist "^4.0.0" + minizlib@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.0.tgz#fd52c645301ef09a63a2c209697c294c6ce02cf3" @@ -19695,18 +19787,18 @@ mkdirp@0.5.1: dependencies: minimist "0.0.8" -"mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.0, mkdirp@~0.5.1: +mkdirp@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" + integrity sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc= + +mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.4, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.4.tgz#fd01504a6797ec5c9be81ff43d204961ed64a512" integrity sha512-iG9AK/dJLtJ0XNgTuDbSyNS3zECqDlAhnQW4CsNxBG3LQJBbHmRX1egw39DmtOdCAqY+dKXV+sgPgilNWUKMVw== dependencies: minimist "^1.2.5" -mkdirp@^0.3.5: - version "0.3.5" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.3.5.tgz#de3e5f8961c88c787ee1368df849ac4413eca8d7" - integrity sha1-3j5fiWHIjHh+4TaN+EmsRBPsqNc= - mkdirp@^1.0.3, mkdirp@^1.0.4, mkdirp@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" @@ -20033,7 +20125,7 @@ mz@^2.4.0: object-assign "^4.0.1" thenify-all "^1.0.0" -nan@^2.13.2, nan@^2.14.0, nan@^2.14.1: +nan@^2.13.2, nan@^2.14.0: version "2.14.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.1.tgz#d7be34dfa3105b91494c3147089315eff8874b01" integrity sha512-isWHgVjnFjh2x2yuJ/tj3JbwoHu3UC2dX5G/88Cm24yB6YopVgxvBObDY7n5xW6ExmFhJpSEQqFPvq9zaXc8Jw== @@ -20148,6 +20240,11 @@ neo-async@^2.5.0, neo-async@^2.6.0, neo-async@^2.6.1: resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.1.tgz#ac27ada66167fa8849a6addd837f6b189ad2081c" integrity sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw== +neo-async@^2.6.2: + version "2.6.2" + resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" + integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== + nested-error-stacks@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-1.0.2.tgz#19f619591519f096769a5ba9a86e6eeec823c3cf" @@ -20250,38 +20347,36 @@ node-gyp-build@^4.2.3: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.2.3.tgz#ce6277f853835f718829efb47db20f3e4d9c4739" integrity sha512-MN6ZpzmfNCRM+3t57PTJHgHyw/h4OWnZ6mR8P5j/uZtqQr46RRuDE/P+g3n0YR/AiYXeWixZZzaip77gdICfRg== -node-gyp@^3.8.0: - version "3.8.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c" - integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA== - dependencies: - fstream "^1.0.0" - glob "^7.0.3" - graceful-fs "^4.1.2" - mkdirp "^0.5.0" - nopt "2 || 3" - npmlog "0 || 1 || 2 || 3 || 4" - osenv "0" - request "^2.87.0" - rimraf "2" - semver "~5.3.0" - tar "^2.0.0" - which "1" - -node-gyp@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.0.0.tgz#2e88425ce84e9b1a4433958ed55d74c70fffb6be" - integrity sha512-ZW34qA3CJSPKDz2SJBHKRvyNQN0yWO5EGKKksJc+jElu9VA468gwJTyTArC1iOXU7rN3Wtfg/CMt/dBAOFIjvg== +node-gyp@^7.1.0: + version "7.1.2" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae" + integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ== dependencies: env-paths "^2.2.0" glob "^7.1.4" graceful-fs "^4.2.3" - nopt "^4.0.3" + nopt "^5.0.0" npmlog "^4.1.2" request "^2.88.2" - rimraf "^2.6.3" + rimraf "^3.0.2" semver "^7.3.2" - tar "^6.0.1" + tar "^6.0.2" + which "^2.0.2" + +node-gyp@^8.0.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-8.2.0.tgz#ef509ccdf5cef3b4d93df0690b90aa55ff8c7977" + integrity sha512-KG8SdcoAnw2d6augGwl1kOayALUrXW/P2uOAm2J2+nmW/HjZo7y+8TDg7LejxbekOOSv3kzhq+NSUYkIDAX8eA== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.6" + make-fetch-happen "^8.0.14" + nopt "^5.0.0" + npmlog "^4.1.2" + rimraf "^3.0.2" + semver "^7.3.5" + tar "^6.1.2" which "^2.0.2" node-int64@^0.4.0: @@ -20388,23 +20483,21 @@ node-releases@^1.1.70: resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.71.tgz#cb1334b179896b1c89ecfdd4b725fb7bbdfc7dbb" integrity sha512-zR6HoT6LrLCRBwukmrVbHv0EpEQjksO6GmFcZQQuCAy139BEsoVKPYnf3jongYW83fAa1torLGYwxxky/p28sg== -node-sass@^4.14.1: - version "4.14.1" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.14.1.tgz#99c87ec2efb7047ed638fb4c9db7f3a42e2217b5" - integrity sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g== +node-sass@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-6.0.1.tgz#cad1ccd0ce63e35c7181f545d8b986f3a9a887fe" + integrity sha512-f+Rbqt92Ful9gX0cGtdYwjTrWAaGURgaK5rZCWOgCNyGWusFYHhbqCCBoFBeat+HKETOU02AyTxNhJV0YZf2jQ== dependencies: async-foreach "^0.1.3" chalk "^1.1.1" - cross-spawn "^3.0.0" + cross-spawn "^7.0.3" gaze "^1.0.0" get-stdin "^4.0.1" glob "^7.0.3" - in-publish "^2.0.0" lodash "^4.17.15" - meow "^3.7.0" - mkdirp "^0.5.1" + meow "^9.0.0" nan "^2.13.2" - node-gyp "^3.8.0" + node-gyp "^7.1.0" npmlog "^4.0.0" request "^2.88.0" sass-graph "2.2.5" @@ -20444,13 +20537,6 @@ nodemon@^2.0.4: undefsafe "^2.0.3" update-notifier "^4.1.0" -"nopt@2 || 3", nopt@~3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" - integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= - dependencies: - abbrev "1" - nopt@^2.2.0: version "2.2.1" resolved "https://registry.yarnpkg.com/nopt/-/nopt-2.2.1.tgz#2aa09b7d1768487b3b89a9c5aa52335bff0baea7" @@ -20458,13 +20544,12 @@ nopt@^2.2.0: dependencies: abbrev "1" -nopt@^4.0.3, nopt@~4.0.1: - version "4.0.3" - resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" - integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== dependencies: abbrev "1" - osenv "^0.1.4" nopt@~1.0.10: version "1.0.10" @@ -20473,7 +20558,22 @@ nopt@~1.0.10: dependencies: abbrev "1" -normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0: +nopt@~3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" + integrity sha1-xkZdvwirzU2zWTF/eaxopkayj/k= + dependencies: + abbrev "1" + +nopt@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/nopt/-/nopt-4.0.3.tgz#a375cad9d02fd921278d954c2254d5aa57e15e48" + integrity sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg== + dependencies: + abbrev "1" + osenv "^0.1.4" + +normalize-package-data@^2.0.0, normalize-package-data@^2.3.2, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -20551,7 +20651,7 @@ npm-run-path@^4.0.0: dependencies: path-key "^3.0.0" -"npmlog@0 || 1 || 2 || 3 || 4", npmlog@^4.0.0, npmlog@^4.1.2: +npmlog@^4.0.0, npmlog@^4.1.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" integrity sha512-2uUqazuKlTaSI/dC8AzicUck7+IrEaOnN/e0jd3Xtt1KcGpwx30v50mL7oPyr/h9bL3E4aZccVwpwP+5W9Vjkg== @@ -20996,7 +21096,7 @@ os-tmpdir@^1.0.0, os-tmpdir@~1.0.1, os-tmpdir@~1.0.2: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= -osenv@0, osenv@^0.1.0, osenv@^0.1.4: +osenv@^0.1.0, osenv@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== @@ -22413,6 +22513,14 @@ promise-polyfill@^8.1.3: resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-8.1.3.tgz#8c99b3cf53f3a91c68226ffde7bde81d7f904116" integrity sha512-MG5r82wBzh7pSKDRa9y+vllNHz3e3d4CNj1PQE4BQYxLme0gKYYBm9YENq+UkEikyZ0XbiGWxYlVw3Rl9O/U8g== +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== + dependencies: + err-code "^2.0.2" + retry "^0.12.0" + promise.allsettled@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.2.tgz#d66f78fbb600e83e863d893e98b3d4376a9c47c9" @@ -22896,14 +23004,14 @@ re-resizable@^6.1.1: dependencies: fast-memoize "^2.5.1" -re2@^1.15.4: - version "1.15.4" - resolved "https://registry.yarnpkg.com/re2/-/re2-1.15.4.tgz#2ffc3e4894fb60430393459978197648be01a0a9" - integrity sha512-7w3K+Daq/JjbX/dz5voMt7B9wlprVBQnMiypyCojAZ99kcAL+3LiJ5uBoX/u47l8eFTVq3Wj+V0pmvU+CT8tOg== +re2@^1.16.0: + version "1.16.0" + resolved "https://registry.yarnpkg.com/re2/-/re2-1.16.0.tgz#f311eb4865b1296123800ea8e013cec8dab25590" + integrity sha512-eizTZL2ZO0ZseLqfD4t3Qd0M3b3Nr0MBWpX81EbPMIud/1d/CSfUIx2GQK8fWiAeHoSekO5EOeFib2udTZLwYw== dependencies: - install-artifact-from-github "^1.0.2" - nan "^2.14.1" - node-gyp "^7.0.0" + install-artifact-from-github "^1.2.0" + nan "^2.14.2" + node-gyp "^8.0.0" react-ace@^7.0.5: version "7.0.5" @@ -23839,14 +23947,6 @@ recursive-readdir@2.2.2: dependencies: minimatch "3.0.4" -redent@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-1.0.0.tgz#cf916ab1fd5f1f16dfb20822dd6ec7f730c2afde" - integrity sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94= - dependencies: - indent-string "^2.1.0" - strip-indent "^1.0.1" - redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -24647,7 +24747,7 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@2.6.3, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: +rimraf@2.6.3, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: version "2.6.3" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== @@ -24839,16 +24939,16 @@ sass-graph@2.2.5: scss-tokenizer "^0.2.3" yargs "^13.3.2" -sass-loader@^8.0.2: - version "8.0.2" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-8.0.2.tgz#debecd8c3ce243c76454f2e8290482150380090d" - integrity sha512-7o4dbSK8/Ol2KflEmSco4jTjQoV988bM82P9CZdmo9hR3RLnvNc0ufMNdMrB0caq38JQ/FgF4/7RcbcfKzxoFQ== +sass-loader@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-10.2.0.tgz#3d64c1590f911013b3fa48a0b22a83d5e1494716" + integrity sha512-kUceLzC1gIHz0zNJPpqRsJyisWatGYNFRmv2CKZK2/ngMJgLqxTbXwe/hJ85luyvZkgqU3VlJ33UVF2T/0g6mw== dependencies: - clone-deep "^4.0.1" - loader-utils "^1.2.3" - neo-async "^2.6.1" - schema-utils "^2.6.1" - semver "^6.3.0" + klona "^2.0.4" + loader-utils "^2.0.0" + neo-async "^2.6.2" + schema-utils "^3.0.0" + semver "^7.3.2" sass-resources-loader@^2.0.1: version "2.0.1" @@ -24917,7 +25017,7 @@ schema-utils@^0.4.5: ajv "^6.1.0" ajv-keywords "^3.1.0" -schema-utils@^2.0.0, schema-utils@^2.0.1, schema-utils@^2.5.0, schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0: +schema-utils@^2.0.0, schema-utils@^2.0.1, schema-utils@^2.5.0, schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.7.0.tgz#17151f76d8eae67fbbf77960c33c676ad9f4efc7" integrity sha512-0ilKFI6QQF5nxDZLFn2dMjvc4hjg/Wkg7rHd3jK6/A4a1Hl9VFdQWvgB1UMGoU94pad1P/8N7fMcEnLnSiju8A== @@ -25214,13 +25314,6 @@ shallow-clone-shim@^2.0.0: resolved "https://registry.yarnpkg.com/shallow-clone-shim/-/shallow-clone-shim-2.0.0.tgz#b62bf55aed79f4c1430ea1dc4d293a193f52cf91" integrity sha512-YRNymdiL3KGOoS67d73TEmk4tdPTO9GSMCoiphQsTcC9EtC+AOmMPjkyBkRoCJfW9ASsaZw1craaiw1dPN2D3Q== -shallow-clone@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" - integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== - dependencies: - kind-of "^6.0.2" - shallow-copy@~0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/shallow-copy/-/shallow-copy-0.0.1.tgz#415f42702d73d810330292cc5ee86eae1a11a170" @@ -25391,6 +25484,11 @@ slide@^1.1.5, slide@~1.1.3: resolved "https://registry.yarnpkg.com/slide/-/slide-1.1.6.tgz#56eb027d65b4d2dce6cb2e2d32c4d4afc9e1d707" integrity sha1-VusCfWW00tzmyy4tMsTUr8nh1wc= +smart-buffer@^4.1.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + snap-shot-compare@2.8.3: version "2.8.3" resolved "https://registry.yarnpkg.com/snap-shot-compare/-/snap-shot-compare-2.8.3.tgz#b4982fb7b4e9cd4fa0b03a40a100b5f005b2d515" @@ -25466,6 +25564,23 @@ sockjs@0.3.20: uuid "^3.4.0" websocket-driver "0.6.5" +socks-proxy-agent@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-5.0.1.tgz#032fb583048a29ebffec2e6a73fca0761f48177e" + integrity sha512-vZdmnjb9a2Tz6WEQVIurybSwElwPxMZaIc7PzqbJTrezcKNznv6giT7J7tZDZ1BojVaa1jvO/UiUdhDVB0ACoQ== + dependencies: + agent-base "^6.0.2" + debug "4" + socks "^2.3.3" + +socks@^2.3.3: + version "2.6.1" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.6.1.tgz#989e6534a07cf337deb1b1c94aaa44296520d30e" + integrity sha512-kLQ9N5ucj8uIcxrDwjm0Jsqk06xdpBjGNQtpXy4Q8/QY2k+fY7nZH8CARy+hkbG+SGAovmzzuauCpBlb8FrnBA== + dependencies: + ip "^1.1.5" + smart-buffer "^4.1.0" + sonic-boom@^1.0.2: version "1.3.0" resolved "https://registry.yarnpkg.com/sonic-boom/-/sonic-boom-1.3.0.tgz#5c77c846ce6c395dddf2eb8e8e65f9cc576f2e76" @@ -26241,13 +26356,6 @@ strip-final-newline@^2.0.0: resolved "https://registry.yarnpkg.com/strip-final-newline/-/strip-final-newline-2.0.0.tgz#89b852fb2fcbe936f6f4b3187afb0a12c1ab58ad" integrity sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA== -strip-indent@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-1.0.1.tgz#0c7962a6adefa7bbd4ac366460a638552ae1a0a2" - integrity sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI= - dependencies: - get-stdin "^4.0.1" - strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -26738,7 +26846,7 @@ tar@4.4.13: safe-buffer "^5.1.2" yallist "^3.0.3" -tar@6.0.2, tar@^6.0.1, tar@^6.0.2: +tar@6.0.2, tar@^6.0.2: version "6.0.2" resolved "https://registry.yarnpkg.com/tar/-/tar-6.0.2.tgz#5df17813468a6264ff14f766886c622b84ae2f39" integrity sha512-Glo3jkRtPcvpDlAs/0+hozav78yoXKFr+c4wgw62NNMO3oo4AaJdCo21Uu7lcwr55h39W2XD1LMERc64wtbItg== @@ -26750,14 +26858,17 @@ tar@6.0.2, tar@^6.0.1, tar@^6.0.2: mkdirp "^1.0.3" yallist "^4.0.0" -tar@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" - integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA== +tar@^6.1.2: + version "6.1.10" + resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.10.tgz#8a320a74475fba54398fa136cd9883aa8ad11175" + integrity sha512-kvvfiVvjGMxeUNB6MyYv5z7vhfFRwbwCXJAeL0/lnbrttBVqcMOnpHUf0X42LrPMR8mMpgapkJMchFH4FSHzNA== dependencies: - block-stream "*" - fstream "^1.0.12" - inherits "2" + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" tcomb-validation@^3.3.0: version "3.4.1" @@ -27219,7 +27330,7 @@ toidentifier@1.0.0: resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553" integrity sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw== -topojson-client@3.1.0, topojson-client@^3.1.0: +topojson-client@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/topojson-client/-/topojson-client-3.1.0.tgz#22e8b1ed08a2b922feeb4af6f53b6ef09a467b99" integrity sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw== @@ -27302,11 +27413,6 @@ treeify@^1.0.1, treeify@^1.1.0: resolved "https://registry.yarnpkg.com/treeify/-/treeify-1.1.0.tgz#4e31c6a463accd0943879f30667c4fdaff411bb8" integrity sha512-1m4RA7xVAJrSGrrXGs0L3YTwyvBs2S8PbRHaLZAkFw7JR8oIFwYtysxlBZhYIa7xSyiYJKZ3iGrrk55cGA3i9A== -trim-newlines@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" - integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= - trim-newlines@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" @@ -27440,6 +27546,13 @@ tsutils@^3.17.1: dependencies: tslib "^1.8.1" +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + tty-browserify@0.0.0: version "0.0.0" resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6" @@ -29295,13 +29408,6 @@ which-typed-array@^1.1.2: has-symbols "^1.0.1" is-typed-array "^1.1.3" -which@1, which@^1.2.14, which@^1.2.9, which@^1.3.1, which@~1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - which@2.0.2, which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" @@ -29309,6 +29415,13 @@ which@2.0.2, which@^2.0.1, which@^2.0.2: dependencies: isexe "^2.0.0" +which@^1.2.14, which@^1.2.9, which@^1.3.1, which@~1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + wide-align@1.1.3, wide-align@^1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.3.tgz#ae074e6bdc0c14a431e804e624549c633b000457"