diff --git a/.buildkite/scripts/build_kibana.sh b/.buildkite/scripts/build_kibana.sh index 7a9878b5bcd13..e26d7790215f3 100755 --- a/.buildkite/scripts/build_kibana.sh +++ b/.buildkite/scripts/build_kibana.sh @@ -5,7 +5,11 @@ set -euo pipefail export KBN_NP_PLUGINS_BUILT=true echo "--- Build Kibana Distribution" -node scripts/build --debug +if [[ "${GITHUB_PR_LABELS:-}" == *"ci:build-all-platforms"* ]]; then + node scripts/build --all-platforms --skip-os-packages +else + node scripts/build +fi echo "--- Archive Kibana Distribution" linuxBuild="$(find "$KIBANA_DIR/target" -name 'kibana-*-linux-x86_64.tar.gz')" diff --git a/.buildkite/scripts/pipelines/pull_request/pipeline.js b/.buildkite/scripts/pipelines/pull_request/pipeline.js index 78dc6e1b29b6d..028c90020a0b8 100644 --- a/.buildkite/scripts/pipelines/pull_request/pipeline.js +++ b/.buildkite/scripts/pipelines/pull_request/pipeline.js @@ -55,21 +55,20 @@ const uploadPipeline = (pipelineContent) => { pipeline.push(getPipeline('.buildkite/pipelines/pull_request/base.yml', false)); if ( - await doAnyChangesMatch([ + (await doAnyChangesMatch([ /^x-pack\/plugins\/security_solution/, /^x-pack\/test\/security_solution_cypress/, /^x-pack\/plugins\/triggers_actions_ui\/public\/application\/sections\/action_connector_form/, /^x-pack\/plugins\/triggers_actions_ui\/public\/application\/context\/actions_connectors_context\.tsx/, - ]) || process.env.GITHUB_PR_LABELS.includes('ci:all-cypress-suites') + ])) || + process.env.GITHUB_PR_LABELS.includes('ci:all-cypress-suites') ) { pipeline.push(getPipeline('.buildkite/pipelines/pull_request/security_solution.yml')); } - // Disabled for now, these are failing/disabled in Jenkins currently as well // if ( - // await doAnyChangesMatch([ - // /^x-pack\/plugins\/apm/, - // ]) || process.env.GITHUB_PR_LABELS.includes('ci:all-cypress-suites') + // (await doAnyChangesMatch([/^x-pack\/plugins\/apm/])) || + // process.env.GITHUB_PR_LABELS.includes('ci:all-cypress-suites') // ) { // pipeline.push(getPipeline('.buildkite/pipelines/pull_request/apm_cypress.yml')); // } diff --git a/.buildkite/scripts/post_build_kibana.sh b/.buildkite/scripts/post_build_kibana.sh index 2194414dd22d3..5f26c80ddb6b6 100755 --- a/.buildkite/scripts/post_build_kibana.sh +++ b/.buildkite/scripts/post_build_kibana.sh @@ -12,7 +12,6 @@ fi echo "--- Upload Build Artifacts" # Moving to `target/` first will keep `buildkite-agent` from including directories in the artifact name cd "$KIBANA_DIR/target" -mv kibana-*-linux-x86_64.tar.gz kibana-default.tar.gz -buildkite-agent artifact upload kibana-default.tar.gz -buildkite-agent artifact upload kibana-default-plugins.tar.gz +cp kibana-*-linux-x86_64.tar.gz kibana-default.tar.gz +buildkite-agent artifact upload "./*.tar.gz;./*.zip" cd - diff --git a/.ci/Dockerfile b/.ci/Dockerfile index d3ea74ca38969..29ed08c84b23e 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.6 +ARG NODE_VERSION=16.11.1 FROM node:${NODE_VERSION} AS base @@ -10,7 +10,7 @@ RUN apt-get update && \ libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 \ libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 \ libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 \ - libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget openjdk-8-jre && \ + libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget openjdk-11-jre && \ rm -rf /var/lib/apt/lists/* RUN curl -sSL https://dl.google.com/linux/linux_signing_key.pub | apt-key add - \ diff --git a/.node-version b/.node-version index 5595ae1aa9e4c..141e9a2a2cef0 100644 --- a/.node-version +++ b/.node-version @@ -1 +1 @@ -14.17.6 +16.11.1 diff --git a/.nvmrc b/.nvmrc index 5595ae1aa9e4c..141e9a2a2cef0 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -14.17.6 +16.11.1 diff --git a/WORKSPACE.bazel b/WORKSPACE.bazel index 3ae3f202a3bfd..287b376037abe 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.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"), + "16.11.1-darwin_amd64": ("node-v16.11.1-darwin-x64.tar.gz", "node-v16.11.1-darwin-x64", "ba54b8ed504bd934d03eb860fefe991419b4209824280d4274f6a911588b5e45"), + "16.11.1-linux_arm64": ("node-v16.11.1-linux-arm64.tar.xz", "node-v16.11.1-linux-arm64", "083fc51f0ea26de9041aaf9821874651a9fd3b20d1cf57071ce6b523a0436f17"), + "16.11.1-linux_s390x": ("node-v16.11.1-linux-s390x.tar.xz", "node-v16.11.1-linux-s390x", "855b5c83c2ccb05273d50bb04376335c68d47df57f3187cdebe1f22b972d2825"), + "16.11.1-linux_amd64": ("node-v16.11.1-linux-x64.tar.xz", "node-v16.11.1-linux-x64", "493bcc9b660eff983a6de65a0f032eb2717f57207edf74c745bcb86e360310b3"), + "16.11.1-windows_amd64": ("node-v16.11.1-win-x64.zip", "node-v16.11.1-win-x64", "4d3c179b82d42e66e321c3948a4e332ed78592917a69d38b86e3a242d7e62fb7"), }, - node_version = "14.17.6", + node_version = "16.11.1", node_urls = [ "https://nodejs.org/dist/v{version}/{filename}", ], diff --git a/config/node.options b/config/node.options index 2927d1b576716..2585745249706 100644 --- a/config/node.options +++ b/config/node.options @@ -4,3 +4,6 @@ ## max size of old space in megabytes #--max-old-space-size=4096 + +## do not terminate process on unhandled promise rejection + --unhandled-rejections=warn 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 51101c8804680..d90972d327041 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,4 +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 apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly customLinks: string;
readonly droppedTransactionSpans: string;
readonly upgrading: string;
readonly metaData: 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 securitySolution: {
readonly trustedApps: 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: Readonly<{
guide: string;
infrastructureThreshold: string;
logsThreshold: string;
metricsThreshold: string;
monitorStatus: string;
monitorUptime: string;
tlsCertificate: string;
uptimeDurationAnomaly: 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;
};
readonly clients: {
readonly guide: string;
readonly goOverview: string;
readonly javaIndex: string;
readonly jsIntro: string;
readonly netGuide: string;
readonly perlGuide: string;
readonly phpGuide: string;
readonly pythonGuide: string;
readonly rubyOverview: string;
readonly rustGuide: string;
};
} | | \ No newline at end of file +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly elasticStackGetStarted: string;
readonly apm: {
readonly kibanaSettings: string;
readonly supportedServiceMaps: string;
readonly customLinks: string;
readonly droppedTransactionSpans: string;
readonly upgrading: string;
readonly metaData: 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 troubleshootGaps: string;
};
readonly securitySolution: {
readonly trustedApps: 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: Readonly<{
guide: string;
infrastructureThreshold: string;
logsThreshold: string;
metricsThreshold: string;
monitorStatus: string;
monitorUptime: string;
tlsCertificate: string;
uptimeDurationAnomaly: 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 spaces: Readonly<{
kibanaLegacyUrlAliases: string;
kibanaDisableLegacyUrlAliasesApi: 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;
learnMoreBlog: string;
apiKeysLearnMore: string;
}>;
readonly ecs: {
readonly guide: string;
};
readonly clients: {
readonly guide: string;
readonly goOverview: string;
readonly javaIndex: string;
readonly jsIntro: string;
readonly netGuide: string;
readonly perlGuide: string;
readonly phpGuide: string;
readonly pythonGuide: string;
readonly rubyOverview: string;
readonly rustGuide: string;
};
} | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.correctiveactions.md similarity index 66% rename from docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md rename to docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.correctiveactions.md index d7d10651033bf..273945166735b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.correctiveactions.md +++ b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.correctiveactions.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [correctiveActions](./kibana-plugin-core-server.deprecationsdetails.correctiveactions.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [BaseDeprecationDetails](./kibana-plugin-core-server.basedeprecationdetails.md) > [correctiveActions](./kibana-plugin-core-server.basedeprecationdetails.correctiveactions.md) -## DeprecationsDetails.correctiveActions property +## BaseDeprecationDetails.correctiveActions property corrective action needed to fix this deprecation. diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.deprecationtype.md b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.deprecationtype.md similarity index 70% rename from docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.deprecationtype.md rename to docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.deprecationtype.md index 3a76bc60ee630..072dfd17418f9 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.deprecationtype.md +++ b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.deprecationtype.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [deprecationType](./kibana-plugin-core-server.deprecationsdetails.deprecationtype.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [BaseDeprecationDetails](./kibana-plugin-core-server.basedeprecationdetails.md) > [deprecationType](./kibana-plugin-core-server.basedeprecationdetails.deprecationtype.md) -## DeprecationsDetails.deprecationType property +## BaseDeprecationDetails.deprecationType property (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. diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.documentationurl.md b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.documentationurl.md similarity index 53% rename from docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.documentationurl.md rename to docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.documentationurl.md index 457cf7b61dac8..c8f0762acdce6 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.documentationurl.md +++ b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.documentationurl.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [documentationUrl](./kibana-plugin-core-server.deprecationsdetails.documentationurl.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [BaseDeprecationDetails](./kibana-plugin-core-server.basedeprecationdetails.md) > [documentationUrl](./kibana-plugin-core-server.basedeprecationdetails.documentationurl.md) -## DeprecationsDetails.documentationUrl property +## BaseDeprecationDetails.documentationUrl property (optional) link to the documentation for more details on the deprecation. diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.level.md b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.level.md similarity index 66% rename from docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.level.md rename to docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.level.md index 64ad22e2c87fb..ad755805d00b9 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.level.md +++ b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.level.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [level](./kibana-plugin-core-server.deprecationsdetails.level.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [BaseDeprecationDetails](./kibana-plugin-core-server.basedeprecationdetails.md) > [level](./kibana-plugin-core-server.basedeprecationdetails.level.md) -## DeprecationsDetails.level property +## BaseDeprecationDetails.level property 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. diff --git a/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.md b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.md new file mode 100644 index 0000000000000..3e47865062352 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [BaseDeprecationDetails](./kibana-plugin-core-server.basedeprecationdetails.md) + +## BaseDeprecationDetails interface + +Base properties shared by all types of deprecations + +Signature: + +```typescript +export interface BaseDeprecationDetails +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [correctiveActions](./kibana-plugin-core-server.basedeprecationdetails.correctiveactions.md) | {
api?: {
path: string;
method: 'POST' | 'PUT';
body?: {
[key: string]: any;
};
omitContextFromBody?: boolean;
};
manualSteps: string[];
} | corrective action needed to fix this deprecation. | +| [deprecationType](./kibana-plugin-core-server.basedeprecationdetails.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.basedeprecationdetails.documentationurl.md) | string | (optional) link to the documentation for more details on the deprecation. | +| [level](./kibana-plugin-core-server.basedeprecationdetails.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.basedeprecationdetails.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.basedeprecationdetails.requirerestart.md) | boolean | (optional) specify the fix for this deprecation requires a full kibana restart. | +| [title](./kibana-plugin-core-server.basedeprecationdetails.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.basedeprecationdetails.message.md similarity index 60% rename from docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.message.md rename to docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.message.md index 906ce8118f95b..5802bc316cc08 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.message.md +++ b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.message.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [message](./kibana-plugin-core-server.deprecationsdetails.message.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [BaseDeprecationDetails](./kibana-plugin-core-server.basedeprecationdetails.md) > [message](./kibana-plugin-core-server.basedeprecationdetails.message.md) -## DeprecationsDetails.message property +## BaseDeprecationDetails.message property The description message to be displayed for 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.requirerestart.md b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.requirerestart.md similarity index 54% rename from docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.requirerestart.md rename to docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.requirerestart.md index 85bddd9436e73..3f589600d0458 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.requirerestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.requirerestart.md @@ -1,8 +1,8 @@ -[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) > [requireRestart](./kibana-plugin-core-server.deprecationsdetails.requirerestart.md) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [BaseDeprecationDetails](./kibana-plugin-core-server.basedeprecationdetails.md) > [requireRestart](./kibana-plugin-core-server.basedeprecationdetails.requirerestart.md) -## DeprecationsDetails.requireRestart property +## BaseDeprecationDetails.requireRestart property (optional) specify the fix for this deprecation requires a full kibana restart. diff --git a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.title.md b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.title.md similarity index 59% rename from docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.title.md rename to docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.title.md index e8907688f6e5e..b6788a4faa7c5 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.title.md +++ b/docs/development/core/server/kibana-plugin-core-server.basedeprecationdetails.title.md @@ -1,8 +1,8 @@ -[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) +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [BaseDeprecationDetails](./kibana-plugin-core-server.basedeprecationdetails.md) > [title](./kibana-plugin-core-server.basedeprecationdetails.title.md) -## DeprecationsDetails.title property +## BaseDeprecationDetails.title property 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.configdeprecationdetails.configpath.md b/docs/development/core/server/kibana-plugin-core-server.configdeprecationdetails.configpath.md new file mode 100644 index 0000000000000..7af6c16d86e4a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.configdeprecationdetails.configpath.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ConfigDeprecationDetails](./kibana-plugin-core-server.configdeprecationdetails.md) > [configPath](./kibana-plugin-core-server.configdeprecationdetails.configpath.md) + +## ConfigDeprecationDetails.configPath property + +Signature: + +```typescript +configPath: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.configdeprecationdetails.deprecationtype.md b/docs/development/core/server/kibana-plugin-core-server.configdeprecationdetails.deprecationtype.md new file mode 100644 index 0000000000000..fb3737062f986 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.configdeprecationdetails.deprecationtype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ConfigDeprecationDetails](./kibana-plugin-core-server.configdeprecationdetails.md) > [deprecationType](./kibana-plugin-core-server.configdeprecationdetails.deprecationtype.md) + +## ConfigDeprecationDetails.deprecationType property + +Signature: + +```typescript +deprecationType: 'config'; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.configdeprecationdetails.md b/docs/development/core/server/kibana-plugin-core-server.configdeprecationdetails.md new file mode 100644 index 0000000000000..d1ccbb0b6164a --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.configdeprecationdetails.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [ConfigDeprecationDetails](./kibana-plugin-core-server.configdeprecationdetails.md) + +## ConfigDeprecationDetails interface + + +Signature: + +```typescript +export interface ConfigDeprecationDetails extends BaseDeprecationDetails +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [configPath](./kibana-plugin-core-server.configdeprecationdetails.configpath.md) | string | | +| [deprecationType](./kibana-plugin-core-server.configdeprecationdetails.deprecationtype.md) | 'config' | | + 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 2ff9f4b792f5d..d8ced1da62416 100644 --- a/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md +++ b/docs/development/core/server/kibana-plugin-core-server.deprecationsdetails.md @@ -2,24 +2,11 @@ [Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) -## DeprecationsDetails interface +## DeprecationsDetails type Signature: ```typescript -export interface DeprecationsDetails +export declare type DeprecationsDetails = ConfigDeprecationDetails | FeatureDeprecationDetails; ``` - -## Properties - -| Property | Type | Description | -| --- | --- | --- | -| [correctiveActions](./kibana-plugin-core-server.deprecationsdetails.correctiveactions.md) | {
api?: {
path: string;
method: 'POST' | 'PUT';
body?: {
[key: string]: any;
};
omitContextFromBody?: boolean;
};
manualSteps: string[];
} | corrective action needed to fix this deprecation. | -| [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 | (optional) link to the documentation for more details on the deprecation. | -| [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 | 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 | (optional) specify the fix for this deprecation requires a full kibana restart. | -| [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.featuredeprecationdetails.deprecationtype.md b/docs/development/core/server/kibana-plugin-core-server.featuredeprecationdetails.deprecationtype.md new file mode 100644 index 0000000000000..b530874d3678b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.featuredeprecationdetails.deprecationtype.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [FeatureDeprecationDetails](./kibana-plugin-core-server.featuredeprecationdetails.md) > [deprecationType](./kibana-plugin-core-server.featuredeprecationdetails.deprecationtype.md) + +## FeatureDeprecationDetails.deprecationType property + +Signature: + +```typescript +deprecationType?: 'feature' | undefined; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.featuredeprecationdetails.md b/docs/development/core/server/kibana-plugin-core-server.featuredeprecationdetails.md new file mode 100644 index 0000000000000..bed3356e36129 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.featuredeprecationdetails.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [FeatureDeprecationDetails](./kibana-plugin-core-server.featuredeprecationdetails.md) + +## FeatureDeprecationDetails interface + + +Signature: + +```typescript +export interface FeatureDeprecationDetails extends BaseDeprecationDetails +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [deprecationType](./kibana-plugin-core-server.featuredeprecationdetails.deprecationtype.md) | 'feature' | undefined | | + diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 89203cb94d573..20b4b43776f21 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -53,9 +53,11 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AuthRedirectedParams](./kibana-plugin-core-server.authredirectedparams.md) | Result of auth redirection. | | [AuthResultParams](./kibana-plugin-core-server.authresultparams.md) | Result of successful authentication. | | [AuthToolkit](./kibana-plugin-core-server.authtoolkit.md) | A tool set defining an outcome of Auth interceptor for incoming request. | +| [BaseDeprecationDetails](./kibana-plugin-core-server.basedeprecationdetails.md) | Base properties shared by all types of deprecations | | [Capabilities](./kibana-plugin-core-server.capabilities.md) | The read-only set of capabilities available for the current UI session. Capabilities are simple key-value pairs of (string, boolean), where the string denotes the capability ID, and the boolean is a flag indicating if the capability is enabled or disabled. | | [CapabilitiesSetup](./kibana-plugin-core-server.capabilitiessetup.md) | APIs to manage the [Capabilities](./kibana-plugin-core-server.capabilities.md) that will be used by the application.Plugins relying on capabilities to toggle some of their features should register them during the setup phase using the registerProvider method.Plugins having the responsibility to restrict capabilities depending on a given context should register their capabilities switcher using the registerSwitcher method.Refers to the methods documentation for complete description and examples. | | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) | APIs to access the application [Capabilities](./kibana-plugin-core-server.capabilities.md). | +| [ConfigDeprecationDetails](./kibana-plugin-core-server.configdeprecationdetails.md) | | | [ContextSetup](./kibana-plugin-core-server.contextsetup.md) | An object that handles registration of context providers and configuring handlers with context. | | [CorePreboot](./kibana-plugin-core-server.corepreboot.md) | Context passed to the setup method of preboot plugins. | | [CoreSetup](./kibana-plugin-core-server.coresetup.md) | Context passed to the setup method of standard plugins. | @@ -65,7 +67,6 @@ The plugin integrates with the core system via lifecycle events: `setup` | [CustomHttpResponseOptions](./kibana-plugin-core-server.customhttpresponseoptions.md) | HTTP response parameters for a response with adjustable status code. | | [DeleteDocumentResponse](./kibana-plugin-core-server.deletedocumentresponse.md) | | | [DeprecationsClient](./kibana-plugin-core-server.deprecationsclient.md) | Server-side client that provides access to fetch all Kibana deprecations | -| [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) | | | [DeprecationSettings](./kibana-plugin-core-server.deprecationsettings.md) | UiSettings deprecation field options. | | [DeprecationsServiceSetup](./kibana-plugin-core-server.deprecationsservicesetup.md) | The deprecations service provides a way for the Kibana platform to communicate deprecated features and configs with its users. These deprecations are only communicated if the deployment is using these features. Allowing for a user tailored experience for upgrading the stack version.The Deprecation service is consumed by the upgrade assistant to assist with the upgrade experience.If a deprecated feature can be resolved without manual user intervention. Using correctiveActions.api allows the Upgrade Assistant to use this api to correct the deprecation upon a user trigger. | | [DiscoveredPlugin](./kibana-plugin-core-server.discoveredplugin.md) | Small container object used to expose information about discovered plugins that may or may not have been started. | @@ -77,6 +78,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [ErrorHttpResponseOptions](./kibana-plugin-core-server.errorhttpresponseoptions.md) | HTTP response parameters | | [ExecutionContextSetup](./kibana-plugin-core-server.executioncontextsetup.md) | | | [FakeRequest](./kibana-plugin-core-server.fakerequest.md) | Fake request object created manually by Kibana plugins. | +| [FeatureDeprecationDetails](./kibana-plugin-core-server.featuredeprecationdetails.md) | | | [GetDeprecationsContext](./kibana-plugin-core-server.getdeprecationscontext.md) | | | [GetResponse](./kibana-plugin-core-server.getresponse.md) | | | [HttpAuth](./kibana-plugin-core-server.httpauth.md) | | @@ -246,6 +248,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [AuthResult](./kibana-plugin-core-server.authresult.md) | | | [CapabilitiesProvider](./kibana-plugin-core-server.capabilitiesprovider.md) | See [CapabilitiesSetup](./kibana-plugin-core-server.capabilitiessetup.md) | | [CapabilitiesSwitcher](./kibana-plugin-core-server.capabilitiesswitcher.md) | See [CapabilitiesSetup](./kibana-plugin-core-server.capabilitiessetup.md) | +| [DeprecationsDetails](./kibana-plugin-core-server.deprecationsdetails.md) | | | [DestructiveRouteMethod](./kibana-plugin-core-server.destructiveroutemethod.md) | Set of HTTP methods changing the state of the server. | | [ElasticsearchClient](./kibana-plugin-core-server.elasticsearchclient.md) | Client used to query the elasticsearch cluster. | | [ElasticsearchClientConfig](./kibana-plugin-core-server.elasticsearchclientconfig.md) | Configuration options to be used to create a [cluster client](./kibana-plugin-core-server.iclusterclient.md) using the [createClient API](./kibana-plugin-core-server.elasticsearchservicestart.createclient.md) | diff --git a/package.json b/package.json index 60c1fee7bf192..e8f6bdbd1c069 100644 --- a/package.json +++ b/package.json @@ -66,7 +66,7 @@ "url": "https://github.com/elastic/kibana.git" }, "resolutions": { - "**/@types/node": "14.14.44", + "**/@types/node": "16.10.2", "**/chokidar": "^3.4.3", "**/deepmerge": "^4.2.2", "**/fast-deep-equal": "^3.1.1", @@ -84,7 +84,7 @@ "**/underscore": "^1.13.1" }, "engines": { - "node": "14.17.6", + "node": "16.11.1", "yarn": "^1.21.1" }, "dependencies": { @@ -573,12 +573,12 @@ "@types/minimatch": "^2.0.29", "@types/minimist": "^1.2.1", "@types/mocha": "^8.2.0", - "@types/mock-fs": "^4.10.0", + "@types/mock-fs": "^4.13.1", "@types/moment-timezone": "^0.5.12", "@types/mustache": "^0.8.31", "@types/ncp": "^2.0.1", "@types/nock": "^10.0.3", - "@types/node": "14.14.44", + "@types/node": "16.10.2", "@types/node-fetch": "^2.5.7", "@types/node-forge": "^0.10.5", "@types/nodemailer": "^6.4.0", @@ -767,7 +767,7 @@ "mocha-junit-reporter": "^2.0.0", "mochawesome": "^6.2.1", "mochawesome-merge": "^4.2.0", - "mock-fs": "^4.12.0", + "mock-fs": "^5.1.1", "mock-http-server": "1.3.0", "ms-chromium-edge-driver": "^0.4.2", "multimatch": "^4.0.0", diff --git a/packages/kbn-config/src/config_service.test.ts b/packages/kbn-config/src/config_service.test.ts index e8fd7ab187596..4a8164b100626 100644 --- a/packages/kbn-config/src/config_service.test.ts +++ b/packages/kbn-config/src/config_service.test.ts @@ -382,6 +382,7 @@ test('logs deprecation if schema is not present and "enabled" is used', async () "foo", Array [ Object { + "configPath": "foo.enabled", "correctiveActions": Object { "manualSteps": Array [ "Remove \\"foo.enabled\\" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to 8.0.0.", @@ -452,10 +453,12 @@ test('logs deprecation warning during validation', async () => { mockApplyDeprecations.mockImplementationOnce((config, deprecations, createAddDeprecation) => { const addDeprecation = createAddDeprecation!(''); addDeprecation({ + configPath: 'test1', message: 'some deprecation message', correctiveActions: { manualSteps: ['do X'] }, }); addDeprecation({ + configPath: 'test2', message: 'another deprecation message', correctiveActions: { manualSteps: ['do Y'] }, }); @@ -521,11 +524,13 @@ test('does not log warnings for silent deprecations during validation', async () .mockImplementationOnce((config, deprecations, createAddDeprecation) => { const addDeprecation = createAddDeprecation!(''); addDeprecation({ + configPath: 'test1', message: 'some deprecation message', correctiveActions: { manualSteps: ['do X'] }, silent: true, }); addDeprecation({ + configPath: 'test2', message: 'another deprecation message', correctiveActions: { manualSteps: ['do Y'] }, }); @@ -534,6 +539,7 @@ test('does not log warnings for silent deprecations during validation', async () .mockImplementationOnce((config, deprecations, createAddDeprecation) => { const addDeprecation = createAddDeprecation!(''); addDeprecation({ + configPath: 'silent', message: 'I am silent', silent: true, correctiveActions: { manualSteps: ['do Z'] }, @@ -617,6 +623,7 @@ describe('getHandledDeprecatedConfigs', () => { deprecations.forEach((deprecation) => { const addDeprecation = createAddDeprecation!(deprecation.path); addDeprecation({ + configPath: 'test1', message: `some deprecation message`, documentationUrl: 'some-url', correctiveActions: { manualSteps: ['do X'] }, @@ -633,6 +640,7 @@ describe('getHandledDeprecatedConfigs', () => { "base", Array [ Object { + "configPath": "test1", "correctiveActions": Object { "manualSteps": Array [ "do X", diff --git a/packages/kbn-config/src/config_service.ts b/packages/kbn-config/src/config_service.ts index d99d4bd410743..f087b3fff8612 100644 --- a/packages/kbn-config/src/config_service.ts +++ b/packages/kbn-config/src/config_service.ts @@ -184,6 +184,7 @@ export class ConfigService { if (validatedConfig?.enabled === undefined && isEnabled !== undefined) { const deprecationPath = pathToString(enabledPath); const deprecatedConfigDetails: DeprecatedConfigDetails = { + configPath: deprecationPath, title: `Setting "${deprecationPath}" is deprecated`, message: `Configuring "${deprecationPath}" is deprecated and will be removed in 8.0.0.`, correctiveActions: { diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts index 415c8fb9f0610..d9fe90ff711ed 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.test.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.test.ts @@ -43,6 +43,7 @@ describe('DeprecationFactory', () => { Array [ Array [ Object { + "configPath": "myplugin.deprecated", "correctiveActions": Object { "manualSteps": Array [ "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to 8.0.0.", @@ -79,6 +80,7 @@ describe('DeprecationFactory', () => { Array [ Array [ Object { + "configPath": "myplugin.section.deprecated", "correctiveActions": Object { "manualSteps": Array [ "Remove \\"myplugin.section.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to 8.0.0.", @@ -134,6 +136,7 @@ describe('DeprecationFactory', () => { Array [ Array [ Object { + "configPath": "myplugin.deprecated", "correctiveActions": Object { "manualSteps": Array [ "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only) before upgrading to 8.0.0.", @@ -197,6 +200,7 @@ describe('DeprecationFactory', () => { Array [ Array [ Object { + "configPath": "myplugin.deprecated", "correctiveActions": Object { "manualSteps": Array [ "Replace \\"myplugin.deprecated\\" with \\"myplugin.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", @@ -254,6 +258,7 @@ describe('DeprecationFactory', () => { Array [ Array [ Object { + "configPath": "myplugin.oldsection.deprecated", "correctiveActions": Object { "manualSteps": Array [ "Replace \\"myplugin.oldsection.deprecated\\" with \\"myplugin.newsection.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", @@ -286,6 +291,7 @@ describe('DeprecationFactory', () => { Array [ Array [ Object { + "configPath": "myplugin.deprecated", "correctiveActions": Object { "manualSteps": Array [ "Make sure \\"myplugin.renamed\\" contains the correct value in the config file, CLI flag, or environment variable (in Docker only).", @@ -331,6 +337,7 @@ describe('DeprecationFactory', () => { Array [ Array [ Object { + "configPath": "myplugin.deprecated", "correctiveActions": Object { "manualSteps": Array [ "Replace \\"myplugin.deprecated\\" with \\"myplugin.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", @@ -373,6 +380,7 @@ describe('DeprecationFactory', () => { Array [ Array [ Object { + "configPath": "oldplugin.deprecated", "correctiveActions": Object { "manualSteps": Array [ "Replace \\"oldplugin.deprecated\\" with \\"newplugin.renamed\\" in the Kibana config file, CLI flag, or environment variable (in Docker only).", @@ -427,6 +435,7 @@ describe('DeprecationFactory', () => { Array [ Array [ Object { + "configPath": "myplugin.deprecated", "correctiveActions": Object { "manualSteps": Array [ "Make sure \\"myplugin.renamed\\" contains the correct value in the config file, CLI flag, or environment variable (in Docker only).", @@ -461,6 +470,7 @@ describe('DeprecationFactory', () => { Array [ Array [ Object { + "configPath": "myplugin.deprecated", "correctiveActions": Object { "manualSteps": Array [ "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only).", @@ -494,6 +504,7 @@ describe('DeprecationFactory', () => { Array [ Array [ Object { + "configPath": "myplugin.section.deprecated", "correctiveActions": Object { "manualSteps": Array [ "Remove \\"myplugin.section.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only).", @@ -546,6 +557,7 @@ describe('DeprecationFactory', () => { Array [ Array [ Object { + "configPath": "myplugin.deprecated", "correctiveActions": Object { "manualSteps": Array [ "Remove \\"myplugin.deprecated\\" from the Kibana config file, CLI flag, or environment variable (in Docker only).", diff --git a/packages/kbn-config/src/deprecation/deprecation_factory.ts b/packages/kbn-config/src/deprecation/deprecation_factory.ts index 1d61733715bd9..ea4db280e915b 100644 --- a/packages/kbn-config/src/deprecation/deprecation_factory.ts +++ b/packages/kbn-config/src/deprecation/deprecation_factory.ts @@ -37,6 +37,7 @@ const _deprecate = ( return; } addDeprecation({ + configPath: fullPath, title: getDeprecationTitle(fullPath), message: i18n.translate('kbnConfig.deprecations.deprecatedSettingMessage', { defaultMessage: 'Configuring "{fullPath}" is deprecated and will be removed in {removeBy}.', @@ -73,6 +74,7 @@ const _rename = ( const newValue = get(config, fullNewPath); if (newValue === undefined) { addDeprecation({ + configPath: fullOldPath, title: getDeprecationTitle(fullOldPath), message: i18n.translate('kbnConfig.deprecations.replacedSettingMessage', { defaultMessage: `Setting "{fullOldPath}" has been replaced by "{fullNewPath}"`, @@ -95,6 +97,7 @@ const _rename = ( }; } else { addDeprecation({ + configPath: fullOldPath, title: getDeprecationTitle(fullOldPath), message: i18n.translate('kbnConfig.deprecations.conflictSettingMessage', { defaultMessage: @@ -135,6 +138,7 @@ const _unused = ( return; } addDeprecation({ + configPath: fullPath, title: getDeprecationTitle(fullPath), message: i18n.translate('kbnConfig.deprecations.unusedSettingMessage', { defaultMessage: 'You no longer need to configure "{fullPath}".', diff --git a/packages/kbn-config/src/deprecation/types.ts b/packages/kbn-config/src/deprecation/types.ts index 12b561aa2b1b9..f5bb240f5cc43 100644 --- a/packages/kbn-config/src/deprecation/types.ts +++ b/packages/kbn-config/src/deprecation/types.ts @@ -20,6 +20,8 @@ export type AddConfigDeprecation = (details: DeprecatedConfigDetails) => void; * @public */ export interface DeprecatedConfigDetails { + /** The path of the deprecated config setting */ + configPath: string; /** The title to be displayed for the deprecation. */ title?: string; /** The message to be displayed for the deprecation. */ @@ -30,7 +32,7 @@ export interface DeprecatedConfigDetails { * - critical: needs to be addressed before upgrade. */ level?: 'warning' | 'critical'; - /** (optional) set false to prevent the config service from logging the deprecation message. */ + /** (optional) set to `true` to prevent the config service from logging the deprecation message. */ silent?: boolean; /** (optional) link to the documentation for more details on the deprecation. */ documentationUrl?: string; diff --git a/packages/kbn-dev-utils/src/proc_runner/proc.ts b/packages/kbn-dev-utils/src/proc_runner/proc.ts index e04a189baf5cd..c9a520de6eb4d 100644 --- a/packages/kbn-dev-utils/src/proc_runner/proc.ts +++ b/packages/kbn-dev-utils/src/proc_runner/proc.ts @@ -131,7 +131,7 @@ export function startProc(name: string, options: ProcOptions, log: ToolingLog) { await withTimeout( async () => { log.debug(`Sending "${signal}" to proc "${name}"`); - await treeKillAsync(childProcess.pid, signal); + await treeKillAsync(childProcess.pid!, signal); await outcomePromise; }, STOP_TIMEOUT, @@ -139,7 +139,7 @@ export function startProc(name: string, options: ProcOptions, log: ToolingLog) { log.warning( `Proc "${name}" was sent "${signal}" didn't emit the "exit" or "error" events after ${STOP_TIMEOUT} ms, sending SIGKILL` ); - await treeKillAsync(childProcess.pid, 'SIGKILL'); + await treeKillAsync(childProcess.pid!, 'SIGKILL'); } ); diff --git a/packages/kbn-es-query/src/es_query/build_es_query.test.ts b/packages/kbn-es-query/src/es_query/build_es_query.test.ts index aca40632960f8..8fd884f65fa0d 100644 --- a/packages/kbn-es-query/src/es_query/build_es_query.test.ts +++ b/packages/kbn-es-query/src/es_query/build_es_query.test.ts @@ -12,13 +12,14 @@ import { luceneStringToDsl } from './lucene_string_to_dsl'; import { decorateQuery } from './decorate_query'; import { MatchAllFilter, Query } from '../filters'; import { fields } from '../filters/stubs'; -import { IndexPatternBase } from './types'; +import { DataViewBase } from './types'; jest.mock('../kuery/grammar'); describe('build query', () => { - const indexPattern: IndexPatternBase = { + const indexPattern: DataViewBase = { fields, + title: 'dataView', }; describe('buildEsQuery', () => { diff --git a/packages/kbn-es-query/src/es_query/from_filters.test.ts b/packages/kbn-es-query/src/es_query/from_filters.test.ts index eacf775194bb8..67de9e4d88736 100644 --- a/packages/kbn-es-query/src/es_query/from_filters.test.ts +++ b/packages/kbn-es-query/src/es_query/from_filters.test.ts @@ -9,11 +9,12 @@ import { buildQueryFromFilters } from './from_filters'; import { ExistsFilter, Filter, MatchAllFilter } from '../filters'; import { fields } from '../filters/stubs'; -import { IndexPatternBase } from './types'; +import { DataViewBase } from './types'; describe('build query', () => { - const indexPattern: IndexPatternBase = { + const indexPattern: DataViewBase = { fields, + title: 'dataView', }; describe('buildQueryFromFilters', () => { diff --git a/packages/kbn-es-query/src/es_query/from_kuery.test.ts b/packages/kbn-es-query/src/es_query/from_kuery.test.ts index 2458013854393..443b44dff5819 100644 --- a/packages/kbn-es-query/src/es_query/from_kuery.test.ts +++ b/packages/kbn-es-query/src/es_query/from_kuery.test.ts @@ -9,14 +9,15 @@ import { buildQueryFromKuery } from './from_kuery'; import { fromKueryExpression, toElasticsearchQuery } from '../kuery'; import { fields } from '../filters/stubs'; -import { IndexPatternBase } from './types'; +import { DataViewBase } from './types'; import { Query } from '..'; jest.mock('../kuery/grammar'); describe('build query', () => { - const indexPattern: IndexPatternBase = { + const indexPattern: DataViewBase = { fields, + title: 'dataView', }; describe('buildQueryFromKuery', () => { diff --git a/packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts b/packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts index 333f4bad54a46..2b9fbbe5ece04 100644 --- a/packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts +++ b/packages/kbn-es-query/src/es_query/handle_nested_filter.test.ts @@ -9,12 +9,13 @@ import { handleNestedFilter } from './handle_nested_filter'; import { fields } from '../filters/stubs'; import { buildPhraseFilter, buildQueryFilter } from '../filters'; -import { IndexPatternBase } from './types'; +import { DataViewBase } from './types'; describe('handleNestedFilter', function () { - const indexPattern: IndexPatternBase = { + const indexPattern: DataViewBase = { id: 'logstash-*', fields, + title: 'dataView', }; it("should return the filter's query wrapped in nested query if the target field is nested", () => { diff --git a/packages/kbn-es-query/src/es_query/types.ts b/packages/kbn-es-query/src/es_query/types.ts index 3a5893d20ef25..75ea320b3431f 100644 --- a/packages/kbn-es-query/src/es_query/types.ts +++ b/packages/kbn-es-query/src/es_query/types.ts @@ -65,7 +65,7 @@ export type IndexPatternFieldBase = DataViewFieldBase; export interface DataViewBase { fields: DataViewFieldBase[]; id?: string; - title?: string; + title: string; } /** diff --git a/packages/kbn-es-query/src/filters/build_filters/build_filter.test.ts b/packages/kbn-es-query/src/filters/build_filters/build_filter.test.ts index 762af90063070..761d0341f1eae 100644 --- a/packages/kbn-es-query/src/filters/build_filters/build_filter.test.ts +++ b/packages/kbn-es-query/src/filters/build_filters/build_filter.test.ts @@ -7,13 +7,14 @@ */ import { buildFilter, FilterStateStore, FILTERS } from '.'; -import { IndexPatternBase } from '../..'; +import { DataViewBase } from '../..'; import { fields as stubFields } from '../stubs'; describe('buildFilter', () => { - const stubIndexPattern: IndexPatternBase = { + const stubIndexPattern: DataViewBase = { id: 'logstash-*', fields: stubFields, + title: 'dataView', }; it('should build phrase filters', () => { diff --git a/packages/kbn-es-query/src/filters/build_filters/exists_filter.test.ts b/packages/kbn-es-query/src/filters/build_filters/exists_filter.test.ts index 35a2a5c3d6057..81aab2c300243 100644 --- a/packages/kbn-es-query/src/filters/build_filters/exists_filter.test.ts +++ b/packages/kbn-es-query/src/filters/build_filters/exists_filter.test.ts @@ -6,13 +6,14 @@ * Side Public License, v 1. */ -import { IndexPatternBase } from '../../es_query'; +import { DataViewBase } from '../../es_query'; import { buildExistsFilter, getExistsFilterField } from './exists_filter'; import { fields } from '../stubs/fields.mocks'; describe('exists filter', function () { - const indexPattern: IndexPatternBase = { + const indexPattern: DataViewBase = { fields, + title: 'dataView', }; describe('getExistsFilterField', function () { diff --git a/packages/kbn-es-query/src/filters/build_filters/get_filter_field.test.ts b/packages/kbn-es-query/src/filters/build_filters/get_filter_field.test.ts index ca67a439757de..06803716aac2f 100644 --- a/packages/kbn-es-query/src/filters/build_filters/get_filter_field.test.ts +++ b/packages/kbn-es-query/src/filters/build_filters/get_filter_field.test.ts @@ -9,13 +9,14 @@ import { buildPhraseFilter } from './phrase_filter'; import { buildQueryFilter } from './query_string_filter'; import { getFilterField } from './get_filter_field'; -import { IndexPatternBase } from '../../es_query'; +import { DataViewBase } from '../../es_query'; import { fields } from '../stubs/fields.mocks'; describe('getFilterField', function () { - const indexPattern: IndexPatternBase = { + const indexPattern: DataViewBase = { id: 'logstash-*', fields, + title: 'dataView', }; it('should return the field name from known filter types that target a specific field', () => { diff --git a/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts b/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts index f23dfde12d977..13f18ad0cc7ea 100644 --- a/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts +++ b/packages/kbn-es-query/src/filters/build_filters/phrase_filter.test.ts @@ -13,16 +13,17 @@ import { PhraseFilter, } from './phrase_filter'; import { fields, getField } from '../stubs'; -import { IndexPatternBase } from '../../es_query'; +import { DataViewBase } from '../../es_query'; import { estypes } from '@elastic/elasticsearch'; describe('Phrase filter builder', () => { - let indexPattern: IndexPatternBase; + let indexPattern: DataViewBase; beforeEach(() => { indexPattern = { id: 'id', fields, + title: 'dataView', }; }); @@ -151,8 +152,9 @@ describe('buildInlineScriptForPhraseFilter', () => { }); describe('getPhraseFilterField', function () { - const indexPattern: IndexPatternBase = { + const indexPattern: DataViewBase = { fields, + title: 'dataView', }; it('should return the name of the field a phrase query is targeting', () => { diff --git a/packages/kbn-es-query/src/filters/build_filters/phrases_filter.test.ts b/packages/kbn-es-query/src/filters/build_filters/phrases_filter.test.ts index c3e7fd37e1ff6..1a125e98f9645 100644 --- a/packages/kbn-es-query/src/filters/build_filters/phrases_filter.test.ts +++ b/packages/kbn-es-query/src/filters/build_filters/phrases_filter.test.ts @@ -6,13 +6,14 @@ * Side Public License, v 1. */ -import { IndexPatternBase } from '../../es_query'; +import { DataViewBase } from '../../es_query'; import { buildPhrasesFilter, getPhrasesFilterField } from './phrases_filter'; import { fields } from '../stubs'; describe('phrases filter', function () { - const indexPattern: IndexPatternBase = { + const indexPattern: DataViewBase = { fields, + title: 'dataView', }; describe('getPhrasesFilterField', function () { diff --git a/packages/kbn-es-query/src/kuery/ast/ast.test.ts b/packages/kbn-es-query/src/kuery/ast/ast.test.ts index edb9150ea00d5..0e4366d6e21d1 100644 --- a/packages/kbn-es-query/src/kuery/ast/ast.test.ts +++ b/packages/kbn-es-query/src/kuery/ast/ast.test.ts @@ -8,18 +8,19 @@ import { fromKueryExpression, fromLiteralExpression, toElasticsearchQuery } from './ast'; import { nodeTypes } from '../node_types'; -import { IndexPatternBase } from '../..'; +import { DataViewBase } from '../..'; import { KueryNode } from '../types'; import { fields } from '../../filters/stubs'; jest.mock('../grammar'); describe('kuery AST API', () => { - let indexPattern: IndexPatternBase; + let indexPattern: DataViewBase; beforeEach(() => { indexPattern = { fields, + title: 'dataView', }; }); diff --git a/packages/kbn-es-query/src/kuery/functions/and.test.ts b/packages/kbn-es-query/src/kuery/functions/and.test.ts index 239342bdc0a1c..07e431791b37a 100644 --- a/packages/kbn-es-query/src/kuery/functions/and.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/and.test.ts @@ -10,7 +10,7 @@ import { nodeTypes } from '../node_types'; import { fields } from '../../filters/stubs'; import * as ast from '../ast'; import * as and from './and'; -import { IndexPatternBase } from '../../es_query'; +import { DataViewBase } from '../../es_query'; jest.mock('../grammar'); @@ -19,11 +19,12 @@ const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg'); describe('kuery functions', () => { describe('and', () => { - let indexPattern: IndexPatternBase; + let indexPattern: DataViewBase; beforeEach(() => { indexPattern = { fields, + title: 'dataView', }; }); diff --git a/packages/kbn-es-query/src/kuery/functions/exists.test.ts b/packages/kbn-es-query/src/kuery/functions/exists.test.ts index 0941e478e816b..ed368d6af2bfd 100644 --- a/packages/kbn-es-query/src/kuery/functions/exists.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/exists.test.ts @@ -8,7 +8,7 @@ import { nodeTypes } from '../node_types'; import { fields } from '../../filters/stubs'; -import { IndexPatternBase } from '../..'; +import { DataViewBase } from '../..'; jest.mock('../grammar'); @@ -17,11 +17,12 @@ import * as exists from './exists'; describe('kuery functions', () => { describe('exists', () => { - let indexPattern: IndexPatternBase; + let indexPattern: DataViewBase; beforeEach(() => { indexPattern = { fields, + title: 'dataView', }; }); diff --git a/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.test.ts b/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.test.ts index 45e6474b22986..9c4a33f50020f 100644 --- a/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/geo_bounding_box.test.ts @@ -9,7 +9,7 @@ import { get } from 'lodash'; import { nodeTypes } from '../node_types'; import { fields } from '../../filters/stubs'; -import { IndexPatternBase } from '../..'; +import { DataViewBase } from '../..'; import * as geoBoundingBox from './geo_bounding_box'; import { JsonObject } from '@kbn/utility-types'; @@ -29,11 +29,12 @@ const params = { describe('kuery functions', () => { describe('geoBoundingBox', () => { - let indexPattern: IndexPatternBase; + let indexPattern: DataViewBase; beforeEach(() => { indexPattern = { fields, + title: 'dataView', }; }); diff --git a/packages/kbn-es-query/src/kuery/functions/geo_polygon.test.ts b/packages/kbn-es-query/src/kuery/functions/geo_polygon.test.ts index 2d1d6ed6cd6de..5a96f6f3cb03d 100644 --- a/packages/kbn-es-query/src/kuery/functions/geo_polygon.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/geo_polygon.test.ts @@ -8,7 +8,7 @@ import { nodeTypes } from '../node_types'; import { fields } from '../../filters/stubs'; -import { IndexPatternBase } from '../..'; +import { DataViewBase } from '../..'; import * as geoPolygon from './geo_polygon'; @@ -31,11 +31,12 @@ const points = [ describe('kuery functions', () => { describe('geoPolygon', () => { - let indexPattern: IndexPatternBase; + let indexPattern: DataViewBase; beforeEach(() => { indexPattern = { fields, + title: 'dataView', }; }); diff --git a/packages/kbn-es-query/src/kuery/functions/is.test.ts b/packages/kbn-es-query/src/kuery/functions/is.test.ts index bc1bbb2508438..fbc6011331dbb 100644 --- a/packages/kbn-es-query/src/kuery/functions/is.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/is.test.ts @@ -10,18 +10,19 @@ import { nodeTypes } from '../node_types'; import { fields } from '../../filters/stubs'; import * as is from './is'; -import { IndexPatternBase } from '../..'; +import { DataViewBase } from '../..'; import { estypes } from '@elastic/elasticsearch'; jest.mock('../grammar'); describe('kuery functions', () => { describe('is', () => { - let indexPattern: IndexPatternBase; + let indexPattern: DataViewBase; beforeEach(() => { indexPattern = { fields, + title: 'dataView', }; }); diff --git a/packages/kbn-es-query/src/kuery/functions/nested.test.ts b/packages/kbn-es-query/src/kuery/functions/nested.test.ts index 7b6b7b695db2d..da7bfd2dce2d8 100644 --- a/packages/kbn-es-query/src/kuery/functions/nested.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/nested.test.ts @@ -8,7 +8,7 @@ import { nodeTypes } from '../node_types'; import { fields } from '../../filters/stubs'; -import { IndexPatternBase } from '../..'; +import { DataViewBase } from '../..'; import * as ast from '../ast'; @@ -20,11 +20,12 @@ const childNode = nodeTypes.function.buildNode('is', 'child', 'foo'); describe('kuery functions', () => { describe('nested', () => { - let indexPattern: IndexPatternBase; + let indexPattern: DataViewBase; beforeEach(() => { indexPattern = { fields, + title: 'dataView', }; }); diff --git a/packages/kbn-es-query/src/kuery/functions/not.test.ts b/packages/kbn-es-query/src/kuery/functions/not.test.ts index af1e8108b40f5..96af4bc693621 100644 --- a/packages/kbn-es-query/src/kuery/functions/not.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/not.test.ts @@ -8,7 +8,7 @@ import { nodeTypes } from '../node_types'; import { fields } from '../../filters/stubs'; -import { IndexPatternBase } from '../..'; +import { DataViewBase } from '../..'; import * as ast from '../ast'; import * as not from './not'; @@ -19,11 +19,12 @@ const childNode = nodeTypes.function.buildNode('is', 'extension', 'jpg'); describe('kuery functions', () => { describe('not', () => { - let indexPattern: IndexPatternBase; + let indexPattern: DataViewBase; beforeEach(() => { indexPattern = { fields, + title: 'dataView', }; }); diff --git a/packages/kbn-es-query/src/kuery/functions/or.test.ts b/packages/kbn-es-query/src/kuery/functions/or.test.ts index eb7db62b22162..63fde9e802401 100644 --- a/packages/kbn-es-query/src/kuery/functions/or.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/or.test.ts @@ -8,7 +8,7 @@ import { nodeTypes } from '../node_types'; import { fields } from '../../filters/stubs'; -import { IndexPatternBase } from '../..'; +import { DataViewBase } from '../..'; import * as ast from '../ast'; @@ -20,11 +20,12 @@ const childNode2 = nodeTypes.function.buildNode('is', 'extension', 'jpg'); describe('kuery functions', () => { describe('or', () => { - let indexPattern: IndexPatternBase; + let indexPattern: DataViewBase; beforeEach(() => { indexPattern = { fields, + title: 'dataView', }; }); diff --git a/packages/kbn-es-query/src/kuery/functions/range.test.ts b/packages/kbn-es-query/src/kuery/functions/range.test.ts index 42005a3fe702d..c541b26ce176f 100644 --- a/packages/kbn-es-query/src/kuery/functions/range.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/range.test.ts @@ -9,7 +9,7 @@ import { get } from 'lodash'; import { nodeTypes } from '../node_types'; import { fields } from '../../filters/stubs'; -import { IndexPatternBase } from '../..'; +import { DataViewBase } from '../..'; import { RangeFilterParams } from '../../filters'; import * as range from './range'; @@ -18,11 +18,12 @@ jest.mock('../grammar'); describe('kuery functions', () => { describe('range', () => { - let indexPattern: IndexPatternBase; + let indexPattern: DataViewBase; beforeEach(() => { indexPattern = { fields, + title: 'dataView', }; }); diff --git a/packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.test.ts b/packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.test.ts index dccfc5d1c463a..046ffdb152e42 100644 --- a/packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.test.ts +++ b/packages/kbn-es-query/src/kuery/functions/utils/get_full_field_name_node.test.ts @@ -8,17 +8,18 @@ import { nodeTypes } from '../../node_types'; import { fields } from '../../../filters/stubs'; -import { IndexPatternBase } from '../../..'; +import { DataViewBase } from '../../..'; import { getFullFieldNameNode } from './get_full_field_name_node'; jest.mock('../../grammar'); describe('getFullFieldNameNode', function () { - let indexPattern: IndexPatternBase; + let indexPattern: DataViewBase; beforeEach(() => { indexPattern = { fields, + title: 'dataView', }; }); diff --git a/packages/kbn-es-query/src/kuery/node_types/function.test.ts b/packages/kbn-es-query/src/kuery/node_types/function.test.ts index 5df6ba1916046..754bf170c5062 100644 --- a/packages/kbn-es-query/src/kuery/node_types/function.test.ts +++ b/packages/kbn-es-query/src/kuery/node_types/function.test.ts @@ -10,18 +10,19 @@ import { nodeTypes } from './index'; import { buildNode, buildNodeWithArgumentNodes, toElasticsearchQuery } from './function'; import { toElasticsearchQuery as isFunctionToElasticsearchQuery } from '../functions/is'; -import { IndexPatternBase } from '../../es_query'; +import { DataViewBase } from '../../es_query'; import { fields } from '../../filters/stubs/fields.mocks'; jest.mock('../grammar'); describe('kuery node types', () => { describe('function', () => { - let indexPattern: IndexPatternBase; + let indexPattern: DataViewBase; beforeEach(() => { indexPattern = { fields, + title: 'dataView', }; }); diff --git a/packages/kbn-i18n/BUILD.bazel b/packages/kbn-i18n/BUILD.bazel index 49d5603b2c516..dc98865feb4f1 100644 --- a/packages/kbn-i18n/BUILD.bazel +++ b/packages/kbn-i18n/BUILD.bazel @@ -50,6 +50,7 @@ TYPES_DEPS = [ "@npm//@types/angular", "@npm//@types/intl-relativeformat", "@npm//@types/jest", + "@npm//@types/node", "@npm//@types/prop-types", "@npm//@types/react", "@npm//@types/react-intl", diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index aaa697199f0a4..e5813f8f38139 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -108,7 +108,7 @@ pageLoadAssetSize: data: 491273 dataViews: 42532 fieldFormats: 65209 - kibanaReact: 99422 + kibanaReact: 84422 uiActions: 35121 dataEnhanced: 24980 embeddable: 87309 diff --git a/packages/kbn-optimizer/src/cli.ts b/packages/kbn-optimizer/src/cli.ts index 7f0c39ccd0e55..41ca656259fc6 100644 --- a/packages/kbn-optimizer/src/cli.ts +++ b/packages/kbn-optimizer/src/cli.ts @@ -207,7 +207,7 @@ export function runKbnOptimizerCli(options: { defaultLimitsPath: string }) { --no-inspect-workers when inspecting the parent process, don't inspect the workers --limits path to a limits.yml file to read, defaults to $KBN_OPTIMIZER_LIMITS_PATH or source file --validate-limits validate the limits.yml config to ensure that there are limits defined for every bundle - --update-limits run a build and rewrite the limits file to include the current bundle sizes +5kb + --update-limits run a build and rewrite the limits file to include the current bundle sizes +15kb `, }, } diff --git a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts index 31f763101c258..bf3d066d59f25 100644 --- a/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts +++ b/packages/kbn-securitysolution-io-ts-list-types/src/typescript_types/index.ts @@ -43,6 +43,7 @@ export interface UseExceptionListsProps { initialPagination?: Pagination; showTrustedApps: boolean; showEventFilters: boolean; + showHostIsolationExceptions: boolean; } export interface UseExceptionListProps { diff --git a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts index c0a5325377dc0..55c1d4dfaa853 100644 --- a/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts +++ b/packages/kbn-securitysolution-list-hooks/src/use_exception_lists/index.ts @@ -41,6 +41,7 @@ const DEFAULT_PAGINATION = { * @param notifications kibana service for displaying toasters * @param showTrustedApps boolean - include/exclude trusted app lists * @param showEventFilters boolean - include/exclude event filters lists + * @param showHostIsolationExceptions boolean - include/exclude host isolation exceptions lists * @param initialPagination * */ @@ -53,6 +54,7 @@ export const useExceptionLists = ({ notifications, showTrustedApps = false, showEventFilters = false, + showHostIsolationExceptions = false, }: UseExceptionListsProps): ReturnExceptionLists => { const [exceptionLists, setExceptionLists] = useState([]); const [pagination, setPagination] = useState(initialPagination); @@ -62,8 +64,14 @@ export const useExceptionLists = ({ const namespaceTypesAsString = useMemo(() => namespaceTypes.join(','), [namespaceTypes]); const filters = useMemo( (): string => - getFilters({ filters: filterOptions, namespaceTypes, showTrustedApps, showEventFilters }), - [namespaceTypes, filterOptions, showTrustedApps, showEventFilters] + getFilters({ + filters: filterOptions, + namespaceTypes, + showTrustedApps, + showEventFilters, + showHostIsolationExceptions, + }), + [namespaceTypes, filterOptions, showTrustedApps, showEventFilters, showHostIsolationExceptions] ); const fetchData = useCallback(async (): Promise => { diff --git a/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts index bfaad52ee8147..6484ac002d56d 100644 --- a/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts +++ b/packages/kbn-securitysolution-list-utils/src/get_filters/index.test.ts @@ -10,68 +10,86 @@ import { getFilters } from '.'; describe('getFilters', () => { describe('single', () => { - test('it properly formats when no filters passed and "showTrustedApps" is false', () => { + test('it properly formats when no filters passed "showTrustedApps", "showEventFilters", and "showHostIsolationExceptions" is false', () => { const filter = getFilters({ filters: {}, namespaceTypes: ['single'], showTrustedApps: false, showEventFilters: false, + showHostIsolationExceptions: false, }); expect(filter).toEqual( - '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - - test('it properly formats when no filters passed and "showTrustedApps" is true', () => { + test('it properly formats when no filters passed "showTrustedApps", "showEventFilters", and "showHostIsolationExceptions" is true', () => { const filter = getFilters({ filters: {}, namespaceTypes: ['single'], showTrustedApps: true, - showEventFilters: false, + showEventFilters: true, + showHostIsolationExceptions: true, }); expect(filter).toEqual( - '(exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + '(exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - test('it properly formats when filters passed and "showTrustedApps" is false', () => { + test('it properly formats when filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is false', () => { const filter = getFilters({ filters: { created_by: 'moi', name: 'Sample' }, namespaceTypes: ['single'], showTrustedApps: false, showEventFilters: false, + showHostIsolationExceptions: false, }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - test('it if filters passed and "showTrustedApps" is true', () => { + test('it properly formats when filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is true', () => { const filter = getFilters({ filters: { created_by: 'moi', name: 'Sample' }, namespaceTypes: ['single'], showTrustedApps: true, - showEventFilters: false, + showEventFilters: true, + showHostIsolationExceptions: true, }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - test('it properly formats when no filters passed and "showEventFilters" is false', () => { + test('it properly formats when no filters passed and "showTrustedApps" is true', () => { const filter = getFilters({ filters: {}, namespaceTypes: ['single'], - showTrustedApps: false, + showTrustedApps: true, + showEventFilters: false, + showHostIsolationExceptions: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)' + ); + }); + + test('it if filters passed and "showTrustedApps" is true', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single'], + showTrustedApps: true, showEventFilters: false, + showHostIsolationExceptions: false, }); expect(filter).toEqual( - '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); @@ -81,103 +99,138 @@ describe('getFilters', () => { namespaceTypes: ['single'], showTrustedApps: false, showEventFilters: true, + showHostIsolationExceptions: false, }); expect(filter).toEqual( - '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*)' + '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - test('it properly formats when filters passed and "showEventFilters" is false', () => { + test('it if filters passed and "showEventFilters" is true', () => { const filter = getFilters({ filters: { created_by: 'moi', name: 'Sample' }, namespaceTypes: ['single'], showTrustedApps: false, + showEventFilters: true, + showHostIsolationExceptions: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)' + ); + }); + + test('it properly formats when no filters passed and "showHostIsolationExceptions" is true', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single'], + showTrustedApps: false, showEventFilters: false, + showHostIsolationExceptions: true, }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*)' + '(not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - test('it if filters passed and "showEventFilters" is true', () => { + test('it if filters passed and "showHostIsolationExceptions" is true', () => { const filter = getFilters({ filters: { created_by: 'moi', name: 'Sample' }, namespaceTypes: ['single'], showTrustedApps: false, - showEventFilters: true, + showEventFilters: false, + showHostIsolationExceptions: true, }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters*)' + '(exception-list.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); }); describe('agnostic', () => { - test('it properly formats when no filters passed and "showTrustedApps" is false', () => { + test('it properly formats when no filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is false', () => { const filter = getFilters({ filters: {}, namespaceTypes: ['agnostic'], showTrustedApps: false, showEventFilters: false, + showHostIsolationExceptions: false, }); expect(filter).toEqual( - '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - test('it properly formats when no filters passed and "showTrustedApps" is true', () => { + test('it properly formats when no filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is true', () => { const filter = getFilters({ filters: {}, namespaceTypes: ['agnostic'], showTrustedApps: true, - showEventFilters: false, + showEventFilters: true, + showHostIsolationExceptions: true, }); expect(filter).toEqual( - '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - test('it properly formats when filters passed and "showTrustedApps" is false', () => { + test('it properly formats when filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is false', () => { const filter = getFilters({ filters: { created_by: 'moi', name: 'Sample' }, namespaceTypes: ['agnostic'], showTrustedApps: false, showEventFilters: false, + showHostIsolationExceptions: false, }); expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - - test('it if filters passed and "showTrustedApps" is true', () => { + test('it properly formats when filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is true', () => { const filter = getFilters({ filters: { created_by: 'moi', name: 'Sample' }, namespaceTypes: ['agnostic'], showTrustedApps: true, - showEventFilters: false, + showEventFilters: true, + showHostIsolationExceptions: true, }); expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - test('it properly formats when no filters passed and "showEventFilters" is false', () => { + test('it properly formats when no filters passed and "showTrustedApps" is true', () => { const filter = getFilters({ filters: {}, namespaceTypes: ['agnostic'], - showTrustedApps: false, + showTrustedApps: true, + showEventFilters: false, + showHostIsolationExceptions: false, + }); + + expect(filter).toEqual( + '(exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' + ); + }); + + test('it if filters passed and "showTrustedApps" is true', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['agnostic'], + showTrustedApps: true, showEventFilters: false, + showHostIsolationExceptions: false, }); expect(filter).toEqual( - '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); @@ -187,103 +240,138 @@ describe('getFilters', () => { namespaceTypes: ['agnostic'], showTrustedApps: false, showEventFilters: true, + showHostIsolationExceptions: false, }); expect(filter).toEqual( - '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - test('it properly formats when filters passed and "showEventFilters" is false', () => { + test('it if filters passed and "showEventFilters" is true', () => { const filter = getFilters({ filters: { created_by: 'moi', name: 'Sample' }, namespaceTypes: ['agnostic'], showTrustedApps: false, + showEventFilters: true, + showHostIsolationExceptions: false, + }); + + expect(filter).toEqual( + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' + ); + }); + + test('it properly formats when no filters passed and "showHostIsolationExceptions" is true', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['agnostic'], + showTrustedApps: false, showEventFilters: false, + showHostIsolationExceptions: true, }); expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - test('it if filters passed and "showEventFilters" is true', () => { + test('it if filters passed and "showHostIsolationExceptions" is true', () => { const filter = getFilters({ filters: { created_by: 'moi', name: 'Sample' }, namespaceTypes: ['agnostic'], showTrustedApps: false, - showEventFilters: true, + showEventFilters: false, + showHostIsolationExceptions: true, }); expect(filter).toEqual( - '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(exception-list-agnostic.attributes.created_by:moi) AND (exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); }); describe('single, agnostic', () => { - test('it properly formats when no filters passed and "showTrustedApps" is false', () => { + test('it properly formats when no filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is false', () => { const filter = getFilters({ filters: {}, namespaceTypes: ['single', 'agnostic'], showTrustedApps: false, showEventFilters: false, + showHostIsolationExceptions: false, }); expect(filter).toEqual( - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - - test('it properly formats when no filters passed and "showTrustedApps" is true', () => { + test('it properly formats when no filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is true', () => { const filter = getFilters({ filters: {}, namespaceTypes: ['single', 'agnostic'], showTrustedApps: true, - showEventFilters: false, + showEventFilters: true, + showHostIsolationExceptions: true, }); expect(filter).toEqual( - '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions* OR exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - test('it properly formats when filters passed and "showTrustedApps" is false', () => { + test('it properly formats when filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is false', () => { const filter = getFilters({ filters: { created_by: 'moi', name: 'Sample' }, namespaceTypes: ['single', 'agnostic'], showTrustedApps: false, showEventFilters: false, + showHostIsolationExceptions: false, }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - test('it properly formats when filters passed and "showTrustedApps" is true', () => { + test('it properly formats when filters passed and "showTrustedApps", "showEventFilters" and "showHostIsolationExceptions" is true', () => { const filter = getFilters({ filters: { created_by: 'moi', name: 'Sample' }, namespaceTypes: ['single', 'agnostic'], showTrustedApps: true, - showEventFilters: false, + showEventFilters: true, + showHostIsolationExceptions: true, }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions* OR exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - test('it properly formats when no filters passed and "showEventFilters" is false', () => { + test('it properly formats when no filters passed and "showTrustedApps" is true', () => { const filter = getFilters({ filters: {}, namespaceTypes: ['single', 'agnostic'], - showTrustedApps: false, + showTrustedApps: true, + showEventFilters: false, + showHostIsolationExceptions: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' + ); + }); + + test('it properly formats when filters passed and "showTrustedApps" is true', () => { + const filter = getFilters({ + filters: { created_by: 'moi', name: 'Sample' }, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: true, showEventFilters: false, + showHostIsolationExceptions: false, }); expect(filter).toEqual( - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); @@ -293,36 +381,52 @@ describe('getFilters', () => { namespaceTypes: ['single', 'agnostic'], showTrustedApps: false, showEventFilters: true, + showHostIsolationExceptions: false, }); expect(filter).toEqual( - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - test('it properly formats when filters passed and "showEventFilters" is false', () => { + test('it properly formats when filters passed and "showEventFilters" is true', () => { const filter = getFilters({ filters: { created_by: 'moi', name: 'Sample' }, namespaceTypes: ['single', 'agnostic'], showTrustedApps: false, + showEventFilters: true, + showHostIsolationExceptions: false, + }); + + expect(filter).toEqual( + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' + ); + }); + test('it properly formats when no filters passed and "showHostIsolationExceptions" is true', () => { + const filter = getFilters({ + filters: {}, + namespaceTypes: ['single', 'agnostic'], + showTrustedApps: false, showEventFilters: false, + showHostIsolationExceptions: true, }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions* OR exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); - test('it properly formats when filters passed and "showEventFilters" is true', () => { + test('it properly formats when filters passed and "showHostIsolationExceptions" is true', () => { const filter = getFilters({ filters: { created_by: 'moi', name: 'Sample' }, namespaceTypes: ['single', 'agnostic'], showTrustedApps: false, - showEventFilters: true, + showEventFilters: false, + showHostIsolationExceptions: true, }); expect(filter).toEqual( - '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)' + '(exception-list.attributes.created_by:moi OR exception-list-agnostic.attributes.created_by:moi) AND (exception-list.attributes.name.text:Sample OR exception-list-agnostic.attributes.name.text:Sample) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions* OR exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' ); }); }); diff --git a/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts b/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts index 238ae5541343c..e8e9e6a581828 100644 --- a/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts +++ b/packages/kbn-securitysolution-list-utils/src/get_filters/index.ts @@ -11,12 +11,14 @@ import { getGeneralFilters } from '../get_general_filters'; import { getSavedObjectTypes } from '../get_saved_object_types'; import { getTrustedAppsFilter } from '../get_trusted_apps_filter'; import { getEventFiltersFilter } from '../get_event_filters_filter'; +import { getHostIsolationExceptionsFilter } from '../get_host_isolation_exceptions_filter'; export interface GetFiltersParams { filters: ExceptionListFilter; namespaceTypes: NamespaceType[]; showTrustedApps: boolean; showEventFilters: boolean; + showHostIsolationExceptions: boolean; } export const getFilters = ({ @@ -24,12 +26,17 @@ export const getFilters = ({ namespaceTypes, showTrustedApps, showEventFilters, + showHostIsolationExceptions, }: GetFiltersParams): string => { const namespaces = getSavedObjectTypes({ namespaceType: namespaceTypes }); const generalFilters = getGeneralFilters(filters, namespaces); const trustedAppsFilter = getTrustedAppsFilter(showTrustedApps, namespaces); const eventFiltersFilter = getEventFiltersFilter(showEventFilters, namespaces); - return [generalFilters, trustedAppsFilter, eventFiltersFilter] + const hostIsolationExceptionsFilter = getHostIsolationExceptionsFilter( + showHostIsolationExceptions, + namespaces + ); + return [generalFilters, trustedAppsFilter, eventFiltersFilter, hostIsolationExceptionsFilter] .filter((filter) => filter.trim() !== '') .join(' AND '); }; diff --git a/packages/kbn-securitysolution-list-utils/src/get_host_isolation_exceptions_filter/index.test.ts b/packages/kbn-securitysolution-list-utils/src/get_host_isolation_exceptions_filter/index.test.ts new file mode 100644 index 0000000000000..30466f459cf65 --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_host_isolation_exceptions_filter/index.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { getHostIsolationExceptionsFilter } from '.'; + +describe('getHostIsolationExceptionsFilter', () => { + test('it returns filter to search for "exception-list" namespace host isolation exceptions', () => { + const filter = getHostIsolationExceptionsFilter(true, ['exception-list']); + + expect(filter).toEqual( + '(exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)' + ); + }); + + test('it returns filter to search for "exception-list" and "agnostic" namespace host isolation exceptions', () => { + const filter = getHostIsolationExceptionsFilter(true, [ + 'exception-list', + 'exception-list-agnostic', + ]); + + expect(filter).toEqual( + '(exception-list.attributes.list_id: endpoint_host_isolation_exceptions* OR exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' + ); + }); + + test('it returns filter to exclude "exception-list" namespace host isolation exceptions', () => { + const filter = getHostIsolationExceptionsFilter(false, ['exception-list']); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_host_isolation_exceptions*)' + ); + }); + + test('it returns filter to exclude "exception-list" and "agnostic" namespace host isolation exceptions', () => { + const filter = getHostIsolationExceptionsFilter(false, [ + 'exception-list', + 'exception-list-agnostic', + ]); + + expect(filter).toEqual( + '(not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)' + ); + }); +}); diff --git a/packages/kbn-securitysolution-list-utils/src/get_host_isolation_exceptions_filter/index.ts b/packages/kbn-securitysolution-list-utils/src/get_host_isolation_exceptions_filter/index.ts new file mode 100644 index 0000000000000..d61f8fe7dac19 --- /dev/null +++ b/packages/kbn-securitysolution-list-utils/src/get_host_isolation_exceptions_filter/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID } from '@kbn/securitysolution-list-constants'; +import { SavedObjectType } from '../types'; + +export const getHostIsolationExceptionsFilter = ( + showFilter: boolean, + namespaceTypes: SavedObjectType[] +): string => { + if (showFilter) { + const filters = namespaceTypes.map((namespace) => { + return `${namespace}.attributes.list_id: ${ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID}*`; + }); + return `(${filters.join(' OR ')})`; + } else { + const filters = namespaceTypes.map((namespace) => { + return `not ${namespace}.attributes.list_id: ${ENDPOINT_HOST_ISOLATION_EXCEPTIONS_LIST_ID}*`; + }); + return `(${filters.join(' AND ')})`; + } +}; diff --git a/packages/kbn-test/src/jest/run.ts b/packages/kbn-test/src/jest/run.ts index 07610a3eb84c6..4a5dd4e9281ba 100644 --- a/packages/kbn-test/src/jest/run.ts +++ b/packages/kbn-test/src/jest/run.ts @@ -28,6 +28,20 @@ import { map } from 'lodash'; // yarn test:jest src/core/public/core_system.test.ts // :kibana/src/core/server/saved_objects yarn test:jest +// Patch node 16 types to be compatible with jest 26 +// https://github.com/facebook/jest/issues/11640#issuecomment-893867514 +/* eslint-disable */ +declare global { + namespace NodeJS { + interface Global {} + interface InspectOptions {} + + interface ConsoleConstructor + extends console.ConsoleConstructor {} + } +} +/* eslint-enable */ + export function runJest(configName = 'jest.config.js') { const argv = buildArgv(process.argv); diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 2da4370242e6c..f39ad9d5f8cde 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -736,10 +736,9 @@ export interface DocLinksStart { // Warning: (ae-forgotten-export) The symbol "DeprecationsDetails" needs to be exported by the entry point index.d.ts // // @internal (undocumented) -export interface DomainDeprecationDetails extends DeprecationsDetails { - // (undocumented) +export type DomainDeprecationDetails = DeprecationsDetails & { domainId: string; -} +}; export { EnvironmentMode } diff --git a/src/core/server/bootstrap.ts b/src/core/server/bootstrap.ts index 5131defc93461..6190665fc78e4 100644 --- a/src/core/server/bootstrap.ts +++ b/src/core/server/bootstrap.ts @@ -51,7 +51,7 @@ export async function bootstrap({ configs, cliArgs, applyConfigOverrides }: Boot // This is only used by the LogRotator service // in order to be able to reload the log configuration // under the cluster mode - process.on('message', (msg) => { + process.on('message', (msg: any) => { if (!msg || msg.reloadConfiguration !== true) { return; } diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index 6909375408fb4..1cf67f479f9b3 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -12,6 +12,7 @@ import { ConfigDeprecationProvider, ConfigDeprecation } from '@kbn/config'; const kibanaPathConf: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (process.env?.KIBANA_PATH_CONF) { addDeprecation({ + configPath: 'env.KIBANA_PATH_CONF', message: `Environment variable "KIBANA_PATH_CONF" is deprecated. It has been replaced with "KBN_PATH_CONF" pointing to a config folder`, correctiveActions: { manualSteps: [ @@ -25,6 +26,7 @@ const kibanaPathConf: ConfigDeprecation = (settings, fromPath, addDeprecation) = const configPathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (process.env?.CONFIG_PATH) { addDeprecation({ + configPath: 'env.CONFIG_PATH', message: `Environment variable "CONFIG_PATH" is deprecated. It has been replaced with "KBN_PATH_CONF" pointing to a config folder`, correctiveActions: { manualSteps: ['Use "KBN_PATH_CONF" instead of "CONFIG_PATH" to point to a config folder.'], @@ -36,6 +38,7 @@ const configPathDeprecation: ConfigDeprecation = (settings, fromPath, addDepreca const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (process.env?.DATA_PATH) { addDeprecation({ + configPath: 'env.DATA_PATH', message: `Environment variable "DATA_PATH" will be removed. It has been replaced with kibana.yml setting "path.data"`, correctiveActions: { manualSteps: [ @@ -49,6 +52,8 @@ const dataPathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecati const rewriteBasePathDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (settings.server?.basePath && !settings.server?.rewriteBasePath) { addDeprecation({ + configPath: 'server.basePath', + title: 'Setting "server.rewriteBasePath" should be set when using "server.basePath"', message: 'You should set server.basePath along with server.rewriteBasePath. Starting in 7.0, Kibana ' + 'will expect that all requests start with server.basePath rather than expecting you to rewrite ' + @@ -69,6 +74,8 @@ const rewriteCorsSettings: ConfigDeprecation = (settings, fromPath, addDeprecati const corsSettings = settings.server?.cors; if (typeof corsSettings === 'boolean') { addDeprecation({ + configPath: 'server.cors', + title: 'Setting "server.cors" is deprecated', message: '"server.cors" is deprecated and has been replaced by "server.cors.enabled"', correctiveActions: { manualSteps: [ @@ -105,6 +112,7 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecati value: [...parsed].map(([policy, sourceList]) => { if (sourceList.find((source) => source.includes(NONCE_STRING))) { addDeprecation({ + configPath: 'csp.rules', message: `csp.rules no longer supports the {nonce} syntax. Replacing with 'self' in ${policy}`, correctiveActions: { manualSteps: [`Replace {nonce} syntax with 'self' in ${policy}`], @@ -123,6 +131,7 @@ const cspRulesDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecati !sourceList.find((source) => source.includes(SELF_STRING)) ) { addDeprecation({ + configPath: 'csp.rules', message: `csp.rules must contain the 'self' source. Automatically adding to ${policy}.`, correctiveActions: { manualSteps: [`Add 'self' source to ${policy}.`], @@ -146,6 +155,7 @@ const mapManifestServiceUrlDeprecation: ConfigDeprecation = ( ) => { if (settings.map?.manifestServiceUrl) { addDeprecation({ + configPath: 'map.manifestServiceUrl', message: 'You should no longer use the map.manifestServiceUrl setting in kibana.yml to configure the location ' + 'of the Elastic Maps Service settings. These settings have moved to the "map.emsTileApiUrl" and ' + @@ -164,6 +174,7 @@ const mapManifestServiceUrlDeprecation: ConfigDeprecation = ( const serverHostZeroDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (settings.server?.host === '0') { addDeprecation({ + configPath: 'server.host', message: 'Support for setting server.host to "0" in kibana.yml is deprecated and will be removed in Kibana version 8.0.0. ' + 'Instead use "0.0.0.0" to bind to all interfaces.', @@ -194,6 +205,7 @@ const opsLoggingEventDeprecation: ConfigDeprecation = ( ) => { if (settings.logging?.events?.ops) { addDeprecation({ + configPath: 'logging.events.ops', documentationUrl: `https://github.com/elastic/kibana/blob/${branch}/src/core/server/logging/README.mdx#loggingevents`, title: i18n.translate('core.deprecations.loggingEventsOps.deprecationTitle', { defaultMessage: `Setting "logging.events.ops" is deprecated`, @@ -224,6 +236,7 @@ const requestLoggingEventDeprecation: ConfigDeprecation = ( ) => { if (settings.logging?.events?.request) { addDeprecation({ + configPath: 'logging.events.request', documentationUrl: `https://github.com/elastic/kibana/blob/${branch}/src/core/server/logging/README.mdx#loggingevents`, title: i18n.translate('core.deprecations.loggingEventsRequest.deprecationTitle', { defaultMessage: `Setting "logging.events.request" is deprecated`, @@ -254,6 +267,7 @@ const responseLoggingEventDeprecation: ConfigDeprecation = ( ) => { if (settings.logging?.events?.response) { addDeprecation({ + configPath: 'logging.events.response', documentationUrl: `https://github.com/elastic/kibana/blob/${branch}/src/core/server/logging/README.mdx#loggingevents`, title: i18n.translate('core.deprecations.loggingEventsResponse.deprecationTitle', { defaultMessage: `Setting "logging.events.response" is deprecated`, @@ -284,6 +298,7 @@ const timezoneLoggingDeprecation: ConfigDeprecation = ( ) => { if (settings.logging?.timezone) { addDeprecation({ + configPath: 'logging.timezone', documentationUrl: `https://github.com/elastic/kibana/blob/${branch}/src/core/server/logging/README.mdx#loggingtimezone`, title: i18n.translate('core.deprecations.loggingTimezone.deprecationTitle', { defaultMessage: `Setting "logging.timezone" is deprecated`, @@ -314,6 +329,7 @@ const destLoggingDeprecation: ConfigDeprecation = ( ) => { if (settings.logging?.dest) { addDeprecation({ + configPath: 'logging.dest', documentationUrl: `https://github.com/elastic/kibana/blob/${branch}/src/core/server/logging/README.mdx#loggingdest`, title: i18n.translate('core.deprecations.loggingDest.deprecationTitle', { defaultMessage: `Setting "logging.dest" is deprecated`, @@ -344,6 +360,7 @@ const quietLoggingDeprecation: ConfigDeprecation = ( ) => { if (settings.logging?.quiet) { addDeprecation({ + configPath: 'logging.quiet', documentationUrl: `https://github.com/elastic/kibana/blob/${branch}/src/core/server/logging/README.mdx#loggingquiet`, title: i18n.translate('core.deprecations.loggingQuiet.deprecationTitle', { defaultMessage: `Setting "logging.quiet" is deprecated`, @@ -373,6 +390,7 @@ const silentLoggingDeprecation: ConfigDeprecation = ( ) => { if (settings.logging?.silent) { addDeprecation({ + configPath: 'logging.silent', documentationUrl: `https://github.com/elastic/kibana/blob/${branch}/src/core/server/logging/README.mdx#loggingsilent`, title: i18n.translate('core.deprecations.loggingSilent.deprecationTitle', { defaultMessage: `Setting "logging.silent" is deprecated`, @@ -402,6 +420,7 @@ const verboseLoggingDeprecation: ConfigDeprecation = ( ) => { if (settings.logging?.verbose) { addDeprecation({ + configPath: 'logging.verbose', documentationUrl: `https://github.com/elastic/kibana/blob/${branch}/src/core/server/logging/README.mdx#loggingverbose`, title: i18n.translate('core.deprecations.loggingVerbose.deprecationTitle', { defaultMessage: `Setting "logging.verbose" is deprecated`, @@ -435,6 +454,7 @@ const jsonLoggingDeprecation: ConfigDeprecation = ( // ` legacyLoggingConfigSchema` returns `true` for the TTY check on `process.stdout.isTTY` if (settings.logging?.json && settings.env !== 'development') { addDeprecation({ + configPath: 'logging.json', documentationUrl: `https://github.com/elastic/kibana/blob/${branch}/src/core/server/logging/README.mdx`, title: i18n.translate('core.deprecations.loggingJson.deprecationTitle', { defaultMessage: `Setting "logging.json" is deprecated`, @@ -466,6 +486,7 @@ const logRotateDeprecation: ConfigDeprecation = ( ) => { if (settings.logging?.rotate) { addDeprecation({ + configPath: 'logging.rotate', documentationUrl: `https://github.com/elastic/kibana/blob/${branch}/src/core/server/logging/README.mdx#rolling-file-appender`, title: i18n.translate('core.deprecations.loggingRotate.deprecationTitle', { defaultMessage: `Setting "logging.rotate" is deprecated`, @@ -496,6 +517,7 @@ const logEventsLogDeprecation: ConfigDeprecation = ( ) => { if (settings.logging?.events?.log) { addDeprecation({ + configPath: 'logging.events.log', documentationUrl: `https://github.com/elastic/kibana/blob/${branch}/src/core/server/logging/README.mdx#loggingevents`, title: i18n.translate('core.deprecations.loggingEventsLog.deprecationTitle', { defaultMessage: `Setting "logging.events.log" is deprecated`, @@ -525,6 +547,7 @@ const logEventsErrorDeprecation: ConfigDeprecation = ( ) => { if (settings.logging?.events?.error) { addDeprecation({ + configPath: 'logging.events.error', documentationUrl: `https://github.com/elastic/kibana/blob/${branch}/src/core/server/logging/README.mdx#loggingevents`, title: i18n.translate('core.deprecations.loggingEventsError.deprecationTitle', { defaultMessage: `Setting "logging.events.error" is deprecated`, @@ -554,6 +577,7 @@ const logFilterDeprecation: ConfigDeprecation = ( ) => { if (settings.logging?.filter) { addDeprecation({ + configPath: 'logging.filter', documentationUrl: `https://github.com/elastic/kibana/blob/${branch}/src/core/server/logging/README.mdx#loggingfilter`, title: i18n.translate('core.deprecations.loggingFilter.deprecationTitle', { defaultMessage: `Setting "logging.filter" is deprecated`, diff --git a/src/core/server/csp/config.ts b/src/core/server/csp/config.ts index 16a2fa4e62894..6697eda62597b 100644 --- a/src/core/server/csp/config.ts +++ b/src/core/server/csp/config.ts @@ -137,6 +137,7 @@ export const config: ServiceConfigDescriptor = { const cspConfig = rawConfig[fromPath]; if (cspConfig?.rules) { addDeprecation({ + configPath: 'csp.rules', message: '`csp.rules` is deprecated in favor of directive specific configuration. Please use `csp.connect_src`, ' + '`csp.default_src`, `csp.font_src`, `csp.frame_ancestors`, `csp.frame_src`, `csp.img_src`, ' + diff --git a/src/core/server/deprecations/deprecation_config.ts b/src/core/server/deprecations/deprecation_config.ts new file mode 100644 index 0000000000000..fb5ff7e5957f5 --- /dev/null +++ b/src/core/server/deprecations/deprecation_config.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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, TypeOf } from '@kbn/config-schema'; +import { ServiceConfigDescriptor } from '../internal_types'; + +const configSchema = schema.object({ + // `deprecation.skip_deprecated_settings` is consistent with the equivalent ES feature and config property + skip_deprecated_settings: schema.arrayOf(schema.string(), { defaultValue: [] }), +}); + +export type DeprecationConfigType = TypeOf; + +export const config: ServiceConfigDescriptor = { + path: 'deprecation', + schema: configSchema, +}; diff --git a/src/core/server/deprecations/deprecations_factory.test.ts b/src/core/server/deprecations/deprecations_factory.test.ts index 85ef521538b15..9a67c6dc36dd2 100644 --- a/src/core/server/deprecations/deprecations_factory.test.ts +++ b/src/core/server/deprecations/deprecations_factory.test.ts @@ -7,20 +7,26 @@ */ import type { GetDeprecationsContext } from './types'; -import { DeprecationsFactory } from './deprecations_factory'; +import { DeprecationsFactory, DeprecationsFactoryConfig } from './deprecations_factory'; import { loggerMock } from '../logging/logger.mock'; +import { DeprecationsDetails } from './types'; describe('DeprecationsFactory', () => { - const logger = loggerMock.create(); + let logger: ReturnType; + let config: DeprecationsFactoryConfig; + beforeEach(() => { - loggerMock.clear(logger); + logger = loggerMock.create(); + config = { + ignoredConfigDeprecations: [], + }; }); describe('getRegistry', () => { const domainId = 'test-plugin'; it('creates a registry for a domainId', async () => { - const deprecationsFactory = new DeprecationsFactory({ logger }); + const deprecationsFactory = new DeprecationsFactory({ logger, config }); const registry = deprecationsFactory.getRegistry(domainId); expect(registry).toHaveProperty('registerDeprecations'); @@ -28,7 +34,7 @@ describe('DeprecationsFactory', () => { }); it('creates one registry for a domainId', async () => { - const deprecationsFactory = new DeprecationsFactory({ logger }); + const deprecationsFactory = new DeprecationsFactory({ logger, config }); const registry = deprecationsFactory.getRegistry(domainId); const sameRegistry = deprecationsFactory.getRegistry(domainId); @@ -36,7 +42,7 @@ describe('DeprecationsFactory', () => { }); it('returns a registered registry', () => { - const deprecationsFactory = new DeprecationsFactory({ logger }); + const deprecationsFactory = new DeprecationsFactory({ logger, config }); const mockRegistry = 'mock-reg'; const mockRegistries = { set: jest.fn(), @@ -61,7 +67,7 @@ describe('DeprecationsFactory', () => { } as unknown as GetDeprecationsContext; it('returns a flattened array of deprecations', async () => { - const deprecationsFactory = new DeprecationsFactory({ logger }); + const deprecationsFactory = new DeprecationsFactory({ logger, config }); const mockPluginDeprecationsInfo = [ { message: 'mockPlugin message', @@ -97,8 +103,8 @@ describe('DeprecationsFactory', () => { getDeprecations: jest.fn().mockResolvedValue(anotherMockPluginDeprecationsInfo), }); - const derpecations = await deprecationsFactory.getAllDeprecations(mockDependencies); - expect(derpecations).toStrictEqual( + const deprecations = await deprecationsFactory.getAllDeprecations(mockDependencies); + expect(deprecations).toStrictEqual( [ mockPluginDeprecationsInfo.map((info) => ({ ...info, domainId: 'mockPlugin' })), anotherMockPluginDeprecationsInfo.map((info) => ({ @@ -110,7 +116,7 @@ describe('DeprecationsFactory', () => { }); it(`returns a failure message for failed getDeprecations functions`, async () => { - const deprecationsFactory = new DeprecationsFactory({ logger }); + const deprecationsFactory = new DeprecationsFactory({ logger, config }); const domainId = 'mockPlugin'; const mockError = new Error(); @@ -142,7 +148,7 @@ describe('DeprecationsFactory', () => { }); it(`returns successful results even when some getDeprecations fail`, async () => { - const deprecationsFactory = new DeprecationsFactory({ logger }); + const deprecationsFactory = new DeprecationsFactory({ logger, config }); const mockPluginRegistry = deprecationsFactory.getRegistry('mockPlugin'); const anotherMockPluginRegistry = deprecationsFactory.getRegistry('anotherMockPlugin'); const mockError = new Error(); @@ -161,14 +167,14 @@ describe('DeprecationsFactory', () => { anotherMockPluginRegistry.registerDeprecations({ getDeprecations: jest.fn().mockRejectedValue(mockError), }); - const derpecations = await deprecationsFactory.getAllDeprecations(mockDependencies); + const deprecations = await deprecationsFactory.getAllDeprecations(mockDependencies); expect(logger.warn).toBeCalledTimes(1); expect(logger.warn).toBeCalledWith( `Failed to get deprecations info for plugin "anotherMockPlugin".`, mockError ); - expect(derpecations).toStrictEqual([ + expect(deprecations).toStrictEqual([ ...mockPluginDeprecationsInfo.map((info) => ({ ...info, domainId: 'mockPlugin' })), { domainId: 'anotherMockPlugin', @@ -181,6 +187,123 @@ describe('DeprecationsFactory', () => { }, ]); }); + + it('excludes config deprecations explicitly ignored via `ignoredConfigDeprecations`', async () => { + const deprecationsFactory = new DeprecationsFactory({ + logger, + config: { + ignoredConfigDeprecations: ['mockPlugin.foo', 'anotherMockPlugin.bar'], + }, + }); + const mockPluginDeprecationsInfo: DeprecationsDetails[] = [ + { + configPath: 'mockPlugin.foo', + title: 'mockPlugin.foo is deprecated', + message: 'mockPlugin.foo is deprecated and will be removed in a future Kibana version', + level: 'critical', + deprecationType: 'config', + correctiveActions: { + manualSteps: ['come on', 'do something'], + }, + }, + { + configPath: 'mockPlugin.bar', + title: 'mockPlugin.bar is deprecated', + message: 'mockPlugin.bar is deprecated and will be removed in a future Kibana version', + level: 'critical', + deprecationType: 'config', + correctiveActions: { + manualSteps: ['come on', 'do something'], + }, + }, + ]; + const anotherMockPluginDeprecationsInfo: DeprecationsDetails[] = [ + { + configPath: 'anotherMockPlugin.foo', + title: 'anotherMockPlugin.foo is deprecated', + message: + 'anotherMockPlugin.foo is deprecated and will be removed in a future Kibana version', + level: 'critical', + deprecationType: 'config', + correctiveActions: { + manualSteps: ['come on', 'do something'], + }, + }, + { + configPath: 'anotherMockPlugin.bar', + title: 'anotherMockPlugin.bar is deprecated', + message: + 'anotherMockPlugin.bar is deprecated and will be removed in a future Kibana version', + level: 'critical', + deprecationType: 'config', + correctiveActions: { + manualSteps: ['come on', 'do something'], + }, + }, + ]; + + const mockPluginRegistry = deprecationsFactory.getRegistry('mockPlugin'); + mockPluginRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockResolvedValue(mockPluginDeprecationsInfo), + }); + + const anotherMockPluginRegistry = deprecationsFactory.getRegistry('anotherMockPlugin'); + anotherMockPluginRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockResolvedValue(anotherMockPluginDeprecationsInfo), + }); + + const deprecations = await deprecationsFactory.getAllDeprecations(mockDependencies); + + expect(deprecations).toHaveLength(2); + expect(deprecations).toEqual([ + expect.objectContaining({ + configPath: 'mockPlugin.bar', + title: 'mockPlugin.bar is deprecated', + }), + expect.objectContaining({ + configPath: 'anotherMockPlugin.foo', + title: 'anotherMockPlugin.foo is deprecated', + }), + ]); + }); + + it('does not throw when configured with paths not matching any deprecation', async () => { + const deprecationsFactory = new DeprecationsFactory({ + logger, + config: { + ignoredConfigDeprecations: ['unknown.bar'], + }, + }); + const mockPluginDeprecationsInfo: DeprecationsDetails[] = [ + { + configPath: 'mockPlugin.foo', + title: 'mockPlugin.foo is deprecated', + message: 'mockPlugin.foo is deprecated and will be removed in a future Kibana version', + level: 'critical', + deprecationType: 'config', + correctiveActions: { + manualSteps: ['come on', 'do something'], + }, + }, + { + configPath: 'mockPlugin.bar', + title: 'mockPlugin.bar is deprecated', + message: 'mockPlugin.bar is deprecated and will be removed in a future Kibana version', + level: 'critical', + deprecationType: 'config', + correctiveActions: { + manualSteps: ['come on', 'do something'], + }, + }, + ]; + + const mockPluginRegistry = deprecationsFactory.getRegistry('mockPlugin'); + mockPluginRegistry.registerDeprecations({ + getDeprecations: jest.fn().mockResolvedValue(mockPluginDeprecationsInfo), + }); + + await expect(deprecationsFactory.getAllDeprecations(mockDependencies)).resolves.toBeDefined(); + }); }); describe('getDeprecations', () => { @@ -190,7 +313,7 @@ describe('DeprecationsFactory', () => { } as unknown as GetDeprecationsContext; it('returns a flattened array of DeprecationInfo', async () => { - const deprecationsFactory = new DeprecationsFactory({ logger }); + const deprecationsFactory = new DeprecationsFactory({ logger, config }); const deprecationsRegistry = deprecationsFactory.getRegistry('mockPlugin'); const deprecationsBody = [ { @@ -215,40 +338,62 @@ describe('DeprecationsFactory', () => { getDeprecations: jest.fn().mockResolvedValue(deprecationsBody), }); - const derpecations = await deprecationsFactory.getDeprecations( + const deprecations = await deprecationsFactory.getDeprecations( 'mockPlugin', mockDependencies ); - expect(derpecations).toStrictEqual( + expect(deprecations).toStrictEqual( deprecationsBody.flat().map((body) => ({ ...body, domainId: 'mockPlugin' })) ); }); - it('removes empty entries from the returned array', async () => { - const deprecationsFactory = new DeprecationsFactory({ logger }); + it('excludes config deprecations explicitly ignored via `ignoredConfigDeprecations`', async () => { + const deprecationsFactory = new DeprecationsFactory({ + logger, + config: { + ignoredConfigDeprecations: ['test.foo'], + }, + }); const deprecationsRegistry = deprecationsFactory.getRegistry('mockPlugin'); - const deprecationsBody = [ + const deprecationsBody: DeprecationsDetails[] = [ { - message: 'mockPlugin message', + configPath: 'test.foo', + title: 'test.foo is deprecated', + message: 'test.foo is deprecated and will be removed in a future Kibana version', level: 'critical', + deprecationType: 'config', correctiveActions: { - manualSteps: ['mockPlugin step 1', 'mockPlugin step 2'], + manualSteps: ['come on', 'do something'], + }, + }, + { + configPath: 'test.bar', + title: 'test.bar is deprecated', + message: 'test.bar is deprecated and will be removed in a future Kibana version', + level: 'critical', + deprecationType: 'config', + correctiveActions: { + manualSteps: ['come on', 'do something'], }, }, - [undefined], - undefined, ]; deprecationsRegistry.registerDeprecations({ getDeprecations: jest.fn().mockResolvedValue(deprecationsBody), }); - const derpecations = await deprecationsFactory.getDeprecations( + const deprecations = await deprecationsFactory.getDeprecations( 'mockPlugin', mockDependencies ); - expect(derpecations).toHaveLength(1); - expect(derpecations).toStrictEqual([{ ...deprecationsBody[0], domainId: 'mockPlugin' }]); + expect(deprecations).toHaveLength(1); + expect(deprecations[0]).toEqual( + expect.objectContaining({ + deprecationType: 'config', + configPath: 'test.bar', + title: 'test.bar is deprecated', + }) + ); }); }); }); diff --git a/src/core/server/deprecations/deprecations_factory.ts b/src/core/server/deprecations/deprecations_factory.ts index 9905f0b26b4f3..ad28af2db528e 100644 --- a/src/core/server/deprecations/deprecations_factory.ts +++ b/src/core/server/deprecations/deprecations_factory.ts @@ -17,13 +17,21 @@ import type { export interface DeprecationsFactoryDeps { logger: Logger; + config: DeprecationsFactoryConfig; +} + +export interface DeprecationsFactoryConfig { + ignoredConfigDeprecations: string[]; } export class DeprecationsFactory { private readonly registries: Map = new Map(); private readonly logger: Logger; - constructor({ logger }: DeprecationsFactoryDeps) { + private readonly config: DeprecationsFactoryConfig; + + constructor({ logger, config }: DeprecationsFactoryDeps) { this.logger = logger; + this.config = config; } public getRegistry = (domainId: string): DeprecationsRegistry => { @@ -41,7 +49,7 @@ export class DeprecationsFactory { dependencies: GetDeprecationsContext ): Promise => { const infoBody = await this.getDeprecationsBody(domainId, dependencies); - return this.createDeprecationInfo(domainId, infoBody).flat(); + return this.createDeprecationInfo(domainId, infoBody); }; public getAllDeprecations = async ( @@ -63,13 +71,10 @@ export class DeprecationsFactory { domainId: string, deprecationInfoBody: DeprecationsDetails[] ): DomainDeprecationDetails[] => { - return deprecationInfoBody - .flat() - .filter(Boolean) - .map((pluginDeprecation) => ({ - ...pluginDeprecation, - domainId, - })); + return deprecationInfoBody.map((pluginDeprecation) => ({ + ...pluginDeprecation, + domainId, + })); }; private getDeprecationsBody = async ( @@ -113,7 +118,7 @@ export class DeprecationsFactory { ]; } - return settledResult.value; + return filterIgnoredDeprecations(settledResult.value.flat(), this.config); }); } catch (err) { this.logger.warn(`Failed to get deprecations info for plugin "${domainId}".`, err); @@ -121,3 +126,15 @@ export class DeprecationsFactory { } }; } + +const filterIgnoredDeprecations = ( + deprecations: DeprecationsDetails[], + config: DeprecationsFactoryConfig +): DeprecationsDetails[] => { + return deprecations.filter((deprecation) => { + if (deprecation.deprecationType === 'config') { + return !config.ignoredConfigDeprecations.includes(deprecation.configPath); + } + return true; + }); +}; diff --git a/src/core/server/deprecations/deprecations_service.mock.ts b/src/core/server/deprecations/deprecations_service.mock.ts index 2f9c8bade6405..c274c4409cce2 100644 --- a/src/core/server/deprecations/deprecations_service.mock.ts +++ b/src/core/server/deprecations/deprecations_service.mock.ts @@ -50,7 +50,7 @@ const createDeprecationsServiceMock = () => { stop: jest.fn(), }; - mocked.setup.mockReturnValue(createInternalSetupContractMock()); + mocked.setup.mockResolvedValue(createInternalSetupContractMock()); return mocked; }; diff --git a/src/core/server/deprecations/deprecations_service.test.mocks.ts b/src/core/server/deprecations/deprecations_service.test.mocks.ts new file mode 100644 index 0000000000000..3174698725f96 --- /dev/null +++ b/src/core/server/deprecations/deprecations_service.test.mocks.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 { mockDeprecationsFactory } from './deprecations_factory.mock'; + +export const mockedDeprecationFactoryInstance = mockDeprecationsFactory.create(); +export const DeprecationsFactoryMock = jest + .fn() + .mockImplementation(() => mockedDeprecationFactoryInstance); + +jest.doMock('./deprecations_factory', () => ({ + DeprecationsFactory: DeprecationsFactoryMock, +})); diff --git a/src/core/server/deprecations/deprecations_service.test.ts b/src/core/server/deprecations/deprecations_service.test.ts index 0067cff1d2306..256cfdaa666b3 100644 --- a/src/core/server/deprecations/deprecations_service.test.ts +++ b/src/core/server/deprecations/deprecations_service.test.ts @@ -6,42 +6,69 @@ * Side Public License, v 1. */ +import { DeprecationsFactoryMock } from './deprecations_service.test.mocks'; + /* eslint-disable dot-notation */ -import { DeprecationsService } from './deprecations_service'; +import { DeprecationsService, DeprecationsSetupDeps } from './deprecations_service'; import { httpServiceMock } from '../http/http_service.mock'; -import { mockRouter } from '../http/router/router.mock'; -import { savedObjectsClientMock, elasticsearchServiceMock } from '../mocks'; +import { savedObjectsClientMock, elasticsearchServiceMock, configServiceMock } from '../mocks'; import { mockCoreContext } from '../core_context.mock'; import { mockDeprecationsFactory } from './deprecations_factory.mock'; import { mockDeprecationsRegistry } from './deprecations_registry.mock'; describe('DeprecationsService', () => { - const coreContext = mockCoreContext.create(); - const http = httpServiceMock.createInternalSetupContract(); - const router = mockRouter.create(); - http.createRouter.mockReturnValue(router); - const deprecationsCoreSetupDeps = { http }; + let coreContext: ReturnType; + let http: ReturnType; + let router: ReturnType; + let deprecationsCoreSetupDeps: DeprecationsSetupDeps; + + beforeEach(() => { + const configService = configServiceMock.create({ + atPath: { skip_deprecated_settings: ['hello', 'world'] }, + }); + coreContext = mockCoreContext.create({ configService }); + http = httpServiceMock.createInternalSetupContract(); + router = httpServiceMock.createRouter(); + http.createRouter.mockReturnValue(router); + deprecationsCoreSetupDeps = { http }; + }); - beforeEach(() => jest.clearAllMocks()); + afterEach(() => { + jest.clearAllMocks(); + DeprecationsFactoryMock.mockClear(); + }); describe('#setup', () => { - it('registers routes', () => { + it('registers routes', async () => { const deprecationsService = new DeprecationsService(coreContext); - deprecationsService.setup(deprecationsCoreSetupDeps); - // Registers correct base api path + await deprecationsService.setup(deprecationsCoreSetupDeps); + // registers correct base api path expect(http.createRouter).toBeCalledWith('/api/deprecations'); // registers get route '/' expect(router.get).toHaveBeenCalledTimes(1); expect(router.get).toHaveBeenCalledWith({ path: '/', validate: false }, expect.any(Function)); }); - it('calls registerConfigDeprecationsInfo', () => { + it('calls registerConfigDeprecationsInfo', async () => { const deprecationsService = new DeprecationsService(coreContext); const mockRegisterConfigDeprecationsInfo = jest.fn(); deprecationsService['registerConfigDeprecationsInfo'] = mockRegisterConfigDeprecationsInfo; - deprecationsService.setup(deprecationsCoreSetupDeps); + await deprecationsService.setup(deprecationsCoreSetupDeps); expect(mockRegisterConfigDeprecationsInfo).toBeCalledTimes(1); }); + + it('creates DeprecationsFactory with the correct parameters', async () => { + const deprecationsService = new DeprecationsService(coreContext); + await deprecationsService.setup(deprecationsCoreSetupDeps); + + expect(DeprecationsFactoryMock).toHaveBeenCalledTimes(1); + expect(DeprecationsFactoryMock).toHaveBeenCalledWith({ + logger: expect.any(Object), + config: { + ignoredConfigDeprecations: ['hello', 'world'], + }, + }); + }); }); describe('#start', () => { @@ -51,7 +78,7 @@ describe('DeprecationsService', () => { const savedObjectsClient = savedObjectsClientMock.create(); const deprecationsService = new DeprecationsService(coreContext); - deprecationsService.setup(deprecationsCoreSetupDeps); + await deprecationsService.setup(deprecationsCoreSetupDeps); const start = deprecationsService.start(); const deprecationsClient = start.asScopedToClient(esClient, savedObjectsClient); @@ -73,6 +100,7 @@ describe('DeprecationsService', () => { 'testDomain', [ { + configPath: 'test', message: 'testMessage', documentationUrl: 'testDocUrl', correctiveActions: { @@ -100,6 +128,7 @@ describe('DeprecationsService', () => { expect(configDeprecations).toMatchInlineSnapshot(` Array [ Object { + "configPath": "test", "correctiveActions": Object { "manualSteps": Array [ "Using Kibana user management, change all users using the kibana_user role to the kibana_admin role.", @@ -124,6 +153,7 @@ describe('DeprecationsService', () => { 'testDomain', [ { + configPath: 'test', message: 'testMessage', level: 'warning', correctiveActions: { diff --git a/src/core/server/deprecations/deprecations_service.ts b/src/core/server/deprecations/deprecations_service.ts index cfc0aae443d1b..0c3fd75987aa6 100644 --- a/src/core/server/deprecations/deprecations_service.ts +++ b/src/core/server/deprecations/deprecations_service.ts @@ -6,11 +6,14 @@ * Side Public License, v 1. */ +import { take } from 'rxjs/operators'; + import { DeprecationsFactory } from './deprecations_factory'; import { DomainDeprecationDetails, RegisterDeprecationsConfig } from './types'; import { registerRoutes } from './routes'; - +import { config as deprecationConfig, DeprecationConfigType } from './deprecation_config'; import { CoreContext } from '../core_context'; +import { IConfigService } from '../config'; import { CoreService } from '../../types'; import { InternalHttpServiceSetup } from '../http'; import { Logger } from '../logging'; @@ -103,6 +106,7 @@ export interface DeprecationsServiceSetup { export interface DeprecationsClient { getAllDeprecations: () => Promise; } + export interface InternalDeprecationsServiceStart { /** * Creates a {@link DeprecationsClient} with provided SO client and ES client. @@ -129,22 +133,33 @@ export class DeprecationsService implements CoreService { private readonly logger: Logger; - private readonly deprecationsFactory: DeprecationsFactory; + private readonly configService: IConfigService; + private deprecationsFactory?: DeprecationsFactory; - constructor(private readonly coreContext: Pick) { + constructor(coreContext: Pick) { this.logger = coreContext.logger.get('deprecations-service'); - this.deprecationsFactory = new DeprecationsFactory({ - logger: this.logger, - }); + this.configService = coreContext.configService; } - public setup({ http }: DeprecationsSetupDeps): InternalDeprecationsServiceSetup { + public async setup({ http }: DeprecationsSetupDeps): Promise { this.logger.debug('Setting up Deprecations service'); - const deprecationsFactory = this.deprecationsFactory; + + const config = await this.configService + .atPath(deprecationConfig.path) + .pipe(take(1)) + .toPromise(); + + this.deprecationsFactory = new DeprecationsFactory({ + logger: this.logger, + config: { + ignoredConfigDeprecations: config.skip_deprecated_settings, + }, + }); registerRoutes({ http }); this.registerConfigDeprecationsInfo(this.deprecationsFactory); + const deprecationsFactory = this.deprecationsFactory; return { getRegistry: (domainId: string): DeprecationsServiceSetup => { const registry = deprecationsFactory.getRegistry(domainId); @@ -156,6 +171,9 @@ export class DeprecationsService } public start(): InternalDeprecationsServiceStart { + if (!this.deprecationsFactory) { + throw new Error('`setup` must be called before `start`'); + } return { asScopedToClient: this.createScopedDeprecations(), }; @@ -169,7 +187,7 @@ export class DeprecationsService ) => DeprecationsClient { return (esClient: IScopedClusterClient, savedObjectsClient: SavedObjectsClientContract) => { return { - getAllDeprecations: this.deprecationsFactory.getAllDeprecations.bind(null, { + getAllDeprecations: this.deprecationsFactory!.getAllDeprecations.bind(null, { savedObjectsClient, esClient, }), @@ -178,7 +196,7 @@ export class DeprecationsService } private registerConfigDeprecationsInfo(deprecationsFactory: DeprecationsFactory) { - const handledDeprecatedConfigs = this.coreContext.configService.getHandledDeprecatedConfigs(); + const handledDeprecatedConfigs = this.configService.getHandledDeprecatedConfigs(); for (const [domainId, deprecationsContexts] of handledDeprecatedConfigs) { const deprecationsRegistry = deprecationsFactory.getRegistry(domainId); @@ -186,12 +204,14 @@ export class DeprecationsService getDeprecations: () => { return deprecationsContexts.map( ({ + configPath, title = `${domainId} has a deprecated setting`, level = 'critical', message, correctiveActions, documentationUrl, }) => ({ + configPath, title, level, message, diff --git a/src/core/server/deprecations/index.ts b/src/core/server/deprecations/index.ts index 5c2a0b87b42de..d9225750f04a1 100644 --- a/src/core/server/deprecations/index.ts +++ b/src/core/server/deprecations/index.ts @@ -7,7 +7,10 @@ */ export type { + BaseDeprecationDetails, DeprecationsDetails, + ConfigDeprecationDetails, + FeatureDeprecationDetails, GetDeprecationsContext, RegisterDeprecationsConfig, DeprecationsGetResponse, @@ -21,3 +24,4 @@ export type { } from './deprecations_service'; export { DeprecationsService } from './deprecations_service'; +export { config } from './deprecation_config'; diff --git a/src/core/server/deprecations/types.ts b/src/core/server/deprecations/types.ts index e24c6a13fceea..3990a76f7578e 100644 --- a/src/core/server/deprecations/types.ts +++ b/src/core/server/deprecations/types.ts @@ -6,22 +6,16 @@ * Side Public License, v 1. */ +import type { MaybePromise } from '@kbn/utility-types'; import type { SavedObjectsClientContract } from '../saved_objects/types'; import type { IScopedClusterClient } from '../elasticsearch'; -type MaybePromise = T | Promise; - -/** - * @internal - */ -export interface DomainDeprecationDetails extends DeprecationsDetails { - domainId: string; -} - /** + * Base properties shared by all types of deprecations + * * @public */ -export interface DeprecationsDetails { +export interface BaseDeprecationDetails { /** * The title of the deprecation. * Check the README for writing deprecations in `src/core/server/deprecations/README.mdx` @@ -82,6 +76,33 @@ export interface DeprecationsDetails { }; } +/** + * @public + */ +export interface ConfigDeprecationDetails extends BaseDeprecationDetails { + configPath: string; + deprecationType: 'config'; +} + +/** + * @public + */ +export interface FeatureDeprecationDetails extends BaseDeprecationDetails { + deprecationType?: 'feature' | undefined; +} + +/** + * @public + */ +export type DeprecationsDetails = ConfigDeprecationDetails | FeatureDeprecationDetails; + +/** + * @internal + */ +export type DomainDeprecationDetails = DeprecationsDetails & { + domainId: string; +}; + /** * @public */ diff --git a/src/core/server/elasticsearch/elasticsearch_config.ts b/src/core/server/elasticsearch/elasticsearch_config.ts index f5d44a87d5404..71b9f87c14fa3 100644 --- a/src/core/server/elasticsearch/elasticsearch_config.ts +++ b/src/core/server/elasticsearch/elasticsearch_config.ts @@ -145,6 +145,7 @@ const deprecations: ConfigDeprecationProvider = () => [ } if (es.username === 'elastic') { addDeprecation({ + configPath: `${fromPath}.username`, message: `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana_system" user instead.`, correctiveActions: { manualSteps: [`Replace [${fromPath}.username] from "elastic" to "kibana_system".`], @@ -152,6 +153,7 @@ const deprecations: ConfigDeprecationProvider = () => [ }); } else if (es.username === 'kibana') { addDeprecation({ + configPath: `${fromPath}.username`, message: `Setting [${fromPath}.username] to "kibana" is deprecated. You should use the "kibana_system" user instead.`, correctiveActions: { manualSteps: [`Replace [${fromPath}.username] from "kibana" to "kibana_system".`], @@ -160,6 +162,7 @@ const deprecations: ConfigDeprecationProvider = () => [ } if (es.ssl?.key !== undefined && es.ssl?.certificate === undefined) { addDeprecation({ + configPath: `${fromPath}.ssl.key`, message: `Setting [${fromPath}.ssl.key] without [${fromPath}.ssl.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, correctiveActions: { manualSteps: [ @@ -169,6 +172,7 @@ const deprecations: ConfigDeprecationProvider = () => [ }); } else if (es.ssl?.certificate !== undefined && es.ssl?.key === undefined) { addDeprecation({ + configPath: `${fromPath}.ssl.certificate`, message: `Setting [${fromPath}.ssl.certificate] without [${fromPath}.ssl.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, correctiveActions: { manualSteps: [ @@ -178,6 +182,7 @@ const deprecations: ConfigDeprecationProvider = () => [ }); } else if (es.logQueries === true) { addDeprecation({ + configPath: `${fromPath}.logQueries`, message: `Setting [${fromPath}.logQueries] is deprecated and no longer used. You should set the log level to "debug" for the "elasticsearch.queries" context in "logging.loggers" or use "logging.verbose: true".`, correctiveActions: { manualSteps: [ diff --git a/src/core/server/environment/environment_service.test.ts b/src/core/server/environment/environment_service.test.ts index 4b074482248b4..0817fad35f882 100644 --- a/src/core/server/environment/environment_service.test.ts +++ b/src/core/server/environment/environment_service.test.ts @@ -136,7 +136,8 @@ describe('UuidService', () => { }); }); - describe('unhandledRejection warnings', () => { + // TODO: From Nodejs v16 emitting an unhandledRejection will kill the process + describe.skip('unhandledRejection warnings', () => { it('logs warn for an unhandeld promise rejected with an Error', async () => { await service.preboot(); diff --git a/src/core/server/execution_context/integration_tests/tracing.test.ts b/src/core/server/execution_context/integration_tests/tracing.test.ts index f07546d44e956..76003868b2941 100644 --- a/src/core/server/execution_context/integration_tests/tracing.test.ts +++ b/src/core/server/execution_context/integration_tests/tracing.test.ts @@ -45,7 +45,7 @@ describe('trace', () => { }, }); await root.preboot(); - }, 30000); + }, 60000); afterEach(async () => { await root.shutdown(); diff --git a/src/core/server/http/http_server.test.ts b/src/core/server/http/http_server.test.ts index a20a225729124..60f2fa51f25bb 100644 --- a/src/core/server/http/http_server.test.ts +++ b/src/core/server/http/http_server.test.ts @@ -7,7 +7,7 @@ */ import { Server } from 'http'; -import { rmdir, mkdtemp, readFile, writeFile } from 'fs/promises'; +import { rm, mkdtemp, readFile, writeFile } from 'fs/promises'; import supertest from 'supertest'; import { omit } from 'lodash'; import { join } from 'path'; @@ -1419,7 +1419,7 @@ describe('setup contract', () => { afterAll(async () => { if (tempDir) { - await rmdir(tempDir, { recursive: true }); + await rm(tempDir, { recursive: true }); } }); diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts index 1f5c03953c9fa..9da2605420a46 100644 --- a/src/core/server/http/router/request.ts +++ b/src/core/server/http/router/request.ts @@ -224,10 +224,8 @@ export class KibanaRequest< } private getEvents(request: Request): KibanaRequestEvents { - const finish$ = merge( - fromEvent(request.raw.res, 'finish'), // Response has been sent - fromEvent(request.raw.req, 'close') // connection was closed - ).pipe(shareReplay(1), first()); + // the response is completed, or its underlying connection was terminated prematurely + const finish$ = fromEvent(request.raw.res, 'close').pipe(shareReplay(1), first()); const aborted$ = fromEvent(request.raw.req, 'aborted').pipe(first(), takeUntil(finish$)); const completed$ = merge(finish$, aborted$).pipe(shareReplay(1), first()); diff --git a/src/core/server/http/router/validator/validator.test.ts b/src/core/server/http/router/validator/validator.test.ts index cb4fb5fd24e31..b516d723edadc 100644 --- a/src/core/server/http/router/validator/validator.test.ts +++ b/src/core/server/http/router/validator/validator.test.ts @@ -48,7 +48,7 @@ describe('Router validator', () => { expect(() => validator.getParams({})).toThrowError('[foo]: Not a string'); expect(() => validator.getParams(undefined)).toThrowError( - `Cannot read property 'foo' of undefined` + `Cannot read properties of undefined (reading 'foo')` ); expect(() => validator.getParams({}, 'myField')).toThrowError('[myField.foo]: Not a string'); diff --git a/src/core/server/index.ts b/src/core/server/index.ts index a9f2f00707646..92c758f7e6980 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -387,7 +387,10 @@ export { EventLoopDelaysMonitor } from './metrics'; export type { I18nServiceSetup } from './i18n'; export type { + BaseDeprecationDetails, DeprecationsDetails, + ConfigDeprecationDetails, + FeatureDeprecationDetails, RegisterDeprecationsConfig, GetDeprecationsContext, DeprecationsServiceSetup, diff --git a/src/core/server/kibana_config.ts b/src/core/server/kibana_config.ts index ead3f2cc2776a..859f25d7082f1 100644 --- a/src/core/server/kibana_config.ts +++ b/src/core/server/kibana_config.ts @@ -17,6 +17,7 @@ const deprecations: ConfigDeprecationProvider = () => [ const kibana = settings[fromPath]; if (kibana?.index) { addDeprecation({ + configPath: 'kibana.index', title: i18n.translate('core.kibana.index.deprecationTitle', { defaultMessage: `Setting "kibana.index" is deprecated`, }), 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 83533e29ad12e..d32dc04aa22c5 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 @@ -7,7 +7,7 @@ */ import { join } from 'path'; -import { rmdir, mkdtemp, readFile, readdir } from 'fs/promises'; +import { rm, mkdtemp, readFile, readdir } from 'fs/promises'; import moment from 'moment-timezone'; import * as kbnTestServer from '../../../test_helpers/kbn_server'; import { getNextRollingTime } from '../appenders/rolling_file/policies/time_interval/get_next_rolling_time'; @@ -49,7 +49,7 @@ describe('RollingFileAppender', () => { afterEach(async () => { if (testDir) { - await rmdir(testDir, { recursive: true }); + await rm(testDir, { recursive: true }); } if (root) { diff --git a/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.ts b/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.ts index 3dff847f83c9b..0f3035c14a923 100644 --- a/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.ts +++ b/src/core/server/metrics/event_loop_delays/event_loop_delays_monitor.ts @@ -6,12 +6,12 @@ * Side Public License, v 1. */ -import type { EventLoopDelayMonitor } from 'perf_hooks'; +import type { IntervalHistogram as PerfIntervalHistogram } from 'perf_hooks'; import { monitorEventLoopDelay } from 'perf_hooks'; import type { IntervalHistogram } from '../types'; export class EventLoopDelaysMonitor { - private readonly loopMonitor: EventLoopDelayMonitor; + private readonly loopMonitor: PerfIntervalHistogram; private fromTimestamp: Date; /** diff --git a/src/core/server/metrics/integration_tests/server_collector.test.ts b/src/core/server/metrics/integration_tests/server_collector.test.ts index 93589648ca0ae..a16e0f2217add 100644 --- a/src/core/server/metrics/integration_tests/server_collector.test.ts +++ b/src/core/server/metrics/integration_tests/server_collector.test.ts @@ -15,6 +15,7 @@ import { HttpService, IRouter } from '../../http'; import { contextServiceMock } from '../../context/context_service.mock'; import { executionContextServiceMock } from '../../execution_context/execution_context_service.mock'; import { ServerMetricsCollector } from '../collectors/server'; +import { setTimeout as setTimeoutPromise } from 'timers/promises'; const requestWaitDelay = 25; @@ -195,6 +196,9 @@ describe('ServerMetricsCollector', () => { waitSubject.next('go'); await Promise.all([res1, res2]); + // Give the event-loop one more cycle to allow concurrent connections to be + // up to date before collecting + await setTimeoutPromise(0); metrics = await collector.collect(); expect(metrics.concurrent_connections).toEqual(0); }); diff --git a/src/core/server/saved_objects/saved_objects_config.ts b/src/core/server/saved_objects/saved_objects_config.ts index 0c27f170f590b..c9b4b4499fa80 100644 --- a/src/core/server/saved_objects/saved_objects_config.ts +++ b/src/core/server/saved_objects/saved_objects_config.ts @@ -27,6 +27,7 @@ const migrationDeprecations: ConfigDeprecationProvider = () => [ const migrationsConfig = settings[fromPath]; if (migrationsConfig?.enableV2 !== undefined) { addDeprecation({ + configPath: `${fromPath}.enableV2`, message: '"migrations.enableV2" is deprecated and will be removed in an upcoming release without any further notice.', documentationUrl: 'https://ela.st/kbn-so-migration-v2', diff --git a/src/core/server/saved_objects/serialization/serializer.test.ts b/src/core/server/saved_objects/serialization/serializer.test.ts index 3fdeb4aa088e1..24eded55615c4 100644 --- a/src/core/server/saved_objects/serialization/serializer.test.ts +++ b/src/core/server/saved_objects/serialization/serializer.test.ts @@ -491,6 +491,52 @@ describe('#rawToSavedObject', () => { expect(actual).toHaveProperty('namespaces', ['baz']); }); }); + + describe('throws if provided invalid type', () => { + expect(() => + singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + // @ts-expect-error expects a string + // eslint-disable-next-line + type: new String('foo'), + }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Expected saved object type to be a string but given [String] with [foo] value."` + ); + + expect(() => + singleNamespaceSerializer.rawToSavedObject({ + _id: 'foo:bar', + _source: { + // @ts-expect-error expects astring + type: { + toString() { + return 'foo'; + }, + }, + }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Expected saved object type to be a string but given [Object] with [foo] value."` + ); + }); + + describe('throws if provided invalid id', () => { + expect(() => + singleNamespaceSerializer.rawToSavedObject({ + // @ts-expect-error expects a string + // eslint-disable-next-line + _id: new String('foo:bar'), + _source: { + type: 'foo', + }, + }) + ).toThrowErrorMatchingInlineSnapshot( + `"Expected document id to be a string but given [String] with [foo:bar] value."` + ); + }); }); describe('#savedObjectToRaw', () => { diff --git a/src/core/server/saved_objects/serialization/serializer.ts b/src/core/server/saved_objects/serialization/serializer.ts index 5e27b3de24409..9d9d65e735866 100644 --- a/src/core/server/saved_objects/serialization/serializer.ts +++ b/src/core/server/saved_objects/serialization/serializer.ts @@ -5,7 +5,7 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - +import typeDetect from 'type-detect'; import { LEGACY_URL_ALIAS_TYPE } from '../object_types'; import { decodeVersion, encodeVersion } from '../version'; import { ISavedObjectTypeRegistry } from '../saved_objects_type_registry'; @@ -236,6 +236,8 @@ function checkIdMatchesPrefix(id: string, prefix: string) { function assertNonEmptyString(value: string, name: string) { if (!value || typeof value !== 'string') { - throw new TypeError(`Expected "${value}" to be a ${name}`); + throw new TypeError( + `Expected ${name} to be a string but given [${typeDetect(value)}] with [${value}] value.` + ); } } diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index 72ca2d15007b8..82a0dd71700f6 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -4011,16 +4011,7 @@ describe('SavedObjectsRepository', () => { ]; const originId = 'some-origin-id'; - const updateSuccess = async (type, id, attributes, options, includeOriginId) => { - if (registry.isMultiNamespace(type)) { - const mockGetResponse = getMockGetResponse({ type, id }, options?.namespace); - client.get.mockResolvedValueOnce( - elasticsearchClientMock.createSuccessTransportRequestPromise( - { ...mockGetResponse }, - { statusCode: 200 } - ) - ); - } + const mockUpdateResponse = (type, id, options, includeOriginId) => { client.update.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise( { @@ -4042,6 +4033,19 @@ describe('SavedObjectsRepository', () => { { statusCode: 200 } ) ); + }; + + const updateSuccess = async (type, id, attributes, options, includeOriginId) => { + if (registry.isMultiNamespace(type)) { + const mockGetResponse = getMockGetResponse({ type, id }, options?.namespace); + client.get.mockResolvedValueOnce( + elasticsearchClientMock.createSuccessTransportRequestPromise( + { ...mockGetResponse }, + { statusCode: 200 } + ) + ); + } + mockUpdateResponse(type, id, options, includeOriginId); const result = await savedObjectsRepository.update(type, id, attributes, options); expect(client.get).toHaveBeenCalledTimes(registry.isMultiNamespace(type) ? 1 : 0); return result; @@ -4085,7 +4089,7 @@ describe('SavedObjectsRepository', () => { await test([]); }); - it(`uses the 'upsertAttributes' option when specified`, async () => { + it(`uses the 'upsertAttributes' option when specified for a single-namespace type`, async () => { await updateSuccess(type, id, attributes, { upsert: { title: 'foo', @@ -4109,6 +4113,42 @@ describe('SavedObjectsRepository', () => { ); }); + it(`uses the 'upsertAttributes' option when specified for a multi-namespace type that does not exist`, async () => { + const options = { upsert: { title: 'foo', description: 'bar' } }; + mockUpdateResponse(MULTI_NAMESPACE_ISOLATED_TYPE, id, options); + await savedObjectsRepository.update(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes, options); + expect(client.get).toHaveBeenCalledTimes(1); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:logstash-*`, + body: expect.objectContaining({ + upsert: expect.objectContaining({ + type: MULTI_NAMESPACE_ISOLATED_TYPE, + [MULTI_NAMESPACE_ISOLATED_TYPE]: { + title: 'foo', + description: 'bar', + }, + }), + }), + }), + expect.anything() + ); + }); + + it(`ignores use the 'upsertAttributes' option when specified for a multi-namespace type that already exists`, async () => { + const options = { upsert: { title: 'foo', description: 'bar' } }; + await updateSuccess(MULTI_NAMESPACE_ISOLATED_TYPE, id, attributes, options); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${MULTI_NAMESPACE_ISOLATED_TYPE}:logstash-*`, + body: expect.not.objectContaining({ + upsert: expect.anything(), + }), + }), + expect.anything() + ); + }); + it(`doesn't accept custom references if not an array`, async () => { const test = async (references) => { await updateSuccess(type, id, attributes, { references }); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index 9cdc58f02f5d1..c74092faad96a 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -161,6 +161,35 @@ export interface SavedObjectsIncrementCounterField { incrementBy?: number; } +/** + * @internal + */ +interface PreflightCheckNamespacesParams { + /** The object type to fetch */ + type: string; + /** The object ID to fetch */ + id: string; + /** The current space */ + namespace: string | undefined; + /** Optional; for an object that is being created, this specifies the initial namespace(s) it will exist in (overriding the current space) */ + initialNamespaces?: string[]; +} + +/** + * @internal + */ +interface PreflightCheckNamespacesResult { + /** If the object exists, and whether or not it exists in the current space */ + checkResult: 'not_found' | 'found_in_namespace' | 'found_outside_namespace'; + /** + * What namespace(s) the object should exist in, if it needs to be created; practically speaking, this will never be undefined if + * checkResult == not_found or checkResult == found_in_namespace + */ + savedObjectNamespaces?: string[]; + /** The source of the raw document, if the object already exists */ + rawDocSource?: GetResponseFound; +} + /** * @public */ @@ -297,12 +326,16 @@ export class SavedObjectsRepository { if (id && overwrite) { // we will overwrite a multi-namespace saved object if it exists; if that happens, ensure we preserve its included namespaces // note: this check throws an error if the object is found but does not exist in this namespace - savedObjectNamespaces = await this.preflightGetNamespaces( + const preflightResult = await this.preflightCheckNamespaces({ type, id, namespace, - initialNamespaces - ); + initialNamespaces, + }); + if (preflightResult.checkResult === 'found_outside_namespace') { + throw SavedObjectsErrorHelpers.createConflictError(type, id); + } + savedObjectNamespaces = preflightResult.savedObjectNamespaces; } else { savedObjectNamespaces = initialNamespaces || getSavedObjectNamespaces(namespace); } @@ -670,11 +703,22 @@ export class SavedObjectsRepository { const namespace = normalizeNamespace(options.namespace); const rawId = this._serializer.generateRawId(namespace, type, id); - let preflightResult: SavedObjectsRawDoc | undefined; + let preflightResult: PreflightCheckNamespacesResult | undefined; if (this._registry.isMultiNamespace(type)) { - preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); - const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult) ?? []; + // note: this check throws an error if the object is found but does not exist in this namespace + preflightResult = await this.preflightCheckNamespaces({ + type, + id, + namespace, + }); + if ( + preflightResult.checkResult === 'found_outside_namespace' || + preflightResult.checkResult === 'not_found' + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } + const existingNamespaces = preflightResult.savedObjectNamespaces ?? []; if ( !force && (existingNamespaces.length > 1 || existingNamespaces.includes(ALL_NAMESPACES_STRING)) @@ -689,7 +733,7 @@ export class SavedObjectsRepository { { id: rawId, index: this.getIndexForType(type), - ...getExpectedVersionProperties(undefined, preflightResult), + ...getExpectedVersionProperties(undefined, preflightResult?.rawDocSource), refresh, }, { ignore: [404] } @@ -1208,22 +1252,33 @@ export class SavedObjectsRepository { const { version, references, upsert, refresh = DEFAULT_REFRESH_SETTING } = options; const namespace = normalizeNamespace(options.namespace); - let preflightResult: SavedObjectsRawDoc | undefined; + let preflightResult: PreflightCheckNamespacesResult | undefined; if (this._registry.isMultiNamespace(type)) { - preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); + preflightResult = await this.preflightCheckNamespaces({ + type, + id, + namespace, + }); + if ( + preflightResult.checkResult === 'found_outside_namespace' || + (!upsert && preflightResult.checkResult === 'not_found') + ) { + throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); + } } const time = getCurrentTime(); let rawUpsert: SavedObjectsRawDoc | undefined; - if (upsert) { + // don't include upsert if the object already exists; ES doesn't allow upsert in combination with version properties + if (upsert && (!preflightResult || preflightResult.checkResult === 'not_found')) { let savedObjectNamespace: string | undefined; let savedObjectNamespaces: string[] | undefined; if (this._registry.isSingleNamespace(type) && namespace) { savedObjectNamespace = namespace; } else if (this._registry.isMultiNamespace(type)) { - savedObjectNamespaces = await this.preflightGetNamespaces(type, id, namespace); + savedObjectNamespaces = preflightResult!.savedObjectNamespaces; } const migrated = this._migrator.migrateDocument({ @@ -1249,9 +1304,8 @@ export class SavedObjectsRepository { .update({ id: this._serializer.generateRawId(namespace, type, id), index: this.getIndexForType(type), - ...getExpectedVersionProperties(version, preflightResult), + ...getExpectedVersionProperties(version, preflightResult?.rawDocSource), refresh, - body: { doc, ...(rawUpsert && { upsert: rawUpsert._source }), @@ -1753,7 +1807,16 @@ export class SavedObjectsRepository { if (this._registry.isSingleNamespace(type) && namespace) { savedObjectNamespace = namespace; } else if (this._registry.isMultiNamespace(type)) { - savedObjectNamespaces = await this.preflightGetNamespaces(type, id, namespace); + // note: this check throws an error if the object is found but does not exist in this namespace + const preflightResult = await this.preflightCheckNamespaces({ + type, + id, + namespace, + }); + if (preflightResult.checkResult === 'found_outside_namespace') { + throw SavedObjectsErrorHelpers.createConflictError(type, id); + } + savedObjectNamespaces = preflightResult.savedObjectNamespaces; } // attributes: { [counterFieldName]: incrementBy }, @@ -2047,24 +2110,14 @@ export class SavedObjectsRepository { } /** - * Pre-flight check to get a multi-namespace saved object's included namespaces. This ensures that, if the saved object exists, it - * includes the target namespace. - * - * @param type The type of the saved object. - * @param id The ID of the saved object. - * @param namespace The target namespace. - * @param initialNamespaces The target namespace(s) we intend to create the object in, if specified. - * @returns Array of namespaces that this saved object currently includes, or (if the object does not exist yet) the namespaces that a - * newly-created object will include. Value may be undefined if an existing saved object has no namespaces attribute; this should not - * happen in normal operations, but it is possible if the Elasticsearch document is manually modified. - * @throws Will throw an error if the saved object exists and it does not include the target namespace. + * Pre-flight check to ensure that a multi-namespace object exists in the current namespace. */ - private async preflightGetNamespaces( - type: string, - id: string, - namespace: string | undefined, - initialNamespaces?: string[] - ) { + private async preflightCheckNamespaces({ + type, + id, + namespace, + initialNamespaces, + }: PreflightCheckNamespacesParams): Promise { if (!this._registry.isMultiNamespace(type)) { throw new Error(`Cannot make preflight get request for non-multi-namespace type '${type}'.`); } @@ -2084,55 +2137,21 @@ export class SavedObjectsRepository { const indexFound = statusCode !== 404; if (indexFound && isFoundGetResponse(body)) { if (!this.rawDocExistsInNamespaces(body, namespaces)) { - throw SavedObjectsErrorHelpers.createConflictError(type, id); + return { checkResult: 'found_outside_namespace' }; } - return initialNamespaces ?? getSavedObjectNamespaces(namespace, body); + return { + checkResult: 'found_in_namespace', + savedObjectNamespaces: initialNamespaces ?? getSavedObjectNamespaces(namespace, body), + rawDocSource: body, + }; } else if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { // checking if the 404 is from Elasticsearch throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); } - return initialNamespaces ?? getSavedObjectNamespaces(namespace); - } - - /** - * Pre-flight check for a multi-namespace saved object's namespaces. This ensures that, if the saved object exists, it includes the target - * namespace. - * - * @param type The type of the saved object. - * @param id The ID of the saved object. - * @param namespace The target namespace. - * @returns Raw document from Elasticsearch. - * @throws Will throw an error if the saved object is not found, if it doesn't include the target namespace or if the response is not identifiable as an Elasticsearch response. - */ - private async preflightCheckIncludesNamespace(type: string, id: string, namespace?: string) { - if (!this._registry.isMultiNamespace(type)) { - throw new Error(`Cannot make preflight get request for non-multi-namespace type '${type}'.`); - } - - const rawId = this._serializer.generateRawId(undefined, type, id); - const { body, statusCode, headers } = await this.client.get( - { - id: rawId, - index: this.getIndexForType(type), - }, - { ignore: [404] } - ); - - const indexFound = statusCode !== 404; - - // check if we have the elasticsearch header when index is not found and if we do, ensure it is Elasticsearch - if (isNotFoundFromUnsupportedServer({ statusCode, headers })) { - throw SavedObjectsErrorHelpers.createGenericNotFoundEsUnavailableError(type, id); - } - - if ( - !indexFound || - !isFoundGetResponse(body) || - !this.rawDocExistsInNamespace(body, namespace) - ) { - throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); - } - return body; + return { + checkResult: 'not_found', + savedObjectNamespaces: initialNamespaces ?? getSavedObjectNamespaces(namespace), + }; } /** The `initialNamespaces` field (create, bulkCreate) is used to create an object in an initial set of spaces. */ diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index a1e5d61744648..958083ae06899 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -33,6 +33,7 @@ import { LoggerFactory } from '@kbn/logging'; import { LogLevel } from '@kbn/logging'; import { LogMeta } from '@kbn/logging'; import { LogRecord } from '@kbn/logging'; +import { MaybePromise } from '@kbn/utility-types'; import { ObjectType } from '@kbn/config-schema'; import { Observable } from 'rxjs'; import { PackageInfo } from '@kbn/config'; @@ -156,6 +157,27 @@ export interface AuthToolkit { } & ResponseHeaders) => AuthResult; } +// @public +export interface BaseDeprecationDetails { + correctiveActions: { + api?: { + path: string; + method: 'POST' | 'PUT'; + body?: { + [key: string]: any; + }; + omitContextFromBody?: boolean; + }; + manualSteps: string[]; + }; + deprecationType?: 'config' | 'feature'; + documentationUrl?: string; + level: 'warning' | 'critical' | 'fetch_error'; + message: string; + requireRestart?: boolean; + title: string; +} + // @public export class BasePath { // @internal @@ -250,6 +272,14 @@ export { ConfigDeprecation } export { ConfigDeprecationContext } +// @public (undocumented) +export interface ConfigDeprecationDetails extends BaseDeprecationDetails { + // (undocumented) + configPath: string; + // (undocumented) + deprecationType: 'config'; +} + export { ConfigDeprecationFactory } export { ConfigDeprecationProvider } @@ -782,25 +812,7 @@ export interface DeprecationsClient { } // @public (undocumented) -export interface DeprecationsDetails { - correctiveActions: { - api?: { - path: string; - method: 'POST' | 'PUT'; - body?: { - [key: string]: any; - }; - omitContextFromBody?: boolean; - }; - manualSteps: string[]; - }; - deprecationType?: 'config' | 'feature'; - documentationUrl?: string; - level: 'warning' | 'critical' | 'fetch_error'; - message: string; - requireRestart?: boolean; - title: string; -} +export type DeprecationsDetails = ConfigDeprecationDetails | FeatureDeprecationDetails; // @public export interface DeprecationSettings { @@ -951,6 +963,12 @@ export interface FakeRequest { headers: Headers; } +// @public (undocumented) +export interface FeatureDeprecationDetails extends BaseDeprecationDetails { + // (undocumented) + deprecationType?: 'feature' | undefined; +} + // @public export type GetAuthHeaders = (request: KibanaRequest) => AuthHeaders | undefined; @@ -1676,8 +1694,6 @@ export type RedirectResponseOptions = HttpResponseOptions & { // @public (undocumented) export interface RegisterDeprecationsConfig { - // Warning: (ae-forgotten-export) The symbol "MaybePromise" needs to be exported by the entry point index.d.ts - // // (undocumented) getDeprecations: (context: GetDeprecationsContext) => MaybePromise; } diff --git a/src/core/server/server.ts b/src/core/server/server.ts index 59cde6835c519..b6f263f0c22ff 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -51,7 +51,7 @@ import { ServiceConfigDescriptor, } from './internal_types'; import { CoreUsageDataService } from './core_usage_data'; -import { DeprecationsService } from './deprecations'; +import { DeprecationsService, config as deprecationConfig } from './deprecations'; import { CoreRouteHandlerContext } from './core_route_handler_context'; import { config as externalUrlConfig } from './external_url'; import { config as executionContextConfig } from './execution_context'; @@ -206,7 +206,7 @@ export class Server { executionContext: executionContextSetup, }); - const deprecationsSetup = this.deprecations.setup({ + const deprecationsSetup = await this.deprecations.setup({ http: httpSetup, }); @@ -388,6 +388,7 @@ export class Server { statusConfig, pidConfig, i18nConfig, + deprecationConfig, ]; this.configService.addDeprecationProvider(rootConfigPath, coreDeprecationProvider); diff --git a/src/core/server/status/plugins_status.ts b/src/core/server/status/plugins_status.ts index 7ef3ddb31d978..719535133e7ab 100644 --- a/src/core/server/status/plugins_status.ts +++ b/src/core/server/status/plugins_status.ts @@ -128,7 +128,7 @@ export class PluginsStatusService { return combineLatest(pluginStatuses).pipe( map((statuses) => Object.fromEntries(statuses)), - distinctUntilChanged(isDeepStrictEqual) + distinctUntilChanged>(isDeepStrictEqual) ); }) ); diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index eaeaecd5e1de9..e9bb57b3c00cf 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -81,7 +81,7 @@ export class StatusService implements CoreService { }); return summary; }), - distinctUntilChanged(isDeepStrictEqual), + distinctUntilChanged>(isDeepStrictEqual), shareReplay(1) ); @@ -97,7 +97,7 @@ export class StatusService implements CoreService { }); return coreOverall; }), - distinctUntilChanged(isDeepStrictEqual), + distinctUntilChanged>(isDeepStrictEqual), shareReplay(1) ); @@ -182,7 +182,7 @@ export class StatusService implements CoreService { elasticsearch: elasticsearchStatus, savedObjects: savedObjectsStatus, })), - distinctUntilChanged(isDeepStrictEqual), + distinctUntilChanged(isDeepStrictEqual), shareReplay(1) ); } diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile b/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile index 510f3debac2db..e2df4de46a551 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/base/Dockerfile @@ -109,7 +109,7 @@ COPY --chown=1000:0 config/kibana.yml /usr/share/kibana/config/kibana.yml # Add the launcher/wrapper script. It knows how to interpret environment # variables and translate them to Kibana CLI options. -COPY --chown=1000:0 bin/kibana-docker /usr/local/bin/ +COPY bin/kibana-docker /usr/local/bin/ # Ensure gid 0 write permissions for OpenShift. RUN chmod g+ws /usr/share/kibana && \ diff --git a/src/dev/build/tasks/patch_native_modules_task.ts b/src/dev/build/tasks/patch_native_modules_task.ts index 5a2f179edeccb..bb2b9cc96b677 100644 --- a/src/dev/build/tasks/patch_native_modules_task.ts +++ b/src/dev/build/tasks/patch_native_modules_task.ts @@ -36,12 +36,12 @@ const packages: Package[] = [ extractMethod: 'gunzip', archives: { 'darwin-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.16.0/darwin-x64-83.gz', - sha256: 'ef49febcba972b488727ce329ea9d2b57590bb44001ed494f2aa1397c0ebc32b', + url: 'https://github.com/uhop/node-re2/releases/download/1.16.0/darwin-x64-93.gz', + sha256: 'a267c6202d86d08170eb4a833acf81d83660ce33e8981fcd5b7f6e0310961d56', }, 'linux-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.16.0/linux-x64-83.gz', - sha256: '160217dd83eb7093b758e905ce09cb45182864c7df858bf2525a68924a23c509', + url: 'https://github.com/uhop/node-re2/releases/download/1.16.0/linux-x64-93.gz', + sha256: 'e0ca5d6527fe7ec0fe98b6960c47b66a5bb2823c3bebb3bf4ed4d58eed3d23c5', }, // ARM build is currently done manually as Github Actions used in upstream project @@ -55,12 +55,12 @@ const packages: Package[] = [ // * 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.16.0/linux-arm64-83.gz', - sha256: '114505c60dbf57ad30556937ac5f49213c6676ad79d92706b96949d3a63f53b4', + url: 'https://storage.googleapis.com/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.16.0/linux-arm64-93.gz', + sha256: '7a786e0b75985e5aafdefa9af55cad8e85e69a3326f16d8c63d21d6b5b3bff1b', }, 'win32-x64': { - url: 'https://github.com/uhop/node-re2/releases/download/1.16.0/win32-x64-83.gz', - sha256: '92ad420a6bfcedeb58dadf807a2f2901b05251d1edd3950051699929eda23073', + url: 'https://github.com/uhop/node-re2/releases/download/1.16.0/win32-x64-93.gz', + sha256: '37245ceb59a086b5e7e9de8746a3cdf148c383be9ae2580f92baea90d0d39947', }, }, }, diff --git a/src/plugins/custom_integrations/common/index.ts b/src/plugins/custom_integrations/common/index.ts index de2a6592465a2..944ac6ba3e6ee 100755 --- a/src/plugins/custom_integrations/common/index.ts +++ b/src/plugins/custom_integrations/common/index.ts @@ -40,8 +40,15 @@ export const INTEGRATION_CATEGORY_DISPLAY = { web: 'Web', // Kibana added - upload_file: 'Upload a file', + communication: 'Communication', + customer_support: 'Customer Support', + document_storage: 'Document Storage', + enterprise_management: 'Enterprise Management', + knowledge_platform: 'Knowledge Platform', language_client: 'Language client', + project_management: 'Project Management', + software_development: 'Software Development', + upload_file: 'Upload a file', }; /** @@ -70,6 +77,7 @@ export interface IntegrationCategoryCount { // TODO: consider i18n export const SHIPPER_DISPLAY = { beats: 'Beats', + enterprise_search: 'Enterprise Search', language_clients: 'Language clients', other: 'Other', sample_data: 'Sample data', diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx index 991033b9a0d6a..52f04bcead665 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx @@ -95,7 +95,8 @@ afterAll(() => { sizeMe.noPlaceholders = false; }); -test('renders DashboardGrid', () => { +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 +test.skip('renders DashboardGrid', () => { const { props, options } = prepare(); const component = mountWithIntl( @@ -108,7 +109,8 @@ test('renders DashboardGrid', () => { expect(panelElements.length).toBe(2); }); -test('renders DashboardGrid with no visualizations', () => { +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 +test.skip('renders DashboardGrid with no visualizations', () => { const { props, options } = prepare(); const component = mountWithIntl( @@ -123,7 +125,8 @@ test('renders DashboardGrid with no visualizations', () => { expect(component.find('EmbeddableChildPanel').length).toBe(0); }); -test('DashboardGrid removes panel when removed from container', () => { +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 +test.skip('DashboardGrid removes panel when removed from container', () => { const { props, options } = prepare(); const component = mountWithIntl( @@ -142,7 +145,8 @@ test('DashboardGrid removes panel when removed from container', () => { expect(panelElements.length).toBe(1); }); -test('DashboardGrid renders expanded panel', () => { +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 +test.skip('DashboardGrid renders expanded panel', () => { const { props, options } = prepare(); const component = mountWithIntl( @@ -170,7 +174,8 @@ test('DashboardGrid renders expanded panel', () => { ).toBeUndefined(); }); -test('DashboardGrid unmount unsubscribes', async (done) => { +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 +test.skip('DashboardGrid unmount unsubscribes', async (done) => { const { props, options } = prepare(); const component = mountWithIntl( diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx index 4397705691314..7a920685bcaae 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx @@ -92,8 +92,8 @@ function getProps(props?: Partial): { options, }; } - -test('renders DashboardViewport', () => { +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 +test.skip('renders DashboardViewport', () => { const { props, options } = getProps(); const component = mount( @@ -108,7 +108,8 @@ test('renders DashboardViewport', () => { expect(panels.length).toBe(2); }); -test('renders DashboardViewport with no visualizations', () => { +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 +test.skip('renders DashboardViewport with no visualizations', () => { const { props, options } = getProps(); props.container.updateInput({ panels: {} }); const component = mount( @@ -126,7 +127,8 @@ test('renders DashboardViewport with no visualizations', () => { component.unmount(); }); -test('renders DashboardEmptyScreen', () => { +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 +test.skip('renders DashboardEmptyScreen', () => { const { props, options } = getProps(); props.container.updateInput({ panels: {} }); const component = mount( @@ -144,7 +146,8 @@ test('renders DashboardEmptyScreen', () => { component.unmount(); }); -test('renders exit full screen button when in full screen mode', async () => { +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 +test.skip('renders exit full screen button when in full screen mode', async () => { const { props, options } = getProps(); props.container.updateInput({ isFullScreenMode: true }); const component = mount( @@ -172,7 +175,8 @@ test('renders exit full screen button when in full screen mode', async () => { component.unmount(); }); -test('renders exit full screen button when in full screen mode and empty screen', async () => { +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 +test.skip('renders exit full screen button when in full screen mode and empty screen', async () => { const { props, options } = getProps(); props.container.updateInput({ panels: {}, isFullScreenMode: true }); const component = mount( @@ -199,7 +203,8 @@ test('renders exit full screen button when in full screen mode and empty screen' component.unmount(); }); -test('DashboardViewport unmount unsubscribes', async (done) => { +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 +test.skip('DashboardViewport unmount unsubscribes', async (done) => { const { props, options } = getProps(); const component = mount( diff --git a/src/plugins/data/common/search/expressions/kibana_context.test.ts b/src/plugins/data/common/search/expressions/kibana_context.test.ts index 77d89792b63c3..c9fe4ee5b7367 100644 --- a/src/plugins/data/common/search/expressions/kibana_context.test.ts +++ b/src/plugins/data/common/search/expressions/kibana_context.test.ts @@ -205,7 +205,7 @@ describe('kibanaContextFn', () => { it('deduplicates duplicated filters and keeps the first enabled filter', async () => { const { fn } = kibanaContextFn; const filter1 = buildFilter( - { fields: [] }, + { fields: [], title: 'dataView' }, { name: 'test', type: 'test' }, FILTERS.PHRASE, false, @@ -217,7 +217,7 @@ describe('kibanaContextFn', () => { FilterStateStore.APP_STATE ); const filter2 = buildFilter( - { fields: [] }, + { fields: [], title: 'dataView' }, { name: 'test', type: 'test' }, FILTERS.PHRASE, false, @@ -230,7 +230,7 @@ describe('kibanaContextFn', () => { ); const filter3 = buildFilter( - { fields: [] }, + { fields: [], title: 'dataView' }, { name: 'test', type: 'test' }, FILTERS.PHRASE, false, diff --git a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap index 3d4a9923fa8c8..82d9183f3d394 100644 --- a/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap +++ b/src/plugins/discover/public/application/components/source_viewer/__snapshots__/source_viewer.test.tsx.snap @@ -594,9 +594,13 @@ exports[`Source Viewer component renders json code editor 1`] = ` fallback={} > - +
+ +
diff --git a/src/plugins/discover/public/application/embeddable/view_saved_search_action.test.ts b/src/plugins/discover/public/application/embeddable/view_saved_search_action.test.ts index 5796dacaa83d8..990be8927766a 100644 --- a/src/plugins/discover/public/application/embeddable/view_saved_search_action.test.ts +++ b/src/plugins/discover/public/application/embeddable/view_saved_search_action.test.ts @@ -11,11 +11,14 @@ import { ContactCardEmbeddable } from 'src/plugins/embeddable/public/lib/test_sa import { ViewSavedSearchAction } from './view_saved_search_action'; import { SavedSearchEmbeddable } from './saved_search_embeddable'; import { createStartContractMock } from '../../__mocks__/start_contract'; +import { uiSettingsServiceMock } from '../../../../../core/public/mocks'; import { savedSearchMock } from '../../__mocks__/saved_search'; import { discoverServiceMock } from '../../__mocks__/services'; import { IndexPattern } from 'src/plugins/data/common'; import { createFilterManagerMock } from 'src/plugins/data/public/query/filter_manager/filter_manager.mock'; import { ViewMode } from 'src/plugins/embeddable/public'; +import { setServices } from '../../kibana_services'; +import type { DiscoverServices } from '../../build_services'; const applicationMock = createStartContractMock(); const savedSearch = savedSearchMock; @@ -45,6 +48,11 @@ const embeddableConfig = { }; describe('view saved search action', () => { + beforeEach(() => { + setServices({ + uiSettings: uiSettingsServiceMock.createStartContract(), + } as unknown as DiscoverServices); + }); it('is compatible when embeddable is of type saved search, in view mode && appropriate permissions are set', async () => { const action = new ViewSavedSearchAction(applicationMock); const embeddable = new SavedSearchEmbeddable( diff --git a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts index 65089bc24317b..67309aab44a76 100644 --- a/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts +++ b/src/plugins/index_pattern_field_editor/__jest__/client_integration/field_editor_flyout_preview.test.ts @@ -366,6 +366,7 @@ describe('Field editor Preview panel', () => { subTitle: 'First doc - subTitle', title: 'First doc - title', }, + documentId: '001', index: 'testIndex', script: { source: 'echo("hello")', diff --git a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_context.tsx b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_context.tsx index e49e0ef6885d0..21ab055c9b05e 100644 --- a/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_context.tsx +++ b/src/plugins/index_pattern_field_editor/public/components/preview/field_preview_context.tsx @@ -335,6 +335,7 @@ export const FieldPreviewProvider: FunctionComponent = ({ children }) => { document: params.document!, context: `${params.type!}_field` as FieldPreviewContext, script: params.script!, + documentId: currentDocId, }); if (currentApiCall !== previewCount.current) { diff --git a/src/plugins/index_pattern_field_editor/public/lib/api.ts b/src/plugins/index_pattern_field_editor/public/lib/api.ts index 9325b5c2faf47..9641619640a52 100644 --- a/src/plugins/index_pattern_field_editor/public/lib/api.ts +++ b/src/plugins/index_pattern_field_editor/public/lib/api.ts @@ -16,11 +16,13 @@ export const initApi = (httpClient: HttpSetup) => { context, script, document, + documentId, }: { index: string; context: FieldPreviewContext; script: { source: string } | null; document: Record; + documentId: string; }) => { return sendRequest(httpClient, { path: `${API_BASE_PATH}/field_preview`, @@ -30,6 +32,7 @@ export const initApi = (httpClient: HttpSetup) => { context, script, document, + documentId, }, }); }; diff --git a/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts b/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts index 11ec1ca7d5666..847dd41e0082b 100644 --- a/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts +++ b/src/plugins/index_pattern_field_editor/server/routes/field_preview.ts @@ -6,8 +6,8 @@ * Side Public License, v 1. */ +import { estypes } from '@elastic/elasticsearch'; import { schema } from '@kbn/config-schema'; -import { HttpResponsePayload } from 'kibana/server'; import { API_BASE_PATH } from '../../common/constants'; import { RouteDependencies } from '../types'; @@ -26,6 +26,7 @@ const bodySchema = schema.object({ schema.literal('long_field'), ]), document: schema.object({}, { unknowns: 'allow' }), + documentId: schema.string(), }); export const registerFieldPreviewRoute = ({ router }: RouteDependencies): void => { @@ -39,30 +40,41 @@ export const registerFieldPreviewRoute = ({ router }: RouteDependencies): void = async (ctx, req, res) => { const { client } = ctx.core.elasticsearch; - const body = JSON.stringify({ - script: req.body.script, - context: req.body.context, - context_setup: { - document: req.body.document, - index: req.body.index, - } as any, - }); + const type = req.body.context.split('_field')[0] as estypes.MappingRuntimeFieldType; + const body = { + runtime_mappings: { + my_runtime_field: { + type, + script: req.body.script, + }, + }, + size: 1, + query: { + term: { + _id: req.body.documentId, + }, + }, + fields: ['my_runtime_field'], + }; try { - const response = await client.asCurrentUser.scriptsPainlessExecute({ - // @ts-expect-error `ExecutePainlessScriptRequest.body` does not allow `string` + const response = await client.asCurrentUser.search({ + index: req.body.index, body, }); - const fieldValue = response.body.result as any[] as HttpResponsePayload; + const fieldValue = response.body.hits.hits[0]?.fields?.my_runtime_field ?? ''; return res.ok({ body: { values: fieldValue } }); - } catch (error) { + } catch (error: any) { // Assume invalid painless script was submitted // Return 200 with error object const handleCustomError = () => { return res.ok({ - body: { values: [], ...error.body }, + body: { + values: [], + error: error.body.error.failed_shards[0]?.reason ?? {}, + }, }); }; diff --git a/src/plugins/kibana_legacy/server/index.ts b/src/plugins/kibana_legacy/server/index.ts index 90c9c2888c9da..15ae8547c73e1 100644 --- a/src/plugins/kibana_legacy/server/index.ts +++ b/src/plugins/kibana_legacy/server/index.ts @@ -27,6 +27,7 @@ export const config: PluginConfigDescriptor = { return; } addDeprecation({ + configPath: 'kibana.defaultAppId', message: `kibana.defaultAppId is deprecated and will be removed in 8.0. Please use the \`defaultRoute\` advanced setting instead`, correctiveActions: { manualSteps: [ diff --git a/src/plugins/kibana_react/public/code_editor/code_editor.tsx b/src/plugins/kibana_react/public/code_editor/code_editor.tsx index 07bda19bf6d14..e0e2c4b3145c0 100644 --- a/src/plugins/kibana_react/public/code_editor/code_editor.tsx +++ b/src/plugins/kibana_react/public/code_editor/code_editor.tsx @@ -14,6 +14,7 @@ import { monaco } from '@kbn/monaco'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import classNames from 'classnames'; +import './register_languages'; import { DARK_THEME, diff --git a/src/plugins/kibana_react/public/code_editor/code_editor_field.tsx b/src/plugins/kibana_react/public/code_editor/code_editor_field.tsx new file mode 100644 index 0000000000000..0e6ab21159f15 --- /dev/null +++ b/src/plugins/kibana_react/public/code_editor/code_editor_field.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 React from 'react'; +import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; +import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { EuiFormControlLayout } from '@elastic/eui'; +import { CodeEditor, Props } from './code_editor'; + +/** + * Renders a Monaco code editor in the same style as other EUI form fields. + */ +export const CodeEditorField: React.FunctionComponent = (props) => { + const { width, height, options, fullWidth, useDarkTheme } = props; + const theme = useDarkTheme ? darkTheme : lightTheme; + const style = { + width, + height, + backgroundColor: options?.readOnly + ? theme.euiFormBackgroundReadOnlyColor + : theme.euiFormBackgroundColor, + }; + + return ( + + ); +}; diff --git a/src/plugins/kibana_react/public/code_editor/index.tsx b/src/plugins/kibana_react/public/code_editor/index.tsx index f7f9c3b7fc8da..9aab815e23544 100644 --- a/src/plugins/kibana_react/public/code_editor/index.tsx +++ b/src/plugins/kibana_react/public/code_editor/index.tsx @@ -7,28 +7,31 @@ */ import React from 'react'; -import { - EuiDelayRender, - EuiErrorBoundary, - EuiLoadingContent, - EuiFormControlLayout, -} from '@elastic/eui'; -import darkTheme from '@elastic/eui/dist/eui_theme_dark.json'; -import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; +import { EuiDelayRender, EuiErrorBoundary, EuiLoadingContent } from '@elastic/eui'; + import { useUiSetting } from '../ui_settings'; -import { Props } from './code_editor'; -import './register_languages'; +import type { Props } from './code_editor'; -export * from './languages'; +export * from './languages/constants'; const LazyBaseEditor = React.lazy(() => import('./code_editor')); - -const Fallback = () => ( - - - +const LazyCodeEditorField = React.lazy(() => + import('./code_editor_field').then((m) => ({ default: m.CodeEditorField })) ); +const Fallback: React.FunctionComponent<{ height: Props['height'] }> = ({ height }) => { + return ( + <> + {/* when height is known, set minHeight to avoid layout shift */} +
+ + + +
+ + ); +}; + export type CodeEditorProps = Props; /** @@ -40,7 +43,7 @@ export const CodeEditor: React.FunctionComponent = (props) => { const darkMode = useUiSetting('theme:darkMode'); return ( - }> + }> @@ -51,38 +54,11 @@ export const CodeEditor: React.FunctionComponent = (props) => { * Renders a Monaco code editor in the same style as other EUI form fields. */ export const CodeEditorField: React.FunctionComponent = (props) => { - const { width, height, options, fullWidth } = props; const darkMode = useUiSetting('theme:darkMode'); - const theme = darkMode ? darkTheme : lightTheme; - const style = { - width, - height, - backgroundColor: options?.readOnly - ? theme.euiFormBackgroundReadOnlyColor - : theme.euiFormBackgroundColor, - }; - return ( - ); diff --git a/src/plugins/telemetry_management_section/public/components/example_security_payload.test.tsx b/src/plugins/kibana_react/public/code_editor/languages/constants.ts similarity index 53% rename from src/plugins/telemetry_management_section/public/components/example_security_payload.test.tsx rename to src/plugins/kibana_react/public/code_editor/languages/constants.ts index 0b22ad5b9c209..af80e4ccc56e2 100644 --- a/src/plugins/telemetry_management_section/public/components/example_security_payload.test.tsx +++ b/src/plugins/kibana_react/public/code_editor/languages/constants.ts @@ -6,12 +6,7 @@ * Side Public License, v 1. */ -import React from 'react'; -import { shallowWithIntl } from '@kbn/test/jest'; -import ExampleSecurityPayload from './example_security_payload'; - -describe('example security payload', () => { - it('renders as expected', () => { - expect(shallowWithIntl()).toMatchSnapshot(); - }); -}); +export { LANG as CssLang } from './css/constants'; +export { LANG as MarkdownLang } from './markdown/constants'; +export { LANG as YamlLang } from './yaml/constants'; +export { LANG as HandlebarsLang } from './handlebars/constants'; diff --git a/src/plugins/kibana_react/public/url_template_editor/url_template_editor.tsx b/src/plugins/kibana_react/public/url_template_editor/url_template_editor.tsx index 0fed4d37e4f7f..c6250d73a8896 100644 --- a/src/plugins/kibana_react/public/url_template_editor/url_template_editor.tsx +++ b/src/plugins/kibana_react/public/url_template_editor/url_template_editor.tsx @@ -66,7 +66,7 @@ export const UrlTemplateEditor: React.FC = ({ return; } - const { dispose } = monaco.languages.registerCompletionItemProvider(HandlebarsLang.ID, { + const { dispose } = monaco.languages.registerCompletionItemProvider(HandlebarsLang, { triggerCharacters: ['{', '/', '?', '&', '='], provideCompletionItems(model, position, context, token) { const { lineNumber } = position; @@ -124,7 +124,7 @@ export const UrlTemplateEditor: React.FC = ({ return (
{ }); }); describe('startChecking', () => { - let originalSetInterval: typeof window['setInterval']; - let mockSetInterval: jest.Mock; - - beforeAll(() => { - originalSetInterval = window.setInterval; - }); - - beforeEach(() => (window.setInterval = mockSetInterval = jest.fn())); - afterAll(() => (window.setInterval = originalSetInterval)); + beforeEach(() => jest.useFakeTimers()); + afterAll(() => jest.useRealTimers()); it('calls sendIfDue every 60000 ms', () => { const telemetryService = mockTelemetryService(); const telemetrySender = new TelemetrySender(telemetryService); telemetrySender.startChecking(); - expect(mockSetInterval).toBeCalledTimes(1); - expect(mockSetInterval).toBeCalledWith(telemetrySender['sendIfDue'], 60000); + expect(setInterval).toBeCalledTimes(1); + expect(setInterval).toBeCalledWith(telemetrySender['sendIfDue'], 60000); }); }); }); diff --git a/src/plugins/telemetry/server/config/deprecations.test.ts b/src/plugins/telemetry/server/config/deprecations.test.ts index 7807cd21916d5..567ef69e8991c 100644 --- a/src/plugins/telemetry/server/config/deprecations.test.ts +++ b/src/plugins/telemetry/server/config/deprecations.test.ts @@ -158,6 +158,7 @@ describe('deprecateEndpointConfigs', () => { expect(mockAddDeprecation.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { + "configPath": "telemetry.url", "correctiveActions": Object { "manualSteps": Array [ "Remove \\"telemetry.url\\" from the Kibana configuration.", @@ -180,6 +181,7 @@ describe('deprecateEndpointConfigs', () => { expect(mockAddDeprecation.mock.calls[0]).toMatchInlineSnapshot(` Array [ Object { + "configPath": "telemetry.optInStatusUrl", "correctiveActions": Object { "manualSteps": Array [ "Remove \\"telemetry.optInStatusUrl\\" from the Kibana configuration.", diff --git a/src/plugins/telemetry/server/config/deprecations.ts b/src/plugins/telemetry/server/config/deprecations.ts index 013ee51a2ddd9..38553be7d5774 100644 --- a/src/plugins/telemetry/server/config/deprecations.ts +++ b/src/plugins/telemetry/server/config/deprecations.ts @@ -35,6 +35,7 @@ export const deprecateEndpointConfigs: ConfigDeprecation = ( } addDeprecation({ + configPath: fullConfigPath, title: i18n.translate('telemetry.endpointConfigs.deprecationTitle', { defaultMessage: 'Setting "{configPath}" is deprecated', values: { configPath: fullConfigPath }, diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/example_security_payload.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/example_security_payload.test.tsx.snap deleted file mode 100644 index a63044ffc8985..0000000000000 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/example_security_payload.test.tsx.snap +++ /dev/null @@ -1,110 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`example security payload renders as expected 1`] = ` - - { - "@timestamp": "2020-09-22T14:34:56.82202300Z", - "agent": { - "build": { - "original": "version: 7.9.1, compiled: Thu Aug 27 14:50:21 2020, branch: 7.9, commit: b594beb958817dee9b9d908191ed766d483df3ea" - }, - "id": "22dd8544-bcac-46cb-b970-5e681bb99e0b", - "type": "endpoint", - "version": "7.9.1" - }, - "Endpoint": { - "policy": { - "applied": { - "artifacts": { - "global": { - "identifiers": [ - { - "sha256": "6a546aade5563d3e8dffc1fe2d93d33edda8f9ca3e17ac3cc9ac707620cb9ecd", - "name": "endpointpe-v4-blocklist" - }, - { - "sha256": "04f9f87accc5d5aea433427bd1bd4ec6908f8528c78ceed26f70df7875a99385", - "name": "endpointpe-v4-exceptionlist" - }, - { - "sha256": "1471838597fcd79a54ea4a3ec9a9beee1a86feaedab6c98e61102559ced822a8", - "name": "endpointpe-v4-model" - }, - { - "sha256": "824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8", - "name": "global-exceptionlist-windows" - } - ], - "version": "1.0.0" - }, - "user": { - "identifiers": [ - { - "sha256": "d801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658", - "name": "endpoint-exceptionlist-windows-v1" - } - ], - "version": "1.0.0" - } - } - } - } - }, - "ecs": { - "version": "1.5.0" - }, - "elastic": { - "agent": { - "id": "b2e88aea-2671-402a-828a-957526bac315" - } - }, - "file": { - "path": "C:\\\\Windows\\\\Temp\\\\mimikatz.exe", - "size": 1263880, - "created": "2020-05-19T07:50:06.0Z", - "accessed": "2020-09-22T14:29:19.93531400Z", - "mtime": "2020-09-22T14:29:03.6040000Z", - "directory": "C:\\\\Windows\\\\Temp", - "hash": { - "sha1": "c9fb7f8a4c6b7b12b493a99a8dc6901d17867388", - "sha256": "cb1553a3c88817e4cc774a5a93f9158f6785bd3815447d04b6c3f4c2c4b21ed7", - "md5": "465d5d850f54d9cde767bda90743df30" - }, - "Ext": { - "code_signature": { - "trusted": true, - "subject_name": "Open Source Developer, Benjamin Delpy", - "exists": true, - "status": "trusted" - }, - "malware_classification": { - "identifier": "endpointpe-v4-model", - "score": 0.99956864118576, - "threshold": 0.71, - "version": "0.0.0" - } - } - }, - "host": { - "os": { - "Ext": { - "variant": "Windows 10 Enterprise Evaluation" - }, - "kernel": "2004 (10.0.19041.388)", - "name": "Windows", - "family": "windows", - "version": "2004 (10.0.19041.388)", - "platform": "windows", - "full": "Windows 10 Enterprise Evaluation 2004 (10.0.19041.388)" - } - }, - "event": { - "kind": "alert" - }, - "cluster_uuid": "kLbKvSMcRiiFAR0t8LebDA", - "cluster_name": "elasticsearch" -} - -`; diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/opt_in_security_example_flyout.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/opt_in_security_example_flyout.test.tsx.snap deleted file mode 100644 index 9110926e39638..0000000000000 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/opt_in_security_example_flyout.test.tsx.snap +++ /dev/null @@ -1,45 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`security flyout renders as expected renders as expected 1`] = ` - - - - -

- Endpoint security data -

-
- - - This is a representative sample of the endpoint security alert event that we collect. Endpoint security data is collected only when the Elastic Endpoint is enabled. It includes information about the endpoint configuration and detection events. - - -
- - - - - - - } - > - - - -
-
-`; diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap index 7c9154cba4f88..758ecf54f4bf0 100644 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -1,50 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`TelemetryManagementSectionComponent does not show the endpoint link when isSecurityExampleEnabled returns false 1`] = ` - -

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

-

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

-
-`; - exports[`TelemetryManagementSectionComponent renders as expected 1`] = ` <_EuiSplitPanelOuter @@ -124,7 +79,7 @@ exports[`TelemetryManagementSectionComponent renders as expected 1`] = `

, - "endpointSecurityData": @@ -287,19 +243,6 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO "timeZone": null, } } - isSecurityExampleEnabled={ - [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": true, - }, - ], - } - } onQueryMatchChange={[MockFunction]} showAppliesSettingMessage={true} telemetryService={ diff --git a/src/plugins/telemetry_management_section/public/components/example_security_payload.tsx b/src/plugins/telemetry_management_section/public/components/example_security_payload.tsx deleted file mode 100644 index 6a18ccac59eeb..0000000000000 --- a/src/plugins/telemetry_management_section/public/components/example_security_payload.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { EuiCodeBlock } from '@elastic/eui'; -import * as React from 'react'; - -const exampleSecurityPayload = { - '@timestamp': '2020-09-22T14:34:56.82202300Z', - agent: { - build: { - original: - 'version: 7.9.1, compiled: Thu Aug 27 14:50:21 2020, branch: 7.9, commit: b594beb958817dee9b9d908191ed766d483df3ea', - }, - id: '22dd8544-bcac-46cb-b970-5e681bb99e0b', - type: 'endpoint', - version: '7.9.1', - }, - Endpoint: { - policy: { - applied: { - artifacts: { - global: { - identifiers: [ - { - sha256: '6a546aade5563d3e8dffc1fe2d93d33edda8f9ca3e17ac3cc9ac707620cb9ecd', - name: 'endpointpe-v4-blocklist', - }, - { - sha256: '04f9f87accc5d5aea433427bd1bd4ec6908f8528c78ceed26f70df7875a99385', - name: 'endpointpe-v4-exceptionlist', - }, - { - sha256: '1471838597fcd79a54ea4a3ec9a9beee1a86feaedab6c98e61102559ced822a8', - name: 'endpointpe-v4-model', - }, - { - sha256: '824859b0c6749cc31951d92a73bbdddfcfe9f38abfe432087934d4dab9766ce8', - name: 'global-exceptionlist-windows', - }, - ], - version: '1.0.0', - }, - user: { - identifiers: [ - { - sha256: 'd801aa1fb7ddcc330a5e3173372ea6af4a3d08ec58074478e85aa5603e926658', - name: 'endpoint-exceptionlist-windows-v1', - }, - ], - version: '1.0.0', - }, - }, - }, - }, - }, - ecs: { - version: '1.5.0', - }, - elastic: { - agent: { - id: 'b2e88aea-2671-402a-828a-957526bac315', - }, - }, - file: { - path: 'C:\\Windows\\Temp\\mimikatz.exe', - size: 1263880, - created: '2020-05-19T07:50:06.0Z', - accessed: '2020-09-22T14:29:19.93531400Z', - mtime: '2020-09-22T14:29:03.6040000Z', - directory: 'C:\\Windows\\Temp', - hash: { - sha1: 'c9fb7f8a4c6b7b12b493a99a8dc6901d17867388', - sha256: 'cb1553a3c88817e4cc774a5a93f9158f6785bd3815447d04b6c3f4c2c4b21ed7', - md5: '465d5d850f54d9cde767bda90743df30', - }, - Ext: { - code_signature: { - trusted: true, - subject_name: 'Open Source Developer, Benjamin Delpy', - exists: true, - status: 'trusted', - }, - malware_classification: { - identifier: 'endpointpe-v4-model', - score: 0.99956864118576, - threshold: 0.71, - version: '0.0.0', - }, - }, - }, - host: { - os: { - Ext: { - variant: 'Windows 10 Enterprise Evaluation', - }, - kernel: '2004 (10.0.19041.388)', - name: 'Windows', - family: 'windows', - version: '2004 (10.0.19041.388)', - platform: 'windows', - full: 'Windows 10 Enterprise Evaluation 2004 (10.0.19041.388)', - }, - }, - event: { - kind: 'alert', - }, - cluster_uuid: 'kLbKvSMcRiiFAR0t8LebDA', - cluster_name: 'elasticsearch', -}; - -const ExampleSecurityPayload: React.FC = () => { - return ( - {JSON.stringify(exampleSecurityPayload, null, 2)} - ); -}; - -// Used for lazy import -// eslint-disable-next-line import/no-default-export -export default ExampleSecurityPayload; diff --git a/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.test.tsx b/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.test.tsx deleted file mode 100644 index 74fd7ddd56cb1..0000000000000 --- a/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.test.tsx +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { shallowWithIntl } from '@kbn/test/jest'; -import { OptInSecurityExampleFlyout } from './opt_in_security_example_flyout'; - -describe('security flyout renders as expected', () => { - it('renders as expected', () => { - expect(shallowWithIntl()).toMatchSnapshot(); - }); -}); diff --git a/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.tsx b/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.tsx deleted file mode 100644 index 58a82487c25da..0000000000000 --- a/src/plugins/telemetry_management_section/public/components/opt_in_security_example_flyout.tsx +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 React from 'react'; - -import { - EuiFlyout, - EuiFlyoutHeader, - EuiFlyoutBody, - EuiPortal, // EuiPortal is a temporary requirement to use EuiFlyout with "ownFocus" - EuiText, - EuiTextColor, - EuiTitle, -} from '@elastic/eui'; -import { loadingSpinner } from './loading_spinner'; - -interface Props { - onClose: () => void; -} - -const LazyExampleSecurityPayload = React.lazy(() => import('./example_security_payload')); - -/** - * React component for displaying the example data associated with the Telemetry opt-in banner. - */ -export class OptInSecurityExampleFlyout extends React.PureComponent { - render() { - return ( - - - - -

Endpoint security data

- - - - This is a representative sample of the endpoint security alert event that we - collect. Endpoint security data is collected only when the Elastic Endpoint is - enabled. It includes information about the endpoint configuration and detection - events. - - - - - - - - - - - ); - } -} diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx index b8332317e6b68..8f27c340720a1 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.test.tsx @@ -21,7 +21,6 @@ describe('TelemetryManagementSectionComponent', () => { it('renders as expected', () => { const onQueryMatchChange = jest.fn(); - const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); const telemetryService = new TelemetryService({ config: { sendUsageTo: 'staging', @@ -45,7 +44,6 @@ describe('TelemetryManagementSectionComponent', () => { onQueryMatchChange={onQueryMatchChange} showAppliesSettingMessage={true} enableSaving={true} - isSecurityExampleEnabled={isSecurityExampleEnabled} toasts={coreStart.notifications.toasts} docLinks={docLinks} /> @@ -55,7 +53,6 @@ describe('TelemetryManagementSectionComponent', () => { it('renders null because query does not match the SEARCH_TERMS', () => { const onQueryMatchChange = jest.fn(); - const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); const telemetryService = new TelemetryService({ config: { enabled: true, @@ -79,7 +76,6 @@ describe('TelemetryManagementSectionComponent', () => { onQueryMatchChange={onQueryMatchChange} showAppliesSettingMessage={false} enableSaving={true} - isSecurityExampleEnabled={isSecurityExampleEnabled} toasts={coreStart.notifications.toasts} docLinks={docLinks} /> @@ -96,7 +92,6 @@ describe('TelemetryManagementSectionComponent', () => { showAppliesSettingMessage={false} enableSaving={true} toasts={coreStart.notifications.toasts} - isSecurityExampleEnabled={isSecurityExampleEnabled} docLinks={docLinks} /> @@ -110,7 +105,6 @@ describe('TelemetryManagementSectionComponent', () => { it('renders because query matches the SEARCH_TERMS', () => { const onQueryMatchChange = jest.fn(); - const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); const telemetryService = new TelemetryService({ config: { enabled: true, @@ -132,7 +126,6 @@ describe('TelemetryManagementSectionComponent', () => { telemetryService={telemetryService} onQueryMatchChange={onQueryMatchChange} showAppliesSettingMessage={false} - isSecurityExampleEnabled={isSecurityExampleEnabled} enableSaving={true} toasts={coreStart.notifications.toasts} docLinks={docLinks} @@ -158,7 +151,6 @@ describe('TelemetryManagementSectionComponent', () => { it('renders null because allowChangingOptInStatus is false', () => { const onQueryMatchChange = jest.fn(); - const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); const telemetryService = new TelemetryService({ config: { enabled: true, @@ -181,7 +173,6 @@ describe('TelemetryManagementSectionComponent', () => { onQueryMatchChange={onQueryMatchChange} showAppliesSettingMessage={true} enableSaving={true} - isSecurityExampleEnabled={isSecurityExampleEnabled} toasts={coreStart.notifications.toasts} docLinks={docLinks} /> @@ -197,7 +188,6 @@ describe('TelemetryManagementSectionComponent', () => { it('shows the OptInExampleFlyout', () => { const onQueryMatchChange = jest.fn(); - const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); const telemetryService = new TelemetryService({ config: { enabled: true, @@ -220,7 +210,6 @@ describe('TelemetryManagementSectionComponent', () => { onQueryMatchChange={onQueryMatchChange} showAppliesSettingMessage={false} enableSaving={true} - isSecurityExampleEnabled={isSecurityExampleEnabled} toasts={coreStart.notifications.toasts} docLinks={docLinks} /> @@ -235,89 +224,8 @@ describe('TelemetryManagementSectionComponent', () => { } }); - it('shows the OptInSecurityExampleFlyout', () => { - const onQueryMatchChange = jest.fn(); - const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); - const telemetryService = new TelemetryService({ - config: { - enabled: true, - banner: true, - allowChangingOptInStatus: true, - optIn: false, - sendUsageTo: 'staging', - sendUsageFrom: 'browser', - }, - isScreenshotMode: false, - reportOptInStatusChange: false, - notifications: coreStart.notifications, - currentKibanaVersion: 'mock_kibana_version', - http: coreSetup.http, - }); - - const component = mountWithIntl( - - ); - try { - const toggleExampleComponent = component.find('FormattedMessage > EuiLink[onClick]').at(1); - const updatedView = toggleExampleComponent.simulate('click'); - updatedView.find('OptInSecurityExampleFlyout'); - updatedView.simulate('close'); - } finally { - component.unmount(); - } - }); - - it('does not show the endpoint link when isSecurityExampleEnabled returns false', () => { - const onQueryMatchChange = jest.fn(); - const isSecurityExampleEnabled = jest.fn().mockReturnValue(false); - const telemetryService = new TelemetryService({ - config: { - enabled: true, - banner: true, - allowChangingOptInStatus: true, - optIn: false, - sendUsageTo: 'staging', - sendUsageFrom: 'browser', - }, - isScreenshotMode: false, - reportOptInStatusChange: false, - currentKibanaVersion: 'mock_kibana_version', - notifications: coreStart.notifications, - http: coreSetup.http, - }); - - const component = mountWithIntl( - - ); - - try { - const description = (component.instance() as TelemetryManagementSection).renderDescription(); - expect(isSecurityExampleEnabled).toBeCalled(); - expect(description).toMatchSnapshot(); - } finally { - component.unmount(); - } - }); - it('toggles the OptIn button', async () => { const onQueryMatchChange = jest.fn(); - const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); const telemetryService = new TelemetryService({ config: { enabled: true, @@ -340,7 +248,6 @@ describe('TelemetryManagementSectionComponent', () => { onQueryMatchChange={onQueryMatchChange} showAppliesSettingMessage={false} enableSaving={true} - isSecurityExampleEnabled={isSecurityExampleEnabled} toasts={coreStart.notifications.toasts} docLinks={docLinks} /> @@ -367,7 +274,6 @@ describe('TelemetryManagementSectionComponent', () => { it('test the wrapper (for coverage purposes)', () => { const onQueryMatchChange = jest.fn(); - const isSecurityExampleEnabled = jest.fn().mockReturnValue(true); const telemetryService = new TelemetryService({ config: { enabled: true, @@ -392,7 +298,6 @@ describe('TelemetryManagementSectionComponent', () => { onQueryMatchChange={onQueryMatchChange} enableSaving={true} toasts={coreStart.notifications.toasts} - isSecurityExampleEnabled={isSecurityExampleEnabled} docLinks={docLinks} /> ).html() diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx index b0d1b42a9b892..3686cb10706bf 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section.tsx @@ -15,7 +15,6 @@ import type { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; import type { DocLinksStart, ToastsStart } from 'src/core/public'; import { PRIVACY_STATEMENT_URL } from '../../../telemetry/common/constants'; import { OptInExampleFlyout } from './opt_in_example_flyout'; -import { OptInSecurityExampleFlyout } from './opt_in_security_example_flyout'; import { LazyField } from '../../../advanced_settings/public'; import { TrackApplicationView } from '../../../usage_collection/public'; @@ -26,7 +25,6 @@ const SEARCH_TERMS = ['telemetry', 'usage', 'data', 'usage data']; interface Props { telemetryService: TelemetryService; onQueryMatchChange: (searchTermMatches: boolean) => void; - isSecurityExampleEnabled: () => boolean; showAppliesSettingMessage: boolean; enableSaving: boolean; query?: { text: string }; @@ -80,9 +78,8 @@ export class TelemetryManagementSection extends Component { } render() { - const { telemetryService, isSecurityExampleEnabled } = this.props; - const { showExample, showSecurityExample, queryMatches, enabled, processing } = this.state; - const securityExampleEnabled = isSecurityExampleEnabled(); + const { telemetryService } = this.props; + const { showExample, queryMatches, enabled, processing } = this.state; if (!telemetryService.getCanChangeOptInStatus()) { return null; @@ -102,11 +99,6 @@ export class TelemetryManagementSection extends Component { /> )} - {showSecurityExample && securityExampleEnabled && ( - - - - )} @@ -182,17 +174,19 @@ export class TelemetryManagementSection extends Component { }; renderDescription = () => { - const { isSecurityExampleEnabled } = this.props; - const securityExampleEnabled = isSecurityExampleEnabled(); const clusterDataLink = ( ); - const endpointSecurityDataLink = ( - - + const securityDataLink = ( + + ); @@ -216,24 +210,14 @@ export class TelemetryManagementSection extends Component { />

- {securityExampleEnabled ? ( - - ) : ( - - )} +

); @@ -277,15 +261,6 @@ export class TelemetryManagementSection extends Component { showExample: !this.state.showExample, }); }; - - toggleSecurityExample = () => { - const { isSecurityExampleEnabled } = this.props; - const securityExampleEnabled = isSecurityExampleEnabled(); - if (!securityExampleEnabled) return; - this.setState({ - showSecurityExample: !this.state.showSecurityExample, - }); - }; } // required for lazy loading diff --git a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx index 91881dffa52d7..30769683803f1 100644 --- a/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx +++ b/src/plugins/telemetry_management_section/public/components/telemetry_management_section_wrapper.tsx @@ -12,21 +12,19 @@ import type { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; import type TelemetryManagementSection from './telemetry_management_section'; export type TelemetryManagementSectionWrapperProps = Omit< TelemetryManagementSection['props'], - 'telemetryService' | 'showAppliesSettingMessage' | 'isSecurityExampleEnabled' + 'telemetryService' | 'showAppliesSettingMessage' >; const TelemetryManagementSectionComponent = lazy(() => import('./telemetry_management_section')); export function telemetryManagementSectionWrapper( - telemetryService: TelemetryPluginSetup['telemetryService'], - shouldShowSecuritySolutionUsageExample: () => boolean + telemetryService: TelemetryPluginSetup['telemetryService'] ) { const TelemetryManagementSectionWrapper = (props: TelemetryManagementSectionWrapperProps) => ( }> diff --git a/src/plugins/telemetry_management_section/public/index.ts b/src/plugins/telemetry_management_section/public/index.ts index db6ea17556ed3..f39d949540192 100644 --- a/src/plugins/telemetry_management_section/public/index.ts +++ b/src/plugins/telemetry_management_section/public/index.ts @@ -10,7 +10,6 @@ import { TelemetryManagementSectionPlugin } from './plugin'; export { OptInExampleFlyout } from './components'; -export type { TelemetryManagementSectionPluginSetup } from './plugin'; export function plugin() { return new TelemetryManagementSectionPlugin(); } diff --git a/src/plugins/telemetry_management_section/public/plugin.tsx b/src/plugins/telemetry_management_section/public/plugin.tsx index 8f2d85f9107b6..e75dbfe9d56b5 100644 --- a/src/plugins/telemetry_management_section/public/plugin.tsx +++ b/src/plugins/telemetry_management_section/public/plugin.tsx @@ -10,7 +10,7 @@ import React from 'react'; import type { AdvancedSettingsSetup } from 'src/plugins/advanced_settings/public'; import type { TelemetryPluginSetup } from 'src/plugins/telemetry/public'; import type { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; -import type { Plugin, CoreStart, CoreSetup } from 'src/core/public'; +import type { CoreStart, CoreSetup } from 'src/core/public'; import { telemetryManagementSectionWrapper, @@ -23,18 +23,7 @@ export interface TelemetryManagementSectionPluginDepsSetup { usageCollection?: UsageCollectionSetup; } -export interface TelemetryManagementSectionPluginSetup { - toggleSecuritySolutionExample: (enabled: boolean) => void; -} - -export class TelemetryManagementSectionPlugin - implements Plugin -{ - private showSecuritySolutionExample = false; - private shouldShowSecuritySolutionExample = () => { - return this.showSecuritySolutionExample; - }; - +export class TelemetryManagementSectionPlugin { public setup( core: CoreSetup, { @@ -50,21 +39,16 @@ export class TelemetryManagementSectionPlugin (props) => { return ( - {telemetryManagementSectionWrapper( - telemetryService, - this.shouldShowSecuritySolutionExample - )(props as TelemetryManagementSectionWrapperProps)} + {telemetryManagementSectionWrapper(telemetryService)( + props as TelemetryManagementSectionWrapperProps + )} ); }, true ); - return { - toggleSecuritySolutionExample: (enabled: boolean) => { - this.showSecuritySolutionExample = enabled; - }, - }; + return {}; } public start(core: CoreStart) {} diff --git a/src/plugins/vis_types/timeseries/public/application/components/markdown_editor.js b/src/plugins/vis_types/timeseries/public/application/components/markdown_editor.js index adee297fe0119..610c694e08121 100644 --- a/src/plugins/vis_types/timeseries/public/application/components/markdown_editor.js +++ b/src/plugins/vis_types/timeseries/public/application/components/markdown_editor.js @@ -116,7 +116,7 @@ export class MarkdownEditor extends Component {
{ `); expect(logMsgArr).toMatchInlineSnapshot(` Array [ - "Exception @ migrateGaugeVerticalSplitToAlignment! TypeError: Cannot read property 'gauge' of undefined", + "Exception @ migrateGaugeVerticalSplitToAlignment! TypeError: Cannot read properties of undefined (reading 'gauge')", "Exception @ migrateGaugeVerticalSplitToAlignment! Payload: {\\"type\\":\\"gauge\\"}", ] `); diff --git a/src/setup_node_env/exit_on_warning.js b/src/setup_node_env/exit_on_warning.js index e9c96f2c49bb4..998dd02a6bff0 100644 --- a/src/setup_node_env/exit_on_warning.js +++ b/src/setup_node_env/exit_on_warning.js @@ -29,6 +29,22 @@ var IGNORE_WARNINGS = [ file: '/node_modules/supertest/node_modules/superagent/lib/node/index.js', line: 418, }, + // TODO @elastic/es-clients + // 'Use of deprecated folder mapping "./" in the "exports" field module resolution of the package + // at node_modules/@elastic/elasticsearch/package.json.' + // This is a breaking change in Node 12, which elasticsearch-js supports. + // https://github.com/elastic/elasticsearch-js/issues/1465 + // https://nodejs.org/api/deprecations.html#DEP0148 + { + name: 'DeprecationWarning', + code: 'DEP0148', + }, + // In future versions of Node.js, fs.rmdir(path, { recursive: true }) will be removed. + // Remove after https://github.com/elastic/synthetics/pull/390 + { + name: 'DeprecationWarning', + code: 'DEP0147', + }, { // TODO: @elastic/es-clients - The new client will attempt a Product check and it will `process.emitWarning` // that the security features are blocking such check. diff --git a/test/api_integration/apis/index_pattern_field_editor/field_preview.ts b/test/api_integration/apis/index_pattern_field_editor/field_preview.ts index a84accc8e5f03..7123be1deb18a 100644 --- a/test/api_integration/apis/index_pattern_field_editor/field_preview.ts +++ b/test/api_integration/apis/index_pattern_field_editor/field_preview.ts @@ -12,11 +12,14 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { API_BASE_PATH } from './constants'; const INDEX_NAME = 'api-integration-test-field-preview'; +const DOC_ID = '1'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const es = getService('es'); + const document = { foo: 1, bar: 'hello' }; + const createIndex = async () => { await es.indices.create({ index: INDEX_NAME, @@ -35,6 +38,15 @@ export default function ({ getService }: FtrProviderContext) { }); }; + const addDoc = async () => { + await es.index({ + index: INDEX_NAME, + id: DOC_ID, + body: document, + refresh: 'wait_for', + }); + }; + const deleteIndex = async () => { await es.indices.delete({ index: INDEX_NAME, @@ -42,12 +54,13 @@ export default function ({ getService }: FtrProviderContext) { }; describe('Field preview', function () { - before(async () => await createIndex()); + before(async () => { + await createIndex(); + await addDoc(); + }); after(async () => await deleteIndex()); describe('should return the script value', () => { - const document = { foo: 1, bar: 'hello' }; - const tests = [ { context: 'keyword_field', @@ -77,6 +90,7 @@ export default function ({ getService }: FtrProviderContext) { const payload = { script: test.script, document, + documentId: DOC_ID, context: test.context, index: INDEX_NAME, }; diff --git a/test/functional/apps/discover/_indexpattern_without_timefield.ts b/test/functional/apps/discover/_indexpattern_without_timefield.ts index 81fb4f92ab730..42291691f3f5f 100644 --- a/test/functional/apps/discover/_indexpattern_without_timefield.ts +++ b/test/functional/apps/discover/_indexpattern_without_timefield.ts @@ -17,7 +17,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const testSubjects = getService('testSubjects'); const PageObjects = getPageObjects(['common', 'timePicker', 'discover']); - describe('indexpattern without timefield', () => { + // FLAKY https://github.com/elastic/kibana/issues/107057 + describe.skip('indexpattern without timefield', () => { before(async () => { await security.testUser.setRoles(['kibana_admin', 'kibana_timefield']); await esArchiver.loadIfNeeded( diff --git a/test/plugin_functional/plugins/core_plugin_deprecations/server/config.ts b/test/plugin_functional/plugins/core_plugin_deprecations/server/config.ts index 650567f761aa3..5b9145be52661 100644 --- a/test/plugin_functional/plugins/core_plugin_deprecations/server/config.ts +++ b/test/plugin_functional/plugins/core_plugin_deprecations/server/config.ts @@ -22,6 +22,7 @@ type ConfigType = TypeOf; const configSecretDeprecation: ConfigDeprecation = (settings, fromPath, addDeprecation) => { if (get(settings, 'corePluginDeprecations.secret') !== 42) { addDeprecation({ + configPath: 'corePluginDeprecations.secret', documentationUrl: 'config-secret-doc-url', message: 'Kibana plugin functional tests will no longer allow corePluginDeprecations.secret ' + diff --git a/test/plugin_functional/test_suites/core/deprecations.ts b/test/plugin_functional/test_suites/core/deprecations.ts index dbaf10008e35e..d8dc82a56cb55 100644 --- a/test/plugin_functional/test_suites/core/deprecations.ts +++ b/test/plugin_functional/test_suites/core/deprecations.ts @@ -19,6 +19,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide const CorePluginDeprecationsPluginDeprecations: DomainDeprecationDetails[] = [ { + configPath: 'corePluginDeprecations.oldProperty', title: 'Setting "corePluginDeprecations.oldProperty" is deprecated', level: 'critical', message: @@ -33,6 +34,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide requireRestart: true, }, { + configPath: 'corePluginDeprecations.noLongerUsed', title: 'Setting "corePluginDeprecations.noLongerUsed" is deprecated', level: 'critical', message: 'You no longer need to configure "corePluginDeprecations.noLongerUsed".', @@ -46,6 +48,7 @@ export default function ({ getService, getPageObjects }: PluginFunctionalProvide requireRestart: true, }, { + configPath: 'corePluginDeprecations.secret', title: 'corePluginDeprecations has a deprecated setting', level: 'critical', message: diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 5ed1cb5336b47..b985a507d5fca 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -146,7 +146,6 @@ def functionalXpack(Map params = [:]) { } } - //temporarily disable apm e2e test since it's breaking. // whenChanged([ // 'x-pack/plugins/apm/', // ]) { diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index 26f7da073223d..b3e019a1bf46d 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -69,6 +69,7 @@ export const config: PluginConfigDescriptor = { ) ) { addDeprecation({ + configPath: 'xpack.actions.customHostSettings.ssl.rejectUnauthorized', message: `"xpack.actions.customHostSettings[].ssl.rejectUnauthorized" is deprecated.` + `Use "xpack.actions.customHostSettings[].ssl.verificationMode" instead, ` + @@ -96,6 +97,7 @@ export const config: PluginConfigDescriptor = { const actions = get(settings, fromPath); if (actions?.hasOwnProperty('rejectUnauthorized')) { addDeprecation({ + configPath: `${fromPath}.rejectUnauthorized`, message: `"xpack.actions.rejectUnauthorized" is deprecated. Use "xpack.actions.verificationMode" instead, ` + `with the setting "verificationMode:full" eql to "rejectUnauthorized:true", ` + @@ -122,6 +124,7 @@ export const config: PluginConfigDescriptor = { const actions = get(settings, fromPath); if (actions?.hasOwnProperty('proxyRejectUnauthorizedCertificates')) { addDeprecation({ + configPath: `${fromPath}.proxyRejectUnauthorizedCertificates`, message: `"xpack.actions.proxyRejectUnauthorizedCertificates" is deprecated. Use "xpack.actions.proxyVerificationMode" instead, ` + `with the setting "proxyVerificationMode:full" eql to "rejectUnauthorized:true",` + @@ -148,6 +151,7 @@ export const config: PluginConfigDescriptor = { const actions = get(settings, fromPath); if (actions?.enabled === false || actions?.enabled === true) { addDeprecation({ + configPath: 'xpack.actions.enabled', message: `"xpack.actions.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, correctiveActions: { manualSteps: [`Remove "xpack.actions.enabled" from your kibana configs.`], diff --git a/x-pack/plugins/alerting/server/index.ts b/x-pack/plugins/alerting/server/index.ts index 3b4688173e9b5..b7a510a37661b 100644 --- a/x-pack/plugins/alerting/server/index.ts +++ b/x-pack/plugins/alerting/server/index.ts @@ -62,6 +62,7 @@ export const config: PluginConfigDescriptor = { const alerting = get(settings, fromPath); if (alerting?.enabled === false || alerting?.enabled === true) { addDeprecation({ + configPath: 'xpack.alerting.enabled', message: `"xpack.alerting.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, correctiveActions: { manualSteps: [`Remove "xpack.alerting.enabled" from your kibana configs.`], diff --git a/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.test.ts new file mode 100644 index 0000000000000..779e201635495 --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.test.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { extractEntityAndBoundaryReferences } from './migrations'; + +describe('geo_containment migration utilities', () => { + test('extractEntityAndBoundaryReferences', () => { + expect( + extractEntityAndBoundaryReferences({ + index: 'foo*', + indexId: 'foobar', + geoField: 'geometry', + entity: 'vehicle_id', + dateField: '@timestamp', + boundaryType: 'entireIndex', + boundaryIndexTitle: 'boundary*', + boundaryIndexId: 'boundaryid', + boundaryGeoField: 'geometry', + }) + ).toEqual({ + params: { + boundaryGeoField: 'geometry', + boundaryIndexRefName: 'boundary_index_boundaryid', + boundaryIndexTitle: 'boundary*', + boundaryType: 'entireIndex', + dateField: '@timestamp', + entity: 'vehicle_id', + geoField: 'geometry', + index: 'foo*', + indexRefName: 'tracked_index_foobar', + }, + references: [ + { + id: 'foobar', + name: 'param:tracked_index_foobar', + type: 'index-pattern', + }, + { + id: 'boundaryid', + name: 'param:boundary_index_boundaryid', + type: 'index-pattern', + }, + ], + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.ts new file mode 100644 index 0000000000000..113b4cf796d2f --- /dev/null +++ b/x-pack/plugins/alerting/server/saved_objects/geo_containment/migrations.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + SavedObjectAttributes, + SavedObjectReference, + SavedObjectUnsanitizedDoc, +} from 'kibana/server'; +import { AlertTypeParams } from '../../index'; +import { Query } from '../../../../../../src/plugins/data/common/query'; +import { RawAlert } from '../../types'; + +// These definitions are dupes of the SO-types in stack_alerts/geo_containment +// There are not exported to avoid deep imports from stack_alerts plugins into here +const GEO_CONTAINMENT_ID = '.geo-containment'; +interface GeoContainmentParams extends AlertTypeParams { + index: string; + indexId: string; + geoField: string; + entity: string; + dateField: string; + boundaryType: string; + boundaryIndexTitle: string; + boundaryIndexId: string; + boundaryGeoField: string; + boundaryNameField?: string; + indexQuery?: Query; + boundaryIndexQuery?: Query; +} +type GeoContainmentExtractedParams = Omit & { + indexRefName: string; + boundaryIndexRefName: string; +}; + +export function extractEntityAndBoundaryReferences(params: GeoContainmentParams): { + params: GeoContainmentExtractedParams; + references: SavedObjectReference[]; +} { + const { indexId, boundaryIndexId, ...otherParams } = params; + + const indexRefNamePrefix = 'tracked_index_'; + const boundaryRefNamePrefix = 'boundary_index_'; + + // Since these are stack-alerts, we need to prefix with the `param:`-namespace + const references = [ + { + name: `param:${indexRefNamePrefix}${indexId}`, + type: `index-pattern`, + id: indexId as string, + }, + { + name: `param:${boundaryRefNamePrefix}${boundaryIndexId}`, + type: 'index-pattern', + id: boundaryIndexId as string, + }, + ]; + return { + params: { + ...otherParams, + indexRefName: `${indexRefNamePrefix}${indexId}`, + boundaryIndexRefName: `${boundaryRefNamePrefix}${boundaryIndexId}`, + }, + references, + }; +} + +export function extractRefsFromGeoContainmentAlert( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + if (doc.attributes.alertTypeId !== GEO_CONTAINMENT_ID) { + return doc; + } + + const { + attributes: { params }, + } = doc; + + const { params: newParams, references } = extractEntityAndBoundaryReferences( + params as GeoContainmentParams + ); + return { + ...doc, + attributes: { + ...doc.attributes, + params: newParams as SavedObjectAttributes, + }, + references: [...(doc.references || []), ...references], + }; +} 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 336d0a1f1cfc1..9f3eb68a953da 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -1913,6 +1913,96 @@ describe('successful migrations', () => { ], }); }); + + test('geo-containment alert migration extracts boundary and index references', () => { + const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const alert = { + ...getMockData({ + alertTypeId: '.geo-containment', + params: { + indexId: 'foo', + boundaryIndexId: 'bar', + }, + }), + }; + + const migratedAlert = migration7160(alert, migrationContext); + + expect(migratedAlert.references).toEqual([ + { id: 'foo', name: 'param:tracked_index_foo', type: 'index-pattern' }, + { id: 'bar', name: 'param:boundary_index_bar', type: 'index-pattern' }, + ]); + + expect(migratedAlert.attributes.params).toEqual({ + boundaryIndexRefName: 'boundary_index_bar', + indexRefName: 'tracked_index_foo', + }); + + expect(migratedAlert.attributes.params.indexId).toEqual(undefined); + expect(migratedAlert.attributes.params.boundaryIndexId).toEqual(undefined); + }); + + test('geo-containment alert migration should preserve foreign references', () => { + const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const alert = { + ...getMockData({ + alertTypeId: '.geo-containment', + params: { + indexId: 'foo', + boundaryIndexId: 'bar', + }, + }), + references: [ + { + name: 'foreign-name', + id: '999', + type: 'foreign-name', + }, + ], + }; + + const migratedAlert = migration7160(alert, migrationContext); + + expect(migratedAlert.references).toEqual([ + { + name: 'foreign-name', + id: '999', + type: 'foreign-name', + }, + { id: 'foo', name: 'param:tracked_index_foo', type: 'index-pattern' }, + { id: 'bar', name: 'param:boundary_index_bar', type: 'index-pattern' }, + ]); + + expect(migratedAlert.attributes.params).toEqual({ + boundaryIndexRefName: 'boundary_index_bar', + indexRefName: 'tracked_index_foo', + }); + + expect(migratedAlert.attributes.params.indexId).toEqual(undefined); + expect(migratedAlert.attributes.params.boundaryIndexId).toEqual(undefined); + }); + + test('geo-containment alert migration ignores other alert-types', () => { + const migration7160 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['7.16.0']; + const alert = { + ...getMockData({ + alertTypeId: '.foo', + references: [ + { + name: 'foreign-name', + id: '999', + type: 'foreign-name', + }, + ], + }), + }; + + const migratedAlert = migration7160(alert, migrationContext); + + expect(typeof migratedAlert.attributes.legacyId).toEqual('string'); // introduced by setLegacyId migration + delete migratedAlert.attributes.legacyId; + expect(migratedAlert).toEqual(alert); + }); }); }); diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index de3d440d3dbde..6034a3c868831 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -19,6 +19,7 @@ import { import { RawAlert, RawAlertAction } from '../types'; import { EncryptedSavedObjectsPluginSetup } from '../../../encrypted_saved_objects/server'; import type { IsMigrationNeededPredicate } from '../../../encrypted_saved_objects/server'; +import { extractRefsFromGeoContainmentAlert } from './geo_containment/migrations'; const SIEM_APP_ID = 'securitySolution'; const SIEM_SERVER_APP_ID = 'siem'; @@ -117,7 +118,8 @@ export function getMigrations( pipeMigrations( setLegacyId, getRemovePreconfiguredConnectorsFromReferencesFn(isPreconfigured), - addRuleIdsToLegacyNotificationReferences + addRuleIdsToLegacyNotificationReferences, + extractRefsFromGeoContainmentAlert ) ); diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index eb7c74a4540be..43a779407d2a4 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -33,8 +33,6 @@ export function getSeverityColor(score: number) { return mlGetSeverityColor(score); } -export const ML_TRANSACTION_LATENCY_DETECTOR_INDEX = 0; - export const ML_ERRORS = { INVALID_LICENSE: i18n.translate( 'xpack.apm.anomaly_detection.error.invalid_license', diff --git a/x-pack/plugins/apm/common/utils/apm_ml_anomaly_query.ts b/x-pack/plugins/apm/common/utils/apm_ml_anomaly_query.ts deleted file mode 100644 index 26b859d37cf7f..0000000000000 --- a/x-pack/plugins/apm/common/utils/apm_ml_anomaly_query.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -export function apmMlAnomalyQuery(detectorIndex: 0 | 1 | 2) { - return [ - { - bool: { - filter: [ - { - terms: { - result_type: ['model_plot', 'record'], - }, - }, - { - term: { detector_index: detectorIndex }, - }, - ], - }, - }, - ]; -} diff --git a/x-pack/plugins/apm/dev_docs/local_setup.md b/x-pack/plugins/apm/dev_docs/local_setup.md index eaa99560400e6..21d861fbb4e0b 100644 --- a/x-pack/plugins/apm/dev_docs/local_setup.md +++ b/x-pack/plugins/apm/dev_docs/local_setup.md @@ -1,6 +1,4 @@ -## Local environment setup - -### Kibana +# Start Kibana ``` git clone git@github.com:elastic/kibana.git @@ -9,25 +7,35 @@ yarn kbn bootstrap yarn start --no-base-path ``` -### APM Server, Elasticsearch and data +# Elasticsearch, APM Server and data generators To access an elasticsearch instance that has live data you have two options: -#### A. Connect to Elasticsearch on Cloud (internal devs only) +## A. Cloud-based ES Cluster (internal devs only) -Find the credentials for the cluster [here](https://github.com/elastic/observability-dev/blob/master/docs/observability-clusters.md) +Use the [oblt-cli](https://github.com/elastic/observability-test-environments/blob/master/tools/oblt_cli/README.md) to connect to a cloud-based ES cluster. -#### B. Start Elastic Stack and APM data generators +## B. Local ES Cluster +### Start Elasticsearch and APM data generators +_Docker Compose is required_ ``` git clone git@github.com:elastic/apm-integration-testing.git cd apm-integration-testing/ ./scripts/compose.py start master --all --no-kibana ``` -_Docker Compose is required_ +### Connect Kibana to Elasticsearch -### Setup default APM users +Update `config/kibana.dev.yml` with: + +```yml +elasticsearch.hosts: http://localhost:9200 +elasticsearch.username: admin +elasticsearch.password: changeme +``` + +# Setup default APM users APM behaves differently depending on which the role and permissions a logged in user has. To create the users run: @@ -37,11 +45,10 @@ node x-pack/plugins/apm/scripts/create-apm-users-and-roles.js --username admin - This will create: -**apm_read_user**: Read only user - -**apm_power_user**: Read+write user. + - **apm_read_user**: Read only user + - **apm_power_user**: Read+write user. -## Debugging Elasticsearch queries +# Debugging Elasticsearch queries All APM api endpoints accept `_inspect=true` as a query param that will output all Elasticsearch queries performed in that request. It will be available in the browser response and on localhost it is also available in the Kibana Node.js process output. diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts index 42da37aa7ef57..5b4a48b65b33f 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts @@ -5,6 +5,27 @@ * 2.0. */ +function deleteAllRules() { + cy.request({ + log: false, + method: 'GET', + url: '/api/alerting/rules/_find', + }).then(({ body }) => { + if (body.data.length > 0) { + cy.log(`Deleting rules`); + } + + body.data.map(({ id }: { id: string }) => { + cy.request({ + headers: { 'kbn-xsrf': 'true' }, + log: false, + method: 'DELETE', + url: `/api/alerting/rule/${id}`, + }); + }); + }); +} + describe('Rules', () => { describe('Error count', () => { const ruleName = 'Error count threshold'; @@ -12,59 +33,30 @@ describe('Rules', () => { '.euiPopover__panel-isOpen [data-test-subj=comboBoxSearchInput]'; const confirmModalButtonSelector = '.euiModal button[data-test-subj=confirmModalConfirmButton]'; - const deleteButtonSelector = - '[data-test-subj=deleteActionHoverButton]:first'; - const editButtonSelector = '[data-test-subj=editActionHoverButton]:first'; describe('when created from APM', () => { describe('when created from Service Inventory', () => { before(() => { cy.loginAsPowerUser(); + deleteAllRules(); }); - it('creates and updates a rule', () => { + after(() => { + deleteAllRules(); + }); + + it('creates a rule', () => { // Create a rule in APM cy.visit('/app/apm/services'); cy.contains('Alerts and rules').click(); cy.contains('Error count').click(); cy.contains('Create threshold rule').click(); - // Change the environment to "testing" - cy.contains('Environment All').click(); - cy.get(comboBoxInputSelector).type('testing{enter}'); - // Save, with no actions cy.contains('button:not(:disabled)', 'Save').click(); cy.get(confirmModalButtonSelector).click(); cy.contains(`Created rule "${ruleName}`); - - // Go to Stack Management - cy.contains('Alerts and rules').click(); - cy.contains('Manage rules').click(); - - // Edit the rule, changing the environment to "All" - cy.get(editButtonSelector).click(); - cy.contains('Environment testing').click(); - cy.get(comboBoxInputSelector).type('All{enter}'); - cy.contains('button:not(:disabled)', 'Save').click(); - - cy.contains(`Updated '${ruleName}'`); - - // Wait for the table to be ready for next edit click - cy.get('.euiBasicTable').not('.euiBasicTable-loading'); - - // Ensure the rule now shows "All" for the environment - cy.get(editButtonSelector).click(); - cy.contains('Environment All'); - cy.contains('button', 'Cancel').click(); - - // Delete the rule - cy.get(deleteButtonSelector).click(); - cy.get(confirmModalButtonSelector).click(); - - // Ensure the table is empty - cy.contains('Create your first rule'); }); }); }); @@ -72,14 +64,29 @@ describe('Rules', () => { describe('when created from Stack management', () => { before(() => { cy.loginAsPowerUser(); + deleteAllRules(); + cy.intercept( + 'GET', + '/api/alerting/rules/_find?page=1&per_page=10&default_search_operator=AND&sort_field=name&sort_order=asc' + ).as('list rules API call'); + }); + + after(() => { + deleteAllRules(); }); it('creates a rule', () => { // Go to stack management cy.visit('/app/management/insightsAndAlerting/triggersActions/rules'); + // Wait for this call to finish so the create rule button does not disappear. + // The timeout is set high because at this point we're also waiting for the + // full page load. + cy.wait('@list rules API call', { timeout: 30000 }); + // Create a rule cy.contains('button', 'Create rule').click(); + cy.get('[name=name]').type(ruleName); cy.contains('.euiFlyout button', ruleName).click(); @@ -92,16 +99,6 @@ describe('Rules', () => { cy.get(confirmModalButtonSelector).click(); cy.contains(`Created rule "${ruleName}`); - - // Wait for the table to be ready for next delete click - cy.get('.euiBasicTable').not('.euiBasicTable-loading'); - - // Delete the rule - cy.get(deleteButtonSelector).click(); - cy.get(confirmModalButtonSelector).click(); - - // Ensure the table is empty - cy.contains('Create your first rule'); }); }); }); 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 93dbe4ba51226..519cb0aa31cdb 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/support/commands.ts @@ -21,6 +21,7 @@ Cypress.Commands.add( cy.log(`Logging in as ${username}`); const kibanaUrl = Cypress.env('KIBANA_URL'); cy.request({ + log: false, method: 'POST', url: `${kibanaUrl}/internal/security/login`, body: { diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts b/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts index 5e4dd9f8657ff..a2ff99c5c377e 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress/tasks/es_archiver.ts @@ -17,7 +17,7 @@ export const esArchiverLoad = (folder: string) => { const path = Path.join(ES_ARCHIVE_DIR, folder); execSync( `node ../../../../scripts/es_archiver load "${path}" --config ../../../test/functional/config.js`, - { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED } } + { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; @@ -25,13 +25,13 @@ export const esArchiverUnload = (folder: string) => { const path = Path.join(ES_ARCHIVE_DIR, folder); execSync( `node ../../../../scripts/es_archiver unload "${path}" --config ../../../test/functional/config.js`, - { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED } } + { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; export const esArchiverResetKibana = () => { execSync( `node ../../../../scripts/es_archiver empty-kibana-index --config ../../../test/functional/config.js`, - { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED } } + { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_types.ts b/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_types.ts index a7520fa65a162..0115718ac07a9 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_types.ts +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_types.ts @@ -16,14 +16,14 @@ export function getComparisonTypes({ start?: string; end?: string; }) { - const momentStart = moment(start); - const momentEnd = moment(end); + const momentStart = moment(start).startOf('second'); + const momentEnd = moment(end).startOf('second'); const dateDiff = getDateDifference({ start: momentStart, end: momentEnd, - unitOfTime: 'days', precise: true, + unitOfTime: 'days', }); // Less than or equals to one day diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx index c29d258b37541..ce7d05d467291 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.test.tsx @@ -96,6 +96,19 @@ describe('TimeComparison', () => { TimeRangeComparisonType.WeekBefore.valueOf(), ]); }); + + it('shows week and day before when 24 hours is selected but milliseconds are different', () => { + expect( + getComparisonTypes({ + start: '2021-10-15T00:52:59.554Z', + end: '2021-10-14T00:52:59.553Z', + }) + ).toEqual([ + TimeRangeComparisonType.DayBefore.valueOf(), + TimeRangeComparisonType.WeekBefore.valueOf(), + ]); + }); + it('shows week before when 25 hours is selected', () => { expect( getComparisonTypes({ diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json b/x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json index d6718b7511179..7f7cbf4e3efa2 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/test-tsconfig.json @@ -3,7 +3,8 @@ "types": [ "node" ], - "noErrorTruncation": true + "noErrorTruncation": true, + "assumeChangesOnlyAffectDirectDependencies": true }, "include": [ "./apm_api_integration/**/*", @@ -11,6 +12,8 @@ "../../typings/**/*" ], "exclude": [ - "**/__fixtures__/**/*" + "**/__fixtures__/**/*", + "**/target/**", + "**/node_modules/**" ] } diff --git a/x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json b/x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json index 8254ec67eb3c5..2ece69d491fca 100644 --- a/x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json +++ b/x-pack/plugins/apm/scripts/optimize-tsconfig/tsconfig.json @@ -13,6 +13,7 @@ "**/node_modules/**" ], "compilerOptions": { - "noErrorTruncation": true + "noErrorTruncation": true, + "assumeChangesOnlyAffectDirectDependencies": true } } diff --git a/x-pack/plugins/apm/scripts/precommit.js b/x-pack/plugins/apm/scripts/precommit.js index 89c5055c6a7f7..a259cf932c912 100644 --- a/x-pack/plugins/apm/scripts/precommit.js +++ b/x-pack/plugins/apm/scripts/precommit.js @@ -66,7 +66,9 @@ const tasks = new Listr( execa( 'node', [ - resolve(__dirname, './jest.js'), + resolve(__dirname, '../../../../scripts/jest.js'), + '--config', + resolve(__dirname, '../jest.config.js'), '--reporters', resolve(__dirname, '../../../../node_modules/jest-silent-reporter'), '--collect-coverage', diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index 10758b6d90cdc..4d4bc8dc185ab 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -5,21 +5,21 @@ * 2.0. */ -import Boom from '@hapi/boom'; import { Logger } from 'kibana/server'; +import uuid from 'uuid/v4'; import { snakeCase } from 'lodash'; +import Boom from '@hapi/boom'; import moment from 'moment'; -import uuid from 'uuid/v4'; import { ML_ERRORS } from '../../../common/anomaly_detection'; -import { - METRICSET_NAME, - PROCESSOR_EVENT, -} from '../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../common/processor_event'; import { environmentQuery } from '../../../common/utils/environment_query'; -import { withApmSpan } from '../../utils/with_apm_span'; import { Setup } from '../helpers/setup_request'; +import { + TRANSACTION_DURATION, + PROCESSOR_EVENT, +} from '../../../common/elasticsearch_fieldnames'; import { APM_ML_JOB_GROUP, ML_MODULE_ID_APM_TRANSACTION } from './constants'; +import { withApmSpan } from '../../utils/with_apm_span'; import { getAnomalyDetectionJobs } from './get_anomaly_detection_jobs'; export async function createAnomalyDetectionJobs( @@ -92,8 +92,8 @@ async function createAnomalyDetectionJob({ query: { bool: { filter: [ - { term: { [PROCESSOR_EVENT]: ProcessorEvent.metric } }, - { term: { [METRICSET_NAME]: 'transaction' } }, + { term: { [PROCESSOR_EVENT]: ProcessorEvent.transaction } }, + { exists: { field: TRANSACTION_DURATION } }, ...environmentQuery(environment), ], }, @@ -105,7 +105,7 @@ async function createAnomalyDetectionJob({ job_tags: { environment, // identifies this as an APM ML job & facilitates future migrations - apm_ml_version: 3, + apm_ml_version: 2, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index 97c95e4e40045..9b2d79dc726ee 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -11,11 +11,7 @@ import { estypes } from '@elastic/elasticsearch'; import { ESSearchResponse } from '../../../../../../src/core/types/elasticsearch'; import { MlPluginSetup } from '../../../../ml/server'; import { PromiseReturnType } from '../../../../observability/typings/common'; -import { - getSeverity, - ML_ERRORS, - ML_TRANSACTION_LATENCY_DETECTOR_INDEX, -} from '../../../common/anomaly_detection'; +import { getSeverity, ML_ERRORS } from '../../../common/anomaly_detection'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; import { getServiceHealthStatus } from '../../../common/service_health_status'; import { @@ -26,7 +22,6 @@ import { rangeQuery } from '../../../../observability/server'; import { withApmSpan } from '../../utils/with_apm_span'; import { getMlJobsWithAPMGroup } from '../anomaly_detection/get_ml_jobs_with_apm_group'; import { Setup } from '../helpers/setup_request'; -import { apmMlAnomalyQuery } from '../../../common/utils/apm_ml_anomaly_query'; export const DEFAULT_ANOMALIES: ServiceAnomaliesResponse = { mlJobIds: [], @@ -61,7 +56,7 @@ export async function getServiceAnomalies({ query: { bool: { filter: [ - ...apmMlAnomalyQuery(ML_TRANSACTION_LATENCY_DETECTOR_INDEX), + { terms: { result_type: ['model_plot', 'record'] } }, ...rangeQuery( Math.min(end - 30 * 60 * 1000, start), end, diff --git a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts index a7357bbc1dd34..a61e0614f5b1a 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_anomaly_data/fetcher.ts @@ -12,8 +12,6 @@ import { rangeQuery } from '../../../../../observability/server'; import { asMutableArray } from '../../../../common/utils/as_mutable_array'; import { withApmSpan } from '../../../utils/with_apm_span'; import { Setup } from '../../helpers/setup_request'; -import { apmMlAnomalyQuery } from '../../../../common/utils/apm_ml_anomaly_query'; -import { ML_TRANSACTION_LATENCY_DETECTOR_INDEX } from '../../../../common/anomaly_detection'; export type ESResponse = Exclude< PromiseReturnType, @@ -42,7 +40,7 @@ export function anomalySeriesFetcher({ query: { bool: { filter: [ - ...apmMlAnomalyQuery(ML_TRANSACTION_LATENCY_DETECTOR_INDEX), + { terms: { result_type: ['model_plot', 'record'] } }, { term: { partition_field_value: serviceName } }, { term: { by_field_value: transactionType } }, ...rangeQuery(start, end, 'timestamp'), diff --git a/x-pack/plugins/banners/server/config.ts b/x-pack/plugins/banners/server/config.ts index 37b4c57fc2ce1..18c6dc7c04687 100644 --- a/x-pack/plugins/banners/server/config.ts +++ b/x-pack/plugins/banners/server/config.ts @@ -44,6 +44,8 @@ export const config: PluginConfigDescriptor = { const pluginConfig = get(rootConfig, fromPath); if (pluginConfig?.placement === 'header') { addDeprecation({ + configPath: 'xpack.banners.placement', + level: 'critical', message: 'The `header` value for xpack.banners.placement has been replaced by `top`', correctiveActions: { manualSteps: [ diff --git a/x-pack/plugins/enterprise_search/kibana.json b/x-pack/plugins/enterprise_search/kibana.json index 5f33fa27c6640..7a844acfa9740 100644 --- a/x-pack/plugins/enterprise_search/kibana.json +++ b/x-pack/plugins/enterprise_search/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "requiredPlugins": ["features", "spaces", "licensing", "data", "charts", "infra"], "configPath": ["enterpriseSearch"], - "optionalPlugins": ["usageCollection", "security", "home", "cloud"], + "optionalPlugins": ["usageCollection", "security", "home", "cloud", "customIntegrations"], "server": true, "ui": true, "requiredBundles": ["home", "kibanaReact"], diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.tsx index 8e1e6487197f9..7539055253732 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curation_suggestion/curation_suggestion.tsx @@ -132,7 +132,7 @@ export const CurationSuggestion: React.FC = () => { result={result} isMetaEngine={isMetaEngine} schemaForTypeHighlights={engine.schema} - resultPosition={index + 1} + resultPosition={index + existingCurationResults.length + 1} /> ))} @@ -152,7 +152,7 @@ export const CurationSuggestion: React.FC = () => { result={result} isMetaEngine={isMetaEngine} schemaForTypeHighlights={engine.schema} - resultPosition={index + 1} + resultPosition={index + suggestedPromotedDocuments.length + 1} /> ))} diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts index cf1b45d468260..753871765896a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/document_creation/document_creation_logic.test.ts @@ -493,7 +493,7 @@ describe('DocumentCreationLogic', () => { await nextTick(); expect(DocumentCreationLogic.actions.setErrors).toHaveBeenCalledWith( - "Cannot read property 'total' of undefined" + "Cannot read properties of undefined (reading 'total')" ); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx index 1fd1a9a79b225..801ed296137c7 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/components/log_retention_callout.tsx @@ -19,12 +19,14 @@ import { AppLogic } from '../../../app_logic'; import { SETTINGS_PATH } from '../../../routes'; import { ANALYTICS_TITLE } from '../../analytics'; import { API_LOGS_TITLE } from '../../api_logs'; +import { CRAWLER_TITLE } from '../../crawler'; import { LogRetentionLogic, LogRetentionOptions, renderLogRetentionDate } from '../index'; const TITLE_MAP = { [LogRetentionOptions.Analytics]: ANALYTICS_TITLE, [LogRetentionOptions.API]: API_LOGS_TITLE, + [LogRetentionOptions.Crawler]: CRAWLER_TITLE, }; interface Props { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts index 251fd93069afe..20c5860160b78 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/log_retention_logic.test.ts @@ -33,6 +33,11 @@ describe('LogRetentionLogic', () => { enabled: true, retention_policy: { is_default: true, min_age_days: 180 }, }, + crawler: { + disabled_at: null, + enabled: true, + retention_policy: { is_default: true, min_age_days: 180 }, + }, }; const TYPICAL_CLIENT_LOG_RETENTION = { @@ -46,6 +51,11 @@ describe('LogRetentionLogic', () => { enabled: true, retentionPolicy: { isDefault: true, minAgeDays: 180 }, }, + crawler: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, }; const DEFAULT_VALUES = { @@ -146,6 +156,11 @@ describe('LogRetentionLogic', () => { enabled: true, retentionPolicy: null, }, + crawler: { + disabledAt: null, + enabled: true, + retentionPolicy: null, + }, }); expect(LogRetentionLogic.values).toEqual({ @@ -161,6 +176,11 @@ describe('LogRetentionLogic', () => { enabled: true, retentionPolicy: null, }, + crawler: { + disabledAt: null, + enabled: true, + retentionPolicy: null, + }, }, }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx index c7c4d90d91ce8..e1e6820204d94 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/constants.tsx @@ -37,6 +37,16 @@ const CAPITALIZATION_MAP = { { defaultMessage: 'API' } ), }, + [LogRetentionOptions.Crawler]: { + capitalized: i18n.translate( + 'xpack.enterpriseSearch.appSearch.logRetention.type.crawler.title.capitalized', + { defaultMessage: 'Web crawler' } + ), + lowercase: i18n.translate( + 'xpack.enterpriseSearch.appSearch.logRetention.type.crawler.title.lowercase', + { defaultMessage: 'web crawler' } + ), + }, }; interface Props { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/types.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/types.ts index a03154b8c57c2..a2c615aee3a0e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/types.ts @@ -8,11 +8,13 @@ export enum LogRetentionOptions { Analytics = 'analytics', API = 'api', + Crawler = 'crawler', } export interface LogRetention { [LogRetentionOptions.Analytics]: LogRetentionSettings; [LogRetentionOptions.API]: LogRetentionSettings; + [LogRetentionOptions.Crawler]: LogRetentionSettings; } export interface LogRetentionPolicy { @@ -29,6 +31,7 @@ export interface LogRetentionSettings { export interface LogRetentionServer { [LogRetentionOptions.Analytics]: LogRetentionServerSettings; [LogRetentionOptions.API]: LogRetentionServerSettings; + [LogRetentionOptions.Crawler]: LogRetentionServerSettings; } export interface LogRetentionServerPolicy { diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/utils/convert_log_retention.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/utils/convert_log_retention.test.ts index b67be6d435c2e..b6a5997209247 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/utils/convert_log_retention.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/utils/convert_log_retention.test.ts @@ -21,6 +21,11 @@ describe('convertLogRetentionFromServerToClient', () => { enabled: true, retention_policy: { is_default: true, min_age_days: 180 }, }, + crawler: { + disabled_at: null, + enabled: true, + retention_policy: { is_default: true, min_age_days: 180 }, + }, }) ).toEqual({ analytics: { @@ -33,6 +38,11 @@ describe('convertLogRetentionFromServerToClient', () => { enabled: true, retentionPolicy: { isDefault: true, minAgeDays: 180 }, }, + crawler: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, }); }); @@ -49,6 +59,11 @@ describe('convertLogRetentionFromServerToClient', () => { enabled: true, retention_policy: { is_default: true, min_age_days: null }, }, + crawler: { + disabled_at: null, + enabled: true, + retention_policy: { is_default: true, min_age_days: null }, + }, }) ).toEqual({ analytics: { @@ -61,6 +76,11 @@ describe('convertLogRetentionFromServerToClient', () => { enabled: true, retentionPolicy: { isDefault: true, minAgeDays: null }, }, + crawler: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: null }, + }, }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/utils/convert_log_retention.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/utils/convert_log_retention.ts index cc8b84b667b68..fd2452d05f530 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/utils/convert_log_retention.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/utils/convert_log_retention.ts @@ -24,6 +24,9 @@ export const convertLogRetentionFromServerToClient = ( [LogRetentionOptions.API]: convertLogRetentionSettingsFromServerToClient( logRetention[LogRetentionOptions.API] ), + [LogRetentionOptions.Crawler]: convertLogRetentionSettingsFromServerToClient( + logRetention[LogRetentionOptions.Crawler] + ), }); const convertLogRetentionSettingsFromServerToClient = ({ diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.test.tsx index fc20a9dbc5519..d04b6613dc671 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.test.tsx @@ -39,6 +39,13 @@ describe('', () => { minAgeDays: 7, }, }, + crawler: { + enabled: true, + retentionPolicy: { + isDefault: true, + minAgeDays: 7, + }, + }, }, }; @@ -128,4 +135,42 @@ describe('', () => { expect(actions.closeModals).toHaveBeenCalled(); }); }); + + describe('crawler', () => { + it('renders the Crawler panel when openedModal is set to Crawler', () => { + setMockValues({ + ...values, + openedModal: LogRetentionOptions.Crawler, + }); + + const logRetentionPanel = shallow(); + expect( + logRetentionPanel.find('[data-test-subj="CrawlerLogRetentionConfirmationModal"]').length + ).toBe(1); + }); + + it('calls saveLogRetention on save when showing crawler', () => { + setMockValues({ + ...values, + openedModal: LogRetentionOptions.Crawler, + }); + + const logRetentionPanel = shallow(); + const genericConfirmationModal = logRetentionPanel.find(GenericConfirmationModal); + genericConfirmationModal.prop('onSave')(); + expect(actions.saveLogRetention).toHaveBeenCalledWith(LogRetentionOptions.Crawler, false); + }); + + it('calls closeModals on close', () => { + setMockValues({ + ...values, + openedModal: LogRetentionOptions.Crawler, + }); + + const logRetentionPanel = shallow(); + const genericConfirmationModal = logRetentionPanel.find(GenericConfirmationModal); + genericConfirmationModal.prop('onClose')(); + expect(actions.closeModals).toHaveBeenCalled(); + }); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx index 28d4257f2487c..694d41f11bbff 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_confirmation_modal.tsx @@ -133,6 +133,54 @@ export const LogRetentionConfirmationModal: React.FC = () => { onSave={() => saveLogRetention(LogRetentionOptions.API, false)} /> )} + + {openedModal === LogRetentionOptions.Crawler && ( + +

+ {i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.modal.crawler.description', + { + defaultMessage: + 'When you disable writing, engines stop logging Web Crawler events. Your existing data is deleted according to the storage time frame.', + } + )} +

+

+ + {CANNOT_BE_RECOVERED_TEXT} + +

+ + } + target={DISABLE_TEXT} + onClose={closeModals} + onSave={() => saveLogRetention(LogRetentionOptions.Crawler, false)} + /> + )} ); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx index 3f6bbe4307b57..0d096727d481c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.test.tsx @@ -101,7 +101,7 @@ describe('', () => { ).toEqual(true); }); - it('enables both switches when isLogRetentionUpdating is false', () => { + it('enables all switches when isLogRetentionUpdating is false', () => { setMockValues({ isLogRetentionUpdating: false, logRetention: mockLogRetention({}), @@ -113,9 +113,12 @@ describe('', () => { expect( logRetentionPanel.find('[data-test-subj="LogRetentionPanelAPISwitch"]').prop('disabled') ).toEqual(false); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelCrawlerSwitch"]').prop('disabled') + ).toEqual(false); }); - it('disables both switches when isLogRetentionUpdating is true', () => { + it('disables all switches when isLogRetentionUpdating is true', () => { setMockValues({ isLogRetentionUpdating: true, logRetention: mockLogRetention({}), @@ -128,6 +131,9 @@ describe('', () => { expect( logRetentionPanel.find('[data-test-subj="LogRetentionPanelAPISwitch"]').prop('disabled') ).toEqual(true); + expect( + logRetentionPanel.find('[data-test-subj="LogRetentionPanelCrawlerSwitch"]').prop('disabled') + ).toEqual(true); }); it('calls toggleLogRetention when analytics log retention option is changed', () => { @@ -150,7 +156,7 @@ describe('', () => { setMockValues({ isLogRetentionUpdating: false, logRetention: mockLogRetention({ - analytics: { + api: { enabled: false, }, }), @@ -159,6 +165,20 @@ describe('', () => { logRetentionPanel.find('[data-test-subj="LogRetentionPanelAPISwitch"]').simulate('change'); expect(actions.toggleLogRetention).toHaveBeenCalledWith('api'); }); + + it('calls toggleLogRetention when crawler log retention option is changed', () => { + setMockValues({ + isLogRetentionUpdating: false, + logRetention: mockLogRetention({ + crawler: { + enabled: false, + }, + }), + }); + const logRetentionPanel = shallow(); + logRetentionPanel.find('[data-test-subj="LogRetentionPanelCrawlerSwitch"]').simulate('change'); + expect(actions.toggleLogRetention).toHaveBeenCalledWith('crawler'); + }); }); const mockLogRetention = (logRetention: Partial) => { @@ -173,6 +193,11 @@ const mockLogRetention = (logRetention: Partial) => { enabled: true, retentionPolicy: { isDefault: true, minAgeDays: 180 }, }, + crawler: { + disabledAt: null, + enabled: true, + retentionPolicy: { isDefault: true, minAgeDays: 180 }, + }, }; return { @@ -184,5 +209,9 @@ const mockLogRetention = (logRetention: Partial) => { ...baseLogRetention.api, ...logRetention.api, }, + crawler: { + ...baseLogRetention.crawler, + ...logRetention.crawler, + }, }; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx index 9012a30b950d6..22a3ea6473183 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/settings/log_retention/log_retention_panel.tsx @@ -32,6 +32,7 @@ export const LogRetentionPanel: React.FC = () => { const hasILM = logRetention !== null; const analyticsLogRetentionSettings = logRetention?.[LogRetentionOptions.Analytics]; const apiLogRetentionSettings = logRetention?.[LogRetentionOptions.API]; + const crawlerLogRetentionSettings = logRetention?.[LogRetentionOptions.Crawler]; useEffect(() => { fetchLogRetention(); @@ -100,6 +101,33 @@ export const LogRetentionPanel: React.FC = () => { data-test-subj="LogRetentionPanelAPISwitch" /> + + + + + {i18n.translate( + 'xpack.enterpriseSearch.appSearch.settings.logRetention.crawler.label', + { + defaultMessage: 'Web Crawler Logs', + } + )} + + {': '} + {hasILM && ( + + + + )} + + } + checked={!!crawlerLogRetentionSettings?.enabled} + onChange={() => toggleLogRetention(LogRetentionOptions.Crawler)} + disabled={isLogRetentionUpdating} + data-test-subj="LogRetentionPanelCrawlerSwitch" + /> +

diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx index a682e10269e6c..db4f80dc37f4b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/frequency.tsx @@ -26,10 +26,10 @@ import { ViewContentHeader } from '../../../../components/shared/view_content_he import { NAV, RESET_BUTTON } from '../../../../constants'; import { DIFFERENT_SYNC_TYPES_DOCS_URL } from '../../../../routes'; import { + LEARN_MORE_LINK, SOURCE_FREQUENCY_DESCRIPTION, SOURCE_SYNC_FREQUENCY_TITLE, BLOCKED_TIME_WINDOWS_TITLE, - SYNC_FREQUENCY_LINK_LABEL, SYNC_UNSAVED_CHANGES_MESSAGE, } from '../../constants'; import { SourceLogic } from '../../source_logic'; @@ -84,16 +84,6 @@ export const Frequency: React.FC = ({ tabId }) => { ); - const docsLinks = ( - - - - {SYNC_FREQUENCY_LINK_LABEL} - - - - ); - return ( = ({ tabId }) => { /> + {SOURCE_FREQUENCY_DESCRIPTION}{' '} + + {LEARN_MORE_LINK} + + + } action={actions} /> - {docsLinks} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx index 54ce0563ddeb3..2dfa2a6420f7f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/objects_and_assets.tsx @@ -27,11 +27,11 @@ import { ViewContentHeader } from '../../../../components/shared/view_content_he import { NAV, RESET_BUTTON } from '../../../../constants'; import { OBJECTS_AND_ASSETS_DOCS_URL } from '../../../../routes'; import { + LEARN_MORE_LINK, SYNC_MANAGEMENT_CONTENT_EXTRACTION_LABEL, SYNC_MANAGEMENT_THUMBNAILS_LABEL, SYNC_MANAGEMENT_THUMBNAILS_GLOBAL_CONFIG_LABEL, SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION, - OBJECTS_AND_ASSETS_LINK_LABEL, SOURCE_OBJECTS_AND_ASSETS_LABEL, SYNC_UNSAVED_CHANGES_MESSAGE, } from '../../constants'; @@ -84,12 +84,16 @@ export const ObjectsAndAssets: React.FC = () => { /> + {SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION}{' '} + + {LEARN_MORE_LINK} + + + } action={actions} /> - - {OBJECTS_AND_ASSETS_LINK_LABEL} - {SOURCE_OBJECTS_AND_ASSETS_LABEL} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.test.tsx index fb9cdc6916fa9..2d1e8105c12b2 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.test.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { shallow } from 'enzyme'; -import { EuiLink, EuiCallOut, EuiSwitch } from '@elastic/eui'; +import { EuiCallOut, EuiSwitch } from '@elastic/eui'; import { Synchronization } from './synchronization'; @@ -28,7 +28,6 @@ describe('Synchronization', () => { it('renders when config enabled', () => { const wrapper = shallow(); - expect(wrapper.find(EuiLink)).toHaveLength(1); expect(wrapper.find(EuiSwitch)).toHaveLength(1); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx index e88d4d251fa54..dec275adb3c50 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/synchronization/synchronization.tsx @@ -15,12 +15,12 @@ import { ViewContentHeader } from '../../../../components/shared/view_content_he import { NAV } from '../../../../constants'; import { SYNCHRONIZATION_DOCS_URL } from '../../../../routes'; import { + LEARN_MORE_LINK, SOURCE_SYNCHRONIZATION_DESCRIPTION, SYNCHRONIZATION_DISABLED_TITLE, SYNCHRONIZATION_DISABLED_DESCRIPTION, SOURCE_SYNCHRONIZATION_TOGGLE_LABEL, SOURCE_SYNCHRONIZATION_TOGGLE_DESCRIPTION, - SYNCHRONIZATION_LINK_LABEL, } from '../../constants'; import { SourceLogic } from '../../source_logic'; import { SourceLayout } from '../source_layout'; @@ -65,11 +65,15 @@ export const Synchronization: React.FC = () => { > + {SOURCE_SYNCHRONIZATION_DESCRIPTION}{' '} + + {LEARN_MORE_LINK} + + + } /> - - {SYNCHRONIZATION_LINK_LABEL} - {isSyncConfigEnabled ? syncToggle : syncDisabledCallout} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts index f44dbae0608ea..087716e565ad0 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/constants.ts @@ -531,7 +531,7 @@ export const SOURCE_SYNCHRONIZATION_DESCRIPTION = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.sourceSynchronizationDescription', { defaultMessage: - 'Synchronization provides control over data being indexed from the content source. Enable synchronization of data from the content source to Workplace Search.', + 'Enable or disable synchronization of data from this content source to Workplace Search.', } ); @@ -539,7 +539,7 @@ export const SOURCE_FREQUENCY_DESCRIPTION = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.sourceFrequencyDescription', { defaultMessage: - 'Schedule the frequency of data synchronization between Workplace search and the content source. Indexing schedules that occur less frequently lower the burden on third-party servers, while more frequent will ensure your data is up-to-date.', + 'Manage the frequency of data synchronization from Workplace search to this content source. Sync more frequently to ensure your data is up to date. Sync less frequently to reduce the burden on third party servers.', } ); @@ -547,7 +547,7 @@ export const SOURCE_OBJECTS_AND_ASSETS_DESCRIPTION = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.sourceObjectsAndAssetsDescription', { defaultMessage: - 'Customize the indexing rules that determine what data is synchronized from this content source to Workplace Search.', + 'Customize the indexing rules that determine which objects and assets are synchronized from this content source to Workplace Search.', } ); @@ -636,13 +636,6 @@ export const BLOCKED_TIME_WINDOWS_TITLE = i18n.translate( } ); -export const SYNCHRONIZATION_LINK_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.synchronizationLinkLabel', - { - defaultMessage: 'Learn more about synchronization', - } -); - export const SYNCHRONIZATION_DISABLED_TITLE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.synchronizationDisabledTitle', { @@ -657,20 +650,6 @@ export const SYNCHRONIZATION_DISABLED_DESCRIPTION = i18n.translate( } ); -export const SYNC_FREQUENCY_LINK_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.syncFrequencyLinkLabel', - { - defaultMessage: 'Learn more about synchronization frequency', - } -); - -export const OBJECTS_AND_ASSETS_LINK_LABEL = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.sources.objectsAndAssetsLinkLabel', - { - defaultMessage: 'Learn more about Objects and assets', - } -); - export const FULL_SYNC_LABEL = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.fullSyncLabel', { diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/box.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/box.svg new file mode 100644 index 0000000000000..b1b542eadd59c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/box.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/confluence_cloud.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/confluence_cloud.svg new file mode 100644 index 0000000000000..7aac36a6fe3c5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/confluence_cloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/confluence_server.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/confluence_server.svg new file mode 100644 index 0000000000000..7aac36a6fe3c5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/confluence_server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/custom_api_source.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/custom_api_source.svg new file mode 100644 index 0000000000000..cc07fbbc50877 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/custom_api_source.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/dropbox.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/dropbox.svg new file mode 100644 index 0000000000000..01e5a7735de12 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/dropbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/github.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/github.svg new file mode 100644 index 0000000000000..aa9c3e5b45146 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/github.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/github_enterprise_server.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/github_enterprise_server.svg new file mode 100644 index 0000000000000..aa9c3e5b45146 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/github_enterprise_server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/gmail.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/gmail.svg new file mode 100644 index 0000000000000..31fe60c6a63f9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/gmail.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/google_drive.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/google_drive.svg new file mode 100644 index 0000000000000..f3fe82cd3cd98 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/google_drive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/jira_cloud.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/jira_cloud.svg new file mode 100644 index 0000000000000..c12e55798d889 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/jira_cloud.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/jira_server.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/jira_server.svg new file mode 100644 index 0000000000000..4dfd0fd910079 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/jira_server.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/onedrive.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/onedrive.svg new file mode 100644 index 0000000000000..c390dff1e418f --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/onedrive.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/salesforce.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/salesforce.svg new file mode 100644 index 0000000000000..ef6d552949424 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/salesforce.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/salesforce_sandbox.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/salesforce_sandbox.svg new file mode 100644 index 0000000000000..ef6d552949424 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/salesforce_sandbox.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/servicenow.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/servicenow.svg new file mode 100644 index 0000000000000..6388ec44d21d7 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/servicenow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/sharepoint_online.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/sharepoint_online.svg new file mode 100644 index 0000000000000..aebfd7a8e49c0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/sharepoint_online.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/slack.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/slack.svg new file mode 100644 index 0000000000000..8f6fc0c987eaa --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/slack.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/public/assets/source_icons/zendesk.svg b/x-pack/plugins/enterprise_search/public/assets/source_icons/zendesk.svg new file mode 100644 index 0000000000000..8afd143dd9a7c --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/assets/source_icons/zendesk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/plugins/enterprise_search/server/integrations.ts b/x-pack/plugins/enterprise_search/server/integrations.ts new file mode 100644 index 0000000000000..48909261243e8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/integrations.ts @@ -0,0 +1,304 @@ +/* + * 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 { HttpServiceSetup } from 'src/core/server'; + +import type { IntegrationCategory } from '../../../../src/plugins/custom_integrations/common'; +import type { CustomIntegrationsPluginSetup } from '../../../../src/plugins/custom_integrations/server'; + +interface WorkplaceSearchIntegration { + id: string; + title: string; + description: string; + categories: IntegrationCategory[]; + uiInternalPath?: string; +} + +const workplaceSearchIntegrations: WorkplaceSearchIntegration[] = [ + { + id: 'box', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.boxName', { + defaultMessage: 'Box', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.boxDescription', + { + defaultMessage: 'Search over your files and folders stored on Box with Workplace Search.', + } + ), + categories: ['document_storage'], + }, + { + id: 'confluence_cloud', + title: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.confluenceCloudName', + { + defaultMessage: 'Confluence Cloud', + } + ), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.confluenceCloudDescription', + { + defaultMessage: + 'Search over your organizational content on Confluence Cloud with Workplace Search.', + } + ), + categories: ['knowledge_platform'], + }, + { + id: 'confluence_server', + title: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.confluenceServerName', + { + defaultMessage: 'Confluence Server', + } + ), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.confluenceServerDescription', + { + defaultMessage: + 'Search over your organizational content on Confluence Server with Workplace Search.', + } + ), + categories: ['knowledge_platform'], + }, + { + id: 'dropbox', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.dropboxName', { + defaultMessage: 'Dropbox', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.dropboxDescription', + { + defaultMessage: + 'Search over your files and folders stored on Dropbox with Workplace Search.', + } + ), + categories: ['document_storage'], + }, + { + id: 'github', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.githubName', { + defaultMessage: 'GitHub', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.githubDescription', + { + defaultMessage: 'Search over your projects and repos on GitHub with Workplace Search.', + } + ), + categories: ['software_development'], + }, + { + id: 'github_enterprise_server', + title: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.githubEnterpriseServerName', + { + defaultMessage: 'GitHub Enterprise Server', + } + ), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.githubEnterpriseServerDescription', + { + defaultMessage: + 'Search over your projects and repos on GitHub Enterprise Server with Workplace Search.', + } + ), + categories: ['software_development'], + }, + { + id: 'gmail', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.gmailName', { + defaultMessage: 'Gmail', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.gmailDescription', + { + defaultMessage: 'Search over your emails managed by Gmail with Workplace Search.', + } + ), + categories: ['communication'], + }, + { + id: 'google_drive', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.googleDriveName', { + defaultMessage: 'Google Drive', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.googleDriveDescription', + { + defaultMessage: 'Search over your documents on Google Drive with Workplace Search.', + } + ), + categories: ['document_storage'], + }, + { + id: 'jira_cloud', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.jiraCloudName', { + defaultMessage: 'Jira Cloud', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.jiraCloudDescription', + { + defaultMessage: 'Search over your project workflow on Jira Cloud with Workplace Search.', + } + ), + categories: ['project_management'], + }, + { + id: 'jira_server', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.jiraServerName', { + defaultMessage: 'Jira Server', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.jiraServerDescription', + { + defaultMessage: 'Search over your project workflow on Jira Server with Workplace Search.', + } + ), + categories: ['project_management'], + }, + { + id: 'onedrive', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.onedriveName', { + defaultMessage: 'OneDrive', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.onedriveDescription', + { + defaultMessage: 'Search over your files stored on OneDrive with Workplace Search.', + } + ), + categories: ['document_storage'], + uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/one_drive', + }, + { + id: 'salesforce', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.salesforceName', { + defaultMessage: 'Salesforce', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.salesforceDescription', + { + defaultMessage: 'Search over your content on Salesforce with Workplace Search.', + } + ), + categories: ['crm'], + }, + { + id: 'salesforce_sandbox', + title: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxName', + { + defaultMessage: 'Salesforce Sandbox', + } + ), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.salesforceSandboxDescription', + { + defaultMessage: 'Search over your content on Salesforce Sandbox with Workplace Search.', + } + ), + categories: ['crm'], + }, + { + id: 'servicenow', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.servicenowName', { + defaultMessage: 'ServiceNow', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.servicenowDescription', + { + defaultMessage: 'Search over your content on ServiceNow with Workplace Search.', + } + ), + categories: ['enterprise_management'], + }, + { + id: 'sharepoint_online', + title: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineName', + { + defaultMessage: 'SharePoint Online', + } + ), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.sharepointOnlineDescription', + { + defaultMessage: 'Search over your files stored on SharePoint Online with Workplace Search.', + } + ), + categories: ['document_storage'], + uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/share_point', + }, + { + id: 'slack', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.slackName', { + defaultMessage: 'Slack', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.slackDescription', + { + defaultMessage: 'Search over your messages on Slack with Workplace Search.', + } + ), + categories: ['communication'], + }, + { + id: 'zendesk', + title: i18n.translate('xpack.enterpriseSearch.workplaceSearch.integrations.zendeskName', { + defaultMessage: 'Zendesk', + }), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.zendeskDescription', + { + defaultMessage: 'Search over your tickets on Zendesk with Workplace Search.', + } + ), + categories: ['customer_support'], + }, + { + id: 'custom_api_source', + title: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceName', + { + defaultMessage: 'Custom API Source', + } + ), + description: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.integrations.customApiSourceDescription', + { + defaultMessage: + 'Search over anything by building your own integration with Workplace Search.', + } + ), + categories: ['custom'], + uiInternalPath: '/app/enterprise_search/workplace_search/sources/add/custom', + }, +]; + +export const registerEnterpriseSearchIntegrations = ( + http: HttpServiceSetup, + customIntegrations: CustomIntegrationsPluginSetup +) => { + workplaceSearchIntegrations.forEach((integration) => { + customIntegrations.registerCustomIntegration({ + uiInternalPath: `/app/enterprise_search/workplace_search/sources/add/${integration.id}`, + icons: [ + { + type: 'svg', + src: http.basePath.prepend( + `/plugins/enterpriseSearch/assets/source_icons/${integration.id}.svg` + ), + }, + ], + isBeta: false, + shipper: 'enterprise_search', + ...integration, + }); + }); +}; diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 26572134aa31c..1d58da5bbdd2b 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -15,6 +15,7 @@ import { KibanaRequest, DEFAULT_APP_CATEGORIES, } from '../../../../src/core/server'; +import { CustomIntegrationsPluginSetup } from '../../../../src/plugins/custom_integrations/server'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { InfraPluginSetup } from '../../infra/server'; @@ -31,6 +32,7 @@ import { import { registerTelemetryUsageCollector as registerASTelemetryUsageCollector } from './collectors/app_search/telemetry'; import { registerTelemetryUsageCollector as registerESTelemetryUsageCollector } from './collectors/enterprise_search/telemetry'; import { registerTelemetryUsageCollector as registerWSTelemetryUsageCollector } from './collectors/workplace_search/telemetry'; +import { registerEnterpriseSearchIntegrations } from './integrations'; import { checkAccess } from './lib/check_access'; import { entSearchHttpAgent } from './lib/enterprise_search_http_agent'; @@ -55,6 +57,7 @@ interface PluginsSetup { security?: SecurityPluginSetup; features: FeaturesPluginSetup; infra: InfraPluginSetup; + customIntegrations?: CustomIntegrationsPluginSetup; } interface PluginsStart { @@ -80,11 +83,15 @@ export class EnterpriseSearchPlugin implements Plugin { public setup( { capabilities, http, savedObjects, getStartServices }: CoreSetup, - { usageCollection, security, features, infra }: PluginsSetup + { usageCollection, security, features, infra, customIntegrations }: PluginsSetup ) { const config = this.config; const log = this.logger; + if (customIntegrations) { + registerEnterpriseSearchIntegrations(http, customIntegrations); + } + /* * Initialize config.ssl.certificateAuthorities file(s) - required for all API calls (+ access checks) */ diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts index aff9cd17dd001..d8ff7f1815a62 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.test.ts @@ -57,13 +57,21 @@ describe('log settings routes', () => { describe('validates', () => { it('validates good data', () => { - const request = { + mockRouter.shouldValidate({ body: { analytics: { enabled: true }, + }, + }); + mockRouter.shouldValidate({ + body: { api: { enabled: true }, }, - }; - mockRouter.shouldValidate(request); + }); + mockRouter.shouldValidate({ + body: { + crawler: { enabled: true }, + }, + }); }); it('rejects bad data', () => { diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.ts index b83dfd9d9ef15..2d0ce1411761a 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/settings.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/settings.ts @@ -38,6 +38,11 @@ export function registerSettingsRoutes({ enabled: schema.boolean(), }) ), + crawler: schema.maybe( + schema.object({ + enabled: schema.boolean(), + }) + ), }), }, }, diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts index 67edcc356e902..4374bbe88e183 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.test.ts @@ -92,6 +92,7 @@ describe('source engine routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ path: '/as/engines/:name/source_engines/bulk_create', + hasJsonResponse: false, }); }); }); @@ -145,6 +146,7 @@ describe('source engine routes', () => { it('creates a request to enterprise search', () => { expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ path: '/as/engines/:name/source_engines/:source_engine_name', + hasJsonResponse: false, }); }); }); diff --git a/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts index 1d41a82287b0e..79be0c9c29322 100644 --- a/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts +++ b/x-pack/plugins/enterprise_search/server/routes/app_search/source_engines.ts @@ -45,6 +45,7 @@ export function registerSourceEnginesRoutes({ }, enterpriseSearchRequestHandler.createRequest({ path: '/as/engines/:name/source_engines/bulk_create', + hasJsonResponse: false, }) ); @@ -60,6 +61,7 @@ export function registerSourceEnginesRoutes({ }, enterpriseSearchRequestHandler.createRequest({ path: '/as/engines/:name/source_engines/:source_engine_name', + hasJsonResponse: false, }) ); } diff --git a/x-pack/plugins/event_log/server/index.ts b/x-pack/plugins/event_log/server/index.ts index deeee970ce68a..b643060415b19 100644 --- a/x-pack/plugins/event_log/server/index.ts +++ b/x-pack/plugins/event_log/server/index.ts @@ -33,6 +33,7 @@ export const config: PluginConfigDescriptor = { settings?.xpack?.eventLog?.enabled === true ) { addDeprecation({ + configPath: 'xpack.eventLog.enabled', message: `"xpack.eventLog.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, correctiveActions: { manualSteps: [`Remove "xpack.eventLog.enabled" from your kibana configs.`], diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx index 5005c029a7588..1092b7ac89c07 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_requirements_page/components/fleet_server_on_prem_instructions.tsx @@ -22,6 +22,7 @@ import { EuiFieldText, EuiForm, EuiFormErrorText, + EuiButtonGroup, } from '@elastic/eui'; import type { EuiStepProps } from '@elastic/eui/src/components/steps/step'; import styled from 'styled-components'; @@ -193,19 +194,11 @@ export const FleetServerCommandStep = ({ /> - - - - } + setPlatform(e.target.value as PLATFORM_TYPE)} - aria-label={i18n.translate('xpack.fleet.fleetServerSetup.platformSelectAriaLabel', { + idSelected={platform} + onChange={(id) => setPlatform(id as PLATFORM_TYPE)} + legend={i18n.translate('xpack.fleet.fleetServerSetup.platformSelectAriaLabel', { defaultMessage: 'Platform', })} /> diff --git a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx index 67bb8921c1834..ecbcf309c5992 100644 --- a/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx +++ b/x-pack/plugins/fleet/public/components/enrollment_instructions/manual/index.tsx @@ -7,7 +7,7 @@ import React from 'react'; import styled from 'styled-components'; -import { EuiText, EuiSpacer, EuiLink, EuiCodeBlock, EuiSelect } from '@elastic/eui'; +import { EuiText, EuiSpacer, EuiLink, EuiCodeBlock, EuiButtonGroup } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; @@ -51,19 +51,11 @@ export const ManualInstructions: React.FunctionComponent = ({ /> - - - - } + setPlatform(e.target.value as PLATFORM_TYPE)} - aria-label={i18n.translate('xpack.fleet.enrollmentInstructions.platformSelectAriaLabel', { + idSelected={platform} + onChange={(id) => setPlatform(id as PLATFORM_TYPE)} + legend={i18n.translate('xpack.fleet.enrollmentInstructions.platformSelectAriaLabel', { defaultMessage: 'Platform', })} /> diff --git a/x-pack/plugins/fleet/public/hooks/use_platform.tsx b/x-pack/plugins/fleet/public/hooks/use_platform.tsx index c9ab7106696e1..b7f9ea34df304 100644 --- a/x-pack/plugins/fleet/public/hooks/use_platform.tsx +++ b/x-pack/plugins/fleet/public/hooks/use_platform.tsx @@ -6,12 +6,36 @@ */ import { useState } from 'react'; +import { i18n } from '@kbn/i18n'; export type PLATFORM_TYPE = 'linux-mac' | 'windows' | 'rpm-deb'; -export const PLATFORM_OPTIONS: Array<{ text: string; value: PLATFORM_TYPE }> = [ - { text: 'Linux / macOS', value: 'linux-mac' }, - { text: 'Windows', value: 'windows' }, - { text: 'RPM / DEB', value: 'rpm-deb' }, + +export const PLATFORM_OPTIONS: Array<{ + label: string; + id: PLATFORM_TYPE; + 'data-test-subj'?: string; +}> = [ + { + id: 'linux-mac', + label: i18n.translate('xpack.fleet.enrollmentInstructions.platformButtons.linux', { + defaultMessage: 'Linux / macOS', + }), + 'data-test-subj': 'platformTypeLinux', + }, + { + id: 'windows', + label: i18n.translate('xpack.fleet.enrollmentInstructions.platformButtons.windows', { + defaultMessage: 'Windows', + }), + 'data-test-subj': 'platformTypeWindows', + }, + { + id: 'rpm-deb', + label: i18n.translate('xpack.fleet.enrollmentInstructions.platformButtons.rpm', { + defaultMessage: 'RPM / DEB', + }), + 'data-test-subj': 'platformTypeRpm', + }, ]; export function usePlatform() { diff --git a/x-pack/plugins/fleet/server/index.ts b/x-pack/plugins/fleet/server/index.ts index 897f160810e29..cc754b87686e6 100644 --- a/x-pack/plugins/fleet/server/index.ts +++ b/x-pack/plugins/fleet/server/index.ts @@ -89,6 +89,7 @@ export const config: PluginConfigDescriptor = { delete fullConfig.xpack.fleet.agents.elasticsearch.host; fullConfig.xpack.fleet.agents.elasticsearch.hosts = [oldValue]; addDeprecation({ + configPath: 'xpack.fleet.agents.elasticsearch.host', message: `Config key [xpack.fleet.agents.elasticsearch.host] is deprecated and replaced by [xpack.fleet.agents.elasticsearch.hosts]`, correctiveActions: { manualSteps: [ diff --git a/x-pack/plugins/fleet/server/plugin.ts b/x-pack/plugins/fleet/server/plugin.ts index a706ca6a54fdc..697ea0fa30d69 100644 --- a/x-pack/plugins/fleet/server/plugin.ts +++ b/x-pack/plugins/fleet/server/plugin.ts @@ -57,7 +57,7 @@ import { registerPreconfigurationRoutes, } from './routes'; -import type { ExternalCallback } from './types'; +import type { ExternalCallback, FleetRequestHandlerContext } from './types'; import type { ESIndexPatternService, AgentService, @@ -231,7 +231,21 @@ export class FleetPlugin }); } - const router = core.http.createRouter(); + core.http.registerRouteHandlerContext( + 'fleet', + (coreContext, request) => ({ + epm: { + // Use a lazy getter to avoid constructing this client when not used by a request handler + get internalSoClient() { + return appContextService + .getSavedObjects() + .getScopedClient(request, { excludedWrappers: ['security'] }); + }, + }, + }) + ); + + const router = core.http.createRouter(); // Register usage collection registerFleetUsageCollector(core, config, deps.usageCollection); diff --git a/x-pack/plugins/fleet/server/routes/epm/handlers.ts b/x-pack/plugins/fleet/server/routes/epm/handlers.ts index 2324d1a423bfc..c98038427cafc 100644 --- a/x-pack/plugins/fleet/server/routes/epm/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/epm/handlers.ts @@ -9,7 +9,7 @@ import path from 'path'; import type { TypeOf } from '@kbn/config-schema'; import mime from 'mime-types'; -import type { RequestHandler, ResponseHeaders, KnownHeaders } from 'src/core/server'; +import type { ResponseHeaders, KnownHeaders } from 'src/core/server'; import type { GetInfoResponse, @@ -34,6 +34,7 @@ import type { DeletePackageRequestSchema, BulkUpgradePackagesFromRegistryRequestSchema, GetStatsRequestSchema, + FleetRequestHandler, UpdatePackageRequestSchema, } from '../../types'; import { @@ -57,7 +58,7 @@ import { getAsset } from '../../services/epm/archive/storage'; import { getPackageUsageStats } from '../../services/epm/packages/get'; import { updatePackage } from '../../services/epm/packages/update'; -export const getCategoriesHandler: RequestHandler< +export const getCategoriesHandler: FleetRequestHandler< undefined, TypeOf > = async (context, request, response) => { @@ -72,12 +73,12 @@ export const getCategoriesHandler: RequestHandler< } }; -export const getListHandler: RequestHandler< +export const getListHandler: FleetRequestHandler< undefined, TypeOf > = async (context, request, response) => { try { - const savedObjectsClient = context.core.savedObjects.client; + const savedObjectsClient = context.fleet.epm.internalSoClient; const res = await getPackages({ savedObjectsClient, ...request.query, @@ -93,9 +94,9 @@ export const getListHandler: RequestHandler< } }; -export const getLimitedListHandler: RequestHandler = async (context, request, response) => { +export const getLimitedListHandler: FleetRequestHandler = async (context, request, response) => { try { - const savedObjectsClient = context.core.savedObjects.client; + const savedObjectsClient = context.fleet.epm.internalSoClient; const res = await getLimitedPackages({ savedObjectsClient }); const body: GetLimitedPackagesResponse = { response: res, @@ -108,110 +109,105 @@ export const getLimitedListHandler: RequestHandler = async (context, request, re } }; -export const getFileHandler: RequestHandler> = async ( - context, - request, - response -) => { - try { - const { pkgName, pkgVersion, filePath } = request.params; - const savedObjectsClient = context.core.savedObjects.client; - const installation = await getInstallation({ savedObjectsClient, pkgName }); - const useLocalFile = pkgVersion === installation?.version; +export const getFileHandler: FleetRequestHandler> = + async (context, request, response) => { + try { + const { pkgName, pkgVersion, filePath } = request.params; + const savedObjectsClient = context.fleet.epm.internalSoClient; + const installation = await getInstallation({ savedObjectsClient, pkgName }); + const useLocalFile = pkgVersion === installation?.version; + + if (useLocalFile) { + const assetPath = `${pkgName}-${pkgVersion}/${filePath}`; + const fileBuffer = getArchiveEntry(assetPath); + // only pull local installation if we don't have it cached + const storedAsset = + !fileBuffer && (await getAsset({ savedObjectsClient, path: assetPath })); - if (useLocalFile) { - const assetPath = `${pkgName}-${pkgVersion}/${filePath}`; - const fileBuffer = getArchiveEntry(assetPath); - // only pull local installation if we don't have it cached - const storedAsset = !fileBuffer && (await getAsset({ savedObjectsClient, path: assetPath })); + // error, if neither is available + if (!fileBuffer && !storedAsset) { + return response.custom({ + body: `installed package file not found: ${filePath}`, + statusCode: 404, + }); + } + + // if storedAsset is not available, fileBuffer *must* be + // b/c we error if we don't have at least one, and storedAsset is the least likely + const { buffer, contentType } = storedAsset + ? { + contentType: storedAsset.media_type, + buffer: storedAsset.data_utf8 + ? Buffer.from(storedAsset.data_utf8, 'utf8') + : Buffer.from(storedAsset.data_base64, 'base64'), + } + : { + contentType: mime.contentType(path.extname(assetPath)), + buffer: fileBuffer, + }; + + if (!contentType) { + return response.custom({ + body: `unknown content type for file: ${filePath}`, + statusCode: 400, + }); + } - // error, if neither is available - if (!fileBuffer && !storedAsset) { return response.custom({ - body: `installed package file not found: ${filePath}`, - statusCode: 404, + body: buffer, + statusCode: 200, + headers: { + 'cache-control': 'max-age=10, public', + 'content-type': contentType, + }, }); - } - - // if storedAsset is not available, fileBuffer *must* be - // b/c we error if we don't have at least one, and storedAsset is the least likely - const { buffer, contentType } = storedAsset - ? { - contentType: storedAsset.media_type, - buffer: storedAsset.data_utf8 - ? Buffer.from(storedAsset.data_utf8, 'utf8') - : Buffer.from(storedAsset.data_base64, 'base64'), + } else { + const registryResponse = await getFile(pkgName, pkgVersion, filePath); + const headersToProxy: KnownHeaders[] = ['content-type', 'cache-control']; + const proxiedHeaders = headersToProxy.reduce((headers, knownHeader) => { + const value = registryResponse.headers.get(knownHeader); + if (value !== null) { + headers[knownHeader] = value; } - : { - contentType: mime.contentType(path.extname(assetPath)), - buffer: fileBuffer, - }; + return headers; + }, {} as ResponseHeaders); - if (!contentType) { return response.custom({ - body: `unknown content type for file: ${filePath}`, - statusCode: 400, + body: registryResponse.body, + statusCode: registryResponse.status, + headers: proxiedHeaders, }); } - - return response.custom({ - body: buffer, - statusCode: 200, - headers: { - 'cache-control': 'max-age=10, public', - 'content-type': contentType, - }, - }); - } else { - const registryResponse = await getFile(pkgName, pkgVersion, filePath); - const headersToProxy: KnownHeaders[] = ['content-type', 'cache-control']; - const proxiedHeaders = headersToProxy.reduce((headers, knownHeader) => { - const value = registryResponse.headers.get(knownHeader); - if (value !== null) { - headers[knownHeader] = value; - } - return headers; - }, {} as ResponseHeaders); - - return response.custom({ - body: registryResponse.body, - statusCode: registryResponse.status, - headers: proxiedHeaders, - }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); } - } catch (error) { - return defaultIngestErrorHandler({ error, response }); - } -}; + }; -export const getInfoHandler: RequestHandler> = async ( - context, - request, - response -) => { - try { - const { pkgkey } = request.params; - const savedObjectsClient = context.core.savedObjects.client; - // TODO: change epm API to /packageName/version so we don't need to do this - const { pkgName, pkgVersion } = splitPkgKey(pkgkey); - const res = await getPackageInfo({ savedObjectsClient, pkgName, pkgVersion }); - const body: GetInfoResponse = { - response: res, - }; - return response.ok({ body }); - } catch (error) { - return defaultIngestErrorHandler({ error, response }); - } -}; +export const getInfoHandler: FleetRequestHandler> = + async (context, request, response) => { + try { + const { pkgkey } = request.params; + const savedObjectsClient = context.fleet.epm.internalSoClient; + // TODO: change epm API to /packageName/version so we don't need to do this + const { pkgName, pkgVersion } = splitPkgKey(pkgkey); + const res = await getPackageInfo({ savedObjectsClient, pkgName, pkgVersion }); + const body: GetInfoResponse = { + response: res, + }; + return response.ok({ body }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } + }; -export const updatePackageHandler: RequestHandler< +export const updatePackageHandler: FleetRequestHandler< TypeOf, unknown, TypeOf > = async (context, request, response) => { try { const { pkgkey } = request.params; - const savedObjectsClient = context.core.savedObjects.client; + const savedObjectsClient = context.fleet.epm.internalSoClient; const { pkgName } = splitPkgKey(pkgkey); @@ -226,30 +222,27 @@ export const updatePackageHandler: RequestHandler< } }; -export const getStatsHandler: RequestHandler> = async ( - context, - request, - response -) => { - try { - const { pkgName } = request.params; - const savedObjectsClient = context.core.savedObjects.client; - const body: GetStatsResponse = { - response: await getPackageUsageStats({ savedObjectsClient, pkgName }), - }; - return response.ok({ body }); - } catch (error) { - return defaultIngestErrorHandler({ error, response }); - } -}; +export const getStatsHandler: FleetRequestHandler> = + async (context, request, response) => { + try { + const { pkgName } = request.params; + const savedObjectsClient = context.fleet.epm.internalSoClient; + const body: GetStatsResponse = { + response: await getPackageUsageStats({ savedObjectsClient, pkgName }), + }; + return response.ok({ body }); + } catch (error) { + return defaultIngestErrorHandler({ error, response }); + } + }; -export const installPackageFromRegistryHandler: RequestHandler< +export const installPackageFromRegistryHandler: FleetRequestHandler< TypeOf, undefined, TypeOf > = async (context, request, response) => { - const savedObjectsClient = context.core.savedObjects.client; - const esClient = context.core.elasticsearch.client.asCurrentUser; + const savedObjectsClient = context.fleet.epm.internalSoClient; + const esClient = context.core.elasticsearch.client.asInternalUser; const { pkgkey } = request.params; const res = await installPackage({ @@ -284,13 +277,13 @@ const bulkInstallServiceResponseToHttpEntry = ( } }; -export const bulkInstallPackagesFromRegistryHandler: RequestHandler< +export const bulkInstallPackagesFromRegistryHandler: FleetRequestHandler< undefined, undefined, TypeOf > = async (context, request, response) => { - const savedObjectsClient = context.core.savedObjects.client; - const esClient = context.core.elasticsearch.client.asCurrentUser; + const savedObjectsClient = context.fleet.epm.internalSoClient; + const esClient = context.core.elasticsearch.client.asInternalUser; const bulkInstalledResponses = await bulkInstallPackages({ savedObjectsClient, esClient, @@ -303,7 +296,7 @@ export const bulkInstallPackagesFromRegistryHandler: RequestHandler< return response.ok({ body }); }; -export const installPackageByUploadHandler: RequestHandler< +export const installPackageByUploadHandler: FleetRequestHandler< undefined, undefined, TypeOf @@ -314,8 +307,8 @@ export const installPackageByUploadHandler: RequestHandler< body: { message: 'Requires Enterprise license' }, }); } - const savedObjectsClient = context.core.savedObjects.client; - const esClient = context.core.elasticsearch.client.asCurrentUser; + const savedObjectsClient = context.fleet.epm.internalSoClient; + const esClient = context.core.elasticsearch.client.asInternalUser; const contentType = request.headers['content-type'] as string; // from types it could also be string[] or undefined but this is checked later const archiveBuffer = Buffer.from(request.body); @@ -336,15 +329,15 @@ export const installPackageByUploadHandler: RequestHandler< } }; -export const deletePackageHandler: RequestHandler< +export const deletePackageHandler: FleetRequestHandler< TypeOf, undefined, TypeOf > = async (context, request, response) => { try { const { pkgkey } = request.params; - const savedObjectsClient = context.core.savedObjects.client; - const esClient = context.core.elasticsearch.client.asCurrentUser; + const savedObjectsClient = context.fleet.epm.internalSoClient; + const esClient = context.core.elasticsearch.client.asInternalUser; const res = await removeInstallation({ savedObjectsClient, pkgkey, diff --git a/x-pack/plugins/fleet/server/routes/epm/index.ts b/x-pack/plugins/fleet/server/routes/epm/index.ts index 0f49b3cfa772d..360f2ec1d446e 100644 --- a/x-pack/plugins/fleet/server/routes/epm/index.ts +++ b/x-pack/plugins/fleet/server/routes/epm/index.ts @@ -8,6 +8,7 @@ import type { IRouter } from 'src/core/server'; import { PLUGIN_ID, EPM_API_ROUTES } from '../../constants'; +import type { FleetRequestHandlerContext } from '../../types'; import { GetCategoriesRequestSchema, GetPackagesRequestSchema, @@ -38,7 +39,7 @@ import { const MAX_FILE_SIZE_BYTES = 104857600; // 100MB -export const registerRoutes = (router: IRouter) => { +export const registerRoutes = (router: IRouter) => { router.get( { path: EPM_API_ROUTES.CATEGORIES_PATTERN, diff --git a/x-pack/plugins/fleet/server/routes/security.ts b/x-pack/plugins/fleet/server/routes/security.ts index 8efea34ed8164..33a510c27f04e 100644 --- a/x-pack/plugins/fleet/server/routes/security.ts +++ b/x-pack/plugins/fleet/server/routes/security.ts @@ -5,13 +5,13 @@ * 2.0. */ -import type { IRouter, RequestHandler } from 'src/core/server'; +import type { IRouter, RequestHandler, RequestHandlerContext } from 'src/core/server'; import { appContextService } from '../services'; -export function enforceSuperUser( - handler: RequestHandler -): RequestHandler { +export function enforceSuperUser( + handler: RequestHandler +): RequestHandler { return function enforceSuperHandler(context, req, res) { if (!appContextService.hasSecurity() || !appContextService.getSecurityLicense().isEnabled()) { return res.forbidden({ @@ -44,7 +44,9 @@ export function enforceSuperUser( }; } -export function makeRouterEnforcingSuperuser(router: IRouter): IRouter { +export function makeRouterEnforcingSuperuser( + router: IRouter +): IRouter { return { get: (options, handler) => router.get(options, enforceSuperUser(handler)), delete: (options, handler) => router.delete(options, enforceSuperUser(handler)), diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts index c196054faf08c..b39c6e7686110 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.test.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { httpServerMock } from 'src/core/server/mocks'; +import { httpServerMock, savedObjectsClientMock } from 'src/core/server/mocks'; import type { PostFleetSetupResponse } from '../../../common'; import { RegistryError } from '../../errors'; import { createAppContextStartContractMock, xpackMocks } from '../../mocks'; import { appContextService } from '../../services/app_context'; import { setupFleet } from '../../services/setup'; +import type { FleetRequestHandlerContext } from '../../types'; import { fleetSetupHandler } from './handlers'; @@ -24,12 +25,19 @@ jest.mock('../../services/setup', () => { const mockSetupFleet = setupFleet as jest.MockedFunction; describe('FleetSetupHandler', () => { - let context: ReturnType; + let context: FleetRequestHandlerContext; let response: ReturnType; let request: ReturnType; beforeEach(async () => { - context = xpackMocks.createRequestHandlerContext(); + context = { + ...xpackMocks.createRequestHandlerContext(), + fleet: { + epm: { + internalSoClient: savedObjectsClientMock.create(), + }, + }, + }; response = httpServerMock.createResponseFactory(); request = httpServerMock.createKibanaRequest({ method: 'post', diff --git a/x-pack/plugins/fleet/server/routes/setup/handlers.ts b/x-pack/plugins/fleet/server/routes/setup/handlers.ts index d24db96667d52..c5b2ef0ade26f 100644 --- a/x-pack/plugins/fleet/server/routes/setup/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/setup/handlers.ts @@ -12,6 +12,7 @@ import type { GetFleetStatusResponse, PostFleetSetupResponse } from '../../../co import { setupFleet } from '../../services/setup'; import { hasFleetServers } from '../../services/fleet_server'; import { defaultIngestErrorHandler } from '../../errors'; +import type { FleetRequestHandler } from '../../types'; export const getFleetStatusHandler: RequestHandler = async (context, request, response) => { try { @@ -42,10 +43,10 @@ export const getFleetStatusHandler: RequestHandler = async (context, request, re } }; -export const fleetSetupHandler: RequestHandler = async (context, request, response) => { +export const fleetSetupHandler: FleetRequestHandler = async (context, request, response) => { try { - const soClient = context.core.savedObjects.client; - const esClient = context.core.elasticsearch.client.asCurrentUser; + const soClient = context.fleet.epm.internalSoClient; + const esClient = context.core.elasticsearch.client.asInternalUser; const setupStatus = await setupFleet(soClient, esClient); const body: PostFleetSetupResponse = { ...setupStatus, diff --git a/x-pack/plugins/fleet/server/routes/setup/index.ts b/x-pack/plugins/fleet/server/routes/setup/index.ts index d64c9f24f2610..591b9c832172d 100644 --- a/x-pack/plugins/fleet/server/routes/setup/index.ts +++ b/x-pack/plugins/fleet/server/routes/setup/index.ts @@ -10,9 +10,11 @@ import type { IRouter } from 'src/core/server'; import { PLUGIN_ID, AGENTS_SETUP_API_ROUTES, SETUP_API_ROUTE } from '../../constants'; import type { FleetConfigType } from '../../../common'; +import type { FleetRequestHandlerContext } from '../../types/request_context'; + import { getFleetStatusHandler, fleetSetupHandler } from './handlers'; -export const registerFleetSetupRoute = (router: IRouter) => { +export const registerFleetSetupRoute = (router: IRouter) => { router.post( { path: SETUP_API_ROUTE, @@ -26,7 +28,7 @@ export const registerFleetSetupRoute = (router: IRouter) => { }; // That route is used by agent to setup Fleet -export const registerCreateFleetSetupRoute = (router: IRouter) => { +export const registerCreateFleetSetupRoute = (router: IRouter) => { router.post( { path: AGENTS_SETUP_API_ROUTES.CREATE_PATTERN, @@ -37,7 +39,7 @@ export const registerCreateFleetSetupRoute = (router: IRouter) => { ); }; -export const registerGetFleetStatusRoute = (router: IRouter) => { +export const registerGetFleetStatusRoute = (router: IRouter) => { router.get( { path: AGENTS_SETUP_API_ROUTES.INFO_PATTERN, @@ -48,7 +50,10 @@ export const registerGetFleetStatusRoute = (router: IRouter) => { ); }; -export const registerRoutes = (router: IRouter, config: FleetConfigType) => { +export const registerRoutes = ( + router: IRouter, + config: FleetConfigType +) => { // Ingest manager setup registerFleetSetupRoute(router); diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/index.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/index.ts index e794507799983..221731b80df0e 100644 --- a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/index.ts +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/index.ts @@ -10,3 +10,4 @@ export { migratePackagePolicyToV7120 } from './to_v7_12_0'; export { migrateEndpointPackagePolicyToV7130 } from './to_v7_13_0'; export { migrateEndpointPackagePolicyToV7140 } from './to_v7_14_0'; export { migratePackagePolicyToV7150 } from './to_v7_15_0'; +export { migratePackagePolicyToV7160 } from './to_v7_16_0'; diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_16_0.test.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_16_0.test.ts new file mode 100644 index 0000000000000..5311d9c8cd7ee --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_16_0.test.ts @@ -0,0 +1,278 @@ +/* + * 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 type { SavedObjectMigrationContext, SavedObjectUnsanitizedDoc } from 'kibana/server'; + +import type { PackagePolicy } from '../../../../common'; + +import { migratePackagePolicyToV7160 as migration } from './to_v7_16_0'; + +describe('7.16.0 Endpoint Package Policy migration', () => { + const policyDoc = ({ + windowsMemory = {}, + windowsBehavior = {}, + windowsPopup = {}, + windowsMalware = {}, + windowsRansomware = {}, + macBehavior = {}, + macMemory = {}, + macMalware = {}, + macPopup = {}, + linuxBehavior = {}, + linuxMemory = {}, + linuxMalware = {}, + linuxPopup = {}, + }) => { + return { + id: 'mock-saved-object-id', + attributes: { + name: 'Some Policy Name', + package: { + name: 'endpoint', + title: '', + version: '', + }, + id: 'endpoint', + policy_id: '', + enabled: true, + namespace: '', + output_id: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'endpoint', + enabled: true, + streams: [], + config: { + policy: { + value: { + windows: { + ...windowsMalware, + ...windowsRansomware, + ...windowsMemory, + ...windowsBehavior, + ...windowsPopup, + }, + mac: { + ...macMalware, + ...macBehavior, + ...macMemory, + ...macPopup, + }, + linux: { + ...linuxMalware, + ...linuxBehavior, + ...linuxMemory, + ...linuxPopup, + }, + }, + }, + }, + }, + ], + }, + type: ' nested', + }; + }; + + it('adds mac and linux memory protection alongside behavior, malware, and ramsomware', () => { + const initialDoc = policyDoc({ + windowsMalware: { malware: { mode: 'off' } }, + windowsRansomware: { ransomware: { mode: 'off', supported: true } }, + windowsBehavior: { behavior_protection: { mode: 'off', supported: true } }, + windowsMemory: { memory_protection: { mode: 'off', supported: true } }, + windowsPopup: { + popup: { + malware: { + message: '', + enabled: true, + }, + ransomware: { + message: '', + enabled: true, + }, + behavior_protection: { + message: '', + enabled: true, + }, + memory_protection: { + message: '', + enabled: true, + }, + }, + }, + macMalware: { malware: { mode: 'off' } }, + macBehavior: { behavior_protection: { mode: 'off', supported: true } }, + macPopup: { + popup: { + malware: { + message: '', + enabled: true, + }, + behavior_protection: { + message: '', + enabled: true, + }, + }, + }, + linuxMalware: { malware: { mode: 'off' } }, + linuxBehavior: { behavior_protection: { mode: 'off', supported: true } }, + linuxPopup: { + popup: { + malware: { + message: '', + enabled: true, + }, + behavior_protection: { + message: '', + enabled: true, + }, + }, + }, + }); + + const migratedDoc = policyDoc({ + windowsMalware: { malware: { mode: 'off' } }, + windowsRansomware: { ransomware: { mode: 'off', supported: true } }, + // new memory protection + windowsMemory: { memory_protection: { mode: 'off', supported: true } }, + windowsBehavior: { behavior_protection: { mode: 'off', supported: true } }, + windowsPopup: { + popup: { + malware: { + message: '', + enabled: true, + }, + ransomware: { + message: '', + enabled: true, + }, + memory_protection: { + message: '', + enabled: true, + }, + behavior_protection: { + message: '', + enabled: true, + }, + }, + }, + macMalware: { malware: { mode: 'off' } }, + macBehavior: { behavior_protection: { mode: 'off', supported: true } }, + macMemory: { memory_protection: { mode: 'off', supported: true } }, + macPopup: { + popup: { + malware: { + message: '', + enabled: true, + }, + behavior_protection: { + message: '', + enabled: true, + }, + // new memory popup setup + memory_protection: { + message: '', + enabled: false, + }, + }, + }, + linuxMalware: { malware: { mode: 'off' } }, + linuxBehavior: { behavior_protection: { mode: 'off', supported: true } }, + linuxMemory: { memory_protection: { mode: 'off', supported: true } }, + linuxPopup: { + popup: { + malware: { + message: '', + enabled: true, + }, + behavior_protection: { + message: '', + enabled: true, + }, + // new memory popup setup + memory_protection: { + message: '', + enabled: false, + }, + }, + }, + }); + + expect(migration(initialDoc, {} as SavedObjectMigrationContext)).toEqual(migratedDoc); + }); + + it('does not modify non-endpoint package policies', () => { + const doc: SavedObjectUnsanitizedDoc = { + id: 'mock-saved-object-id', + attributes: { + name: 'Some Policy Name', + package: { + name: 'notEndpoint', + title: '', + version: '', + }, + id: 'notEndpoint', + policy_id: '', + enabled: true, + namespace: '', + output_id: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'notEndpoint', + enabled: true, + streams: [], + config: {}, + }, + ], + }, + type: ' nested', + }; + + expect( + migration(doc, {} as SavedObjectMigrationContext) as SavedObjectUnsanitizedDoc + ).toEqual({ + attributes: { + name: 'Some Policy Name', + package: { + name: 'notEndpoint', + title: '', + version: '', + }, + id: 'notEndpoint', + policy_id: '', + enabled: true, + namespace: '', + output_id: '', + revision: 0, + updated_at: '', + updated_by: '', + created_at: '', + created_by: '', + inputs: [ + { + type: 'notEndpoint', + enabled: true, + streams: [], + config: {}, + }, + ], + }, + type: ' nested', + id: 'mock-saved-object-id', + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_16_0.ts b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_16_0.ts new file mode 100644 index 0000000000000..ca565ca086756 --- /dev/null +++ b/x-pack/plugins/fleet/server/saved_objects/migrations/security_solution/to_v7_16_0.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { SavedObjectMigrationFn, SavedObjectUnsanitizedDoc } from 'kibana/server'; +import { cloneDeep } from 'lodash'; + +import type { PackagePolicy } from '../../../../common'; + +export const migratePackagePolicyToV7160: SavedObjectMigrationFn = ( + packagePolicyDoc +) => { + if (packagePolicyDoc.attributes.package?.name !== 'endpoint') { + return packagePolicyDoc; + } + + const updatedPackagePolicyDoc: SavedObjectUnsanitizedDoc = + cloneDeep(packagePolicyDoc); + + const input = updatedPackagePolicyDoc.attributes.inputs[0]; + const memory = { + mode: 'off', + // This value is based on license. + // For the migration, we add 'true', our license watcher will correct it, if needed, when the app starts. + supported: true, + }; + const memoryPopup = { + message: '', + enabled: false, + }; + if (input && input.config) { + const policy = input.config.policy.value; + + policy.mac.memory_protection = memory; + policy.mac.popup.memory_protection = memoryPopup; + policy.linux.memory_protection = memory; + policy.linux.popup.memory_protection = memoryPopup; + } + + return updatedPackagePolicyDoc; +}; diff --git a/x-pack/plugins/fleet/server/services/epm/registry/requests.ts b/x-pack/plugins/fleet/server/services/epm/registry/requests.ts index 40943aa0cffff..ed6df5f6459ec 100644 --- a/x-pack/plugins/fleet/server/services/epm/registry/requests.ts +++ b/x-pack/plugins/fleet/server/services/epm/registry/requests.ts @@ -97,7 +97,6 @@ export function getFetchOptions(targetUrl: string): RequestInit | undefined { logger.debug(`Using ${proxyUrl} as proxy for ${targetUrl}`); return { - // @ts-expect-error The types exposed by 'HttpsProxyAgent' isn't up to date with 'Agent' agent: getProxyAgent({ proxyUrl, targetUrl }), }; } diff --git a/x-pack/plugins/fleet/server/services/managed_package_policies.test.ts b/x-pack/plugins/fleet/server/services/managed_package_policies.test.ts index 52c1c71446d64..b27248a3cb933 100644 --- a/x-pack/plugins/fleet/server/services/managed_package_policies.test.ts +++ b/x-pack/plugins/fleet/server/services/managed_package_policies.test.ts @@ -7,9 +7,12 @@ import { elasticsearchServiceMock, savedObjectsClientMock } from 'src/core/server/mocks'; -import { upgradeManagedPackagePolicies } from './managed_package_policies'; +import type { Installation, PackageInfo } from '../../common'; +import { AUTO_UPDATE_PACKAGES } from '../../common'; + +import { shouldUpgradePolicies, upgradeManagedPackagePolicies } from './managed_package_policies'; import { packagePolicyService } from './package_policy'; -import { getPackageInfo } from './epm/packages'; +import { getPackageInfo, getInstallation } from './epm/packages'; jest.mock('./package_policy'); jest.mock('./epm/packages'); @@ -24,11 +27,12 @@ jest.mock('./app_context', () => { }; }); -describe('managed package policies', () => { +describe('upgradeManagedPackagePolicies', () => { afterEach(() => { (packagePolicyService.get as jest.Mock).mockReset(); (packagePolicyService.getUpgradeDryRunDiff as jest.Mock).mockReset(); (getPackageInfo as jest.Mock).mockReset(); + (getInstallation as jest.Mock).mockReset(); (packagePolicyService.upgrade as jest.Mock).mockReset(); }); @@ -50,7 +54,7 @@ describe('managed package policies', () => { package: { name: 'non-managed-package', title: 'Non-Managed Package', - version: '0.0.1', + version: '1.0.0', }, }; } @@ -74,6 +78,11 @@ describe('managed package policies', () => { }) ); + (getInstallation as jest.Mock).mockResolvedValueOnce({ + id: 'test-installation', + version: '0.0.1', + }); + await upgradeManagedPackagePolicies(soClient, esClient, ['non-managed-package-id']); expect(packagePolicyService.upgrade).not.toBeCalled(); @@ -121,6 +130,11 @@ describe('managed package policies', () => { }) ); + (getInstallation as jest.Mock).mockResolvedValueOnce({ + id: 'test-installation', + version: '1.0.0', + }); + await upgradeManagedPackagePolicies(soClient, esClient, ['managed-package-id']); expect(packagePolicyService.upgrade).toBeCalledWith(soClient, esClient, ['managed-package-id']); @@ -172,6 +186,11 @@ describe('managed package policies', () => { }) ); + (getInstallation as jest.Mock).mockResolvedValueOnce({ + id: 'test-installation', + version: '1.0.0', + }); + const result = await upgradeManagedPackagePolicies(soClient, esClient, [ 'conflicting-package-policy', ]); @@ -206,3 +225,133 @@ describe('managed package policies', () => { }); }); }); + +describe('shouldUpgradePolicies', () => { + describe('package is marked as AUTO_UPDATE', () => { + describe('keep_policies_up_to_date is true', () => { + it('returns false', () => { + const packageInfo = { + version: '1.0.0', + keepPoliciesUpToDate: true, + name: AUTO_UPDATE_PACKAGES[0].name, + }; + + const installedPackage = { + version: '1.0.0', + }; + + const result = shouldUpgradePolicies( + packageInfo as PackageInfo, + installedPackage as Installation + ); + + expect(result).toBe(false); + }); + }); + + describe('keep_policies_up_to_date is false', () => { + it('returns false', () => { + const packageInfo = { + version: '1.0.0', + keepPoliciesUpToDate: false, + name: AUTO_UPDATE_PACKAGES[0].name, + }; + + const installedPackage = { + version: '1.0.0', + }; + + const result = shouldUpgradePolicies( + packageInfo as PackageInfo, + installedPackage as Installation + ); + + expect(result).toBe(false); + }); + }); + }); + + describe('package policy is up-to-date', () => { + describe('keep_policies_up_to_date is true', () => { + it('returns false', () => { + const packageInfo = { + version: '1.0.0', + keepPoliciesUpToDate: true, + }; + + const installedPackage = { + version: '1.0.0', + }; + + const result = shouldUpgradePolicies( + packageInfo as PackageInfo, + installedPackage as Installation + ); + + expect(result).toBe(false); + }); + }); + + describe('keep_policies_up_to_date is false', () => { + it('returns false', () => { + const packageInfo = { + version: '1.0.0', + keepPoliciesUpToDate: false, + }; + + const installedPackage = { + version: '1.0.0', + }; + + const result = shouldUpgradePolicies( + packageInfo as PackageInfo, + installedPackage as Installation + ); + + expect(result).toBe(false); + }); + }); + }); + + describe('package policy is out-of-date', () => { + describe('keep_policies_up_to_date is true', () => { + it('returns true', () => { + const packageInfo = { + version: '1.0.0', + keepPoliciesUpToDate: true, + }; + + const installedPackage = { + version: '1.1.0', + }; + + const result = shouldUpgradePolicies( + packageInfo as PackageInfo, + installedPackage as Installation + ); + + expect(result).toBe(true); + }); + }); + + describe('keep_policies_up_to_date is false', () => { + it('returns false', () => { + const packageInfo = { + version: '1.0.0', + keepPoliciesUpToDate: false, + }; + + const installedPackage = { + version: '1.1.0', + }; + + const result = shouldUpgradePolicies( + packageInfo as PackageInfo, + installedPackage as Installation + ); + + expect(result).toBe(false); + }); + }); + }); +}); diff --git a/x-pack/plugins/fleet/server/services/managed_package_policies.ts b/x-pack/plugins/fleet/server/services/managed_package_policies.ts index 25e2482892712..306725ae01953 100644 --- a/x-pack/plugins/fleet/server/services/managed_package_policies.ts +++ b/x-pack/plugins/fleet/server/services/managed_package_policies.ts @@ -6,9 +6,13 @@ */ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/server'; +import semverGte from 'semver/functions/gte'; -import type { UpgradePackagePolicyDryRunResponseItem } from '../../common'; -import { AUTO_UPDATE_PACKAGES } from '../../common'; +import type { + Installation, + PackageInfo, + UpgradePackagePolicyDryRunResponseItem, +} from '../../common'; import { appContextService } from './app_context'; import { getInstallation, getPackageInfo } from './epm/packages'; @@ -16,7 +20,7 @@ import { packagePolicyService } from './package_policy'; export interface UpgradeManagedPackagePoliciesResult { packagePolicyId: string; - diff: UpgradePackagePolicyDryRunResponseItem['diff']; + diff?: UpgradePackagePolicyDryRunResponseItem['diff']; errors: any; } @@ -49,15 +53,16 @@ export const upgradeManagedPackagePolicies = async ( pkgName: packagePolicy.package.name, }); - const isPolicyVersionAlignedWithInstalledVersion = - packageInfo.version === installedPackage?.version; + if (!installedPackage) { + results.push({ + packagePolicyId, + errors: [`${packagePolicy.package.name} is not installed`], + }); - const shouldUpgradePolicies = - !isPolicyVersionAlignedWithInstalledVersion && - (AUTO_UPDATE_PACKAGES.some((pkg) => pkg.name === packageInfo.name) || - packageInfo.keepPoliciesUpToDate); + continue; + } - if (shouldUpgradePolicies) { + if (shouldUpgradePolicies(packageInfo, installedPackage)) { // Since upgrades don't report diffs/errors, we need to perform a dry run first in order // to notify the user of any granular policy upgrade errors that occur during Fleet's // preconfiguration check @@ -91,3 +96,15 @@ export const upgradeManagedPackagePolicies = async ( return results; }; + +export function shouldUpgradePolicies( + packageInfo: PackageInfo, + installedPackage: Installation +): boolean { + const isPolicyVersionGteInstalledVersion = semverGte( + packageInfo.version, + installedPackage.version + ); + + return !isPolicyVersionGteInstalledVersion && !!packageInfo.keepPoliciesUpToDate; +} diff --git a/x-pack/plugins/fleet/server/types/index.tsx b/x-pack/plugins/fleet/server/types/index.tsx index 63e6c277ed710..5bdd95ef0b874 100644 --- a/x-pack/plugins/fleet/server/types/index.tsx +++ b/x-pack/plugins/fleet/server/types/index.tsx @@ -96,3 +96,4 @@ export interface BulkActionResult { export * from './models'; export * from './rest_spec'; export * from './extensions'; +export { FleetRequestHandler, FleetRequestHandlerContext } from './request_context'; diff --git a/x-pack/plugins/fleet/server/types/request_context.ts b/x-pack/plugins/fleet/server/types/request_context.ts new file mode 100644 index 0000000000000..a3b414119b685 --- /dev/null +++ b/x-pack/plugins/fleet/server/types/request_context.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { + KibanaResponseFactory, + RequestHandler, + RequestHandlerContext, + RouteMethod, + SavedObjectsClientContract, +} from '../../../../../src/core/server'; + +/** @internal */ +export interface FleetRequestHandlerContext extends RequestHandlerContext { + fleet: { + epm: { + /** + * Saved Objects client configured to use kibana_system privileges instead of end-user privileges. Should only be + * used by routes that have additional privilege checks for authorization (such as requiring superuser). + */ + readonly internalSoClient: SavedObjectsClientContract; + }; + }; +} + +/** + * Convenience type for request handlers in Fleet that includes the FleetRequestHandlerContext type + * @internal + */ +export type FleetRequestHandler< + P = unknown, + Q = unknown, + B = unknown, + Method extends RouteMethod = any, + ResponseFactory extends KibanaResponseFactory = KibanaResponseFactory +> = RequestHandler; diff --git a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts index 270d5b2ec1079..94e2f1b3163ed 100644 --- a/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts +++ b/x-pack/plugins/index_management/__jest__/client_integration/home/indices_tab.test.ts @@ -22,7 +22,8 @@ import { stubWebWorker } from '@kbn/test/jest'; import { createMemoryHistory } from 'history'; stubWebWorker(); -describe('', () => { +// unhandled promise rejection https://github.com/elastic/kibana/issues/112699 +describe.skip('', () => { const { server, httpRequestsMockHelpers } = setupEnvironment(); let testBed: IndicesTestBed; diff --git a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx index 0e5ef1ca78033..2b0aaf5175b88 100644 --- a/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx +++ b/x-pack/plugins/infra/public/containers/with_kuery_autocompletion.tsx @@ -83,7 +83,6 @@ class WithKueryAutocompletionComponent extends React.Component< query: expression, selectionStart: cursorPosition, selectionEnd: cursorPosition, - // @ts-expect-error (until data service updates to new types) indexPatterns: [indexPattern], boolFilter: [], })) || []; diff --git a/x-pack/plugins/lens/common/constants.ts b/x-pack/plugins/lens/common/constants.ts index bba3ac7e8a9ca..edf7654deb7b7 100644 --- a/x-pack/plugins/lens/common/constants.ts +++ b/x-pack/plugins/lens/common/constants.ts @@ -17,7 +17,10 @@ export const NOT_INTERNATIONALIZED_PRODUCT_NAME = 'Lens Visualizations'; export const BASE_API_URL = '/api/lens'; export const LENS_EDIT_BY_VALUE = 'edit_by_value'; -export const layerTypes: Record = { DATA: 'data', THRESHOLD: 'threshold' }; +export const layerTypes: Record = { + DATA: 'data', + REFERENCELINE: 'referenceLine', +}; export function getBasePath() { return `#/`; diff --git a/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts b/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts index 9ff1b5a4dc3f7..0b9667353706d 100644 --- a/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts +++ b/x-pack/plugins/lens/common/expressions/xy_chart/axis_config.ts @@ -173,24 +173,24 @@ export const yAxisConfig: ExpressionFunctionDefinition< lineStyle: { types: ['string'], options: ['solid', 'dotted', 'dashed'], - help: 'The style of the threshold line', + help: 'The style of the reference line', }, lineWidth: { types: ['number'], - help: 'The width of the threshold line', + help: 'The width of the reference line', }, icon: { types: ['string'], - help: 'An optional icon used for threshold lines', + help: 'An optional icon used for reference lines', }, iconPosition: { types: ['string'], options: ['auto', 'above', 'below', 'left', 'right'], - help: 'The placement of the icon for the threshold line', + help: 'The placement of the icon for the reference line', }, textVisibility: { types: ['boolean'], - help: 'Visibility of the label on the threshold line', + help: 'Visibility of the label on the reference line', }, fill: { types: ['string'], diff --git a/x-pack/plugins/lens/common/types.ts b/x-pack/plugins/lens/common/types.ts index 659d3c0eced26..38e198c01e730 100644 --- a/x-pack/plugins/lens/common/types.ts +++ b/x-pack/plugins/lens/common/types.ts @@ -61,4 +61,4 @@ export interface CustomPaletteParams { export type RequiredPaletteParamTypes = Required; -export type LayerType = 'data' | 'threshold'; +export type LayerType = 'data' | 'referenceLine'; diff --git a/x-pack/plugins/lens/public/assets/chart_bar_threshold.tsx b/x-pack/plugins/lens/public/assets/chart_bar_reference_line.tsx similarity index 97% rename from x-pack/plugins/lens/public/assets/chart_bar_threshold.tsx rename to x-pack/plugins/lens/public/assets/chart_bar_reference_line.tsx index 88e0a46b5538c..447641540a284 100644 --- a/x-pack/plugins/lens/public/assets/chart_bar_threshold.tsx +++ b/x-pack/plugins/lens/public/assets/chart_bar_reference_line.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiIconProps } from '@elastic/eui'; -export const LensIconChartBarThreshold = ({ +export const LensIconChartBarReferenceLine = ({ title, titleId, ...props diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx index 61d37d4cc9fed..a8436edb63f63 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/config_panel.test.tsx @@ -280,7 +280,7 @@ describe('ConfigPanel', () => { instance.update(); act(() => { instance - .find(`[data-test-subj="lnsLayerAddButton-${layerTypes.THRESHOLD}"]`) + .find(`[data-test-subj="lnsLayerAddButton-${layerTypes.REFERENCELINE}"]`) .first() .simulate('click'); }); @@ -301,8 +301,8 @@ describe('ConfigPanel', () => { props.activeVisualization.getSupportedLayers = jest.fn(() => [ { type: layerTypes.DATA, label: 'Data Layer' }, { - type: layerTypes.THRESHOLD, - label: 'Threshold layer', + type: layerTypes.REFERENCELINE, + label: 'Reference layer', }, ]); datasourceMap.testDatasource.initializeDimension = jest.fn(); @@ -331,8 +331,8 @@ describe('ConfigPanel', () => { ], }, { - type: layerTypes.THRESHOLD, - label: 'Threshold layer', + type: layerTypes.REFERENCELINE, + label: 'Reference layer', }, ]); datasourceMap.testDatasource.initializeDimension = jest.fn(); @@ -349,8 +349,8 @@ describe('ConfigPanel', () => { props.activeVisualization.getSupportedLayers = jest.fn(() => [ { type: layerTypes.DATA, label: 'Data Layer' }, { - type: layerTypes.THRESHOLD, - label: 'Threshold layer', + type: layerTypes.REFERENCELINE, + label: 'Reference layer', initialDimensions: [ { groupId: 'testGroup', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx index 8286ab492f14d..93718c88b251c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/dimension_editor.tsx @@ -26,6 +26,7 @@ import { insertOrReplaceColumn, replaceColumn, updateColumnParam, + updateDefaultLabels, resetIncomplete, FieldBasedIndexPatternColumn, canTransition, @@ -151,13 +152,27 @@ export function DimensionEditor(props: DimensionEditorProps) { const addStaticValueColumn = (prevLayer = props.state.layers[props.layerId]) => { if (selectedColumn?.operationType !== staticValueOperationName) { trackUiEvent(`indexpattern_dimension_operation_static_value`); - return insertOrReplaceColumn({ + const layer = insertOrReplaceColumn({ layer: prevLayer, indexPattern: currentIndexPattern, columnId, op: staticValueOperationName, visualizationGroups: dimensionGroups, }); + const value = props.activeData?.[layerId]?.rows[0]?.[columnId]; + // replace the default value with the one from the active data + if (value != null) { + return updateDefaultLabels( + updateColumnParam({ + layer, + columnId, + paramName: 'value', + value: props.activeData?.[layerId]?.rows[0]?.[columnId], + }), + currentIndexPattern + ); + } + return layer; } return prevLayer; }; @@ -173,7 +188,18 @@ export function DimensionEditor(props: DimensionEditorProps) { if (temporaryStaticValue) { setTemporaryState('none'); } - return setStateWrapper(setter, { forceRender: true }); + if (typeof setter === 'function') { + return setState( + (prevState) => { + const layer = setter(addStaticValueColumn(prevState.layers[layerId])); + return mergeLayer({ state: prevState, layerId, newLayer: layer }); + }, + { + isDimensionComplete: true, + forceRender: true, + } + ); + } }; const ParamEditor = getParamEditor( diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx index bf4b10de386a1..9315b61adcc54 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern_suggestions.test.tsx @@ -1206,11 +1206,11 @@ describe('IndexPattern Data Source suggestions', () => { const modifiedState: IndexPatternPrivateState = { ...initialState, layers: { - thresholdLayer: { + referenceLineLayer: { indexPatternId: '1', - columnOrder: ['threshold'], + columnOrder: ['referenceLine'], columns: { - threshold: { + referenceLine: { dataType: 'number', isBucketed: false, label: 'Static Value: 0', @@ -1251,10 +1251,10 @@ describe('IndexPattern Data Source suggestions', () => { modifiedState, '1', documentField, - (layerId) => layerId !== 'thresholdLayer' + (layerId) => layerId !== 'referenceLineLayer' ) ); - // should ignore the threshold layer + // should ignore the referenceLine layer expect(suggestions).toContainEqual( expect.objectContaining({ table: expect.objectContaining({ @@ -1704,7 +1704,7 @@ describe('IndexPattern Data Source suggestions', () => { ); }); - it('adds date histogram over default time field for tables without time dimension and a threshold', async () => { + it('adds date histogram over default time field for tables without time dimension and a referenceLine', async () => { const initialState = testInitialState(); const state: IndexPatternPrivateState = { ...initialState, @@ -1738,11 +1738,11 @@ describe('IndexPattern Data Source suggestions', () => { }, }, }, - threshold: { + referenceLine: { indexPatternId: '2', - columnOrder: ['thresholda'], + columnOrder: ['referenceLineA'], columns: { - thresholda: { + referenceLineA: { label: 'My Op', customLabel: true, dataType: 'number', @@ -1758,7 +1758,7 @@ describe('IndexPattern Data Source suggestions', () => { expect( getSuggestionSubset( - getDatasourceSuggestionsFromCurrentState(state, (layerId) => layerId !== 'threshold') + getDatasourceSuggestionsFromCurrentState(state, (layerId) => layerId !== 'referenceLine') ) ).toContainEqual( expect.objectContaining({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts index d68fd8b9555f9..8b1eaeb109d9b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.test.ts @@ -21,7 +21,7 @@ describe('utils', () => { describe('checkForDataLayerType', () => { it('should return an error if the layer is of the wrong type', () => { - expect(checkForDataLayerType(layerTypes.THRESHOLD, 'Operation')).toEqual([ + expect(checkForDataLayerType(layerTypes.REFERENCELINE, 'Operation')).toEqual([ 'Operation is disabled for this type of layer.', ]); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts index 87c4355c1dc9f..0ec7ad046ac2a 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/calculations/utils.ts @@ -24,7 +24,7 @@ export const buildLabelFunction = }; export function checkForDataLayerType(layerType: LayerType, name: string) { - if (layerType === layerTypes.THRESHOLD) { + if (layerType === layerTypes.REFERENCELINE) { return [ i18n.translate('xpack.lens.indexPattern.calculations.layerDataType', { defaultMessage: '{name} is disabled for this type of layer.', diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx index 0a6620eecf308..1c574fe69611c 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.test.tsx @@ -80,14 +80,14 @@ describe('static_value', () => { }; }); - function getLayerWithStaticValue(newValue: string): IndexPatternLayer { + function getLayerWithStaticValue(newValue: string | null | undefined): IndexPatternLayer { return { ...layer, columns: { ...layer.columns, col2: { ...layer.columns.col2, - label: `Static value: ${newValue}`, + label: `Static value: ${newValue ?? String(newValue)}`, params: { value: newValue, }, @@ -155,8 +155,9 @@ describe('static_value', () => { ).toBeUndefined(); }); - it('should return error for invalid values', () => { - for (const value of ['NaN', 'Infinity', 'string']) { + it.each(['NaN', 'Infinity', 'string'])( + 'should return error for invalid values: %s', + (value) => { expect( staticValueOperation.getErrorMessage!( getLayerWithStaticValue(value), @@ -165,6 +166,16 @@ describe('static_value', () => { ) ).toEqual(expect.arrayContaining([expect.stringMatching('is not a valid number')])); } + ); + + it.each([null, undefined])('should return no error for: %s', (value) => { + expect( + staticValueOperation.getErrorMessage!( + getLayerWithStaticValue(value), + 'col2', + createMockedIndexPattern() + ) + ).toBe(undefined); }); }); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx index a76c5f64d1750..26be4e7b114da 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/static_value.tsx @@ -61,7 +61,7 @@ export const staticValueOperation: OperationDefinition< getErrorMessage(layer, columnId) { const column = layer.columns[columnId] as StaticValueIndexPatternColumn; - return !isValidNumber(column.params.value) + return column.params.value != null && !isValidNumber(column.params.value) ? [ i18n.translate('xpack.lens.indexPattern.staticValueError', { defaultMessage: 'The static value of {value} is not a valid number', @@ -176,10 +176,7 @@ export const staticValueOperation: OperationDefinition< // Pick the data from the current activeData (to be used when the current operation is not static_value) const activeDataValue = - activeData && - activeData[layerId] && - activeData[layerId]?.rows?.length === 1 && - activeData[layerId].rows[0][columnId]; + activeData?.[layerId]?.rows?.length === 1 && activeData[layerId].rows[0][columnId]; const fallbackValue = currentColumn?.operationType !== 'static_value' && activeDataValue != null @@ -206,7 +203,7 @@ export const staticValueOperation: OperationDefinition<

{i18n.translate('xpack.lens.indexPattern.staticValue.label', { - defaultMessage: 'Threshold value', + defaultMessage: 'Reference line value', })} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts index b3b98e5054aa6..9f3cba89ce17b 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/layer_helpers.ts @@ -1406,7 +1406,7 @@ export function isOperationAllowedAsReference({ // Labels need to be updated when columns are added because reference-based column labels // are sometimes copied into the parents -function updateDefaultLabels( +export function updateDefaultLabels( layer: IndexPatternLayer, indexPattern: IndexPattern ): IndexPatternLayer { diff --git a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts index 30238507c3566..5ed6ec052a0da 100644 --- a/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts +++ b/x-pack/plugins/lens/public/xy_visualization/color_assignment.ts @@ -24,7 +24,7 @@ interface LayerColorConfig { layerType: LayerType; } -export const defaultThresholdColor = euiLightVars.euiColorDarkShade; +export const defaultReferenceLineColor = euiLightVars.euiColorDarkShade; export type ColorAssignments = Record< string, @@ -117,11 +117,11 @@ export function getAccessorColorConfig( triggerIcon: 'disabled', }; } - if (layer.layerType === layerTypes.THRESHOLD) { + if (layer.layerType === layerTypes.REFERENCELINE) { return { columnId: accessor as string, triggerIcon: 'color', - color: currentYConfig?.color || defaultThresholdColor, + color: currentYConfig?.color || defaultReferenceLineColor, }; } const columnToLabel = getColumnToLabelMap(layer, frame.datasourceLayers[layer.layerId]); diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx index af2995fb65b71..9c272202e4565 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.test.tsx @@ -330,7 +330,7 @@ function sampleArgs() { return { data, args }; } -function sampleArgsWithThreshold(thresholdValue: number = 150) { +function sampleArgsWithReferenceLine(value: number = 150) { const { data, args } = sampleArgs(); return { @@ -338,16 +338,16 @@ function sampleArgsWithThreshold(thresholdValue: number = 150) { ...data, tables: { ...data.tables, - threshold: { + referenceLine: { type: 'datatable', columns: [ { - id: 'threshold-a', + id: 'referenceLine-a', meta: { params: { id: 'number' }, type: 'number' }, name: 'Static value', }, ], - rows: [{ 'threshold-a': thresholdValue }], + rows: [{ 'referenceLine-a': value }], }, }, } as LensMultiTable, @@ -356,16 +356,16 @@ function sampleArgsWithThreshold(thresholdValue: number = 150) { layers: [ ...args.layers, { - layerType: layerTypes.THRESHOLD, - accessors: ['threshold-a'], - layerId: 'threshold', + layerType: layerTypes.REFERENCELINE, + accessors: ['referenceLine-a'], + layerId: 'referenceLine', seriesType: 'line', xScaleType: 'linear', yScaleType: 'linear', palette: mockPaletteOutput, isHistogram: false, hide: true, - yConfig: [{ axisMode: 'left', forAccessor: 'threshold-a', type: 'lens_xy_yConfig' }], + yConfig: [{ axisMode: 'left', forAccessor: 'referenceLine-a', type: 'lens_xy_yConfig' }], }, ], } as XYArgs, @@ -874,8 +874,8 @@ describe('xy_expression', () => { }); }); - test('it does include threshold values when in full extent mode', () => { - const { data, args } = sampleArgsWithThreshold(); + test('it does include referenceLine values when in full extent mode', () => { + const { data, args } = sampleArgsWithReferenceLine(); const component = shallow(); expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ @@ -885,8 +885,8 @@ describe('xy_expression', () => { }); }); - test('it should ignore threshold values when set to custom extents', () => { - const { data, args } = sampleArgsWithThreshold(); + test('it should ignore referenceLine values when set to custom extents', () => { + const { data, args } = sampleArgsWithReferenceLine(); const component = shallow( { }); }); - test('it should work for negative values in thresholds', () => { - const { data, args } = sampleArgsWithThreshold(-150); + test('it should work for negative values in referenceLines', () => { + const { data, args } = sampleArgsWithReferenceLine(-150); const component = shallow(); expect(component.find(Axis).find('[id="left"]').prop('domain')).toEqual({ diff --git a/x-pack/plugins/lens/public/xy_visualization/expression.tsx b/x-pack/plugins/lens/public/xy_visualization/expression.tsx index 7aee537ebbedd..36f1b92b8a1f4 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression.tsx @@ -64,10 +64,10 @@ import { getXDomain, XyEndzones } from './x_domain'; import { getLegendAction } from './get_legend_action'; import { computeChartMargins, - getThresholdRequiredPaddings, - ThresholdAnnotations, -} from './expression_thresholds'; -import { computeOverallDataDomain } from './threshold_helpers'; + getReferenceLineRequiredPaddings, + ReferenceLineAnnotations, +} from './expression_reference_lines'; +import { computeOverallDataDomain } from './reference_line_helpers'; declare global { interface Window { @@ -264,7 +264,9 @@ export function XYChart({ const icon: IconType = layers.length > 0 ? getIconForSeriesType(layers[0].seriesType) : 'bar'; return ; } - const thresholdLayers = layers.filter((layer) => layer.layerType === layerTypes.THRESHOLD); + const referenceLineLayers = layers.filter( + (layer) => layer.layerType === layerTypes.REFERENCELINE + ); // use formatting hint of first x axis column to format ticks const xAxisColumn = data.tables[filteredLayers[0].layerId].columns.find( @@ -332,7 +334,7 @@ export function XYChart({ left: yAxesConfiguration.find(({ groupId }) => groupId === 'left'), right: yAxesConfiguration.find(({ groupId }) => groupId === 'right'), }; - const thresholdPaddings = getThresholdRequiredPaddings(thresholdLayers, yAxesMap); + const referenceLinePaddings = getReferenceLineRequiredPaddings(referenceLineLayers, yAxesMap); const getYAxesTitles = ( axisSeries: Array<{ layer: string; accessor: string }>, @@ -364,9 +366,9 @@ export function XYChart({ ? args.labelsOrientation?.yRight || 0 : args.labelsOrientation?.yLeft || 0, padding: - thresholdPaddings[groupId] != null + referenceLinePaddings[groupId] != null ? { - inner: thresholdPaddings[groupId], + inner: referenceLinePaddings[groupId], } : undefined, }, @@ -377,9 +379,9 @@ export function XYChart({ : axisTitlesVisibilitySettings?.yLeft, // if labels are not visible add the padding to the title padding: - !tickVisible && thresholdPaddings[groupId] != null + !tickVisible && referenceLinePaddings[groupId] != null ? { - inner: thresholdPaddings[groupId], + inner: referenceLinePaddings[groupId], } : undefined, }, @@ -406,10 +408,10 @@ export function XYChart({ max = extent.upperBound ?? NaN; } } else { - const axisHasThreshold = thresholdLayers.some(({ yConfig }) => + const axisHasReferenceLine = referenceLineLayers.some(({ yConfig }) => yConfig?.some(({ axisMode }) => axisMode === axis.groupId) ); - if (!fit && axisHasThreshold) { + if (!fit && axisHasReferenceLine) { // Remove this once the chart will support automatic annotation fit for other type of charts const { min: computedMin, max: computedMax } = computeOverallDataDomain( filteredLayers, @@ -421,7 +423,7 @@ export function XYChart({ max = Math.max(computedMax, max || 0); min = Math.min(computedMin, min || 0); } - for (const { layerId, yConfig } of thresholdLayers) { + for (const { layerId, yConfig } of referenceLineLayers) { const table = data.tables[layerId]; for (const { axisMode, forAccessor } of yConfig || []) { if (axis.groupId === axisMode) { @@ -575,11 +577,11 @@ export function XYChart({ legend: { labelOptions: { maxLines: legend.shouldTruncate ? legend?.maxLines ?? 1 : 0 }, }, - // if not title or labels are shown for axes, add some padding if required by threshold markers + // if not title or labels are shown for axes, add some padding if required by reference line markers chartMargins: { ...chartTheme.chartPaddings, ...computeChartMargins( - thresholdPaddings, + referenceLinePaddings, tickLabelsVisibilitySettings, axisTitlesVisibilitySettings, yAxesMap, @@ -622,13 +624,15 @@ export function XYChart({ visible: tickLabelsVisibilitySettings?.x, rotation: labelsOrientation?.x, padding: - thresholdPaddings.bottom != null ? { inner: thresholdPaddings.bottom } : undefined, + referenceLinePaddings.bottom != null + ? { inner: referenceLinePaddings.bottom } + : undefined, }, axisTitle: { visible: axisTitlesVisibilitySettings.x, padding: - !tickLabelsVisibilitySettings?.x && thresholdPaddings.bottom != null - ? { inner: thresholdPaddings.bottom } + !tickLabelsVisibilitySettings?.x && referenceLinePaddings.bottom != null + ? { inner: referenceLinePaddings.bottom } : undefined, }, }} @@ -911,9 +915,9 @@ export function XYChart({ } }) )} - {thresholdLayers.length ? ( - ) : null} diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.scss b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.scss new file mode 100644 index 0000000000000..07946b52b0000 --- /dev/null +++ b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.scss @@ -0,0 +1,18 @@ +.lnsXyDecorationRotatedWrapper { + display: inline-block; + overflow: hidden; + line-height: 1.5; + + .lnsXyDecorationRotatedWrapper__label { + display: inline-block; + white-space: nowrap; + transform: translate(0, 100%) rotate(-90deg); + transform-origin: 0 0; + + &::after { + content: ''; + float: left; + margin-top: 100%; + } + } +} \ No newline at end of file diff --git a/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx similarity index 81% rename from x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx rename to x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx index 67e994b734b84..42e02871026df 100644 --- a/x-pack/plugins/lens/public/xy_visualization/expression_thresholds.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/expression_reference_lines.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import './expression_thresholds.scss'; +import './expression_reference_lines.scss'; import React from 'react'; import { groupBy } from 'lodash'; import { EuiIcon } from '@elastic/eui'; @@ -15,59 +15,59 @@ import type { FieldFormat } from 'src/plugins/field_formats/common'; import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; import type { LayerArgs, YConfig } from '../../common/expressions'; import type { LensMultiTable } from '../../common/types'; -import { hasIcon } from './xy_config_panel/threshold_panel'; +import { hasIcon } from './xy_config_panel/reference_line_panel'; -const THRESHOLD_MARKER_SIZE = 20; +const REFERENCE_LINE_MARKER_SIZE = 20; export const computeChartMargins = ( - thresholdPaddings: Partial>, + referenceLinePaddings: Partial>, labelVisibility: Partial>, titleVisibility: Partial>, axesMap: Record<'left' | 'right', unknown>, isHorizontal: boolean ) => { const result: Partial> = {}; - if (!labelVisibility?.x && !titleVisibility?.x && thresholdPaddings.bottom) { + if (!labelVisibility?.x && !titleVisibility?.x && referenceLinePaddings.bottom) { const placement = isHorizontal ? mapVerticalToHorizontalPlacement('bottom') : 'bottom'; - result[placement] = thresholdPaddings.bottom; + result[placement] = referenceLinePaddings.bottom; } if ( - thresholdPaddings.left && + referenceLinePaddings.left && (isHorizontal || (!labelVisibility?.yLeft && !titleVisibility?.yLeft)) ) { const placement = isHorizontal ? mapVerticalToHorizontalPlacement('left') : 'left'; - result[placement] = thresholdPaddings.left; + result[placement] = referenceLinePaddings.left; } if ( - thresholdPaddings.right && + referenceLinePaddings.right && (isHorizontal || !axesMap.right || (!labelVisibility?.yRight && !titleVisibility?.yRight)) ) { const placement = isHorizontal ? mapVerticalToHorizontalPlacement('right') : 'right'; - result[placement] = thresholdPaddings.right; + result[placement] = referenceLinePaddings.right; } // there's no top axis, so just check if a margin has been computed - if (thresholdPaddings.top) { + if (referenceLinePaddings.top) { const placement = isHorizontal ? mapVerticalToHorizontalPlacement('top') : 'top'; - result[placement] = thresholdPaddings.top; + result[placement] = referenceLinePaddings.top; } return result; }; -// Note: it does not take into consideration whether the threshold is in view or not -export const getThresholdRequiredPaddings = ( - thresholdLayers: LayerArgs[], +// Note: it does not take into consideration whether the reference line is in view or not +export const getReferenceLineRequiredPaddings = ( + referenceLineLayers: LayerArgs[], axesMap: Record<'left' | 'right', unknown> ) => { // collect all paddings for the 4 axis: if any text is detected double it. const paddings: Partial> = {}; const icons: Partial> = {}; - thresholdLayers.forEach((layer) => { + referenceLineLayers.forEach((layer) => { layer.yConfig?.forEach(({ axisMode, icon, iconPosition, textVisibility }) => { if (axisMode && (hasIcon(icon) || textVisibility)) { const placement = getBaseIconPlacement(iconPosition, axisMode, axesMap); paddings[placement] = Math.max( paddings[placement] || 0, - THRESHOLD_MARKER_SIZE * (textVisibility ? 2 : 1) // double the padding size if there's text + REFERENCE_LINE_MARKER_SIZE * (textVisibility ? 2 : 1) // double the padding size if there's text ); icons[placement] = (icons[placement] || 0) + (hasIcon(icon) ? 1 : 0); } @@ -77,7 +77,7 @@ export const getThresholdRequiredPaddings = ( // if no icon is present for the placement, just reduce the padding (Object.keys(paddings) as Position[]).forEach((placement) => { if (!icons[placement]) { - paddings[placement] = THRESHOLD_MARKER_SIZE; + paddings[placement] = REFERENCE_LINE_MARKER_SIZE; } }); @@ -133,7 +133,7 @@ function getMarkerBody(label: string | undefined, isHorizontal: boolean) { } if (isHorizontal) { return ( -
+
{label}
); @@ -142,13 +142,13 @@ function getMarkerBody(label: string | undefined, isHorizontal: boolean) {
{label} @@ -180,32 +180,32 @@ function getMarkerToShow( } } -export const ThresholdAnnotations = ({ - thresholdLayers, +export const ReferenceLineAnnotations = ({ + layers, data, formatters, paletteService, syncColors, axesMap, isHorizontal, - thresholdPaddingMap, + paddingMap, }: { - thresholdLayers: LayerArgs[]; + layers: LayerArgs[]; data: LensMultiTable; formatters: Record<'left' | 'right' | 'bottom', FieldFormat | undefined>; paletteService: PaletteRegistry; syncColors: boolean; axesMap: Record<'left' | 'right', boolean>; isHorizontal: boolean; - thresholdPaddingMap: Partial>; + paddingMap: Partial>; }) => { return ( <> - {thresholdLayers.flatMap((thresholdLayer) => { - if (!thresholdLayer.yConfig) { + {layers.flatMap((layer) => { + if (!layer.yConfig) { return []; } - const { columnToLabel, yConfig: yConfigs, layerId } = thresholdLayer; + const { columnToLabel, yConfig: yConfigs, layerId } = layer; const columnToLabelMap: Record = columnToLabel ? JSON.parse(columnToLabel) : {}; @@ -218,6 +218,9 @@ export const ThresholdAnnotations = ({ ); const groupedByDirection = groupBy(yConfigByValue, 'fill'); + if (groupedByDirection.below) { + groupedByDirection.below.reverse(); + } return yConfigByValue.flatMap((yConfig, i) => { // Find the formatter for the given axis @@ -240,7 +243,7 @@ export const ThresholdAnnotations = ({ ); // the padding map is built for vertical chart const hasReducedPadding = - thresholdPaddingMap[markerPositionVertical] === THRESHOLD_MARKER_SIZE; + paddingMap[markerPositionVertical] === REFERENCE_LINE_MARKER_SIZE; const props = { groupId, @@ -306,7 +309,7 @@ export const ThresholdAnnotations = ({ const indexFromSameType = groupedByDirection[yConfig.fill].findIndex( ({ forAccessor }) => forAccessor === yConfig.forAccessor ); - const shouldCheckNextThreshold = + const shouldCheckNextReferenceLine = indexFromSameType < groupedByDirection[yConfig.fill].length - 1; annotations.push( { + const nextValue = + !isFillAbove && shouldCheckNextReferenceLine + ? row[groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor] + : undefined; if (yConfig.axisMode === 'bottom') { return { coordinates: { - x0: isFillAbove ? row[yConfig.forAccessor] : undefined, + x0: isFillAbove ? row[yConfig.forAccessor] : nextValue, y0: undefined, - x1: isFillAbove - ? shouldCheckNextThreshold - ? row[ - groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor - ] - : undefined - : row[yConfig.forAccessor], + x1: isFillAbove ? nextValue : row[yConfig.forAccessor], y1: undefined, }, header: columnToLabelMap[yConfig.forAccessor], @@ -336,15 +337,9 @@ export const ThresholdAnnotations = ({ return { coordinates: { x0: undefined, - y0: isFillAbove ? row[yConfig.forAccessor] : undefined, + y0: isFillAbove ? row[yConfig.forAccessor] : nextValue, x1: undefined, - y1: isFillAbove - ? shouldCheckNextThreshold - ? row[ - groupedByDirection[yConfig.fill!][indexFromSameType + 1].forAccessor - ] - : undefined - : row[yConfig.forAccessor], + y1: isFillAbove ? nextValue : row[yConfig.forAccessor], }, header: columnToLabelMap[yConfig.forAccessor], details: diff --git a/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.test.ts b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts similarity index 99% rename from x-pack/plugins/lens/public/xy_visualization/threshold_helpers.test.ts rename to x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts index d7286de0316d6..9dacc12c68d65 100644 --- a/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.test.ts @@ -7,7 +7,7 @@ import { XYLayerConfig } from '../../common/expressions'; import { FramePublicAPI } from '../types'; -import { computeOverallDataDomain, getStaticValue } from './threshold_helpers'; +import { computeOverallDataDomain, getStaticValue } from './reference_line_helpers'; function getActiveData(json: Array<{ id: string; rows: Array> }>) { return json.reduce((memo, { id, rows }) => { @@ -25,7 +25,7 @@ function getActiveData(json: Array<{ id: string; rows: Array); } -describe('threshold helpers', () => { +describe('reference_line helpers', () => { describe('getStaticValue', () => { const hasDateHistogram = () => false; const hasAllNumberHistogram = () => true; diff --git a/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.tsx b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx similarity index 83% rename from x-pack/plugins/lens/public/xy_visualization/threshold_helpers.tsx rename to x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx index 8bf5f84b15bad..71ce2d0ea2082 100644 --- a/x-pack/plugins/lens/public/xy_visualization/threshold_helpers.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/reference_line_helpers.tsx @@ -15,17 +15,17 @@ import { isPercentageSeries, isStackedChart } from './state_helpers'; import type { XYState } from './types'; import { checkScaleOperation } from './visualization_helpers'; -export interface ThresholdBase { +export interface ReferenceLineBase { label: 'x' | 'yRight' | 'yLeft'; } /** - * Return the threshold layers groups to show based on multiple criteria: + * Return the reference layers groups to show based on multiple criteria: * * what groups are current defined in data layers - * * what existing threshold are currently defined in data thresholds + * * what existing reference line are currently defined in reference layers */ -export function getGroupsToShow( - thresholdLayers: T[], +export function getGroupsToShow( + referenceLayers: T[], state: XYState | undefined, datasourceLayers: Record, tables: Record | undefined @@ -37,16 +37,16 @@ export function getGroupsToShow layerType === layerTypes.DATA ); const groupsAvailable = getGroupsAvailableInData(dataLayers, datasourceLayers, tables); - return thresholdLayers + return referenceLayers .filter(({ label, config }: T) => groupsAvailable[label] || config?.length) .map((layer) => ({ ...layer, valid: groupsAvailable[layer.label] })); } /** - * Returns the threshold layers groups to show based on what groups are current defined in data layers. + * Returns the reference layers groups to show based on what groups are current defined in data layers. */ -export function getGroupsRelatedToData( - thresholdLayers: T[], +export function getGroupsRelatedToData( + referenceLayers: T[], state: XYState | undefined, datasourceLayers: Record, tables: Record | undefined @@ -58,7 +58,7 @@ export function getGroupsRelatedToData( ({ layerType = layerTypes.DATA }) => layerType === layerTypes.DATA ); const groupsAvailable = getGroupsAvailableInData(dataLayers, datasourceLayers, tables); - return thresholdLayers.filter(({ label }: T) => groupsAvailable[label]); + return referenceLayers.filter(({ label }: T) => groupsAvailable[label]); } /** * Returns a dictionary with the groups filled in all the data layers @@ -90,7 +90,7 @@ export function getStaticValue( return fallbackValue; } - // filter and organize data dimensions into threshold groups + // filter and organize data dimensions into reference layer groups // now pick the columnId in the active data const { dataLayers: filteredLayers, @@ -128,29 +128,22 @@ function getAccessorCriteriaForGroup( ...rest, accessors: [xAccessor] as string[], })), - // need the untouched ones for some checks later on + // need the untouched ones to check if there are invalid layers from the filtered ones + // to perform the checks the original accessor structure needs to be accessed untouchedDataLayers: filteredDataLayers, accessors: filteredDataLayers.map(({ xAccessor }) => xAccessor) as string[], }; } - case 'yLeft': { - const { left } = groupAxesByType(dataLayers, activeData); - const leftIds = new Set(left.map(({ layer }) => layer)); - const filteredDataLayers = dataLayers.filter(({ layerId }) => leftIds.has(layerId)); - return { - dataLayers: filteredDataLayers, - untouchedDataLayers: filteredDataLayers, - accessors: left.map(({ accessor }) => accessor), - }; - } + case 'yLeft': case 'yRight': { - const { right } = groupAxesByType(dataLayers, activeData); - const rightIds = new Set(right.map(({ layer }) => layer)); + const prop = groupId === 'yLeft' ? 'left' : 'right'; + const { [prop]: axis } = groupAxesByType(dataLayers, activeData); + const rightIds = new Set(axis.map(({ layer }) => layer)); const filteredDataLayers = dataLayers.filter(({ layerId }) => rightIds.has(layerId)); return { dataLayers: filteredDataLayers, untouchedDataLayers: filteredDataLayers, - accessors: right.map(({ accessor }) => accessor), + accessors: axis.map(({ accessor }) => accessor), }; } } @@ -224,11 +217,11 @@ function computeStaticValueForGroup( activeData: NonNullable, minZeroOrNegativeBase: boolean = true ) { - const defaultThresholdFactor = 3 / 4; + const defaultReferenceLineFactor = 3 / 4; if (dataLayers.length && accessorIds.length) { if (dataLayers.some(({ seriesType }) => isPercentageSeries(seriesType))) { - return defaultThresholdFactor; + return defaultReferenceLineFactor; } const { min, max } = computeOverallDataDomain(dataLayers, accessorIds, activeData); @@ -237,7 +230,7 @@ function computeStaticValueForGroup( // Custom axis bounds can go below 0, so consider also lower values than 0 const finalMinValue = minZeroOrNegativeBase ? Math.min(0, min) : min; const interval = max - finalMinValue; - return Number((finalMinValue + interval * defaultThresholdFactor).toFixed(2)); + return Number((finalMinValue + interval * defaultReferenceLineFactor).toFixed(2)); } } } diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts index d174fb831c2dc..7d1bd64abe906 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.test.ts @@ -13,7 +13,7 @@ import { Operation } from '../types'; import { createMockDatasource, createMockFramePublicAPI } from '../mocks'; import { layerTypes } from '../../common'; import { fieldFormatsServiceMock } from '../../../../../src/plugins/field_formats/public/mocks'; -import { defaultThresholdColor } from './color_assignment'; +import { defaultReferenceLineColor } from './color_assignment'; describe('#toExpression', () => { const xyVisualization = getXyVisualization({ @@ -338,8 +338,8 @@ describe('#toExpression', () => { yConfig: [{ forAccessor: 'a' }], }, { - layerId: 'threshold', - layerType: layerTypes.THRESHOLD, + layerId: 'referenceLine', + layerType: layerTypes.REFERENCELINE, seriesType: 'area', splitAccessor: 'd', xAccessor: 'a', @@ -348,7 +348,7 @@ describe('#toExpression', () => { }, ], }, - { ...frame.datasourceLayers, threshold: mockDatasource.publicAPIMock } + { ...frame.datasourceLayers, referenceLine: mockDatasource.publicAPIMock } ) as Ast; function getYConfigColorForLayer(ast: Ast, index: number) { @@ -356,6 +356,6 @@ describe('#toExpression', () => { .chain[0].arguments.color; } expect(getYConfigColorForLayer(expression, 0)).toEqual([]); - expect(getYConfigColorForLayer(expression, 1)).toEqual([defaultThresholdColor]); + expect(getYConfigColorForLayer(expression, 1)).toEqual([defaultReferenceLineColor]); }); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts index 96ea9b84dd983..1cd0bab48cd68 100644 --- a/x-pack/plugins/lens/public/xy_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/xy_visualization/to_expression.ts @@ -13,8 +13,8 @@ import { OperationMetadata, DatasourcePublicAPI } from '../types'; import { getColumnToLabelMap } from './state_helpers'; import type { ValidLayer, XYLayerConfig } from '../../common/expressions'; import { layerTypes } from '../../common'; -import { hasIcon } from './xy_config_panel/threshold_panel'; -import { defaultThresholdColor } from './color_assignment'; +import { hasIcon } from './xy_config_panel/reference_line_panel'; +import { defaultReferenceLineColor } from './color_assignment'; export const getSortedAccessors = (datasource: DatasourcePublicAPI, layer: XYLayerConfig) => { const originalOrder = datasource @@ -59,7 +59,7 @@ export function toPreviewExpression( layers: state.layers.map((layer) => layer.layerType === layerTypes.DATA ? { ...layer, hide: true } - : // cap the threshold line to 1px + : // cap the reference line to 1px { ...layer, hide: true, @@ -338,8 +338,8 @@ export const buildExpression = ( forAccessor: [yConfig.forAccessor], axisMode: yConfig.axisMode ? [yConfig.axisMode] : [], color: - layer.layerType === layerTypes.THRESHOLD - ? [yConfig.color || defaultThresholdColor] + layer.layerType === layerTypes.REFERENCELINE + ? [yConfig.color || defaultReferenceLineColor] : yConfig.color ? [yConfig.color] : [], diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts index 8052b0d593215..01fbbd892a118 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.test.ts @@ -319,7 +319,7 @@ describe('xy_visualization', () => { }); }); - it('should add a dimension to a threshold layer', () => { + it('should add a dimension to a reference layer', () => { expect( xyVisualization.setDimension({ frame, @@ -327,20 +327,20 @@ describe('xy_visualization', () => { ...exampleState(), layers: [ { - layerId: 'threshold', - layerType: layerTypes.THRESHOLD, + layerId: 'referenceLine', + layerType: layerTypes.REFERENCELINE, seriesType: 'line', accessors: [], }, ], }, - layerId: 'threshold', - groupId: 'xThreshold', + layerId: 'referenceLine', + groupId: 'xReferenceLine', columnId: 'newCol', }).layers[0] ).toEqual({ - layerId: 'threshold', - layerType: layerTypes.THRESHOLD, + layerId: 'referenceLine', + layerType: layerTypes.REFERENCELINE, seriesType: 'line', accessors: ['newCol'], yConfig: [ @@ -538,15 +538,15 @@ describe('xy_visualization', () => { expect(ops.filter(filterOperations).map((x) => x.dataType)).toEqual(['number']); }); - describe('thresholds', () => { + describe('reference lines', () => { beforeEach(() => { frame.datasourceLayers = { first: mockDatasource.publicAPIMock, - threshold: mockDatasource.publicAPIMock, + referenceLine: mockDatasource.publicAPIMock, }; }); - function getStateWithBaseThreshold(): State { + function getStateWithBaseReferenceLine(): State { return { ...exampleState(), layers: [ @@ -559,8 +559,8 @@ describe('xy_visualization', () => { accessors: ['a'], }, { - layerId: 'threshold', - layerType: layerTypes.THRESHOLD, + layerId: 'referenceLine', + layerType: layerTypes.REFERENCELINE, seriesType: 'line', accessors: [], yConfig: [{ axisMode: 'left', forAccessor: 'a' }], @@ -570,28 +570,28 @@ describe('xy_visualization', () => { } it('should support static value', () => { - const state = getStateWithBaseThreshold(); + const state = getStateWithBaseReferenceLine(); state.layers[0].accessors = []; state.layers[1].yConfig = undefined; expect( xyVisualization.getConfiguration({ - state: getStateWithBaseThreshold(), + state: getStateWithBaseReferenceLine(), frame, - layerId: 'threshold', + layerId: 'referenceLine', }).supportStaticValue ).toBeTruthy(); }); - it('should return no threshold groups for a empty data layer', () => { - const state = getStateWithBaseThreshold(); + it('should return no referenceLine groups for a empty data layer', () => { + const state = getStateWithBaseReferenceLine(); state.layers[0].accessors = []; state.layers[1].yConfig = undefined; const options = xyVisualization.getConfiguration({ state, frame, - layerId: 'threshold', + layerId: 'referenceLine', }).groups; expect(options).toHaveLength(0); @@ -599,37 +599,37 @@ describe('xy_visualization', () => { it('should return a group for the vertical left axis', () => { const options = xyVisualization.getConfiguration({ - state: getStateWithBaseThreshold(), + state: getStateWithBaseReferenceLine(), frame, - layerId: 'threshold', + layerId: 'referenceLine', }).groups; expect(options).toHaveLength(1); - expect(options[0].groupId).toBe('yThresholdLeft'); + expect(options[0].groupId).toBe('yReferenceLineLeft'); }); it('should return a group for the vertical right axis', () => { - const state = getStateWithBaseThreshold(); + const state = getStateWithBaseReferenceLine(); state.layers[0].yConfig = [{ axisMode: 'right', forAccessor: 'a' }]; state.layers[1].yConfig![0].axisMode = 'right'; const options = xyVisualization.getConfiguration({ state, frame, - layerId: 'threshold', + layerId: 'referenceLine', }).groups; expect(options).toHaveLength(1); - expect(options[0].groupId).toBe('yThresholdRight'); + expect(options[0].groupId).toBe('yReferenceLineRight'); }); - it('should compute no groups for thresholds when the only data accessor available is a date histogram', () => { - const state = getStateWithBaseThreshold(); + it('should compute no groups for referenceLines when the only data accessor available is a date histogram', () => { + const state = getStateWithBaseReferenceLine(); state.layers[0].xAccessor = 'b'; state.layers[0].accessors = []; state.layers[1].yConfig = []; // empty the configuration // set the xAccessor as date_histogram - frame.datasourceLayers.threshold.getOperationForColumnId = jest.fn((accessor) => { + frame.datasourceLayers.referenceLine.getOperationForColumnId = jest.fn((accessor) => { if (accessor === 'b') { return { dataType: 'date', @@ -644,19 +644,19 @@ describe('xy_visualization', () => { const options = xyVisualization.getConfiguration({ state, frame, - layerId: 'threshold', + layerId: 'referenceLine', }).groups; expect(options).toHaveLength(0); }); it('should mark horizontal group is invalid when xAccessor is changed to a date histogram', () => { - const state = getStateWithBaseThreshold(); + const state = getStateWithBaseReferenceLine(); state.layers[0].xAccessor = 'b'; state.layers[0].accessors = []; state.layers[1].yConfig![0].axisMode = 'bottom'; // set the xAccessor as date_histogram - frame.datasourceLayers.threshold.getOperationForColumnId = jest.fn((accessor) => { + frame.datasourceLayers.referenceLine.getOperationForColumnId = jest.fn((accessor) => { if (accessor === 'b') { return { dataType: 'date', @@ -671,19 +671,19 @@ describe('xy_visualization', () => { const options = xyVisualization.getConfiguration({ state, frame, - layerId: 'threshold', + layerId: 'referenceLine', }).groups; expect(options[0]).toEqual( expect.objectContaining({ invalid: true, - groupId: 'xThreshold', + groupId: 'xReferenceLine', }) ); }); it('should return groups in a specific order (left, right, bottom)', () => { - const state = getStateWithBaseThreshold(); + const state = getStateWithBaseReferenceLine(); state.layers[0].xAccessor = 'c'; state.layers[0].accessors = ['a', 'b']; // invert them on purpose @@ -697,7 +697,7 @@ describe('xy_visualization', () => { { forAccessor: 'a', axisMode: 'left' }, ]; // set the xAccessor as number histogram - frame.datasourceLayers.threshold.getOperationForColumnId = jest.fn((accessor) => { + frame.datasourceLayers.referenceLine.getOperationForColumnId = jest.fn((accessor) => { if (accessor === 'c') { return { dataType: 'number', @@ -712,21 +712,21 @@ describe('xy_visualization', () => { const [left, right, bottom] = xyVisualization.getConfiguration({ state, frame, - layerId: 'threshold', + layerId: 'referenceLine', }).groups; - expect(left.groupId).toBe('yThresholdLeft'); - expect(right.groupId).toBe('yThresholdRight'); - expect(bottom.groupId).toBe('xThreshold'); + expect(left.groupId).toBe('yReferenceLineLeft'); + expect(right.groupId).toBe('yReferenceLineRight'); + expect(bottom.groupId).toBe('xReferenceLine'); }); it('should ignore terms operation for xAccessor', () => { - const state = getStateWithBaseThreshold(); + const state = getStateWithBaseReferenceLine(); state.layers[0].xAccessor = 'b'; state.layers[0].accessors = []; state.layers[1].yConfig = []; // empty the configuration // set the xAccessor as top values - frame.datasourceLayers.threshold.getOperationForColumnId = jest.fn((accessor) => { + frame.datasourceLayers.referenceLine.getOperationForColumnId = jest.fn((accessor) => { if (accessor === 'b') { return { dataType: 'string', @@ -741,19 +741,19 @@ describe('xy_visualization', () => { const options = xyVisualization.getConfiguration({ state, frame, - layerId: 'threshold', + layerId: 'referenceLine', }).groups; expect(options).toHaveLength(0); }); it('should mark horizontal group is invalid when accessor is changed to a terms operation', () => { - const state = getStateWithBaseThreshold(); + const state = getStateWithBaseReferenceLine(); state.layers[0].xAccessor = 'b'; state.layers[0].accessors = []; state.layers[1].yConfig![0].axisMode = 'bottom'; // set the xAccessor as date_histogram - frame.datasourceLayers.threshold.getOperationForColumnId = jest.fn((accessor) => { + frame.datasourceLayers.referenceLine.getOperationForColumnId = jest.fn((accessor) => { if (accessor === 'b') { return { dataType: 'string', @@ -768,13 +768,13 @@ describe('xy_visualization', () => { const options = xyVisualization.getConfiguration({ state, frame, - layerId: 'threshold', + layerId: 'referenceLine', }).groups; expect(options[0]).toEqual( expect.objectContaining({ invalid: true, - groupId: 'xThreshold', + groupId: 'xReferenceLine', }) ); }); @@ -813,20 +813,20 @@ describe('xy_visualization', () => { }, }; - const state = getStateWithBaseThreshold(); + const state = getStateWithBaseReferenceLine(); state.layers[0].accessors = ['yAccessorId', 'yAccessorId2']; state.layers[1].yConfig = []; // empty the configuration const options = xyVisualization.getConfiguration({ state, frame: { ...frame, activeData: tables }, - layerId: 'threshold', + layerId: 'referenceLine', }).groups; expect(options).toEqual( expect.arrayContaining([ - expect.objectContaining({ groupId: 'yThresholdLeft' }), - expect.objectContaining({ groupId: 'yThresholdRight' }), + expect.objectContaining({ groupId: 'yReferenceLineLeft' }), + expect.objectContaining({ groupId: 'yReferenceLineRight' }), ]) ); }); diff --git a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx index 4e279d2e0026d..db1a2aeffb670 100644 --- a/x-pack/plugins/lens/public/xy_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/visualization.tsx @@ -27,14 +27,14 @@ import { LensIconChartMixedXy } from '../assets/chart_mixed_xy'; import { LensIconChartBarHorizontal } from '../assets/chart_bar_horizontal'; import { getAccessorColorConfig, getColorAssignments } from './color_assignment'; import { getColumnToLabelMap } from './state_helpers'; -import { LensIconChartBarThreshold } from '../assets/chart_bar_threshold'; +import { LensIconChartBarReferenceLine } from '../assets/chart_bar_reference_line'; import { generateId } from '../id_generator'; import { getGroupsAvailableInData, getGroupsRelatedToData, getGroupsToShow, getStaticValue, -} from './threshold_helpers'; +} from './reference_line_helpers'; import { checkScaleOperation, checkXAccessorCompatibility, @@ -194,17 +194,17 @@ export const getXyVisualization = ({ }, getSupportedLayers(state, frame) { - const thresholdGroupIds = [ + const referenceLineGroupIds = [ { - id: 'yThresholdLeft', + id: 'yReferenceLineLeft', label: 'yLeft' as const, }, { - id: 'yThresholdRight', + id: 'yReferenceLineRight', label: 'yRight' as const, }, { - id: 'xThreshold', + id: 'xReferenceLine', label: 'x' as const, }, ]; @@ -220,8 +220,8 @@ export const getXyVisualization = ({ 'number', frame?.datasourceLayers || {} ); - const thresholdGroups = getGroupsRelatedToData( - thresholdGroupIds, + const referenceLineGroups = getGroupsRelatedToData( + referenceLineGroupIds, state, frame?.datasourceLayers || {}, frame?.activeData @@ -236,22 +236,22 @@ export const getXyVisualization = ({ icon: LensIconChartMixedXy, }, { - type: layerTypes.THRESHOLD, - label: i18n.translate('xpack.lens.xyChart.addThresholdLayerLabel', { - defaultMessage: 'Add threshold layer', + type: layerTypes.REFERENCELINE, + label: i18n.translate('xpack.lens.xyChart.addReferenceLineLayerLabel', { + defaultMessage: 'Add reference layer', }), - icon: LensIconChartBarThreshold, + icon: LensIconChartBarReferenceLine, disabled: !filledDataLayers.length || (!dataLayers.some(layerHasNumberHistogram) && dataLayers.every(({ accessors }) => !accessors.length)), tooltipContent: filledDataLayers.length ? undefined - : i18n.translate('xpack.lens.xyChart.addThresholdLayerLabelDisabledHelp', { - defaultMessage: 'Add some data to enable threshold layer', + : i18n.translate('xpack.lens.xyChart.addReferenceLineLayerLabelDisabledHelp', { + defaultMessage: 'Add some data to enable reference layer', }), initialDimensions: state - ? thresholdGroups.map(({ id, label }) => ({ + ? referenceLineGroups.map(({ id, label }) => ({ groupId: id, columnId: generateId(), dataType: 'number', @@ -318,25 +318,25 @@ export const getXyVisualization = ({ ); const groupsToShow = getGroupsToShow( [ - // When a threshold layer panel is added, a static threshold should automatically be included by default + // When a reference layer panel is added, a static reference line should automatically be included by default // in the first available axis, in the following order: vertical left, vertical right, horizontal. { config: left, - id: 'yThresholdLeft', + id: 'yReferenceLineLeft', label: 'yLeft', - dataTestSubj: 'lnsXY_yThresholdLeftPanel', + dataTestSubj: 'lnsXY_yReferenceLineLeftPanel', }, { config: right, - id: 'yThresholdRight', + id: 'yReferenceLineRight', label: 'yRight', - dataTestSubj: 'lnsXY_yThresholdRightPanel', + dataTestSubj: 'lnsXY_yReferenceLineRightPanel', }, { config: bottom, - id: 'xThreshold', + id: 'xReferenceLine', label: 'x', - dataTestSubj: 'lnsXY_xThresholdPanel', + dataTestSubj: 'lnsXY_xReferenceLinePanel', }, ], state, @@ -346,9 +346,9 @@ export const getXyVisualization = ({ return { supportFieldFormat: false, supportStaticValue: true, - // Each thresholds layer panel will have sections for each available axis + // Each reference lines layer panel will have sections for each available axis // (horizontal axis, vertical axis left, vertical axis right). - // Only axes that support numeric thresholds should be shown + // Only axes that support numeric reference lines should be shown groups: groupsToShow.map(({ config = [], id, label, dataTestSubj, valid }) => ({ groupId: id, groupLabel: getAxisName(label, { isHorizontal }), @@ -363,10 +363,16 @@ export const getXyVisualization = ({ enableDimensionEditor: true, dataTestSubj, invalid: !valid, - invalidMessage: i18n.translate('xpack.lens.configure.invalidThresholdDimension', { - defaultMessage: - 'This threshold is assigned to an axis that no longer exists. You may move this threshold to another available axis or remove it.', - }), + invalidMessage: + label === 'x' + ? i18n.translate('xpack.lens.configure.invalidBottomReferenceLineDimension', { + defaultMessage: + 'This reference line is assigned to an axis that no longer exists or is no longer valid. You may move this reference line to another available axis or remove it.', + }) + : i18n.translate('xpack.lens.configure.invalidReferenceLineDimension', { + defaultMessage: + 'This reference line is assigned to an axis that no longer exists. You may move this reference line to another available axis or remove it.', + }), requiresPreviousColumnOnDuplicate: true, })), }; @@ -439,7 +445,7 @@ export const getXyVisualization = ({ newLayer.splitAccessor = columnId; } - if (newLayer.layerType === layerTypes.THRESHOLD) { + if (newLayer.layerType === layerTypes.REFERENCELINE) { newLayer.accessors = [...newLayer.accessors.filter((a) => a !== columnId), columnId]; const hasYConfig = newLayer.yConfig?.some(({ forAccessor }) => forAccessor === columnId); const previousYConfig = previousColumn @@ -454,9 +460,9 @@ export const getXyVisualization = ({ // but keep the new group & id config forAccessor: columnId, axisMode: - groupId === 'xThreshold' + groupId === 'xReferenceLine' ? 'bottom' - : groupId === 'yThresholdRight' + : groupId === 'yReferenceLineRight' ? 'right' : 'left', }, @@ -491,9 +497,9 @@ export const getXyVisualization = ({ } let newLayers = prevState.layers.map((l) => (l.layerId === layerId ? newLayer : l)); - // // check if there's any threshold layer and pull it off if all data layers have no dimensions set + // check if there's any reference layer and pull it off if all data layers have no dimensions set const layersByType = groupBy(newLayers, ({ layerType }) => layerType); - // // check for data layers if they all still have xAccessors + // check for data layers if they all still have xAccessors const groupsAvailable = getGroupsAvailableInData( layersByType[layerTypes.DATA], frame.datasourceLayers, diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx index 516adbf585b9f..e3e53126015eb 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/color_picker.tsx @@ -16,7 +16,7 @@ import { State } from '../types'; import { FormatFactory, layerTypes } from '../../../common'; import { getSeriesColor } from '../state_helpers'; import { - defaultThresholdColor, + defaultReferenceLineColor, getAccessorColorConfig, getColorAssignments, } from '../color_assignment'; @@ -60,8 +60,8 @@ export const ColorPicker = ({ const overwriteColor = getSeriesColor(layer, accessor); const currentColor = useMemo(() => { if (overwriteColor || !frame.activeData) return overwriteColor; - if (layer.layerType === layerTypes.THRESHOLD) { - return defaultThresholdColor; + if (layer.layerType === layerTypes.REFERENCELINE) { + return defaultReferenceLineColor; } const datasource = frame.datasourceLayers[layer.layerId]; diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx index 41d00e2eef32a..e18ea18c30fb0 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/index.tsx @@ -24,7 +24,7 @@ import type { FramePublicAPI, } from '../../types'; import { State, visualizationTypes, XYState } from '../types'; -import type { FormatFactory } from '../../../common'; +import { FormatFactory, layerTypes } from '../../../common'; import { SeriesType, YAxisMode, @@ -39,7 +39,7 @@ import { getAxesConfiguration, GroupsConfiguration } from '../axes_configuration import { VisualOptionsPopover } from './visual_options_popover'; import { getScaleType } from '../to_expression'; import { ColorPicker } from './color_picker'; -import { ThresholdPanel } from './threshold_panel'; +import { ReferenceLinePanel } from './reference_line_panel'; import { PalettePicker, TooltipWrapper } from '../../shared_components'; type UnwrapArray = T extends Array ? P : T; @@ -564,8 +564,8 @@ export function DimensionEditor( ); } - if (layer.layerType === 'threshold') { - return ; + if (layer.layerType === layerTypes.REFERENCELINE) { + return ; } return ( diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx index dde4de0dd4bc3..d81979f603943 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/layer_header.tsx @@ -17,7 +17,7 @@ import { isHorizontalChart, isHorizontalSeries } from '../state_helpers'; import { trackUiEvent } from '../../lens_ui_telemetry'; import { StaticHeader } from '../../shared_components'; import { ToolbarButton } from '../../../../../../src/plugins/kibana_react/public'; -import { LensIconChartBarThreshold } from '../../assets/chart_bar_threshold'; +import { LensIconChartBarReferenceLine } from '../../assets/chart_bar_reference_line'; import { updateLayer } from '.'; export function LayerHeader(props: VisualizationLayerWidgetProps) { @@ -29,13 +29,13 @@ export function LayerHeader(props: VisualizationLayerWidgetProps) { if (!layer) { return null; } - // if it's a threshold just draw a static text - if (layer.layerType === layerTypes.THRESHOLD) { + // if it's a reference line just draw a static text + if (layer.layerType === layerTypes.REFERENCELINE) { return ( ); diff --git a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx similarity index 71% rename from x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx rename to x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx index 7c31d72e6cbde..7b9fd01e540fe 100644 --- a/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/threshold_panel.tsx +++ b/x-pack/plugins/lens/public/xy_visualization/xy_config_panel/reference_line_panel.tsx @@ -30,53 +30,55 @@ import { TooltipWrapper, useDebouncedValue } from '../../shared_components'; const icons = [ { value: 'none', - label: i18n.translate('xpack.lens.xyChart.thresholds.noIconLabel', { defaultMessage: 'None' }), + label: i18n.translate('xpack.lens.xyChart.referenceLine.noIconLabel', { + defaultMessage: 'None', + }), }, { value: 'asterisk', - label: i18n.translate('xpack.lens.xyChart.thresholds.asteriskIconLabel', { + label: i18n.translate('xpack.lens.xyChart.referenceLine.asteriskIconLabel', { defaultMessage: 'Asterisk', }), }, { value: 'bell', - label: i18n.translate('xpack.lens.xyChart.thresholds.bellIconLabel', { + label: i18n.translate('xpack.lens.xyChart.referenceLine.bellIconLabel', { defaultMessage: 'Bell', }), }, { value: 'bolt', - label: i18n.translate('xpack.lens.xyChart.thresholds.boltIconLabel', { + label: i18n.translate('xpack.lens.xyChart.referenceLine.boltIconLabel', { defaultMessage: 'Bolt', }), }, { value: 'bug', - label: i18n.translate('xpack.lens.xyChart.thresholds.bugIconLabel', { + label: i18n.translate('xpack.lens.xyChart.referenceLine.bugIconLabel', { defaultMessage: 'Bug', }), }, { value: 'editorComment', - label: i18n.translate('xpack.lens.xyChart.thresholds.commentIconLabel', { + label: i18n.translate('xpack.lens.xyChart.referenceLine.commentIconLabel', { defaultMessage: 'Comment', }), }, { value: 'alert', - label: i18n.translate('xpack.lens.xyChart.thresholds.alertIconLabel', { + label: i18n.translate('xpack.lens.xyChart.referenceLine.alertIconLabel', { defaultMessage: 'Alert', }), }, { value: 'flag', - label: i18n.translate('xpack.lens.xyChart.thresholds.flagIconLabel', { + label: i18n.translate('xpack.lens.xyChart.referenceLine.flagIconLabel', { defaultMessage: 'Flag', }), }, { value: 'tag', - label: i18n.translate('xpack.lens.xyChart.thresholds.tagIconLabel', { + label: i18n.translate('xpack.lens.xyChart.referenceLine.tagIconLabel', { defaultMessage: 'Tag', }), }, @@ -116,20 +118,57 @@ const IconSelect = ({ ); }; -function getIconPositionOptions({ - isHorizontal, - axisMode, -}: { +interface LabelConfigurationOptions { isHorizontal: boolean; axisMode: YConfig['axisMode']; -}) { +} + +function getFillPositionOptions({ isHorizontal, axisMode }: LabelConfigurationOptions) { + const aboveLabel = i18n.translate('xpack.lens.xyChart.referenceLineFill.above', { + defaultMessage: 'Above', + }); + const belowLabel = i18n.translate('xpack.lens.xyChart.referenceLineFill.below', { + defaultMessage: 'Below', + }); + const beforeLabel = i18n.translate('xpack.lens.xyChart.referenceLineFill.before', { + defaultMessage: 'Before', + }); + const afterLabel = i18n.translate('xpack.lens.xyChart.referenceLineFill.after', { + defaultMessage: 'After', + }); + + const aboveOptionLabel = axisMode !== 'bottom' && !isHorizontal ? aboveLabel : afterLabel; + const belowOptionLabel = axisMode !== 'bottom' && !isHorizontal ? belowLabel : beforeLabel; + + return [ + { + id: `${idPrefix}none`, + label: i18n.translate('xpack.lens.xyChart.referenceLineFill.none', { + defaultMessage: 'None', + }), + 'data-test-subj': 'lnsXY_referenceLine_fill_none', + }, + { + id: `${idPrefix}above`, + label: aboveOptionLabel, + 'data-test-subj': 'lnsXY_referenceLine_fill_above', + }, + { + id: `${idPrefix}below`, + label: belowOptionLabel, + 'data-test-subj': 'lnsXY_referenceLine_fill_below', + }, + ]; +} + +function getIconPositionOptions({ isHorizontal, axisMode }: LabelConfigurationOptions) { const options = [ { id: `${idPrefix}auto`, - label: i18n.translate('xpack.lens.xyChart.thresholdMarker.auto', { + label: i18n.translate('xpack.lens.xyChart.referenceLineMarker.auto', { defaultMessage: 'Auto', }), - 'data-test-subj': 'lnsXY_markerPosition_auto', + 'data-test-subj': 'lnsXY_referenceLine_markerPosition_auto', }, ]; const topLabel = i18n.translate('xpack.lens.xyChart.markerPosition.above', { @@ -149,12 +188,12 @@ function getIconPositionOptions({ { id: `${idPrefix}above`, label: isHorizontal ? rightLabel : topLabel, - 'data-test-subj': 'lnsXY_markerPosition_above', + 'data-test-subj': 'lnsXY_referenceLine_markerPosition_above', }, { id: `${idPrefix}below`, label: isHorizontal ? leftLabel : bottomLabel, - 'data-test-subj': 'lnsXY_markerPosition_below', + 'data-test-subj': 'lnsXY_referenceLine_markerPosition_below', }, ]; if (isHorizontal) { @@ -168,12 +207,12 @@ function getIconPositionOptions({ { id: `${idPrefix}left`, label: isHorizontal ? bottomLabel : leftLabel, - 'data-test-subj': 'lnsXY_markerPosition_left', + 'data-test-subj': 'lnsXY_referenceLine_markerPosition_left', }, { id: `${idPrefix}right`, label: isHorizontal ? topLabel : rightLabel, - 'data-test-subj': 'lnsXY_markerPosition_right', + 'data-test-subj': 'lnsXY_referenceLine_markerPosition_right', }, ]; if (isHorizontal) { @@ -188,7 +227,7 @@ export function hasIcon(icon: string | undefined): icon is string { return icon != null && icon !== 'none'; } -export const ThresholdPanel = ( +export const ReferenceLinePanel = ( props: VisualizationDimensionEditorProps & { formatFactory: FormatFactory; paletteService: PaletteRegistry; @@ -232,18 +271,18 @@ export const ThresholdPanel = ( return ( <> { setYConfig({ forAccessor: accessor, textVisibility: !currentYConfig?.textVisibility }); @@ -253,7 +292,7 @@ export const ThresholdPanel = ( @@ -268,15 +307,18 @@ export const ThresholdPanel = ( display="columnCompressed" fullWidth isDisabled={!hasIcon(currentYConfig?.icon) && !currentYConfig?.textVisibility} - label={i18n.translate('xpack.lens.xyChart.thresholdMarker.position', { + label={i18n.translate('xpack.lens.xyChart.referenceLineMarker.position', { defaultMessage: 'Decoration position', })} > @@ -372,41 +414,19 @@ export const ThresholdPanel = ( { const newMode = id.replace(idPrefix, '') as FillStyle; @@ -440,7 +460,7 @@ const LineThicknessSlider = ({ return ( { namespaceTypes: ['single', 'agnostic'], notifications: mockKibanaNotificationsService, showEventFilters: false, + showHostIsolationExceptions: false, showTrustedApps: false, }) ); @@ -86,6 +87,7 @@ describe('useExceptionLists', () => { namespaceTypes: ['single', 'agnostic'], notifications: mockKibanaNotificationsService, showEventFilters: false, + showHostIsolationExceptions: false, showTrustedApps: false, }) ); @@ -127,6 +129,7 @@ describe('useExceptionLists', () => { namespaceTypes: ['single', 'agnostic'], notifications: mockKibanaNotificationsService, showEventFilters: false, + showHostIsolationExceptions: false, showTrustedApps: true, }) ); @@ -137,7 +140,7 @@ describe('useExceptionLists', () => { expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ filters: - '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', + '(exception-list.attributes.list_id: endpoint_trusted_apps* OR exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)', http: mockKibanaHttpService, namespaceTypes: 'single,agnostic', pagination: { page: 1, perPage: 20 }, @@ -163,6 +166,7 @@ describe('useExceptionLists', () => { namespaceTypes: ['single', 'agnostic'], notifications: mockKibanaNotificationsService, showEventFilters: false, + showHostIsolationExceptions: false, showTrustedApps: false, }) ); @@ -173,7 +177,7 @@ describe('useExceptionLists', () => { expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ filters: - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)', http: mockKibanaHttpService, namespaceTypes: 'single,agnostic', pagination: { page: 1, perPage: 20 }, @@ -199,6 +203,7 @@ describe('useExceptionLists', () => { namespaceTypes: ['single', 'agnostic'], notifications: mockKibanaNotificationsService, showEventFilters: true, + showHostIsolationExceptions: false, showTrustedApps: false, }) ); @@ -209,7 +214,7 @@ describe('useExceptionLists', () => { expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ filters: - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (exception-list.attributes.list_id: endpoint_event_filters* OR exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)', http: mockKibanaHttpService, namespaceTypes: 'single,agnostic', pagination: { page: 1, perPage: 20 }, @@ -235,6 +240,7 @@ describe('useExceptionLists', () => { namespaceTypes: ['single', 'agnostic'], notifications: mockKibanaNotificationsService, showEventFilters: false, + showHostIsolationExceptions: false, showTrustedApps: false, }) ); @@ -245,7 +251,81 @@ describe('useExceptionLists', () => { expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ filters: - '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)', + http: mockKibanaHttpService, + namespaceTypes: 'single,agnostic', + pagination: { page: 1, perPage: 20 }, + signal: new AbortController().signal, + }); + }); + }); + + test('fetches host isolation exceptions lists if "hostIsolationExceptionsFilter" is true', async () => { + const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists'); + + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useExceptionLists({ + errorMessage: 'Uh oh', + filterOptions: {}, + http: mockKibanaHttpService, + initialPagination: { + page: 1, + perPage: 20, + total: 0, + }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, + showEventFilters: false, + showHostIsolationExceptions: true, + showTrustedApps: false, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ + filters: + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (exception-list.attributes.list_id: endpoint_host_isolation_exceptions* OR exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)', + http: mockKibanaHttpService, + namespaceTypes: 'single,agnostic', + pagination: { page: 1, perPage: 20 }, + signal: new AbortController().signal, + }); + }); + }); + + test('does not fetch host isolation exceptions lists if "showHostIsolationExceptions" is false', async () => { + const spyOnfetchExceptionLists = jest.spyOn(api, 'fetchExceptionLists'); + + await act(async () => { + const { waitForNextUpdate } = renderHook(() => + useExceptionLists({ + errorMessage: 'Uh oh', + filterOptions: {}, + http: mockKibanaHttpService, + initialPagination: { + page: 1, + perPage: 20, + total: 0, + }, + namespaceTypes: ['single', 'agnostic'], + notifications: mockKibanaNotificationsService, + showEventFilters: false, + showHostIsolationExceptions: false, + showTrustedApps: false, + }) + ); + // NOTE: First `waitForNextUpdate` is initialization + // Second call applies the params + await waitForNextUpdate(); + await waitForNextUpdate(); + + expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ + filters: + '(not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)', http: mockKibanaHttpService, namespaceTypes: 'single,agnostic', pagination: { page: 1, perPage: 20 }, @@ -274,6 +354,7 @@ describe('useExceptionLists', () => { namespaceTypes: ['single', 'agnostic'], notifications: mockKibanaNotificationsService, showEventFilters: false, + showHostIsolationExceptions: false, showTrustedApps: false, }) ); @@ -284,7 +365,7 @@ describe('useExceptionLists', () => { expect(spyOnfetchExceptionLists).toHaveBeenCalledWith({ filters: - '(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*)', + '(exception-list.attributes.created_by:Moi OR exception-list-agnostic.attributes.created_by:Moi) AND (exception-list.attributes.name.text:Sample Endpoint OR exception-list-agnostic.attributes.name.text:Sample Endpoint) AND (not exception-list.attributes.list_id: endpoint_trusted_apps* AND not exception-list-agnostic.attributes.list_id: endpoint_trusted_apps*) AND (not exception-list.attributes.list_id: endpoint_event_filters* AND not exception-list-agnostic.attributes.list_id: endpoint_event_filters*) AND (not exception-list.attributes.list_id: endpoint_host_isolation_exceptions* AND not exception-list-agnostic.attributes.list_id: endpoint_host_isolation_exceptions*)', http: mockKibanaHttpService, namespaceTypes: 'single,agnostic', pagination: { page: 1, perPage: 20 }, @@ -318,6 +399,7 @@ describe('useExceptionLists', () => { namespaceTypes, notifications, showEventFilters, + showHostIsolationExceptions: false, showTrustedApps, }), { @@ -333,6 +415,7 @@ describe('useExceptionLists', () => { namespaceTypes: ['single'], notifications: mockKibanaNotificationsService, showEventFilters: false, + showHostIsolationExceptions: false, showTrustedApps: false, }, } @@ -354,6 +437,7 @@ describe('useExceptionLists', () => { namespaceTypes: ['single', 'agnostic'], notifications: mockKibanaNotificationsService, showEventFilters: false, + showHostIsolationExceptions: false, showTrustedApps: false, }); // NOTE: Only need one call here because hook already initilaized @@ -382,6 +466,7 @@ describe('useExceptionLists', () => { namespaceTypes: ['single', 'agnostic'], notifications: mockKibanaNotificationsService, showEventFilters: false, + showHostIsolationExceptions: false, showTrustedApps: false, }) ); @@ -421,6 +506,7 @@ describe('useExceptionLists', () => { namespaceTypes: ['single', 'agnostic'], notifications: mockKibanaNotificationsService, showEventFilters: false, + showHostIsolationExceptions: false, showTrustedApps: false, }) ); diff --git a/x-pack/plugins/maps/server/index.ts b/x-pack/plugins/maps/server/index.ts index 4e5dfcf1f94c4..6a846bc245824 100644 --- a/x-pack/plugins/maps/server/index.ts +++ b/x-pack/plugins/maps/server/index.ts @@ -34,6 +34,7 @@ export const config: PluginConfigDescriptor = { return completeConfig; } addDeprecation({ + configPath: 'xpack.maps.showMapVisualizationTypes', message: i18n.translate('xpack.maps.deprecation.showMapVisualizationTypes.message', { defaultMessage: 'xpack.maps.showMapVisualizationTypes is deprecated and is no longer used', @@ -59,6 +60,7 @@ export const config: PluginConfigDescriptor = { return completeConfig; } addDeprecation({ + configPath: 'map.proxyElasticMapsServiceInMaps', documentationUrl: `https://www.elastic.co/guide/en/kibana/${branch}/maps-connect-to-ems.html#elastic-maps-server`, message: i18n.translate('xpack.maps.deprecation.proxyEMS.message', { defaultMessage: 'map.proxyElasticMapsServiceInMaps is deprecated and is no longer used', @@ -86,6 +88,7 @@ export const config: PluginConfigDescriptor = { return completeConfig; } addDeprecation({ + configPath: 'map.regionmap', message: i18n.translate('xpack.maps.deprecation.regionmap.message', { defaultMessage: 'map.regionmap is deprecated and is no longer used', }), diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json index 123232935df05..f8feaef3be5f8 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/manifest.json @@ -1,29 +1,29 @@ { "id": "apm_transaction", "title": "APM", - "description": "Detect anomalies in transactions from your APM services for metric data.", + "description": "Detect anomalies in transactions from your APM services.", "type": "Transaction data", "logoFile": "logo.json", - "defaultIndexPattern": "apm-*-metric,metrics-apm*", + "defaultIndexPattern": "apm-*-transaction", "query": { "bool": { "filter": [ - { "term": { "processor.event": "metric" } }, - { "term": { "metricset.name": "transaction" } } + { "term": { "processor.event": "transaction" } }, + { "exists": { "field": "transaction.duration" } } ] } }, "jobs": [ { - "id": "apm_metrics", - "file": "apm_metrics.json" + "id": "high_mean_transaction_duration", + "file": "high_mean_transaction_duration.json" } ], "datafeeds": [ { - "id": "datafeed-apm_metrics", - "file": "datafeed_apm_metrics.json", - "job_id": "apm_metrics" + "id": "datafeed-high_mean_transaction_duration", + "file": "datafeed_high_mean_transaction_duration.json", + "job_id": "high_mean_transaction_duration" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_metrics.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_metrics.json deleted file mode 100644 index d5092f3ffc553..0000000000000 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/apm_metrics.json +++ /dev/null @@ -1,53 +0,0 @@ -{ - "job_type": "anomaly_detector", - "groups": [ - "apm" - ], - "description": "Detects anomalies in transaction duration, throughput and error percentage for metric data.", - "analysis_config": { - "bucket_span": "15m", - "summary_count_field_name" : "doc_count", - "detectors" : [ - { - "detector_description" : "high duration by transaction type for an APM service", - "function" : "high_mean", - "field_name" : "transaction_duration", - "by_field_name" : "transaction.type", - "partition_field_name" : "service.name" - }, - { - "detector_description" : "transactions per minute for an APM service", - "function" : "mean", - "field_name" : "transactions_per_min", - "by_field_name" : "transaction.type", - "partition_field_name" : "service.name" - }, - { - "detector_description" : "percent failed for an APM service", - "function" : "high_mean", - "field_name" : "transaction_failure_percentage", - "by_field_name" : "transaction.type", - "partition_field_name" : "service.name" - } - ], - "influencers" : [ - "transaction.type", - "service.name" - ] - }, - "analysis_limits": { - "model_memory_limit": "32mb" - }, - "data_description": { - "time_field" : "@timestamp", - "time_format" : "epoch_ms" - }, - "model_plot_config": { - "enabled" : true, - "annotations_enabled" : true - }, - "results_index_name" : "custom-apm", - "custom_settings": { - "created_by": "ml-module-apm-transaction" - } -} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_metrics.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_metrics.json deleted file mode 100644 index ba45582252cd7..0000000000000 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_apm_metrics.json +++ /dev/null @@ -1,95 +0,0 @@ -{ - "job_id": "JOB_ID", - "indices": [ - "INDEX_PATTERN_NAME" - ], - "chunking_config" : { - "mode" : "off" - }, - "query": { - "bool": { - "filter": [ - { "term": { "processor.event": "metric" } }, - { "term": { "metricset.name": "transaction" } } - ] - } - }, - "aggregations" : { - "buckets" : { - "composite" : { - "size" : 5000, - "sources" : [ - { - "date" : { - "date_histogram" : { - "field" : "@timestamp", - "fixed_interval" : "90s" - } - } - }, - { - "transaction.type" : { - "terms" : { - "field" : "transaction.type" - } - } - }, - { - "service.name" : { - "terms" : { - "field" : "service.name" - } - } - } - ] - }, - "aggs" : { - "@timestamp" : { - "max" : { - "field" : "@timestamp" - } - }, - "transactions_per_min" : { - "rate" : { - "unit" : "minute" - } - }, - "transaction_duration" : { - "avg" : { - "field" : "transaction.duration.histogram" - } - }, - "error_count" : { - "filter" : { - "term" : { - "event.outcome" : "failure" - } - }, - "aggs" : { - "actual_error_count" : { - "value_count" : { - "field" : "event.outcome" - } - } - } - }, - "success_count" : { - "filter" : { - "term" : { - "event.outcome" : "success" - } - } - }, - "transaction_failure_percentage" : { - "bucket_script" : { - "buckets_path" : { - "failure_count" : "error_count>_count", - "success_count" : "success_count>_count" - }, - "script" : "if ((params.failure_count + params.success_count)==0){return 0;}else{return params.failure_count/(params.failure_count + params.success_count);}" - } - } - } - } - } -} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json new file mode 100644 index 0000000000000..d312577902f51 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/datafeed_high_mean_transaction_duration.json @@ -0,0 +1,14 @@ +{ + "job_id": "JOB_ID", + "indices": [ + "INDEX_PATTERN_NAME" + ], + "query": { + "bool": { + "filter": [ + { "term": { "processor.event": "transaction" } }, + { "exists": { "field": "transaction.duration.us" } } + ] + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json new file mode 100644 index 0000000000000..77284cb275cd8 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/apm_transaction/ml/high_mean_transaction_duration.json @@ -0,0 +1,35 @@ +{ + "job_type": "anomaly_detector", + "groups": [ + "apm" + ], + "description": "Detect transaction duration anomalies across transaction types for your APM services.", + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "high duration by transaction type for an APM service", + "function": "high_mean", + "field_name": "transaction.duration.us", + "by_field_name": "transaction.type", + "partition_field_name": "service.name" + } + ], + "influencers": [ + "transaction.type", + "service.name" + ] + }, + "analysis_limits": { + "model_memory_limit": "32mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "model_plot_config": { + "enabled": true + }, + "custom_settings": { + "created_by": "ml-module-apm-transaction" + } +} diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/beats_template.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/beats_template.tsx index 3bab01af8ceb7..7a070c735bbea 100644 --- a/x-pack/plugins/monitoring/public/application/pages/beats/beats_template.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/beats/beats_template.tsx @@ -23,6 +23,7 @@ export const BeatsTemplate: React.FC = ({ instance, ...props defaultMessage: 'Overview', }), route: '/beats', + testSubj: 'beatsOverviewPage', }); tabs.push({ id: 'instances', @@ -30,6 +31,7 @@ export const BeatsTemplate: React.FC = ({ instance, ...props defaultMessage: 'Instances', }), route: '/beats/beats', + testSubj: 'beatsListingPage', }); } else { tabs.push({ diff --git a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx index 489ad110c40fd..4611f17159621 100644 --- a/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/beats/instances.tsx @@ -71,12 +71,7 @@ export const BeatsInstancesPage: React.FC = ({ clusters }) => { ]); return ( - +
= ({ clusters }) => { }; return ( - -
{renderOverview(data)}
+ +
{renderOverview(data)}
); }; diff --git a/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx b/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx index 26072f53f4752..e798e7d74ad38 100644 --- a/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx +++ b/x-pack/plugins/monitoring/public/application/pages/no_data/no_data_page.tsx @@ -220,12 +220,10 @@ async function executeCheck(checker: SettingsChecker, http: { fetch: any }): Pro return { found, reason }; } catch (err: any) { - const { data } = err; - return { error: true, found: false, - errorReason: data, + errorReason: err.body, }; } } diff --git a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap index b6d117fb3269c..8852d104fe00a 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/__snapshots__/no_data.test.js.snap @@ -3,7 +3,13 @@ exports[`NoData should show a default message if reason is unknown 1`] = `
+

+ No monitoring data found. +

+

+ No monitoring data found. +

{ + return {children}; + }; + if (isCloudEnabled) { return ( - +

- + ); } if (useInternalCollection) { return ( - + + +

+ +

+
-
+ ); } return ( - + + +

+ +

+
-
+ ); } diff --git a/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/we_tried.test.js.snap b/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/we_tried.test.js.snap index 482e6e97115b7..4e1209a5fd2aa 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/we_tried.test.js.snap +++ b/x-pack/plugins/monitoring/public/components/no_data/reasons/__snapshots__/we_tried.test.js.snap @@ -1,15 +1,17 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`WeTried should render "we tried" message 1`] = ` -Array [ +

We couldn't activate monitoring -

, +


, + />
@@ -19,6 +21,6 @@ Array [

If data is in your cluster, your monitoring dashboards will show up here.

-
, -] +
+
`; diff --git a/x-pack/plugins/monitoring/public/components/no_data/reasons/we_tried.js b/x-pack/plugins/monitoring/public/components/no_data/reasons/we_tried.js index 17e171451e3a3..37504f5842a74 100644 --- a/x-pack/plugins/monitoring/public/components/no_data/reasons/we_tried.js +++ b/x-pack/plugins/monitoring/public/components/no_data/reasons/we_tried.js @@ -5,13 +5,13 @@ * 2.0. */ -import React, { Fragment } from 'react'; +import React from 'react'; import { EuiText, EuiHorizontalRule, EuiTitle } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; export function WeTried() { return ( - +

- +

); } diff --git a/x-pack/plugins/monitoring/server/config.test.ts b/x-pack/plugins/monitoring/server/config.test.ts index 5e02767fea98a..4fe5d9c765734 100644 --- a/x-pack/plugins/monitoring/server/config.test.ts +++ b/x-pack/plugins/monitoring/server/config.test.ts @@ -107,7 +107,7 @@ describe('config schema', () => { "index": "metricbeat-*", }, "min_interval_seconds": 10, - "render_react_app": false, + "render_react_app": true, "show_license_expiration": true, }, } diff --git a/x-pack/plugins/monitoring/server/config.ts b/x-pack/plugins/monitoring/server/config.ts index 5c2bdc1424f93..ddbfd480a9f4e 100644 --- a/x-pack/plugins/monitoring/server/config.ts +++ b/x-pack/plugins/monitoring/server/config.ts @@ -51,7 +51,7 @@ export const configSchema = schema.object({ }), min_interval_seconds: schema.number({ defaultValue: 10 }), show_license_expiration: schema.boolean({ defaultValue: true }), - render_react_app: schema.boolean({ defaultValue: false }), + render_react_app: schema.boolean({ defaultValue: true }), }), kibana: schema.object({ collection: schema.object({ diff --git a/x-pack/plugins/monitoring/server/deprecations.ts b/x-pack/plugins/monitoring/server/deprecations.ts index 3554abd569581..9dec7b105f2f6 100644 --- a/x-pack/plugins/monitoring/server/deprecations.ts +++ b/x-pack/plugins/monitoring/server/deprecations.ts @@ -55,6 +55,7 @@ export const deprecations = ({ const legacyKey = get(config, `xpack.monitoring.${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}`); if (emailNotificationsEnabled && !updatedKey && !legacyKey) { addDeprecation({ + configPath: `cluster_alerts.email_notifications.enabled`, message: `Config key [${fromPath}.${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}] will be required for email notifications to work in 8.0."`, correctiveActions: { manualSteps: [ @@ -70,6 +71,7 @@ export const deprecations = ({ if (es) { if (es.username === 'elastic') { addDeprecation({ + configPath: 'elasticsearch.username', message: `Setting [${fromPath}.username] to "elastic" is deprecated. You should use the "kibana_system" user instead.`, correctiveActions: { manualSteps: [`Replace [${fromPath}.username] from "elastic" to "kibana_system".`], @@ -77,6 +79,7 @@ export const deprecations = ({ }); } else if (es.username === 'kibana') { addDeprecation({ + configPath: 'elasticsearch.username', message: `Setting [${fromPath}.username] to "kibana" is deprecated. You should use the "kibana_system" user instead.`, correctiveActions: { manualSteps: [`Replace [${fromPath}.username] from "kibana" to "kibana_system".`], @@ -91,6 +94,7 @@ export const deprecations = ({ if (ssl) { if (ssl.key !== undefined && ssl.certificate === undefined) { addDeprecation({ + configPath: 'elasticsearch.ssl.key', message: `Setting [${fromPath}.key] without [${fromPath}.certificate] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, correctiveActions: { manualSteps: [ @@ -100,6 +104,7 @@ export const deprecations = ({ }); } else if (ssl.certificate !== undefined && ssl.key === undefined) { addDeprecation({ + configPath: 'elasticsearch.ssl.certificate', message: `Setting [${fromPath}.certificate] without [${fromPath}.key] is deprecated. This has no effect, you should use both settings to enable TLS client authentication to Elasticsearch.`, correctiveActions: { manualSteps: [ diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index a45c084131a4d..1eeafb4e0c513 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -25,6 +25,7 @@ export const config: PluginConfigDescriptor = { const reporting = get(settings, fromPath); if (reporting?.index) { addDeprecation({ + configPath: `${fromPath}.index`, title: i18n.translate('xpack.reporting.deprecations.reportingIndex.title', { defaultMessage: 'Setting "{fromPath}.index" is deprecated', values: { fromPath }, @@ -51,6 +52,7 @@ export const config: PluginConfigDescriptor = { if (reporting?.roles?.enabled !== false) { addDeprecation({ + configPath: `${fromPath}.roles.enabled`, level: 'warning', title: i18n.translate('xpack.reporting.deprecations.reportingRoles.title', { defaultMessage: 'Setting "{fromPath}.roles" is deprecated', diff --git a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts index 4303de6a3ef56..b5258d91485f7 100644 --- a/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/decrypt_job_headers.test.ts @@ -25,7 +25,7 @@ describe('headers', () => { logger ); await expect(getDecryptedHeaders()).rejects.toMatchInlineSnapshot( - `[Error: Failed to decrypt report job data. Please ensure that xpack.reporting.encryptionKey is set and re-generate this report. Error: Invalid IV length]` + `[Error: Failed to decrypt report job data. Please ensure that xpack.reporting.encryptionKey is set and re-generate this report. TypeError: Invalid initialization vector]` ); }); diff --git a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts index 5032eaab46e84..b045f4872fcb0 100644 --- a/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/csv/execute_job.test.ts @@ -303,7 +303,9 @@ describe('CSV Execute Job', function () { }); await expect( runTask('job123', jobParams, cancellationToken, stream) - ).rejects.toMatchInlineSnapshot(`[TypeError: Cannot read property 'indexOf' of undefined]`); + ).rejects.toMatchInlineSnapshot( + `[TypeError: Cannot read properties of undefined (reading 'indexOf')]` + ); expect(mockEsClient.clearScroll).toHaveBeenCalledWith( expect.objectContaining({ body: { scroll_id: lastScrollId } }) diff --git a/x-pack/plugins/security/server/config_deprecations.test.ts b/x-pack/plugins/security/server/config_deprecations.test.ts index 634df081d77d7..d4b18a2e1b296 100644 --- a/x-pack/plugins/security/server/config_deprecations.test.ts +++ b/x-pack/plugins/security/server/config_deprecations.test.ts @@ -17,6 +17,7 @@ const deprecationContext = configDeprecationsMock.createContext(); const applyConfigDeprecations = (settings: Record = {}) => { const deprecations = securityConfigDeprecationProvider(configDeprecationFactory); const deprecationMessages: string[] = []; + const configPaths: string[] = []; const { config: migrated } = applyDeprecations( settings, deprecations.map((deprecation) => ({ @@ -25,10 +26,13 @@ const applyConfigDeprecations = (settings: Record = {}) => { context: deprecationContext, })), () => - ({ message }) => - deprecationMessages.push(message) + ({ message, configPath }) => { + deprecationMessages.push(message); + configPaths.push(configPath); + } ); return { + configPaths, messages: deprecationMessages, migrated, }; @@ -356,12 +360,14 @@ describe('Config Deprecations', () => { }, }, }; - const { messages } = applyConfigDeprecations(cloneDeep(config)); + const { messages, configPaths } = applyConfigDeprecations(cloneDeep(config)); expect(messages).toMatchInlineSnapshot(` Array [ "\\"xpack.security.authc.providers.saml..maxRedirectURLSize\\" is no longer used.", ] `); + + expect(configPaths).toEqual(['xpack.security.authc.providers.saml.saml1.maxRedirectURLSize']); }); it(`warns when 'xpack.security.authc.providers' is an array of strings`, () => { diff --git a/x-pack/plugins/security/server/config_deprecations.ts b/x-pack/plugins/security/server/config_deprecations.ts index ce9eb76fb1dc8..c8c8e64648c4b 100644 --- a/x-pack/plugins/security/server/config_deprecations.ts +++ b/x-pack/plugins/security/server/config_deprecations.ts @@ -35,6 +35,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ const legacyAuditLoggerEnabled = !settings?.xpack?.security?.audit?.appender; if (auditLoggingEnabled && legacyAuditLoggerEnabled) { addDeprecation({ + configPath: 'xpack.security.audit.appender', title: i18n.translate('xpack.security.deprecations.auditLoggerTitle', { defaultMessage: 'The legacy audit logger is deprecated', }), @@ -59,6 +60,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ (settings, fromPath, addDeprecation) => { if (Array.isArray(settings?.xpack?.security?.authc?.providers)) { addDeprecation({ + configPath: 'xpack.security.authc.providers', title: i18n.translate('xpack.security.deprecations.authcProvidersTitle', { defaultMessage: 'Defining "xpack.security.authc.providers" as an array of provider types is deprecated', @@ -92,6 +94,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ if (hasProviderType('basic') && hasProviderType('token')) { addDeprecation({ + configPath: 'xpack.security.authc.providers', title: i18n.translate('xpack.security.deprecations.basicAndTokenProvidersTitle', { defaultMessage: 'Both "basic" and "token" authentication providers are enabled in "xpack.security.authc.providers"', @@ -119,8 +122,13 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ string, any >; - if (Object.values(samlProviders).find((provider) => !!provider.maxRedirectURLSize)) { + + const foundProvider = Object.entries(samlProviders).find( + ([_, provider]) => !!provider.maxRedirectURLSize + ); + if (foundProvider) { addDeprecation({ + configPath: `xpack.security.authc.providers.saml.${foundProvider[0]}.maxRedirectURLSize`, title: i18n.translate('xpack.security.deprecations.maxRedirectURLSizeTitle', { defaultMessage: '"xpack.security.authc.providers.saml..maxRedirectURLSize" is deprecated', @@ -143,6 +151,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ (settings, fromPath, addDeprecation, { branch }) => { if ('enabled' in (settings?.xpack?.security || {})) { addDeprecation({ + configPath: 'xpack.security.enabled', title: i18n.translate('xpack.security.deprecations.enabledTitle', { defaultMessage: 'Setting "xpack.security.enabled" is deprecated', }), @@ -169,6 +178,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ (settings, fromPath, addDeprecation, { branch }) => { if (settings?.xpack?.security?.session?.idleTimeout === undefined) { addDeprecation({ + configPath: 'xpack.security.session.idleTimeout', level: 'warning', title: i18n.translate('xpack.security.deprecations.idleTimeoutTitle', { defaultMessage: 'The "xpack.security.session.idleTimeout" default is changing', @@ -192,6 +202,7 @@ export const securityConfigDeprecationProvider: ConfigDeprecationProvider = ({ if (settings?.xpack?.security?.session?.lifespan === undefined) { addDeprecation({ + configPath: 'xpack.security.session.lifespan', level: 'warning', title: i18n.translate('xpack.security.deprecations.lifespanTitle', { defaultMessage: 'The "xpack.security.session.lifespan" default is changing', diff --git a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts index 942aed4166595..2da3a604478fc 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/policy_config.ts @@ -75,6 +75,10 @@ export const policyFactory = (): PolicyConfig => { mode: ProtectionModes.prevent, supported: true, }, + memory_protection: { + mode: ProtectionModes.prevent, + supported: true, + }, popup: { malware: { message: '', @@ -84,6 +88,10 @@ export const policyFactory = (): PolicyConfig => { message: '', enabled: true, }, + memory_protection: { + message: '', + enabled: true, + }, }, logging: { file: 'info', @@ -102,6 +110,10 @@ export const policyFactory = (): PolicyConfig => { mode: ProtectionModes.prevent, supported: true, }, + memory_protection: { + mode: ProtectionModes.prevent, + supported: true, + }, popup: { malware: { message: '', @@ -111,6 +123,10 @@ export const policyFactory = (): PolicyConfig => { message: '', enabled: true, }, + memory_protection: { + message: '', + enabled: true, + }, }, logging: { file: 'info', @@ -167,12 +183,20 @@ export const policyFactoryWithoutPaidFeatures = ( mode: ProtectionModes.off, supported: false, }, + memory_protection: { + mode: ProtectionModes.off, + supported: false, + }, popup: { ...policy.mac.popup, malware: { message: '', enabled: true, }, + memory_protection: { + message: '', + enabled: false, + }, behavior_protection: { message: '', enabled: false, @@ -185,12 +209,20 @@ export const policyFactoryWithoutPaidFeatures = ( mode: ProtectionModes.off, supported: false, }, + memory_protection: { + mode: ProtectionModes.off, + supported: false, + }, popup: { ...policy.linux.popup, malware: { message: '', enabled: true, }, + memory_protection: { + message: '', + enabled: false, + }, behavior_protection: { message: '', enabled: false, @@ -229,6 +261,10 @@ export const policyFactoryWithSupportedFeatures = ( ...policy.windows.behavior_protection, supported: true, }, + memory_protection: { + ...policy.mac.memory_protection, + supported: true, + }, }, linux: { ...policy.linux, @@ -236,6 +272,10 @@ export const policyFactoryWithSupportedFeatures = ( ...policy.windows.behavior_protection, supported: true, }, + memory_protection: { + ...policy.linux.memory_protection, + supported: true, + }, }, }; }; diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 297b1d2442c78..2fee3e4c39d1d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -942,6 +942,7 @@ export interface PolicyConfig { }; malware: ProtectionFields; behavior_protection: ProtectionFields & SupportedFields; + memory_protection: ProtectionFields & SupportedFields; popup: { malware: { message: string; @@ -951,6 +952,10 @@ export interface PolicyConfig { message: string; enabled: boolean; }; + memory_protection: { + message: string; + enabled: boolean; + }; }; logging: { file: string; @@ -965,6 +970,7 @@ export interface PolicyConfig { }; malware: ProtectionFields; behavior_protection: ProtectionFields & SupportedFields; + memory_protection: ProtectionFields & SupportedFields; popup: { malware: { message: string; @@ -974,6 +980,10 @@ export interface PolicyConfig { message: string; enabled: boolean; }; + memory_protection: { + message: string; + enabled: boolean; + }; }; logging: { file: string; @@ -1004,14 +1014,14 @@ export interface UIPolicyConfig { */ mac: Pick< PolicyConfig['mac'], - 'malware' | 'events' | 'popup' | 'advanced' | 'behavior_protection' + 'malware' | 'events' | 'popup' | 'advanced' | 'behavior_protection' | 'memory_protection' >; /** * Linux-specific policy configuration that is supported via the UI */ linux: Pick< PolicyConfig['linux'], - 'malware' | 'events' | 'popup' | 'advanced' | 'behavior_protection' + 'malware' | 'events' | 'popup' | 'advanced' | 'behavior_protection' | 'memory_protection' >; } diff --git a/x-pack/plugins/security_solution/common/license/policy_config.test.ts b/x-pack/plugins/security_solution/common/license/policy_config.test.ts index 725d6ba74afd0..e08a096be82ef 100644 --- a/x-pack/plugins/security_solution/common/license/policy_config.test.ts +++ b/x-pack/plugins/security_solution/common/license/policy_config.test.ts @@ -84,6 +84,10 @@ describe('policy_config and licenses', () => { // memory protection policy.windows.memory_protection.mode = ProtectionModes.prevent; policy.windows.memory_protection.supported = true; + policy.mac.memory_protection.mode = ProtectionModes.prevent; + policy.mac.memory_protection.supported = true; + policy.linux.memory_protection.mode = ProtectionModes.prevent; + policy.linux.memory_protection.supported = true; // behavior protection policy.windows.behavior_protection.mode = ProtectionModes.prevent; policy.windows.behavior_protection.supported = true; @@ -104,6 +108,10 @@ describe('policy_config and licenses', () => { // memory protection policy.windows.popup.memory_protection.enabled = true; policy.windows.memory_protection.supported = true; + policy.mac.popup.memory_protection.enabled = true; + policy.mac.memory_protection.supported = true; + policy.linux.popup.memory_protection.enabled = true; + policy.linux.memory_protection.supported = true; // behavior protection policy.windows.popup.behavior_protection.enabled = true; policy.windows.behavior_protection.supported = true; @@ -157,6 +165,8 @@ describe('policy_config and licenses', () => { it('blocks memory_protection to be turned on for Gold and below licenses', () => { const policy = policyFactoryWithoutPaidFeatures(); policy.windows.memory_protection.mode = ProtectionModes.prevent; + policy.mac.memory_protection.mode = ProtectionModes.prevent; + policy.linux.memory_protection.mode = ProtectionModes.prevent; let valid = isEndpointPolicyValidForLicense(policy, Gold); expect(valid).toBeFalsy(); @@ -167,6 +177,9 @@ describe('policy_config and licenses', () => { it('blocks memory_protection notification to be turned on for Gold and below licenses', () => { const policy = policyFactoryWithoutPaidFeatures(); policy.windows.popup.memory_protection.enabled = true; + policy.mac.popup.memory_protection.enabled = true; + policy.linux.popup.memory_protection.enabled = true; + let valid = isEndpointPolicyValidForLicense(policy, Gold); expect(valid).toBeFalsy(); @@ -177,6 +190,8 @@ describe('policy_config and licenses', () => { it('allows memory_protection notification message changes with a Platinum license', () => { const policy = policyFactory(); policy.windows.popup.memory_protection.message = 'BOOM'; + policy.mac.popup.memory_protection.message = 'BOOM'; + policy.linux.popup.memory_protection.message = 'BOOM'; const valid = isEndpointPolicyValidForLicense(policy, Platinum); expect(valid).toBeTruthy(); }); @@ -184,6 +199,8 @@ describe('policy_config and licenses', () => { it('blocks memory_protection notification message changes for Gold and below licenses', () => { const policy = policyFactory(); policy.windows.popup.memory_protection.message = 'BOOM'; + policy.mac.popup.memory_protection.message = 'BOOM'; + policy.linux.popup.memory_protection.message = 'BOOM'; let valid = isEndpointPolicyValidForLicense(policy, Gold); expect(valid).toBeFalsy(); @@ -280,10 +297,26 @@ describe('policy_config and licenses', () => { policy.windows.popup.memory_protection.enabled = false; policy.windows.popup.memory_protection.message = popupMessage; + policy.linux.memory_protection.mode = ProtectionModes.detect; + policy.linux.popup.memory_protection.enabled = false; + policy.linux.popup.memory_protection.message = popupMessage; + + policy.mac.memory_protection.mode = ProtectionModes.detect; + policy.mac.popup.memory_protection.enabled = false; + policy.mac.popup.memory_protection.message = popupMessage; + const retPolicy = unsetPolicyFeaturesAccordingToLicenseLevel(policy, Platinum); expect(retPolicy.windows.memory_protection.mode).toEqual(ProtectionModes.detect); expect(retPolicy.windows.popup.memory_protection.enabled).toBeFalsy(); expect(retPolicy.windows.popup.memory_protection.message).toEqual(popupMessage); + + expect(retPolicy.linux.memory_protection.mode).toEqual(ProtectionModes.detect); + expect(retPolicy.linux.popup.memory_protection.enabled).toBeFalsy(); + expect(retPolicy.linux.popup.memory_protection.message).toEqual(popupMessage); + + expect(retPolicy.mac.memory_protection.mode).toEqual(ProtectionModes.detect); + expect(retPolicy.mac.popup.memory_protection.enabled).toBeFalsy(); + expect(retPolicy.mac.popup.memory_protection.message).toEqual(popupMessage); }); it('does not change any behavior fields with a Platinum license', () => { @@ -356,6 +389,8 @@ describe('policy_config and licenses', () => { const policy = policyFactory(); // what we will modify, and should be reset const popupMessage = 'WOOP WOOP'; policy.windows.popup.memory_protection.message = popupMessage; + policy.mac.popup.memory_protection.message = popupMessage; + policy.linux.popup.memory_protection.message = popupMessage; const retPolicy = unsetPolicyFeaturesAccordingToLicenseLevel(policy, Gold); @@ -367,10 +402,28 @@ describe('policy_config and licenses', () => { ); expect(retPolicy.windows.popup.memory_protection.message).not.toEqual(popupMessage); + expect(retPolicy.mac.memory_protection.mode).toEqual(defaults.mac.memory_protection.mode); + expect(retPolicy.mac.popup.memory_protection.enabled).toEqual( + defaults.mac.popup.memory_protection.enabled + ); + expect(retPolicy.mac.popup.memory_protection.message).not.toEqual(popupMessage); + + expect(retPolicy.linux.memory_protection.mode).toEqual(defaults.linux.memory_protection.mode); + expect(retPolicy.linux.popup.memory_protection.enabled).toEqual( + defaults.linux.popup.memory_protection.enabled + ); + expect(retPolicy.linux.popup.memory_protection.message).not.toEqual(popupMessage); + // need to invert the test, since it could be either value expect(['', DefaultPolicyRuleNotificationMessage]).toContain( retPolicy.windows.popup.memory_protection.message ); + expect(['', DefaultPolicyRuleNotificationMessage]).toContain( + retPolicy.mac.popup.memory_protection.message + ); + expect(['', DefaultPolicyRuleNotificationMessage]).toContain( + retPolicy.linux.popup.memory_protection.message + ); }); it('resets Platinum-paid behavior_protection fields for lower license tiers', () => { @@ -445,24 +498,40 @@ describe('policy_config and licenses', () => { const defaults = policyFactoryWithoutPaidFeatures(); // reference const policy = policyFactory(); // what we will modify, and should be reset policy.windows.memory_protection.supported = true; + policy.mac.memory_protection.supported = true; + policy.linux.memory_protection.supported = true; const retPolicy = unsetPolicyFeaturesAccordingToLicenseLevel(policy, Gold); expect(retPolicy.windows.memory_protection.supported).toEqual( defaults.windows.memory_protection.supported ); + expect(retPolicy.mac.memory_protection.supported).toEqual( + defaults.mac.memory_protection.supported + ); + expect(retPolicy.linux.memory_protection.supported).toEqual( + defaults.linux.memory_protection.supported + ); }); it('sets memory_protection supported field to true when license is at Platinum', () => { const defaults = policyFactoryWithSupportedFeatures(); // reference const policy = policyFactory(); // what we will modify, and should be reset policy.windows.memory_protection.supported = false; + policy.mac.memory_protection.supported = false; + policy.linux.memory_protection.supported = false; const retPolicy = unsetPolicyFeaturesAccordingToLicenseLevel(policy, Platinum); expect(retPolicy.windows.memory_protection.supported).toEqual( defaults.windows.memory_protection.supported ); + expect(retPolicy.mac.memory_protection.supported).toEqual( + defaults.mac.memory_protection.supported + ); + expect(retPolicy.linux.memory_protection.supported).toEqual( + defaults.linux.memory_protection.supported + ); }); it('sets behavior_protection supported field to false when license is below Platinum', () => { const defaults = policyFactoryWithoutPaidFeatures(); // reference diff --git a/x-pack/plugins/security_solution/common/license/policy_config.ts b/x-pack/plugins/security_solution/common/license/policy_config.ts index a05478eef8eba..7342968a380b1 100644 --- a/x-pack/plugins/security_solution/common/license/policy_config.ts +++ b/x-pack/plugins/security_solution/common/license/policy_config.ts @@ -87,7 +87,9 @@ function isEndpointMemoryPolicyValidForLicense(policy: PolicyConfig, license: IL const defaults = policyFactoryWithSupportedFeatures(); // only platinum or higher may enable memory protection if ( - policy.windows.memory_protection.supported !== defaults.windows.memory_protection.supported + policy.windows.memory_protection.supported !== defaults.windows.memory_protection.supported || + policy.mac.memory_protection.supported !== defaults.mac.memory_protection.supported || + policy.linux.memory_protection.supported !== defaults.linux.memory_protection.supported ) { return false; } @@ -101,25 +103,39 @@ function isEndpointMemoryPolicyValidForLicense(policy: PolicyConfig, license: IL // only platinum or higher may enable memory_protection const defaults = policyFactoryWithoutPaidFeatures(); - if (policy.windows.memory_protection.mode !== defaults.windows.memory_protection.mode) { + if ( + policy.windows.memory_protection.mode !== defaults.windows.memory_protection.mode || + policy.mac.memory_protection.mode !== defaults.mac.memory_protection.mode || + policy.linux.memory_protection.mode !== defaults.linux.memory_protection.mode + ) { return false; } if ( policy.windows.popup.memory_protection.enabled !== - defaults.windows.popup.memory_protection.enabled + defaults.windows.popup.memory_protection.enabled || + policy.mac.popup.memory_protection.enabled !== defaults.mac.popup.memory_protection.enabled || + policy.linux.popup.memory_protection.enabled !== defaults.linux.popup.memory_protection.enabled ) { return false; } if ( - policy.windows.popup.memory_protection.message !== '' && - policy.windows.popup.memory_protection.message !== DefaultPolicyRuleNotificationMessage + (policy.windows.popup.memory_protection.message !== '' && + policy.windows.popup.memory_protection.message !== DefaultPolicyRuleNotificationMessage) || + (policy.mac.popup.memory_protection.message !== '' && + policy.mac.popup.memory_protection.message !== DefaultPolicyRuleNotificationMessage) || + (policy.linux.popup.memory_protection.message !== '' && + policy.linux.popup.memory_protection.message !== DefaultPolicyRuleNotificationMessage) ) { return false; } - if (policy.windows.memory_protection.supported !== defaults.windows.memory_protection.supported) { + if ( + policy.windows.memory_protection.supported !== defaults.windows.memory_protection.supported || + policy.mac.memory_protection.supported !== defaults.mac.memory_protection.supported || + policy.linux.memory_protection.supported !== defaults.linux.memory_protection.supported + ) { return false; } return true; diff --git a/x-pack/plugins/security_solution/cypress/screens/alerts.ts b/x-pack/plugins/security_solution/cypress/screens/alerts.ts index 0a815705f5b21..c9660668f488b 100644 --- a/x-pack/plugins/security_solution/cypress/screens/alerts.ts +++ b/x-pack/plugins/security_solution/cypress/screens/alerts.ts @@ -58,7 +58,7 @@ export const TAKE_ACTION_POPOVER_BTN = '[data-test-subj="selectedShowBulkActions export const TIMELINE_CONTEXT_MENU_BTN = '[data-test-subj="timeline-context-menu-button"]'; -export const ATTACH_ALERT_TO_CASE_BUTTON = '[data-test-subj="attach-alert-to-case-button"]'; +export const ATTACH_ALERT_TO_CASE_BUTTON = '[data-test-subj="add-existing-case-menu-item"]'; export const ALERT_COUNT_TABLE_FIRST_ROW_COUNT = '[data-test-subj="alertsCountTable"] tr:nth-child(1) td:nth-child(2) .euiTableCellContent__text'; diff --git a/x-pack/plugins/security_solution/kibana.json b/x-pack/plugins/security_solution/kibana.json index a76b942e555bc..80613ae12f51b 100644 --- a/x-pack/plugins/security_solution/kibana.json +++ b/x-pack/plugins/security_solution/kibana.json @@ -38,8 +38,7 @@ "lens", "lists", "home", - "telemetry", - "telemetryManagementSection" + "telemetry" ], "server": true, "ui": true, diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.test.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.test.ts index 8a678be0616b9..ba806da195461 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.test.ts @@ -6,7 +6,8 @@ */ import { navTabs } from '../../../app/home/home_navigations'; -import { getTitle } from './helpers'; +import { getTitle, isQueryStateEmpty } from './helpers'; +import { CONSTANTS } from './constants'; describe('Helpers Url_State', () => { describe('getTitle', () => { @@ -31,4 +32,58 @@ describe('Helpers Url_State', () => { expect(result).toEqual(''); }); }); + + describe('isQueryStateEmpty', () => { + test('returns true if queryState is undefined', () => { + const result = isQueryStateEmpty(undefined, CONSTANTS.savedQuery); + expect(result).toBeTruthy(); + }); + + test('returns true if queryState is null', () => { + const result = isQueryStateEmpty(null, CONSTANTS.savedQuery); + expect(result).toBeTruthy(); + }); + + test('returns true if url key is "query" and queryState is empty string', () => { + const result = isQueryStateEmpty({}, CONSTANTS.appQuery); + expect(result).toBeTruthy(); + }); + + test('returns false if url key is "query" and queryState is not empty', () => { + const result = isQueryStateEmpty( + { query: { query: '*:*' }, language: 'kuery' }, + CONSTANTS.appQuery + ); + expect(result).toBeFalsy(); + }); + + test('returns true if url key is "filters" and queryState is empty', () => { + const result = isQueryStateEmpty([], CONSTANTS.filters); + expect(result).toBeTruthy(); + }); + + test('returns false if url key is "filters" and queryState is not empty', () => { + const result = isQueryStateEmpty( + [{ query: { query: '*:*' }, meta: { key: '123' } }], + CONSTANTS.filters + ); + expect(result).toBeFalsy(); + }); + + // TODO: Is this a bug, or intended? + test('returns false if url key is "timeline" and queryState is empty', () => { + const result = isQueryStateEmpty({}, CONSTANTS.timeline); + expect(result).toBeFalsy(); + }); + + test('returns true if url key is "timeline" and queryState id is empty string', () => { + const result = isQueryStateEmpty({ id: '', isOpen: true }, CONSTANTS.timeline); + expect(result).toBeTruthy(); + }); + + test('returns false if url key is "timeline" and queryState is not empty', () => { + const result = isQueryStateEmpty({ id: '123', isOpen: true }, CONSTANTS.timeline); + expect(result).toBeFalsy(); + }); + }); }); diff --git a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts index ba09ed914dc68..5b6bb0400ccdf 100644 --- a/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts +++ b/x-pack/plugins/security_solution/public/common/components/url_state/helpers.ts @@ -199,8 +199,11 @@ export const updateTimerangeUrl = ( return timeRange; }; -export const isQueryStateEmpty = (queryState: ValueUrlState | null, urlKey: KeyUrlState) => - queryState === null || +export const isQueryStateEmpty = ( + queryState: ValueUrlState | undefined | null, + urlKey: KeyUrlState +): boolean => + queryState == null || (urlKey === CONSTANTS.appQuery && isEmpty((queryState as Query).query)) || (urlKey === CONSTANTS.filters && isEmpty(queryState)) || (urlKey === CONSTANTS.timeline && (queryState as TimelineUrl).id === ''); diff --git a/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts b/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts index 4dbcd515db4c5..5d6744de9dbe3 100644 --- a/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts +++ b/x-pack/plugins/security_solution/public/common/lib/telemetry/index.ts @@ -27,14 +27,9 @@ export const track: TrackFn = (type, event, count) => { }; export const initTelemetry = ( - { - usageCollection, - telemetryManagementSection, - }: Pick, + { usageCollection }: Pick, appId: string ) => { - telemetryManagementSection?.toggleSecuritySolutionExample(true); - _track = usageCollection?.reportUiCounter?.bind(null, appId) ?? noop; }; diff --git a/x-pack/plugins/security_solution/public/common/mock/utils.ts b/x-pack/plugins/security_solution/public/common/mock/utils.ts index b1851fd055b33..0bafdc4fad1e8 100644 --- a/x-pack/plugins/security_solution/public/common/mock/utils.ts +++ b/x-pack/plugins/security_solution/public/common/mock/utils.ts @@ -21,11 +21,12 @@ import { mockGlobalState } from './global_state'; import { TimelineState } from '../../timelines/store/timeline/types'; import { defaultHeaders } from '../../timelines/components/timeline/body/column_headers/default_headers'; -interface Global extends NodeJS.Global { +type GlobalThis = typeof globalThis; +interface Global extends GlobalThis { // eslint-disable-next-line @typescript-eslint/no-explicit-any - window?: any; + window: any; // eslint-disable-next-line @typescript-eslint/no-explicit-any - document?: any; + document: any; } export const globalNode: Global = global; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/config.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/config.ts index cb5a23e711974..a835628fae6cf 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/config.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/config.ts @@ -19,6 +19,9 @@ export const alertsStackByOptions: AlertsStackByOption[] = [ { text: 'signal.rule.name', value: 'signal.rule.name' }, { text: 'source.ip', value: 'source.ip' }, { text: 'user.name', value: 'user.name' }, + { text: 'process.name', value: 'process.name' }, + { text: 'file.name', value: 'file.name' }, + { text: 'hash.sha256', value: 'hash.sha256' }, ]; export const DEFAULT_STACK_BY_FIELD = 'signal.rule.name'; diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/types.ts b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/types.ts index 833c05bfc7a79..f561c3f6faa21 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/types.ts +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_kpis/common/types.ts @@ -21,4 +21,7 @@ export type AlertsStackByField = | 'signal.rule.type' | 'signal.rule.name' | 'source.ip' - | 'user.name'; + | 'user.name' + | 'process.name' + | 'file.name' + | 'hash.sha256'; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx index 5c2d5f5d62b5c..8528d64b7261d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/exceptions/exceptions_table.tsx @@ -85,6 +85,7 @@ export const ExceptionListsTable = React.memo(() => { notifications, showTrustedApps: false, showEventFilters: false, + showHostIsolationExceptions: false, }); const [loadingTableInfo, exceptionListsWithRuleRefs, exceptionsListsRef] = useAllExceptionLists({ exceptionLists: exceptions ?? [], diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx index 7167b07c7da5d..774b9463bed69 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/details/index.tsx @@ -300,10 +300,8 @@ const RuleDetailsPageComponent: React.FC = ({ }, [rule, spacesApi]); const getLegacyUrlConflictCallout = useMemo(() => { - const outcome = rule?.outcome; - if (rule != null && spacesApi && outcome === 'conflict') { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const aliasTargetId = rule?.alias_target_id!; // This is always defined if outcome === 'conflict' + if (rule?.alias_target_id != null && spacesApi && rule.outcome === 'conflict') { + const aliasTargetId = rule.alias_target_id; // We have resolved to one rule, but there is another one with a legacy URL associated with this page. Display a // callout with a warning for the user, and provide a way for them to navigate to the other rule. const otherRulePath = `rules/id/${aliasTargetId}${window.location.search}${window.location.hash}`; 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 c96deabfa245a..37b1646319f3f 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 @@ -67,7 +67,7 @@ export const AdministrationListPage: FC diff --git a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx index bde1961dd782d..50500a789fd4e 100644 --- a/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/artifact_entry_card/artifact_entry_card.test.tsx @@ -63,7 +63,8 @@ describe.each([ ); }); - it('should display dates in expected format', () => { + // FLAKY https://github.com/elastic/kibana/issues/113892 + it.skip('should display dates in expected format', () => { render(); expect(renderResult.getByTestId('testCard-header-updated').textContent).toEqual( diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx index ddee8e13f069d..d7db249475df7 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.test.tsx @@ -5,50 +5,78 @@ * 2.0. */ -import { mount } from 'enzyme'; import React from 'react'; +import { act, fireEvent } from '@testing-library/react'; +import { AppContextTestRender, createAppRootMockRenderer } from '../../../common/mock/endpoint'; +import { + EndpointPrivileges, + useEndpointPrivileges, +} from '../../../common/components/user_privileges/use_endpoint_privileges'; +import { EndpointDocGenerator } from '../../../../common/endpoint/generate_data'; -import { SearchExceptions } from '.'; +import { SearchExceptions, SearchExceptionsProps } from '.'; +jest.mock('../../../common/components/user_privileges/use_endpoint_privileges'); let onSearchMock: jest.Mock; +const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; -interface EuiFieldSearchPropsFake { - onSearch(value: string): void; -} - +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 describe('Search exceptions', () => { + let appTestContext: AppContextTestRender; + let renderResult: ReturnType; + let render: ( + props?: Partial + ) => ReturnType; + + const loadedUserEndpointPrivilegesState = ( + endpointOverrides: Partial = {} + ): EndpointPrivileges => ({ + loading: false, + canAccessFleet: true, + canAccessEndpointManagement: true, + isPlatinumPlus: false, + ...endpointOverrides, + }); + beforeEach(() => { onSearchMock = jest.fn(); + appTestContext = createAppRootMockRenderer(); + + render = (overrideProps = {}) => { + const props: SearchExceptionsProps = { + placeholder: 'search test', + onSearch: onSearchMock, + ...overrideProps, + }; + + renderResult = appTestContext.render(); + return renderResult; + }; + + mockUseEndpointPrivileges.mockReturnValue(loadedUserEndpointPrivilegesState()); }); - const getElement = (defaultValue: string = '') => ( - - ); + afterAll(() => { + mockUseEndpointPrivileges.mockReset(); + }); it('should have a default value', () => { const expectedDefaultValue = 'this is a default value'; - const element = mount(getElement(expectedDefaultValue)); - const defaultValue = element - .find('[data-test-subj="searchField"]') - .first() - .props().defaultValue; - expect(defaultValue).toBe(expectedDefaultValue); + const element = render({ defaultValue: expectedDefaultValue }); + + expect(element.getByDisplayValue(expectedDefaultValue)).not.toBeNull(); }); it('should dispatch search action when submit search field', () => { const expectedDefaultValue = 'this is a default value'; - const element = mount(getElement()); + const element = render(); expect(onSearchMock).toHaveBeenCalledTimes(0); - const searchFieldProps = element - .find('[data-test-subj="searchField"]') - .first() - .props() as EuiFieldSearchPropsFake; - searchFieldProps.onSearch(expectedDefaultValue); + act(() => { + fireEvent.change(element.getByTestId('searchField'), { + target: { value: expectedDefaultValue }, + }); + }); expect(onSearchMock).toHaveBeenCalledTimes(1); expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue, '', ''); @@ -56,11 +84,42 @@ describe('Search exceptions', () => { it('should dispatch search action when click on button', () => { const expectedDefaultValue = 'this is a default value'; - const element = mount(getElement(expectedDefaultValue)); + const element = render({ defaultValue: expectedDefaultValue }); expect(onSearchMock).toHaveBeenCalledTimes(0); - element.find('[data-test-subj="searchButton"]').first().simulate('click'); + act(() => { + fireEvent.click(element.getByTestId('searchButton')); + }); + expect(onSearchMock).toHaveBeenCalledTimes(1); expect(onSearchMock).toHaveBeenCalledWith(expectedDefaultValue, '', ''); }); + + it('should hide refresh button', () => { + const element = render({ hideRefreshButton: true }); + + expect(element.queryByTestId('searchButton')).toBeNull(); + }); + + it('should hide policies selector when no license', () => { + const generator = new EndpointDocGenerator('policy-list'); + const policy = generator.generatePolicyPackagePolicy(); + mockUseEndpointPrivileges.mockReturnValue( + loadedUserEndpointPrivilegesState({ isPlatinumPlus: false }) + ); + const element = render({ policyList: [policy], hasPolicyFilter: true }); + + expect(element.queryByTestId('policiesSelectorButton')).toBeNull(); + }); + + it('should display policies selector when right license', () => { + const generator = new EndpointDocGenerator('policy-list'); + const policy = generator.generatePolicyPackagePolicy(); + mockUseEndpointPrivileges.mockReturnValue( + loadedUserEndpointPrivilegesState({ isPlatinumPlus: true }) + ); + const element = render({ policyList: [policy], hasPolicyFilter: true }); + + expect(element.queryByTestId('policiesSelectorButton')).not.toBeNull(); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx index 2b7b2e6b66884..1f3eab5db2947 100644 --- a/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx +++ b/x-pack/plugins/security_solution/public/management/components/search_exceptions/search_exceptions.tsx @@ -19,6 +19,7 @@ export interface SearchExceptionsProps { policyList?: ImmutableArray; defaultExcludedPolicies?: string; defaultIncludedPolicies?: string; + hideRefreshButton?: boolean; onSearch(query: string, includedPolicies?: string, excludedPolicies?: string): void; } @@ -31,6 +32,7 @@ export const SearchExceptions = memo( policyList, defaultIncludedPolicies, defaultExcludedPolicies, + hideRefreshButton = false, }) => { const { isPlatinumPlus } = useEndpointPrivileges(); const [query, setQuery] = useState(defaultValue); @@ -101,13 +103,15 @@ export const SearchExceptions = memo( ) : null} - - - {i18n.translate('xpack.securitySolution.management.search.button', { - defaultMessage: 'Refresh', - })} - - + {!hideRefreshButton ? ( + + + {i18n.translate('xpack.securitySolution.management.search.button', { + defaultMessage: 'Refresh', + })} + + + ) : null} ); } 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 15d0684a2864b..43fa4e104067f 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 @@ -61,7 +61,8 @@ jest.mock('../../../../common/lib/kibana'); type EndpointListStore = Store, Immutable>; -describe('endpoint list middleware', () => { +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 +describe.skip('endpoint list middleware', () => { const getKibanaServicesMock = KibanaServices.get as jest.Mock; let fakeCoreStart: jest.Mocked; let depsStart: DepsStartMock; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts index 4d7ca74ca19f8..eb134c4413ae8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/models/advanced_policy_schema.ts @@ -746,4 +746,48 @@ export const AdvancedPolicySchema: AdvancedPolicySchemaType[] = [ } ), }, + { + key: 'mac.advanced.memory_protection.memory_scan_collect_sample', + first_supported_version: '7.16', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.memory_protection.memory_scan_collect_sample', + { + defaultMessage: + 'Collect 4MB of memory surrounding detected malicious memory regions. Default: false. Enabling this value may significantly increase the amount of data stored in Elasticsearch.', + } + ), + }, + { + key: 'mac.advanced.memory_protection.memory_scan', + first_supported_version: '7.16', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.mac.advanced.memory_protection.memory_scan', + { + defaultMessage: + 'Enable scanning for malicious memory regions as a part of memory protection. Default: true.', + } + ), + }, + { + key: 'linux.advanced.memory_protection.memory_scan_collect_sample', + first_supported_version: '7.16', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.memory_protection.memory_scan_collect_sample', + { + defaultMessage: + 'Collect 4MB of memory surrounding detected malicious memory regions. Default: false. Enabling this value may significantly increase the amount of data stored in Elasticsearch.', + } + ), + }, + { + key: 'linux.advanced.memory_protection.memory_scan', + first_supported_version: '7.16', + documentation: i18n.translate( + 'xpack.securitySolution.endpoint.policy.advanced.linux.advanced.memory_protection.memory_scan', + { + defaultMessage: + 'Enable scanning for malicious memory regions as a part of memory protection. Default: true.', + } + ), + }, ]; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts index e0c4ee2600588..da390fc1187b0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/index.test.ts @@ -314,6 +314,7 @@ describe('policy details: ', () => { events: { process: true, file: true, network: true }, malware: { mode: 'prevent' }, behavior_protection: { mode: 'off', supported: false }, + memory_protection: { mode: 'off', supported: false }, popup: { malware: { enabled: true, @@ -323,6 +324,10 @@ describe('policy details: ', () => { enabled: false, message: '', }, + memory_protection: { + enabled: false, + message: '', + }, }, logging: { file: 'info' }, }, @@ -331,6 +336,7 @@ describe('policy details: ', () => { logging: { file: 'info' }, malware: { mode: 'prevent' }, behavior_protection: { mode: 'off', supported: false }, + memory_protection: { mode: 'off', supported: false }, popup: { malware: { enabled: true, @@ -340,6 +346,10 @@ describe('policy details: ', () => { enabled: false, message: '', }, + memory_protection: { + enabled: false, + message: '', + }, }, }, }, diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts index 5f612f4f4e6f6..784565b5d8e1d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/middleware/policy_settings_middleware.ts @@ -57,6 +57,14 @@ export const policySettingsMiddlewareRunner: MiddlewareRunner = async ( policyItem.inputs[0].config.policy.value.windows.popup.memory_protection.message = DefaultPolicyRuleNotificationMessage; } + if (policyItem.inputs[0].config.policy.value.mac.popup.memory_protection.message === '') { + policyItem.inputs[0].config.policy.value.mac.popup.memory_protection.message = + DefaultPolicyRuleNotificationMessage; + } + if (policyItem.inputs[0].config.policy.value.linux.popup.memory_protection.message === '') { + policyItem.inputs[0].config.policy.value.linux.popup.memory_protection.message = + DefaultPolicyRuleNotificationMessage; + } if ( policyItem.inputs[0].config.policy.value.windows.popup.behavior_protection.message === '' ) { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts index 40d77f5869b67..6729f8094b840 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/store/policy_details/selectors/policy_settings_selectors.ts @@ -154,6 +154,7 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel events: mac.events, malware: mac.malware, behavior_protection: mac.behavior_protection, + memory_protection: mac.memory_protection, popup: mac.popup, }, linux: { @@ -161,6 +162,7 @@ export const policyConfig: (s: PolicyDetailsState) => UIPolicyConfig = createSel events: linux.events, malware: linux.malware, behavior_protection: linux.behavior_protection, + memory_protection: linux.memory_protection, popup: linux.popup, }, }; @@ -220,7 +222,7 @@ export const totalLinuxEvents = (state: PolicyDetailsState): number => { return 0; }; -/** Returns the number of selected liinux eventing configurations */ +/** Returns the number of selected linux eventing configurations */ export const selectedLinuxEvents = (state: PolicyDetailsState): number => { const config = policyConfig(state); if (config) { diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts index 283c3afb573b6..ad06f027542df 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/policy/types.ts @@ -175,8 +175,8 @@ export type PolicyProtection = UIPolicyConfig['windows'], 'malware' | 'ransomware' | 'memory_protection' | 'behavior_protection' > - | keyof Pick - | keyof Pick; + | keyof Pick + | keyof Pick; export type MacPolicyProtection = keyof Pick; diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx index 3292bc0c44cb9..c176ce9cacd43 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_details.test.tsx @@ -23,6 +23,7 @@ describe('Policy Details', () => { const generator = new EndpointDocGenerator(); let history: AppContextTestRender['history']; let coreStart: AppContextTestRender['coreStart']; + let middlewareSpy: AppContextTestRender['middlewareSpy']; let http: typeof coreStart.http; let render: (ui: Parameters[0]) => ReturnType; let policyPackagePolicy: ReturnType; @@ -32,13 +33,20 @@ describe('Policy Details', () => { const appContextMockRenderer = createAppRootMockRenderer(); const AppWrapper = appContextMockRenderer.AppWrapper; - ({ history, coreStart } = appContextMockRenderer); + ({ history, coreStart, middlewareSpy } = appContextMockRenderer); render = (ui) => mount(ui, { wrappingComponent: AppWrapper }); http = coreStart.http; }); describe('when displayed with invalid id', () => { + let releaseApiFailure: () => void; + beforeEach(() => { + http.get.mockImplementation(async () => { + await new Promise((_, reject) => { + releaseApiFailure = reject.bind(null, new Error('policy not found')); + }); + }); history.push(policyDetailsPathUrl); policyView = render(); }); @@ -46,7 +54,19 @@ describe('Policy Details', () => { it('should NOT display timeline', async () => { expect(policyView.find('flyoutOverlay')).toHaveLength(0); }); + + it('should show loader followed by error message', async () => { + expect(policyView.find('EuiLoadingSpinner').length).toBe(1); + releaseApiFailure(); + await middlewareSpy.waitForAction('serverFailedToReturnPolicyDetailsData'); + policyView.update(); + const callout = policyView.find('EuiCallOut'); + expect(callout).toHaveLength(1); + expect(callout.prop('color')).toEqual('danger'); + expect(callout.text()).toEqual('policy not found'); + }); }); + describe('when displayed with valid id', () => { let asyncActions: Promise = Promise.resolve(); 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 660dda6493c39..65308012df080 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 @@ -8,8 +8,14 @@ import React, { useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { useLocation } from 'react-router-dom'; +import { EuiCallOut, EuiLoadingSpinner, EuiPageTemplate } from '@elastic/eui'; import { usePolicyDetailsSelector } from './policy_hooks'; -import { policyDetails, agentStatusSummary } from '../store/policy_details/selectors'; +import { + policyDetails, + agentStatusSummary, + isLoading, + apiError, +} from '../store/policy_details/selectors'; import { AgentsSummary } from './agents_summary'; import { useIsExperimentalFeatureEnabled } from '../../../../common/hooks/use_experimental_features'; import { PolicyTabs } from './tabs'; @@ -33,6 +39,8 @@ export const PolicyDetails = React.memo(() => { const { getAppUrl } = useAppUrl(); // Store values + const loading = usePolicyDetailsSelector(isLoading); + const policyApiError = usePolicyDetailsSelector(apiError); const policyItem = usePolicyDetailsSelector(policyDetails); const policyAgentStatusSummary = usePolicyDetailsSelector(agentStatusSummary); @@ -81,22 +89,48 @@ export const PolicyDetails = React.memo(() => { ); + const pageBody: React.ReactNode = useMemo(() => { + if (loading) { + return ( + + + + ); + } + + if (policyApiError) { + return ( + + + {policyApiError?.message} + + + ); + } + + // TODO: Remove this and related code when removing FF + if (isTrustedAppsByPolicyEnabled) { + return ; + } + + return ; + }, [isTrustedAppsByPolicyEnabled, loading, policyApiError]); + return ( - {isTrustedAppsByPolicyEnabled ? ( - - ) : ( - // TODO: Remove this and related code when removing FF - - )} + {pageBody} ); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx index 87c16e411c702..650bf6115c9d9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.test.tsx @@ -30,7 +30,6 @@ describe('Policy Form Layout', () => { const generator = new EndpointDocGenerator(); let history: AppContextTestRender['history']; let coreStart: AppContextTestRender['coreStart']; - let middlewareSpy: AppContextTestRender['middlewareSpy']; let http: typeof coreStart.http; let render: (ui: Parameters[0]) => ReturnType; let policyPackagePolicy: ReturnType; @@ -40,7 +39,7 @@ describe('Policy Form Layout', () => { const appContextMockRenderer = createAppRootMockRenderer(); const AppWrapper = appContextMockRenderer.AppWrapper; - ({ history, coreStart, middlewareSpy } = appContextMockRenderer); + ({ history, coreStart } = appContextMockRenderer); render = (ui) => mount(ui, { wrappingComponent: AppWrapper }); http = coreStart.http; }); @@ -52,33 +51,6 @@ describe('Policy Form Layout', () => { jest.clearAllMocks(); }); - describe('when displayed with invalid id', () => { - let releaseApiFailure: () => void; - beforeEach(() => { - http.get.mockImplementation(async () => { - await new Promise((_, reject) => { - releaseApiFailure = reject.bind(null, new Error('policy not found')); - }); - }); - history.push(policyDetailsPathUrl); - policyFormLayoutView = render(); - }); - - it('should NOT display timeline', async () => { - expect(policyFormLayoutView.find('flyoutOverlay')).toHaveLength(0); - }); - - it('should show loader followed by error message', async () => { - expect(policyFormLayoutView.find('EuiLoadingSpinner').length).toBe(1); - releaseApiFailure(); - await middlewareSpy.waitForAction('serverFailedToReturnPolicyDetailsData'); - policyFormLayoutView.update(); - const callout = policyFormLayoutView.find('EuiCallOut'); - expect(callout).toHaveLength(1); - expect(callout.prop('color')).toEqual('danger'); - expect(callout.text()).toEqual('policy not found'); - }); - }); describe('when displayed with valid id', () => { let asyncActions: Promise = Promise.resolve(); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx index 4573b15b8fabc..bae2c21242d97 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/components/policy_form_layout.tsx @@ -11,7 +11,6 @@ import { EuiFlexItem, EuiButton, EuiButtonEmpty, - EuiCallOut, EuiLoadingSpinner, EuiBottomBar, EuiSpacer, @@ -27,7 +26,6 @@ import { agentStatusSummary, updateStatus, isLoading, - apiError, } from '../../../store/policy_details/selectors'; import { toMountPoint } from '../../../../../../../../../../src/plugins/kibana_react/public'; @@ -58,7 +56,6 @@ export const PolicyFormLayout = React.memo(() => { const policyAgentStatusSummary = usePolicyDetailsSelector(agentStatusSummary); const policyUpdateStatus = usePolicyDetailsSelector(updateStatus); const isPolicyLoading = usePolicyDetailsSelector(isLoading); - const policyApiError = usePolicyDetailsSelector(apiError); // Local state const [showConfirm, setShowConfirm] = useState(false); @@ -137,13 +134,7 @@ export const PolicyFormLayout = React.memo(() => { if (!policyItem) { return ( - {isPolicyLoading ? ( - - ) : policyApiError ? ( - - {policyApiError?.message} - - ) : null} + {isPolicyLoading ? : null} ); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx index 792664f3e6f25..2f47d52e37bf6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/policy_forms/protections/memory.tsx @@ -23,7 +23,7 @@ import { ProtectionSwitch } from '../components/protection_switch'; * which will configure for all relevant OSes. */ export const MemoryProtection = React.memo(() => { - const OSes: Immutable = [OS.windows]; + const OSes: Immutable = [OS.windows, OS.mac, OS.linux]; const protection = 'memory_protection'; const protectionLabel = i18n.translate( 'xpack.securitySolution.endpoint.policy.protections.memory', @@ -36,7 +36,7 @@ export const MemoryProtection = React.memo(() => { type={i18n.translate('xpack.securitySolution.endpoint.policy.details.memory_protection', { defaultMessage: 'Memory threat', })} - supportedOss={[OperatingSystem.WINDOWS]} + supportedOss={[OperatingSystem.WINDOWS, OperatingSystem.MAC, OperatingSystem.LINUX]} dataTestSubj="memoryProtectionsForm" rightCorner={ diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx index 0ccdf9bcb388d..ee52e1210a481 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/empty/policy_trusted_apps_empty_unassigned.tsx @@ -10,6 +10,7 @@ import { EuiEmptyPrompt, EuiButton, EuiPageTemplate, EuiLink } from '@elastic/eu import { FormattedMessage } from '@kbn/i18n/react'; import { usePolicyDetailsNavigateCallback } from '../../policy_hooks'; import { useGetLinkTo } from './use_policy_trusted_apps_empty_hooks'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; interface CommonProps { policyId: string; @@ -17,6 +18,7 @@ interface CommonProps { } export const PolicyTrustedAppsEmptyUnassigned = memo(({ policyId, policyName }) => { + const { isPlatinumPlus } = useEndpointPrivileges(); const navigateCallback = usePolicyDetailsNavigateCallback(); const { onClickHandler, toRouteUrl } = useGetLinkTo(policyId, policyName); const onClickPrimaryButtonHandler = useCallback( @@ -47,12 +49,21 @@ export const PolicyTrustedAppsEmptyUnassigned = memo(({ policyId, p /> } actions={[ - - - , + ...(isPlatinumPlus + ? [ + + + , + ] + : []), // eslint-disable-next-line @elastic/eui/href-or-on-click { defaultMessage: 'Search trusted applications', } )} + hideRefreshButton /> @@ -218,7 +219,7 @@ export const PolicyTrustedAppsFlyout = React.memo(() => { title={ } /> diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx index 5d5d36d41aaf8..d46775d38834b 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.test.tsx @@ -19,8 +19,20 @@ import { createLoadedResourceState, isLoadedResourceState } from '../../../../.. import { getPolicyDetailsArtifactsListPath } from '../../../../../common/routing'; import { EndpointDocGenerator } from '../../../../../../../common/endpoint/generate_data'; import { policyListApiPathHandlers } from '../../../store/test_mock_utils'; +import { licenseService } from '../../../../../../common/hooks/use_license'; jest.mock('../../../../trusted_apps/service'); +jest.mock('../../../../../../common/hooks/use_license', () => { + const licenseServiceInstance = { + isPlatinumPlus: jest.fn(), + }; + return { + licenseService: licenseServiceInstance, + useLicense: () => { + return licenseServiceInstance; + }, + }; +}); let mockedContext: AppContextTestRender; let waitForAction: MiddlewareActionSpyHelper['waitForAction']; @@ -30,7 +42,8 @@ let coreStart: AppContextTestRender['coreStart']; let http: typeof coreStart.http; const generator = new EndpointDocGenerator(); -describe('Policy trusted apps layout', () => { +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 +describe.skip('Policy trusted apps layout', () => { beforeEach(() => { mockedContext = createAppRootMockRenderer(); http = mockedContext.coreStart.http; @@ -106,4 +119,31 @@ describe('Policy trusted apps layout', () => { expect(component.getByTestId('policyDetailsTrustedAppsCount')).not.toBeNull(); }); + + it('should hide assign button on empty state with unassigned policies when downgraded to a gold or below license', async () => { + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); + const component = render(); + mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234')); + + await waitForAction('assignedTrustedAppsListStateChanged'); + + mockedContext.store.dispatch({ + type: 'policyArtifactsDeosAnyTrustedAppExists', + payload: createLoadedResourceState(true), + }); + expect(component.queryByTestId('assign-ta-button')).toBeNull(); + }); + it('should hide the `Assign trusted applications` button when there is data and the license is downgraded to gold or below', async () => { + (licenseService.isPlatinumPlus as jest.Mock).mockReturnValue(false); + TrustedAppsHttpServiceMock.mockImplementation(() => { + return { + getTrustedAppsList: () => getMockListResponse(), + }; + }); + const component = render(); + mockedContext.history.push(getPolicyDetailsArtifactsListPath('1234')); + + await waitForAction('assignedTrustedAppsListStateChanged'); + expect(component.queryByTestId('assignTrustedAppButton')).toBeNull(); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx index 64e40e330ad2b..2421602f4e5af 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/layout/policy_trusted_apps_layout.tsx @@ -25,6 +25,7 @@ import { import { usePolicyDetailsNavigateCallback, usePolicyDetailsSelector } from '../../policy_hooks'; import { PolicyTrustedAppsFlyout } from '../flyout'; import { PolicyTrustedAppsList } from '../list/policy_trusted_apps_list'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; export const PolicyTrustedAppsLayout = React.memo(() => { const location = usePolicyDetailsSelector(getCurrentArtifactsLocation); @@ -33,6 +34,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => { const policyItem = usePolicyDetailsSelector(policyDetails); const navigateCallback = usePolicyDetailsNavigateCallback(); const hasAssignedTrustedApps = usePolicyDetailsSelector(doesPolicyHaveTrustedApps); + const { isPlatinumPlus } = useEndpointPrivileges(); const showListFlyout = location.show === 'list'; @@ -41,6 +43,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => { navigateCallback({ show: 'list', @@ -88,7 +91,7 @@ export const PolicyTrustedAppsLayout = React.memo(() => { - {assignTrustedAppButton} + {isPlatinumPlus && assignTrustedAppButton} ) : null} { )} - {showListFlyout ? : null} + {isPlatinumPlus && showListFlyout ? : null}
) : null; }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx index ff94e3befe8c8..316b70064d9db 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.test.tsx @@ -21,6 +21,13 @@ import { } from '../../../../../state'; import { fireEvent, within, act, waitFor } from '@testing-library/react'; import { APP_ID } from '../../../../../../../common/constants'; +import { + EndpointPrivileges, + useEndpointPrivileges, +} from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; + +jest.mock('../../../../../../common/components/user_privileges/use_endpoint_privileges'); +const mockUseEndpointPrivileges = useEndpointPrivileges as jest.Mock; describe('when rendering the PolicyTrustedAppsList', () => { // The index (zero based) of the card created by the generator that is policy specific @@ -32,6 +39,16 @@ describe('when rendering the PolicyTrustedAppsList', () => { let mockedApis: ReturnType; let waitForAction: AppContextTestRender['middlewareSpy']['waitForAction']; + const loadedUserEndpointPrivilegesState = ( + endpointOverrides: Partial = {} + ): EndpointPrivileges => ({ + loading: false, + canAccessFleet: true, + canAccessEndpointManagement: true, + isPlatinumPlus: true, + ...endpointOverrides, + }); + const getCardByIndexPosition = (cardIndex: number = 0) => { const card = renderResult.getAllByTestId('policyTrustedAppsGrid-card')[cardIndex]; @@ -66,8 +83,12 @@ describe('when rendering the PolicyTrustedAppsList', () => { ); }; + afterAll(() => { + mockUseEndpointPrivileges.mockReset(); + }); beforeEach(() => { appTestContext = createAppRootMockRenderer(); + mockUseEndpointPrivileges.mockReturnValue(loadedUserEndpointPrivilegesState()); mockedApis = policyDetailsPageAllApiHttpMocks(appTestContext.coreStart.http); appTestContext.setExperimentalFlag({ trustedAppsByPolicyEnabled: true }); @@ -297,4 +318,16 @@ describe('when rendering the PolicyTrustedAppsList', () => { }) ); }); + + it('does not show remove option in actions menu if license is downgraded to gold or below', async () => { + await render(); + mockUseEndpointPrivileges.mockReturnValue( + loadedUserEndpointPrivilegesState({ + isPlatinumPlus: false, + }) + ); + await toggleCardActionMenu(POLICY_SPECIFIC_CARD_INDEX); + + expect(renderResult.queryByTestId('policyTrustedAppsGrid-removeAction')).toBeNull(); + }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx index 5d6c9731c7070..8ab2f5fd465e0 100644 --- a/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/policy/view/trusted_apps/list/policy_trusted_apps_list.tsx @@ -38,6 +38,7 @@ import { ContextMenuItemNavByRouterProps } from '../../../../../components/conte import { ArtifactEntryCollapsibleCardProps } from '../../../../../components/artifact_entry_card'; import { useTestIdGenerator } from '../../../../../components/hooks/use_test_id_generator'; import { RemoveTrustedAppFromPolicyModal } from './remove_trusted_app_from_policy_modal'; +import { useEndpointPrivileges } from '../../../../../../common/components/user_privileges/use_endpoint_privileges'; const DATA_TEST_SUBJ = 'policyTrustedAppsGrid'; @@ -46,6 +47,7 @@ export const PolicyTrustedAppsList = memo(() => { const toasts = useToasts(); const history = useHistory(); const { getAppUrl } = useAppUrl(); + const { isPlatinumPlus } = useEndpointPrivileges(); const policyId = usePolicyDetailsSelector(policyIdFromParams); const hasTrustedApps = usePolicyDetailsSelector(doesPolicyHaveTrustedApps); const isLoading = usePolicyDetailsSelector(isPolicyTrustedAppListLoading); @@ -132,44 +134,50 @@ export const PolicyTrustedAppsList = memo(() => { return byIdPolicies; }, {}); + const fullDetailsAction: ArtifactCardGridCardComponentProps['actions'] = [ + { + icon: 'controlsHorizontal', + children: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.list.viewAction', + { defaultMessage: 'View full details' } + ), + href: getAppUrl({ appId: APP_ID, path: viewUrlPath }), + navigateAppId: APP_ID, + navigateOptions: { path: viewUrlPath }, + 'data-test-subj': getTestId('viewFullDetailsAction'), + }, + ]; const thisTrustedAppCardProps: ArtifactCardGridCardComponentProps = { expanded: Boolean(isCardExpanded[trustedApp.id]), - actions: [ - { - icon: 'controlsHorizontal', - children: i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.list.viewAction', - { defaultMessage: 'View full details' } - ), - href: getAppUrl({ appId: APP_ID, path: viewUrlPath }), - navigateAppId: APP_ID, - navigateOptions: { path: viewUrlPath }, - 'data-test-subj': getTestId('viewFullDetailsAction'), - }, - { - icon: 'trash', - children: i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeAction', - { defaultMessage: 'Remove from policy' } - ), - onClick: () => { - setTrustedAppsForRemoval([trustedApp]); - setShowRemovalModal(true); - }, - disabled: isGlobal, - toolTipContent: isGlobal - ? i18n.translate( - 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeActionNotAllowed', - { - defaultMessage: - 'Globally applied trusted applications cannot be removed from policy.', - } - ) - : undefined, - toolTipPosition: 'top', - 'data-test-subj': getTestId('removeAction'), - }, - ], + actions: isPlatinumPlus + ? [ + ...fullDetailsAction, + { + icon: 'trash', + children: i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeAction', + { defaultMessage: 'Remove from policy' } + ), + onClick: () => { + setTrustedAppsForRemoval([trustedApp]); + setShowRemovalModal(true); + }, + disabled: isGlobal, + toolTipContent: isGlobal + ? i18n.translate( + 'xpack.securitySolution.endpoint.policy.trustedApps.list.removeActionNotAllowed', + { + defaultMessage: + 'Globally applied trusted applications cannot be removed from policy.', + } + ) + : undefined, + toolTipPosition: 'top', + 'data-test-subj': getTestId('removeAction'), + }, + ] + : fullDetailsAction, + policies: assignedPoliciesMenuItems, }; @@ -177,7 +185,7 @@ export const PolicyTrustedAppsList = memo(() => { } return newCardProps; - }, [allPoliciesById, getAppUrl, getTestId, isCardExpanded, trustedAppItems]); + }, [allPoliciesById, getAppUrl, getTestId, isCardExpanded, trustedAppItems, isPlatinumPlus]); const provideCardProps = useCallback['cardComponentProps']>( (item) => { diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 371b68e9bec8e..4885538269264 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -97,7 +97,6 @@ export class Plugin implements IPlugin { + const defaultProps = { + eventType: 'all' as TimelineEventsType, + onChangeEventTypeAndIndexesName: jest.fn(), + }; + const initialPatterns = [ + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline].selectedPatterns, + mockGlobalState.sourcerer.signalIndexName, + ]; + const { storage } = createSecuritySolutionStorageMock(); + const state = { + ...mockGlobalState, + sourcerer: { + ...mockGlobalState.sourcerer, + kibanaIndexPatterns: [ + { id: '1234', title: 'auditbeat-*' }, + { id: '9100', title: 'filebeat-*' }, + { id: '9100', title: 'auditbeat-*,filebeat-*' }, + { id: '5678', title: 'auditbeat-*,.siem-signals-default' }, + ], + configIndexPatterns: + mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline].selectedPatterns, + signalIndexName: mockGlobalState.sourcerer.signalIndexName, + sourcererScopes: { + ...mockGlobalState.sourcerer.sourcererScopes, + [SourcererScopeName.timeline]: { + ...mockGlobalState.sourcerer.sourcererScopes[SourcererScopeName.timeline], + loading: false, + selectedPatterns: ['filebeat-*'], + }, + }, + }, + }; + const store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + it('renders', () => { + const wrapper = render( + + + + ); + fireEvent.click(wrapper.getByTestId('sourcerer-timeline-trigger')); + expect(wrapper.getByTestId('timeline-sourcerer').textContent).toEqual( + initialPatterns.sort().join('') + ); + }); + it('correctly filters options', () => { + const wrapper = render( + + + + ); + fireEvent.click(wrapper.getByTestId('sourcerer-timeline-trigger')); + fireEvent.click(wrapper.getByTestId('comboBoxToggleListButton')); + const optionNodes = wrapper.getAllByTestId('sourcerer-option'); + expect(optionNodes.length).toBe(9); + }); + it('reset button works', () => { + const wrapper = render( + + + + ); + fireEvent.click(wrapper.getByTestId('sourcerer-timeline-trigger')); + expect(wrapper.getByTestId('timeline-sourcerer').textContent).toEqual('filebeat-*'); + + fireEvent.click(wrapper.getByTestId('sourcerer-reset')); + expect(wrapper.getByTestId('timeline-sourcerer').textContent).toEqual( + initialPatterns.sort().join('') + ); + fireEvent.click(wrapper.getByTestId('comboBoxToggleListButton')); + const optionNodes = wrapper.getAllByTestId('sourcerer-option'); + expect(optionNodes.length).toBe(2); + }); +}); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx index 5682bdb91ff58..dbe04eccac521 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/search_or_filter/pick_events.tsx @@ -144,7 +144,7 @@ const PickEventTypeComponents: React.FC = ({ ...kibanaIndexPatterns.map((kip) => kip.title), signalIndexName, ].reduce>>((acc, index) => { - if (index != null && !acc.some((o) => o.label.includes(index))) { + if (index != null && !acc.some((o) => o.label === index)) { return [...acc, { label: index, value: index }]; } return acc; @@ -153,16 +153,15 @@ const PickEventTypeComponents: React.FC = ({ ); const renderOption = useCallback( - (option) => { - const { value } = option; + ({ value }) => { if (kibanaIndexPatterns.some((kip) => kip.title === value)) { return ( - <> + {value} - + ); } - return <>{value}; + return {value}; }, [kibanaIndexPatterns] ); @@ -193,14 +192,14 @@ const PickEventTypeComponents: React.FC = ({ setFilterEventType(filter); if (filter === 'all') { setSelectedOptions( - [...configIndexPatterns, signalIndexName ?? ''].map((indexSelected) => ({ + [...configIndexPatterns.sort(), signalIndexName ?? ''].map((indexSelected) => ({ label: indexSelected, value: indexSelected, })) ); } else if (filter === 'raw') { setSelectedOptions( - configIndexPatterns.map((indexSelected) => ({ + configIndexPatterns.sort().map((indexSelected) => ({ label: indexSelected, value: indexSelected, })) @@ -240,14 +239,8 @@ const PickEventTypeComponents: React.FC = ({ }, [filterEventType, onChangeEventTypeAndIndexesName, selectedOptions]); const resetDataSources = useCallback(() => { - setSelectedOptions( - sourcererScope.selectedPatterns.map((indexSelected) => ({ - label: indexSelected, - value: indexSelected, - })) - ); - setFilterEventType(eventType); - }, [eventType, sourcererScope.selectedPatterns]); + onChangeFilter('all'); + }, [onChangeFilter]); const comboBox = useMemo( () => ( diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index 3750bc22ddc69..95ad6c5d44ca3 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -37,7 +37,9 @@ export const { setSelected, setTGridSelectAll, toggleDetailPanel, + updateColumnOrder, updateColumns, + updateColumnWidth, updateIsLoading, updateItemsPerPage, updateItemsPerPageOptions, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx index 01bc589393d2e..131f255b5a7a7 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.test.tsx @@ -25,7 +25,9 @@ import { removeColumn, upsertColumn, applyDeltaToColumnWidth, + updateColumnOrder, updateColumns, + updateColumnWidth, updateItemsPerPage, updateSort, } from './actions'; @@ -168,4 +170,35 @@ describe('epicLocalStorage', () => { ); await waitFor(() => expect(addTimelineInStorageMock).toHaveBeenCalled()); }); + + it('persists updates to the column order to local storage', async () => { + shallow( + + + + ); + store.dispatch( + updateColumnOrder({ + columnIds: ['event.severity', '@timestamp', 'event.category'], + id: 'test', + }) + ); + await waitFor(() => expect(addTimelineInStorageMock).toHaveBeenCalled()); + }); + + it('persists updates to the column width to local storage', async () => { + shallow( + + + + ); + store.dispatch( + updateColumnWidth({ + columnId: 'event.severity', + id: 'test', + width: 123, + }) + ); + await waitFor(() => expect(addTimelineInStorageMock).toHaveBeenCalled()); + }); }); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts index 9a889e9ec1af8..6c4ebf91b7adf 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/epic_local_storage.ts @@ -19,6 +19,8 @@ import { applyDeltaToColumnWidth, setExcludedRowRendererIds, updateColumns, + updateColumnOrder, + updateColumnWidth, updateItemsPerPage, updateSort, } from './actions'; @@ -30,6 +32,8 @@ const timelineActionTypes = [ upsertColumn.type, applyDeltaToColumnWidth.type, updateColumns.type, + updateColumnOrder.type, + updateColumnWidth.type, updateItemsPerPage.type, updateSort.type, setExcludedRowRendererIds.type, diff --git a/x-pack/plugins/security_solution/public/types.ts b/x-pack/plugins/security_solution/public/types.ts index e595b905b998e..bee0e9b3a3d1d 100644 --- a/x-pack/plugins/security_solution/public/types.ts +++ b/x-pack/plugins/security_solution/public/types.ts @@ -15,7 +15,6 @@ import { NewsfeedPublicPluginStart } from '../../../../src/plugins/newsfeed/publ import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/public'; -import { TelemetryManagementSectionPluginSetup } from '../../../../src/plugins/telemetry_management_section/public'; import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { FleetStart } from '../../fleet/public'; import { PluginStart as ListsPluginStart } from '../../lists/public'; @@ -49,7 +48,6 @@ export interface SetupPlugins { security: SecurityPluginSetup; triggersActionsUi: TriggersActionsSetup; usageCollection?: UsageCollectionSetup; - telemetryManagementSection?: TelemetryManagementSectionPluginSetup; ml?: MlPluginSetup; } diff --git a/x-pack/plugins/security_solution/server/deprecation_privileges/index.test.ts b/x-pack/plugins/security_solution/server/deprecation_privileges/index.test.ts index f61940387c413..213beb6207184 100644 --- a/x-pack/plugins/security_solution/server/deprecation_privileges/index.test.ts +++ b/x-pack/plugins/security_solution/server/deprecation_privileges/index.test.ts @@ -473,8 +473,8 @@ describe('deprecations', () => { }, "deprecationType": "feature", "level": "warning", - "message": "The \\"Security\\" feature will be split into two separate features in 8.0. The \\"first_role\\" role grants access to this feature, and it needs to be updated before you upgrade Kibana. This will ensure that users have access to the same features after the upgrade.", - "title": "The \\"first_role\\" role needs to be updated", + "message": "The Security feature will be split into the Security and Cases features in 8.0. The \\"first_role\\" role grants access to the Security feature only. Update the role to also grant access to the Cases feature.", + "title": "The Security feature is changing, and the \\"first_role\\" role requires an update", }, ] `); @@ -608,8 +608,8 @@ describe('deprecations', () => { }, "deprecationType": "feature", "level": "warning", - "message": "The \\"Security\\" feature will be split into two separate features in 8.0. The \\"second_role\\" role grants access to this feature, and it needs to be updated before you upgrade Kibana. This will ensure that users have access to the same features after the upgrade.", - "title": "The \\"second_role\\" role needs to be updated", + "message": "The Security feature will be split into the Security and Cases features in 8.0. The \\"second_role\\" role grants access to the Security feature only. Update the role to also grant access to the Cases feature.", + "title": "The Security feature is changing, and the \\"second_role\\" role requires an update", }, ] `); @@ -772,13 +772,13 @@ describe('deprecations', () => { }); const response = await getDeprecations(mockContext); expect(response).toEqual([ - expect.objectContaining({ title: 'The "role_siem_all" role needs to be updated' }), - expect.objectContaining({ title: 'The "role_siem_read" role needs to be updated' }), + expect.objectContaining({ message: expect.stringMatching(/"role_siem_all"/) }), + expect.objectContaining({ message: expect.stringMatching(/"role_siem_read"/) }), expect.objectContaining({ - title: 'The "role_siem_minimal_all_cases_all_cases_read" role needs to be updated', + message: expect.stringMatching(/"role_siem_minimal_all_cases_all_cases_read"/), }), expect.objectContaining({ - title: 'The "role_siem_minimal_read_cases_read" role needs to be updated', + message: expect.stringMatching(/"role_siem_minimal_read_cases_read"/), }), // the fifth_role and sixth_role have been filtered out because they do not grant access to Cases ]); diff --git a/x-pack/plugins/security_solution/server/deprecation_privileges/index.ts b/x-pack/plugins/security_solution/server/deprecation_privileges/index.ts index 803231b236cbd..b56583d26261f 100644 --- a/x-pack/plugins/security_solution/server/deprecation_privileges/index.ts +++ b/x-pack/plugins/security_solution/server/deprecation_privileges/index.ts @@ -119,7 +119,8 @@ export const registerPrivilegeDeprecations = ({ title: i18n.translate( 'xpack.securitySolution.privilegeDeprecations.casesSubFeaturePrivileges.title', { - defaultMessage: 'The "{roleName}" role needs to be updated', + defaultMessage: + 'The Security feature is changing, and the "{roleName}" role requires an update', values: { roleName }, } ), @@ -127,7 +128,7 @@ export const registerPrivilegeDeprecations = ({ 'xpack.securitySolution.privilegeDeprecations.casesSubFeaturePrivileges.message', { defaultMessage: - 'The "Security" feature will be split into two separate features in 8.0. The "{roleName}" role grants access to this feature, and it needs to be updated before you upgrade Kibana. This will ensure that users have access to the same features after the upgrade.', + 'The Security feature will be split into the Security and Cases features in 8.0. The "{roleName}" role grants access to the Security feature only. Update the role to also grant access to the Cases feature.', values: { roleName }, } ), diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts index 2e5e331b71b00..81f229c636bd8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; import { alertsMock } from '../../../../../alerting/server/mocks'; import { scheduleThrottledNotificationActions } from './schedule_throttle_notification_actions'; import { @@ -19,8 +19,10 @@ jest.mock('./schedule_notification_actions', () => ({ describe('schedule_throttle_notification_actions', () => { let notificationRuleParams: NotificationRuleTypeParams; + let logger: ReturnType; beforeEach(() => { + logger = loggingSystemMock.createLogger(); (scheduleNotificationActions as jest.Mock).mockReset(); notificationRuleParams = { author: ['123'], @@ -82,6 +84,38 @@ describe('schedule_throttle_notification_actions', () => { ), alertInstance: alertsMock.createAlertInstanceFactory(), notificationRuleParams, + logger, + signals: [], + }); + + expect(scheduleNotificationActions as jest.Mock).toHaveBeenCalled(); + }); + + it('should call "scheduleNotificationActions" if the signals length is 1 or greater', async () => { + await scheduleThrottledNotificationActions({ + throttle: '1d', + startedAt: new Date('2021-08-24T19:19:22.094Z'), + id: '123', + kibanaSiemAppUrl: 'http://www.example.com', + outputIndex: 'output-123', + ruleId: 'rule-123', + esClient: elasticsearchServiceMock.createElasticsearchClient( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + hits: { + hits: [], + total: 0, + }, + }) + ), + alertInstance: alertsMock.createAlertInstanceFactory(), + notificationRuleParams, + logger, + signals: [ + { + _id: '123', + index: '123', + }, + ], }); expect(scheduleNotificationActions as jest.Mock).toHaveBeenCalled(); @@ -105,6 +139,8 @@ describe('schedule_throttle_notification_actions', () => { ), alertInstance: alertsMock.createAlertInstanceFactory(), notificationRuleParams, + logger, + signals: [], }); expect(scheduleNotificationActions as jest.Mock).not.toHaveBeenCalled(); @@ -132,6 +168,8 @@ describe('schedule_throttle_notification_actions', () => { ), alertInstance: alertsMock.createAlertInstanceFactory(), notificationRuleParams, + logger, + signals: [], }); expect(scheduleNotificationActions as jest.Mock).not.toHaveBeenCalled(); @@ -161,6 +199,8 @@ describe('schedule_throttle_notification_actions', () => { ), alertInstance: alertsMock.createAlertInstanceFactory(), notificationRuleParams, + logger, + signals: [], }); expect((scheduleNotificationActions as jest.Mock).mock.calls[0][0].resultsLink).toMatch( @@ -174,4 +214,384 @@ describe('schedule_throttle_notification_actions', () => { }) ); }); + + it('should log debug information when passing through in expected format and no error messages', async () => { + await scheduleThrottledNotificationActions({ + throttle: '1d', + startedAt: new Date('2021-08-24T19:19:22.094Z'), + id: '123', + kibanaSiemAppUrl: 'http://www.example.com', + outputIndex: 'output-123', + ruleId: 'rule-123', + esClient: elasticsearchServiceMock.createElasticsearchClient( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + hits: { + hits: [ + { + _source: {}, + }, + ], + total: 1, + }, + }) + ), + alertInstance: alertsMock.createAlertInstanceFactory(), + notificationRuleParams, + logger, + signals: [], + }); + // We only test the first part since it has date math using math + expect(logger.debug.mock.calls[0][0]).toMatch( + /The notification throttle resultsLink created is/ + ); + expect(logger.debug.mock.calls[1][0]).toEqual( + 'The notification throttle query result size before deconflicting duplicates is: 1. The notification throttle passed in signals size before deconflicting duplicates is: 0. The deconflicted size and size of the signals sent into throttle notification is: 1. The signals count from results size is: 1. The final signals count being sent to the notification is: 1.' + ); + // error should not have been called in this case. + expect(logger.error).not.toHaveBeenCalled(); + }); + + it('should log error information if "throttle" is an invalid string', async () => { + await scheduleThrottledNotificationActions({ + throttle: 'invalid', + startedAt: new Date('2021-08-24T19:19:22.094Z'), + id: '123', + kibanaSiemAppUrl: 'http://www.example.com', + outputIndex: 'output-123', + ruleId: 'rule-123', + esClient: elasticsearchServiceMock.createElasticsearchClient( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + hits: { + hits: [ + { + _source: {}, + }, + ], + total: 1, + }, + }) + ), + alertInstance: alertsMock.createAlertInstanceFactory(), + notificationRuleParams, + logger, + signals: [], + }); + + expect(logger.error).toHaveBeenCalledWith( + 'The notification throttle "from" and/or "to" range values could not be constructed as valid. Tried to construct the values of "from": now-invalid "to": 2021-08-24T19:19:22.094Z. This will cause a reset of the notification throttle. Expect either missing alert notifications or alert notifications happening earlier than expected.' + ); + }); + + it('should count correctly if it does a deconflict', async () => { + await scheduleThrottledNotificationActions({ + throttle: '1d', + startedAt: new Date('2021-08-24T19:19:22.094Z'), + id: '123', + kibanaSiemAppUrl: 'http://www.example.com', + outputIndex: 'output-123', + ruleId: 'rule-123', + esClient: elasticsearchServiceMock.createElasticsearchClient( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + hits: { + hits: [ + { + _index: 'index-123', + _id: 'id-123', + _source: { + test: 123, + }, + }, + { + _index: 'index-456', + _id: 'id-456', + _source: { + test: 456, + }, + }, + ], + total: 2, + }, + }) + ), + alertInstance: alertsMock.createAlertInstanceFactory(), + notificationRuleParams, + logger, + signals: [ + { + _index: 'index-456', + _id: 'id-456', + test: 456, + }, + ], + }); + expect(scheduleNotificationActions).toHaveBeenCalledWith( + expect.objectContaining({ + signalsCount: 2, + signals: [ + { + _id: 'id-456', + _index: 'index-456', + test: 456, + }, + { + _id: 'id-123', + _index: 'index-123', + test: 123, + }, + ], + ruleParams: notificationRuleParams, + }) + ); + }); + + it('should count correctly if it does not do a deconflict', async () => { + await scheduleThrottledNotificationActions({ + throttle: '1d', + startedAt: new Date('2021-08-24T19:19:22.094Z'), + id: '123', + kibanaSiemAppUrl: 'http://www.example.com', + outputIndex: 'output-123', + ruleId: 'rule-123', + esClient: elasticsearchServiceMock.createElasticsearchClient( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + hits: { + hits: [ + { + _index: 'index-123', + _id: 'id-123', + _source: { + test: 123, + }, + }, + { + _index: 'index-456', + _id: 'id-456', + _source: { + test: 456, + }, + }, + ], + total: 2, + }, + }) + ), + alertInstance: alertsMock.createAlertInstanceFactory(), + notificationRuleParams, + logger, + signals: [ + { + _index: 'index-789', + _id: 'id-789', + test: 456, + }, + ], + }); + expect(scheduleNotificationActions).toHaveBeenCalledWith( + expect.objectContaining({ + signalsCount: 3, + signals: [ + { + _id: 'id-789', + _index: 'index-789', + test: 456, + }, + { + _id: 'id-123', + _index: 'index-123', + test: 123, + }, + { + _id: 'id-456', + _index: 'index-456', + test: 456, + }, + ], + ruleParams: notificationRuleParams, + }) + ); + }); + + it('should count total hit with extra total elements', async () => { + await scheduleThrottledNotificationActions({ + throttle: '1d', + startedAt: new Date('2021-08-24T19:19:22.094Z'), + id: '123', + kibanaSiemAppUrl: 'http://www.example.com', + outputIndex: 'output-123', + ruleId: 'rule-123', + esClient: elasticsearchServiceMock.createElasticsearchClient( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + hits: { + hits: [ + { + _index: 'index-123', + _id: 'id-123', + _source: { + test: 123, + }, + }, + ], + total: 20, // total can be different from the actual return values so we have to ensure we count these. + }, + }) + ), + alertInstance: alertsMock.createAlertInstanceFactory(), + notificationRuleParams, + logger, + signals: [ + { + _index: 'index-789', + _id: 'id-789', + test: 456, + }, + ], + }); + expect(scheduleNotificationActions).toHaveBeenCalledWith( + expect.objectContaining({ + signalsCount: 21, + signals: [ + { + _id: 'id-789', + _index: 'index-789', + test: 456, + }, + { + _id: 'id-123', + _index: 'index-123', + test: 123, + }, + ], + ruleParams: notificationRuleParams, + }) + ); + }); + + it('should count correctly if it does a deconflict and the total has extra values', async () => { + await scheduleThrottledNotificationActions({ + throttle: '1d', + startedAt: new Date('2021-08-24T19:19:22.094Z'), + id: '123', + kibanaSiemAppUrl: 'http://www.example.com', + outputIndex: 'output-123', + ruleId: 'rule-123', + esClient: elasticsearchServiceMock.createElasticsearchClient( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + hits: { + hits: [ + { + _index: 'index-123', + _id: 'id-123', + _source: { + test: 123, + }, + }, + { + _index: 'index-456', + _id: 'id-456', + _source: { + test: 456, + }, + }, + ], + total: 20, // total can be different from the actual return values so we have to ensure we count these. + }, + }) + ), + alertInstance: alertsMock.createAlertInstanceFactory(), + notificationRuleParams, + logger, + signals: [ + { + _index: 'index-456', + _id: 'id-456', + test: 456, + }, + ], + }); + expect(scheduleNotificationActions).toHaveBeenCalledWith( + expect.objectContaining({ + signalsCount: 20, + signals: [ + { + _id: 'id-456', + _index: 'index-456', + test: 456, + }, + { + _id: 'id-123', + _index: 'index-123', + test: 123, + }, + ], + ruleParams: notificationRuleParams, + }) + ); + }); + + it('should add extra count element if it has signals added', async () => { + await scheduleThrottledNotificationActions({ + throttle: '1d', + startedAt: new Date('2021-08-24T19:19:22.094Z'), + id: '123', + kibanaSiemAppUrl: 'http://www.example.com', + outputIndex: 'output-123', + ruleId: 'rule-123', + esClient: elasticsearchServiceMock.createElasticsearchClient( + elasticsearchServiceMock.createSuccessTransportRequestPromise({ + hits: { + hits: [ + { + _index: 'index-123', + _id: 'id-123', + _source: { + test: 123, + }, + }, + { + _index: 'index-456', + _id: 'id-456', + _source: { + test: 456, + }, + }, + ], + total: 20, // total can be different from the actual return values so we have to ensure we count these. + }, + }) + ), + alertInstance: alertsMock.createAlertInstanceFactory(), + notificationRuleParams, + logger, + signals: [ + { + _index: 'index-789', + _id: 'id-789', + test: 789, + }, + ], + }); + expect(scheduleNotificationActions).toHaveBeenCalledWith( + expect.objectContaining({ + signalsCount: 21, // should be 1 more than the total since we pushed in an extra signal + signals: [ + { + _id: 'id-789', + _index: 'index-789', + test: 789, + }, + { + _id: 'id-123', + _index: 'index-123', + test: 123, + }, + { + _id: 'id-456', + _index: 'index-456', + test: 456, + }, + ], + ruleParams: notificationRuleParams, + }) + ); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts index 5dd583d47b403..5bf18496e6375 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/schedule_throttle_notification_actions.ts @@ -5,11 +5,11 @@ * 2.0. */ -import { ElasticsearchClient, SavedObject } from 'src/core/server'; +import { ElasticsearchClient, SavedObject, Logger } from 'src/core/server'; import { parseScheduleDates } from '@kbn/securitysolution-io-ts-utils'; import { AlertInstance } from '../../../../../alerting/server'; import { RuleParams } from '../schemas/rule_schemas'; -import { getNotificationResultsLink } from '../notifications/utils'; +import { deconflictSignalsAndResults, getNotificationResultsLink } from '../notifications/utils'; import { DEFAULT_RULE_NOTIFICATION_QUERY_SIZE } from '../../../../common/constants'; import { getSignals } from '../notifications/get_signals'; import { @@ -18,8 +18,25 @@ import { } from './schedule_notification_actions'; import { AlertAttributes } from '../signals/types'; +interface ScheduleThrottledNotificationActionsOptions { + id: SavedObject['id']; + startedAt: Date; + throttle: AlertAttributes['throttle']; + kibanaSiemAppUrl: string | undefined; + outputIndex: RuleParams['outputIndex']; + ruleId: RuleParams['ruleId']; + esClient: ElasticsearchClient; + alertInstance: AlertInstance; + notificationRuleParams: NotificationRuleTypeParams; + signals: unknown[]; + logger: Logger; +} + /** * Schedules a throttled notification action for executor rules. + * NOTE: It's important that since this is throttled that you call this in _ALL_ cases including error conditions or results being empty or not a success. + * If you do not call this within your rule executor then this will cause a "reset" and will stop "throttling" and the next call will cause an immediate action + * to be sent through the system. * @param throttle The throttle which is the alerting saved object throttle * @param startedAt When the executor started at * @param id The id the alert which caused the notifications @@ -40,17 +57,9 @@ export const scheduleThrottledNotificationActions = async ({ esClient, alertInstance, notificationRuleParams, -}: { - id: SavedObject['id']; - startedAt: Date; - throttle: AlertAttributes['throttle']; - kibanaSiemAppUrl: string | undefined; - outputIndex: RuleParams['outputIndex']; - ruleId: RuleParams['ruleId']; - esClient: ElasticsearchClient; - alertInstance: AlertInstance; - notificationRuleParams: NotificationRuleTypeParams; -}): Promise => { + signals, + logger, +}: ScheduleThrottledNotificationActionsOptions): Promise => { const fromInMs = parseScheduleDates(`now-${throttle}`); const toInMs = parseScheduleDates(startedAt.toISOString()); @@ -62,6 +71,22 @@ export const scheduleThrottledNotificationActions = async ({ kibanaSiemAppUrl, }); + logger.debug( + [ + `The notification throttle resultsLink created is: ${resultsLink}.`, + ' Notification throttle is querying the results using', + ` "from:" ${fromInMs.valueOf()}`, + ' "to":', + ` ${toInMs.valueOf()}`, + ' "size":', + ` ${DEFAULT_RULE_NOTIFICATION_QUERY_SIZE}`, + ' "index":', + ` ${outputIndex}`, + ' "ruleId":', + ` ${ruleId}`, + ].join('') + ); + const results = await getSignals({ from: `${fromInMs.valueOf()}`, to: `${toInMs.valueOf()}`, @@ -71,18 +96,56 @@ export const scheduleThrottledNotificationActions = async ({ esClient, }); - const signalsCount = + // This will give us counts up to the max of 10k from tracking total hits. + const signalsCountFromResults = typeof results.hits.total === 'number' ? results.hits.total : results.hits.total.value; - const signals = results.hits.hits.map((hit) => hit._source); - if (results.hits.hits.length !== 0) { + const resultsFlattened = results.hits.hits.map((hit) => { + return { + _id: hit._id, + _index: hit._index, + ...hit._source, + }; + }); + + const deconflicted = deconflictSignalsAndResults({ + logger, + signals, + querySignals: resultsFlattened, + }); + + // Difference of how many deconflicted results we have to subtract from our signals count. + const deconflictedDiff = resultsFlattened.length + signals.length - deconflicted.length; + + // Subtract any deconflicted differences from the total count. + const signalsCount = signalsCountFromResults + signals.length - deconflictedDiff; + logger.debug( + [ + `The notification throttle query result size before deconflicting duplicates is: ${resultsFlattened.length}.`, + ` The notification throttle passed in signals size before deconflicting duplicates is: ${signals.length}.`, + ` The deconflicted size and size of the signals sent into throttle notification is: ${deconflicted.length}.`, + ` The signals count from results size is: ${signalsCountFromResults}.`, + ` The final signals count being sent to the notification is: ${signalsCount}.`, + ].join('') + ); + + if (deconflicted.length !== 0) { scheduleNotificationActions({ alertInstance, signalsCount, - signals, + signals: deconflicted, resultsLink, ruleParams: notificationRuleParams, }); } + } else { + logger.error( + [ + 'The notification throttle "from" and/or "to" range values could not be constructed as valid. Tried to construct the values of', + ` "from": now-${throttle}`, + ` "to": ${startedAt.toISOString()}.`, + ' This will cause a reset of the notification throttle. Expect either missing alert notifications or alert notifications happening earlier than expected.', + ].join('') + ); } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.test.ts index 5a667616a9a39..2da7a0398bd3f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.test.ts @@ -5,18 +5,384 @@ * 2.0. */ -import { getNotificationResultsLink } from './utils'; +import { SearchHit } from '@elastic/elasticsearch/api/types'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { SignalSource } from '../signals/types'; +import { deconflictSignalsAndResults, getNotificationResultsLink } from './utils'; describe('utils', () => { - it('getNotificationResultsLink', () => { - const resultLink = getNotificationResultsLink({ - kibanaSiemAppUrl: 'http://localhost:5601/app/security', - id: 'notification-id', - from: '00000', - to: '1111', - }); - expect(resultLink).toEqual( - `http://localhost:5601/app/security/detections/rules/id/notification-id?timerange=(global:(linkTo:!(timeline),timerange:(from:00000,kind:absolute,to:1111)),timeline:(linkTo:!(global),timerange:(from:00000,kind:absolute,to:1111)))` - ); + let logger = loggingSystemMock.create().get('security_solution'); + + beforeEach(() => { + logger = loggingSystemMock.create().get('security_solution'); + }); + + describe('getNotificationResultsLink', () => { + test('it returns expected link', () => { + const resultLink = getNotificationResultsLink({ + kibanaSiemAppUrl: 'http://localhost:5601/app/security', + id: 'notification-id', + from: '00000', + to: '1111', + }); + expect(resultLink).toEqual( + `http://localhost:5601/app/security/detections/rules/id/notification-id?timerange=(global:(linkTo:!(timeline),timerange:(from:00000,kind:absolute,to:1111)),timeline:(linkTo:!(global),timerange:(from:00000,kind:absolute,to:1111)))` + ); + }); + }); + + describe('deconflictSignalsAndResults', () => { + type FuncReturn = ReturnType; + + test('given no signals and no query results it returns an empty array', () => { + expect( + deconflictSignalsAndResults({ logger, querySignals: [], signals: [] }) + ).toEqual([]); + }); + + test('given an empty signal and a single query result it returns the query result in the array', () => { + const querySignals: Array> = [ + { + _id: 'id-123', + _index: 'index-123', + _source: { + test: '123', + }, + }, + ]; + expect( + deconflictSignalsAndResults({ logger, querySignals, signals: [] }) + ).toEqual(querySignals); + }); + + test('given a single signal and an empty query result it returns the query result in the array', () => { + const signals: Array> = [ + { + _id: 'id-123', + _index: 'index-123', + _source: { + test: '123', + }, + }, + ]; + expect( + deconflictSignalsAndResults({ logger, querySignals: [], signals }) + ).toEqual(signals); + }); + + test('given a signal and a different query result it returns both combined together', () => { + const querySignals: Array> = [ + { + _id: 'id-123', + _index: 'index-123', + _source: { + test: '123', + }, + }, + ]; + const signals: Array> = [ + { + _id: 'id-789', + _index: 'index-456', + _source: { + test: '456', + }, + }, + ]; + expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([ + ...signals, + ...querySignals, + ]); + }); + + test('given a duplicate in querySignals it returns both combined together without the duplicate', () => { + const querySignals: Array> = [ + { + _id: 'id-123', + _index: 'index-123', // This should only show up once and not be duplicated twice + _source: { + test: '123', + }, + }, + { + _index: 'index-890', + _id: 'id-890', + _source: { + test: '890', + }, + }, + ]; + const signals: Array> = [ + { + _id: 'id-123', // This should only show up once and not be duplicated twice + _index: 'index-123', + _source: { + test: '123', + }, + }, + { + _id: 'id-789', + _index: 'index-456', + _source: { + test: '456', + }, + }, + ]; + expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([ + { + _id: 'id-123', + _index: 'index-123', + _source: { + test: '123', + }, + }, + { + _id: 'id-789', + _index: 'index-456', + _source: { + test: '456', + }, + }, + { + _id: 'id-890', + _index: 'index-890', + _source: { + test: '890', + }, + }, + ]); + }); + + test('given a duplicate in signals it returns both combined together without the duplicate', () => { + const signals: Array> = [ + { + _id: 'id-123', + _index: 'index-123', // This should only show up once and not be duplicated twice + _source: { + test: '123', + }, + }, + { + _index: 'index-890', + _id: 'id-890', + _source: { + test: '890', + }, + }, + ]; + const querySignals: Array> = [ + { + _id: 'id-123', // This should only show up once and not be duplicated twice + _index: 'index-123', + _source: { + test: '123', + }, + }, + { + _id: 'id-789', + _index: 'index-456', + _source: { + test: '456', + }, + }, + ]; + expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([ + { + _id: 'id-123', + _index: 'index-123', + _source: { test: '123' }, + }, + { + _id: 'id-890', + _index: 'index-890', + _source: { test: '890' }, + }, + { + _id: 'id-789', + _index: 'index-456', + _source: { test: '456' }, + }, + ]); + }); + + test('does not give a duplicate in signals if they are only different by their index', () => { + const signals: Array> = [ + { + _id: 'id-123', + _index: 'index-123-a', // This is only different by index + _source: { + test: '123', + }, + }, + { + _index: 'index-890', + _id: 'id-890', + _source: { + test: '890', + }, + }, + ]; + const querySignals: Array> = [ + { + _id: 'id-123', // This is only different by index + _index: 'index-123-b', + _source: { + test: '123', + }, + }, + { + _id: 'id-789', + _index: 'index-456', + _source: { + test: '456', + }, + }, + ]; + expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([ + ...signals, + ...querySignals, + ]); + }); + + test('it logs a debug statement when it sees a duplicate and returns nothing if both are identical', () => { + const querySignals: Array> = [ + { + _id: 'id-123', + _index: 'index-123', + _source: { + test: '123', + }, + }, + ]; + const signals: Array> = [ + { + _id: 'id-123', + _index: 'index-123', + _source: { + test: '456', + }, + }, + ]; + expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([ + { + _id: 'id-123', + _index: 'index-123', + _source: { + test: '456', + }, + }, + ]); + expect(logger.debug).toHaveBeenCalledWith( + 'Notification throttle removing duplicate signal and query result found of "_id": id-123, "_index": index-123' + ); + }); + + test('it logs an error statement if it sees a signal missing an "_id" for an uncommon reason and returns both documents', () => { + const querySignals: Array> = [ + { + _id: 'id-123', + _index: 'index-123', + _source: { + test: '123', + }, + }, + ]; + const signals: unknown[] = [ + { + _index: 'index-123', + _source: { + test: '456', + }, + }, + ]; + expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([ + ...signals, + ...querySignals, + ]); + expect(logger.error).toHaveBeenCalledWith( + 'Notification throttle cannot determine if we can de-conflict as either the passed in signal or the results query has a null value for either "_id" or "_index". Expect possible duplications in your alerting actions. Passed in signals "_id": undefined. Passed in signals "_index": index-123. Passed in query "result._id": id-123. Passed in query "result._index": index-123.' + ); + }); + + test('it logs an error statement if it sees a signal missing a "_index" for an uncommon reason and returns both documents', () => { + const querySignals: Array> = [ + { + _id: 'id-123', + _index: 'index-123', + _source: { + test: '123', + }, + }, + ]; + const signals: unknown[] = [ + { + _id: 'id-123', + _source: { + test: '456', + }, + }, + ]; + expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([ + ...signals, + ...querySignals, + ]); + expect(logger.error).toHaveBeenCalledWith( + 'Notification throttle cannot determine if we can de-conflict as either the passed in signal or the results query has a null value for either "_id" or "_index". Expect possible duplications in your alerting actions. Passed in signals "_id": id-123. Passed in signals "_index": undefined. Passed in query "result._id": id-123. Passed in query "result._index": index-123.' + ); + }); + + test('it logs an error statement if it sees a querySignals missing an "_id" for an uncommon reason and returns both documents', () => { + const querySignals: Array> = [ + { + _index: 'index-123', + _source: { + test: '123', + }, + }, + ] as unknown[] as Array>; + const signals: unknown[] = [ + { + _id: 'id-123', + _index: 'index-123', + _source: { + test: '456', + }, + }, + ]; + expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([ + ...signals, + ...querySignals, + ]); + expect(logger.error).toHaveBeenCalledWith( + 'Notification throttle cannot determine if we can de-conflict as either the passed in signal or the results query has a null value for either "_id" or "_index". Expect possible duplications in your alerting actions. Passed in signals "_id": id-123. Passed in signals "_index": index-123. Passed in query "result._id": undefined. Passed in query "result._index": index-123.' + ); + }); + + test('it logs an error statement if it sees a querySignals missing a "_index" for an uncommon reason and returns both documents', () => { + const querySignals: Array> = [ + { + _id: 'id-123', + _source: { + test: '123', + }, + }, + ] as unknown[] as Array>; + const signals: unknown[] = [ + { + _id: 'id-123', + _index: 'index-123', + _source: { + test: '456', + }, + }, + ]; + expect(deconflictSignalsAndResults({ logger, querySignals, signals })).toEqual([ + ...signals, + ...querySignals, + ]); + expect(logger.error).toHaveBeenCalledWith( + 'Notification throttle cannot determine if we can de-conflict as either the passed in signal or the results query has a null value for either "_id" or "_index". Expect possible duplications in your alerting actions. Passed in signals "_id": id-123. Passed in signals "_index": index-123. Passed in query "result._id": id-123. Passed in query "result._index": undefined.' + ); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.ts index 4c4bac7da6a62..c8fc6febe4d0f 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/notifications/utils.ts @@ -5,7 +5,9 @@ * 2.0. */ +import { Logger } from 'src/core/server'; import { APP_PATH } from '../../../../common/constants'; +import { SignalSearchResponse } from '../signals/types'; export const getNotificationResultsLink = ({ kibanaSiemAppUrl = APP_PATH, @@ -22,3 +24,54 @@ export const getNotificationResultsLink = ({ return `${kibanaSiemAppUrl}/detections/rules/id/${id}?timerange=(global:(linkTo:!(timeline),timerange:(from:${from},kind:absolute,to:${to})),timeline:(linkTo:!(global),timerange:(from:${from},kind:absolute,to:${to})))`; }; + +interface DeconflictOptions { + signals: unknown[]; + querySignals: SignalSearchResponse['hits']['hits']; + logger: Logger; +} + +/** + * Given a signals array of unknown that at least has a '_id' and '_index' this will deconflict it with a results. + * @param signals The signals array to deconflict with results + * @param results The results to deconflict with the signals + * @param logger The logger to log results + */ +export const deconflictSignalsAndResults = ({ + signals, + querySignals, + logger, +}: DeconflictOptions): unknown[] => { + const querySignalsFiltered = querySignals.filter((result) => { + return !signals.find((signal) => { + const { _id, _index } = signal as { _id?: string; _index?: string }; + if (_id == null || _index == null || result._id == null || result._index == null) { + logger.error( + [ + 'Notification throttle cannot determine if we can de-conflict as either the passed in signal or the results query has a null value for either "_id" or "_index".', + ' Expect possible duplications in your alerting actions.', + ` Passed in signals "_id": ${_id}.`, + ` Passed in signals "_index": ${_index}.`, + ` Passed in query "result._id": ${result._id}.`, + ` Passed in query "result._index": ${result._index}.`, + ].join('') + ); + return false; + } else { + if (result._id === _id && result._index === _index) { + logger.debug( + [ + 'Notification throttle removing duplicate signal and query result found of', + ` "_id": ${_id},`, + ` "_index": ${_index}`, + ].join('') + ); + return true; + } else { + return false; + } + } + }); + }); + return [...signals, ...querySignalsFiltered]; +}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.test.ts index 8414aa93c7984..7dd05c5122a61 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.test.ts @@ -20,6 +20,8 @@ describe('legacy_migrations', () => { test('it migrates both a "ruleAlertId" and a actions array with 1 element into the references array', () => { const doc = { attributes: { + ruleThrottle: '1d', + alertThrottle: '1d', ruleAlertId: '123', actions: [ { @@ -37,6 +39,8 @@ describe('legacy_migrations', () => { ) ).toEqual({ attributes: { + ruleThrottle: '1d', + alertThrottle: '1d', actions: [ { actionRef: 'action_0', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts index 8a52d3a13f065..aa85898e94a33 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_actions/legacy_migrations.ts @@ -245,7 +245,7 @@ export const legacyMigrateRuleAlertId = ( return { ...doc, attributes: { - ...attributesWithoutRuleAlertId.attributes, + ...attributesWithoutRuleAlertId, actions: actionsWithRef, }, references: [...existingReferences, ...alertReferences, ...actionsReferences], diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts index 77981d92b2ba7..0ad416e86e31a 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rule_types/create_security_rule_type_wrapper.ts @@ -111,6 +111,12 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = let result = createResultObject(state); + const notificationRuleParams: NotificationRuleTypeParams = { + ...params, + name: name as string, + id: ruleSO.id as string, + } as unknown as NotificationRuleTypeParams; + // check if rule has permissions to access given index pattern // move this collection of lines into a function in utils // so that we can use it in create rules route, bulk, etc. @@ -296,12 +302,6 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = const createdSignalsCount = result.createdSignals.length; if (actions.length) { - const notificationRuleParams: NotificationRuleTypeParams = { - ...params, - name: name as string, - id: ruleSO.id as string, - } as unknown as NotificationRuleTypeParams; - const fromInMs = parseScheduleDates(`now-${interval}`)?.format('x'); const toInMs = parseScheduleDates('now')?.format('x'); const resultsLink = getNotificationResultsLink({ @@ -328,6 +328,8 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = ruleId, esClient: services.scopedClusterClient.asCurrentUser, notificationRuleParams, + signals: result.createdSignals, + logger, }); } else if (createdSignalsCount) { const alertInstance = services.alertInstanceFactory(alertId); @@ -372,6 +374,21 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = ) ); } else { + // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: ruleSO.attributes.throttle, + startedAt, + id: ruleSO.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex: ruleDataClient.indexName, + ruleId, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + signals: result.createdSignals, + logger, + }); const errorMessage = buildRuleMessage( 'Bulk Indexing of signals failed:', truncateMessageList(result.errors).join() @@ -389,6 +406,22 @@ export const createSecurityRuleTypeWrapper: CreateSecurityRuleTypeWrapper = }); } } catch (error) { + // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: ruleSO.attributes.throttle, + startedAt, + id: ruleSO.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex: ruleDataClient.indexName, + ruleId, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + signals: result.createdSignals, + logger, + }); + const errorMessage = error.message ?? '(no error message given)'; const message = buildRuleMessage( 'An error occurred during rule execution:', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts index c2923b566175e..88b276358a705 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.test.ts @@ -35,6 +35,7 @@ import { allowedExperimentalValues } from '../../../../common/experimental_featu import { scheduleNotificationActions } from '../notifications/schedule_notification_actions'; import { ruleExecutionLogClientMock } from '../rule_execution_log/__mocks__/rule_execution_log_client'; import { RuleExecutionStatus } from '../../../../common/detection_engine/schemas/common/schemas'; +import { scheduleThrottledNotificationActions } from '../notifications/schedule_throttle_notification_actions'; import { eventLogServiceMock } from '../../../../../event_log/server/mocks'; import { createMockConfig } from '../routes/__mocks__'; @@ -58,7 +59,7 @@ jest.mock('@kbn/securitysolution-io-ts-utils', () => { parseScheduleDates: jest.fn(), }; }); - +jest.mock('../notifications/schedule_throttle_notification_actions'); const mockRuleExecutionLogClient = ruleExecutionLogClientMock.create(); jest.mock('../rule_execution_log/rule_execution_log_client', () => ({ @@ -200,6 +201,7 @@ describe('signal_rule_alert_type', () => { }); mockRuleExecutionLogClient.logStatusChange.mockClear(); + (scheduleThrottledNotificationActions as jest.Mock).mockClear(); }); describe('executor', () => { @@ -520,5 +522,28 @@ describe('signal_rule_alert_type', () => { }) ); }); + + it('should call scheduleThrottledNotificationActions if result is false to prevent the throttle from being reset', async () => { + const result: SearchAfterAndBulkCreateReturnType = { + success: false, + warning: false, + searchAfterTimes: [], + bulkCreateTimes: [], + lastLookBackDate: null, + createdSignalsCount: 0, + createdSignals: [], + warningMessages: [], + errors: ['Error that bubbled up.'], + }; + (queryExecutor as jest.Mock).mockResolvedValue(result); + await alert.executor(payload); + expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(1); + }); + + it('should call scheduleThrottledNotificationActions if an error was thrown to prevent the throttle from being reset', async () => { + (queryExecutor as jest.Mock).mockRejectedValue({}); + await alert.executor(payload); + expect(scheduleThrottledNotificationActions).toHaveBeenCalledTimes(1); + }); }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index 2094264cbf15f..4e98bee83aeb5 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -172,6 +172,12 @@ export const signalRulesAlertType = ({ newStatus: RuleExecutionStatus['going to run'], }); + const notificationRuleParams: NotificationRuleTypeParams = { + ...params, + name, + id: savedObject.id, + }; + // check if rule has permissions to access given index pattern // move this collection of lines into a function in utils // so that we can use it in create rules route, bulk, etc. @@ -396,12 +402,6 @@ export const signalRulesAlertType = ({ if (result.success) { if (actions.length) { - const notificationRuleParams: NotificationRuleTypeParams = { - ...params, - name, - id: savedObject.id, - }; - const fromInMs = parseScheduleDates(`now-${interval}`)?.format('x'); const toInMs = parseScheduleDates('now')?.format('x'); const resultsLink = getNotificationResultsLink({ @@ -426,8 +426,10 @@ export const signalRulesAlertType = ({ ?.kibana_siem_app_url, outputIndex, ruleId, + signals: result.createdSignals, esClient: services.scopedClusterClient.asCurrentUser, notificationRuleParams, + logger, }); } else if (result.createdSignalsCount) { const alertInstance = services.alertInstanceFactory(alertId); @@ -471,6 +473,22 @@ export const signalRulesAlertType = ({ ) ); } else { + // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: savedObject.attributes.throttle, + startedAt, + id: savedObject.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex, + ruleId, + signals: result.createdSignals, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + logger, + }); + const errorMessage = buildRuleMessage( 'Bulk Indexing of signals failed:', truncateMessageList(result.errors).join() @@ -488,6 +506,21 @@ export const signalRulesAlertType = ({ }); } } catch (error) { + // NOTE: Since this is throttled we have to call it even on an error condition, otherwise it will "reset" the throttle and fire early + await scheduleThrottledNotificationActions({ + alertInstance: services.alertInstanceFactory(alertId), + throttle: savedObject.attributes.throttle, + startedAt, + id: savedObject.id, + kibanaSiemAppUrl: (meta as { kibana_siem_app_url?: string } | undefined) + ?.kibana_siem_app_url, + outputIndex, + ruleId, + signals: result.createdSignals, + esClient: services.scopedClusterClient.asCurrentUser, + notificationRuleParams, + logger, + }); const errorMessage = error.message ?? '(no error message given)'; const message = buildRuleMessage( 'An error occurred during rule execution:', diff --git a/x-pack/plugins/spaces/server/config.ts b/x-pack/plugins/spaces/server/config.ts index 4abaf4f0ca569..a8c3a1c6223da 100644 --- a/x-pack/plugins/spaces/server/config.ts +++ b/x-pack/plugins/spaces/server/config.ts @@ -28,6 +28,7 @@ export function createConfig$(context: PluginInitializerContext) { const disabledDeprecation: ConfigDeprecation = (config, fromPath, addDeprecation) => { if ('enabled' in (config?.xpack?.spaces || {})) { addDeprecation({ + configPath: 'xpack.spaces.enabled', title: i18n.translate('xpack.spaces.deprecations.enabledTitle', { defaultMessage: 'Setting "xpack.spaces.enabled" is deprecated', }), diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts index 111fda3bdaca8..2a98a4670f2b5 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/alert_type.ts @@ -7,7 +7,7 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; -import { Logger } from 'src/core/server'; +import { Logger, SavedObjectReference } from 'src/core/server'; import { STACK_ALERTS_FEATURE_ID } from '../../../common'; import { getGeoContainmentExecutor } from './geo_containment'; import { @@ -15,14 +15,37 @@ import { AlertTypeState, AlertInstanceState, AlertInstanceContext, + RuleParamsAndRefs, AlertTypeParams, } from '../../../../alerting/server'; import { Query } from '../../../../../../src/plugins/data/common/query'; -export const GEO_CONTAINMENT_ID = '.geo-containment'; export const ActionGroupId = 'Tracked entity contained'; export const RecoveryActionGroupId = 'notGeoContained'; +export const GEO_CONTAINMENT_ID = '.geo-containment'; +export interface GeoContainmentParams extends AlertTypeParams { + index: string; + indexId: string; + geoField: string; + entity: string; + dateField: string; + boundaryType: string; + boundaryIndexTitle: string; + boundaryIndexId: string; + boundaryGeoField: string; + boundaryNameField?: string; + indexQuery?: Query; + boundaryIndexQuery?: Query; +} +export type GeoContainmentExtractedParams = Omit< + GeoContainmentParams, + 'indexId' | 'boundaryIndexId' +> & { + indexRefName: string; + boundaryIndexRefName: string; +}; + const actionVariableContextEntityIdLabel = i18n.translate( 'xpack.stackAlerts.geoContainment.actionVariableContextEntityIdLabel', { @@ -103,20 +126,6 @@ export const ParamsSchema = schema.object({ boundaryIndexQuery: schema.maybe(schema.any({})), }); -export interface GeoContainmentParams extends AlertTypeParams { - index: string; - indexId: string; - geoField: string; - entity: string; - dateField: string; - boundaryType: string; - boundaryIndexTitle: string; - boundaryIndexId: string; - boundaryGeoField: string; - boundaryNameField?: string; - indexQuery?: Query; - boundaryIndexQuery?: Query; -} export interface GeoContainmentState extends AlertTypeState { shapesFilters: Record; shapesIdsNamesMap: Record; @@ -140,7 +149,7 @@ export interface GeoContainmentInstanceContext extends AlertInstanceContext { export type GeoContainmentAlertType = AlertType< GeoContainmentParams, - never, // Only use if defining useSavedObjectReferences hook + GeoContainmentExtractedParams, GeoContainmentState, GeoContainmentInstanceState, GeoContainmentInstanceContext, @@ -148,6 +157,56 @@ export type GeoContainmentAlertType = AlertType< typeof RecoveryActionGroupId >; +export function extractEntityAndBoundaryReferences(params: GeoContainmentParams): { + params: GeoContainmentExtractedParams; + references: SavedObjectReference[]; +} { + const { indexId, boundaryIndexId, ...otherParams } = params; + + // Reference names omit the `param:`-prefix. This is handled by the alerting framework already + const references = [ + { + name: `tracked_index_${indexId}`, + type: 'index-pattern', + id: indexId as string, + }, + { + name: `boundary_index_${boundaryIndexId}`, + type: 'index-pattern', + id: boundaryIndexId as string, + }, + ]; + return { + params: { + ...otherParams, + indexRefName: `tracked_index_${indexId}`, + boundaryIndexRefName: `boundary_index_${boundaryIndexId}`, + }, + references, + }; +} + +export function injectEntityAndBoundaryIds( + params: GeoContainmentExtractedParams, + references: SavedObjectReference[] +): GeoContainmentParams { + const { indexRefName, boundaryIndexRefName, ...otherParams } = params; + const { id: indexId = null } = references.find((ref) => ref.name === indexRefName) || {}; + const { id: boundaryIndexId = null } = + references.find((ref) => ref.name === boundaryIndexRefName) || {}; + if (!indexId) { + throw new Error(`Index "${indexId}" not found in references array`); + } + if (!boundaryIndexId) { + throw new Error(`Boundary index "${boundaryIndexId}" not found in references array`); + } + return { + ...otherParams, + indexId, + boundaryIndexId, + } as GeoContainmentParams; +} + export function getAlertType(logger: Logger): GeoContainmentAlertType { const alertTypeName = i18n.translate('xpack.stackAlerts.geoContainment.alertTypeTitle', { defaultMessage: 'Tracking containment', @@ -179,5 +238,18 @@ export function getAlertType(logger: Logger): GeoContainmentAlertType { actionVariables, minimumLicenseRequired: 'gold', isExportable: true, + useSavedObjectReferences: { + extractReferences: ( + params: GeoContainmentParams + ): RuleParamsAndRefs => { + return extractEntityAndBoundaryReferences(params); + }, + injectReferences: ( + params: GeoContainmentExtractedParams, + references: SavedObjectReference[] + ) => { + return injectEntityAndBoundaryIds(params, references); + }, + }, }; } diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts index 21a536dd474ba..f227ae4fc23cc 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/geo_containment.ts @@ -12,13 +12,14 @@ import { executeEsQueryFactory, getShapesFilters, OTHER_CATEGORY } from './es_qu import { AlertServices } from '../../../../alerting/server'; import { ActionGroupId, - GEO_CONTAINMENT_ID, GeoContainmentInstanceState, GeoContainmentAlertType, GeoContainmentInstanceContext, GeoContainmentState, } from './alert_type'; +import { GEO_CONTAINMENT_ID } from './alert_type'; + export type LatestEntityLocation = GeoContainmentInstanceState; // Flatten agg results and get latest locations for each entity diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts index 023ea168a77d2..195ffb97bd81f 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/index.ts @@ -8,7 +8,6 @@ import { Logger } from 'src/core/server'; import { AlertingSetup } from '../../types'; import { - GeoContainmentParams, GeoContainmentState, GeoContainmentInstanceState, GeoContainmentInstanceContext, @@ -17,6 +16,8 @@ import { RecoveryActionGroupId, } from './alert_type'; +import { GeoContainmentExtractedParams, GeoContainmentParams } from './alert_type'; + interface RegisterParams { logger: Logger; alerting: AlertingSetup; @@ -26,7 +27,7 @@ export function register(params: RegisterParams) { const { logger, alerting } = params; alerting.registerType< GeoContainmentParams, - never, // Only use if defining useSavedObjectReferences hook + GeoContainmentExtractedParams, GeoContainmentState, GeoContainmentInstanceState, GeoContainmentInstanceContext, diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts index e8f699eb06161..9fc382240d0be 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/alert_type.test.ts @@ -6,7 +6,12 @@ */ import { loggingSystemMock } from '../../../../../../../src/core/server/mocks'; -import { getAlertType, GeoContainmentParams } from '../alert_type'; +import { + getAlertType, + injectEntityAndBoundaryIds, + GeoContainmentParams, + extractEntityAndBoundaryReferences, +} from '../alert_type'; describe('alertType', () => { const logger = loggingSystemMock.create().get(); @@ -43,4 +48,94 @@ describe('alertType', () => { expect(alertType.validate?.params?.validate(params)).toBeTruthy(); }); + + test('injectEntityAndBoundaryIds', () => { + expect( + injectEntityAndBoundaryIds( + { + boundaryGeoField: 'geometry', + boundaryIndexRefName: 'boundary_index_boundaryid', + boundaryIndexTitle: 'boundary*', + boundaryType: 'entireIndex', + dateField: '@timestamp', + entity: 'vehicle_id', + geoField: 'geometry', + index: 'foo*', + indexRefName: 'tracked_index_foobar', + }, + [ + { + id: 'foreign', + name: 'foobar', + type: 'foreign', + }, + { + id: 'foobar', + name: 'tracked_index_foobar', + type: 'index-pattern', + }, + { + id: 'foreignToo', + name: 'boundary_index_shouldbeignored', + type: 'index-pattern', + }, + { + id: 'boundaryid', + name: 'boundary_index_boundaryid', + type: 'index-pattern', + }, + ] + ) + ).toEqual({ + index: 'foo*', + indexId: 'foobar', + geoField: 'geometry', + entity: 'vehicle_id', + dateField: '@timestamp', + boundaryType: 'entireIndex', + boundaryIndexTitle: 'boundary*', + boundaryIndexId: 'boundaryid', + boundaryGeoField: 'geometry', + }); + }); + + test('extractEntityAndBoundaryReferences', () => { + expect( + extractEntityAndBoundaryReferences({ + index: 'foo*', + indexId: 'foobar', + geoField: 'geometry', + entity: 'vehicle_id', + dateField: '@timestamp', + boundaryType: 'entireIndex', + boundaryIndexTitle: 'boundary*', + boundaryIndexId: 'boundaryid', + boundaryGeoField: 'geometry', + }) + ).toEqual({ + params: { + boundaryGeoField: 'geometry', + boundaryIndexRefName: 'boundary_index_boundaryid', + boundaryIndexTitle: 'boundary*', + boundaryType: 'entireIndex', + dateField: '@timestamp', + entity: 'vehicle_id', + geoField: 'geometry', + index: 'foo*', + indexRefName: 'tracked_index_foobar', + }, + references: [ + { + id: 'foobar', + name: 'tracked_index_foobar', + type: 'index-pattern', + }, + { + id: 'boundaryid', + name: 'boundary_index_boundaryid', + type: 'index-pattern', + }, + ], + }); + }); }); diff --git a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts index 364c484a02080..8b78441d174b2 100644 --- a/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts +++ b/x-pack/plugins/stack_alerts/server/alert_types/geo_containment/tests/geo_containment.test.ts @@ -17,11 +17,8 @@ import { getGeoContainmentExecutor, } from '../geo_containment'; import { OTHER_CATEGORY } from '../es_query_builder'; -import { - GeoContainmentInstanceContext, - GeoContainmentInstanceState, - GeoContainmentParams, -} from '../alert_type'; +import { GeoContainmentInstanceContext, GeoContainmentInstanceState } from '../alert_type'; +import type { GeoContainmentParams } from '../alert_type'; const alertInstanceFactory = (contextKeys: unknown[], testAlertActionArr: unknown[]) => (instanceId: string) => { diff --git a/x-pack/plugins/stack_alerts/server/index.ts b/x-pack/plugins/stack_alerts/server/index.ts index 9491f3e646c70..1ac774a2d6c3f 100644 --- a/x-pack/plugins/stack_alerts/server/index.ts +++ b/x-pack/plugins/stack_alerts/server/index.ts @@ -18,6 +18,7 @@ export const config: PluginConfigDescriptor = { const stackAlerts = get(settings, fromPath); if (stackAlerts?.enabled === false || stackAlerts?.enabled === true) { addDeprecation({ + configPath: 'xpack.stack_alerts.enabled', message: `"xpack.stack_alerts.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, correctiveActions: { manualSteps: [`Remove "xpack.stack_alerts.enabled" from your kibana configs.`], diff --git a/x-pack/plugins/stack_alerts/server/plugin.test.ts b/x-pack/plugins/stack_alerts/server/plugin.test.ts index b9263553173d2..b2bf076eaf49d 100644 --- a/x-pack/plugins/stack_alerts/server/plugin.test.ts +++ b/x-pack/plugins/stack_alerts/server/plugin.test.ts @@ -11,7 +11,8 @@ import { alertsMock } from '../../alerting/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; import { BUILT_IN_ALERTS_FEATURE } from './feature'; -describe('AlertingBuiltins Plugin', () => { +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 +describe.skip('AlertingBuiltins Plugin', () => { describe('setup()', () => { let context: ReturnType; let plugin: AlertingBuiltinsPlugin; diff --git a/x-pack/plugins/task_manager/server/index.ts b/x-pack/plugins/task_manager/server/index.ts index 84bee044c4de9..2a360fc1a1d90 100644 --- a/x-pack/plugins/task_manager/server/index.ts +++ b/x-pack/plugins/task_manager/server/index.ts @@ -49,6 +49,7 @@ export const config: PluginConfigDescriptor = { const taskManager = get(settings, fromPath); if (taskManager?.index) { addDeprecation({ + configPath: `${fromPath}.index`, documentationUrl: 'https://ela.st/kbn-remove-legacy-multitenancy', message: `"${fromPath}.index" is deprecated. Multitenancy by changing "kibana.index" will not be supported starting in 8.0. See https://ela.st/kbn-remove-legacy-multitenancy for more details`, correctiveActions: { @@ -61,6 +62,7 @@ export const config: PluginConfigDescriptor = { } if (taskManager?.max_workers > MAX_WORKERS_LIMIT) { addDeprecation({ + configPath: `${fromPath}.max_workers`, message: `setting "${fromPath}.max_workers" (${taskManager?.max_workers}) greater than ${MAX_WORKERS_LIMIT} is deprecated. Values greater than ${MAX_WORKERS_LIMIT} will not be supported starting in 8.0.`, correctiveActions: { manualSteps: [ @@ -75,6 +77,7 @@ export const config: PluginConfigDescriptor = { const taskManager = get(settings, fromPath); if (taskManager?.enabled === false || taskManager?.enabled === true) { addDeprecation({ + configPath: 'xpack.task_manager.enabled', message: `"xpack.task_manager.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, correctiveActions: { manualSteps: [`Remove "xpack.task_manager.enabled" from your kibana configs.`], diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_existing_case_button.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_existing_case_button.tsx index af19a6b7cdb74..30181a96aa70b 100644 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_existing_case_button.tsx +++ b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_existing_case_button.tsx @@ -32,7 +32,7 @@ const AddToCaseActionComponent: React.FC = ({ {userCanCrud && ( = ({ {userCanCrud && ( = ({ id: 0, items: [ { - icon: , - name: i18n.HIDE_COLUMN, + icon: , + name: i18n.REMOVE_COLUMN, onClick: () => { dispatch(tGridActions.removeColumn({ id: timelineId, columnId: header.id })); handleClosePopOverTrigger(); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx index 2e684b9eda989..47fcb8c8e1509 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx @@ -98,6 +98,7 @@ describe('helpers', () => { describe('getColumnHeaders', () => { // additional properties used by `EuiDataGrid`: const actions = { + showHide: false, showSortAsc: true, showSortDesc: true, }; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx index c658000e6d331..66ec3ec1c399f 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.tsx @@ -27,6 +27,7 @@ import { allowSorting } from '../helpers'; const defaultActions: EuiDataGridColumnActions = { showSortAsc: true, showSortDesc: true, + showHide: false, }; const getAllBrowserFields = (browserFields: BrowserFields): Array> => diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/translations.ts b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/translations.ts index 2d4fbcbd54cfa..202eef8d675b8 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/translations.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/translations.ts @@ -23,10 +23,6 @@ export const FULL_SCREEN = i18n.translate('xpack.timelines.timeline.fullScreenBu defaultMessage: 'Full screen', }); -export const HIDE_COLUMN = i18n.translate('xpack.timelines.timeline.hideColumnLabel', { - defaultMessage: 'Hide column', -}); - export const SORT_AZ = i18n.translate('xpack.timelines.timeline.sortAZLabel', { defaultMessage: 'Sort A-Z', }); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx index 50764af3c7f2f..5a7ae6e407b0b 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.test.tsx @@ -6,9 +6,11 @@ */ import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; import { BodyComponent, StatefulBodyProps } from '.'; import { Sort } from './sort'; +import { REMOVE_COLUMN } from './column_headers/translations'; import { Direction } from '../../../../common/search_strategy'; import { useMountAppended } from '../../utils/use_mount_appended'; import { defaultHeaders, mockBrowserFields, mockTimelineData, TestProviders } from '../../../mock'; @@ -273,4 +275,57 @@ describe('Body', () => { .find((c) => c.id === 'signal.rule.risk_score')?.cellActions ).toBeUndefined(); }); + + test('it does NOT render switches for hiding columns in the `EuiDataGrid` `Columns` popover', async () => { + render( + + + + ); + + // Click the `EuidDataGrid` `Columns` button to open the popover: + fireEvent.click(screen.getByTestId('dataGridColumnSelectorButton')); + + // `EuiDataGrid` renders switches for hiding in the `Columns` popover when `showColumnSelector.allowHide` is `true` + const switches = await screen.queryAllByRole('switch'); + + expect(switches.length).toBe(0); // no switches are rendered, because `allowHide` is `false` + }); + + test('it dispatches the `REMOVE_COLUMN` action when a user clicks `Remove column` in the column header popover', async () => { + render( + + + + ); + + // click the `@timestamp` column header to display the popover + fireEvent.click(screen.getByText('@timestamp')); + + // click the `Remove column` action in the popover + fireEvent.click(await screen.getByText(REMOVE_COLUMN)); + + expect(mockDispatch).toBeCalledWith({ + payload: { columnId: '@timestamp', id: 'timeline-test' }, + type: 'x-pack/timelines/t-grid/REMOVE_COLUMN', + }); + }); + + test('it dispatches the `UPDATE_COLUMN_WIDTH` action when a user resizes a column', async () => { + render( + + + + ); + + // simulate resizing the column + fireEvent.mouseDown(screen.getAllByTestId('dataGridColumnResizer')[0]); + fireEvent.mouseMove(screen.getAllByTestId('dataGridColumnResizer')[0]); + fireEvent.mouseUp(screen.getAllByTestId('dataGridColumnResizer')[0]); + + expect(mockDispatch).toBeCalledWith({ + payload: { columnId: '@timestamp', id: 'timeline-test', width: NaN }, + type: 'x-pack/timelines/t-grid/UPDATE_COLUMN_WIDTH', + }); + }); }); 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 619571a0c8e81..9e43c16fd5e6f 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 @@ -75,6 +75,7 @@ import { ViewSelection } from '../event_rendered_view/selector'; import { EventRenderedView } from '../event_rendered_view'; import { useDataGridHeightHack } from './height_hack'; import { Filter } from '../../../../../../../src/plugins/data/public'; +import { REMOVE_COLUMN } from './column_headers/translations'; const StatefulAlertStatusBulkActions = lazy( () => import('../toolbar/bulk_actions/alert_status_bulk_actions') @@ -497,7 +498,7 @@ export const BodyComponent = React.memo( showFullScreenSelector: false, } : { - showColumnSelector: { allowHide: true, allowReorder: true }, + showColumnSelector: { allowHide: false, allowReorder: true }, showSortSelector: true, showFullScreenSelector: true, }), @@ -559,13 +560,32 @@ export const BodyComponent = React.memo( [columnHeaders, dispatch, id, loadPage] ); - const [visibleColumns, setVisibleColumns] = useState(() => - columnHeaders.map(({ id: cid }) => cid) - ); // initializes to the full set of columns + const visibleColumns = useMemo(() => columnHeaders.map(({ id: cid }) => cid), [columnHeaders]); // the full set of columns - useEffect(() => { - setVisibleColumns(columnHeaders.map(({ id: cid }) => cid)); - }, [columnHeaders]); + const onColumnResize = useCallback( + ({ columnId, width }: { columnId: string; width: number }) => { + dispatch( + tGridActions.updateColumnWidth({ + columnId, + id, + width, + }) + ); + }, + [dispatch, id] + ); + + const onSetVisibleColumns = useCallback( + (newVisibleColumns: string[]) => { + dispatch( + tGridActions.updateColumnOrder({ + columnIds: newVisibleColumns, + id, + }) + ); + }, + [dispatch, id] + ); const setEventsLoading = useCallback( ({ eventIds, isLoading: loading }) => { @@ -654,6 +674,19 @@ export const BodyComponent = React.memo( return { ...header, + actions: { + ...header.actions, + additional: [ + { + iconType: 'cross', + label: REMOVE_COLUMN, + onClick: () => { + dispatch(tGridActions.removeColumn({ id, columnId: header.id })); + }, + size: 'xs', + }, + ], + }, ...(hasCellActions(header.id) ? { cellActions: @@ -663,7 +696,7 @@ export const BodyComponent = React.memo( : {}), }; }), - [columnHeaders, defaultCellActions, browserFields, data, pageSize, id] + [columnHeaders, defaultCellActions, browserFields, data, pageSize, id, dispatch] ); const renderTGridCellValue = useMemo(() => { @@ -761,7 +794,7 @@ export const BodyComponent = React.memo( data-test-subj="body-data-grid" aria-label={i18n.TGRID_BODY_ARIA_LABEL} columns={columnsWithCellActions} - columnVisibility={{ visibleColumns, setVisibleColumns }} + columnVisibility={{ visibleColumns, setVisibleColumns: onSetVisibleColumns }} gridStyle={gridStyle} leadingControlColumns={leadingTGridControlColumns} trailingControlColumns={trailingTGridControlColumns} @@ -769,6 +802,7 @@ export const BodyComponent = React.memo( rowCount={totalItems} renderCellValue={renderTGridCellValue} sorting={{ columns: sortingColumns, onSort }} + onColumnResize={onColumnResize} pagination={{ pageIndex: activePage, pageSize, diff --git a/x-pack/plugins/timelines/public/store/t_grid/actions.ts b/x-pack/plugins/timelines/public/store/t_grid/actions.ts index a039a236fb186..feab12b616c78 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/actions.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/actions.ts @@ -32,6 +32,17 @@ export const applyDeltaToColumnWidth = actionCreator<{ delta: number; }>('APPLY_DELTA_TO_COLUMN_WIDTH'); +export const updateColumnOrder = actionCreator<{ + columnIds: string[]; + id: string; +}>('UPDATE_COLUMN_ORDER'); + +export const updateColumnWidth = actionCreator<{ + columnId: string; + id: string; + width: number; +}>('UPDATE_COLUMN_WIDTH'); + export type ToggleDetailPanel = TimelineExpandedDetailType & { tabType?: TimelineTabs; timelineId: string; diff --git a/x-pack/plugins/timelines/public/store/t_grid/helpers.test.tsx b/x-pack/plugins/timelines/public/store/t_grid/helpers.test.tsx index 121e5bda78ed8..1e1fbe290a115 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/store/t_grid/helpers.test.tsx @@ -7,7 +7,11 @@ import { SortColumnTimeline } from '../../../common'; import { tGridDefaults } from './defaults'; -import { setInitializeTgridSettings } from './helpers'; +import { + setInitializeTgridSettings, + updateTGridColumnOrder, + updateTGridColumnWidth, +} from './helpers'; import { mockGlobalState } from '../../mock/global_state'; import { TGridModelSettings } from '.'; @@ -57,3 +61,112 @@ describe('setInitializeTgridSettings', () => { expect(result).toBe(timelineById); }); }); + +describe('updateTGridColumnOrder', () => { + test('it returns the columns in the new expected order', () => { + const originalIdOrder = defaultTimelineById.test.columns.map((x) => x.id); // ['@timestamp', 'event.severity', 'event.category', '...'] + + // the new order swaps the positions of the first and second columns: + const newIdOrder = [originalIdOrder[1], originalIdOrder[0], ...originalIdOrder.slice(2)]; // ['event.severity', '@timestamp', 'event.category', '...'] + + expect( + updateTGridColumnOrder({ + columnIds: newIdOrder, + id: 'test', + timelineById: defaultTimelineById, + }) + ).toEqual({ + ...defaultTimelineById, + test: { + ...defaultTimelineById.test, + columns: [ + defaultTimelineById.test.columns[1], // event.severity + defaultTimelineById.test.columns[0], // @timestamp + ...defaultTimelineById.test.columns.slice(2), // all remaining columns + ], + }, + }); + }); + + test('it omits unknown column IDs when re-ordering columns', () => { + const originalIdOrder = defaultTimelineById.test.columns.map((x) => x.id); // ['@timestamp', 'event.severity', 'event.category', '...'] + const unknownColumId = 'does.not.exist'; + const newIdOrder = [originalIdOrder[0], unknownColumId, ...originalIdOrder.slice(1)]; // ['@timestamp', 'does.not.exist', 'event.severity', 'event.category', '...'] + + expect( + updateTGridColumnOrder({ + columnIds: newIdOrder, + id: 'test', + timelineById: defaultTimelineById, + }) + ).toEqual({ + ...defaultTimelineById, + test: { + ...defaultTimelineById.test, + }, + }); + }); + + test('it returns an empty collection of columns if none of the new column IDs are found', () => { + const newIdOrder = ['this.id.does.NOT.exist', 'this.id.also.does.NOT.exist']; // all unknown IDs + + expect( + updateTGridColumnOrder({ + columnIds: newIdOrder, + id: 'test', + timelineById: defaultTimelineById, + }) + ).toEqual({ + ...defaultTimelineById, + test: { + ...defaultTimelineById.test, + columns: [], // <-- empty, because none of the new column IDs match the old IDs + }, + }); + }); +}); + +describe('updateTGridColumnWidth', () => { + test("it updates (only) the specified column's width", () => { + const columnId = '@timestamp'; + const width = 1234; + + const expectedUpdatedColumn = { + ...defaultTimelineById.test.columns[0], // @timestamp + initialWidth: width, + }; + + expect( + updateTGridColumnWidth({ + columnId, + id: 'test', + timelineById: defaultTimelineById, + width, + }) + ).toEqual({ + ...defaultTimelineById, + test: { + ...defaultTimelineById.test, + columns: [expectedUpdatedColumn, ...defaultTimelineById.test.columns.slice(1)], + }, + }); + }); + + test('it is a noop if the the specified column is unknown', () => { + const unknownColumId = 'does.not.exist'; + + expect( + updateTGridColumnWidth({ + columnId: unknownColumId, + id: 'test', + timelineById: defaultTimelineById, + width: 90210, + }) + ).toEqual({ + ...defaultTimelineById, + test: { + ...defaultTimelineById.test, + }, + }); + }); +}); diff --git a/x-pack/plugins/timelines/public/store/t_grid/helpers.ts b/x-pack/plugins/timelines/public/store/t_grid/helpers.ts index f7b0d86f88621..34de86d32a9b2 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/helpers.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/helpers.ts @@ -8,6 +8,7 @@ import { omit, union } from 'lodash/fp'; import { isEmpty } from 'lodash'; +import { EuiDataGridColumn } from '@elastic/eui'; import type { ToggleDetailPanel } from './actions'; import { TGridPersistInput, TimelineById, TimelineId } from './types'; import type { TGridModel, TGridModelSettings } from './model'; @@ -232,6 +233,63 @@ export const applyDeltaToTimelineColumnWidth = ({ }; }; +type Columns = Array< + Pick & ColumnHeaderOptions +>; + +export const updateTGridColumnOrder = ({ + columnIds, + id, + timelineById, +}: { + columnIds: string[]; + id: string; + timelineById: TimelineById; +}): TimelineById => { + const timeline = timelineById[id]; + + const columns = columnIds.reduce((acc, cid) => { + const columnIndex = timeline.columns.findIndex((c) => c.id === cid); + + return columnIndex !== -1 ? [...acc, timeline.columns[columnIndex]] : acc; + }, []); + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + +export const updateTGridColumnWidth = ({ + columnId, + id, + timelineById, + width, +}: { + columnId: string; + id: string; + timelineById: TimelineById; + width: number; +}): TimelineById => { + const timeline = timelineById[id]; + + const columns = timeline.columns.map((x) => ({ + ...x, + initialWidth: x.id === columnId ? width : x.initialWidth, + })); + + return { + ...timelineById, + [id]: { + ...timeline, + columns, + }, + }; +}; + interface UpdateTimelineColumnsParams { id: string; columns: ColumnHeaderOptions[]; diff --git a/x-pack/plugins/timelines/public/store/t_grid/reducer.ts b/x-pack/plugins/timelines/public/store/t_grid/reducer.ts index d29240d5658db..d3af1dc4e9b30 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/reducer.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/reducer.ts @@ -23,7 +23,9 @@ import { setSelected, setTimelineUpdatedAt, toggleDetailPanel, + updateColumnOrder, updateColumns, + updateColumnWidth, updateIsLoading, updateItemsPerPage, updateItemsPerPageOptions, @@ -40,6 +42,8 @@ import { setDeletedTimelineEvents, setLoadingTimelineEvents, setSelectedTimelineEvents, + updateTGridColumnOrder, + updateTGridColumnWidth, updateTimelineColumns, updateTimelineItemsPerPage, updateTimelinePerPageOptions, @@ -91,6 +95,23 @@ export const tGridReducer = reducerWithInitialState(initialTGridState) timelineById: state.timelineById, }), })) + .case(updateColumnOrder, (state, { id, columnIds }) => ({ + ...state, + timelineById: updateTGridColumnOrder({ + columnIds, + id, + timelineById: state.timelineById, + }), + })) + .case(updateColumnWidth, (state, { id, columnId, width }) => ({ + ...state, + timelineById: updateTGridColumnWidth({ + columnId, + id, + timelineById: state.timelineById, + width, + }), + })) .case(removeColumn, (state, { id, columnId }) => ({ ...state, timelineById: removeTimelineColumn({ diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index a563079a101c3..52446ddc4a89f 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -4619,8 +4619,6 @@ "telemetry.provideUsageStatisticsTitle": "使用統計を提供", "telemetry.readOurUsageDataPrivacyStatementLinkText": "プライバシーポリシー", "telemetry.securityData": "Endpoint Security データ", - "telemetry.seeExampleOfClusterData": "収集する {clusterData} の例をご覧ください。", - "telemetry.seeExampleOfClusterDataAndEndpointSecuity": "当社が収集する{clusterData}および{endpointSecurityData}の例をご覧ください。", "telemetry.telemetryBannerDescription": "Elastic Stackの改善にご協力ください使用状況データの収集は現在無効です。使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。", "telemetry.telemetryConfigAndLinkDescription": "使用状況データの収集を有効にすると、製品とサービスを管理して改善することができます。詳細については、{privacyStatementLink}をご覧ください。", "telemetry.telemetryConfigDescription": "基本的な機能の利用状況に関する統計情報を提供して、Elastic Stack の改善にご協力ください。このデータは Elastic 社外と共有されません。", @@ -10810,7 +10808,6 @@ "xpack.fleet.enrollmentInstructions.moreInstructionsLink": "Elastic エージェントドキュメント", "xpack.fleet.enrollmentInstructions.moreInstructionsText": "RPM/DEB デプロイの手順については、{link}を参照してください。", "xpack.fleet.enrollmentInstructions.platformSelectAriaLabel": "プラットフォーム", - "xpack.fleet.enrollmentInstructions.platformSelectLabel": "プラットフォーム", "xpack.fleet.enrollmentInstructions.troubleshootingLink": "トラブルシューティングガイド", "xpack.fleet.enrollmentInstructions.troubleshootingText": "接続の問題が発生している場合は、{link}を参照してください。", "xpack.fleet.enrollmentStepAgentPolicy.enrollmentTokenSelectLabel": "登録トークン", @@ -10907,7 +10904,6 @@ "xpack.fleet.fleetServerSetup.generateServiceTokenDescription": "サービストークンは、Elasticsearchに書き込むためのFleetサーバーアクセス権を付与します。", "xpack.fleet.fleetServerSetup.installAgentDescription": "エージェントディレクトリから、適切なクイックスタートコマンドをコピーして実行し、生成されたトークンと自己署名証明書を使用して、ElasticエージェントをFleetサーバーとして起動します。本番デプロイで独自の証明書を使用する手順については、{userGuideLink}を参照してください。すべてのコマンドには管理者権限が必要です。", "xpack.fleet.fleetServerSetup.platformSelectAriaLabel": "プラットフォーム", - "xpack.fleet.fleetServerSetup.platformSelectLabel": "プラットフォーム", "xpack.fleet.fleetServerSetup.productionText": "本番運用", "xpack.fleet.fleetServerSetup.quickStartText": "クイックスタート", "xpack.fleet.fleetServerSetup.saveServiceTokenDescription": "サービストークン情報を保存します。これは1回だけ表示されます。", @@ -24617,7 +24613,6 @@ "xpack.timelines.timeline.fieldTooltip": "フィールド", "xpack.timelines.timeline.flyout.pane.removeColumnButtonLabel": "列を削除", "xpack.timelines.timeline.fullScreenButton": "全画面", - "xpack.timelines.timeline.hideColumnLabel": "列を非表示", "xpack.timelines.timeline.openedAlertFailedToastMessage": "アラートを開けませんでした", "xpack.timelines.timeline.openSelectedTitle": "選択した項目を開く", "xpack.timelines.timeline.properties.timelineToggleButtonAriaLabel": "タイムライン {title} を{isOpen, select, false {開く} true {閉じる} other {切り替える}}", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 4781e206cac20..10cfb10a01445 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -4665,8 +4665,6 @@ "telemetry.provideUsageStatisticsTitle": "提供使用情况统计", "telemetry.readOurUsageDataPrivacyStatementLinkText": "隐私声明", "telemetry.securityData": "终端安全数据", - "telemetry.seeExampleOfClusterData": "查看我们收集的{clusterData}的示例。", - "telemetry.seeExampleOfClusterDataAndEndpointSecuity": "查看我们收集的{clusterData}和 {endpointSecurityData}示例。", "telemetry.telemetryBannerDescription": "想帮助我们改进 Elastic Stack?数据使用情况收集当前已禁用。启用使用情况数据收集可帮助我们管理并改善产品和服务。有关更多详情,请参阅我们的{privacyStatementLink}。", "telemetry.telemetryConfigAndLinkDescription": "启用使用情况数据收集可帮助我们管理并改善产品和服务。有关更多详情,请参阅我们的{privacyStatementLink}。", "telemetry.telemetryConfigDescription": "通过提供基本功能的使用情况统计信息,来帮助我们改进 Elastic Stack。我们不会在 Elastic 之外共享此数据。", @@ -10925,7 +10923,6 @@ "xpack.fleet.enrollmentInstructions.moreInstructionsLink": "Elastic 代理文档", "xpack.fleet.enrollmentInstructions.moreInstructionsText": "有关 RPM/DEB 部署说明,请参见 {link}。", "xpack.fleet.enrollmentInstructions.platformSelectAriaLabel": "平台", - "xpack.fleet.enrollmentInstructions.platformSelectLabel": "平台", "xpack.fleet.enrollmentInstructions.troubleshootingLink": "故障排除指南", "xpack.fleet.enrollmentInstructions.troubleshootingText": "如果有连接问题,请参阅我们的{link}。", "xpack.fleet.enrollmentStepAgentPolicy.enrollmentTokenSelectLabel": "注册令牌", @@ -11022,7 +11019,6 @@ "xpack.fleet.fleetServerSetup.generateServiceTokenDescription": "服务令牌授予 Fleet 服务器向 Elasticsearch 写入的权限。", "xpack.fleet.fleetServerSetup.installAgentDescription": "从代理目录中,复制并运行适当的快速启动命令,以使用生成的令牌和自签名证书将 Elastic 代理启动为 Fleet 服务器。有关如何将自己的证书用于生产部署,请参阅 {userGuideLink}。所有命令都需要管理员权限。", "xpack.fleet.fleetServerSetup.platformSelectAriaLabel": "平台", - "xpack.fleet.fleetServerSetup.platformSelectLabel": "平台", "xpack.fleet.fleetServerSetup.productionText": "生产", "xpack.fleet.fleetServerSetup.quickStartText": "快速启动", "xpack.fleet.fleetServerSetup.saveServiceTokenDescription": "保存服务令牌信息。其仅显示一次。", @@ -25034,7 +25030,6 @@ "xpack.timelines.timeline.fieldTooltip": "字段", "xpack.timelines.timeline.flyout.pane.removeColumnButtonLabel": "移除列", "xpack.timelines.timeline.fullScreenButton": "全屏", - "xpack.timelines.timeline.hideColumnLabel": "隐藏列", "xpack.timelines.timeline.openedAlertFailedToastMessage": "无法打开告警", "xpack.timelines.timeline.openedAlertSuccessToastMessage": "已成功打开 {totalAlerts} 个{totalAlerts, plural, other {告警}}。", "xpack.timelines.timeline.openSelectedTitle": "打开所选", diff --git a/x-pack/plugins/triggers_actions_ui/server/index.ts b/x-pack/plugins/triggers_actions_ui/server/index.ts index c7d363af45247..72ca584250d03 100644 --- a/x-pack/plugins/triggers_actions_ui/server/index.ts +++ b/x-pack/plugins/triggers_actions_ui/server/index.ts @@ -31,6 +31,7 @@ export const config: PluginConfigDescriptor = { const triggersActionsUi = get(settings, fromPath); if (triggersActionsUi?.enabled === false || triggersActionsUi?.enabled === true) { addDeprecation({ + configPath: 'xpack.trigger_actions_ui.enabled', message: `"xpack.trigger_actions_ui.enabled" is deprecated. The ability to disable this plugin will be removed in 8.0.0.`, correctiveActions: { manualSteps: [`Remove "xpack.trigger_actions_ui.enabled" from your kibana configs.`], diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts index 1975d0abaf11d..fdd8a1c993937 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/default_deprecation_flyout.test.ts @@ -35,7 +35,7 @@ describe('Default deprecation flyout', () => { testBed.component.update(); }); - it('renders a flyout with deprecation details', async () => { + test('renders a flyout with deprecation details', async () => { const multiFieldsDeprecation = esDeprecationsMockResponse.deprecations[2]; const { actions, find, exists } = testBed; @@ -45,6 +45,9 @@ describe('Default deprecation flyout', () => { expect(find('defaultDeprecationDetails.flyoutTitle').text()).toContain( multiFieldsDeprecation.message ); + expect(find('defaultDeprecationDetails.documentationLink').props().href).toBe( + multiFieldsDeprecation.url + ); expect(find('defaultDeprecationDetails.flyoutDescription').text()).toContain( multiFieldsDeprecation.index ); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts index 145cea24dde8b..f62d24081ed56 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/index_settings_deprecation_flyout.test.ts @@ -33,16 +33,21 @@ describe('Index settings deprecation flyout', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { find, exists, actions, component } = testBed; - + const { actions, component } = testBed; component.update(); - await actions.table.clickDeprecationRowAt('indexSetting', 0); + }); + + test('renders a flyout with deprecation details', async () => { + const { find, exists } = testBed; expect(exists('indexSettingsDetails')).toBe(true); expect(find('indexSettingsDetails.flyoutTitle').text()).toContain( indexSettingDeprecation.message ); + expect(find('indexSettingsDetails.documentationLink').props().href).toBe( + indexSettingDeprecation.url + ); expect(exists('removeSettingsPrompt')).toBe(true); }); diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts index 6bcb3fa95985c..b24cd5a69a28e 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/es_deprecations/ml_snapshots_deprecation_flyout.test.ts @@ -35,16 +35,19 @@ describe('Machine learning deprecation flyout', () => { testBed = await setupElasticsearchPage({ isReadOnlyMode: false }); }); - const { find, exists, actions, component } = testBed; - + const { actions, component } = testBed; component.update(); - await actions.table.clickDeprecationRowAt('mlSnapshot', 0); + }); + + test('renders a flyout with deprecation details', async () => { + const { find, exists } = testBed; expect(exists('mlSnapshotDetails')).toBe(true); expect(find('mlSnapshotDetails.flyoutTitle').text()).toContain( 'Upgrade or delete model snapshot' ); + expect(find('mlSnapshotDetails.documentationLink').props().href).toBe(mlDeprecation.url); }); describe('upgrade snapshots', () => { diff --git a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/service.mock.ts b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/service.mock.ts index 1767143fdf527..6a3d376acecab 100644 --- a/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/service.mock.ts +++ b/x-pack/plugins/upgrade_assistant/__jest__/client_integration/kibana_deprecations/service.mock.ts @@ -22,6 +22,7 @@ const kibanaDeprecations: DomainDeprecationDetails[] = [ title: 'Test deprecation title 1', message: 'Test deprecation message 1', deprecationType: 'config', + configPath: 'test', }, { correctiveActions: { diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx index c436d585db9ae..6ec05b0c4fc99 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/default/flyout.tsx @@ -17,12 +17,11 @@ import { EuiTitle, EuiText, EuiTextColor, - EuiLink, EuiSpacer, } from '@elastic/eui'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; -import { DeprecationBadge } from '../../../shared'; +import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../../../shared'; export interface DefaultDeprecationFlyoutProps { deprecation: EnrichedDeprecationInfo; @@ -40,12 +39,6 @@ const i18nTexts = { }, } ), - learnMoreLinkLabel: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.deprecationDetailsFlyout.learnMoreLinkLabel', - { - defaultMessage: 'Learn more about this deprecation', - } - ), closeButtonLabel: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.deprecationDetailsFlyout.closeButtonLabel', { @@ -80,9 +73,7 @@ export const DefaultDeprecationFlyout = ({

{details}

- - {i18nTexts.learnMoreLinkLabel} - +

diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx index 3c19268a293f0..d0aac8ee922f7 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/index_settings/flyout.tsx @@ -19,7 +19,6 @@ import { EuiTitle, EuiText, EuiTextColor, - EuiLink, EuiSpacer, EuiCallOut, } from '@elastic/eui'; @@ -30,7 +29,7 @@ import { ResponseError, } from '../../../../../../common/types'; import type { Status } from '../../../types'; -import { DeprecationBadge } from '../../../shared'; +import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../../../shared'; export interface RemoveIndexSettingsFlyoutProps { deprecation: EnrichedDeprecationInfo; @@ -53,12 +52,6 @@ const i18nTexts = { }, } ), - learnMoreLinkLabel: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.removeSettingsFlyout.learnMoreLinkLabel', - { - defaultMessage: 'Learn more about this deprecation', - } - ), removeButtonLabel: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.removeSettingsFlyout.removeButtonLabel', { @@ -146,9 +139,7 @@ export const RemoveIndexSettingsFlyout = ({

{details}

- - {i18nTexts.learnMoreLinkLabel} - +

diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx index 4e3d77ba72ae8..c4145bf3d4146 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/es_deprecations/deprecation_types/ml_snapshots/flyout.tsx @@ -25,9 +25,9 @@ import { } from '@elastic/eui'; import { EnrichedDeprecationInfo } from '../../../../../../common/types'; -import { DeprecationBadge } from '../../../shared'; -import { MlSnapshotContext } from './context'; import { useAppContext } from '../../../../app_context'; +import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../../../shared'; +import { MlSnapshotContext } from './context'; import { SnapshotState } from './use_snapshot_state'; export interface FixSnapshotsFlyoutProps extends MlSnapshotContext { @@ -93,12 +93,6 @@ const i18nTexts = { defaultMessage: 'Error upgrading snapshot', } ), - learnMoreLinkLabel: i18n.translate( - 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.learnMoreLinkLabel', - { - defaultMessage: 'Learn more about this deprecation', - } - ), upgradeModeEnabledErrorTitle: i18n.translate( 'xpack.upgradeAssistant.esDeprecations.mlSnapshots.upgradeModeEnabledErrorTitle', { @@ -229,9 +223,7 @@ export const FixSnapshotsFlyout = ({

{deprecation.details}

- - {i18nTexts.learnMoreLinkLabel} - +

diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx index 577354b35fa4e..5d10350caad9e 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/deprecation_details_flyout.tsx @@ -20,12 +20,11 @@ import { EuiTitle, EuiText, EuiCallOut, - EuiLink, EuiSpacer, } from '@elastic/eui'; +import { DeprecationFlyoutLearnMoreLink, DeprecationBadge } from '../shared'; import type { DeprecationResolutionState, KibanaDeprecationDetails } from './kibana_deprecations'; -import { DeprecationBadge } from '../shared'; import './_deprecation_details_flyout.scss'; @@ -37,12 +36,6 @@ export interface DeprecationDetailsFlyoutProps { } const i18nTexts = { - learnMoreLinkLabel: i18n.translate( - 'xpack.upgradeAssistant.kibanaDeprecations.flyout.learnMoreLinkLabel', - { - defaultMessage: 'Learn more about this deprecation', - } - ), closeButtonLabel: i18n.translate( 'xpack.upgradeAssistant.kibanaDeprecations.flyout.closeButtonLabel', { @@ -162,12 +155,9 @@ export const DeprecationDetailsFlyout = ({

{message}

- {documentationUrl && (

- - {i18nTexts.learnMoreLinkLabel} - +

)}
diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx index 23697b00923c8..013f59a7dcf56 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/kibana_deprecations/kibana_deprecations.tsx @@ -75,10 +75,10 @@ export interface DeprecationResolutionState { resolveDeprecationError?: string; } -export interface KibanaDeprecationDetails extends DomainDeprecationDetails { +export type KibanaDeprecationDetails = DomainDeprecationDetails & { id: string; filterType: DomainDeprecationDetails['deprecationType'] | 'uncategorized'; -} +}; const getDeprecationCountByLevel = (deprecations: KibanaDeprecationDetails[]) => { const criticalDeprecations: KibanaDeprecationDetails[] = []; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx index b061ab5ea2d4d..590bfac96770d 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/overview/fix_issues_step/fix_issues_step.tsx @@ -70,7 +70,7 @@ export const getFixIssuesStep = ({

diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_count.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_count.tsx index 3312508a87073..32d214f0d80f2 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_count.tsx +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_count.tsx @@ -6,10 +6,11 @@ */ import React, { FunctionComponent } from 'react'; - import { EuiFlexGroup, EuiFlexItem, EuiHealth } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { LevelInfoTip } from './level_info_tip'; + const i18nTexts = { getCriticalStatusLabel: (count: number) => i18n.translate('xpack.upgradeAssistant.deprecationCount.criticalStatusLabel', { @@ -39,14 +40,31 @@ export const DeprecationCount: FunctionComponent = ({ return ( - - {i18nTexts.getCriticalStatusLabel(totalCriticalDeprecations)} - + + + + {i18nTexts.getCriticalStatusLabel(totalCriticalDeprecations)} + + + + + + + + - - {i18nTexts.getWarningStatusLabel(totalWarningDeprecations)} - + + + + {i18nTexts.getWarningStatusLabel(totalWarningDeprecations)} + + + + + + + ); diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_flyout_learn_more_link.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_flyout_learn_more_link.tsx new file mode 100644 index 0000000000000..da8c83597f7e2 --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/deprecation_flyout_learn_more_link.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 { i18n } from '@kbn/i18n'; +import { EuiLink } from '@elastic/eui'; + +interface Props { + documentationUrl?: string; +} + +export const DeprecationFlyoutLearnMoreLink = ({ documentationUrl }: Props) => { + return ( + + {i18n.translate('xpack.upgradeAssistant.deprecationFlyout.learnMoreLinkLabel', { + defaultMessage: 'Learn more', + })} + + ); +}; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts b/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts index ef7916f6e8d17..34496e1e8eb55 100644 --- a/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/index.ts @@ -9,3 +9,5 @@ export { NoDeprecationsPrompt } from './no_deprecations'; export { DeprecationCount } from './deprecation_count'; export { DeprecationBadge } from './deprecation_badge'; export { DeprecationsPageLoadingError } from './deprecations_page_loading_error'; +export { DeprecationFlyoutLearnMoreLink } from './deprecation_flyout_learn_more_link'; +export { LevelInfoTip } from './level_info_tip'; diff --git a/x-pack/plugins/upgrade_assistant/public/application/components/shared/level_info_tip.tsx b/x-pack/plugins/upgrade_assistant/public/application/components/shared/level_info_tip.tsx new file mode 100644 index 0000000000000..d3600a7290b4e --- /dev/null +++ b/x-pack/plugins/upgrade_assistant/public/application/components/shared/level_info_tip.tsx @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiIconTip } from '@elastic/eui'; + +const i18nTexts = { + critical: i18n.translate('xpack.upgradeAssistant.levelInfoTip.criticalLabel', { + defaultMessage: 'Critical issues must be resolved before you upgrade', + }), + warning: i18n.translate('xpack.upgradeAssistant.levelInfoTip.warningLabel', { + defaultMessage: 'Warning issues can be ignored at your discretion', + }), +}; + +interface Props { + level: 'critical' | 'warning'; +} + +export const LevelInfoTip: FunctionComponent = ({ level }) => { + return ; +}; 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 63532543a418b..296a313abef38 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 @@ -20,7 +20,7 @@ const mockKibanaDeprecations: DomainDeprecationDetails[] = [ 'Using Kibana role-mapping management, change all role-mappings which assing the kibana_user role to the kibana_admin role.', ], }, - deprecationType: 'config', + deprecationType: 'feature', documentationUrl: 'testDocUrl', level: 'critical', message: 'testMessage', diff --git a/x-pack/plugins/uptime/e2e/tasks/es_archiver.ts b/x-pack/plugins/uptime/e2e/tasks/es_archiver.ts index ce82be18dff7f..dac5672bdf649 100644 --- a/x-pack/plugins/uptime/e2e/tasks/es_archiver.ts +++ b/x-pack/plugins/uptime/e2e/tasks/es_archiver.ts @@ -17,7 +17,7 @@ export const esArchiverLoad = (folder: string) => { const path = Path.join(ES_ARCHIVE_DIR, folder); execSync( `node ../../../../scripts/es_archiver load "${path}" --config ../../../test/functional/config.js`, - { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED } } + { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; @@ -25,13 +25,13 @@ export const esArchiverUnload = (folder: string) => { const path = Path.join(ES_ARCHIVE_DIR, folder); execSync( `node ../../../../scripts/es_archiver unload "${path}" --config ../../../test/functional/config.js`, - { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED } } + { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; export const esArchiverResetKibana = () => { execSync( `node ../../../../scripts/es_archiver empty-kibana-index --config ../../../test/functional/config.js`, - { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED } } + { env: { ...process.env, NODE_TLS_REJECT_UNAUTHORIZED }, stdio: 'inherit' } ); }; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx index f16e72837b343..26ee26cc8ed7f 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx @@ -50,7 +50,8 @@ const defaultValidation = centralValidation[DataStream.HTTP]; const defaultHTTPConfig = defaultConfig[DataStream.HTTP]; const defaultTCPConfig = defaultConfig[DataStream.TCP]; -describe('', () => { +// unhandled promise rejection: https://github.com/elastic/kibana/issues/112699 +describe.skip('', () => { const WrappedComponent = ({ validate = defaultValidation, typeEditable = false, diff --git a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.test.tsx b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.test.tsx index d044ad4e6a3a2..240697af470b0 100644 --- a/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.test.tsx +++ b/x-pack/plugins/uptime/public/components/overview/monitor_list/monitor_list_drawer/monitor_list_drawer.test.tsx @@ -5,7 +5,6 @@ * 2.0. */ -import 'jest'; import React from 'react'; import { MonitorListDrawerComponent } from './monitor_list_drawer'; import { MonitorDetails, MonitorSummary, makePing } from '../../../../../common/runtime_types'; diff --git a/x-pack/plugins/uptime/public/hooks/use_url_params.ts b/x-pack/plugins/uptime/public/hooks/use_url_params.ts index 329e0ccef4d96..1318b635693c7 100644 --- a/x-pack/plugins/uptime/public/hooks/use_url_params.ts +++ b/x-pack/plugins/uptime/public/hooks/use_url_params.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { useCallback, useEffect, useMemo } from 'react'; +import { useCallback, useEffect } from 'react'; import { parse, stringify } from 'query-string'; import { useLocation, useHistory } from 'react-router-dom'; import { useDispatch, useSelector } from 'react-redux'; @@ -28,7 +28,7 @@ const getParsedParams = (search: string) => { export const useGetUrlParams: GetUrlParams = () => { const { search } = useLocation(); - return useMemo(() => getSupportedUrlParams(getParsedParams(search)), [search]); + return getSupportedUrlParams(getParsedParams(search)); }; const getMapFromFilters = (value: any): Map | undefined => { diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 7d69e80dae584..7e8272b0a8afa 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -232,31 +232,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); expect(resp.body.retry).to.eql(false); - // Node.js 12 oddity: - // - // The first time after the server is booted, the error message will be: - // - // undefined is not iterable (cannot read property Symbol(Symbol.iterator)) - // - // After this, the error will be: - // - // Cannot destructure property 'value' of 'undefined' as it is undefined. - // - // The error seems to come from the exact same place in the code based on the - // exact same circomstances: - // - // https://github.com/elastic/kibana/blob/b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1/packages/kbn-config-schema/src/types/literal_type.ts#L28 - // - // What triggers the error is that the `handleError` function expects its 2nd - // argument to be an object containing a `valids` property of type array. - // - // In this test the object does not contain a `valids` property, so hence the - // error. - // - // Why the error message isn't the same in all scenarios is unknown to me and - // could be a bug in V8. - expect(resp.body.message).to.match( - /^error validating action params: (undefined is not iterable \(cannot read property Symbol\(Symbol.iterator\)\)|Cannot destructure property 'value' of 'undefined' as it is undefined\.)$/ + expect(resp.body.message).to.be( + `error validating action params: Cannot destructure property 'Symbol(Symbol.iterator)' of 'undefined' as it is undefined.` ); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts index 00989b35fd4e2..4421c984b4aed 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts @@ -234,31 +234,8 @@ export default function resilientTest({ getService }: FtrProviderContext) { expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); expect(resp.body.retry).to.eql(false); - // Node.js 12 oddity: - // - // The first time after the server is booted, the error message will be: - // - // undefined is not iterable (cannot read property Symbol(Symbol.iterator)) - // - // After this, the error will be: - // - // Cannot destructure property 'value' of 'undefined' as it is undefined. - // - // The error seems to come from the exact same place in the code based on the - // exact same circomstances: - // - // https://github.com/elastic/kibana/blob/b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1/packages/kbn-config-schema/src/types/literal_type.ts#L28 - // - // What triggers the error is that the `handleError` function expects its 2nd - // argument to be an object containing a `valids` property of type array. - // - // In this test the object does not contain a `valids` property, so hence the - // error. - // - // Why the error message isn't the same in all scenarios is unknown to me and - // could be a bug in V8. - expect(resp.body.message).to.match( - /^error validating action params: (undefined is not iterable \(cannot read property Symbol\(Symbol.iterator\)\)|Cannot destructure property 'value' of 'undefined' as it is undefined\.)$/ + expect(resp.body.message).to.be( + `error validating action params: Cannot destructure property 'Symbol(Symbol.iterator)' of 'undefined' as it is undefined.` ); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts index fe1ebdf8d28a9..5ff1663975145 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_itsm.ts @@ -242,31 +242,8 @@ export default function serviceNowITSMTest({ getService }: FtrProviderContext) { expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); expect(resp.body.retry).to.eql(false); - // Node.js 12 oddity: - // - // The first time after the server is booted, the error message will be: - // - // undefined is not iterable (cannot read property Symbol(Symbol.iterator)) - // - // After this, the error will be: - // - // Cannot destructure property 'value' of 'undefined' as it is undefined. - // - // The error seems to come from the exact same place in the code based on the - // exact same circumstances: - // - // https://github.com/elastic/kibana/blob/b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1/packages/kbn-config-schema/src/types/literal_type.ts#L28 - // - // What triggers the error is that the `handleError` function expects its 2nd - // argument to be an object containing a `valids` property of type array. - // - // In this test the object does not contain a `valids` property, so hence the - // error. - // - // Why the error message isn't the same in all scenarios is unknown to me and - // could be a bug in V8. - expect(resp.body.message).to.match( - /^error validating action params: (undefined is not iterable \(cannot read property Symbol\(Symbol.iterator\)\)|Cannot destructure property 'value' of 'undefined' as it is undefined\.)$/ + expect(resp.body.message).to.be( + `error validating action params: Cannot destructure property 'Symbol(Symbol.iterator)' of 'undefined' as it is undefined.` ); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts index eee3425b6a61f..bc4ec43fb4c7b 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/servicenow_sir.ts @@ -246,31 +246,8 @@ export default function serviceNowSIRTest({ getService }: FtrProviderContext) { expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); expect(resp.body.retry).to.eql(false); - // Node.js 12 oddity: - // - // The first time after the server is booted, the error message will be: - // - // undefined is not iterable (cannot read property Symbol(Symbol.iterator)) - // - // After this, the error will be: - // - // Cannot destructure property 'value' of 'undefined' as it is undefined. - // - // The error seems to come from the exact same place in the code based on the - // exact same circumstances: - // - // https://github.com/elastic/kibana/blob/b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1/packages/kbn-config-schema/src/types/literal_type.ts#L28 - // - // What triggers the error is that the `handleError` function expects its 2nd - // argument to be an object containing a `valids` property of type array. - // - // In this test the object does not contain a `valids` property, so hence the - // error. - // - // Why the error message isn't the same in all scenarios is unknown to me and - // could be a bug in V8. - expect(resp.body.message).to.match( - /^error validating action params: (undefined is not iterable \(cannot read property Symbol\(Symbol.iterator\)\)|Cannot destructure property 'value' of 'undefined' as it is undefined\.)$/ + expect(resp.body.message).to.be( + `error validating action params: Cannot destructure property 'Symbol(Symbol.iterator)' of 'undefined' as it is undefined.` ); }); }); diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts index eae630593b4df..93d3a6c9e003f 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts @@ -323,31 +323,8 @@ export default function swimlaneTest({ getService }: FtrProviderContext) { expect(resp.body.connector_id).to.eql(simulatedActionId); expect(resp.body.status).to.eql('error'); expect(resp.body.retry).to.eql(false); - // Node.js 12 oddity: - // - // The first time after the server is booted, the error message will be: - // - // undefined is not iterable (cannot read property Symbol(Symbol.iterator)) - // - // After this, the error will be: - // - // Cannot destructure property 'value' of 'undefined' as it is undefined. - // - // The error seems to come from the exact same place in the code based on the - // exact same circomstances: - // - // https://github.com/elastic/kibana/blob/b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1/packages/kbn-config-schema/src/types/literal_type.ts#L28 - // - // What triggers the error is that the `handleError` function expects its 2nd - // argument to be an object containing a `valids` property of type array. - // - // In this test the object does not contain a `valids` property, so hence the - // error. - // - // Why the error message isn't the same in all scenarios is unknown to me and - // could be a bug in V8. - expect(resp.body.message).to.match( - /^error validating action params: (undefined is not iterable \(cannot read property Symbol\(Symbol.iterator\)\)|Cannot destructure property 'value' of 'undefined' as it is undefined\.)$/ + expect(resp.body.message).to.be( + `error validating action params: undefined is not iterable (cannot read property Symbol(Symbol.iterator))` ); }); }); diff --git a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts index 00b820a025c8b..2742fbff294c0 100644 --- a/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/recognize_module.ts @@ -44,7 +44,7 @@ export default ({ getService }: FtrProviderContext) => { user: USER.ML_POWERUSER, expected: { responseCode: 200, - moduleIds: ['apm_jsbase', 'apm_nodejs'], + moduleIds: ['apm_jsbase', 'apm_transaction', 'apm_nodejs'], }, }, { diff --git a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts index 6ff6b8113cb1a..c4dd529ac14f5 100644 --- a/x-pack/test/api_integration/apis/ml/modules/setup_module.ts +++ b/x-pack/test/api_integration/apis/ml/modules/setup_module.ts @@ -187,11 +187,9 @@ export default ({ getService }: FtrProviderContext) => { dashboards: [] as string[], }, }, - // Set startDatafeed and estimateModelMemory to false for the APM transaction test - // until there is a new data set available with metric data. { testTitleSuffix: - 'for apm_transaction with prefix, startDatafeed false and estimateModelMemory false', + 'for apm_transaction with prefix, startDatafeed true and estimateModelMemory true', sourceDataArchive: 'x-pack/test/functional/es_archives/ml/module_apm', indexPattern: { name: 'ft_module_apm', timeField: '@timestamp' }, module: 'apm_transaction', @@ -199,14 +197,14 @@ export default ({ getService }: FtrProviderContext) => { requestBody: { prefix: 'pf5_', indexPatternName: 'ft_module_apm', - startDatafeed: false, - estimateModelMemory: false, + startDatafeed: true, + end: Date.now(), }, expected: { responseCode: 200, jobs: [ { - jobId: 'pf5_apm_metrics', + jobId: 'pf5_high_mean_transaction_duration', jobState: JOB_STATE.CLOSED, datafeedState: DATAFEED_STATE.STOPPED, }, diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 8caae0afe746e..c15a7d39a6cf6 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -229,6 +229,10 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./historical_data/has_data')); }); + describe('latency/service_apis', function () { + loadTestFile(require.resolve('./latency/service_apis')); + }); + registry.run(providerContext); }); } diff --git a/x-pack/test/apm_api_integration/tests/latency/service_apis.ts b/x-pack/test/apm_api_integration/tests/latency/service_apis.ts new file mode 100644 index 0000000000000..a09442cd73a2a --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/latency/service_apis.ts @@ -0,0 +1,191 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { service, timerange } from '@elastic/apm-generator'; +import expect from '@kbn/expect'; +import { meanBy, sumBy } from 'lodash'; +import { LatencyAggregationType } from '../../../../plugins/apm/common/latency_aggregation_types'; +import { isFiniteNumber } from '../../../../plugins/apm/common/utils/is_finite_number'; +import { PromiseReturnType } from '../../../../plugins/observability/typings/common'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + const traceData = getService('traceData'); + + const serviceName = 'synth-go'; + const start = new Date('2021-01-01T00:00:00.000Z').getTime(); + const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1; + + async function getLatencyValues({ + processorEvent, + latencyAggregationType = LatencyAggregationType.avg, + }: { + processorEvent: 'transaction' | 'metric'; + latencyAggregationType?: LatencyAggregationType; + }) { + const commonQuery = { + start: new Date(start).toISOString(), + end: new Date(end).toISOString(), + environment: 'ENVIRONMENT_ALL', + }; + const [ + serviceInventoryAPIResponse, + serviceLantencyAPIResponse, + transactionsGroupDetailsAPIResponse, + serviceInstancesAPIResponse, + ] = await Promise.all([ + apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services', + params: { + query: { + ...commonQuery, + kuery: `service.name : "${serviceName}" and processor.event : "${processorEvent}"`, + }, + }, + }), + apmApiClient.readUser({ + endpoint: 'GET /internal/apm/services/{serviceName}/transactions/charts/latency', + params: { + path: { serviceName }, + query: { + ...commonQuery, + kuery: `processor.event : "${processorEvent}"`, + latencyAggregationType, + transactionType: 'request', + }, + }, + }), + apmApiClient.readUser({ + endpoint: `GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics`, + params: { + path: { serviceName }, + query: { + ...commonQuery, + kuery: `processor.event : "${processorEvent}"`, + transactionType: 'request', + latencyAggregationType: 'avg' as LatencyAggregationType, + }, + }, + }), + apmApiClient.readUser({ + endpoint: `GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics`, + params: { + path: { serviceName }, + query: { + ...commonQuery, + kuery: `processor.event : "${processorEvent}"`, + transactionType: 'request', + latencyAggregationType: 'avg' as LatencyAggregationType, + }, + }, + }), + ]); + + const serviceInventoryLatency = serviceInventoryAPIResponse.body.items[0].latency; + + const latencyChartApiMean = meanBy( + serviceLantencyAPIResponse.body.currentPeriod.latencyTimeseries.filter( + (item) => isFiniteNumber(item.y) && item.y > 0 + ), + 'y' + ); + + const transactionsGroupLatencySum = sumBy( + transactionsGroupDetailsAPIResponse.body.transactionGroups, + 'latency' + ); + + const serviceInstancesLatencySum = sumBy( + serviceInstancesAPIResponse.body.currentPeriod, + 'latency' + ); + + return { + serviceInventoryLatency, + latencyChartApiMean, + transactionsGroupLatencySum, + serviceInstancesLatencySum, + }; + } + + let latencyMetricValues: PromiseReturnType; + let latencyTransactionValues: PromiseReturnType; + + registry.when('Services APIs', { config: 'basic', archives: ['apm_8.0.0_empty'] }, () => { + describe('when data is loaded ', () => { + const GO_PROD_RATE = 80; + const GO_DEV_RATE = 20; + const GO_PROD_DURATION = 1000; + const GO_DEV_DURATION = 500; + before(async () => { + const serviceGoProdInstance = service(serviceName, 'production', 'go').instance( + 'instance-a' + ); + const serviceGoDevInstance = service(serviceName, 'development', 'go').instance( + 'instance-b' + ); + await traceData.index([ + ...timerange(start, end) + .interval('1m') + .rate(GO_PROD_RATE) + .flatMap((timestamp) => + serviceGoProdInstance + .transaction('GET /api/product/list') + .duration(GO_PROD_DURATION) + .timestamp(timestamp) + .serialize() + ), + ...timerange(start, end) + .interval('1m') + .rate(GO_DEV_RATE) + .flatMap((timestamp) => + serviceGoDevInstance + .transaction('GET /api/product/:id') + .duration(GO_DEV_DURATION) + .timestamp(timestamp) + .serialize() + ), + ]); + }); + + after(() => traceData.clean()); + + describe('compare latency value between service inventory, latency chart, service inventory and transactions apis', () => { + before(async () => { + [latencyTransactionValues, latencyMetricValues] = await Promise.all([ + getLatencyValues({ processorEvent: 'transaction' }), + getLatencyValues({ processorEvent: 'metric' }), + ]); + }); + + it('returns same avg latency value for Transaction-based and Metric-based data', () => { + const expectedLatencyAvgValueMs = + ((GO_PROD_RATE * GO_PROD_DURATION + GO_DEV_RATE * GO_DEV_DURATION) / + (GO_PROD_RATE + GO_DEV_RATE)) * + 1000; + [ + latencyTransactionValues.latencyChartApiMean, + latencyTransactionValues.serviceInventoryLatency, + latencyMetricValues.latencyChartApiMean, + latencyMetricValues.serviceInventoryLatency, + ].forEach((value) => expect(value).to.be.equal(expectedLatencyAvgValueMs)); + }); + + it('returns same sum latency value for Transaction-based and Metric-based data', () => { + const expectedLatencySumValueMs = (GO_PROD_DURATION + GO_DEV_DURATION) * 1000; + [ + latencyTransactionValues.transactionsGroupLatencySum, + latencyTransactionValues.serviceInstancesLatencySum, + latencyMetricValues.transactionsGroupLatencySum, + latencyMetricValues.serviceInstancesLatencySum, + ].forEach((value) => expect(value).to.be.equal(expectedLatencySumValueMs)); + }); + }); + }); + }); +} diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts index 9c169c1c34207..d5e9050ed9d41 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/ip_array.ts @@ -486,7 +486,8 @@ export default ({ getService }: FtrProviderContext) => { expect(ips).to.eql([[], ['127.0.0.8', '127.0.0.9', '127.0.0.10']]); }); - it('will return 1 result if we have a list that includes all ips', async () => { + // FLAKY https://github.com/elastic/kibana/issues/89052 + it.skip('will return 1 result if we have a list that includes all ips', async () => { await importFile( supertest, 'ip', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts index 2a2c8df30981f..e852558aaa6a8 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/keyword_array.ts @@ -328,7 +328,8 @@ export default ({ getService }: FtrProviderContext) => { }); describe('"exists" operator', () => { - it('will return 1 results if matching against keyword for the empty array', async () => { + // FLAKY https://github.com/elastic/kibana/issues/115308 + it.skip('will return 1 results if matching against keyword for the empty array', async () => { const rule = getRuleForSignalTesting(['keyword_as_array']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ @@ -496,7 +497,8 @@ export default ({ getService }: FtrProviderContext) => { expect(hits).to.eql([[], ['word eight', 'word nine', 'word ten']]); }); - it('will return only the empty array for results if we have a list that includes all keyword', async () => { + // FLAKY https://github.com/elastic/kibana/issues/115304 + it.skip('will return only the empty array for results if we have a list that includes all keyword', async () => { await importFile( supertest, 'keyword', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts index b152b44867a09..f0a5fe7c1ffb1 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/exception_operators_data_types/text_array.ts @@ -326,7 +326,8 @@ export default ({ getService }: FtrProviderContext) => { }); describe('"exists" operator', () => { - it('will return 1 results if matching against text for the empty array', async () => { + // FLAKY https://github.com/elastic/kibana/issues/115313 + it.skip('will return 1 results if matching against text for the empty array', async () => { const rule = getRuleForSignalTesting(['text_as_array']); const { id } = await createRuleWithExceptionEntries(supertest, rule, [ [ @@ -494,7 +495,8 @@ export default ({ getService }: FtrProviderContext) => { expect(hits).to.eql([[], ['word eight', 'word nine', 'word ten']]); }); - it('will return only the empty array for results if we have a list that includes all text', async () => { + // FLAKY https://github.com/elastic/kibana/issues/113418 + it.skip('will return only the empty array for results if we have a list that includes all text', async () => { await importFile( supertest, 'text', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts index d25fb5bfa5899..4c0f21df8c0ff 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/migrations.ts @@ -65,6 +65,27 @@ export default ({ getService }: FtrProviderContext): void => { undefined ); }); + + it('migrates legacy siem-detection-engine-rule-actions and retains "ruleThrottle" and "alertThrottle" as the same attributes as before', async () => { + const response = await es.get<{ + 'siem-detection-engine-rule-actions': { + ruleThrottle: string; + alertThrottle: string; + }; + }>({ + index: '.kibana', + id: 'siem-detection-engine-rule-actions:fce024a0-0452-11ec-9b15-d13d79d162f3', + }); + expect(response.statusCode).to.eql(200); + + // "alertThrottle" and "ruleThrottle" should still exist + expect(response.body._source?.['siem-detection-engine-rule-actions'].alertThrottle).to.eql( + '7d' + ); + expect(response.body._source?.['siem-detection-engine-rule-actions'].ruleThrottle).to.eql( + '7d' + ); + }); }); }); }; diff --git a/x-pack/test/fleet_api_integration/apis/epm/index.js b/x-pack/test/fleet_api_integration/apis/epm/index.js index b6a1fd5d7346d..3428b4c1ded08 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/index.js +++ b/x-pack/test/fleet_api_integration/apis/epm/index.js @@ -15,6 +15,7 @@ export default function loadTests({ loadTestFile }) { loadTestFile(require.resolve('./template')); loadTestFile(require.resolve('./ilm')); loadTestFile(require.resolve('./install_by_upload')); + loadTestFile(require.resolve('./install_endpoint')); loadTestFile(require.resolve('./install_overrides')); loadTestFile(require.resolve('./install_prerelease')); loadTestFile(require.resolve('./install_remove_assets')); diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_endpoint.ts b/x-pack/test/fleet_api_integration/apis/epm/install_endpoint.ts new file mode 100644 index 0000000000000..ba9264e1d1999 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/epm/install_endpoint.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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { skipIfNoDockerRegistry } from '../../helpers'; +import { setupFleetAndAgents } from '../agents/services'; + +export default function (providerContext: FtrProviderContext) { + /** + * There are a few features that are only currently supported for the Endpoint + * package due to security concerns. + */ + describe('Install endpoint package', () => { + const { getService } = providerContext; + skipIfNoDockerRegistry(providerContext); + setupFleetAndAgents(providerContext); + + const supertest = getService('supertest'); + const dockerServers = getService('dockerServers'); + const server = dockerServers.get('registry'); + const es = getService('es'); + const pkgName = 'endpoint'; + let pkgVersion: string; + + const transforms = [ + { + id: 'endpoint.metadata_current-default', + dest: 'metrics-endpoint.metadata_current_default', + }, + { + id: 'endpoint.metadata_united-default', + dest: '.metrics-endpoint.metadata_united_default', + }, + ]; + + before(async () => { + if (!server.enabled) return; + // The latest endpoint package is already installed by default in our FTR config, + // just get the most recent version number. + const getResp = await supertest.get(`/api/fleet/epm/packages/${pkgName}`).expect(200); + pkgVersion = getResp.body.response.version; + }); + + describe('install', () => { + transforms.forEach((transform) => { + it(`should have installed the [${transform.id}] transform`, async function () { + const res = await es.transport.request({ + method: 'GET', + path: `/_transform/${transform.id}-${pkgVersion}`, + }); + expect(res.statusCode).equal(200); + }); + it(`should have created the destination index for the [${transform.id}] transform`, async function () { + // the index is defined in the transform file + const res = await es.transport.request({ + method: 'GET', + path: `/${transform.dest}`, + }); + expect(res.statusCode).equal(200); + }); + }); + }); + + const uninstallPackage = async (pkg: string) => + supertest.delete(`/api/fleet/epm/packages/${pkg}`).set('kbn-xsrf', 'xxxx'); + + // Endpoint doesn't currently support uninstalls + describe.skip('uninstall', () => { + before(async () => { + await uninstallPackage(`${pkgName}-${pkgVersion}`); + }); + + transforms.forEach((transform) => { + it(`should have uninstalled the [${transform.id}] transforms`, async function () { + const res = await es.transport.request( + { + method: 'GET', + path: `/_transform/${transform.id}`, + }, + { + ignore: [404], + } + ); + expect(res.statusCode).equal(404); + }); + + it(`should have deleted the index for the [${transform.id}] transform`, async function () { + // the index is defined in the transform file + const res = await es.transport.request( + { + method: 'GET', + path: `/${transform.dest}`, + }, + { + ignore: [404], + } + ); + expect(res.statusCode).equal(404); + }); + }); + }); + }); +} diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index e57899531e939..7e48ed9d297c7 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -155,31 +155,6 @@ export default function (providerContext: FtrProviderContext) { ); expect(resPipeline2.statusCode).equal(404); }); - it('should have uninstalled the transforms', async function () { - const res = await es.transport.request( - { - method: 'GET', - path: `/_transform/${pkgName}-test-default-${pkgVersion}`, - }, - { - ignore: [404], - } - ); - expect(res.statusCode).equal(404); - }); - it('should have deleted the index for the transform', async function () { - // the index is defined in the transform file - const res = await es.transport.request( - { - method: 'GET', - path: `/logs-all_assets.test_log_current_default`, - }, - { - ignore: [404], - } - ); - expect(res.statusCode).equal(404); - }); it('should have uninstalled the kibana assets', async function () { let resDashboard; try { @@ -380,21 +355,6 @@ const expectAssetsInstalled = ({ }); expect(resUserSettings.statusCode).equal(200); }); - it('should have installed the transform components', async function () { - const res = await es.transport.request({ - method: 'GET', - path: `/_transform/${pkgName}.test-default-${pkgVersion}`, - }); - expect(res.statusCode).equal(200); - }); - it('should have created the index for the transform', async function () { - // the index is defined in the transform file - const res = await es.transport.request({ - method: 'GET', - path: `/logs-all_assets.test_log_current_default`, - }); - expect(res.statusCode).equal(200); - }); it('should have installed the kibana assets', async function () { // These are installed from Fleet along with every package const resIndexPatternLogs = await kibanaServer.savedObjects.get({ @@ -575,10 +535,6 @@ const expectAssetsInstalled = ({ id: 'logs-all_assets.test_logs-0.1.0-pipeline2', type: 'ingest_pipeline', }, - { - id: 'all_assets.test-default-0.1.0', - type: 'transform', - }, ], es_index_patterns: { test_logs: 'logs-all_assets.test_logs-*', @@ -597,7 +553,6 @@ const expectAssetsInstalled = ({ { id: 'f839c76e-d194-555a-90a1-3265a45789e4', type: 'epm-packages-assets' }, { id: '9af7bbb3-7d8a-50fa-acc9-9dde6f5efca2', type: 'epm-packages-assets' }, { id: '1e97a20f-9d1c-529b-8ff2-da4e8ba8bb71', type: 'epm-packages-assets' }, - { id: '8cfe0a2b-7016-5522-87e4-6d352360d1fc', type: 'epm-packages-assets' }, { id: 'bd5ff3c5-655e-5385-9918-b60ff3040aad', type: 'epm-packages-assets' }, { id: '0954ce3b-3165-5c1f-a4c0-56eb5f2fa487', type: 'epm-packages-assets' }, { id: '60d6d054-57e4-590f-a580-52bf3f5e7cca', type: 'epm-packages-assets' }, diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/elasticsearch/transform/test/default.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/elasticsearch/transform/test/default.json deleted file mode 100644 index eddc6bc0c304a..0000000000000 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/all_assets/0.1.0/elasticsearch/transform/test/default.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "source": { - "index": "logs-all_assets.test_log-default*" - }, - "dest": { - "index": "logs-all_assets.test_log_current_default" - }, - "pivot": { - "group_by": { - "agent.id": { - "terms": { - "field": "agent.id" - } - } - }, - "aggregations": { - "HostDetails": { - "scripted_metric": { - "init_script": "state.timestamp_latest = 0L; state.last_doc=''", - "map_script": "def current_date = doc['@timestamp'].getValue().toInstant().toEpochMilli(); if (current_date \u003e state.timestamp_latest) {state.timestamp_latest = current_date;state.last_doc = new HashMap(params['_source']);}", - "combine_script": "return state", - "reduce_script": "def last_doc = '';def timestamp_latest = 0L; for (s in states) {if (s.timestamp_latest \u003e (timestamp_latest)) {timestamp_latest = s.timestamp_latest; last_doc = s.last_doc;}} return last_doc" - } - } - } - }, - "description": "collapse and update the latest document for each host", - "frequency": "1m", - "sync": { - "time": { - "field": "event.ingested", - "delay": "60s" - } - } -} diff --git a/x-pack/test/functional/apps/lens/formula.ts b/x-pack/test/functional/apps/lens/formula.ts index a8d074ad0631b..29caf422b7acd 100644 --- a/x-pack/test/functional/apps/lens/formula.ts +++ b/x-pack/test/functional/apps/lens/formula.ts @@ -247,11 +247,11 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { field: 'bytes', }); - await PageObjects.lens.createLayer('threshold'); + await PageObjects.lens.createLayer('referenceLine'); await PageObjects.lens.configureDimension( { - dimension: 'lnsXY_yThresholdLeftPanel > lns-dimensionTrigger', + dimension: 'lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger', operation: 'formula', formula: `count()`, keepOpen: true, @@ -263,9 +263,9 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.closeDimensionEditor(); await PageObjects.common.sleep(1000); - expect(await PageObjects.lens.getDimensionTriggerText('lnsXY_yThresholdLeftPanel', 0)).to.eql( - 'count()' - ); + expect( + await PageObjects.lens.getDimensionTriggerText('lnsXY_yReferenceLineLeftPanel', 0) + ).to.eql('count()'); }); it('should allow numeric only formulas', async () => { diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index a6f579a9a4890..38cf7759a7782 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -43,7 +43,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./lens_tagging')); loadTestFile(require.resolve('./formula')); loadTestFile(require.resolve('./heatmap')); - loadTestFile(require.resolve('./thresholds')); + loadTestFile(require.resolve('./reference_lines')); loadTestFile(require.resolve('./inspector')); // has to be last one in the suite because it overrides saved objects diff --git a/x-pack/test/functional/apps/lens/thresholds.ts b/x-pack/test/functional/apps/lens/reference_lines.ts similarity index 57% rename from x-pack/test/functional/apps/lens/thresholds.ts rename to x-pack/test/functional/apps/lens/reference_lines.ts index 10e330114442b..8ea66cb29f5a8 100644 --- a/x-pack/test/functional/apps/lens/thresholds.ts +++ b/x-pack/test/functional/apps/lens/reference_lines.ts @@ -14,21 +14,21 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { const retry = getService('retry'); const testSubjects = getService('testSubjects'); - describe('lens thresholds tests', () => { - it('should show a disabled threshold layer button if no data dimension is defined', async () => { + describe('lens reference lines tests', () => { + it('should show a disabled reference layer button if no data dimension is defined', async () => { await PageObjects.visualize.navigateToNewVisualization(); await PageObjects.visualize.clickVisType('lens'); await testSubjects.click('lnsLayerAddButton'); await retry.waitFor('wait for layer popup to appear', async () => - testSubjects.exists(`lnsLayerAddButton-threshold`) + testSubjects.exists(`lnsLayerAddButton-referenceLine`) ); expect( - await (await testSubjects.find(`lnsLayerAddButton-threshold`)).getAttribute('disabled') + await (await testSubjects.find(`lnsLayerAddButton-referenceLine`)).getAttribute('disabled') ).to.be('true'); }); - it('should add a threshold layer with a static value in it', async () => { + it('should add a reference layer with a static value in it', async () => { await PageObjects.lens.goToTimeRange(); await PageObjects.lens.configureDimension({ @@ -43,29 +43,28 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { field: 'bytes', }); - await PageObjects.lens.createLayer('threshold'); + await PageObjects.lens.createLayer('referenceLine'); expect((await find.allByCssSelector(`[data-test-subj^="lns-layerPanel-"]`)).length).to.eql(2); expect( await ( - await testSubjects.find('lnsXY_yThresholdLeftPanel > lns-dimensionTrigger') + await testSubjects.find('lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger') ).getVisibleText() ).to.eql('Static value: 4992.44'); }); - it('should create a dynamic threshold when dragging a field to a threshold dimension group', async () => { + it('should create a dynamic referenceLine when dragging a field to a referenceLine dimension group', async () => { await PageObjects.lens.dragFieldToDimensionTrigger( 'bytes', - 'lnsXY_yThresholdLeftPanel > lns-empty-dimension' + 'lnsXY_yReferenceLineLeftPanel > lns-empty-dimension' ); - expect(await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yThresholdLeftPanel')).to.eql([ - 'Static value: 4992.44', - 'Median of bytes', - ]); + expect( + await PageObjects.lens.getDimensionTriggersTexts('lnsXY_yReferenceLineLeftPanel') + ).to.eql(['Static value: 4992.44', 'Median of bytes']); }); - it('should add a new group to the threshold layer when a right axis is enabled', async () => { + it('should add a new group to the reference layer when a right axis is enabled', async () => { await PageObjects.lens.configureDimension({ dimension: 'lnsXY_yDimensionPanel > lns-empty-dimension', operation: 'average', @@ -77,42 +76,46 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await PageObjects.lens.closeDimensionEditor(); - await testSubjects.existOrFail('lnsXY_yThresholdRightPanel > lns-empty-dimension'); + await testSubjects.existOrFail('lnsXY_yReferenceLineRightPanel > lns-empty-dimension'); }); - it('should carry the style when moving a threshold to another group', async () => { + it('should carry the style when moving a reference line to another group', async () => { // style it enabling the fill - await testSubjects.click('lnsXY_yThresholdLeftPanel > lns-dimensionTrigger'); - await testSubjects.click('lnsXY_fill_below'); + await testSubjects.click('lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger'); + await testSubjects.click('lnsXY_referenceLine_fill_below'); await PageObjects.lens.closeDimensionEditor(); // drag and drop it to the left axis await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_yThresholdLeftPanel > lns-dimensionTrigger', - 'lnsXY_yThresholdRightPanel > lns-empty-dimension' + 'lnsXY_yReferenceLineLeftPanel > lns-dimensionTrigger', + 'lnsXY_yReferenceLineRightPanel > lns-empty-dimension' ); - await testSubjects.click('lnsXY_yThresholdRightPanel > lns-dimensionTrigger'); + await testSubjects.click('lnsXY_yReferenceLineRightPanel > lns-dimensionTrigger'); expect( - await find.existsByCssSelector('[data-test-subj="lnsXY_fill_below"][class$="isSelected"]') + await find.existsByCssSelector( + '[data-test-subj="lnsXY_referenceLine_fill_below"][class$="isSelected"]' + ) ).to.be(true); await PageObjects.lens.closeDimensionEditor(); }); - it('should duplicate also the original style when duplicating a threshold', async () => { + it('should duplicate also the original style when duplicating a reference line', async () => { // drag and drop to the empty field to generate a duplicate await PageObjects.lens.dragDimensionToDimension( - 'lnsXY_yThresholdRightPanel > lns-dimensionTrigger', - 'lnsXY_yThresholdRightPanel > lns-empty-dimension' + 'lnsXY_yReferenceLineRightPanel > lns-dimensionTrigger', + 'lnsXY_yReferenceLineRightPanel > lns-empty-dimension' ); await ( await find.byCssSelector( - '[data-test-subj="lnsXY_yThresholdRightPanel"]:nth-child(2) [data-test-subj="lns-dimensionTrigger"]' + '[data-test-subj="lnsXY_yReferenceLineRightPanel"]:nth-child(2) [data-test-subj="lns-dimensionTrigger"]' ) ).click(); expect( - await find.existsByCssSelector('[data-test-subj="lnsXY_fill_below"][class$="isSelected"]') + await find.existsByCssSelector( + '[data-test-subj="lnsXY_referenceLine_fill_below"][class$="isSelected"]' + ) ).to.be(true); await PageObjects.lens.closeDimensionEditor(); }); diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js b/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js index 6b1658dd9ed0e..07fda7a143a99 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail.js @@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }) { const nodesList = getService('monitoringElasticsearchNodes'); const nodeDetail = getService('monitoringElasticsearchNodeDetail'); - describe('Elasticsearch node detail', () => { + // FLAKY https://github.com/elastic/kibana/issues/115130 + describe.skip('Elasticsearch node detail', () => { describe('Active Nodes', () => { const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); diff --git a/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail_mb.js b/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail_mb.js index 9130ce91e7b4d..70c9b42b37f42 100644 --- a/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail_mb.js +++ b/x-pack/test/functional/apps/monitoring/elasticsearch/node_detail_mb.js @@ -14,7 +14,8 @@ export default function ({ getService, getPageObjects }) { const nodesList = getService('monitoringElasticsearchNodes'); const nodeDetail = getService('monitoringElasticsearchNodeDetail'); - describe('Elasticsearch node detail mb', () => { + // Failing: See https://github.com/elastic/kibana/issues/115255 + describe.skip('Elasticsearch node detail mb', () => { describe('Active Nodes', () => { const { setup, tearDown } = getLifecycleMethods(getService, getPageObjects); diff --git a/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js b/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js index 79bd479c45a17..cce6401453d21 100644 --- a/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js +++ b/x-pack/test/functional/apps/monitoring/enable_monitoring/index.js @@ -11,7 +11,6 @@ export default function ({ getService, getPageObjects }) { const PageObjects = getPageObjects(['monitoring', 'common', 'header']); const esSupertest = getService('esSupertest'); const noData = getService('monitoringNoData'); - const testSubjects = getService('testSubjects'); const clusterOverview = getService('monitoringClusterOverview'); const retry = getService('retry'); const esDeleteAllIndices = getService('esDeleteAllIndices'); @@ -53,8 +52,6 @@ export default function ({ getService, getPageObjects }) { // Here we are checking that once Monitoring is enabled, // it moves on to the cluster overview page. await retry.tryForTime(20000, async () => { - // Click the refresh button - await testSubjects.click('querySubmitButton'); await clusterOverview.closeAlertsModal(); expect(await clusterOverview.isOnClusterOverview()).to.be(true); }); diff --git a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts index 988bbdc621f5f..bf83892ce1934 100644 --- a/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts +++ b/x-pack/test/functional/apps/monitoring/feature_controls/monitoring_security.ts @@ -13,6 +13,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const security = getService('security'); const appsMenu = getService('appsMenu'); const PageObjects = getPageObjects(['common', 'security']); + const noData = getService('monitoringNoData'); describe('security', () => { before(async () => { @@ -103,5 +104,32 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { expect(navLinks).to.contain('Stack Monitoring'); }); }); + + describe('monitoring_user and kibana_admin roles', function () { + this.tags(['skipCloud']); + before(async () => { + await security.user.create('monitoring_kibana_admin_user', { + password: 'monitoring_user-password', + roles: ['monitoring_user', 'kibana_admin'], + full_name: 'monitoring user', + }); + + await PageObjects.security.login( + 'monitoring_kibana_admin_user', + 'monitoring_user-password' + ); + }); + + after(async () => { + await security.user.delete('monitoring_kibana_admin_user'); + }); + + it('denies enabling monitoring without enough permissions', async () => { + await PageObjects.common.navigateToApp('monitoring'); + await noData.isOnNoDataPage(); + await noData.clickSetupWithSelfMonitoring(); + expect(await noData.isOnNoDataPageMonitoringEnablementDenied()).to.be(true); + }); + }); }); } diff --git a/x-pack/test/functional/services/monitoring/no_data.js b/x-pack/test/functional/services/monitoring/no_data.js index 7b4410425dcfe..bd34c45c2d293 100644 --- a/x-pack/test/functional/services/monitoring/no_data.js +++ b/x-pack/test/functional/services/monitoring/no_data.js @@ -30,5 +30,13 @@ export function MonitoringNoDataProvider({ getService }) { const pageId = await retry.try(() => testSubjects.find('noDataContainer')); return pageId !== null; } + + async isOnNoDataPageMonitoringEnablementDenied() { + return testSubjects.exists('weTriedContainer'); + } + + async clickSetupWithSelfMonitoring() { + await testSubjects.click('useInternalCollection'); + } })(); } diff --git a/x-pack/test/functional/services/observability/alerts/add_to_case.ts b/x-pack/test/functional/services/observability/alerts/add_to_case.ts new file mode 100644 index 0000000000000..1669fc69a683c --- /dev/null +++ b/x-pack/test/functional/services/observability/alerts/add_to_case.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +const ADD_TO_EXISTING_CASE_SELECTOR = 'add-existing-case-menu-item'; +const ADD_TO_NEW_CASE_SELECTOR = 'add-new-case-item'; +const CREATE_CASE_FLYOUT = 'create-case-flyout'; +const SELECT_CASE_MODAL = 'all-cases-modal'; + +export function ObservabilityAlertsAddToCaseProvider({ getService }: FtrProviderContext) { + const testSubjects = getService('testSubjects'); + + const getAddToExistingCaseSelector = async () => { + return await testSubjects.find(ADD_TO_EXISTING_CASE_SELECTOR); + }; + + const getAddToExistingCaseSelectorOrFail = async () => { + return await testSubjects.existOrFail(ADD_TO_EXISTING_CASE_SELECTOR); + }; + + const missingAddToExistingCaseSelectorOrFail = async () => { + return await testSubjects.missingOrFail(ADD_TO_EXISTING_CASE_SELECTOR); + }; + + const getAddToNewCaseSelector = async () => { + return await testSubjects.find(ADD_TO_NEW_CASE_SELECTOR); + }; + + const getAddToNewCaseSelectorOrFail = async () => { + return await testSubjects.existOrFail(ADD_TO_NEW_CASE_SELECTOR); + }; + + const missingAddToNewCaseSelectorOrFail = async () => { + return await testSubjects.missingOrFail(ADD_TO_NEW_CASE_SELECTOR); + }; + + const addToNewCaseButtonClick = async () => { + return await (await getAddToNewCaseSelector()).click(); + }; + + const addToExistingCaseButtonClick = async () => { + return await (await getAddToExistingCaseSelector()).click(); + }; + + const getCreateCaseFlyoutOrFail = async () => { + return await testSubjects.existOrFail(CREATE_CASE_FLYOUT); + }; + + const closeFlyout = async () => { + return await (await testSubjects.find('euiFlyoutCloseButton')).click(); + }; + + const getAddtoExistingCaseModalOrFail = async () => { + return await testSubjects.existOrFail(SELECT_CASE_MODAL); + }; + + return { + getAddToExistingCaseSelector, + getAddToExistingCaseSelectorOrFail, + missingAddToExistingCaseSelectorOrFail, + getAddToNewCaseSelector, + getAddToNewCaseSelectorOrFail, + missingAddToNewCaseSelectorOrFail, + getCreateCaseFlyoutOrFail, + closeFlyout, + addToNewCaseButtonClick, + addToExistingCaseButtonClick, + getAddtoExistingCaseModalOrFail, + }; +} diff --git a/x-pack/test/functional/services/observability/alerts/common.ts b/x-pack/test/functional/services/observability/alerts/common.ts index 7098fdec2a9d4..d5a2ce2a18c41 100644 --- a/x-pack/test/functional/services/observability/alerts/common.ts +++ b/x-pack/test/functional/services/observability/alerts/common.ts @@ -204,5 +204,6 @@ export function ObservabilityAlertsCommonProvider({ setWorkflowStatusFilter, submitQuery, typeInQueryBar, + openActionsMenuForRow, }; } diff --git a/x-pack/test/functional/services/observability/alerts/index.ts b/x-pack/test/functional/services/observability/alerts/index.ts index f373b0d75c543..f2b5173dfe5b0 100644 --- a/x-pack/test/functional/services/observability/alerts/index.ts +++ b/x-pack/test/functional/services/observability/alerts/index.ts @@ -7,15 +7,17 @@ import { ObservabilityAlertsPaginationProvider } from './pagination'; import { ObservabilityAlertsCommonProvider } from './common'; +import { ObservabilityAlertsAddToCaseProvider } from './add_to_case'; import { FtrProviderContext } from '../../../ftr_provider_context'; export function ObservabilityAlertsProvider(context: FtrProviderContext) { const common = ObservabilityAlertsCommonProvider(context); const pagination = ObservabilityAlertsPaginationProvider(context); - + const addToCase = ObservabilityAlertsAddToCaseProvider(context); return { common, pagination, + addToCase, }; } diff --git a/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts b/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts new file mode 100644 index 0000000000000..f29111f2cb66b --- /dev/null +++ b/x-pack/test/observability_functional/apps/observability/alerts/add_to_case.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default ({ getService, getPageObjects }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const observability = getService('observability'); + const retry = getService('retry'); + + describe('Observability alerts / Add to case', function () { + this.tags('includeFirefox'); + + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); + }); + + describe('When user has all priviledges for cases', () => { + before(async () => { + await observability.users.setTestUserRole( + observability.users.defineBasicObservabilityRole({ + observabilityCases: ['all'], + logs: ['all'], + }) + ); + await observability.alerts.common.navigateToTimeWithData(); + }); + + after(async () => { + await observability.users.restoreDefaultTestUserRole(); + }); + + it('renders case options in the overflow menu', async () => { + await observability.alerts.common.openActionsMenuForRow(0); + await retry.try(async () => { + await observability.alerts.addToCase.getAddToExistingCaseSelectorOrFail(); + await observability.alerts.addToCase.getAddToNewCaseSelectorOrFail(); + }); + }); + + it('opens a flyout when Add to new case is clicked', async () => { + await observability.alerts.addToCase.addToNewCaseButtonClick(); + + await retry.try(async () => { + await observability.alerts.addToCase.getCreateCaseFlyoutOrFail(); + await observability.alerts.addToCase.closeFlyout(); + }); + }); + + it('opens a modal when Add to existing case is clicked', async () => { + await observability.alerts.common.openActionsMenuForRow(0); + + await retry.try(async () => { + await observability.alerts.addToCase.addToExistingCaseButtonClick(); + await observability.alerts.addToCase.getAddtoExistingCaseModalOrFail(); + }); + }); + }); + + describe('When user has read permissions for cases', () => { + before(async () => { + await observability.users.setTestUserRole( + observability.users.defineBasicObservabilityRole({ + observabilityCases: ['read'], + logs: ['all'], + }) + ); + await observability.alerts.common.navigateToTimeWithData(); + }); + + after(async () => { + await observability.users.restoreDefaultTestUserRole(); + }); + + it('does not render case options in the overflow menu', async () => { + await observability.alerts.common.openActionsMenuForRow(0); + await retry.try(async () => { + await observability.alerts.addToCase.missingAddToExistingCaseSelectorOrFail(); + await observability.alerts.addToCase.missingAddToNewCaseSelectorOrFail(); + }); + }); + }); + }); +}; diff --git a/x-pack/test/observability_functional/apps/observability/index.ts b/x-pack/test/observability_functional/apps/observability/index.ts index b163d4d6bb8d5..43e056bae65c0 100644 --- a/x-pack/test/observability_functional/apps/observability/index.ts +++ b/x-pack/test/observability_functional/apps/observability/index.ts @@ -15,5 +15,6 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./alerts')); loadTestFile(require.resolve('./alerts/workflow_status')); loadTestFile(require.resolve('./alerts/pagination')); + loadTestFile(require.resolve('./alerts/add_to_case')); }); } 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 6d78c69798e94..323b08dd88be1 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 @@ -313,6 +313,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { advanced: { agent: { connection_delay: 'true' } }, malware: { mode: 'prevent' }, behavior_protection: { mode: 'prevent', supported: true }, + memory_protection: { mode: 'prevent', supported: true }, popup: { malware: { enabled: true, @@ -322,6 +323,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { enabled: true, message: 'Elastic Security {action} {rule}', }, + memory_protection: { + enabled: true, + message: 'Elastic Security {action} {rule}', + }, }, }, mac: { @@ -329,6 +334,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { logging: { file: 'info' }, malware: { mode: 'prevent' }, behavior_protection: { mode: 'prevent', supported: true }, + memory_protection: { mode: 'prevent', supported: true }, popup: { malware: { enabled: true, @@ -338,6 +344,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { enabled: true, message: 'Elastic Security {action} {rule}', }, + memory_protection: { + enabled: true, + message: 'Elastic Security {action} {rule}', + }, }, }, windows: { @@ -537,6 +547,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { advanced: { agent: { connection_delay: 'true' } }, malware: { mode: 'prevent' }, behavior_protection: { mode: 'prevent', supported: true }, + memory_protection: { mode: 'prevent', supported: true }, popup: { malware: { enabled: true, @@ -546,6 +557,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { enabled: true, message: 'Elastic Security {action} {rule}', }, + memory_protection: { + enabled: true, + message: 'Elastic Security {action} {rule}', + }, }, }, mac: { @@ -553,6 +568,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { logging: { file: 'info' }, malware: { mode: 'prevent' }, behavior_protection: { mode: 'prevent', supported: true }, + memory_protection: { mode: 'prevent', supported: true }, popup: { malware: { enabled: true, @@ -562,6 +578,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { enabled: true, message: 'Elastic Security {action} {rule}', }, + memory_protection: { + enabled: true, + message: 'Elastic Security {action} {rule}', + }, }, }, windows: { @@ -758,6 +778,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { logging: { file: 'info' }, malware: { mode: 'prevent' }, behavior_protection: { mode: 'prevent', supported: true }, + memory_protection: { mode: 'prevent', supported: true }, popup: { malware: { enabled: true, @@ -767,6 +788,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { enabled: true, message: 'Elastic Security {action} {rule}', }, + memory_protection: { + enabled: true, + message: 'Elastic Security {action} {rule}', + }, }, }, mac: { @@ -774,6 +799,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { logging: { file: 'info' }, malware: { mode: 'prevent' }, behavior_protection: { mode: 'prevent', supported: true }, + memory_protection: { mode: 'prevent', supported: true }, popup: { malware: { enabled: true, @@ -783,6 +809,10 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { enabled: true, message: 'Elastic Security {action} {rule}', }, + memory_protection: { + enabled: true, + message: 'Elastic Security {action} {rule}', + }, }, }, windows: { diff --git a/yarn.lock b/yarn.lock index b2d03018a0e0c..94e95113f772a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6617,10 +6617,10 @@ resolved "https://registry.yarnpkg.com/@types/mocha/-/mocha-8.2.0.tgz#3eb56d13a1de1d347ecb1957c6860c911704bc44" integrity sha512-/Sge3BymXo4lKc31C8OINJgXLaw+7vL1/L1pGiBNpGrBiT8FQiaFpSYV0uhTaG4y78vcMBTMFsWaHDvuD+xGzQ== -"@types/mock-fs@^4.10.0": - version "4.10.0" - resolved "https://registry.yarnpkg.com/@types/mock-fs/-/mock-fs-4.10.0.tgz#460061b186993d76856f669d5317cda8a007c24b" - integrity sha512-FQ5alSzmHMmliqcL36JqIA4Yyn9jyJKvRSGV3mvPh108VFatX7naJDzSG4fnFQNZFq9dIx0Dzoe6ddflMB2Xkg== +"@types/mock-fs@^4.13.1": + version "4.13.1" + resolved "https://registry.yarnpkg.com/@types/mock-fs/-/mock-fs-4.13.1.tgz#9201554ceb23671badbfa8ac3f1fa9e0706305be" + integrity sha512-m6nFAJ3lBSnqbvDZioawRvpLXSaPyn52Srf7OfzjubYbYX8MTUdIgDxQl0wEapm4m/pNYSd9TXocpQ0TvZFlYA== dependencies: "@types/node" "*" @@ -6672,10 +6672,10 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@14.14.44", "@types/node@8.10.54", "@types/node@>= 8", "@types/node@>=8.9.0", "@types/node@^10.1.0", "@types/node@^14.14.31": - version "14.14.44" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.44.tgz#df7503e6002847b834371c004b372529f3f85215" - integrity sha512-+gaugz6Oce6ZInfI/tK4Pq5wIIkJMEJUu92RB3Eu93mtj4wjjjz9EB5mLp5s1pSsLXdC/CPut/xF20ZzAQJbTA== +"@types/node@*", "@types/node@16.10.2", "@types/node@8.10.54", "@types/node@>= 8", "@types/node@>=8.9.0", "@types/node@^10.1.0", "@types/node@^14.14.31": + version "16.10.2" + resolved "https://registry.yarnpkg.com/@types/node/-/node-16.10.2.tgz#5764ca9aa94470adb4e1185fe2e9f19458992b2e" + integrity sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ== "@types/nodemailer@^6.4.0": version "6.4.0" @@ -7743,14 +7743,7 @@ agent-base@5: resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-5.1.1.tgz#e8fb3f242959db44d63be665db7a8e739537a32c" integrity sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g== -agent-base@6: - version "6.0.0" - resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.0.tgz#5d0101f19bbfaed39980b22ae866de153b93f09a" - integrity sha512-j1Q7cSCqN+AwrmDd+pzgqc0/NpC655x2bUf5ZjRIO77DcNBFmh+OgRNzF6OKdCC9RSCb19fGd99+bhXFdkRNqw== - dependencies: - debug "4" - -agent-base@^6.0.2: +agent-base@6, 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== @@ -20750,10 +20743,10 @@ mochawesome@^6.2.1: strip-ansi "^6.0.0" uuid "^7.0.3" -mock-fs@^4.12.0: - version "4.12.0" - resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-4.12.0.tgz#a5d50b12d2d75e5bec9dac3b67ffe3c41d31ade4" - integrity sha512-/P/HtrlvBxY4o/PzXY9cCNBrdylDNxg7gnrv2sMNxj+UJ2m8jSpl0/A6fuJeNAWr99ZvGWH8XCbE0vmnM5KupQ== +mock-fs@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/mock-fs/-/mock-fs-5.1.1.tgz#d4c95e916abf400664197079d7e399d133bb6048" + integrity sha512-p/8oZ3qvfKGPw+4wdVCyjDxa6wn2tP0TCf3WXC1UyUBAevezPn1TtOoxtMYVbZu/S/iExg+Ghed1busItj2CEw== mock-http-server@1.3.0: version "1.3.0"