diff --git a/.buildkite/scripts/bootstrap.sh b/.buildkite/scripts/bootstrap.sh index df38c105d2fd30..272cd0a0861702 100755 --- a/.buildkite/scripts/bootstrap.sh +++ b/.buildkite/scripts/bootstrap.sh @@ -6,7 +6,17 @@ source .buildkite/scripts/common/util.sh source .buildkite/scripts/common/setup_bazel.sh echo "--- yarn install and bootstrap" -retry 2 15 yarn kbn bootstrap +if ! yarn kbn bootstrap; then + echo "bootstrap failed, trying again in 15 seconds" + sleep 15 + + # Most bootstrap failures will result in a problem inside node_modules that does not get fixed on the next bootstrap + # So, we should just delete node_modules in between attempts + rm -rf node_modules + + echo "--- yarn install and bootstrap, attempt 2" + yarn kbn bootstrap +fi ### ### upload ts-refs-cache artifacts as quickly as possible so they are available for download diff --git a/.buildkite/scripts/common/env.sh b/.buildkite/scripts/common/env.sh index cd33cdc714cbef..0715b07fd58e86 100755 --- a/.buildkite/scripts/common/env.sh +++ b/.buildkite/scripts/common/env.sh @@ -42,7 +42,11 @@ if is_pr; then export ELASTIC_APM_ACTIVE=false fi - export CHECKS_REPORTER_ACTIVE=true + if [[ "${GITHUB_STEP_COMMIT_STATUS_ENABLED:-}" != "true" ]]; then + export CHECKS_REPORTER_ACTIVE=true + else + export CHECKS_REPORTER_ACTIVE=false + fi # These can be removed once we're not supporting Jenkins and Buildkite at the same time # These are primarily used by github checks reporter and can be configured via /github_checks_api.json diff --git a/.buildkite/scripts/lifecycle/post_build.sh b/.buildkite/scripts/lifecycle/post_build.sh index 35e5a6006ee243..5a181e8fa5489b 100755 --- a/.buildkite/scripts/lifecycle/post_build.sh +++ b/.buildkite/scripts/lifecycle/post_build.sh @@ -5,7 +5,9 @@ set -euo pipefail BUILD_SUCCESSFUL=$(node "$(dirname "${0}")/build_status.js") export BUILD_SUCCESSFUL -"$(dirname "${0}")/commit_status_complete.sh" +if [[ "${GITHUB_BUILD_COMMIT_STATUS_ENABLED:-}" != "true" ]]; then + "$(dirname "${0}")/commit_status_complete.sh" +fi node "$(dirname "${0}")/ci_stats_complete.js" diff --git a/.buildkite/scripts/lifecycle/pre_build.sh b/.buildkite/scripts/lifecycle/pre_build.sh index d91597a00a0801..d901594e36ce41 100755 --- a/.buildkite/scripts/lifecycle/pre_build.sh +++ b/.buildkite/scripts/lifecycle/pre_build.sh @@ -4,7 +4,9 @@ set -euo pipefail source .buildkite/scripts/common/util.sh -"$(dirname "${0}")/commit_status_start.sh" +if [[ "${GITHUB_BUILD_COMMIT_STATUS_ENABLED:-}" != "true" ]]; then + "$(dirname "${0}")/commit_status_start.sh" +fi export CI_STATS_TOKEN="$(retry 5 5 vault read -field=api_token secret/kibana-issues/dev/kibana_ci_stats)" export CI_STATS_HOST="$(retry 5 5 vault read -field=api_host secret/kibana-issues/dev/kibana_ci_stats)" diff --git a/.eslintrc.js b/.eslintrc.js index 60f3ae1528fbcc..00c96e5cf0491f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -902,17 +902,6 @@ module.exports = { }, }, - /** - * Cases overrides - */ - { - files: ['x-pack/plugins/cases/**/*.{js,mjs,ts,tsx}'], - rules: { - 'no-duplicate-imports': 'off', - '@typescript-eslint/no-duplicate-imports': ['error'], - }, - }, - /** * Security Solution overrides. These rules below are maintained and owned by * the people within the security-solution-platform team. Please see ping them @@ -928,6 +917,8 @@ module.exports = { 'x-pack/plugins/security_solution/common/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/timelines/public/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/timelines/common/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/cases/public/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/cases/common/**/*.{js,mjs,ts,tsx}', ], rules: { 'import/no-nodejs-modules': 'error', @@ -949,10 +940,12 @@ module.exports = { files: [ 'x-pack/plugins/security_solution/**/*.{ts,tsx}', 'x-pack/plugins/timelines/**/*.{ts,tsx}', + 'x-pack/plugins/cases/**/*.{ts,tsx}', ], excludedFiles: [ 'x-pack/plugins/security_solution/**/*.{test,mock,test_helper}.{ts,tsx}', 'x-pack/plugins/timelines/**/*.{test,mock,test_helper}.{ts,tsx}', + 'x-pack/plugins/cases/**/*.{test,mock,test_helper}.{ts,tsx}', ], rules: { '@typescript-eslint/no-non-null-assertion': 'error', @@ -963,6 +956,7 @@ module.exports = { files: [ 'x-pack/plugins/security_solution/**/*.{ts,tsx}', 'x-pack/plugins/timelines/**/*.{ts,tsx}', + 'x-pack/plugins/cases/**/*.{ts,tsx}', ], rules: { '@typescript-eslint/no-this-alias': 'error', @@ -985,6 +979,7 @@ module.exports = { files: [ 'x-pack/plugins/security_solution/**/*.{js,mjs,ts,tsx}', 'x-pack/plugins/timelines/**/*.{js,mjs,ts,tsx}', + 'x-pack/plugins/cases/**/*.{js,mjs,ts,tsx}', ], plugins: ['eslint-plugin-node', 'react'], env: { diff --git a/config/kibana.yml b/config/kibana.yml index eeb7c84df43183..f6f85f057172c7 100644 --- a/config/kibana.yml +++ b/config/kibana.yml @@ -1,3 +1,7 @@ +# For more configuration options see the configuration guide for Kibana in +# https://www.elastic.co/guide/index.html + +# =================== System: Kibana Server =================== # Kibana is served by a back end server. This setting specifies the port to use. #server.port: 5601 @@ -14,8 +18,7 @@ # Specifies whether Kibana should rewrite requests that are prefixed with # `server.basePath` or require that they are rewritten by your reverse proxy. -# This setting was effectively always `false` before Kibana 6.3 and will -# default to `true` starting in Kibana 7.0. +# Defaults to `false`. #server.rewriteBasePath: false # Specifies the public URL at which Kibana is available for end users. If @@ -25,9 +28,17 @@ # The maximum payload size in bytes for incoming server requests. #server.maxPayload: 1048576 -# The Kibana server's name. This is used for display purposes. +# The Kibana server's name. This is used for display purposes. #server.name: "your-hostname" +# =================== System: Kibana Server (Optional) =================== +# Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively. +# These settings enable SSL for outgoing requests from the Kibana server to the browser. +#server.ssl.enabled: false +#server.ssl.certificate: /path/to/your/server.crt +#server.ssl.key: /path/to/your/server.key + +# =================== System: Elasticsearch =================== # The URLs of the Elasticsearch instances to use for all your queries. #elasticsearch.hosts: ["http://localhost:9200"] @@ -39,28 +50,10 @@ #elasticsearch.password: "pass" # Kibana can also authenticate to Elasticsearch via "service account tokens". -# If may use this token instead of a username/password. +# Service account tokens are Bearer style tokens that replace the traditional username/password based configuration. +# Use this token instead of a username/password. # elasticsearch.serviceAccountToken: "my_token" -# Enables SSL and paths to the PEM-format SSL certificate and SSL key files, respectively. -# These settings enable SSL for outgoing requests from the Kibana server to the browser. -#server.ssl.enabled: false -#server.ssl.certificate: /path/to/your/server.crt -#server.ssl.key: /path/to/your/server.key - -# Optional settings that provide the paths to the PEM-format SSL certificate and key files. -# These files are used to verify the identity of Kibana to Elasticsearch and are required when -# xpack.security.http.ssl.client_authentication in Elasticsearch is set to required. -#elasticsearch.ssl.certificate: /path/to/your/client.crt -#elasticsearch.ssl.key: /path/to/your/client.key - -# Optional setting that enables you to specify a path to the PEM file for the certificate -# authority for your Elasticsearch instance. -#elasticsearch.ssl.certificateAuthorities: [ "/path/to/your/CA.pem" ] - -# To disregard the validity of SSL certificates, change this setting's value to 'none'. -#elasticsearch.ssl.verificationMode: full - # Time in milliseconds to wait for Elasticsearch to respond to pings. Defaults to the value of # the elasticsearch.requestTimeout setting. #elasticsearch.pingTimeout: 1500 @@ -80,10 +73,21 @@ # Time in milliseconds for Elasticsearch to wait for responses from shards. Set to 0 to disable. #elasticsearch.shardTimeout: 30000 -# Specifies the path where Kibana creates the process ID file. -#pid.file: /run/kibana/kibana.pid +# =================== System: Elasticsearch (Optional) =================== +# These files are used to verify the identity of Kibana to Elasticsearch and are required when +# xpack.security.http.ssl.client_authentication in Elasticsearch is set to required. +#elasticsearch.ssl.certificate: /path/to/your/client.crt +#elasticsearch.ssl.key: /path/to/your/client.key + +# Enables you to specify a path to the PEM file for the certificate +# authority for your Elasticsearch instance. +#elasticsearch.ssl.certificateAuthorities: [ "/path/to/your/CA.pem" ] + +# To disregard the validity of SSL certificates, change this setting's value to 'none'. +#elasticsearch.ssl.verificationMode: full -# Set the value of this setting to off to suppress all logging output, or to debug to log everything. +# =================== System: Logging =================== +# Set the value of this setting to off to suppress all logging output, or to debug to log everything. Defaults to 'info' #logging.root.level: debug # Enables you to specify a file where Kibana stores log output. @@ -108,10 +112,47 @@ # - name: metrics.ops # level: debug +# =================== System: Other =================== +# The path where Kibana stores persistent data not saved in Elasticsearch. Defaults to data +#path.data: data + +# Specifies the path where Kibana creates the process ID file. +#pid.file: /run/kibana/kibana.pid + # Set the interval in milliseconds to sample system and process performance -# metrics. Minimum is 100ms. Defaults to 5000. +# metrics. Minimum is 100ms. Defaults to 5000ms. #ops.interval: 5000 # Specifies locale to be used for all localizable strings, dates and number formats. # Supported languages are the following: English - en , by default , Chinese - zh-CN . #i18n.locale: "en" + +# =================== Frequently used (Optional)=================== + +# =================== Saved Objects: Migrations =================== +# Saved object migrations run at startup. If you run into migration-related issues, you might need to adjust these settings. + +# The number of documents migrated at a time. +# If Kibana can't start up or upgrade due to an Elasticsearch `circuit_breaking_exception`, +# use a smaller batchSize value to reduce the memory pressure. Defaults to 1000 objects per batch. +#migrations.batchSize: 1000 + +# The maximum payload size for indexing batches of upgraded saved objects. +# To avoid migrations failing due to a 413 Request Entity Too Large response from Elasticsearch. +# This value should be lower than or equal to your Elasticsearch cluster’s `http.max_content_length` +# configuration option. Default: 100mb +#migrations.maxBatchSizeBytes: 100mb + +# The number of times to retry temporary migration failures. Increase the setting +# if migrations fail frequently with a message such as `Unable to complete the [...] step after +# 15 attempts, terminating`. Defaults to 15 +#migrations.retryAttempts: 15 + +# =================== Search Autocomplete =================== +# Time in milliseconds to wait for autocomplete suggestions from Elasticsearch. +# This value must be a whole number greater than zero. Defaults to 1000ms +#data.autocomplete.valueSuggestions.timeout: 1000 + +# Maximum number of documents loaded by each shard to generate autocomplete suggestions. +# This value must be a whole number greater than zero. Defaults to 100_000 +#data.autocomplete.valueSuggestions.terminateAfter: 100000 diff --git a/docs/api/dashboard-api.asciidoc b/docs/api/dashboard-api.asciidoc index 94511c3154fe09..e6f54dd9156ecb 100644 --- a/docs/api/dashboard-api.asciidoc +++ b/docs/api/dashboard-api.asciidoc @@ -1,7 +1,7 @@ [[dashboard-api]] == Import and export dashboard APIs -deprecated::[7.15.0,These experimental APIs have been deprecated in favor of <> and <>.] +deprecated::[7.15.0,Both of these APIs have been deprecated in favor of <> and <>.] Import and export dashboards with the corresponding saved objects, such as visualizations, saved searches, and index patterns. diff --git a/docs/api/dashboard/export-dashboard.asciidoc b/docs/api/dashboard/export-dashboard.asciidoc index 098ec976569bd8..3a20eff0a54d25 100644 --- a/docs/api/dashboard/export-dashboard.asciidoc +++ b/docs/api/dashboard/export-dashboard.asciidoc @@ -6,7 +6,7 @@ deprecated::[7.15.0,Use <> instead.] -experimental[] Export dashboards and corresponding saved objects. +Export dashboards and corresponding saved objects. [[dashboard-api-export-request]] ==== Request diff --git a/docs/api/dashboard/import-dashboard.asciidoc b/docs/api/dashboard/import-dashboard.asciidoc index 41eb47500c8d7a..e4817d6cb7ee9b 100644 --- a/docs/api/dashboard/import-dashboard.asciidoc +++ b/docs/api/dashboard/import-dashboard.asciidoc @@ -6,7 +6,7 @@ deprecated::[7.15.0,Use <> instead.] -experimental[] Import dashboards and corresponding saved objects. +Import dashboards and corresponding saved objects. [[dashboard-api-import-request]] ==== Request diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md index 4e44df9d4e1835..676f7420c8bb9f 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.links.md @@ -261,5 +261,8 @@ readonly links: { readonly rubyOverview: string; readonly rustGuide: string; }; + readonly endpoints: { + readonly troubleshooting: string; + }; }; ``` diff --git a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md index 5871a84c5402ed..788f0b9de82186 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly settings: string;
readonly 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;
elasticsearchEnableApiKeys: 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<{
datastreamsILM: string;
beatsAgentComparison: string;
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
installElasticAgent: 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;
};
} | | +| [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;
elasticsearchEnableApiKeys: 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<{
datastreamsILM: string;
beatsAgentComparison: string;
guide: string;
fleetServer: string;
fleetServerAddFleetServer: string;
settings: string;
settingsFleetServerHostSettings: string;
troubleshooting: string;
elasticAgent: string;
datastreams: string;
datastreamsNamingScheme: string;
installElasticAgent: 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;
};
readonly endpoints: {
readonly troubleshooting: string;
};
} | | diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.maskprops.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.maskprops.md new file mode 100644 index 00000000000000..3cb3e0b4902a96 --- /dev/null +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.maskprops.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-core-public](./kibana-plugin-core-public.md) > [OverlayFlyoutOpenOptions](./kibana-plugin-core-public.overlayflyoutopenoptions.md) > [maskProps](./kibana-plugin-core-public.overlayflyoutopenoptions.maskprops.md) + +## OverlayFlyoutOpenOptions.maskProps property + +Signature: + +```typescript +maskProps?: EuiOverlayMaskProps; +``` diff --git a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md index dcecdeb8408695..611b2206bccdca 100644 --- a/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md +++ b/docs/development/core/public/kibana-plugin-core-public.overlayflyoutopenoptions.md @@ -20,6 +20,7 @@ export interface OverlayFlyoutOpenOptions | [className](./kibana-plugin-core-public.overlayflyoutopenoptions.classname.md) | string | | | [closeButtonAriaLabel](./kibana-plugin-core-public.overlayflyoutopenoptions.closebuttonarialabel.md) | string | | | [hideCloseButton](./kibana-plugin-core-public.overlayflyoutopenoptions.hideclosebutton.md) | boolean | | +| [maskProps](./kibana-plugin-core-public.overlayflyoutopenoptions.maskprops.md) | EuiOverlayMaskProps | | | [maxWidth](./kibana-plugin-core-public.overlayflyoutopenoptions.maxwidth.md) | boolean | number | string | | | [onClose](./kibana-plugin-core-public.overlayflyoutopenoptions.onclose.md) | (flyout: OverlayRef) => void | EuiFlyout onClose handler. If provided the consumer is responsible for calling flyout.close() to close the flyout; | | [ownFocus](./kibana-plugin-core-public.overlayflyoutopenoptions.ownfocus.md) | boolean | | diff --git a/docs/maps/asset-tracking-tutorial.asciidoc b/docs/maps/asset-tracking-tutorial.asciidoc index 4ba045681e1486..ff62f5c019b74f 100644 --- a/docs/maps/asset-tracking-tutorial.asciidoc +++ b/docs/maps/asset-tracking-tutorial.asciidoc @@ -156,16 +156,16 @@ image::maps/images/asset-tracking-tutorial/logstash_output.png[] . Leave the terminal window open and Logstash running throughout this tutorial. [float] -==== Step 3: Create a {kib} index pattern for the tri_met_tracks {es} index +==== Step 3: Create a data view for the tri_met_tracks {es} index -. In Kibana, open the main menu, and click *Stack Management > Index Patterns*. -. Click *Create index pattern*. -. Give the index pattern a name: *tri_met_tracks**. +. In {kib}, open the main menu, and click *Stack Management > Data Views*. +. Click *Create data view*. +. Give the data view a name: *tri_met_tracks**. . Click *Next step*. . Set the *Time field* to *time*. -. Click *Create index pattern*. +. Click *Create data view*. -{kib} shows the fields in your index pattern. +{kib} shows the fields in your data view. [role="screenshot"] image::maps/images/asset-tracking-tutorial/index_pattern.png[] @@ -174,7 +174,7 @@ image::maps/images/asset-tracking-tutorial/index_pattern.png[] ==== Step 4: Explore the Portland bus data . Open the main menu, and click *Discover*. -. Set the index pattern to *tri_met_tracks**. +. Set the data view to *tri_met_tracks**. . Open the <>, and set the time range to the last 15 minutes. . Expand a document and explore some of the fields that you will use later in this tutorial: `bearing`, `in_congestion`, `location`, and `vehicle_id`. @@ -202,7 +202,7 @@ Add a layer to show the bus routes for the last 15 minutes. . Click *Add layer*. . Click *Tracks*. -. Select the *tri_met_tracks** index pattern. +. Select the *tri_met_tracks** data view. . Define the tracks: .. Set *Entity* to *vehicle_id*. .. Set *Sort* to *time*. @@ -225,7 +225,7 @@ image::maps/images/asset-tracking-tutorial/tracks_layer.png[] Add a layer that uses attributes in the data to set the style and orientation of the buses. You’ll see the direction buses are headed and what traffic is like. . Click *Add layer*, and then select *Top Hits per entity*. -. Select the *tri_met_tracks** index pattern. +. Select the *tri_met_tracks** data view. . To display the most recent location per bus: .. Set *Entity* to *vehicle_id*. .. Set *Documents per entity* to 1. diff --git a/docs/maps/geojson-upload.asciidoc b/docs/maps/geojson-upload.asciidoc index 3c9bea11176cc8..15ef3471e58d7d 100644 --- a/docs/maps/geojson-upload.asciidoc +++ b/docs/maps/geojson-upload.asciidoc @@ -30,11 +30,11 @@ a preview of the data on the map. . Use the default *Index type* of {ref}/geo-point.html[geo_point] for point data, or override it and select {ref}/geo-shape.html[geo_shape]. All other shapes will default to a type of `geo_shape`. -. Leave the default *Index name* and *Index pattern* names (the name of the uploaded +. Leave the default *Index name* and *Data view* names (the name of the uploaded file minus its extension). You might need to change the index name if it is invalid. . Click *Import file*. + -Upon completing the indexing process and creating the associated index pattern, +Upon completing the indexing process and creating the associated data view, the Elasticsearch responses are shown on the *Layer add panel* and the indexed data appears on the map. The geospatial data on the map should be identical to the locally-previewed data, but now it's indexed data from Elasticsearch. diff --git a/docs/maps/indexing-geojson-data-tutorial.asciidoc b/docs/maps/indexing-geojson-data-tutorial.asciidoc index 434c9ab369a5bd..50f2e9aed9248d 100644 --- a/docs/maps/indexing-geojson-data-tutorial.asciidoc +++ b/docs/maps/indexing-geojson-data-tutorial.asciidoc @@ -58,8 +58,8 @@ auto-populate *Index type* with either {ref}/geo-point.html[geo_point] or . Click *Import file*. + You'll see activity as the GeoJSON Upload utility creates a new index -and index pattern for the data set. When the process is complete, you should -receive messages that the creation of the new index and index pattern +and data view for the data set. When the process is complete, you should +receive messages that the creation of the new index and data view were successful. . Click *Add layer*. diff --git a/docs/maps/maps-aggregations.asciidoc b/docs/maps/maps-aggregations.asciidoc index 7f4af952653e7c..fced15771c3864 100644 --- a/docs/maps/maps-aggregations.asciidoc +++ b/docs/maps/maps-aggregations.asciidoc @@ -62,7 +62,7 @@ To enable a grid aggregation layer: To enable a blended layer that dynamically shows clusters or documents: . Click *Add layer*, then select the *Documents* layer. -. Configure *Index pattern* and the *Geospatial field*. +. Configure *Data view* and the *Geospatial field*. . In *Scaling*, select *Show clusters when results exceed 10000*. @@ -77,7 +77,7 @@ then accumulates the most relevant documents based on sort order for each entry To enable top hits: . Click *Add layer*, then select the *Top hits per entity* layer. -. Configure *Index pattern* and *Geospatial field*. +. Configure *Data view* and *Geospatial field*. . Set *Entity* to the field that identifies entities in your documents. This field will be used in the terms aggregation to group your documents into entity buckets. . Set *Documents per entity* to configure the maximum number of documents accumulated per entity. diff --git a/docs/maps/maps-getting-started.asciidoc b/docs/maps/maps-getting-started.asciidoc index 014be570253bb7..89d06fce60183d 100644 --- a/docs/maps/maps-getting-started.asciidoc +++ b/docs/maps/maps-getting-started.asciidoc @@ -49,7 +49,7 @@ and lighter shades will symbolize countries with less traffic. . From the **Layer** dropdown menu, select **World Countries**. . In **Statistics source**, set: -** **Index pattern** to **kibana_sample_data_logs** +** **Data view** to **kibana_sample_data_logs** ** **Join field** to **geo.dest** . Click **Add layer**. @@ -95,7 +95,7 @@ The layer is only visible when users zoom in. . Click **Add layer**, and then click **Documents**. -. Set **Index pattern** to **kibana_sample_data_logs**. +. Set **Data view** to **kibana_sample_data_logs**. . Set **Scaling** to *Limits results to 10000.* @@ -129,7 +129,7 @@ more total bytes transferred, and smaller circles will symbolize grids with less bytes transferred. . Click **Add layer**, and select **Clusters and grids**. -. Set **Index pattern** to **kibana_sample_data_logs**. +. Set **Data view** to **kibana_sample_data_logs**. . Click **Add layer**. . In **Layer settings**, set: ** **Name** to `Total Requests and Bytes` diff --git a/docs/maps/reverse-geocoding-tutorial.asciidoc b/docs/maps/reverse-geocoding-tutorial.asciidoc index 0c942f120a4da6..8760d3ab4df8b5 100644 --- a/docs/maps/reverse-geocoding-tutorial.asciidoc +++ b/docs/maps/reverse-geocoding-tutorial.asciidoc @@ -141,7 +141,7 @@ PUT kibana_sample_data_logs/_settings ---------------------------------- . Open the main menu, and click *Discover*. -. Set the index pattern to *kibana_sample_data_logs*. +. Set the data view to *kibana_sample_data_logs*. . Open the <>, and set the time range to the last 30 days. . Scan through the list of *Available fields* until you find the `csa.GEOID` field. You can also search for the field by name. . Click image:images/reverse-geocoding-tutorial/add-icon.png[Add icon] to toggle the field into the document table. @@ -162,10 +162,10 @@ Now that our web traffic contains CSA region identifiers, you'll visualize CSA r . Click *Choropleth*. . For *Boundaries source*: .. Select *Points, lines, and polygons from Elasticsearch*. -.. Set *Index pattern* to *csa*. +.. Set *Data view* to *csa*. .. Set *Join field* to *GEOID*. . For *Statistics source*: -.. Set *Index pattern* to *kibana_sample_data_logs*. +.. Set *Data view* to *kibana_sample_data_logs*. .. Set *Join field* to *csa.GEOID.keyword*. . Click *Add layer*. . Scroll to *Layer Style* and Set *Label* to *Fixed*. diff --git a/docs/maps/trouble-shooting.asciidoc b/docs/maps/trouble-shooting.asciidoc index 60bcabad3a6b4c..13c8b97c30b3d0 100644 --- a/docs/maps/trouble-shooting.asciidoc +++ b/docs/maps/trouble-shooting.asciidoc @@ -21,18 +21,18 @@ image::maps/images/inspector.png[] === Solutions to common problems [float] -==== Index not listed when adding layer +==== Data view not listed when adding layer * Verify your geospatial data is correctly mapped as {ref}/geo-point.html[geo_point] or {ref}/geo-shape.html[geo_shape]. - ** Run `GET myIndexPatternTitle/_field_caps?fields=myGeoFieldName` in <>, replacing `myIndexPatternTitle` and `myGeoFieldName` with your index pattern title and geospatial field name. + ** Run `GET myIndexName/_field_caps?fields=myGeoFieldName` in <>, replacing `myIndexName` and `myGeoFieldName` with your index and geospatial field name. ** Ensure response specifies `type` as `geo_point` or `geo_shape`. -* Verify your geospatial data is correctly mapped in your <>. - ** Open your index pattern in <>. +* Verify your geospatial data is correctly mapped in your <>. + ** Open your data view in <>. ** Ensure your geospatial field type is `geo_point` or `geo_shape`. ** Ensure your geospatial field is searchable and aggregatable. ** If your geospatial field type does not match your Elasticsearch mapping, click the *Refresh* button to refresh the field list from Elasticsearch. -* Index patterns with thousands of fields can exceed the default maximum payload size. -Increase <> for large index patterns. +* Data views with thousands of fields can exceed the default maximum payload size. +Increase <> for large data views. [float] ==== Features are not displayed diff --git a/docs/maps/vector-tooltips.asciidoc b/docs/maps/vector-tooltips.asciidoc index 2dda35aa28f768..2e4ee99d5b84ff 100644 --- a/docs/maps/vector-tooltips.asciidoc +++ b/docs/maps/vector-tooltips.asciidoc @@ -18,7 +18,7 @@ image::maps/images/multifeature_tooltip.png[] ==== Format tooltips You can format the attributes in a tooltip by adding <> to your -index pattern. You can use field formatters to round numbers, provide units, +data view. You can use field formatters to round numbers, provide units, and even display images in your tooltip. [float] diff --git a/docs/user/alerting/rule-types.asciidoc b/docs/user/alerting/rule-types.asciidoc index 4c1d3b94bdee66..ab2349f2fb1023 100644 --- a/docs/user/alerting/rule-types.asciidoc +++ b/docs/user/alerting/rule-types.asciidoc @@ -26,6 +26,9 @@ see {subscriptions}[the subscription page]. | <> | Run a user-configured {es} query, compare the number of matches to a configured threshold, and schedule actions to run when the threshold condition is met. +| {ref}/transform-alerts.html[{transform-cap} rules] beta:[] +| beta:[] Run scheduled checks on a {ctransform} to check its health. If a {ctransform} meets the conditions, an alert is created and the associated action is triggered. + |=== [float] @@ -47,7 +50,7 @@ Domain rules are registered by *Observability*, *Security*, <> and < | Run an {es} query to determine if any documents are currently contained in any boundaries from a specified boundary index and generate alerts when a rule's conditions are met. | {ml-docs}/ml-configuring-alerts.html[{ml-cap} rules] beta:[] -| Run scheduled checks on an anomaly detection job to detect anomalies with certain conditions. If an anomaly meets the conditions, an alert is created and the associated action is triggered. +| beta:[] Run scheduled checks on an {anomaly-job} to detect anomalies with certain conditions. If an anomaly meets the conditions, an alert is created and the associated action is triggered. |=== diff --git a/docs/user/dashboard/images/lens_areaChartCumulativeNumberOfSalesOnWeekend_7.16.png b/docs/user/dashboard/images/lens_areaChartCumulativeNumberOfSalesOnWeekend_7.16.png new file mode 100644 index 00000000000000..82e0337ffed397 Binary files /dev/null and b/docs/user/dashboard/images/lens_areaChartCumulativeNumberOfSalesOnWeekend_7.16.png differ diff --git a/docs/user/dashboard/images/lens_areaPercentageNumberOfOrdersByCategory_7.16.png b/docs/user/dashboard/images/lens_areaPercentageNumberOfOrdersByCategory_7.16.png new file mode 100644 index 00000000000000..6addc8bc276e96 Binary files /dev/null and b/docs/user/dashboard/images/lens_areaPercentageNumberOfOrdersByCategory_7.16.png differ diff --git a/docs/user/dashboard/images/lens_barChartCustomTimeInterval_7.16.png b/docs/user/dashboard/images/lens_barChartCustomTimeInterval_7.16.png new file mode 100644 index 00000000000000..3aa5484cb6258d Binary files /dev/null and b/docs/user/dashboard/images/lens_barChartCustomTimeInterval_7.16.png differ diff --git a/docs/user/dashboard/images/lens_barChartDistributionOfNumberField_7.16.png b/docs/user/dashboard/images/lens_barChartDistributionOfNumberField_7.16.png new file mode 100644 index 00000000000000..631477e7d68cc0 Binary files /dev/null and b/docs/user/dashboard/images/lens_barChartDistributionOfNumberField_7.16.png differ diff --git a/docs/user/dashboard/images/lens_clickAndDragZoom_7.16.gif b/docs/user/dashboard/images/lens_clickAndDragZoom_7.16.gif new file mode 100644 index 00000000000000..65fed435dfa25c Binary files /dev/null and b/docs/user/dashboard/images/lens_clickAndDragZoom_7.16.gif differ diff --git a/docs/user/dashboard/images/lens_end_to_end_2_1_1.png b/docs/user/dashboard/images/lens_end_to_end_2_1_1.png index e996b58520d410..f1bee569f29c20 100644 Binary files a/docs/user/dashboard/images/lens_end_to_end_2_1_1.png and b/docs/user/dashboard/images/lens_end_to_end_2_1_1.png differ diff --git a/docs/user/dashboard/images/lens_end_to_end_6_1.png b/docs/user/dashboard/images/lens_end_to_end_6_1.png index 73299bac0354ef..942c4d636d1fca 100644 Binary files a/docs/user/dashboard/images/lens_end_to_end_6_1.png and b/docs/user/dashboard/images/lens_end_to_end_6_1.png differ diff --git a/docs/user/dashboard/images/lens_indexPatternDropDown_7.16.png b/docs/user/dashboard/images/lens_indexPatternDropDown_7.16.png new file mode 100644 index 00000000000000..f8e797c7dd4b65 Binary files /dev/null and b/docs/user/dashboard/images/lens_indexPatternDropDown_7.16.png differ diff --git a/docs/user/dashboard/images/lens_index_pattern.png b/docs/user/dashboard/images/lens_index_pattern.png deleted file mode 100644 index 0c89e7ab7f8147..00000000000000 Binary files a/docs/user/dashboard/images/lens_index_pattern.png and /dev/null differ diff --git a/docs/user/dashboard/images/lens_layerVisualizationTypeMenu_7.16.png b/docs/user/dashboard/images/lens_layerVisualizationTypeMenu_7.16.png new file mode 100644 index 00000000000000..6ee73e9a676625 Binary files /dev/null and b/docs/user/dashboard/images/lens_layerVisualizationTypeMenu_7.16.png differ diff --git a/docs/user/dashboard/images/lens_leftAxisMenu_7.16.png b/docs/user/dashboard/images/lens_leftAxisMenu_7.16.png new file mode 100644 index 00000000000000..054731adbeef5e Binary files /dev/null and b/docs/user/dashboard/images/lens_leftAxisMenu_7.16.png differ diff --git a/docs/user/dashboard/images/lens_lineChartMetricOverTime_7.16.png b/docs/user/dashboard/images/lens_lineChartMetricOverTime_7.16.png new file mode 100644 index 00000000000000..34fd8dae1407d2 Binary files /dev/null and b/docs/user/dashboard/images/lens_lineChartMetricOverTime_7.16.png differ diff --git a/docs/user/dashboard/images/lens_lineChartMultipleDataSeries_7.16.png b/docs/user/dashboard/images/lens_lineChartMultipleDataSeries_7.16.png new file mode 100644 index 00000000000000..373fc76b5db415 Binary files /dev/null and b/docs/user/dashboard/images/lens_lineChartMultipleDataSeries_7.16.png differ diff --git a/docs/user/dashboard/images/lens_logsDashboard_7.16.png b/docs/user/dashboard/images/lens_logsDashboard_7.16.png new file mode 100644 index 00000000000000..cdfe0accdbbb5d Binary files /dev/null and b/docs/user/dashboard/images/lens_logsDashboard_7.16.png differ diff --git a/docs/user/dashboard/images/lens_metricUniqueCountOfClientip_7.16.png b/docs/user/dashboard/images/lens_metricUniqueCountOfClientip_7.16.png new file mode 100644 index 00000000000000..bed6acf501a3a7 Binary files /dev/null and b/docs/user/dashboard/images/lens_metricUniqueCountOfClientip_7.16.png differ diff --git a/docs/user/dashboard/images/lens_metricUniqueVisitors_7.16.png b/docs/user/dashboard/images/lens_metricUniqueVisitors_7.16.png new file mode 100644 index 00000000000000..92fe4fb0676f22 Binary files /dev/null and b/docs/user/dashboard/images/lens_metricUniqueVisitors_7.16.png differ diff --git a/docs/user/dashboard/images/lens_mixedXYChart_7.16.png b/docs/user/dashboard/images/lens_mixedXYChart_7.16.png new file mode 100644 index 00000000000000..76fc96a44a4026 Binary files /dev/null and b/docs/user/dashboard/images/lens_mixedXYChart_7.16.png differ diff --git a/docs/user/dashboard/images/lens_pieChartCompareSubsetOfDocs_7.16.png b/docs/user/dashboard/images/lens_pieChartCompareSubsetOfDocs_7.16.png new file mode 100644 index 00000000000000..f8e8ba98f691e3 Binary files /dev/null and b/docs/user/dashboard/images/lens_pieChartCompareSubsetOfDocs_7.16.png differ diff --git a/docs/user/dashboard/images/lens_referenceLine_7.16.png b/docs/user/dashboard/images/lens_referenceLine_7.16.png new file mode 100644 index 00000000000000..3df7e99e0aafee Binary files /dev/null and b/docs/user/dashboard/images/lens_referenceLine_7.16.png differ diff --git a/docs/user/dashboard/images/lens_tableTopFieldValues_7.16.png b/docs/user/dashboard/images/lens_tableTopFieldValues_7.16.png new file mode 100644 index 00000000000000..64417a9a6392cf Binary files /dev/null and b/docs/user/dashboard/images/lens_tableTopFieldValues_7.16.png differ diff --git a/docs/user/dashboard/images/lens_timeSeriesDataTutorialDashboard_7.16.png b/docs/user/dashboard/images/lens_timeSeriesDataTutorialDashboard_7.16.png new file mode 100644 index 00000000000000..bce904c8606ca3 Binary files /dev/null and b/docs/user/dashboard/images/lens_timeSeriesDataTutorialDashboard_7.16.png differ diff --git a/docs/user/dashboard/images/lens_treemapMultiLevelChart_7.16.png b/docs/user/dashboard/images/lens_treemapMultiLevelChart_7.16.png new file mode 100644 index 00000000000000..6d772a32e9ef4a Binary files /dev/null and b/docs/user/dashboard/images/lens_treemapMultiLevelChart_7.16.png differ diff --git a/docs/user/dashboard/images/lens_visualizationTypeDropdown_7.16.png b/docs/user/dashboard/images/lens_visualizationTypeDropdown_7.16.png new file mode 100644 index 00000000000000..dce53da1f2cad2 Binary files /dev/null and b/docs/user/dashboard/images/lens_visualizationTypeDropdown_7.16.png differ diff --git a/docs/user/dashboard/lens-advanced.asciidoc b/docs/user/dashboard/lens-advanced.asciidoc index 02e0afd2c03118..324676ecb0a8e6 100644 --- a/docs/user/dashboard/lens-advanced.asciidoc +++ b/docs/user/dashboard/lens-advanced.asciidoc @@ -2,18 +2,21 @@ == Analyze time series data In this tutorial, you'll use the ecommerce sample data to analyze sales trends, but you can use any type of data to complete the tutorial. -Before using this tutorial, review the <>. + +When you're done, you'll have a complete overview of the sample web logs data. [role="screenshot"] -image::images/final_time_series_analysis_dashboard.png[Final dashboard with ecommerce sample data, width=50%] +image::images/lens_timeSeriesDataTutorialDashboard_7.16.png[Final dashboard with ecommerce sample data] + +Before you begin, you should be familiar with the <>. [discrete] [[add-the-data-and-create-the-dashboard-advanced]] === Add the data and create the dashboard -Add the sample ecommerce data that you'll use to create the dashboard panels. +Add the sample ecommerce data, and create and set up the dashboard. -. Go to the {kib} *Home* page, then click *Try our sample data*. +. Go to the *Home* page, then click *Try sample data*. . On the *Sample eCommerce orders* card, click *Add data*. @@ -25,40 +28,30 @@ Create the dashboard where you'll display the visualization panels. [float] [[open-and-set-up-lens-advanced]] -=== Open and set up Lens +=== Open and set up the visualization editor -Open *Lens*, then make sure the correct fields appear. +Open the visualization editor, then make sure the correct fields appear. -. From the dashboard, click *Create visualization*. +. On the dashboard, click *Create visualization*. -. Make sure the *kibana_sample_data_ecommerce* index appears. -+ -If you are using your own data, select the <> that contains your data. +. Make sure the *kibana_sample_data_ecommerce* index appears, then set the <> to *Last 30 days*. [discrete] [[custom-time-interval]] -=== View a date histogram with a custom time interval - -It is common to use the automatic date histogram interval, but sometimes you want a larger or smaller -interval. For performance reasonse, *Lens* lets you choose the minimum time interval, not the exact time interval. The performance limit is controlled by the <> setting and the <>. +=== Create visualizations with custom time intervals -If you are using your own data, use one of the following options to see hourly sales over the last 30 days: +When you create visualizations with time series data, you can use the default time interval, or increase and decrease the interval. For performance reasons, the visualization editor allows you to choose the minimum time interval, but not the exact time interval. The interval limit is controlled by the <> setting and <>. -* View less than 30 days at a time, then use the time filter to select each day separately. - -* Increase `histogram:maxBars` to at least 720, which is the number of hours in 30 days. This affects all visualizations and can reduce performance. - -If you are using the sample data, use *Normalize unit*, which converts *Average sales per 12 hours* -into *Average sales per 12 hours (per hour)* by dividing the number of hours: - -. Set the <> to *Last 30 days*. +To analyze the data with a custom time interval, create a bar chart that shows you how many orders were made at your store every hour: . From the *Available fields* list, drag *Records* to the workspace. ++ +The visualization editor creates a bar chart. -. To zoom in on the data you want to view, click and drag your cursor across the bars. +. To zoom in on the data, click and drag your cursor across the bars. + [role="screenshot"] -image::images/lens_advanced_1_1.png[Added records to the workspace] +image::images/lens_clickAndDragZoom_7.16.gif[Cursor clicking and dragging across the bars to zoom in on the data] . In the layer pane, click *Count of Records*. @@ -67,32 +60,51 @@ image::images/lens_advanced_1_1.png[Added records to the workspace] .. Click *Add advanced options > Normalize by unit*. .. From the *Normalize by unit* dropdown, select *per hour*, then click *Close*. ++ +*Normalize unit* converts *Average sales per 12 hours* into *Average sales per 12 hours (per hour)* by dividing the number of hours. . To hide the *Horizontal axis* label, open the *Bottom Axis* menu, then deselect *Show*. -+ -You have a bar chart that shows you how many orders were made at your store every hour. + +To identify the 75th percentile of orders, add a reference line: + +. In the layer pane, click *Add layer > Add reference layer*. + +. Click *Static value*. + +. Click the *Percentile* function, then enter `75` in the *Percentile* field. + +. Configure the display options. + +.. In the *Display name* field, enter `75th`. + +.. Select *Show display name*. + +.. From the *Icon* dropdown, select *Tag*. + +.. In the *Color* field, enter `#E7664C`. + +. Click *Close*. + [role="screenshot"] -image::images/lens_advanced_1_2.png[Orders per day] +image::images/lens_barChartCustomTimeInterval_7.16.png[Orders per day] . Click *Save and return*. [discrete] [[add-a-data-layer-advanced]] -=== Monitor multiple series +=== Analyze multiple data series -It is often required to monitor multiple series within a time interval. These series can have similar configurations with minor differences. -*Lens* copies a function when you drag it to the *Drop a field or click to add* field within the same group. +You can create visualizations with multiple data series within the same time interval, even when the series have similar configurations with minor differences. -To quickly create many copies of a percentile metric that shows distribution of price over time: +To analyze multiple series, create a line chart that displays the price distribution of products sold over time: . On the dashboard, click *Create visualization*. -. Open the *Chart Type* dropdown, then select *Line*. +. Open the *Visualization type* dropdown, then select *Line*. . From the *Available fields* list, drag *products.price* to the workspace. -Create the 95th percentile. +Create the 95th price distribution percentile: . In the layer pane, click *Median of products.price*. @@ -100,9 +112,9 @@ Create the 95th percentile. . In the *Display name* field, enter `95th`, then click *Close*. -To create the 90th percentile, duplicate the `95th` percentile. +To copy a function, you drag it to the *Drop a field or click to add* field within the same group. To create the 90th percentile, duplicate the `95th` percentile: -. Drag the *95th* field to the *Drop a field or click to add* field in the *Vertical axis* group. +. Drag the *95th* field to *Add or drag-and-drop a field* for *Vertical axis*. + [role="screenshot"] image::images/lens_advanced_2_2.gif[Easily duplicate the items with drag and drop] @@ -111,22 +123,22 @@ image::images/lens_advanced_2_2.gif[Easily duplicate the items with drag and dro . In the *Display name* field enter `90th`, then click *Close*. -. Repeat the duplication steps to create the `50th` and `10th` percentiles. +. To create the `50th` and `10th` percentiles, repeat the duplication steps. . Open the *Left Axis* menu, then enter `Percentiles for product prices` in the *Axis name* field. + -You have a line chart that shows you the price distribution of products sold over time. -+ [role="screenshot"] -image::images/lens_advanced_2_3.png[Percentiles for product prices chart] +image::images/lens_lineChartMultipleDataSeries_7.16.png[Percentiles for product prices chart] . Click *Save and return*. [discrete] [[add-a-data-layer]] -==== Add multiple chart types or index patterns +=== Analyze multiple visualization types + +With layers, you can analyze your data with multiple visualization types. When you create layered visualizations, match the data on the horizontal axis so that it uses the same scale. -To overlay visualization types or index patterns, add layers. When you create layered charts, match the data on the horizontal axis so that it uses the same scale. +To analyze multiple visualization types, create an area chart that displays the average order prices, then add a line chart layer that displays the number of customers. . On the dashboard, click *Create visualization*. @@ -136,19 +148,19 @@ To overlay visualization types or index patterns, add layers. When you create la .. Click the *Average* function. -.. In the *Display name* field, enter `Average of prices`, then click *Close*. +.. In the *Display name* field, enter `Average price`, then click *Close*. -. Open the *Chart Type* dropdown, then select *Area*. +. Open the *Visualization type* dropdown, then select *Area*. -Create a new layer to overlay with custom traffic. +Add a layer to display the customer traffic: -. In the layer pane, click *+*. +. In the layer pane, click *Add layer > Add visualization layer*. . From the *Available fields* list, drag *customer_id* to the *Vertical Axis* field in the second layer. -. In the second layer, click *Unique count of customer_id*. +. In the layer pane, click *Unique count of customer_id*. -.. In the *Display name* field, enter `Unique customers`. +.. In the *Display name* field, enter `Number of customers`. .. In the *Series color* field, enter *#D36086*. @@ -156,12 +168,15 @@ Create a new layer to overlay with custom traffic. . From the *Available fields* list, drag *order_date* to the *Horizontal Axis* field in the second layer. -. In the second layer pane, open the *Chart type* menu, then click the line chart. +. In the second layer, open the *Layer visualization type* menu, then click *Line*. + [role="screenshot"] -image::images/lens_advanced_3_2.png[Change layer type] +image::images/lens_layerVisualizationTypeMenu_7.16.png[Layer visualization type menu] -. Open the *Legend* menu, then select the arrow that points up. +. To change the position of the legend, open the *Legend* menu, then select the *Alignment* arrow that points up. ++ +[role="screenshot"] +image::images/lens_mixedXYChart_7.16.png[Layer visualization type menu] . Click *Save and return*. @@ -169,35 +184,35 @@ image::images/lens_advanced_3_2.png[Change layer type] [[percentage-stacked-area]] === Compare the change in percentage over time -By default, *Lens* shows *date histograms* using a stacked chart visualization, which helps understand how distinct sets of documents perform over time. Sometimes it is useful to understand how the distributions of these sets change over time. -Combine *filters* and *date histogram* functions to see the change over time in specific -sets of documents. To view this as a percentage, use a *Stacked percentage* bar or area chart. +By default, the visualization editor displays time series data with stacked charts, which show how the different document sets change over time. + +To view change over time as a percentage, create an *Area percentage* chart that displays three order categories over time: . On the dashboard, click *Create visualization*. . From the *Available fields* list, drag *Records* to the workspace. -. Open the *Chart type* dropdown, then select *Area percentage*. +. Open the *Visualization type* dropdown, then select *Area percentage*. -For each category type, create a filter. +For each order category, create a filter: -. In the layer pane, click the *Drop a field or click to add* field for *Break down by*. +. In the layer pane, click *Add or drag-and-drop a field* for *Break down by*. . Click the *Filters* function. -. Click *All records*, enter the following, then press Return: +. Click *All records*, enter the following in the query bar, then press Return: * *KQL* — `category.keyword : *Clothing` * *Label* — `Clothing` -. Click *Add a filter*, enter the following, then press Return: +. Click *Add a filter*, enter the following in the query bar, then press Return: * *KQL* — `category.keyword : *Shoes` * *Label* — `Shoes` -. Click *Add a filter*, enter the following, then press Return: +. Click *Add a filter*, enter the following in the query bar, then press Return: * *KQL* — `category.keyword : *Accessories` @@ -205,10 +220,10 @@ For each category type, create a filter. . Click *Close*. -. Open the *Legend* menu, then select the arrow that points up. +. Open the *Legend* menu, then select the *Alignment* arrow that points up. + [role="screenshot"] -image::images/lens_advanced_4_1.png[Prices share by category] +image::images/lens_areaPercentageNumberOfOrdersByCategory_7.16.png[Prices share by category] . Click *Save and return*. @@ -220,9 +235,9 @@ To determine the number of orders made only on Saturday and Sunday, create an ar . On the dashboard, click *Create visualization*. -. Open the *Chart Type* dropdown, then select *Area*. +. Open the *Visualization type* dropdown, then select *Area*. -Configure the cumulative sum of the store orders. +Configure the cumulative sum of store orders: . From the *Available fields* list, drag *Records* to the workspace. @@ -230,15 +245,15 @@ Configure the cumulative sum of the store orders. . Click the *Cumulative sum* function. -. In the *Display name* field, enter `Cumulative orders during weekend days`, then click *Close*. +. In the *Display name* field, enter `Cumulative weekend orders`, then click *Close*. -Filter the results to display the data for only Saturday and Sunday. +Filter the results to display the data for only Saturday and Sunday: -. In the layer pane, click the *Drop a field or click to add* field for *Break down by*. +. In the layer pane, click *Add or drag-and-drop a field* for *Break down by*. . Click the *Filters* function. -. Click *All records*, enter the following, then press Return: +. Click *All records*, enter the following in the query bar, then press Return: * *KQL* — `day_of_week : "Saturday" or day_of_week : "Sunday"` @@ -249,7 +264,7 @@ The <> displays all documents where `day_of_week` matche . Open the *Legend* menu, then click *Hide*. + [role="screenshot"] -image::images/lens_advanced_5_2.png[Line chart with cumulative sum of orders made on the weekend] +image::images/lens_areaChartCumulativeNumberOfSalesOnWeekend_7.16.png[Area chart with cumulative sum of orders made on the weekend] . Click *Save and return*. @@ -257,30 +272,25 @@ image::images/lens_advanced_5_2.png[Line chart with cumulative sum of orders mad [[compare-time-ranges]] === Compare time ranges -*Lens* allows you to compare the selected time range with historical data using the *Time shift* option. +With *Time shift*, you can compare the data from different time ranges. To make sure the data correctly displays, choose a multiple of the date histogram interval when you use multiple time shifts. For example, you are unable to use a *36h* time shift for one series, and a *1d* time shift for the second series if the interval is *days*. -If multiple time shifts are used in a single chart, a multiple of the date histogram interval should be chosen, or the data points might not line up and gaps can appear. -For example, if a daily interval is used, shifting one series by *36h*, and another by *1d* is not recommended. You can reduce the interval to *12h*, or create two separate charts. - -To compare current sales numbers with sales from a week ago, follow these steps: +To compare two time ranges, create a line chart that compares the sales in the current week with sales from the previous week: . On the dashboard, click *Create visualization*. -. Open the *Chart Type* dropdown, then select *Line*. +. Open the *Visualization type* dropdown, then select *Line*. . From the *Available fields* list, drag *Records* to the workspace. -. In the layer pane, drag *Count of Records* to the *Drop a field or click to add* field in the *Vertical axis* group. +. To duplicate *Count of Records*, drag *Count of Records* to *Add or drag-and-drop a field* for *Vertical axis* in the layer pane. -To create a week-over-week comparison, shift the second *Count of Records* by one week. +To create a week-over-week comparison, shift *Count of Records [1]* by one week: . In the layer pane, click *Count of Records [1]*. -. Open the *Add advanced options* dropdown, then select *Time shift*. - -. Click *1 week ago*. +. Click *Add advanced options > Time shift*, select *1 week ago*, then click *Close*. + -To define custom time shifts, enter the time value, the time increment, then press Enter. For example, to use a one week time shift, enter *1w*. +To use custom time shifts, enter the time value and increment, then press Enter. For example, enter *1w* to use the *1 week ago* time shift. + [role="screenshot"] image::images/lens_time_shift.png[Line chart with week-over-week sales comparison] @@ -289,9 +299,11 @@ image::images/lens_time_shift.png[Line chart with week-over-week sales compariso [float] [[compare-time-as-percent]] -==== Compare time ranges as a percent change +==== Analyze the percent change between time ranges -To view the percent change in sales between the current time and the previous week, create a *Formula*. +With *Formula*, you can analyze the percent change in your data from different time ranges. + +To compare time range changes as a percent, create a bar chart that compares the sales in the current week with sales from the previous week: . On the dashboard, click *Create visualization*. @@ -299,11 +311,11 @@ To view the percent change in sales between the current time and the previous we . In the layer pane, click *Count of Records*. -.. Click *Formula*, then enter `count() / count(shift='1w') - 1`. +. Click *Formula*, then enter `count() / count(shift='1w') - 1`. -.. Open the *Value format* dropdown, select *Percent*, then enter `0` in the *D*ecimals* field. +. Open the *Value format* dropdown, select *Percent*, then enter `0` in the *Decimals* field. -.. In the *Display name* field, enter `Percent change`, then click *Close*. +. In the *Display name* field, enter `Percent of change`, then click *Close*. + [role="screenshot"] image::images/lens_percent_chage.png[Bar chart with percent change in sales between the current time and the previous week] @@ -312,34 +324,33 @@ image::images/lens_percent_chage.png[Bar chart with percent change in sales betw [discrete] [[view-customers-over-time-by-continents]] -=== Create a table of customers by category over time +=== Analyze the data in a table -Tables are useful when you want to display the actual field values. -You can build a date histogram table, and group the customer count metric by category, such as the continent registered in user accounts. +With tables, you can view and compare the field values, which is useful for displaying the locations of customer orders. -In *Lens* you can split the metric in a table leveraging the *Columns* field, where each data value from the aggregation is used as column of the table and the relative metric value is shown. +Create a date histogram table and group the customer count metric by category, such as the continent registered in user accounts: . On the dashboard, click *Create visualization*. -. Open the *Chart Type* dropdown, then click *Table*. +. Open the *Visualization type* dropdown, then select *Table*. . From the *Available fields* list, drag *customer_id* to the *Metrics* field in the layer pane. -. In the layer pane, click *Unique count of customer_id*. +.. In the layer pane, click *Unique count of customer_id*. -. In the *Display name* field, enter `Customers`, then click *Close*. +.. In the *Display name* field, enter `Customers`, then click *Close*. . From the *Available fields* list, drag *order_date* to the *Rows* field in the layer pane. -. In the layer pane, click the *order_date*. +.. In the layer pane, click the *order_date*. .. Select *Customize time interval*. .. Change the *Minimum interval* to *1 days*. -.. In the *Display name* field, enter `Sale`, then click *Close*. +.. In the *Display name* field, enter `Sales`, then click *Close*. -Add columns for each continent. +To split the metric, add columns for each continent using the *Columns* field: . From the *Available fields* list, drag *geoip.continent_name* to the *Columns* field in the layer pane. + @@ -360,3 +371,6 @@ Now that you have a complete overview of your ecommerce sales data, save the das . Select *Store time with dashboard*. . Click *Save*. + +[role="screenshot"] +image::images/lens_timeSeriesDataTutorialDashboard_7.16.png[Final dashboard with ecommerce sample data] diff --git a/docs/user/dashboard/lens.asciidoc b/docs/user/dashboard/lens.asciidoc index c3e0a5523a78d1..23a6d1fbcfd3d5 100644 --- a/docs/user/dashboard/lens.asciidoc +++ b/docs/user/dashboard/lens.asciidoc @@ -48,6 +48,8 @@ Choose the data you want to visualize. . If you want to learn more about the data a field contains, click the field. +. To visualize more than one index pattern, click *Add layer > Add visualization layer*, then select the index pattern. + Edit and delete. . To change the aggregation *Quick function* and display options, click the field in the layer pane. @@ -60,11 +62,11 @@ Edit and delete. Change the fields list to display a different index pattern, different time range, or add your own fields. -* To create a visualization with fields in a different index pattern, open the *Change index pattern* dropdown, then select the index pattern. +* To create a visualization with fields in a different index pattern, open the *Index pattern* dropdown, then select the index pattern. * If the fields list is empty, change the <>. -* To add fields, open the action menu (*...*) next to the *Change index pattern* dropdown, then select *Add field to index pattern*. +* To add fields, open the action menu (*...*) next to the *Index pattern* dropdown, then select *Add field to index pattern*. + [role="screenshot"] image:images/runtime-field-menu.png[Dropdown menu located next to index pattern field with items for adding and managing fields, width=50%] @@ -176,6 +178,29 @@ Compare your real-time data set to the results that are offset by a time increme For a time shift example, refer to <>. +[float] +[[add-reference-lines]] +==== Add reference lines + +With reference lines, you can identify specific values in your visualizations with icons, colors, and other display options. You can add reference lines to any visualization type that displays axes. + +For example, to track the number of bytes in the 75th percentile, add a shaded *Percentile* reference line to your time series visualization. + +[role="screenshot"] +image::images/lens_referenceLine_7.16.png[Lens drag and drop focus state] + +. In the layer pane, click *Add layer > Add reference layer*. + +. Click the reference line value, then specify the reference line you want to use: + +* To add a static reference line, click *Static*, then enter the reference line value you want to use. + +* To add a dynamic reference line, click *Quick functions*, then click and configure the functions you want to use. + +* To calculate the reference line value with math, click *Formula*, then enter the formula. + +. Specify the display options, such as *Display name* and *Icon*, then click *Close*. + [float] [[filter-the-data]] ==== Apply filters @@ -236,9 +261,29 @@ The following component menus are available: * *Left axis*, *Bottom axis*, and *Right axis* — Specify how you want to display the chart axes. For example, add axis labels and change the orientation and bounds. +[float] +[[view-data-and-requests]] +==== View the visualization data and requests + +To view the data included in the visualization and the requests that collected the data, use the *Inspector*. + +. In the toolbar, click *Inspect*. + +. Open the *View* dropdown, then click *Data*. + +.. From the dropdown, select the table that contains the data you want to view. + +.. To download the data, click *Download CSV*, then select the format type. + +. Open the *View* dropdown, then click *Requests*. + +.. From the dropdown, select the requests you want to view. + +.. To view the requests in *Console*, click *Request*, then click *Open in Console*. + [float] [[save-the-lens-panel]] -===== Save and add the panel +==== Save and add the panel Save the panel to the *Visualize Library* and add it to the dashboard, or add it to the dashboard without saving. @@ -408,7 +453,7 @@ To configure the bounds, use the menus in the editor toolbar. Bar and area chart .*Is it possible to display icons in data tables?* [%collapsible] ==== -You can display icons with <> in data tables. +You can display icons with <> in data tables. ==== [discrete] diff --git a/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc b/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc index c3d76ee88322ba..e270c16cf60f6b 100644 --- a/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc +++ b/docs/user/dashboard/tutorial-create-a-dashboard-of-lens-panels.asciidoc @@ -1,21 +1,24 @@ [[create-a-dashboard-of-panels-with-web-server-data]] -== Build your first dashboard +== Create your first dashboard -Learn the most common ways to build a dashboard from your own data. +Learn the most common ways to create a dashboard from your own data. The tutorial will use sample data from the perspective of an analyst looking at website logs, but this type of dashboard works on any type of data. -Before using this tutorial, you should be familiar with the <>. + +When you're done, you'll have a complete overview of the sample web logs data. [role="screenshot"] -image::images/lens_end_to_end_dashboard.png[Final dashboard vis] +image::images/lens_logsDashboard_7.16.png[Logs dashboard] + +Before you begin, you should be familiar with the <>. [discrete] [[add-the-data-and-create-the-dashboard]] === Add the data and create the dashboard -Add the sample web logs data that you'll use to create the dashboard panels. +Add the sample web logs data, and create and set up the dashboard. -. Go to the {kib} *Home* page, then click *Try our sample data*. +. Go to the *Home* page, then click *Try sample data*. . On the *Sample web logs* card, click *Add data*. @@ -29,56 +32,70 @@ Create the dashboard where you'll display the visualization panels. [float] [[open-and-set-up-lens]] -=== Open Lens and get familiar with the data +=== Open the visualization editor and get familiar with the data + +Open the visualization editor, then make sure the correct fields appear. . On the dashboard, click *Create visualization*. . Make sure the *kibana_sample_data_logs* index appears. + [role="screenshot"] -image::images/lens_end_to_end_1_2.png[Lens index pattern selector, width=50%] +image::images/lens_indexPatternDropDown_7.16.png[Index pattern dropdown] + +To create the visualizations in this tutorial, you'll use the following fields: + +* *Records* -. To create the visualizations in this tutorial, you'll use the *Records*, *timestamp*, *bytes*, *clientip*, and *referer.keyword* fields. To see the most frequent values of a field, hover over the field name, then click *i*. +* *timestamp* + +* *bytes* + +* *clientip* + +* *referer.keyword* + +To see the most frequent values in a field, hover over the field name, then click *i*. [discrete] [[view-the-number-of-website-visitors]] === Create your first visualization -Pick a field you want to analyze, such as *clientip*. If you want -to analyze only this field, you can use the *Metric* visualization to display a big number. -The only number function that you can use with *clientip* is *Unique count*. -*Unique count*, also referred to as cardinality, approximates the number of unique values -of the *clientip* field. +Pick a field you want to analyze, such as *clientip*. To analyze only the *clientip* field, use the *Metric* visualization to display the field as a number. + +The only number function that you can use with *clientip* is *Unique count*, also referred to as cardinality, which approximates the number of unique values. -. To select the visualization type, open the *Chart type* dropdown, then select *Metric*. +. Open the *Visualization type* dropdown, then select *Metric*. + [role="screenshot"] -image::images/lens_end_to_end_1_2_1.png[Chart Type dropdown with Metric selected, width=50%] +image::images/lens_visualizationTypeDropdown_7.16.png[Visualization type dropdown] -. From the *Available fields* list, drag *clientip* to the workspace. +. From the *Available fields* list, drag *clientip* to the workspace or layer pane. + [role="screenshot"] -image::images/lens_end_to_end_1_3.png[Changed type and dropped clientip field] +image::images/lens_metricUniqueCountOfClientip_7.16.png[Metric visualization of the clientip field] + -*Lens* selects the *Unique count* function because it is the only numeric function -that works for IP addresses. You can also drag *clientip* to the layer pane for the same result. +In the layer pane, *Unique count of clientip* appears because the editor automatically applies the *Unique count* function to the *clientip* field. *Unique count* is the only numeric function that works with IP addresses. . In the layer pane, click *Unique count of clientip*. .. In the *Display name* field, enter `Unique visitors`. .. Click *Close*. ++ +[role="screenshot"] +image::images/lens_metricUniqueVisitors_7.16.png[Metric visualization that displays number of unique visitors] . Click *Save and return*. + -The metric visualization has its own label, so you do not need to add a panel title. +*[No Title]* appears in the visualization panel header. Since the visualization has its own `Unique visitors` label, you do not need to add a panel title. [discrete] [[mixed-multiaxis]] === View a metric over time -*Lens* has two shortcuts that simplify viewing metrics over time. -If you drag a numeric field to the workspace, *Lens* adds the default +There are two shortcuts you can use to view metrics over time. +When you drag a numeric field to the workspace, the visualization editor adds the default time field from the index pattern. When you use the *Date histogram* function, you can replace the time field by dragging the field to the workspace. @@ -88,78 +105,76 @@ To visualize the *bytes* field over time: . From the *Available fields* list, drag *bytes* to the workspace. + -*Lens* creates a bar chart with the *timestamp* and *Median of bytes* fields, and automatically chooses a date interval. +The visualization editor creates a bar chart with the *timestamp* and *Median of bytes* fields. -. To zoom in on the data you want to view, click and drag your cursor across the bars. +. To zoom in on the data, click and drag your cursor across the bars. + [role="screenshot"] image::images/lens_end_to_end_3_1_1.gif[Zoom in on the data] -To emphasize the change in *Median of bytes* over time, change to a line chart with one of the following options: - -* From the *Suggestions*, click the line chart. -* Open the *Chart type* dropdown in the editor toolbar, then select *Line*. -* Open the *Chart type* menu in the layer pane, then click the line chart. +To emphasize the change in *Median of bytes* over time, change the visualization type to *Line* with one of the following options: -You can increase and decrease the minimum interval that *Lens* uses, but you are unable to decrease the interval -below the <>. +* In the *Suggestions*, click the line chart. +* In the editor toolbar, open the *Visualization type* dropdown, then select *Line*. +* In the layer pane, open the *Layer visualization type* menu, then click *Line*. -To set the minimum time interval: +To increase the minimum time interval: . In the layer pane, click *timestamp*. . Select *Customize time interval*. . Change the *Minimum interval* to *1 days*, then click *Close*. ++ +You can increase and decrease the minimum interval, but you are unable to decrease the interval below the <>. -To save space on the dashboard, hide the vertical and horizontal axis labels. +To save space on the dashboard, hide the axis labels. . Open the *Left axis* menu, then deselect *Show*. + [role="screenshot"] -image::images/lens_end_to_end_4_3.png[Turn off axis label] +image::images/lens_leftAxisMenu_7.16.png[Left axis menu] . Open the *Bottom axis* menu, then deselect *Show*. ++ +[role="screenshot"] +image::images/lens_lineChartMetricOverTime_7.16.png[Line chart that displays metric data over time] . Click *Save and return* -Add a panel title to explain the panel, which is necessary because you removed the axis labels. +Since you removed the axis labels, add a panel title: -.. Open the panel menu, then select *Edit panel title*. +. Open the panel menu, then select *Edit panel title*. -.. In the *Panel title* field, enter `Median of bytes`, then click *Save*. +. In the *Panel title* field, enter `Median of bytes`, then click *Save*. [discrete] [[view-the-distribution-of-visitors-by-operating-system]] === View the top values of a field +Create a visualization that displays the most frequent values of *request.keyword* on your website, ranked by the unique visitors. +To create the visualization, use *Top values of request.keyword* ranked by *Unique count of clientip*, instead of being ranked by *Count of records*. + The *Top values* function ranks the unique values of a field by another function. The values are the most frequent when ranked by a *Count* function, and the largest when ranked by the *Sum* function. -Create a visualization that displays the most frequent values of *request.keyword* on your website, ranked by the unique visitors. -To create the visualization, use *Top values of request.keyword* ranked by *Unique count of clientip*, instead of -being ranked by *Count of records*. - . On the dashboard, click *Create visualization*. . From the *Available fields* list, drag *clientip* to the *Vertical axis* field in the layer pane. + -*Lens* automatically chooses the *Unique count* function. If you drag *clientip* to the workspace, *Lens* adds the field to the incorrect axis. -+ -When you drag a text or IP address field to the workspace, -*Lens* adds the *Top values* function ranked by *Count of records* to show the most frequent values. +The visualization editor automatically applies the *Unique count* function. If you drag *clientip* to the workspace, the editor adds the field to the incorrect axis. . Drag *request.keyword* to the workspace. + [role="screenshot"] image::images/lens_end_to_end_2_1_1.png[Vertical bar chart with top values of request.keyword by most unique visitors] + -*Lens* adds *Top values of request.keyword* to the *Horizontal axis*. +When you drag a text or IP address field to the workspace, +the editor adds the *Top values* function ranked by *Count of records* to show the most frequent values. -The chart is hard to read because the *request.keyword* field contains long text. You could try -using one of the *Suggestions*, but the suggestions also have issues with long text. Instead, create a *Table* visualization. +The chart labels are unable to display because the *request.keyword* field contains long text fields. You could use one of the *Suggestions*, but the suggestions also have issues with long text. The best way to display long text fields is with the *Table* visualization. -. Open the *Chart type* dropdown, then select *Table*. +. Open the *Visualization type* dropdown, then select *Table*. + [role="screenshot"] image::images/lens_end_to_end_2_1_2.png[Table with top values of request.keyword by most unique visitors] @@ -171,16 +186,19 @@ image::images/lens_end_to_end_2_1_2.png[Table with top values of request.keyword .. In the *Display name* field, enter `Page URL`. .. Click *Close*. ++ +[role="screenshot"] +image::images/lens_tableTopFieldValues_7.16.png[Table that displays the top field values] . Click *Save and return*. + -The table does not need a panel title because the columns are clearly labeled. +Since the table columns are labeled, you do not need to add a panel title. [discrete] [[custom-ranges]] === Compare a subset of documents to all documents -Create a proportional visualization that helps you to determine if your users transfer more bytes from documents under 10KB versus documents over 10 Kb. +Create a proportional visualization that helps you determine if your users transfer more bytes from documents under 10KB versus documents over 10Kb. . On the dashboard, click *Create visualization*. @@ -190,12 +208,14 @@ Create a proportional visualization that helps you to determine if your users tr . From the *Available fields* list, drag *bytes* to the *Break down by* field in the layer pane. -Use the *Intervals* function to select documents based on the number range of a field. -If the ranges were non numeric, or if the query required multiple clauses, you could use the *Filters* function. +To select documents based on the number range of a field, use the *Intervals* function. +When the ranges are non numeric, or the query requires multiple clauses, you could use the *Filters* function. + +Specify the file size ranges: -. To specify the file size ranges, click *bytes* in the layer pane. +. In the layer pane, click *bytes*. -. Click *Create custom ranges*, enter the following, then press Return: +. Click *Create custom ranges*, enter the following in the *Ranges* field, then press Return: * *Ranges* — `0` -> `10240` @@ -214,27 +234,30 @@ image::images/lens_end_to_end_6_1.png[Custom ranges configuration] To display the values as a percentage of the sum of all values, use the *Pie* chart. -. Open the *Chart Type* dropdown, then select *Pie*. +. Open the *Visualization Type* dropdown, then select *Pie*. ++ +[role="screenshot"] +image::images/lens_pieChartCompareSubsetOfDocs_7.16.png[Pie chart that compares a subset of documents to all documents] . Click *Save and return*. -. Add a panel title. +Add a panel title: -.. Open the panel menu, then select *Edit panel title*. +. Open the panel menu, then select *Edit panel title*. -.. In the *Panel title* field, enter `Sum of bytes from large requests`, then click *Save*. +. In the *Panel title* field, enter `Sum of bytes from large requests`, then click *Save*. [discrete] [[histogram]] === View the distribution of a number field -Knowing the distribution of a number helps you find patterns. For example, you can analyze the website traffic per hour to find the best time to do routine maintenance. +The distribution of a number can help you find patterns. For example, you can analyze the website traffic per hour to find the best time for routine maintenance. . On the dashboard, click *Create visualization*. . From the *Available fields* list, drag *bytes* to *Vertical axis* field in the layer pane. -. In the layer pane, click *Median of bytes* +. In the layer pane, click *Median of bytes*. .. Click the *Sum* function. @@ -246,70 +269,80 @@ Knowing the distribution of a number helps you find patterns. For example, you c . In the layer pane, click *hour_of_day*, then slide the *Intervals granularity* slider until the horizontal axis displays hourly intervals. + -The *Intervals* function displays an evenly spaced distribution of the field. +[role="screenshot"] +image::images/lens_barChartDistributionOfNumberField_7.16.png[Bar chart that displays the distribution of a number field] . Click *Save and return*. +Add a panel title: + +. Open the panel menu, then select *Edit panel title*. + +. In the *Panel title* field, enter `Website traffic`, then click *Save*. + [discrete] [[treemap]] === Create a multi-level chart -You can use multiple functions in data tables and proportion charts. For example, -to create a chart that breaks down the traffic sources and user geography, use *Filters* and -*Top values*. +*Table* and *Proportion* visualizations support multiple functions. For example, to create visualizations that break down the data by website traffic sources and user geography, apply the *Filters* and *Top values* functions. . On the dashboard, click *Create visualization*. -. Open the *Chart type* dropdown, then select *Treemap*. +. Open the *Visualization type* dropdown, then select *Treemap*. . From the *Available fields* list, drag *Records* to the *Size by* field in the layer pane. -. In the editor, click the *Drop a field or click to add* field for *Group by*, then create a filter for each website traffic source. +. In the editor, click *Add or drag-and-drop a field* for *Group by*. -.. From *Select a function*, click *Filters*. +Create a filter for each website traffic source: -.. Click *All records*, enter the following, then press Return: +. From *Select a function*, click *Filters*. + +. Click *All records*, enter the following in the query bar, then press Return: * *KQL* — `referer : *facebook.com*` * *Label* — `Facebook` -.. Click *Add a filter*, enter the following, then press Return: +. Click *Add a filter*, enter the following in the query bar, then press Return: * *KQL* — `referer : *twitter.com*` * *Label* — `Twitter` -.. Click *Add a filter*, enter the following, then press Return: +. Click *Add a filter*, enter the following in the query bar, then press Return: * *KQL* — `NOT referer : *twitter.com* OR NOT referer: *facebook.com*` * *Label* — `Other` -.. Click *Close*. +. Click *Close*. -Add a geography grouping: +Add the user geography grouping: -. From the *Available fields* list, drag *geo.src* to the workspace. +. From the *Available fields* list, drag *geo.srcdest* to the workspace. -. To change the *Group by* order, drag *Top values of geo.src* so that it appears first. +. To change the *Group by* order, drag *Top values of geo.srcdest* in the layer pane so that appears first. + [role="screenshot"] image::images/lens_end_to_end_7_2.png[Treemap visualization] -. To view only the Facebook and Twitter data, remove the *Other* category. +Remove the documents that do not match the filter criteria: -.. In the layer pane, click *Top values of geo.src*. +. In the layer pane, click *Top values of geo.srcdest*. -.. Open the *Advanced* dropdown, deselect *Group other values as "Other"*, then click *Close*. +. Click *Advanced*, then deselect *Group other values as "Other"*, the click *Close*. ++ +[role="screenshot"] +image::images/lens_treemapMultiLevelChart_7.16.png[Treemap visualization] . Click *Save and return*. -. Add a panel title. +Add a panel title: -.. Open the panel menu, then select *Edit panel title*. +. Open the panel menu, then select *Edit panel title*. -.. In the *Panel title* field, enter `Page views by location and referrer`, then click *Save*. +. In the *Panel title* field, enter `Page views by location and referrer`, then click *Save*. [float] [[arrange-the-lens-panels]] @@ -317,7 +350,7 @@ image::images/lens_end_to_end_7_2.png[Treemap visualization] Resize and move the panels so they all appear on the dashboard without scrolling. -Decrease the size of the following panels, then move them to the first row: +Decrease the size of the following panels, then move the panels to the first row: * *Unique visitors* @@ -325,7 +358,10 @@ Decrease the size of the following panels, then move them to the first row: * *Sum of bytes from large requests* -* *hour_of_day* +* *Website traffic* ++ +[role="screenshot"] +image::images/lens_logsDashboard_7.16.png[Logs dashboard] [discrete] === Save the dashboard diff --git a/docs/user/security/securing-kibana.asciidoc b/docs/user/security/securing-kibana.asciidoc index a4ec2ecadece31..bdb36a6fe117c7 100644 --- a/docs/user/security/securing-kibana.asciidoc +++ b/docs/user/security/securing-kibana.asciidoc @@ -108,7 +108,7 @@ TIP: For more information on Basic Authentication and additional methods of auth TIP: You can define as many different roles for your {kib} users as you need. For example, create roles that have `read` and `view_index_metadata` privileges -on specific index patterns. For more information, see +on specific data views. For more information, see {ref}/authorization.html[User authorization]. -- diff --git a/package.json b/package.json index b77c88f7a2ba33..e68654549a4f24 100644 --- a/package.json +++ b/package.json @@ -95,9 +95,9 @@ "@dnd-kit/core": "^3.1.1", "@dnd-kit/sortable": "^4.0.0", "@dnd-kit/utilities": "^2.0.0", - "@elastic/apm-synthtrace": "link:bazel-bin/packages/elastic-apm-synthtrace", "@elastic/apm-rum": "^5.9.1", "@elastic/apm-rum-react": "^1.3.1", + "@elastic/apm-synthtrace": "link:bazel-bin/packages/elastic-apm-synthtrace", "@elastic/charts": "38.1.3", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.35", @@ -111,7 +111,10 @@ "@elastic/request-crypto": "1.1.4", "@elastic/safer-lodash-set": "link:bazel-bin/packages/elastic-safer-lodash-set", "@elastic/search-ui-app-search-connector": "^1.6.0", + "@emotion/cache": "^11.4.0", + "@emotion/css": "^11.4.0", "@emotion/react": "^11.4.0", + "@emotion/serialize": "^1.0.2", "@hapi/accept": "^5.0.2", "@hapi/boom": "^9.1.4", "@hapi/cookie": "^11.0.2", @@ -542,6 +545,7 @@ "@types/jest-when": "^2.7.2", "@types/joi": "^17.2.3", "@types/jquery": "^3.3.31", + "@types/js-levenshtein": "^1.1.0", "@types/js-search": "^1.4.0", "@types/js-yaml": "^3.11.1", "@types/jsdom": "^16.2.3", @@ -595,6 +599,7 @@ "@types/react-router-dom": "^5.1.5", "@types/react-test-renderer": "^16.9.1", "@types/react-virtualized": "^9.18.7", + "@types/react-vis": "^1.11.9", "@types/read-pkg": "^4.0.0", "@types/recompose": "^0.30.6", "@types/reduce-reducers": "^1.0.0", @@ -766,7 +771,6 @@ "oboe": "^2.1.4", "parse-link-header": "^1.0.1", "pbf": "3.2.1", - "pdf-to-img": "^1.1.1", "pirates": "^4.0.1", "pixelmatch": "^5.1.0", "postcss": "^7.0.32", diff --git a/packages/elastic-apm-synthtrace/src/lib/utils/clean_write_targets.ts b/packages/elastic-apm-synthtrace/src/lib/utils/clean_write_targets.ts index 3c514e1097b31f..4a2ab281a28496 100644 --- a/packages/elastic-apm-synthtrace/src/lib/utils/clean_write_targets.ts +++ b/packages/elastic-apm-synthtrace/src/lib/utils/clean_write_targets.ts @@ -27,6 +27,7 @@ export async function cleanWriteTargets({ index: targets, allow_no_indices: true, conflicts: 'proceed', + refresh: true, body: { query: { match_all: {}, diff --git a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts index 9cb902882ffd76..9fa13b013f1959 100644 --- a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts +++ b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts @@ -68,6 +68,7 @@ it('produces the right watch and ignore list', () => { /x-pack/plugins/reporting/chromium, /x-pack/plugins/security_solution/cypress, /x-pack/plugins/apm/scripts, + /x-pack/plugins/apm/ftr_e2e, /x-pack/plugins/canvas/canvas_plugin_src, /x-pack/plugins/cases/server/scripts, /x-pack/plugins/lists/scripts, diff --git a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts index 53f52279c8be81..e1bd431d280a42 100644 --- a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts +++ b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts @@ -59,6 +59,7 @@ export function getServerWatchPaths({ pluginPaths, pluginScanDirs }: Options) { fromRoot('x-pack/plugins/reporting/chromium'), fromRoot('x-pack/plugins/security_solution/cypress'), fromRoot('x-pack/plugins/apm/scripts'), + fromRoot('x-pack/plugins/apm/ftr_e2e'), // prevents restarts for APM cypress tests fromRoot('x-pack/plugins/canvas/canvas_plugin_src'), // prevents server from restarting twice for Canvas plugin changes, fromRoot('x-pack/plugins/cases/server/scripts'), fromRoot('x-pack/plugins/lists/scripts'), diff --git a/packages/kbn-securitysolution-autocomplete/src/field/index.tsx b/packages/kbn-securitysolution-autocomplete/src/field/index.tsx index 69408e919bb1e1..a89e0a096b673a 100644 --- a/packages/kbn-securitysolution-autocomplete/src/field/index.tsx +++ b/packages/kbn-securitysolution-autocomplete/src/field/index.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useMemo, useState } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { IndexPatternBase, IndexPatternFieldBase } from '@kbn/es-query'; +import { DataViewBase, DataViewFieldBase } from '@kbn/es-query'; import { getGenericComboBoxProps, @@ -20,14 +20,14 @@ const AS_PLAIN_TEXT = { asPlainText: true }; interface OperatorProps { fieldInputWidth?: number; fieldTypeFilter?: string[]; - indexPattern: IndexPatternBase | undefined; + indexPattern: DataViewBase | undefined; isClearable: boolean; isDisabled: boolean; isLoading: boolean; isRequired?: boolean; - onChange: (a: IndexPatternFieldBase[]) => void; + onChange: (a: DataViewFieldBase[]) => void; placeholder: string; - selectedField: IndexPatternFieldBase | undefined; + selectedField: DataViewFieldBase | undefined; } export const FieldComponent: React.FC = ({ @@ -56,7 +56,7 @@ export const FieldComponent: React.FC = ({ const handleValuesChange = useCallback( (newOptions: EuiComboBoxOptionOption[]): void => { - const newValues: IndexPatternFieldBase[] = newOptions.map( + const newValues: DataViewFieldBase[] = newOptions.map( ({ label }) => availableFields[labels.indexOf(label)] ); onChange(newValues); @@ -94,13 +94,13 @@ export const FieldComponent: React.FC = ({ FieldComponent.displayName = 'Field'; interface ComboBoxFields { - availableFields: IndexPatternFieldBase[]; - selectedFields: IndexPatternFieldBase[]; + availableFields: DataViewFieldBase[]; + selectedFields: DataViewFieldBase[]; } const getComboBoxFields = ( - indexPattern: IndexPatternBase | undefined, - selectedField: IndexPatternFieldBase | undefined, + indexPattern: DataViewBase | undefined, + selectedField: DataViewFieldBase | undefined, fieldTypeFilter: string[] ): ComboBoxFields => { const existingFields = getExistingFields(indexPattern); @@ -113,29 +113,27 @@ const getComboBoxFields = ( const getComboBoxProps = (fields: ComboBoxFields): GetGenericComboBoxPropsReturn => { const { availableFields, selectedFields } = fields; - return getGenericComboBoxProps({ + return getGenericComboBoxProps({ getLabel: (field) => field.name, options: availableFields, selectedOptions: selectedFields, }); }; -const getExistingFields = (indexPattern: IndexPatternBase | undefined): IndexPatternFieldBase[] => { +const getExistingFields = (indexPattern: DataViewBase | undefined): DataViewFieldBase[] => { return indexPattern != null ? indexPattern.fields : []; }; -const getSelectedFields = ( - selectedField: IndexPatternFieldBase | undefined -): IndexPatternFieldBase[] => { +const getSelectedFields = (selectedField: DataViewFieldBase | undefined): DataViewFieldBase[] => { return selectedField ? [selectedField] : []; }; const getAvailableFields = ( - existingFields: IndexPatternFieldBase[], - selectedFields: IndexPatternFieldBase[], + existingFields: DataViewFieldBase[], + selectedFields: DataViewFieldBase[], fieldTypeFilter: string[] -): IndexPatternFieldBase[] => { - const fieldsByName = new Map(); +): DataViewFieldBase[] => { + const fieldsByName = new Map(); existingFields.forEach((f) => fieldsByName.set(f.name, f)); selectedFields.forEach((f) => fieldsByName.set(f.name, f)); diff --git a/packages/kbn-securitysolution-hook-utils/src/use_async/index.test.ts b/packages/kbn-securitysolution-hook-utils/src/use_async/index.test.ts index 886a3dd27befcb..7503f2c5c2be81 100644 --- a/packages/kbn-securitysolution-hook-utils/src/use_async/index.test.ts +++ b/packages/kbn-securitysolution-hook-utils/src/use_async/index.test.ts @@ -18,6 +18,13 @@ interface TestArgs { type TestReturn = Promise; describe('useAsync', () => { + /** + * Timeout for both jest tests and for the waitForNextUpdate. + * jest tests default to 5 seconds and waitForNextUpdate defaults to 1 second. + * 20_0000 = 20,000 milliseconds = 20 seconds + */ + const timeout = 20_000; + let fn: jest.Mock; let args: TestArgs; @@ -31,16 +38,20 @@ describe('useAsync', () => { expect(fn).not.toHaveBeenCalled(); }); - it('invokes the function when start is called', async () => { - const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); + it( + 'invokes the function when start is called', + async () => { + const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); - act(() => { - result.current.start(args); - }); - await waitForNextUpdate(); + act(() => { + result.current.start(args); + }); + await waitForNextUpdate({ timeout }); - expect(fn).toHaveBeenCalled(); - }); + expect(fn).toHaveBeenCalled(); + }, + timeout + ); it('invokes the function with start args', async () => { const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); @@ -49,84 +60,99 @@ describe('useAsync', () => { act(() => { result.current.start(args); }); - await waitForNextUpdate(); + await waitForNextUpdate({ timeout }); expect(fn).toHaveBeenCalledWith(expectedArgs); }); - it('populates result with the resolved value of the fn', async () => { - const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); - fn.mockResolvedValue({ resolved: 'value' }); - - act(() => { - result.current.start(args); - }); - await waitForNextUpdate(); - - expect(result.current.result).toEqual({ resolved: 'value' }); - expect(result.current.error).toBeUndefined(); - }); - - it('populates error if function rejects', async () => { - fn.mockRejectedValue(new Error('whoops')); - const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); - - act(() => { - result.current.start(args); - }); - await waitForNextUpdate(); - - expect(result.current.result).toBeUndefined(); - expect(result.current.error).toEqual(new Error('whoops')); - }); - - it('populates the loading state while the function is pending', async () => { - let resolve: () => void; - fn.mockImplementation(() => new Promise((_resolve) => (resolve = _resolve))); - - const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); - - act(() => { - result.current.start(args); - }); - - expect(result.current.loading).toBe(true); - - act(() => resolve()); - await waitForNextUpdate(); - - expect(result.current.loading).toBe(false); - }); - - it('multiple start calls reset state', async () => { - let resolve: (result: string) => void; - fn.mockImplementation(() => new Promise((_resolve) => (resolve = _resolve))); - - const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); - - act(() => { - result.current.start(args); - }); - - expect(result.current.loading).toBe(true); - - act(() => resolve('result')); - await waitForNextUpdate(); - - expect(result.current.loading).toBe(false); - expect(result.current.result).toBe('result'); - - act(() => { - result.current.start(args); - }); - - expect(result.current.loading).toBe(true); - expect(result.current.result).toBe(undefined); - - act(() => resolve('result')); - await waitForNextUpdate(); - - expect(result.current.loading).toBe(false); - expect(result.current.result).toBe('result'); - }); + it( + 'populates result with the resolved value of the fn', + async () => { + const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); + fn.mockResolvedValue({ resolved: 'value' }); + + act(() => { + result.current.start(args); + }); + await waitForNextUpdate({ timeout }); + + expect(result.current.result).toEqual({ resolved: 'value' }); + expect(result.current.error).toBeUndefined(); + }, + timeout + ); + + it( + 'populates error if function rejects', + async () => { + fn.mockRejectedValue(new Error('whoops')); + const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); + + act(() => { + result.current.start(args); + }); + await waitForNextUpdate({ timeout }); + + expect(result.current.result).toBeUndefined(); + expect(result.current.error).toEqual(new Error('whoops')); + }, + timeout + ); + + it( + 'populates the loading state while the function is pending', + async () => { + let resolve: () => void; + fn.mockImplementation(() => new Promise((_resolve) => (resolve = _resolve))); + + const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); + + act(() => { + result.current.start(args); + }); + + expect(result.current.loading).toBe(true); + + act(() => resolve()); + await waitForNextUpdate({ timeout }); + + expect(result.current.loading).toBe(false); + }, + timeout + ); + + it( + 'multiple start calls reset state', + async () => { + let resolve: (result: string) => void; + fn.mockImplementation(() => new Promise((_resolve) => (resolve = _resolve))); + + const { result, waitForNextUpdate } = renderHook(() => useAsync(fn)); + + act(() => { + result.current.start(args); + }); + + expect(result.current.loading).toBe(true); + + act(() => resolve('result')); + await waitForNextUpdate({ timeout }); + + expect(result.current.loading).toBe(false); + expect(result.current.result).toBe('result'); + + act(() => { + result.current.start(args); + }); + + expect(result.current.loading).toBe(true); + expect(result.current.result).toBe(undefined); + act(() => resolve('result')); + await waitForNextUpdate({ timeout }); + + expect(result.current.loading).toBe(false); + expect(result.current.result).toBe('result'); + }, + timeout + ); }); diff --git a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts index b5d379d3426e72..4130cd8d138b87 100644 --- a/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts +++ b/packages/kbn-test/src/functional_test_runner/functional_test_runner.ts @@ -78,18 +78,33 @@ export class FunctionalTestRunner { // replace the function of custom service providers so that they return // promise-like objects which never resolve, essentially disabling them // allowing us to load the test files and populate the mocha suites - const readStubbedProviderSpec = (type: string, providers: any) => + const readStubbedProviderSpec = (type: string, providers: any, skip: string[]) => readProviderSpec(type, providers).map((p) => ({ ...p, - fn: () => ({ - then: () => {}, - }), + fn: skip.includes(p.name) + ? (...args: unknown[]) => { + const result = p.fn(...args); + if ('then' in result) { + throw new Error( + `Provider [${p.name}] returns a promise so it can't loaded during test analysis` + ); + } + + return result; + } + : () => ({ + then: () => {}, + }), })); const providers = new ProviderCollection(this.log, [ ...coreProviders, - ...readStubbedProviderSpec('Service', config.get('services')), - ...readStubbedProviderSpec('PageObject', config.get('pageObjects')), + ...readStubbedProviderSpec( + 'Service', + config.get('services'), + config.get('servicesRequiredForTestAnalysis') + ), + ...readStubbedProviderSpec('PageObject', config.get('pageObjects'), []), ]); const mocha = await setupMocha(this.lifecycle, this.log, config, providers); diff --git a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts index 7fae313c68bd33..a9ceaa643a60f3 100644 --- a/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts +++ b/packages/kbn-test/src/functional_test_runner/lib/config/schema.ts @@ -89,6 +89,7 @@ export const schema = Joi.object() }) .default(), + servicesRequiredForTestAnalysis: Joi.array().items(Joi.string()).default([]), services: Joi.object().pattern(ID_PATTERN, Joi.func().required()).default(), pageObjects: Joi.object().pattern(ID_PATTERN, Joi.func().required()).default(), diff --git a/scripts/docs.js b/scripts/docs.js index 6522079c7aca33..f310903b90bac5 100644 --- a/scripts/docs.js +++ b/scripts/docs.js @@ -7,4 +7,4 @@ */ require('../src/setup_node_env'); -require('../src/docs/cli'); +require('../src/dev/run_build_docs_cli').runBuildDocsCli(); diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 7284a1a3f06f03..c54922ecd67dfe 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -305,9 +305,9 @@ export class DocLinksService { }, observability: { guide: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/index.html`, - infrastructureThreshold: `{ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/infrastructure-threshold-alert.html`, - logsThreshold: `{ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/logs-threshold-alert.html`, - metricsThreshold: `{ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/metrics-threshold-alert.html`, + infrastructureThreshold: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/infrastructure-threshold-alert.html`, + logsThreshold: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/logs-threshold-alert.html`, + metricsThreshold: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/metrics-threshold-alert.html`, monitorStatus: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/monitor-status-alert.html`, monitorUptime: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/monitor-uptime.html`, tlsCertificate: `${ELASTIC_WEBSITE_URL}guide/en/observability/${DOC_LINK_VERSION}/tls-certificate-alert.html`, @@ -507,6 +507,9 @@ export class DocLinksService { rubyOverview: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/ruby-api/${DOC_LINK_VERSION}/ruby_client.html`, rustGuide: `${ELASTIC_WEBSITE_URL}guide/en/elasticsearch/client/rust-api/${DOC_LINK_VERSION}/index.html`, }, + endpoints: { + troubleshooting: `${ELASTIC_WEBSITE_URL}guide/en/security/${DOC_LINK_VERSION}/ts-management.html#ts-endpoints`, + }, }, }); } @@ -770,5 +773,8 @@ export interface DocLinksStart { readonly rubyOverview: string; readonly rustGuide: string; }; + readonly endpoints: { + readonly troubleshooting: string; + }; }; } diff --git a/src/core/public/overlays/flyout/flyout_service.tsx b/src/core/public/overlays/flyout/flyout_service.tsx index 6e986cc8ecb48c..79047738da4ddc 100644 --- a/src/core/public/overlays/flyout/flyout_service.tsx +++ b/src/core/public/overlays/flyout/flyout_service.tsx @@ -8,7 +8,7 @@ /* eslint-disable max-classes-per-file */ -import { EuiFlyout, EuiFlyoutSize } from '@elastic/eui'; +import { EuiFlyout, EuiFlyoutSize, EuiOverlayMaskProps } from '@elastic/eui'; import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { Subject } from 'rxjs'; @@ -86,6 +86,7 @@ export interface OverlayFlyoutOpenOptions { size?: EuiFlyoutSize; maxWidth?: boolean | number | string; hideCloseButton?: boolean; + maskProps?: EuiOverlayMaskProps; /** * EuiFlyout onClose handler. * If provided the consumer is responsible for calling flyout.close() to close the flyout; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 6c377bd2870aef..83aea9774bb56b 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -15,6 +15,7 @@ import { EuiButtonEmptyProps } from '@elastic/eui'; import { EuiConfirmModalProps } from '@elastic/eui'; import { EuiFlyoutSize } from '@elastic/eui'; import { EuiGlobalToastListToast } from '@elastic/eui'; +import { EuiOverlayMaskProps } from '@elastic/eui'; import { History } from 'history'; import { Href } from 'history'; import { IconType } from '@elastic/eui'; @@ -728,6 +729,9 @@ export interface DocLinksStart { readonly rubyOverview: string; readonly rustGuide: string; }; + readonly endpoints: { + readonly troubleshooting: string; + }; }; } @@ -1048,6 +1052,8 @@ export interface OverlayFlyoutOpenOptions { // (undocumented) hideCloseButton?: boolean; // (undocumented) + maskProps?: EuiOverlayMaskProps; + // (undocumented) maxWidth?: boolean | number | string; onClose?: (flyout: OverlayRef) => void; // (undocumented) diff --git a/src/core/server/ui_settings/settings/misc.test.ts b/src/core/server/ui_settings/settings/misc.test.ts deleted file mode 100644 index 7b6788664c9976..00000000000000 --- a/src/core/server/ui_settings/settings/misc.test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { UiSettingsParams } from '../../../types'; -import { getMiscUiSettings } from './misc'; - -describe('misc settings', () => { - const miscSettings = getMiscUiSettings(); - - const getValidationFn = (setting: UiSettingsParams) => (value: any) => - setting.schema.validate(value); - - describe('truncate:maxHeight', () => { - const validate = getValidationFn(miscSettings['truncate:maxHeight']); - - it('should only accept positive numeric values', () => { - expect(() => validate(127)).not.toThrow(); - expect(() => validate(-12)).toThrowErrorMatchingInlineSnapshot( - `"Value must be equal to or greater than [0]."` - ); - expect(() => validate('foo')).toThrowErrorMatchingInlineSnapshot( - `"expected value of type [number] but got [string]"` - ); - }); - }); -}); diff --git a/src/core/server/ui_settings/settings/misc.ts b/src/core/server/ui_settings/settings/misc.ts index cd9e43400d3c91..ad7411dfd12afa 100644 --- a/src/core/server/ui_settings/settings/misc.ts +++ b/src/core/server/ui_settings/settings/misc.ts @@ -6,23 +6,11 @@ * Side Public License, v 1. */ -import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; import { UiSettingsParams } from '../types'; export const getMiscUiSettings = (): Record => { return { - 'truncate:maxHeight': { - name: i18n.translate('core.ui_settings.params.maxCellHeightTitle', { - defaultMessage: 'Maximum table cell height', - }), - value: 115, - description: i18n.translate('core.ui_settings.params.maxCellHeightText', { - defaultMessage: - 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', - }), - schema: schema.number({ min: 0 }), - }, buildNum: { readonly: true, schema: schema.maybe(schema.number()), diff --git a/src/dev/bazel/index.bzl b/src/dev/bazel/index.bzl index 313cc9f06236c2..83d6361ff95f72 100644 --- a/src/dev/bazel/index.bzl +++ b/src/dev/bazel/index.bzl @@ -12,6 +12,8 @@ Please do not import from any other files when looking to use a custom rule load("//src/dev/bazel:jsts_transpiler.bzl", _jsts_transpiler = "jsts_transpiler") load("//src/dev/bazel:ts_project.bzl", _ts_project = "ts_project") +load("//src/dev/bazel:pkg_npm.bzl", _pkg_npm = "pkg_npm") jsts_transpiler = _jsts_transpiler +pkg_npm = _pkg_npm ts_project = _ts_project diff --git a/src/dev/bazel/pkg_npm.bzl b/src/dev/bazel/pkg_npm.bzl new file mode 100644 index 00000000000000..263d941d4b435d --- /dev/null +++ b/src/dev/bazel/pkg_npm.bzl @@ -0,0 +1,16 @@ +"Simple wrapper over the general pkg_npm rule from rules_nodejs so we can override some configs" + +load("@build_bazel_rules_nodejs//internal/pkg_npm:pkg_npm.bzl", _pkg_npm = "pkg_npm_macro") + +def pkg_npm(validate = False, **kwargs): + """A macro around the upstream pkg_npm rule. + + Args: + validate: boolean; Whether to check that the attributes match the package.json. Defaults to false + **kwargs: the rest + """ + + _pkg_npm( + validate = validate, + **kwargs + ) diff --git a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts index 340a035adea4cf..96d66b111b0624 100644 --- a/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts +++ b/src/dev/build/tasks/os_packages/docker_generator/templates/kibana_yml.template.ts @@ -22,7 +22,7 @@ function generator({ imageFlavor }: TemplateContext) { server.host: "0.0.0.0" server.shutdownTimeout: "5s" elasticsearch.hosts: [ "http://elasticsearch:9200" ] - ${!imageFlavor ? 'monitoring.ui.container.elasticsearch.enabled: true' : ''} + monitoring.ui.container.elasticsearch.enabled: true `); } diff --git a/src/dev/run_build_docs_cli.ts b/src/dev/run_build_docs_cli.ts new file mode 100644 index 00000000000000..aad524b4437d35 --- /dev/null +++ b/src/dev/run_build_docs_cli.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import Path from 'path'; + +import dedent from 'dedent'; +import { run, REPO_ROOT, createFailError } from '@kbn/dev-utils'; + +const DEFAULT_DOC_REPO_PATH = Path.resolve(REPO_ROOT, '..', 'docs'); + +const rel = (path: string) => Path.relative(process.cwd(), path); + +export function runBuildDocsCli() { + run( + async ({ flags, procRunner }) => { + const docRepoPath = + typeof flags.docrepo === 'string' && flags.docrepo + ? Path.resolve(process.cwd(), flags.docrepo) + : DEFAULT_DOC_REPO_PATH; + + try { + await procRunner.run('build_docs', { + cmd: rel(Path.resolve(docRepoPath, 'build_docs')), + args: [ + ['--doc', rel(Path.resolve(REPO_ROOT, 'docs/index.asciidoc'))], + ['--chunk', '1'], + flags.open ? ['--open'] : [], + ].flat(), + cwd: REPO_ROOT, + wait: true, + }); + } catch (error) { + if (error.code === 'ENOENT') { + throw createFailError(dedent` + Unable to run "build_docs" script from docs repo. + Does it exist at [${rel(docRepoPath)}]? + Do you need to pass --docrepo to specify the correct path or clone it there? + `); + } + + throw error; + } + }, + { + description: 'Build the docs and serve them from a docker container', + flags: { + string: ['docrepo'], + boolean: ['open'], + default: { + docrepo: DEFAULT_DOC_REPO_PATH, + }, + help: ` + --docrepo [path] Path to the doc repo, defaults to ${rel(DEFAULT_DOC_REPO_PATH)} + --open Automatically open the built docs in your default browser after building + `, + }, + } + ); +} diff --git a/src/docs/cli.js b/src/docs/cli.js deleted file mode 100644 index ac17c3908f0caa..00000000000000 --- a/src/docs/cli.js +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { execFileSync } from 'child_process'; -import { Command } from 'commander'; - -import { defaultDocsRepoPath, buildDocsScript, buildDocsArgs } from './docs_repo'; - -const cmd = new Command('node scripts/docs'); -cmd - .option('--docrepo [path]', 'local path to the docs repo', defaultDocsRepoPath()) - .option('--open', 'open the docs in the browser', false) - .parse(process.argv); - -try { - execFileSync(buildDocsScript(cmd), buildDocsArgs(cmd)); -} catch (err) { - if (err.code === 'ENOENT') { - console.error(`elastic/docs repo must be cloned to ${cmd.docrepo}`); - } else { - console.error(err.stack); - } - - process.exit(1); -} diff --git a/src/docs/docs_repo.js b/src/docs/docs_repo.js deleted file mode 100644 index 2d3589c444b34a..00000000000000 --- a/src/docs/docs_repo.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { resolve } from 'path'; - -const kibanaDir = resolve(__dirname, '..', '..'); - -export function buildDocsScript(cmd) { - return resolve(process.cwd(), cmd.docrepo, 'build_docs'); -} - -export function buildDocsArgs(cmd) { - const docsIndexFile = resolve(kibanaDir, 'docs', 'index.asciidoc'); - let args = ['--doc', docsIndexFile, '--direct_html', '--chunk=1']; - if (cmd.open) { - args = [...args, '--open']; - } - return args; -} - -export function defaultDocsRepoPath() { - return resolve(kibanaDir, '..', 'docs'); -} diff --git a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap index 2e37dc61fe851e..2f383adb3f5c3b 100644 --- a/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap +++ b/src/plugins/dashboard/public/application/listing/__snapshots__/dashboard_listing.test.tsx.snap @@ -34,13 +34,13 @@ exports[`after fetch When given a title that matches multiple dashboards, filter iconType="plusInCircle" onClick={[Function]} > - Create new dashboard + Create a dashboard } body={

- You can combine data views from any Kibana app into one dashboard and see everything in one place. + Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.

- Install some sample data + Add some sample data , } } @@ -146,13 +146,13 @@ exports[`after fetch initialFilter 1`] = ` iconType="plusInCircle" onClick={[Function]} > - Create new dashboard + Create a dashboard } body={

- You can combine data views from any Kibana app into one dashboard and see everything in one place. + Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.

- Install some sample data + Add some sample data , } } @@ -257,13 +257,13 @@ exports[`after fetch renders all table rows 1`] = ` iconType="plusInCircle" onClick={[Function]} > - Create new dashboard + Create a dashboard } body={

- You can combine data views from any Kibana app into one dashboard and see everything in one place. + Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.

- Install some sample data + Add some sample data , } } @@ -368,13 +368,13 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` iconType="plusInCircle" onClick={[Function]} > - Create new dashboard + Create a dashboard } body={

- You can combine data views from any Kibana app into one dashboard and see everything in one place. + Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.

- Install some sample data + Add some sample data , } } @@ -446,6 +446,128 @@ exports[`after fetch renders call to action when no dashboards exist 1`] = ` `; +exports[`after fetch renders call to action with continue when no dashboards exist but one is in progress 1`] = ` + + + + + Discard changes + + + + + Continue editing + + + + } + body={ + +

+ Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations. +

+
+ } + iconType="dashboardApp" + title={ +

+ Dashboard in progress +

+ } + /> + } + entityName="dashboard" + entityNamePlural="dashboards" + findItems={[Function]} + headingId="dashboardListingHeading" + initialFilter="" + initialPageSize={20} + listingLimit={100} + rowHeader="title" + searchFilters={Array []} + tableCaption="Dashboards" + tableColumns={ + Array [ + Object { + "field": "title", + "name": "Title", + "render": [Function], + "sortable": true, + }, + Object { + "field": "description", + "name": "Description", + "render": [Function], + "sortable": true, + }, + ] + } + tableListTitle="Dashboards" + toastNotifications={ + Object { + "add": [MockFunction], + "addDanger": [MockFunction], + "addError": [MockFunction], + "addInfo": [MockFunction], + "addSuccess": [MockFunction], + "addWarning": [MockFunction], + "get$": [MockFunction], + "remove": [MockFunction], + } + } + /> + +`; + exports[`after fetch renders warning when listingLimit is exceeded 1`] = ` - Create new dashboard + Create a dashboard } body={

- You can combine data views from any Kibana app into one dashboard and see everything in one place. + Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.

- Install some sample data + Add some sample data , } } diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx index 37ee0ec13d7c94..ff34a63bdce195 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.test.tsx @@ -16,6 +16,7 @@ import { KibanaContextProvider } from '../../services/kibana_react'; import { createKbnUrlStateStorage } from '../../services/kibana_utils'; import { DashboardListing, DashboardListingProps } from './dashboard_listing'; import { makeDefaultServices } from '../test_helpers'; +import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_session_storage'; function makeDefaultProps(): DashboardListingProps { return { @@ -72,6 +73,25 @@ describe('after fetch', () => { expect(component).toMatchSnapshot(); }); + test('renders call to action with continue when no dashboards exist but one is in progress', async () => { + const services = makeDefaultServices(); + services.savedDashboards.find = () => { + return Promise.resolve({ + total: 0, + hits: [], + }); + }; + services.dashboardSessionStorage.getDashboardIdsWithUnsavedChanges = () => [ + DASHBOARD_PANELS_UNSAVED_ID, + ]; + const { component } = mountWith({ services }); + // Ensure all promises resolve + await new Promise((resolve) => process.nextTick(resolve)); + // Ensure the state changes are reflected + component.update(); + expect(component).toMatchSnapshot(); + }); + test('initialFilter', async () => { const props = makeDefaultProps(); props.initialFilter = 'testFilter'; diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx index 827e5abf2bd6a6..8b99b5c51598a5 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx @@ -7,7 +7,15 @@ */ import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiLink, EuiButton, EuiEmptyPrompt, EuiBasicTableColumn } from '@elastic/eui'; +import { + EuiLink, + EuiButton, + EuiEmptyPrompt, + EuiBasicTableColumn, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, +} from '@elastic/eui'; import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { attemptLoadDashboardByTitle } from '../lib'; import { DashboardAppServices, DashboardRedirect } from '../../types'; @@ -15,6 +23,8 @@ import { getDashboardBreadcrumb, dashboardListingTable, noItemsStrings, + dashboardUnsavedListingStrings, + getNewDashboardTitle, } from '../../dashboard_strings'; import { ApplicationStart, SavedObjectsFindOptionsReference } from '../../../../../core/public'; import { syncQueryStateWithUrl } from '../../services/data'; @@ -22,8 +32,9 @@ import { IKbnUrlStateStorage } from '../../services/kibana_utils'; import { TableListView, useKibana } from '../../services/kibana_react'; import { SavedObjectsTaggingApi } from '../../services/saved_objects_tagging_oss'; import { DashboardUnsavedListing } from './dashboard_unsaved_listing'; -import { confirmCreateWithUnsaved } from './confirm_overlays'; +import { confirmCreateWithUnsaved, confirmDiscardUnsavedChanges } from './confirm_overlays'; import { getDashboardListItemLink } from './get_dashboard_list_item_link'; +import { DASHBOARD_PANELS_UNSAVED_ID } from '../lib/dashboard_session_storage'; export interface DashboardListingProps { kbnUrlStateStorage: IKbnUrlStateStorage; @@ -117,10 +128,109 @@ export const DashboardListing = ({ } }, [dashboardSessionStorage, redirectTo, core.overlays]); - const emptyPrompt = useMemo( - () => getNoItemsMessage(showWriteControls, core.application, createItem), - [createItem, core.application, showWriteControls] - ); + const emptyPrompt = useMemo(() => { + if (!showWriteControls) { + return ( + {noItemsStrings.getReadonlyTitle()}} + body={

{noItemsStrings.getReadonlyBody()}

} + /> + ); + } + + const isEditingFirstDashboard = unsavedDashboardIds.length === 1; + + const emptyAction = isEditingFirstDashboard ? ( + + + + confirmDiscardUnsavedChanges(core.overlays, () => { + dashboardSessionStorage.clearState(DASHBOARD_PANELS_UNSAVED_ID); + setUnsavedDashboardIds(dashboardSessionStorage.getDashboardIdsWithUnsavedChanges()); + }) + } + data-test-subj="discardDashboardPromptButton" + aria-label={dashboardUnsavedListingStrings.getDiscardAriaLabel(getNewDashboardTitle())} + > + {dashboardUnsavedListingStrings.getDiscardTitle()} + + + + redirectTo({ destination: 'dashboard' })} + data-test-subj="createDashboardPromptButton" + aria-label={dashboardUnsavedListingStrings.getEditAriaLabel(getNewDashboardTitle())} + > + {dashboardUnsavedListingStrings.getEditTitle()} + + + + ) : ( + + {noItemsStrings.getCreateNewDashboardText()} + + ); + + return ( + + {isEditingFirstDashboard + ? noItemsStrings.getReadEditInProgressTitle() + : noItemsStrings.getReadEditTitle()} + + } + body={ + <> +

{noItemsStrings.getReadEditDashboardDescription()}

+ {!isEditingFirstDashboard && ( +

+ + core.application.navigateToApp('home', { + path: '#/tutorial_directory/sampleData', + }) + } + > + {noItemsStrings.getSampleDataLinkText()} + + ), + }} + /> +

+ )} + + } + actions={emptyAction} + /> + ); + }, [ + redirectTo, + createItem, + core.overlays, + core.application, + showWriteControls, + unsavedDashboardIds, + dashboardSessionStorage, + ]); const fetchItems = useCallback( (filter: string) => { @@ -233,60 +343,3 @@ const getTableColumns = ( ...(savedObjectsTagging ? [savedObjectsTagging.ui.getTableColumnDefinition()] : []), ] as unknown as Array>>; }; - -const getNoItemsMessage = ( - showWriteControls: boolean, - application: ApplicationStart, - createItem: () => void -) => { - if (!showWriteControls) { - return ( - {noItemsStrings.getReadonlyTitle()}} - body={

{noItemsStrings.getReadonlyBody()}

} - /> - ); - } - - return ( - {noItemsStrings.getReadEditTitle()}} - body={ - <> -

{noItemsStrings.getReadEditDashboardDescription()}

-

- - application.navigateToApp('home', { - path: '#/tutorial_directory/sampleData', - }) - } - > - {noItemsStrings.getSampleDataLinkText()} - - ), - }} - /> -

- - } - actions={ - - {noItemsStrings.getCreateNewDashboardText()} - - } - /> - ); -}; diff --git a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx index 8a46a16c1bf0cf..effbf8ce980d73 100644 --- a/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx +++ b/src/plugins/dashboard/public/application/top_nav/editor_menu.tsx @@ -231,7 +231,7 @@ export const EditorMenu = ({ dashboardContainer, createNewVisType }: Props) => { i18n.translate('dashboard.createConfirmModal.unsavedChangesSubtitle', { - defaultMessage: 'You can continue editing or start with a blank dashboard.', + defaultMessage: 'Continue editing or start over with a blank dashboard.', }), getStartOverButtonText: () => i18n.translate('dashboard.createConfirmModal.confirmButtonLabel', { @@ -420,7 +420,7 @@ export const dashboardListingTable = { export const dashboardUnsavedListingStrings = { getUnsavedChangesTitle: (plural = false) => i18n.translate('dashboard.listing.unsaved.unsavedChangesTitle', { - defaultMessage: 'You have unsaved changes in the following {dash}.', + defaultMessage: 'You have unsaved changes in the following {dash}:', values: { dash: plural ? dashboardListingTable.getEntityNamePlural() @@ -469,17 +469,21 @@ export const noItemsStrings = { i18n.translate('dashboard.listing.createNewDashboard.title', { defaultMessage: 'Create your first dashboard', }), + getReadEditInProgressTitle: () => + i18n.translate('dashboard.listing.createNewDashboard.inProgressTitle', { + defaultMessage: 'Dashboard in progress', + }), getReadEditDashboardDescription: () => i18n.translate('dashboard.listing.createNewDashboard.combineDataViewFromKibanaAppDescription', { defaultMessage: - 'You can combine data views from any Kibana app into one dashboard and see everything in one place.', + 'Analyze all of your Elastic data in one place by creating a dashboard and adding visualizations.', }), getSampleDataLinkText: () => i18n.translate('dashboard.listing.createNewDashboard.sampleDataInstallLinkText', { - defaultMessage: `Install some sample data`, + defaultMessage: `Add some sample data`, }), getCreateNewDashboardText: () => i18n.translate('dashboard.listing.createNewDashboard.createButtonLabel', { - defaultMessage: `Create new dashboard`, + defaultMessage: `Create a dashboard`, }), }; diff --git a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.test.ts b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.test.ts index a65d4d551cf7ca..1a8b7054802585 100644 --- a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.test.ts +++ b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.test.ts @@ -5,7 +5,6 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ - import { IndexPatternsFetcher } from '.'; import { ElasticsearchClient } from 'kibana/server'; import * as indexNotFoundException from './index_not_found_exception.json'; @@ -15,36 +14,36 @@ describe('Index Pattern Fetcher - server', () => { let esClient: ElasticsearchClient; const emptyResponse = { body: { - count: 0, + indices: [], }, }; const response = { body: { - count: 1115, + indices: ['b'], + fields: [{ name: 'foo' }, { name: 'bar' }, { name: 'baz' }], }, }; const patternList = ['a', 'b', 'c']; beforeEach(() => { + jest.clearAllMocks(); esClient = { - count: jest.fn().mockResolvedValueOnce(emptyResponse).mockResolvedValue(response), + fieldCaps: jest.fn().mockResolvedValueOnce(emptyResponse).mockResolvedValue(response), } as unknown as ElasticsearchClient; indexPatterns = new IndexPatternsFetcher(esClient); }); - it('Removes pattern without matching indices', async () => { const result = await indexPatterns.validatePatternListActive(patternList); expect(result).toEqual(['b', 'c']); }); - it('Returns all patterns when all match indices', async () => { esClient = { - count: jest.fn().mockResolvedValue(response), + fieldCaps: jest.fn().mockResolvedValue(response), } as unknown as ElasticsearchClient; indexPatterns = new IndexPatternsFetcher(esClient); const result = await indexPatterns.validatePatternListActive(patternList); expect(result).toEqual(patternList); }); - it('Removes pattern when "index_not_found_exception" error is thrown', async () => { + it('Removes pattern when error is thrown', async () => { class ServerError extends Error { public body?: Record; constructor( @@ -56,9 +55,8 @@ describe('Index Pattern Fetcher - server', () => { this.body = errBody; } } - esClient = { - count: jest + fieldCaps: jest .fn() .mockResolvedValueOnce(response) .mockRejectedValue( @@ -69,4 +67,22 @@ describe('Index Pattern Fetcher - server', () => { const result = await indexPatterns.validatePatternListActive(patternList); expect(result).toEqual([patternList[0]]); }); + it('When allowNoIndices is false, run validatePatternListActive', async () => { + const fieldCapsMock = jest.fn(); + esClient = { + fieldCaps: fieldCapsMock.mockResolvedValue(response), + } as unknown as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient); + await indexPatterns.getFieldsForWildcard({ pattern: patternList }); + expect(fieldCapsMock.mock.calls).toHaveLength(4); + }); + it('When allowNoIndices is true, do not run validatePatternListActive', async () => { + const fieldCapsMock = jest.fn(); + esClient = { + fieldCaps: fieldCapsMock.mockResolvedValue(response), + } as unknown as ElasticsearchClient; + indexPatterns = new IndexPatternsFetcher(esClient, true); + await indexPatterns.getFieldsForWildcard({ pattern: patternList }); + expect(fieldCapsMock.mock.calls).toHaveLength(1); + }); }); diff --git a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts index 7dae85c920ebff..c054d547e956ff 100644 --- a/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts +++ b/src/plugins/data_views/server/fetcher/index_patterns_fetcher.ts @@ -36,12 +36,10 @@ interface FieldSubType { export class IndexPatternsFetcher { private elasticsearchClient: ElasticsearchClient; private allowNoIndices: boolean; - constructor(elasticsearchClient: ElasticsearchClient, allowNoIndices: boolean = false) { this.elasticsearchClient = elasticsearchClient; this.allowNoIndices = allowNoIndices; } - /** * Get a list of field objects for an index pattern that may contain wildcards * @@ -60,23 +58,22 @@ export class IndexPatternsFetcher { }): Promise { const { pattern, metaFields, fieldCapsOptions, type, rollupIndex } = options; const patternList = Array.isArray(pattern) ? pattern : pattern.split(','); + const allowNoIndices = fieldCapsOptions + ? fieldCapsOptions.allow_no_indices + : this.allowNoIndices; let patternListActive: string[] = patternList; // if only one pattern, don't bother with validation. We let getFieldCapabilities fail if the single pattern is bad regardless - if (patternList.length > 1) { + if (patternList.length > 1 && !allowNoIndices) { patternListActive = await this.validatePatternListActive(patternList); } const fieldCapsResponse = await getFieldCapabilities( this.elasticsearchClient, - // if none of the patterns are active, pass the original list to get an error - patternListActive.length > 0 ? patternListActive : patternList, + patternListActive, metaFields, { - allow_no_indices: fieldCapsOptions - ? fieldCapsOptions.allow_no_indices - : this.allowNoIndices, + allow_no_indices: allowNoIndices, } ); - if (type === 'rollup' && rollupIndex) { const rollupFields: FieldDescriptor[] = []; const rollupIndexCapabilities = getCapabilitiesForRollupIndices( @@ -87,13 +84,11 @@ export class IndexPatternsFetcher { ).body )[rollupIndex].aggs; const fieldCapsResponseObj = keyBy(fieldCapsResponse, 'name'); - // Keep meta fields metaFields!.forEach( (field: string) => fieldCapsResponseObj[field] && rollupFields.push(fieldCapsResponseObj[field]) ); - return mergeCapabilitiesWithFields( rollupIndexCapabilities, fieldCapsResponseObj, @@ -137,23 +132,20 @@ export class IndexPatternsFetcher { async validatePatternListActive(patternList: string[]) { const result = await Promise.all( patternList - .map((pattern) => - this.elasticsearchClient.count({ - index: pattern, - }) - ) - .map((p) => - p.catch((e) => { - if (e.body.error.type === 'index_not_found_exception') { - return { body: { count: 0 } }; - } - throw e; - }) - ) + .map(async (index) => { + const searchResponse = await this.elasticsearchClient.fieldCaps({ + index, + fields: '_id', + ignore_unavailable: true, + allow_no_indices: false, + }); + return searchResponse.body.indices.length > 0; + }) + .map((p) => p.catch(() => false)) ); return result.reduce( - (acc: string[], { body: { count } }, patternListIndex) => - count > 0 ? [...acc, patternList[patternListIndex]] : acc, + (acc: string[], isValid, patternListIndex) => + isValid ? [...acc, patternList[patternListIndex]] : acc, [] ); } diff --git a/src/plugins/discover/common/index.ts b/src/plugins/discover/common/index.ts index 32704d95423f71..6262855409b294 100644 --- a/src/plugins/discover/common/index.ts +++ b/src/plugins/discover/common/index.ts @@ -21,4 +21,5 @@ export const SEARCH_FIELDS_FROM_SOURCE = 'discover:searchFieldsFromSource'; export const MAX_DOC_FIELDS_DISPLAYED = 'discover:maxDocFieldsDisplayed'; export const SHOW_FIELD_STATISTICS = 'discover:showFieldStatistics'; export const SHOW_MULTIFIELDS = 'discover:showMultiFields'; +export const TRUNCATE_MAX_HEIGHT = 'truncate:maxHeight'; export const SEARCH_EMBEDDABLE_TYPE = 'search'; diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/_doc_table.scss b/src/plugins/discover/public/application/apps/main/components/doc_table/_doc_table.scss index d19a1fd0420691..164b61d42df191 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/_doc_table.scss +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/_doc_table.scss @@ -103,10 +103,6 @@ text-align: center; } -.truncate-by-height { - overflow: hidden; -} - .table { // Nesting .table { diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx index 0bf4a36555d169..515782ce23f450 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/components/table_row.tsx @@ -88,7 +88,7 @@ export const TableRow = ({ return ( // formatFieldValue always returns sanitized HTML // eslint-disable-next-line react/no-danger -
+
); }; const inlineFilter = useCallback( diff --git a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx index a73bc3f175be12..d5660a091f0aa0 100644 --- a/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx +++ b/src/plugins/discover/public/application/apps/main/components/doc_table/lib/row_formatter.tsx @@ -20,7 +20,7 @@ interface Props { } const TemplateComponent = ({ defPairs }: Props) => { return ( -
+
{defPairs.map((pair, idx) => (
{pair[0]}:
diff --git a/src/plugins/discover/public/application/components/table/table.test.tsx b/src/plugins/discover/public/application/components/table/table.test.tsx index e61333cce11660..176a1961378aa6 100644 --- a/src/plugins/discover/public/application/components/table/table.test.tsx +++ b/src/plugins/discover/public/application/components/table/table.test.tsx @@ -236,7 +236,7 @@ describe('DocViewTable at Discover Context', () => { const btn = findTestSubject(component, `collapseBtn`); const html = component.html(); - expect(component.html()).toContain('truncate-by-height'); + expect(component.html()).toContain('dscTruncateByHeight'); expect(btn.length).toBe(1); btn.simulate('click'); diff --git a/src/plugins/discover/public/application/components/table/table_cell_value.tsx b/src/plugins/discover/public/application/components/table/table_cell_value.tsx index e006de1cd7aeb1..ebb4ea243fb254 100644 --- a/src/plugins/discover/public/application/components/table/table_cell_value.tsx +++ b/src/plugins/discover/public/application/components/table/table_cell_value.tsx @@ -104,7 +104,7 @@ export const TableFieldValue = ({ const valueClassName = classNames({ // eslint-disable-next-line @typescript-eslint/naming-convention kbnDocViewer__value: true, - 'truncate-by-height': isCollapsible && isCollapsed, + dscTruncateByHeight: isCollapsible && isCollapsed, }); const onToggleCollapse = () => setFieldOpen((fieldOpenPrev) => !fieldOpenPrev); diff --git a/src/plugins/discover/public/application/discover_router.tsx b/src/plugins/discover/public/application/discover_router.tsx index 320ce3e5f644a0..b3fe36358bbd49 100644 --- a/src/plugins/discover/public/application/discover_router.tsx +++ b/src/plugins/discover/public/application/discover_router.tsx @@ -22,6 +22,7 @@ export const discoverRouter = (services: DiscoverServices, history: History) => services, history, }; + return ( diff --git a/src/plugins/discover/public/application/helpers/truncate_styles.ts b/src/plugins/discover/public/application/helpers/truncate_styles.ts new file mode 100644 index 00000000000000..dbe8b770e1793f --- /dev/null +++ b/src/plugins/discover/public/application/helpers/truncate_styles.ts @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 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 createCache from '@emotion/cache'; +import { cache } from '@emotion/css'; +import { serializeStyles } from '@emotion/serialize'; + +/** + * The following emotion cache management was introduced here + * https://ntsim.uk/posts/how-to-update-or-remove-global-styles-in-emotion/ + */ +const TRUNCATE_GRADIENT_HEIGHT = 15; +const globalThemeCache = createCache({ key: 'truncation' }); + +const buildStylesheet = (maxHeight: number) => { + return [ + ` + .dscTruncateByHeight { + overflow: hidden; + max-height: ${maxHeight}px !important; + display: inline-block; + } + .dscTruncateByHeight:before { + top: ${maxHeight - TRUNCATE_GRADIENT_HEIGHT}px; + } + `, + ]; +}; + +const flushThemedGlobals = () => { + globalThemeCache.sheet.flush(); + globalThemeCache.inserted = {}; + globalThemeCache.registered = {}; +}; + +export const injectTruncateStyles = (maxHeight: number) => { + if (maxHeight <= 0) { + flushThemedGlobals(); + return; + } + + const serialized = serializeStyles(buildStylesheet(maxHeight), cache.registered); + if (!globalThemeCache.inserted[serialized.name]) { + globalThemeCache.insert('', serialized, globalThemeCache.sheet, true); + } +}; diff --git a/src/plugins/discover/public/plugin.tsx b/src/plugins/discover/public/plugin.tsx index c91bcf3897e145..62a5a7972a2782 100644 --- a/src/plugins/discover/public/plugin.tsx +++ b/src/plugins/discover/public/plugin.tsx @@ -62,6 +62,8 @@ import { DeferredSpinner } from './shared'; import { ViewSavedSearchAction } from './application/embeddable/view_saved_search_action'; import type { SpacesPluginStart } from '../../../../x-pack/plugins/spaces/public'; import { FieldFormatsStart } from '../../field_formats/public'; +import { injectTruncateStyles } from './application/helpers/truncate_styles'; +import { TRUNCATE_MAX_HEIGHT } from '../common'; declare module '../../share/public' { export interface UrlGeneratorStateMapping { @@ -413,6 +415,8 @@ export class DiscoverPlugin const services = buildServices(core, plugins, this.initializerContext); setServices(services); + injectTruncateStyles(services.uiSettings.get(TRUNCATE_MAX_HEIGHT)); + return { urlGenerator: this.urlGenerator, locator: this.locator, diff --git a/src/plugins/discover/server/ui_settings.ts b/src/plugins/discover/server/ui_settings.ts index 529ba0d1beef12..df06260d45d212 100644 --- a/src/plugins/discover/server/ui_settings.ts +++ b/src/plugins/discover/server/ui_settings.ts @@ -26,6 +26,7 @@ import { SEARCH_FIELDS_FROM_SOURCE, MAX_DOC_FIELDS_DISPLAYED, SHOW_MULTIFIELDS, + TRUNCATE_MAX_HEIGHT, SHOW_FIELD_STATISTICS, } from '../common'; @@ -241,4 +242,16 @@ export const getUiSettings: () => Record = () => ({ category: ['discover'], schema: schema.boolean(), }, + [TRUNCATE_MAX_HEIGHT]: { + name: i18n.translate('discover.advancedSettings.params.maxCellHeightTitle', { + defaultMessage: 'Maximum table cell height', + }), + value: 115, + category: ['discover'], + description: i18n.translate('discover.advancedSettings.params.maxCellHeightText', { + defaultMessage: + 'The maximum height that a cell in a table should occupy. Set to 0 to disable truncation', + }), + schema: schema.number({ min: 0 }), + }, }); diff --git a/src/plugins/field_formats/common/index.ts b/src/plugins/field_formats/common/index.ts index 5863bf79adcbaf..092fc49af3d283 100644 --- a/src/plugins/field_formats/common/index.ts +++ b/src/plugins/field_formats/common/index.ts @@ -25,7 +25,6 @@ export { NumberFormat, PercentFormat, RelativeDateFormat, - SourceFormat, StaticLookupFormat, UrlFormat, StringFormat, diff --git a/src/plugins/index_pattern_field_editor/public/open_editor.tsx b/src/plugins/index_pattern_field_editor/public/open_editor.tsx index 19b5d1fde83151..0109b8d95db522 100644 --- a/src/plugins/index_pattern_field_editor/public/open_editor.tsx +++ b/src/plugins/index_pattern_field_editor/public/open_editor.tsx @@ -150,6 +150,9 @@ export const getFieldEditorOpener = flyout.close(); } }, + maskProps: { + className: 'indexPatternFieldEditorMaskOverlay', + }, } ); diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index ac78e8cac4f074..a154770bbfffda 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -6,16 +6,14 @@ * Side Public License, v 1. */ -import { CoreStart, CoreSetup } from 'kibana/public'; -import { injectHeaderStyle } from './utils/inject_header_style'; +import type { CoreSetup } from 'kibana/public'; export class KibanaLegacyPlugin { public setup(core: CoreSetup<{}, KibanaLegacyStart>) { return {}; } - public start({ uiSettings }: CoreStart) { - injectHeaderStyle(uiSettings); + public start() { return { /** * Loads the font-awesome icon font. Should be removed once the last consumer has migrated to EUI diff --git a/src/plugins/kibana_legacy/public/utils/inject_header_style.ts b/src/plugins/kibana_legacy/public/utils/inject_header_style.ts deleted file mode 100644 index 967aa2232838ee..00000000000000 --- a/src/plugins/kibana_legacy/public/utils/inject_header_style.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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 { IUiSettingsClient } from 'kibana/public'; - -export function buildCSS(maxHeight = 0, truncateGradientHeight = 15) { - return ` -.truncate-by-height { - max-height: ${maxHeight > 0 ? `${maxHeight}px !important` : 'none'}; - display: inline-block; -} -.truncate-by-height:before { - top: ${maxHeight > 0 ? maxHeight - truncateGradientHeight : truncateGradientHeight * -1}px; -} -`; -} - -export function injectHeaderStyle(uiSettings: IUiSettingsClient) { - const style = document.createElement('style'); - style.setAttribute('id', 'style-compile'); - document.getElementsByTagName('head')[0].appendChild(style); - - uiSettings.get$('truncate:maxHeight').subscribe((value: number) => { - // eslint-disable-next-line no-unsanitized/property - style.innerHTML = buildCSS(value); - }); -} diff --git a/src/plugins/kibana_overview/public/components/overview/overview.tsx b/src/plugins/kibana_overview/public/components/overview/overview.tsx index 6a0279bd12465e..5108150f7ff8d1 100644 --- a/src/plugins/kibana_overview/public/components/overview/overview.tsx +++ b/src/plugins/kibana_overview/public/components/overview/overview.tsx @@ -84,6 +84,9 @@ export const Overview: FC = ({ newsFetchResult, solutions, features }) => solution: i18n.translate('kibanaOverview.noDataConfig.solutionName', { defaultMessage: `Analytics`, }), + pageTitle: i18n.translate('kibanaOverview.noDataConfig.pageTitle', { + defaultMessage: `Welcome to Analytics!`, + }), logo: 'logoKibana', actions: { elasticAgent: { diff --git a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts index dafd4414db192b..d4f069560443b3 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/event_loop_delays/rollups/integration_tests/daily_rollups.test.ts @@ -56,7 +56,8 @@ const outdatedRawEventLoopDelaysDaily = [ createRawObject(moment().subtract(7, 'days')), ]; -describe('daily rollups integration test', () => { +// FLAKY https://github.com/elastic/kibana/issues/111821 +describe.skip('daily rollups integration test', () => { let esServer: TestElasticsearchUtils; let root: TestKibanaUtils['root']; let internalRepository: ISavedObjectsRepository; diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index d8d0215fd751f2..356aaf60b423c8 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -420,6 +420,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'integer', _meta: { description: 'Non-default value of setting.' }, }, + 'observability:enableComparisonByDefault': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'banners:placement': { type: 'keyword', _meta: { description: 'Non-default value of setting.' }, @@ -440,6 +444,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'labs:canvas:byValueEmbeddable': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'labs:canvas:useDataService': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 9dcd2038edb9d7..69287d37dfa284 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -37,6 +37,7 @@ export interface UsageStats { 'securitySolution:rulesTableRefresh': string; 'observability:enableInspectEsQueries': boolean; 'observability:maxSuggestions': number; + 'observability:enableComparisonByDefault': boolean; 'visualize:enableLabs': boolean; 'visualization:heatmap:maxBuckets': number; 'visualization:colorMapping': string; @@ -121,6 +122,7 @@ export interface UsageStats { 'banners:textColor': string; 'banners:backgroundColor': string; 'labs:canvas:enable_ui': boolean; + 'labs:canvas:byValueEmbeddable': boolean; 'labs:canvas:useDataService': boolean; 'labs:presentation:timeToPresent': boolean; 'labs:dashboard:enable_ui': boolean; diff --git a/src/plugins/presentation_util/common/labs.ts b/src/plugins/presentation_util/common/labs.ts index cb976e73b5edfe..8eefbd6981280c 100644 --- a/src/plugins/presentation_util/common/labs.ts +++ b/src/plugins/presentation_util/common/labs.ts @@ -11,7 +11,9 @@ import { i18n } from '@kbn/i18n'; export const LABS_PROJECT_PREFIX = 'labs:'; export const DEFER_BELOW_FOLD = `${LABS_PROJECT_PREFIX}dashboard:deferBelowFold` as const; export const DASHBOARD_CONTROLS = `${LABS_PROJECT_PREFIX}dashboard:dashboardControls` as const; -export const projectIDs = [DEFER_BELOW_FOLD, DASHBOARD_CONTROLS] as const; +export const BY_VALUE_EMBEDDABLE = `${LABS_PROJECT_PREFIX}canvas:byValueEmbeddable` as const; + +export const projectIDs = [DEFER_BELOW_FOLD, DASHBOARD_CONTROLS, BY_VALUE_EMBEDDABLE] as const; export const environmentNames = ['kibana', 'browser', 'session'] as const; export const solutionNames = ['canvas', 'dashboard', 'presentation'] as const; @@ -48,6 +50,19 @@ export const projects: { [ID in ProjectID]: ProjectConfig & { id: ID } } = { }), solutions: ['dashboard'], }, + [BY_VALUE_EMBEDDABLE]: { + id: BY_VALUE_EMBEDDABLE, + isActive: true, + isDisplayed: true, + environments: ['kibana', 'browser', 'session'], + name: i18n.translate('presentationUtil.labs.enableByValueEmbeddableName', { + defaultMessage: 'By-Value Embeddables', + }), + description: i18n.translate('presentationUtil.labs.enableByValueEmbeddableDescription', { + defaultMessage: 'Enables support for by-value embeddables in Canvas', + }), + solutions: ['canvas'], + }, }; export type ProjectID = typeof projectIDs[number]; diff --git a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss index 434f06b69a6847..9f70ae353405b1 100644 --- a/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss +++ b/src/plugins/presentation_util/public/components/solution_toolbar/items/quick_group.scss @@ -1,9 +1,25 @@ .quickButtonGroup { - .quickButtonGroup__button { - background-color: $euiColorEmptyShade; - // sass-lint:disable-block no-important - border-width: $euiBorderWidthThin !important; - border-style: solid !important; - border-color: $euiBorderColor !important; + .euiButtonGroup__buttons { + border-radius: $euiBorderRadius; + + .quickButtonGroup__button { + background-color: $euiColorEmptyShade; + // sass-lint:disable-block no-important + border-width: $euiBorderWidthThin !important; + border-style: solid !important; + border-color: $euiBorderColor !important; + } + + .quickButtonGroup__button:first-of-type { + // sass-lint:disable-block no-important + border-top-left-radius: $euiBorderRadius !important; + border-bottom-left-radius: $euiBorderRadius !important; + } + + .quickButtonGroup__button:last-of-type { + // sass-lint:disable-block no-important + border-top-right-radius: $euiBorderRadius !important; + border-bottom-right-radius: $euiBorderRadius !important; + } } } diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 251fed955788eb..21273482dd2b88 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -7647,6 +7647,12 @@ "description": "Non-default value of setting." } }, + "observability:enableComparisonByDefault": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "banners:placement": { "type": "keyword", "_meta": { @@ -7677,6 +7683,12 @@ "description": "Non-default value of setting." } }, + "labs:canvas:byValueEmbeddable": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "labs:canvas:useDataService": { "type": "boolean", "_meta": { @@ -9158,4 +9170,4 @@ } } } -} +} \ No newline at end of file diff --git a/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts b/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts index e51b47bc4c7fa7..b01a04c1623755 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts +++ b/src/plugins/vis_types/xy/public/editor/components/options/point_series/point_series.mocks.ts @@ -389,7 +389,7 @@ export const getVis = (bucketType: string) => { labels: { show: true, rotate: 0, - filter: false, + filter: true, truncate: 100, }, title: { @@ -822,7 +822,7 @@ export const getStateParams = (type: string, thresholdPanelOn: boolean) => { labels: { show: true, rotate: 0, - filter: false, + filter: true, truncate: 100, }, title: { diff --git a/src/plugins/vis_types/xy/public/mocks.ts b/src/plugins/vis_types/xy/public/mocks.ts index bb740354857234..6c0de8de1ac368 100644 --- a/src/plugins/vis_types/xy/public/mocks.ts +++ b/src/plugins/vis_types/xy/public/mocks.ts @@ -118,7 +118,7 @@ export const visParamsWithTwoYAxes = { }, labels: { type: 'label', - filter: false, + filter: true, rotate: 0, show: true, truncate: 100, @@ -138,7 +138,7 @@ export const visParamsWithTwoYAxes = { mode: 'normal', }, labels: { - filter: false, + filter: true, rotate: 0, show: true, truncate: 100, diff --git a/src/plugins/vis_types/xy/public/vis_types/area.ts b/src/plugins/vis_types/xy/public/vis_types/area.ts index 3ff840f1817e92..a140164cf2eb8a 100644 --- a/src/plugins/vis_types/xy/public/vis_types/area.ts +++ b/src/plugins/vis_types/xy/public/vis_types/area.ts @@ -74,7 +74,7 @@ export const areaVisTypeDefinition = { labels: { show: true, rotate: LabelRotation.Horizontal, - filter: false, + filter: true, truncate: 100, }, title: { diff --git a/src/plugins/vis_types/xy/public/vis_types/histogram.ts b/src/plugins/vis_types/xy/public/vis_types/histogram.ts index dd65d6f31cb801..c9d17c8ed4501f 100644 --- a/src/plugins/vis_types/xy/public/vis_types/histogram.ts +++ b/src/plugins/vis_types/xy/public/vis_types/histogram.ts @@ -76,7 +76,7 @@ export const histogramVisTypeDefinition = { labels: { show: true, rotate: LabelRotation.Horizontal, - filter: false, + filter: true, truncate: 100, }, title: { diff --git a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts index c8494024d1d0af..f6d2a6e0e429ac 100644 --- a/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts +++ b/src/plugins/vis_types/xy/public/vis_types/horizontal_bar.ts @@ -56,7 +56,7 @@ export const horizontalBarVisTypeDefinition = { labels: { show: true, rotate: LabelRotation.Horizontal, - filter: false, + filter: true, truncate: 200, }, title: {}, diff --git a/src/plugins/vis_types/xy/public/vis_types/line.ts b/src/plugins/vis_types/xy/public/vis_types/line.ts index 08e17f7e97d463..3b6c9dc1a2084c 100644 --- a/src/plugins/vis_types/xy/public/vis_types/line.ts +++ b/src/plugins/vis_types/xy/public/vis_types/line.ts @@ -74,7 +74,7 @@ export const lineVisTypeDefinition = { labels: { show: true, rotate: LabelRotation.Horizontal, - filter: false, + filter: true, truncate: 100, }, title: { diff --git a/test/api_integration/apis/console/proxy_route.ts b/test/api_integration/apis/console/proxy_route.ts index d8a5f57a41a6e3..a208ef405306ff 100644 --- a/test/api_integration/apis/console/proxy_route.ts +++ b/test/api_integration/apis/console/proxy_route.ts @@ -12,7 +12,8 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); - describe('POST /api/console/proxy', () => { + // Failing: See https://github.com/elastic/kibana/issues/117674 + describe.skip('POST /api/console/proxy', () => { describe('system indices behavior', () => { it('returns warning header when making requests to .kibana index', async () => { return await supertest diff --git a/test/api_integration/apis/custom_integration/integrations.ts b/test/api_integration/apis/custom_integration/integrations.ts index 0784a86e4b546c..036eb2ef33c78f 100644 --- a/test/api_integration/apis/custom_integration/integrations.ts +++ b/test/api_integration/apis/custom_integration/integrations.ts @@ -22,7 +22,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.be.an('array'); - expect(resp.body.length).to.be(33); + expect(resp.body.length).to.be(34); // Test for sample data card expect(resp.body.findIndex((c: { id: string }) => c.id === 'sample_data_all')).to.be.above( @@ -40,7 +40,7 @@ export default function ({ getService }: FtrProviderContext) { expect(resp.body).to.be.an('array'); - expect(resp.body.length).to.be.above(109); // at least the beats + apm + expect(resp.body.length).to.be(109); // the beats }); }); }); diff --git a/test/functional/page_objects/common_page.ts b/test/functional/page_objects/common_page.ts index 69fbf7e49df3c5..3955e457b5ffce 100644 --- a/test/functional/page_objects/common_page.ts +++ b/test/functional/page_objects/common_page.ts @@ -279,6 +279,9 @@ export class CommonPageObject extends FtrService { this.log.debug(msg); throw new Error(msg); } + if (appName === 'discover') { + await this.browser.setLocalStorageItem('data.autocompleteFtuePopover', 'true'); + } return currentUrl; }); diff --git a/test/functional/page_objects/dashboard_page.ts b/test/functional/page_objects/dashboard_page.ts index 3d2ba53e7ba985..77ea098c76878b 100644 --- a/test/functional/page_objects/dashboard_page.ts +++ b/test/functional/page_objects/dashboard_page.ts @@ -292,6 +292,15 @@ export class DashboardPageObject extends FtrService { } public async clickNewDashboard(continueEditing = false) { + const discardButtonExists = await this.testSubjects.exists('discardDashboardPromptButton'); + if (!continueEditing && discardButtonExists) { + this.log.debug('found discard button'); + await this.testSubjects.click('discardDashboardPromptButton'); + const confirmation = await this.testSubjects.exists('confirmModalTitleText'); + if (confirmation) { + await this.common.clickConfirmOnModal(); + } + } await this.listingTable.clickNewButton('createDashboardPromptButton'); if (await this.testSubjects.exists('dashboardCreateConfirm')) { if (continueEditing) { @@ -305,6 +314,15 @@ export class DashboardPageObject extends FtrService { } public async clickNewDashboardExpectWarning(continueEditing = false) { + const discardButtonExists = await this.testSubjects.exists('discardDashboardPromptButton'); + if (!continueEditing && discardButtonExists) { + this.log.debug('found discard button'); + await this.testSubjects.click('discardDashboardPromptButton'); + const confirmation = await this.testSubjects.exists('confirmModalTitleText'); + if (confirmation) { + await this.common.clickConfirmOnModal(); + } + } await this.listingTable.clickNewButton('createDashboardPromptButton'); await this.testSubjects.existOrFail('dashboardCreateConfirm'); if (continueEditing) { diff --git a/test/functional/page_objects/discover_page.ts b/test/functional/page_objects/discover_page.ts index fa7aee4e3c54c4..f9328e89cd19eb 100644 --- a/test/functional/page_objects/discover_page.ts +++ b/test/functional/page_objects/discover_page.ts @@ -48,7 +48,7 @@ export class DiscoverPageObject extends FtrService { await fieldSearch.clearValue(); } - public async saveSearch(searchName: string) { + public async saveSearch(searchName: string, saveAsNew?: boolean) { await this.clickSaveSearchButton(); // preventing an occasional flakiness when the saved object wasn't set and the form can't be submitted await this.retry.waitFor( @@ -59,6 +59,14 @@ export class DiscoverPageObject extends FtrService { return (await saveButton.getAttribute('disabled')) !== 'true'; } ); + + if (saveAsNew !== undefined) { + await this.retry.waitFor(`save as new switch is set`, async () => { + await this.testSubjects.setEuiSwitch('saveAsNewCheckbox', saveAsNew ? 'check' : 'uncheck'); + return (await this.testSubjects.isEuiSwitchChecked('saveAsNewCheckbox')) === saveAsNew; + }); + } + await this.testSubjects.click('confirmSaveSavedObjectButton'); await this.header.waitUntilLoadingHasFinished(); // LeeDr - this additional checking for the saved search name was an attempt diff --git a/typings/@elastic/eui/index.d.ts b/typings/@elastic/eui/index.d.ts index f5baf73df70571..814a386a3b5e70 100644 --- a/typings/@elastic/eui/index.d.ts +++ b/typings/@elastic/eui/index.d.ts @@ -11,7 +11,3 @@ declare module '@elastic/eui/lib/services' { export const RIGHT_ALIGNMENT: any; } - -declare module '@elastic/eui/lib/services/format' { - export const dateFormatAliases: any; -} diff --git a/typings/accept.d.ts b/typings/accept.d.ts deleted file mode 100644 index e868063c7f7c0a..00000000000000 --- a/typings/accept.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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. - */ - -declare module 'accept' { - // @types/accept does not include the `preferences` argument so we override the type to include it - export function encodings(encodingHeader?: string, preferences?: string[]): string[]; -} diff --git a/typings/global_fetch.d.ts b/typings/global_fetch.d.ts deleted file mode 100644 index 597bc7e89497c5..00000000000000 --- a/typings/global_fetch.d.ts +++ /dev/null @@ -1,11 +0,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. - */ - -// This type needs to still exist due to apollo-link-http-common hasn't yet updated -// it's usage (https://github.com/apollographql/apollo-link/issues/1131) -declare type GlobalFetch = WindowOrWorkerGlobalScope; diff --git a/typings/js_levenshtein.d.ts b/typings/js_levenshtein.d.ts deleted file mode 100644 index 7c934333dbc7b6..00000000000000 --- a/typings/js_levenshtein.d.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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. - */ - -declare module 'js-levenshtein' { - const levenshtein: (a: string, b: string) => number; - export = levenshtein; -} diff --git a/typings/react_vis.d.ts b/typings/react_vis.d.ts deleted file mode 100644 index 209dd398e86f44..00000000000000 --- a/typings/react_vis.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 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. - */ - -declare module 'react-vis'; diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index 9988e951ae86db..9f48a45fc4664e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -24,7 +24,6 @@ import { import { getActionType as getJiraActionType } from './jira'; import { getActionType as getResilientActionType } from './resilient'; import { getActionType as getTeamsActionType } from './teams'; -import { ENABLE_ITOM } from '../constants/connectors'; export type { ActionParamsType as EmailActionParams } from './email'; export { ActionTypeId as EmailActionTypeId } from './email'; export type { ActionParamsType as IndexActionParams } from './es_index'; @@ -72,12 +71,8 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServiceNowITSMActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServiceNowSIRActionType({ logger, configurationUtilities })); + actionTypeRegistry.register(getServiceNowITOMActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getJiraActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getResilientActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getTeamsActionType({ logger, configurationUtilities })); - - // TODO: Remove when ITOM is ready - if (ENABLE_ITOM) { - actionTypeRegistry.register(getServiceNowITOMActionType({ logger, configurationUtilities })); - } } diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.test.ts index 41f723bc9e2aa2..26550f17326553 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.test.ts @@ -44,7 +44,7 @@ describe('config', () => { importSetTable: 'x_elas2_inc_int_elastic_incident', appScope: 'x_elas2_inc_int', table: 'em_event', - useImportAPI: true, + useImportAPI: false, commentFieldKey: 'work_notes', }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts index 52d2eb7662f53f..ba29bcc39b25a8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/config.ts @@ -5,11 +5,6 @@ * 2.0. */ -import { - ENABLE_ITOM, - ENABLE_NEW_SN_ITSM_CONNECTOR, - ENABLE_NEW_SN_SIR_CONNECTOR, -} from '../../constants/connectors'; import { SNProductsConfig } from './types'; export const serviceNowITSMTable = 'incident'; @@ -24,21 +19,21 @@ export const snExternalServiceConfig: SNProductsConfig = { importSetTable: 'x_elas2_inc_int_elastic_incident', appScope: 'x_elas2_inc_int', table: 'incident', - useImportAPI: ENABLE_NEW_SN_ITSM_CONNECTOR, + useImportAPI: true, commentFieldKey: 'work_notes', }, '.servicenow-sir': { importSetTable: 'x_elas2_sir_int_elastic_si_incident', appScope: 'x_elas2_sir_int', table: 'sn_si_incident', - useImportAPI: ENABLE_NEW_SN_SIR_CONNECTOR, + useImportAPI: true, commentFieldKey: 'work_notes', }, '.servicenow-itom': { importSetTable: 'x_elas2_inc_int_elastic_incident', appScope: 'x_elas2_inc_int', table: 'em_event', - useImportAPI: ENABLE_ITOM, + useImportAPI: false, commentFieldKey: 'work_notes', }, }; diff --git a/x-pack/plugins/actions/server/constants/connectors.ts b/x-pack/plugins/actions/server/constants/connectors.ts deleted file mode 100644 index 94324e4d82bc24..00000000000000 --- a/x-pack/plugins/actions/server/constants/connectors.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// TODO: Remove when Elastic for ITSM is published. -export const ENABLE_NEW_SN_ITSM_CONNECTOR = true; - -// TODO: Remove when Elastic for Security Operations is published. -export const ENABLE_NEW_SN_SIR_CONNECTOR = true; - -// TODO: Remove when ready -export const ENABLE_ITOM = true; diff --git a/x-pack/plugins/apm/dev_docs/testing.md b/x-pack/plugins/apm/dev_docs/testing.md index 95ba2467befcdd..2a7533402ecca3 100644 --- a/x-pack/plugins/apm/dev_docs/testing.md +++ b/x-pack/plugins/apm/dev_docs/testing.md @@ -27,7 +27,7 @@ API tests are separated in two suites: node scripts/test/api [--trial] [--help] ``` -The API tests are located in `x-pack/test/apm_api_integration/`. +The API tests are located in [`x-pack/test/apm_api_integration/`](/x-pack/test/apm_api_integration/). **API Test tips** @@ -43,11 +43,12 @@ The API tests are located in `x-pack/test/apm_api_integration/`. node scripts/test/e2e [--trial] [--help] ``` -The E2E tests are located [here](../ftr_e2e) +The E2E tests are located in [`x-pack/plugins/apm/ftr_e2e`](../ftr_e2e) --- ## Functional tests (Security and Correlations tests) + TODO: We could try moving this tests to the new e2e tests located at `x-pack/plugins/apm/ftr_e2e`. **Start server** @@ -66,10 +67,10 @@ APM tests are located in `x-pack/test/functional/apps/apm`. For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) diff --git a/x-pack/plugins/apm/scripts/test/README.md b/x-pack/plugins/apm/scripts/test/README.md - ## Storybook ### Start + ``` yarn storybook apm ``` @@ -77,6 +78,7 @@ yarn storybook apm All files with a .stories.tsx extension will be loaded. You can access the development environment at http://localhost:9001. ## Data generation + For end-to-end (e.g. agent -> apm server -> elasticsearch <- kibana) development and testing of Elastic APM please check the the [APM Integration Testing repository](https://github.com/elastic/apm-integration-testing). -Data can also be generated using the [elastic-apm-synthtrace](../../../../packages/elastic-apm-synthtrace/README.md) CLI. \ No newline at end of file +Data can also be generated using the [elastic-apm-synthtrace](../../../../packages/elastic-apm-synthtrace/README.md) CLI. diff --git a/x-pack/plugins/apm/ftr_e2e/cypress_open.ts b/x-pack/plugins/apm/ftr_e2e/cypress_open.ts deleted file mode 100644 index 3f7758b40b90df..00000000000000 --- a/x-pack/plugins/apm/ftr_e2e/cypress_open.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrConfigProviderContext } from '@kbn/test'; -import { cypressOpenTests } from './cypress_start'; - -async function openE2ETests({ readConfigFile }: FtrConfigProviderContext) { - const kibanaConfig = await readConfigFile(require.resolve('./config.ts')); - return { - ...kibanaConfig.getAll(), - testRunner: cypressOpenTests, - }; -} - -// eslint-disable-next-line import/no-default-export -export default openE2ETests; diff --git a/x-pack/plugins/apm/ftr_e2e/cypress_run.ts b/x-pack/plugins/apm/ftr_e2e/cypress_run.ts deleted file mode 100644 index 16f93b39910f3f..00000000000000 --- a/x-pack/plugins/apm/ftr_e2e/cypress_run.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { argv } from 'yargs'; -import { FtrConfigProviderContext } from '@kbn/test'; -import { cypressRunTests } from './cypress_start'; - -const specArg = argv.spec as string | undefined; - -async function runE2ETests({ readConfigFile }: FtrConfigProviderContext) { - const kibanaConfig = await readConfigFile(require.resolve('./config.ts')); - return { - ...kibanaConfig.getAll(), - testRunner: cypressRunTests(specArg), - }; -} - -// eslint-disable-next-line import/no-default-export -export default runE2ETests; diff --git a/x-pack/plugins/apm/ftr_e2e/cypress_start.ts b/x-pack/plugins/apm/ftr_e2e/cypress_start.ts index 2d0be8c0070897..c8ab216cbce5cc 100644 --- a/x-pack/plugins/apm/ftr_e2e/cypress_start.ts +++ b/x-pack/plugins/apm/ftr_e2e/cypress_start.ts @@ -7,30 +7,16 @@ /* eslint-disable no-console */ +import { argv } from 'yargs'; import Url from 'url'; import cypress from 'cypress'; import { FtrProviderContext } from './ftr_provider_context'; import { createApmUsersAndRoles } from '../scripts/create-apm-users-and-roles/create_apm_users_and_roles'; import { esArchiverLoad, esArchiverUnload } from './cypress/tasks/es_archiver'; -export function cypressRunTests(spec?: string) { - return async ({ getService }: FtrProviderContext) => { - const result = await cypressStart(getService, cypress.run, spec); - - if (result && (result.status === 'failed' || result.totalFailed > 0)) { - throw new Error(`APM Cypress tests failed`); - } - }; -} - -export async function cypressOpenTests({ getService }: FtrProviderContext) { - await cypressStart(getService, cypress.open); -} - -async function cypressStart( +export async function cypressStart( getService: FtrProviderContext['getService'], - cypressExecution: typeof cypress.run | typeof cypress.open, - spec?: string + cypressExecution: typeof cypress.run | typeof cypress.open ) { const config = getService('config'); @@ -68,8 +54,9 @@ async function cypressStart( console.log(`Loading ES archive "${archiveName}"`); await esArchiverLoad(archiveName); + const spec = argv.grep as string | undefined; const res = await cypressExecution({ - ...(spec !== undefined ? { spec } : {}), + ...(spec ? { spec } : {}), config: { baseUrl: kibanaUrl }, env: { KIBANA_URL: kibanaUrl, diff --git a/x-pack/plugins/apm/ftr_e2e/config.ts b/x-pack/plugins/apm/ftr_e2e/ftr_config.ts similarity index 100% rename from x-pack/plugins/apm/ftr_e2e/config.ts rename to x-pack/plugins/apm/ftr_e2e/ftr_config.ts diff --git a/x-pack/plugins/apm/ftr_e2e/ftr_config_open.ts b/x-pack/plugins/apm/ftr_e2e/ftr_config_open.ts new file mode 100644 index 00000000000000..92f605d3287899 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/ftr_config_open.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; +import cypress from 'cypress'; +import { FtrProviderContext } from './ftr_provider_context'; +import { cypressStart } from './cypress_start'; + +async function ftrConfigOpen({ readConfigFile }: FtrConfigProviderContext) { + const kibanaConfig = await readConfigFile(require.resolve('./ftr_config.ts')); + return { + ...kibanaConfig.getAll(), + testRunner, + }; +} + +export async function testRunner({ getService }: FtrProviderContext) { + await cypressStart(getService, cypress.open); +} + +// eslint-disable-next-line import/no-default-export +export default ftrConfigOpen; diff --git a/x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts b/x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts new file mode 100644 index 00000000000000..51c859a8477f23 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/ftr_config_run.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrConfigProviderContext } from '@kbn/test'; +import cypress from 'cypress'; +import { cypressStart } from './cypress_start'; +import { FtrProviderContext } from './ftr_provider_context'; + +async function ftrConfigRun({ readConfigFile }: FtrConfigProviderContext) { + const kibanaConfig = await readConfigFile(require.resolve('./ftr_config.ts')); + return { + ...kibanaConfig.getAll(), + testRunner, + }; +} + +async function testRunner({ getService }: FtrProviderContext) { + const result = await cypressStart(getService, cypress.run); + + if (result && (result.status === 'failed' || result.totalFailed > 0)) { + throw new Error(`APM Cypress tests failed`); + } +} + +// eslint-disable-next-line import/no-default-export +export default ftrConfigRun; diff --git a/x-pack/plugins/apm/jest.config.js b/x-pack/plugins/apm/jest.config.js index 66b4b164a794c7..cc985407698bfd 100644 --- a/x-pack/plugins/apm/jest.config.js +++ b/x-pack/plugins/apm/jest.config.js @@ -19,5 +19,6 @@ module.exports = { coverageReporters: ['text', 'html'], collectCoverageFrom: [ '/x-pack/plugins/apm/{common,public,server}/**/*.{js,ts,tsx}', + '!/**/*.stories.*', ], }; diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx index cc4bd0d14e290a..54c34121ea0cb3 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumHome.tsx @@ -48,7 +48,7 @@ export function RumHome() { 'Enable RUM with the APM agent to collect user experience data.', } ), - href: core.http.basePath.prepend(`integrations/detail/apm`), + href: core.http.basePath.prepend(`/app/home#/tutorial/apm`), }, }, docsLink: core.docLinks.links.observability.guide, diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx new file mode 100644 index 00000000000000..0a4adc07e1a980 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Meta, Story } from '@storybook/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; +import { TimeRangeComparisonEnum } from '../../../../common/runtime_types/comparison_type_rt'; +import { AnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/anomaly_detection_jobs_context'; +import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { ServiceInventory } from './'; + +const stories: Meta<{}> = { + title: 'app/ServiceInventory', + component: ServiceInventory, + decorators: [ + (StoryComponent) => { + const coreMock = { + http: { + get: (endpoint: string) => { + switch (endpoint) { + case '/internal/apm/services': + return { items: [] }; + default: + return {}; + } + return {}; + }, + }, + notifications: { toasts: { add: () => {}, addWarning: () => {} } }, + uiSettings: { get: () => [] }, + } as unknown as CoreStart; + + const KibanaReactContext = createKibanaReactContext(coreMock); + + const anomlyDetectionJobsContextValue = { + anomalyDetectionJobsData: { jobs: [], hasLegacyJobs: false }, + anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS, + anomalyDetectionJobsRefetch: () => {}, + }; + + return ( + + + + + + + + + + + + ); + }, + ], +}; +export default stories; + +export const Example: Story<{}> = () => { + return ; +}; diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx index 4a020f9b0db4ef..36b1053248d252 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.test.tsx @@ -5,249 +5,17 @@ * 2.0. */ -import { render, waitFor } from '@testing-library/react'; -import { CoreStart } from 'kibana/public'; -import { merge } from 'lodash'; -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; -import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; -import { ServiceHealthStatus } from '../../../../common/service_health_status'; -import { TimeRangeComparisonEnum } from '../../../../common/runtime_types/comparison_type_rt'; -import { ServiceInventory } from '.'; -import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; -import { - mockApmPluginContextValue, - MockApmPluginContextWrapper, -} from '../../../context/apm_plugin/mock_apm_plugin_context'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import { clearCache } from '../../../services/rest/callApi'; -import * as useDynamicDataViewHooks from '../../../hooks/use_dynamic_data_view'; -import { SessionStorageMock } from '../../../services/__mocks__/SessionStorageMock'; -import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; -import * as hook from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context'; +import { composeStories } from '@storybook/testing-react'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import * as stories from './service_inventory.stories'; -const KibanaReactContext = createKibanaReactContext({ - usageCollection: { reportUiCounter: () => {} }, -} as Partial); - -const addWarning = jest.fn(); -const httpGet = jest.fn(); - -function wrapper({ children }: { children?: ReactNode }) { - const mockPluginContext = merge({}, mockApmPluginContextValue, { - core: { - http: { - get: httpGet, - }, - notifications: { - toasts: { - addWarning, - }, - }, - }, - }) as unknown as ApmPluginContextValue; - - return ( - - - - - - {children} - - - - - - ); -} +const { Example } = composeStories(stories); describe('ServiceInventory', () => { - beforeEach(() => { - // @ts-expect-error - global.sessionStorage = new SessionStorageMock(); - clearCache(); - - jest.spyOn(hook, 'useAnomalyDetectionJobsContext').mockReturnValue({ - anomalyDetectionJobsData: { jobs: [], hasLegacyJobs: false }, - anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS, - anomalyDetectionJobsRefetch: () => {}, - }); - - jest - .spyOn(useDynamicDataViewHooks, 'useDynamicDataViewFetcher') - .mockReturnValue({ - dataView: undefined, - status: FETCH_STATUS.SUCCESS, - }); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should render services, when list is not empty', async () => { - // mock rest requests - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [ - { - serviceName: 'My Python Service', - agentName: 'python', - transactionsPerMinute: 100, - errorsPerMinute: 200, - avgResponseTime: 300, - environments: ['test', 'dev'], - healthStatus: ServiceHealthStatus.warning, - }, - { - serviceName: 'My Go Service', - agentName: 'go', - transactionsPerMinute: 400, - errorsPerMinute: 500, - avgResponseTime: 600, - environments: [], - severity: ServiceHealthStatus.healthy, - }, - ], - }); - - const { container, findByText } = render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); - await findByText('My Python Service'); - - expect(container.querySelectorAll('.euiTableRow')).toHaveLength(2); - }); - - it('should render empty message, when list is empty and historical data is found', async () => { - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [], - }); - - const { findByText } = render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); - const noServicesText = await findByText('No services found'); - - expect(noServicesText).not.toBeEmptyDOMElement(); - }); - - describe('when legacy data is found', () => { - it('renders an upgrade migration notification', async () => { - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: true, - hasHistoricalData: true, - items: [], - }); - - render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); - - expect(addWarning).toHaveBeenLastCalledWith( - expect.objectContaining({ - title: 'Legacy data was detected within the selected time range', - }) - ); - }); - }); - - describe('when legacy data is not found', () => { - it('does not render an upgrade migration notification', async () => { - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [], - }); - - render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); - - expect(addWarning).not.toHaveBeenCalled(); - }); - }); - - describe('when ML data is not found', () => { - it('does not render the health column', async () => { - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [ - { - serviceName: 'My Python Service', - agentName: 'python', - transactionsPerMinute: 100, - errorsPerMinute: 200, - avgResponseTime: 300, - environments: ['test', 'dev'], - }, - ], - }); - - const { queryByText } = render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); - - expect(queryByText('Health')).toBeNull(); - }); - }); - - describe('when ML data is found', () => { - it('renders the health column', async () => { - httpGet - .mockResolvedValueOnce({ fallbackToTransactions: false }) - .mockResolvedValueOnce({ - hasLegacyData: false, - hasHistoricalData: true, - items: [ - { - serviceName: 'My Python Service', - agentName: 'python', - transactionsPerMinute: 100, - errorsPerMinute: 200, - avgResponseTime: 300, - environments: ['test', 'dev'], - healthStatus: ServiceHealthStatus.warning, - }, - ], - }); - - const { queryAllByText } = render(, { wrapper }); - - // wait for requests to be made - await waitFor(() => expect(httpGet).toHaveBeenCalledTimes(2)); + it('renders', async () => { + render(); - expect(queryAllByText('Health').length).toBeGreaterThan(1); - }); + expect(await screen.findByRole('table')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.stories.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.stories.tsx new file mode 100644 index 00000000000000..b632d3a33dea86 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.stories.tsx @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Meta, Story } from '@storybook/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import type { CoreStart } from '../../../../../../../src/core/public'; +import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; +import type { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; +import { + APMServiceContext, + APMServiceContextValue, +} from '../../../context/apm_service/apm_service_context'; +import { ServiceOverview } from './'; + +const stories: Meta<{}> = { + title: 'app/ServiceOverview', + component: ServiceOverview, + decorators: [ + (StoryComponent) => { + const serviceName = 'testServiceName'; + const mockCore = { + http: { + basePath: { prepend: () => {} }, + get: (endpoint: string) => { + switch (endpoint) { + case `/api/apm/services/${serviceName}/annotation/search`: + return { annotations: [] }; + case '/internal/apm/fallback_to_transactions': + return { fallbackToTransactions: false }; + case `/internal/apm/services/${serviceName}/dependencies`: + return { serviceDependencies: [] }; + default: + return {}; + } + }, + }, + notifications: { toasts: { add: () => {} } }, + uiSettings: { get: () => 'Browser' }, + } as unknown as CoreStart; + const serviceContextValue = { + alerts: [], + serviceName, + } as unknown as APMServiceContextValue; + const KibanaReactContext = createKibanaReactContext(mockCore); + + return ( + + + + + + + + + + ); + }, + ], +}; +export default stories; + +export const Example: Story<{}> = () => { + return ; +}; diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx index 7e9b4325591d9a..fb60604aa53b2e 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview.test.tsx @@ -5,178 +5,19 @@ * 2.0. */ -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { CoreStart } from 'src/core/public'; -import { isEqual } from 'lodash'; -import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; -import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; -import { - mockApmPluginContextValue, - MockApmPluginContextWrapper, -} from '../../../context/apm_plugin/mock_apm_plugin_context'; -import * as useDynamicDataViewHooks from '../../../hooks/use_dynamic_data_view'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; -import * as useAnnotationsHooks from '../../../context/annotations/use_annotations_context'; -import * as useTransactionBreakdownHooks from '../../shared/charts/transaction_breakdown_chart/use_transaction_breakdown'; -import { renderWithTheme } from '../../../utils/testHelpers'; -import { ServiceOverview } from './'; -import { waitFor } from '@testing-library/dom'; -import * as useApmServiceContextHooks from '../../../context/apm_service/use_apm_service_context'; -import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; -import { - getCallApmApiSpy, - getCreateCallApmApiSpy, -} from '../../../services/rest/callApmApiSpy'; -import { fromQuery } from '../../shared/Links/url_helpers'; -import { MockUrlParamsContextProvider } from '../../../context/url_params_context/mock_url_params_context_provider'; -import { uiSettingsServiceMock } from '../../../../../../../src/core/public/mocks'; +import { composeStories } from '@storybook/testing-react'; +import { render, screen } from '@testing-library/react'; +import React from 'react'; +import * as stories from './service_overview.stories'; -const uiSettings = uiSettingsServiceMock.create().setup({} as any); - -const KibanaReactContext = createKibanaReactContext({ - notifications: { toasts: { add: () => {} } }, - uiSettings, - usageCollection: { reportUiCounter: () => {} }, -} as unknown as Partial); - -const mockParams = { - rangeFrom: 'now-15m', - rangeTo: 'now', - latencyAggregationType: LatencyAggregationType.avg, -}; - -const location = { - pathname: '/services/test%20service%20name/overview', - search: fromQuery(mockParams), -}; - -function Wrapper({ children }: { children?: ReactNode }) { - const value = { - ...mockApmPluginContextValue, - core: { - ...mockApmPluginContextValue.core, - http: { - basePath: { prepend: () => {} }, - get: () => {}, - }, - }, - } as unknown as ApmPluginContextValue; - - return ( - - - - - {children} - - - - - ); -} +const { Example } = composeStories(stories); describe('ServiceOverview', () => { it('renders', async () => { - jest - .spyOn(useApmServiceContextHooks, 'useApmServiceContext') - .mockReturnValue({ - serviceName: 'test service name', - agentName: 'java', - transactionType: 'request', - transactionTypes: ['request'], - alerts: [], - }); - jest - .spyOn(useAnnotationsHooks, 'useAnnotationsContext') - .mockReturnValue({ annotations: [] }); - jest - .spyOn(useDynamicDataViewHooks, 'useDynamicDataViewFetcher') - .mockReturnValue({ - dataView: undefined, - status: FETCH_STATUS.SUCCESS, - }); - - /* eslint-disable @typescript-eslint/naming-convention */ - const calls = { - 'GET /internal/apm/services/{serviceName}/error_groups/main_statistics': { - error_groups: [] as any[], - }, - 'GET /internal/apm/services/{serviceName}/transactions/groups/main_statistics': - { - transactionGroups: [] as any[], - totalTransactionGroups: 0, - isAggregationAccurate: true, - }, - 'GET /internal/apm/services/{serviceName}/dependencies': { - serviceDependencies: [], - }, - 'GET /internal/apm/services/{serviceName}/service_overview_instances/main_statistics': - [], - 'GET /internal/apm/services/{serviceName}/transactions/charts/latency': { - currentPeriod: { - overallAvgDuration: null, - latencyTimeseries: [], - }, - previousPeriod: { - overallAvgDuration: null, - latencyTimeseries: [], - }, - }, - 'GET /internal/apm/services/{serviceName}/throughput': { - currentPeriod: [], - previousPeriod: [], - }, - 'GET /internal/apm/services/{serviceName}/transactions/charts/error_rate': - { - currentPeriod: { - timeseries: [], - average: null, - }, - previousPeriod: { - timeseries: [], - average: null, - }, - }, - 'GET /api/apm/services/{serviceName}/annotation/search': { - annotations: [], - }, - 'GET /internal/apm/fallback_to_transactions': { - fallbackToTransactions: false, - }, - }; - /* eslint-enable @typescript-eslint/naming-convention */ - - const callApmApiSpy = getCallApmApiSpy().mockImplementation( - ({ endpoint }) => { - const response = calls[endpoint as keyof typeof calls]; - - return response - ? Promise.resolve(response) - : Promise.reject(`Response for ${endpoint} is not defined`); - } - ); - - getCreateCallApmApiSpy().mockImplementation(() => callApmApiSpy as any); - jest - .spyOn(useTransactionBreakdownHooks, 'useTransactionBreakdown') - .mockReturnValue({ - data: { timeseries: [] }, - error: undefined, - status: FETCH_STATUS.SUCCESS, - }); - - const { findAllByText } = renderWithTheme(, { - wrapper: Wrapper, - }); - - await waitFor(() => { - const endpoints = callApmApiSpy.mock.calls.map( - (call) => call[0].endpoint - ); - return isEqual(endpoints.sort(), Object.keys(calls).sort()); - }); + render(); - expect((await findAllByText('Latency')).length).toBeGreaterThan(0); + expect( + await screen.findByRole('heading', { name: /Latency/ }) + ).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx index 058c7d97b43ccc..7472eb780f119f 100644 --- a/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx +++ b/x-pack/plugins/apm/public/components/app/service_overview/service_overview_throughput_chart.tsx @@ -30,7 +30,6 @@ import { const INITIAL_STATE = { currentPeriod: [], previousPeriod: [], - throughputUnit: 'minute' as const, }; export function ServiceOverviewThroughputChart({ diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx index 025fa8ddcc8a05..25a68592d2b119 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -69,6 +69,8 @@ export const home = { t.partial({ refreshPaused: t.union([t.literal('true'), t.literal('false')]), refreshInterval: t.string, + comparisonEnabled: toBooleanRt, + comparisonType: comparisonTypeRt, }), ]), }), diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index 4afa10cbf9a5d1..37259f7c91e229 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -135,6 +135,8 @@ export const serviceDetail = { t.partial({ traceId: t.string, transactionId: t.string, + comparisonEnabled: toBooleanRt, + comparisonType: comparisonTypeRt, }), ]), }), diff --git a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx index 8b885526fb67c7..3de16cf4db0299 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/apm/transaction_detail_link.tsx @@ -14,6 +14,7 @@ import { useLegacyUrlParams } from '../../../../context/url_params_context/use_u import { pickKeys } from '../../../../../common/utils/pick_keys'; import { APMQueryParams } from '../url_helpers'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { TimeRangeComparisonType } from '../../../../../common/runtime_types/comparison_type_rt'; interface Props extends APMLinkExtendProps { serviceName: string; @@ -23,6 +24,8 @@ interface Props extends APMLinkExtendProps { transactionType: string; latencyAggregationType?: string; environment?: string; + comparisonEnabled?: boolean; + comparisonType?: TimeRangeComparisonType; } const persistedFilters: Array = [ @@ -38,6 +41,8 @@ export function TransactionDetailLink({ transactionType, latencyAggregationType, environment, + comparisonEnabled, + comparisonType, ...rest }: Props) { const { urlParams } = useLegacyUrlParams(); @@ -51,6 +56,8 @@ export function TransactionDetailLink({ transactionId, transactionName, transactionType, + comparisonEnabled, + comparisonType, ...pickKeys(urlParams as APMQueryParams, ...persistedFilters), ...pickBy({ latencyAggregationType, environment }, identity), }, diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_enabled.test.ts b/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_enabled.test.ts new file mode 100644 index 00000000000000..23e1c947292048 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_enabled.test.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { CoreStart } from 'kibana/public'; +import { getComparisonEnabled } from './get_comparison_enabled'; + +describe('getComparisonEnabled', () => { + function mockValues({ + uiSettings, + urlComparisonEnabled, + }: { + uiSettings: boolean; + urlComparisonEnabled?: boolean; + }) { + return { + core: { uiSettings: { get: () => uiSettings } } as unknown as CoreStart, + urlComparisonEnabled, + }; + } + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('returns false when kibana config is disabled and url is empty', () => { + const { core, urlComparisonEnabled } = mockValues({ + uiSettings: false, + urlComparisonEnabled: undefined, + }); + expect(getComparisonEnabled({ core, urlComparisonEnabled })).toBeFalsy(); + }); + + it('returns true when kibana config is enabled and url is empty', () => { + const { core, urlComparisonEnabled } = mockValues({ + uiSettings: true, + urlComparisonEnabled: undefined, + }); + expect(getComparisonEnabled({ core, urlComparisonEnabled })).toBeTruthy(); + }); + + it('returns true when defined as true in the url', () => { + const { core, urlComparisonEnabled } = mockValues({ + uiSettings: false, + urlComparisonEnabled: true, + }); + expect(getComparisonEnabled({ core, urlComparisonEnabled })).toBeTruthy(); + }); + + it('returns false when defined as false in the url', () => { + const { core, urlComparisonEnabled } = mockValues({ + uiSettings: true, + urlComparisonEnabled: false, + }); + expect(getComparisonEnabled({ core, urlComparisonEnabled })).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_enabled.ts b/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_enabled.ts new file mode 100644 index 00000000000000..5f2ca5dca46566 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/get_comparison_enabled.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; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { CoreStart } from 'kibana/public'; +import { enableComparisonByDefault } from '../../../../../observability/public'; + +export function getComparisonEnabled({ + core, + urlComparisonEnabled, +}: { + core: CoreStart; + urlComparisonEnabled?: boolean; +}) { + const isEnabledByDefault = core.uiSettings.get( + enableComparisonByDefault + ); + + return urlComparisonEnabled ?? isEnabledByDefault; +} diff --git a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx index 9d077713ff12ec..db085861ae0951 100644 --- a/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/time_comparison/index.tsx @@ -14,10 +14,12 @@ import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common' import { useUiTracker } from '../../../../../observability/public'; import { TimeRangeComparisonEnum } from '../../../../common/runtime_types/comparison_type_rt'; import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; import { useApmParams } from '../../../hooks/use_apm_params'; import { useBreakpoints } from '../../../hooks/use_breakpoints'; import { useTimeRange } from '../../../hooks/use_time_range'; import * as urlHelpers from '../../shared/Links/url_helpers'; +import { getComparisonEnabled } from './get_comparison_enabled'; import { getComparisonTypes } from './get_comparison_types'; import { getTimeRangeComparison } from './get_time_range_comparison'; @@ -113,6 +115,7 @@ export function getSelectOptions({ } export function TimeComparison() { + const { core } = useApmPluginContext(); const trackApmEvent = useUiTracker({ app: 'apm' }); const history = useHistory(); const { isSmall } = useBreakpoints(); @@ -138,7 +141,13 @@ export function TimeComparison() { if (comparisonEnabled === undefined || comparisonType === undefined) { urlHelpers.replace(history, { query: { - comparisonEnabled: comparisonEnabled === false ? 'false' : 'true', + comparisonEnabled: + getComparisonEnabled({ + core, + urlComparisonEnabled: comparisonEnabled, + }) === false + ? 'false' + : 'true', comparisonType: comparisonType ? comparisonType : comparisonTypes[0], }, }); diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx index 18e9beb2c87954..c44fbb8b7f87aa 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/get_columns.tsx @@ -15,6 +15,7 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { ValuesType } from 'utility-types'; import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; +import { TimeRangeComparisonType } from '../../../../common/runtime_types/comparison_type_rt'; import { asMillisecondDuration, asPercent, @@ -42,12 +43,14 @@ export function getColumns({ transactionGroupDetailedStatistics, comparisonEnabled, shouldShowSparkPlots = true, + comparisonType, }: { serviceName: string; latencyAggregationType?: LatencyAggregationType; transactionGroupDetailedStatistics?: TransactionGroupDetailedStatistics; comparisonEnabled?: boolean; shouldShowSparkPlots?: boolean; + comparisonType?: TimeRangeComparisonType; }): Array> { return [ { @@ -67,6 +70,8 @@ export function getColumns({ transactionName={name} transactionType={type} latencyAggregationType={latencyAggregationType} + comparisonEnabled={comparisonEnabled} + comparisonType={comparisonType} > {name} diff --git a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx index 6c934cc51e2f7a..2d9f6584535fa5 100644 --- a/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/transactions_table/index.tsx @@ -222,6 +222,7 @@ export function TransactionsTable({ transactionGroupDetailedStatistics, comparisonEnabled, shouldShowSparkPlots, + comparisonType, }); const isLoading = status === FETCH_STATUS.LOADING; diff --git a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx index 9d207eee2fbaa6..c99ef519f9e695 100644 --- a/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx +++ b/x-pack/plugins/apm/public/context/apm_service/apm_service_context.tsx @@ -23,14 +23,20 @@ export type APMServiceAlert = ValuesType< APIReturnType<'GET /internal/apm/services/{serviceName}/alerts'>['alerts'] >; -export const APMServiceContext = createContext<{ +export interface APMServiceContextValue { serviceName: string; agentName?: string; transactionType?: string; transactionTypes: string[]; alerts: APMServiceAlert[]; runtimeName?: string; -}>({ serviceName: '', transactionTypes: [], alerts: [] }); +} + +export const APMServiceContext = createContext({ + serviceName: '', + transactionTypes: [], + alerts: [], +}); export function ApmServiceContextProvider({ children, diff --git a/x-pack/plugins/apm/scripts/test/api.js b/x-pack/plugins/apm/scripts/test/api.js index 4f0d82d0c11630..1905c8eb7c2dda 100644 --- a/x-pack/plugins/apm/scripts/test/api.js +++ b/x-pack/plugins/apm/scripts/test/api.js @@ -33,9 +33,15 @@ const { argv } = yargs(process.argv.slice(2)) description: 'Run all tests (an instance of Elasticsearch and kibana are needs to be available)', }) + .option('grep', { + alias: 'spec', + default: false, + type: 'string', + description: 'Specify the spec files to run', + }) .help(); -const { trial, server, runner } = argv; +const { trial, server, runner, grep } = argv; const license = trial ? 'trial' : 'basic'; console.log(`License: ${license}`); @@ -46,7 +52,10 @@ if (server) { } else if (runner) { ftrScript = 'functional_test_runner'; } -childProcess.execSync( - `node ../../../../scripts/${ftrScript} --config ../../../../test/apm_api_integration/${license}/config.ts`, - { cwd: path.join(__dirname), stdio: 'inherit' } -); + +const grepArg = grep ? `--grep "${grep}"` : ''; +const cmd = `node ../../../../scripts/${ftrScript} ${grepArg} --config ../../../../test/apm_api_integration/${license}/config.ts`; + +console.log(`Running ${cmd}`); + +childProcess.execSync(cmd, { cwd: path.join(__dirname), stdio: 'inherit' }); diff --git a/x-pack/plugins/apm/scripts/test/e2e.js b/x-pack/plugins/apm/scripts/test/e2e.js index b3ce510a8e569b..13055dce2fec56 100644 --- a/x-pack/plugins/apm/scripts/test/e2e.js +++ b/x-pack/plugins/apm/scripts/test/e2e.js @@ -20,7 +20,7 @@ const { argv } = yargs(process.argv.slice(2)) .option('server', { default: false, type: 'boolean', - description: 'Start Elasticsearch and kibana', + description: 'Start Elasticsearch and Kibana', }) .option('runner', { default: false, @@ -28,14 +28,26 @@ const { argv } = yargs(process.argv.slice(2)) description: 'Run all tests (an instance of Elasticsearch and kibana are needs to be available)', }) + .option('grep', { + alias: 'spec', + default: false, + type: 'string', + description: + 'Specify the spec files to run (use doublequotes for glob matching)', + }) .option('open', { default: false, type: 'boolean', description: 'Opens the Cypress Test Runner', }) + .option('bail', { + default: false, + type: 'boolean', + description: 'stop tests after the first failure', + }) .help(); -const { server, runner, open, kibanaInstallDir } = argv; +const { server, runner, open, grep, bail, kibanaInstallDir } = argv; const e2eDir = path.join(__dirname, '../../ftr_e2e'); @@ -46,9 +58,10 @@ if (server) { ftrScript = 'functional_test_runner'; } -const config = open ? './cypress_open.ts' : './cypress_run.ts'; +const config = open ? './ftr_config_open.ts' : './ftr_config_run.ts'; +const grepArg = grep ? `--grep "${grep}"` : ''; +const bailArg = bail ? `--bail` : ''; +const cmd = `node ../../../../scripts/${ftrScript} --config ${config} ${grepArg} ${bailArg} --kibana-install-dir '${kibanaInstallDir}'`; -childProcess.execSync( - `node ../../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}'`, - { cwd: e2eDir, stdio: 'inherit' } -); +console.log(`Running ${cmd}`); +childProcess.execSync(cmd, { cwd: e2eDir, stdio: 'inherit' }); diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts index 8767b5a60d9b25..693502d7629e81 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_duration.ts @@ -6,7 +6,7 @@ */ import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { rangeQuery } from '../../../../../observability/server'; +import { rangeQuery, termQuery } from '../../../../../observability/server'; import { SERVICE_NAME, TRANSACTION_TYPE, @@ -46,10 +46,8 @@ export async function getTransactionDurationChartPreview({ const query = { bool: { filter: [ - ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), - ...(transactionType - ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] - : []), + ...termQuery(SERVICE_NAME, serviceName), + ...termQuery(TRANSACTION_TYPE, transactionType), ...rangeQuery(start, end), ...environmentQuery(environment), ...getDocumentTypeFilterForTransactions(searchAggregatedTransactions), diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts index 0cd1c1cddc6513..0e1fa74199f601 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_count.ts @@ -8,7 +8,7 @@ import { SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; import { AlertParams } from '../../../routes/alerts/chart_preview'; -import { rangeQuery } from '../../../../../observability/server'; +import { rangeQuery, termQuery } from '../../../../../observability/server'; import { environmentQuery } from '../../../../common/utils/environment_query'; import { Setup } from '../../helpers/setup_request'; @@ -25,7 +25,7 @@ export async function getTransactionErrorCountChartPreview({ const query = { bool: { filter: [ - ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), + ...termQuery(SERVICE_NAME, serviceName), ...rangeQuery(start, end), ...environmentQuery(environment), ], diff --git a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts index d3f03c597e8fb8..e2bfaf29f83cb4 100644 --- a/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/alerts/chart_preview/get_transaction_error_rate.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { rangeQuery } from '../../../../../observability/server'; +import { rangeQuery, termQuery } from '../../../../../observability/server'; import { SERVICE_NAME, TRANSACTION_TYPE, @@ -52,10 +52,8 @@ export async function getTransactionErrorRateChartPreview({ query: { bool: { filter: [ - ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), - ...(transactionType - ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] - : []), + ...termQuery(SERVICE_NAME, serviceName), + ...termQuery(TRANSACTION_TYPE, transactionType), ...rangeQuery(start, end), ...environmentQuery(environment), ...getDocumentTypeFilterForTransactions( diff --git a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts index 7fe2adcfe24d79..17beacae4b14d3 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_error_count_alert_type.ts @@ -41,6 +41,7 @@ import { getApmIndices } from '../settings/apm_indices/get_apm_indices'; import { apmActionVariables } from './action_variables'; import { alertingEsClient } from './alerting_es_client'; import { RegisterRuleDependencies } from './register_apm_alerts'; +import { termQuery } from '../../../../observability/server'; const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; @@ -113,9 +114,7 @@ export function registerErrorCountAlertType({ }, }, { term: { [PROCESSOR_EVENT]: ProcessorEvent.error } }, - ...(alertParams.serviceName - ? [{ term: { [SERVICE_NAME]: alertParams.serviceName } }] - : []), + ...termQuery(SERVICE_NAME, alertParams.serviceName), ...environmentQuery(alertParams.environment), ], }, diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts index 2809d7feadb378..ec2fbb4028b742 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -46,6 +46,7 @@ import { getEnvironmentEsField, getEnvironmentLabel, } from '../../../common/environment_filter_values'; +import { termQuery } from '../../../../observability/server'; const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; @@ -157,24 +158,11 @@ export function registerTransactionDurationAnomalyAlertType({ }, }, }, - ...(alertParams.serviceName - ? [ - { - term: { - partition_field_value: alertParams.serviceName, - }, - }, - ] - : []), - ...(alertParams.transactionType - ? [ - { - term: { - by_field_value: alertParams.transactionType, - }, - }, - ] - : []), + ...termQuery( + 'partition_field_value', + alertParams.serviceName + ), + ...termQuery('by_field_value', alertParams.transactionType), ] as QueryDslQueryContainer[], }, }, diff --git a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts index 5ba7ed5321d703..43dfbaf156f6c1 100644 --- a/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts +++ b/x-pack/plugins/apm/server/lib/alerts/register_transaction_error_rate_alert_type.ts @@ -48,6 +48,7 @@ import { RegisterRuleDependencies } from './register_apm_alerts'; import { SearchAggregatedTransactionSetting } from '../../../common/aggregated_transactions'; import { getDocumentTypeFilterForTransactions } from '../helpers/transactions'; import { asPercent } from '../../../../observability/common/utils/formatters'; +import { termQuery } from '../../../../observability/server'; const ALERT_EVALUATION_THRESHOLD: typeof ALERT_EVALUATION_THRESHOLD_TYPED = ALERT_EVALUATION_THRESHOLD_NON_TYPED; @@ -142,18 +143,8 @@ export function registerTransactionErrorRateAlertType({ ], }, }, - ...(alertParams.serviceName - ? [{ term: { [SERVICE_NAME]: alertParams.serviceName } }] - : []), - ...(alertParams.transactionType - ? [ - { - term: { - [TRANSACTION_TYPE]: alertParams.transactionType, - }, - }, - ] - : []), + ...termQuery(SERVICE_NAME, alertParams.serviceName), + ...termQuery(TRANSACTION_TYPE, alertParams.transactionType), ...environmentQuery(alertParams.environment), ], }, diff --git a/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts b/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts index 1fbdd1c680c58d..5a7e06683f25a7 100644 --- a/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts +++ b/x-pack/plugins/apm/server/lib/backends/get_throughput_charts_for_backend.ts @@ -15,7 +15,6 @@ import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../helpers/setup_request'; import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms'; import { getBucketSize } from '../helpers/get_bucket_size'; -import { calculateThroughputWithInterval } from '../helpers/calculate_throughput'; export async function getThroughputChartsForBackend({ backendName, @@ -42,7 +41,7 @@ export async function getThroughputChartsForBackend({ offset, }); - const { intervalString, bucketSize } = getBucketSize({ + const { intervalString } = getBucketSize({ start: startWithOffset, end: endWithOffset, minBucketSize: 60, @@ -73,9 +72,10 @@ export async function getThroughputChartsForBackend({ extended_bounds: { min: startWithOffset, max: endWithOffset }, }, aggs: { - spanDestinationLatencySum: { - sum: { + throughput: { + rate: { field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT, + unit: 'minute', }, }, }, @@ -88,10 +88,7 @@ export async function getThroughputChartsForBackend({ response.aggregations?.timeseries.buckets.map((bucket) => { return { x: bucket.key + offsetInMs, - y: calculateThroughputWithInterval({ - bucketSize, - value: bucket.spanDestinationLatencySum.value || 0, - }), + y: bucket.throughput.value, }; }) ?? [] ); diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index d6c53aeea078ee..7bdb21b9fda783 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { termQuery } from '../../../../observability/server'; import { ProcessorEvent } from '../../../common/processor_event'; import { Setup } from '../helpers/setup_request'; import { @@ -37,11 +38,6 @@ export async function getAllEnvironments({ const { apmEventClient } = setup; - // omit filter for service.name if "All" option is selected - const serviceNameFilter = serviceName - ? [{ term: { [SERVICE_NAME]: serviceName } }] - : []; - const params = { apm: { events: [ @@ -57,7 +53,7 @@ export async function getAllEnvironments({ size: 0, query: { bool: { - filter: [...serviceNameFilter], + filter: [...termQuery(SERVICE_NAME, serviceName)], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/environments/get_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_environments.ts index 678cfd891ae575..cd5caab6d25872 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_environments.ts @@ -11,7 +11,7 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; import { ProcessorEvent } from '../../../common/processor_event'; -import { rangeQuery } from '../../../../observability/server'; +import { rangeQuery, termQuery } from '../../../../observability/server'; import { getProcessorEventForTransactions } from '../helpers/transactions'; import { Setup } from '../helpers/setup_request'; @@ -40,14 +40,6 @@ export async function getEnvironments({ const { apmEventClient } = setup; - const filter = rangeQuery(start, end); - - if (serviceName) { - filter.push({ - term: { [SERVICE_NAME]: serviceName }, - }); - } - const params = { apm: { events: [ @@ -60,7 +52,10 @@ export async function getEnvironments({ size: 0, query: { bool: { - filter, + filter: [ + ...rangeQuery(start, end), + ...termQuery(SERVICE_NAME, serviceName), + ], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts index dce8a3f397eaaa..625089e99d3600 100644 --- a/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts +++ b/x-pack/plugins/apm/server/lib/errors/distribution/get_buckets.ts @@ -5,13 +5,16 @@ * 2.0. */ -import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { ERROR_GROUP_ID, SERVICE_NAME, } from '../../../../common/elasticsearch_fieldnames'; import { ProcessorEvent } from '../../../../common/processor_event'; -import { rangeQuery, kqlQuery } from '../../../../../observability/server'; +import { + rangeQuery, + kqlQuery, + termQuery, +} from '../../../../../observability/server'; import { environmentQuery } from '../../../../common/utils/environment_query'; import { Setup } from '../../helpers/setup_request'; @@ -35,16 +38,6 @@ export async function getBuckets({ end: number; }) { const { apmEventClient } = setup; - const filter: ESFilter[] = [ - { term: { [SERVICE_NAME]: serviceName } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; - - if (groupId) { - filter.push({ term: { [ERROR_GROUP_ID]: groupId } }); - } const params = { apm: { @@ -54,7 +47,13 @@ export async function getBuckets({ size: 0, query: { bool: { - filter, + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ...termQuery(ERROR_GROUP_ID, groupId), + ], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts index 7eaa90845d6522..f4829f2d5faa01 100644 --- a/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts +++ b/x-pack/plugins/apm/server/lib/metrics/transform_metrics_chart.ts @@ -26,26 +26,30 @@ export function transformDataToMetricsChart( title: chartBase.title, key: chartBase.key, yUnit: chartBase.yUnit, - series: Object.keys(chartBase.series).map((seriesKey, i) => { - const overallValue = aggregations?.[seriesKey]?.value; + series: + result.hits.total.value > 0 + ? Object.keys(chartBase.series).map((seriesKey, i) => { + const overallValue = aggregations?.[seriesKey]?.value; - return { - title: chartBase.series[seriesKey].title, - key: seriesKey, - type: chartBase.type, - color: - chartBase.series[seriesKey].color || getVizColorForIndex(i, theme), - overallValue, - data: - timeseriesData?.buckets.map((bucket) => { - const { value } = bucket[seriesKey]; - const y = value === null || isNaN(value) ? null : value; return { - x: bucket.key, - y, + title: chartBase.series[seriesKey].title, + key: seriesKey, + type: chartBase.type, + color: + chartBase.series[seriesKey].color || + getVizColorForIndex(i, theme), + overallValue, + data: + timeseriesData?.buckets.map((bucket) => { + const { value } = bucket[seriesKey]; + const y = value === null || isNaN(value) ? null : value; + return { + x: bucket.key, + y, + }; + }) || [], }; - }) || [], - }; - }), + }) + : [], }; } diff --git a/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts index 829afa8330164e..de4d6dec4e1fe2 100644 --- a/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts +++ b/x-pack/plugins/apm/server/lib/observability_overview/get_transactions_per_minute.ts @@ -16,10 +16,7 @@ import { getDocumentTypeFilterForTransactions, getProcessorEventForTransactions, } from '../helpers/transactions'; -import { - calculateThroughputWithInterval, - calculateThroughputWithRange, -} from '../helpers/calculate_throughput'; +import { calculateThroughputWithRange } from '../helpers/calculate_throughput'; export async function getTransactionsPerMinute({ setup, @@ -70,6 +67,9 @@ export async function getTransactionsPerMinute({ fixed_interval: intervalString, min_doc_count: 0, }, + aggs: { + throughput: { rate: { unit: 'minute' as const } }, + }, }, }, }, @@ -98,10 +98,7 @@ export async function getTransactionsPerMinute({ timeseries: topTransactionTypeBucket?.timeseries.buckets.map((bucket) => ({ x: bucket.key, - y: calculateThroughputWithInterval({ - bucketSize, - value: bucket.doc_count, - }), + y: bucket.throughput.value, })) || [], }; } diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts index ae511d0fed8f80..aaf55413d97745 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map.ts @@ -7,14 +7,14 @@ import { Logger } from 'kibana/server'; import { chunk } from 'lodash'; +import { ProcessorEvent } from '../../../common/processor_event'; +import { rangeQuery, termQuery } from '../../../../observability/server'; import { PromiseReturnType } from '../../../../observability/typings/common'; import { AGENT_NAME, SERVICE_ENVIRONMENT, SERVICE_NAME, } from '../../../common/elasticsearch_fieldnames'; -import { getServicesProjection } from '../../projections/services'; -import { mergeProjection } from '../../projections/util/merge_projection'; import { environmentQuery } from '../../../common/utils/environment_query'; import { withApmSpan } from '../../utils/with_apm_span'; import { Setup } from '../helpers/setup_request'; @@ -26,6 +26,7 @@ import { getServiceMapFromTraceIds } from './get_service_map_from_trace_ids'; import { getTraceSampleIds } from './get_trace_sample_ids'; import { transformServiceMapResponses } from './transform_service_map_responses'; import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; +import { getProcessorEventForTransactions } from '../helpers/transactions'; export interface IEnvOptions { setup: Setup; @@ -94,40 +95,29 @@ async function getServicesData(options: IEnvOptions) { const { environment, setup, searchAggregatedTransactions, start, end } = options; - const projection = getServicesProjection({ - setup, - searchAggregatedTransactions, - kuery: '', - start, - end, - }); - - let filter = [ - ...projection.body.query.bool.filter, - ...environmentQuery(environment), - ]; - - if (options.serviceName) { - filter = filter.concat({ - term: { - [SERVICE_NAME]: options.serviceName, - }, - }); - } - - const params = mergeProjection(projection, { + const params = { + apm: { + events: [ + getProcessorEventForTransactions(searchAggregatedTransactions), + ProcessorEvent.metric as const, + ProcessorEvent.error as const, + ], + }, body: { size: 0, query: { bool: { - ...projection.body.query.bool, - filter, + filter: [ + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...termQuery(SERVICE_NAME, options.serviceName), + ], }, }, aggs: { services: { terms: { - field: projection.body.aggs.services.terms.field, + field: SERVICE_NAME, size: 500, }, aggs: { @@ -140,7 +130,7 @@ async function getServicesData(options: IEnvOptions) { }, }, }, - }); + }; const { apmEventClient } = setup; diff --git a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index d6d6219440dadf..95bd6106b9ff24 100644 --- a/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -98,6 +98,9 @@ Object { }, }, "size": 1, + "sort": Object { + "_score": "desc", + }, }, "terminate_after": 1, } diff --git a/x-pack/plugins/apm/server/lib/services/get_service_agent.ts b/x-pack/plugins/apm/server/lib/services/get_service_agent.ts index 4c9ff9f124b107..dc3fee20fdf68c 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_agent.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_agent.ts @@ -13,7 +13,6 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { rangeQuery } from '../../../../observability/server'; import { Setup } from '../helpers/setup_request'; -import { getProcessorEventForTransactions } from '../helpers/transactions'; interface ServiceAgent { agent?: { @@ -29,13 +28,11 @@ interface ServiceAgent { export async function getServiceAgent({ serviceName, setup, - searchAggregatedTransactions, start, end, }: { serviceName: string; setup: Setup; - searchAggregatedTransactions: boolean; start: number; end: number; }) { @@ -46,7 +43,7 @@ export async function getServiceAgent({ apm: { events: [ ProcessorEvent.error, - getProcessorEventForTransactions(searchAggregatedTransactions), + ProcessorEvent.transaction, ProcessorEvent.metric, ], }, @@ -71,6 +68,9 @@ export async function getServiceAgent({ }, }, }, + sort: { + _score: 'desc' as const, + }, }, }; diff --git a/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts b/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts index 11669d5934303b..09946187b90a2e 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_infrastructure.ts @@ -6,7 +6,6 @@ */ import { Setup } from '../helpers/setup_request'; -import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; import { rangeQuery, kqlQuery } from '../../../../observability/server'; import { environmentQuery } from '../../../common/utils/environment_query'; import { ProcessorEvent } from '../../../common/processor_event'; @@ -33,13 +32,6 @@ export const getServiceInfrastructure = async ({ }) => { const { apmEventClient } = setup; - const filter: ESFilter[] = [ - { term: { [SERVICE_NAME]: serviceName } }, - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; - const response = await apmEventClient.search('get_service_infrastructure', { apm: { events: [ProcessorEvent.metric], @@ -48,7 +40,12 @@ export const getServiceInfrastructure = async ({ size: 0, query: { bool: { - filter, + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ], }, }, aggs: { diff --git a/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts b/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts index 686555e7764ab1..ea153a5ddcd4ce 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services_detailed_statistics/get_service_transaction_detailed_statistics.ts @@ -70,7 +70,7 @@ export async function getServiceTransactionDetailedStatistics({ }; const response = await apmEventClient.search( - 'get_service_transaction_stats', + 'get_service_transaction_detail_stats', { apm: { events: [ @@ -82,6 +82,7 @@ export async function getServiceTransactionDetailedStatistics({ query: { bool: { filter: [ + { terms: { [SERVICE_NAME]: serviceNames } }, ...getDocumentTypeFilterForTransactions( searchAggregatedTransactions ), @@ -95,8 +96,6 @@ export async function getServiceTransactionDetailedStatistics({ services: { terms: { field: SERVICE_NAME, - include: serviceNames, - size: serviceNames.length, }, aggs: { transactionType: { diff --git a/x-pack/plugins/apm/server/lib/services/get_throughput.ts b/x-pack/plugins/apm/server/lib/services/get_throughput.ts index e31e9dd3b8c9f3..3161066ebadf9c 100644 --- a/x-pack/plugins/apm/server/lib/services/get_throughput.ts +++ b/x-pack/plugins/apm/server/lib/services/get_throughput.ts @@ -5,20 +5,23 @@ * 2.0. */ -import { ESFilter } from '../../../../../../src/core/types/elasticsearch'; +import { AggregationsDateInterval } from '@elastic/elasticsearch/lib/api/types'; import { SERVICE_NAME, TRANSACTION_NAME, TRANSACTION_TYPE, } from '../../../common/elasticsearch_fieldnames'; -import { kqlQuery, rangeQuery } from '../../../../observability/server'; +import { + kqlQuery, + rangeQuery, + termQuery, +} from '../../../../observability/server'; import { environmentQuery } from '../../../common/utils/environment_query'; import { getDocumentTypeFilterForTransactions, getProcessorEventForTransactions, } from '../helpers/transactions'; import { Setup } from '../helpers/setup_request'; -import { calculateThroughputWithInterval } from '../helpers/calculate_throughput'; interface Options { environment: string; @@ -49,30 +52,27 @@ export async function getThroughput({ }: Options) { const { apmEventClient } = setup; - const filter: ESFilter[] = [ - { term: { [SERVICE_NAME]: serviceName } }, - { term: { [TRANSACTION_TYPE]: transactionType } }, - ...getDocumentTypeFilterForTransactions(searchAggregatedTransactions), - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; - - if (transactionName) { - filter.push({ - term: { - [TRANSACTION_NAME]: transactionName, - }, - }); - } - const params = { apm: { events: [getProcessorEventForTransactions(searchAggregatedTransactions)], }, body: { size: 0, - query: { bool: { filter } }, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + { term: { [TRANSACTION_TYPE]: transactionType } }, + ...getDocumentTypeFilterForTransactions( + searchAggregatedTransactions + ), + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ...termQuery(TRANSACTION_NAME, transactionName), + ], + }, + }, aggs: { timeseries: { date_histogram: { @@ -81,6 +81,11 @@ export async function getThroughput({ min_doc_count: 0, extended_bounds: { min: start, max: end }, }, + aggs: { + throughput: { + rate: { unit: 'minute' as AggregationsDateInterval }, + }, + }, }, }, }, @@ -95,10 +100,7 @@ export async function getThroughput({ response.aggregations?.timeseries.buckets.map((bucket) => { return { x: bucket.key, - y: calculateThroughputWithInterval({ - bucketSize, - value: bucket.doc_count, - }), + y: bucket.throughput.value, }; }) ?? [] ); diff --git a/x-pack/plugins/apm/server/lib/services/queries.test.ts b/x-pack/plugins/apm/server/lib/services/queries.test.ts index 4ed6f856d735bd..30d89214959da5 100644 --- a/x-pack/plugins/apm/server/lib/services/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/services/queries.test.ts @@ -28,7 +28,6 @@ describe('services queries', () => { getServiceAgent({ serviceName: 'foo', setup, - searchAggregatedTransactions: false, start: 0, end: 50000, }) diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts index 200d3d6ac7459e..aea92d06b75897 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/fetcher.ts @@ -10,7 +10,11 @@ import { sortBy } from 'lodash'; import moment from 'moment'; import { Unionize } from 'utility-types'; import { AggregationOptionsByType } from '../../../../../../src/core/types/elasticsearch'; -import { kqlQuery, rangeQuery } from '../../../../observability/server'; +import { + kqlQuery, + rangeQuery, + termQuery, +} from '../../../../observability/server'; import { PARENT_ID, SERVICE_NAME, @@ -69,10 +73,6 @@ function getRequest(topTraceOptions: TopTraceOptions) { end, } = topTraceOptions; - const transactionNameFilter = transactionName - ? [{ term: { [TRANSACTION_NAME]: transactionName } }] - : []; - return { apm: { events: [getProcessorEventForTransactions(searchAggregatedTransactions)], @@ -82,7 +82,7 @@ function getRequest(topTraceOptions: TopTraceOptions) { query: { bool: { filter: [ - ...transactionNameFilter, + ...termQuery(TRANSACTION_NAME, transactionName), ...getDocumentTypeFilterForTransactions( searchAggregatedTransactions ), diff --git a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts index b7318e81a84a3e..328d2da0f6df0c 100644 --- a/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts +++ b/x-pack/plugins/apm/server/lib/transaction_groups/get_error_rate.ts @@ -13,7 +13,11 @@ import { } from '../../../common/elasticsearch_fieldnames'; import { EventOutcome } from '../../../common/event_outcome'; import { offsetPreviousPeriodCoordinates } from '../../../common/utils/offset_previous_period_coordinate'; -import { kqlQuery, rangeQuery } from '../../../../observability/server'; +import { + kqlQuery, + rangeQuery, + termQuery, +} from '../../../../observability/server'; import { environmentQuery } from '../../../common/utils/environment_query'; import { Coordinate } from '../../../typings/timeseries'; import { @@ -54,13 +58,6 @@ export async function getErrorRate({ }> { const { apmEventClient } = setup; - const transactionNamefilter = transactionName - ? [{ term: { [TRANSACTION_NAME]: transactionName } }] - : []; - const transactionTypefilter = transactionType - ? [{ term: { [TRANSACTION_TYPE]: transactionType } }] - : []; - const filter = [ { term: { [SERVICE_NAME]: serviceName } }, { @@ -68,8 +65,8 @@ export async function getErrorRate({ [EVENT_OUTCOME]: [EventOutcome.failure, EventOutcome.success], }, }, - ...transactionNamefilter, - ...transactionTypefilter, + ...termQuery(TRANSACTION_NAME, transactionName), + ...termQuery(TRANSACTION_TYPE, transactionType), ...getDocumentTypeFilterForTransactions(searchAggregatedTransactions), ...rangeQuery(start, end), ...environmentQuery(environment), diff --git a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts index c4bae841764cf7..4612d399b54a16 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_latency_charts/index.ts @@ -5,7 +5,6 @@ * 2.0. */ -import { ESFilter } from '../../../../../../../src/core/types/elasticsearch'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { SERVICE_NAME, @@ -14,7 +13,11 @@ import { } from '../../../../common/elasticsearch_fieldnames'; import { LatencyAggregationType } from '../../../../common/latency_aggregation_types'; import { offsetPreviousPeriodCoordinates } from '../../../../common/utils/offset_previous_period_coordinate'; -import { kqlQuery, rangeQuery } from '../../../../../observability/server'; +import { + kqlQuery, + rangeQuery, + termQuery, +} from '../../../../../observability/server'; import { environmentQuery } from '../../../../common/utils/environment_query'; import { getDocumentTypeFilterForTransactions, @@ -61,22 +64,6 @@ function searchLatency({ searchAggregatedTransactions, }); - const filter: ESFilter[] = [ - { term: { [SERVICE_NAME]: serviceName } }, - ...getDocumentTypeFilterForTransactions(searchAggregatedTransactions), - ...rangeQuery(start, end), - ...environmentQuery(environment), - ...kqlQuery(kuery), - ]; - - if (transactionName) { - filter.push({ term: { [TRANSACTION_NAME]: transactionName } }); - } - - if (transactionType) { - filter.push({ term: { [TRANSACTION_TYPE]: transactionType } }); - } - const transactionDurationField = getTransactionDurationFieldForTransactions( searchAggregatedTransactions ); @@ -87,7 +74,21 @@ function searchLatency({ }, body: { size: 0, - query: { bool: { filter } }, + query: { + bool: { + filter: [ + { term: { [SERVICE_NAME]: serviceName } }, + ...getDocumentTypeFilterForTransactions( + searchAggregatedTransactions + ), + ...rangeQuery(start, end), + ...environmentQuery(environment), + ...kqlQuery(kuery), + ...termQuery(TRANSACTION_NAME, transactionName), + ...termQuery(TRANSACTION_TYPE, transactionType), + ], + }, + }, aggs: { latencyTimeseries: { date_histogram: { diff --git a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts index e5d8c930393e06..6d0bbcdb55ca43 100644 --- a/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -9,7 +9,7 @@ import { TRACE_ID, TRANSACTION_ID, } from '../../../../common/elasticsearch_fieldnames'; -import { rangeQuery } from '../../../../../observability/server'; +import { rangeQuery, termQuery } from '../../../../../observability/server'; import { Setup } from '../../helpers/setup_request'; import { ProcessorEvent } from '../../../../common/processor_event'; import { asMutableArray } from '../../../../common/utils/as_mutable_array'; @@ -39,7 +39,7 @@ export async function getTransaction({ bool: { filter: asMutableArray([ { term: { [TRANSACTION_ID]: transactionId } }, - ...(traceId ? [{ term: { [TRACE_ID]: traceId } }] : []), + ...termQuery(TRACE_ID, traceId), ...(start && end ? rangeQuery(start, end) : []), ]), }, diff --git a/x-pack/plugins/apm/server/projections/services.ts b/x-pack/plugins/apm/server/projections/services.ts deleted file mode 100644 index 139c86acd5144b..00000000000000 --- a/x-pack/plugins/apm/server/projections/services.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { Setup } from '../../server/lib/helpers/setup_request'; -import { SERVICE_NAME } from '../../common/elasticsearch_fieldnames'; -import { rangeQuery, kqlQuery } from '../../../observability/server'; -import { ProcessorEvent } from '../../common/processor_event'; -import { getProcessorEventForTransactions } from '../lib/helpers/transactions'; - -export function getServicesProjection({ - kuery, - setup, - searchAggregatedTransactions, - start, - end, -}: { - kuery: string; - setup: Setup; - searchAggregatedTransactions: boolean; - start: number; - end: number; -}) { - return { - apm: { - events: [ - getProcessorEventForTransactions(searchAggregatedTransactions), - ProcessorEvent.metric as const, - ProcessorEvent.error as const, - ], - }, - body: { - size: 0, - query: { - bool: { - filter: [...rangeQuery(start, end), ...kqlQuery(kuery)], - }, - }, - aggs: { - services: { - terms: { - field: SERVICE_NAME, - }, - }, - }, - }, - }; -} diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 257aec216eb06c..3af829d59d3fdb 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -191,18 +191,9 @@ const serviceAgentRoute = createApmServerRoute({ const { serviceName } = params.path; const { start, end } = params.query; - const searchAggregatedTransactions = await getSearchAggregatedTransactions({ - apmEventClient: setup.apmEventClient, - config: setup.config, - start, - end, - kuery: '', - }); - return getServiceAgent({ serviceName, setup, - searchAggregatedTransactions, start, end, }); diff --git a/x-pack/plugins/apm/server/tutorial/index.ts b/x-pack/plugins/apm/server/tutorial/index.ts index 66ff8f5b2c92cc..f04b794091ff29 100644 --- a/x-pack/plugins/apm/server/tutorial/index.ts +++ b/x-pack/plugins/apm/server/tutorial/index.ts @@ -103,7 +103,6 @@ It allows you to monitor the performance of thousands of applications in real ti } ), euiIconType: 'apmApp', - eprPackageOverlap: 'apm', integrationBrowserCategories: ['web'], artifacts, customStatusCheckName: 'apm_fleet_server_status_check', diff --git a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts index 66cb95a4a210a7..1f447c7ed834e8 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/expression_types/embeddable.ts @@ -6,7 +6,7 @@ */ import { ExpressionTypeDefinition } from '../../../../../src/plugins/expressions'; -import { EmbeddableInput } from '../../../../../src/plugins/embeddable/common/'; +import { EmbeddableInput } from '../../types'; import { EmbeddableTypes } from './embeddable_types'; export const EmbeddableExpressionType = 'embeddable'; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts index 2cfdebafb70df4..d6d7a0f867849c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/browser/index.ts @@ -6,7 +6,6 @@ */ import { functions as commonFunctions } from '../common'; -import { functions as externalFunctions } from '../external'; import { location } from './location'; import { markdown } from './markdown'; import { urlparam } from './urlparam'; @@ -14,13 +13,4 @@ import { escount } from './escount'; import { esdocs } from './esdocs'; import { essql } from './essql'; -export const functions = [ - location, - markdown, - urlparam, - escount, - esdocs, - essql, - ...commonFunctions, - ...externalFunctions, -]; +export const functions = [location, markdown, urlparam, escount, esdocs, essql, ...commonFunctions]; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts new file mode 100644 index 00000000000000..001fb0e3f62e3a --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { embeddableFunctionFactory } from './embeddable'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; +import { ExpressionValueFilter } from '../../../types'; +import { encode } from '../../../common/lib/embeddable_dataurl'; +import { InitializeArguments } from '.'; + +const filterContext: ExpressionValueFilter = { + type: 'filter', + and: [ + { + type: 'filter', + and: [], + value: 'filter-value', + column: 'filter-column', + filterType: 'exactly', + }, + { + type: 'filter', + and: [], + column: 'time-column', + filterType: 'time', + from: '2019-06-04T04:00:00.000Z', + to: '2019-06-05T04:00:00.000Z', + }, + ], +}; + +describe('embeddable', () => { + const fn = embeddableFunctionFactory({} as InitializeArguments)().fn; + const config = { + id: 'some-id', + timerange: { from: '15m', to: 'now' }, + title: 'test embeddable', + }; + + const args = { + config: encode(config), + type: 'visualization', + }; + + it('accepts null context', () => { + const expression = fn(null, args, {} as any); + + expect(expression.input.filters).toEqual([]); + }); + + it('accepts filter context', () => { + const expression = fn(filterContext, args, {} as any); + const embeddableFilters = getQueryFilters(filterContext.and); + + expect(expression.input.filters).toEqual(embeddableFilters); + }); +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts new file mode 100644 index 00000000000000..7ef8f0a09eb907 --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/embeddable.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; +import { ExpressionValueFilter, EmbeddableInput } from '../../../types'; +import { EmbeddableExpressionType, EmbeddableExpression } from '../../expression_types'; +import { getFunctionHelp } from '../../../i18n'; +import { SavedObjectReference } from '../../../../../../src/core/types'; +import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; +import { decode, encode } from '../../../common/lib/embeddable_dataurl'; +import { InitializeArguments } from '.'; + +export interface Arguments { + config: string; + type: string; +} + +const defaultTimeRange = { + from: 'now-15m', + to: 'now', +}; + +const baseEmbeddableInput = { + timeRange: defaultTimeRange, + disableTriggers: true, + renderMode: 'noInteractivity', +}; + +type Return = EmbeddableExpression; + +type EmbeddableFunction = ExpressionFunctionDefinition< + 'embeddable', + ExpressionValueFilter | null, + Arguments, + Return +>; + +export function embeddableFunctionFactory({ + embeddablePersistableStateService, +}: InitializeArguments): () => EmbeddableFunction { + return function embeddable(): EmbeddableFunction { + const { help, args: argHelp } = getFunctionHelp().embeddable; + + return { + name: 'embeddable', + help, + args: { + config: { + aliases: ['_'], + types: ['string'], + required: true, + help: argHelp.config, + }, + type: { + types: ['string'], + required: true, + help: argHelp.type, + }, + }, + context: { + types: ['filter'], + }, + type: EmbeddableExpressionType, + fn: (input, args) => { + const filters = input ? input.and : []; + + const embeddableInput = decode(args.config) as EmbeddableInput; + + return { + type: EmbeddableExpressionType, + input: { + ...baseEmbeddableInput, + ...embeddableInput, + filters: getQueryFilters(filters), + }, + generatedAt: Date.now(), + embeddableType: args.type, + }; + }, + + extract(state) { + const input = decode(state.config[0] as string); + + // extracts references for by-reference embeddables + if (input.savedObjectId) { + const refName = 'embeddable.savedObjectId'; + + const references: SavedObjectReference[] = [ + { + name: refName, + type: state.type[0] as string, + id: input.savedObjectId as string, + }, + ]; + + return { + state, + references, + }; + } + + // extracts references for by-value embeddables + const { state: extractedState, references: extractedReferences } = + embeddablePersistableStateService.extract({ + ...input, + type: state.type[0], + }); + + const { type, ...extractedInput } = extractedState; + + return { + state: { ...state, config: [encode(extractedInput)], type: [type] }, + references: extractedReferences, + }; + }, + + inject(state, references) { + const input = decode(state.config[0] as string); + const savedObjectReference = references.find( + (ref) => ref.name === 'embeddable.savedObjectId' + ); + + // injects saved object id for by-references embeddable + if (savedObjectReference) { + input.savedObjectId = savedObjectReference.id; + state.config[0] = encode(input); + state.type[0] = savedObjectReference.type; + } else { + // injects references for by-value embeddables + const { type, ...injectedInput } = embeddablePersistableStateService.inject( + { ...input, type: state.type[0] }, + references + ); + state.config[0] = encode(injectedInput); + state.type[0] = type; + } + return state; + }, + }; + }; +} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts index 407a0e2ebfe05a..1d69e181b5fd9d 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/index.ts @@ -5,9 +5,26 @@ * 2.0. */ +import { EmbeddableStart } from 'src/plugins/embeddable/public'; +import { embeddableFunctionFactory } from './embeddable'; import { savedLens } from './saved_lens'; import { savedMap } from './saved_map'; import { savedSearch } from './saved_search'; import { savedVisualization } from './saved_visualization'; -export const functions = [savedLens, savedMap, savedVisualization, savedSearch]; +export interface InitializeArguments { + embeddablePersistableStateService: { + extract: EmbeddableStart['extract']; + inject: EmbeddableStart['inject']; + }; +} + +export function initFunctions(initialize: InitializeArguments) { + return [ + embeddableFunctionFactory(initialize), + savedLens, + savedMap, + savedSearch, + savedVisualization, + ]; +} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts index 082a69a874cae2..67947691f7757c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_lens.ts @@ -9,9 +9,8 @@ import { ExpressionFunctionDefinition } from 'src/plugins/expressions/common'; import { PaletteOutput } from 'src/plugins/charts/common'; import { Filter as DataFilter } from '@kbn/es-query'; import { TimeRange } from 'src/plugins/data/common'; -import { EmbeddableInput } from 'src/plugins/embeddable/common'; import { getQueryFilters } from '../../../common/lib/build_embeddable_filters'; -import { ExpressionValueFilter, TimeRange as TimeRangeArg } from '../../../types'; +import { ExpressionValueFilter, EmbeddableInput, TimeRange as TimeRangeArg } from '../../../types'; import { EmbeddableTypes, EmbeddableExpressionType, @@ -27,7 +26,7 @@ interface Arguments { } export type SavedLensInput = EmbeddableInput & { - id: string; + savedObjectId: string; timeRange?: TimeRange; filters: DataFilter[]; palette?: PaletteOutput; @@ -73,18 +72,19 @@ export function savedLens(): ExpressionFunctionDefinition< }, }, type: EmbeddableExpressionType, - fn: (input, args) => { + fn: (input, { id, timerange, title, palette }) => { const filters = input ? input.and : []; return { type: EmbeddableExpressionType, input: { - id: args.id, + id, + savedObjectId: id, filters: getQueryFilters(filters), - timeRange: args.timerange || defaultTimeRange, - title: args.title === null ? undefined : args.title, + timeRange: timerange || defaultTimeRange, + title: title === null ? undefined : title, disableTriggers: true, - palette: args.palette, + palette, }, embeddableType: EmbeddableTypes.lens, generatedAt: Date.now(), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts index 538ed3f9198239..a7471c755155c8 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_map.ts @@ -30,7 +30,7 @@ const defaultTimeRange = { to: 'now', }; -type Output = EmbeddableExpression; +type Output = EmbeddableExpression; export function savedMap(): ExpressionFunctionDefinition< 'savedMap', @@ -85,8 +85,9 @@ export function savedMap(): ExpressionFunctionDefinition< return { type: EmbeddableExpressionType, input: { - attributes: { title: '' }, id: args.id, + attributes: { title: '' }, + savedObjectId: args.id, filters: getQueryFilters(filters), timeRange: args.timerange || defaultTimeRange, refreshConfig: { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts index 5c0442b43250c8..31e3fb2a8c5643 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/functions/external/saved_visualization.ts @@ -25,7 +25,7 @@ interface Arguments { title: string | null; } -type Output = EmbeddableExpression; +type Output = EmbeddableExpression; const defaultTimeRange = { from: 'now-15m', @@ -94,6 +94,7 @@ export function savedVisualization(): ExpressionFunctionDefinition< type: EmbeddableExpressionType, input: { id, + savedObjectId: id, disableTriggers: true, timeRange: timerange || defaultTimeRange, filters: getQueryFilters(filters), diff --git a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts index 91c573fc4148ba..591795637aebea 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/plugin.ts @@ -7,12 +7,14 @@ import { CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { ChartsPluginStart } from 'src/plugins/charts/public'; +import { PresentationUtilPluginStart } from 'src/plugins/presentation_util/public'; import { CanvasSetup } from '../public'; import { EmbeddableStart } from '../../../../src/plugins/embeddable/public'; import { UiActionsStart } from '../../../../src/plugins/ui_actions/public'; import { Start as InspectorStart } from '../../../../src/plugins/inspector/public'; import { functions } from './functions/browser'; +import { initFunctions } from './functions/external'; import { typeFunctions } from './expression_types'; import { renderFunctions, renderFunctionFactories } from './renderers'; @@ -25,6 +27,7 @@ export interface StartDeps { uiActions: UiActionsStart; inspector: InspectorStart; charts: ChartsPluginStart; + presentationUtil: PresentationUtilPluginStart; } export type SetupInitializer = (core: CoreSetup, plugins: SetupDeps) => T; @@ -39,6 +42,13 @@ export class CanvasSrcPlugin implements Plugin plugins.canvas.addRenderers(renderFunctions); core.getStartServices().then(([coreStart, depsStart]) => { + const externalFunctions = initFunctions({ + embeddablePersistableStateService: { + extract: depsStart.embeddable.extract, + inject: depsStart.embeddable.inject, + }, + }); + plugins.canvas.addFunctions(externalFunctions); plugins.canvas.addRenderers( renderFunctionFactories.map((factory: any) => factory(coreStart, depsStart)) ); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx index 73e839433c25e0..953746c2808406 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable.tsx @@ -13,16 +13,17 @@ import { IEmbeddable, EmbeddableFactory, EmbeddableFactoryNotFoundError, + isErrorEmbeddable, } from '../../../../../../src/plugins/embeddable/public'; import { EmbeddableExpression } from '../../expression_types/embeddable'; import { RendererStrings } from '../../../i18n'; import { embeddableInputToExpression } from './embeddable_input_to_expression'; -import { EmbeddableInput } from '../../expression_types'; -import { RendererFactory } from '../../../types'; +import { RendererFactory, EmbeddableInput } from '../../../types'; import { CANVAS_EMBEDDABLE_CLASSNAME } from '../../../common/lib'; const { embeddable: strings } = RendererStrings; +// registry of references to embeddables on the workpad const embeddablesRegistry: { [key: string]: IEmbeddable | Promise; } = {}; @@ -30,11 +31,11 @@ const embeddablesRegistry: { const renderEmbeddableFactory = (core: CoreStart, plugins: StartDeps) => { const I18nContext = core.i18n.Context; - return (embeddableObject: IEmbeddable, domNode: HTMLElement) => { + return (embeddableObject: IEmbeddable) => { return (
@@ -56,6 +57,9 @@ export const embeddableRendererFactory = ( reuseDomNode: true, render: async (domNode, { input, embeddableType }, handlers) => { const uniqueId = handlers.getElementId(); + const isByValueEnabled = plugins.presentationUtil.labsService.isProjectEnabled( + 'labs:canvas:byValueEmbeddable' + ); if (!embeddablesRegistry[uniqueId]) { const factory = Array.from(plugins.embeddable.getEmbeddableFactories()).find( @@ -67,15 +71,27 @@ export const embeddableRendererFactory = ( throw new EmbeddableFactoryNotFoundError(embeddableType); } - const embeddablePromise = factory - .createFromSavedObject(input.id, input) - .then((embeddable) => { - embeddablesRegistry[uniqueId] = embeddable; - return embeddable; - }); - embeddablesRegistry[uniqueId] = embeddablePromise; - - const embeddableObject = await (async () => embeddablePromise)(); + const embeddableInput = { ...input, id: uniqueId }; + + const embeddablePromise = input.savedObjectId + ? factory + .createFromSavedObject(input.savedObjectId, embeddableInput) + .then((embeddable) => { + // stores embeddable in registrey + embeddablesRegistry[uniqueId] = embeddable; + return embeddable; + }) + : factory.create(embeddableInput).then((embeddable) => { + if (!embeddable || isErrorEmbeddable(embeddable)) { + return; + } + // stores embeddable in registry + embeddablesRegistry[uniqueId] = embeddable as IEmbeddable; + return embeddable; + }); + embeddablesRegistry[uniqueId] = embeddablePromise as Promise; + + const embeddableObject = (await (async () => embeddablePromise)()) as IEmbeddable; const palettes = await plugins.charts.palettes.getPalettes(); @@ -86,7 +102,8 @@ export const embeddableRendererFactory = ( const updatedExpression = embeddableInputToExpression( updatedInput, embeddableType, - palettes + palettes, + isByValueEnabled ); if (updatedExpression) { @@ -94,15 +111,7 @@ export const embeddableRendererFactory = ( } }); - ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () => - handlers.done() - ); - - handlers.onResize(() => { - ReactDOM.render(renderEmbeddable(embeddableObject, domNode), domNode, () => - handlers.done() - ); - }); + ReactDOM.render(renderEmbeddable(embeddableObject), domNode, () => handlers.done()); handlers.onDestroy(() => { subscription.unsubscribe(); @@ -115,6 +124,7 @@ export const embeddableRendererFactory = ( } else { const embeddable = embeddablesRegistry[uniqueId]; + // updating embeddable input with changes made to expression or filters if ('updateInput' in embeddable) { embeddable.updateInput(input); embeddable.reload(); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts index 41cefad6a470fa..80830eac240212 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/embeddable_input_to_expression.ts @@ -10,6 +10,7 @@ import { EmbeddableTypes, EmbeddableInput } from '../../expression_types'; import { toExpression as mapToExpression } from './input_type_to_expression/map'; import { toExpression as visualizationToExpression } from './input_type_to_expression/visualization'; import { toExpression as lensToExpression } from './input_type_to_expression/lens'; +import { toExpression as genericToExpression } from './input_type_to_expression/embeddable'; export const inputToExpressionTypeMap = { [EmbeddableTypes.map]: mapToExpression, @@ -23,8 +24,13 @@ export const inputToExpressionTypeMap = { export function embeddableInputToExpression( input: EmbeddableInput, embeddableType: string, - palettes: PaletteRegistry + palettes: PaletteRegistry, + useGenericEmbeddable?: boolean ): string | undefined { + if (useGenericEmbeddable) { + return genericToExpression(input, embeddableType); + } + if (inputToExpressionTypeMap[embeddableType]) { return inputToExpressionTypeMap[embeddableType](input as any, palettes); } diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.test.ts new file mode 100644 index 00000000000000..4b78acec8750ac --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.test.ts @@ -0,0 +1,128 @@ +/* + * 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 { toExpression } from './embeddable'; +import { EmbeddableInput } from '../../../../types'; +import { decode } from '../../../../common/lib/embeddable_dataurl'; +import { fromExpression } from '@kbn/interpreter/common'; + +describe('toExpression', () => { + describe('by-reference embeddable input', () => { + const baseEmbeddableInput = { + id: 'elementId', + savedObjectId: 'embeddableId', + filters: [], + }; + + it('converts to an embeddable expression', () => { + const input: EmbeddableInput = baseEmbeddableInput; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + expect(ast.type).toBe('expression'); + expect(ast.chain[0].function).toBe('embeddable'); + expect(ast.chain[0].arguments.type[0]).toBe('visualization'); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config.savedObjectId).toStrictEqual(input.savedObjectId); + }); + + it('includes optional input values', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: 'title', + timeRange: { + from: 'now-1h', + to: 'now', + }, + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + expect(config).toHaveProperty('timeRange'); + expect(config.timeRange).toHaveProperty('from', input.timeRange?.from); + expect(config.timeRange).toHaveProperty('to', input.timeRange?.to); + }); + + it('includes empty panel title', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: '', + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + }); + }); + + describe('by-value embeddable input', () => { + const baseEmbeddableInput = { + id: 'elementId', + disableTriggers: true, + filters: [], + }; + it('converts to an embeddable expression', () => { + const input: EmbeddableInput = baseEmbeddableInput; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + expect(ast.type).toBe('expression'); + expect(ast.chain[0].function).toBe('embeddable'); + expect(ast.chain[0].arguments.type[0]).toBe('visualization'); + + const config = decode(ast.chain[0].arguments.config[0] as string); + expect(config.filters).toStrictEqual(input.filters); + expect(config.disableTriggers).toStrictEqual(input.disableTriggers); + }); + + it('includes optional input values', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: 'title', + timeRange: { + from: 'now-1h', + to: 'now', + }, + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + expect(config).toHaveProperty('timeRange'); + expect(config.timeRange).toHaveProperty('from', input.timeRange?.from); + expect(config.timeRange).toHaveProperty('to', input.timeRange?.to); + }); + + it('includes empty panel title', () => { + const input: EmbeddableInput = { + ...baseEmbeddableInput, + title: '', + }; + + const expression = toExpression(input, 'visualization'); + const ast = fromExpression(expression); + + const config = decode(ast.chain[0].arguments.config[0] as string); + + expect(config).toHaveProperty('title', input.title); + }); + }); +}); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.ts new file mode 100644 index 00000000000000..94d86f6640be1d --- /dev/null +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/embeddable.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { encode } from '../../../../common/lib/embeddable_dataurl'; +import { EmbeddableInput } from '../../../expression_types'; + +export function toExpression(input: EmbeddableInput, embeddableType: string): string { + return `embeddable config="${encode(input)}" type="${embeddableType}"`; +} diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts index 24da7238bcee94..224cdfba389d77 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.test.ts @@ -11,7 +11,8 @@ import { fromExpression, Ast } from '@kbn/interpreter/common'; import { chartPluginMock } from 'src/plugins/charts/public/mocks'; const baseEmbeddableInput = { - id: 'embeddableId', + id: 'elementId', + savedObjectId: 'embeddableId', filters: [], }; @@ -27,7 +28,7 @@ describe('toExpression', () => { expect(ast.type).toBe('expression'); expect(ast.chain[0].function).toBe('savedLens'); - expect(ast.chain[0].arguments.id).toStrictEqual([input.id]); + expect(ast.chain[0].arguments.id).toStrictEqual([input.savedObjectId]); expect(ast.chain[0].arguments).not.toHaveProperty('title'); expect(ast.chain[0].arguments).not.toHaveProperty('timerange'); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts index 35e106f234fa4e..5a13b73b3fe746 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/lens.ts @@ -14,7 +14,7 @@ export function toExpression(input: SavedLensInput, palettes: PaletteRegistry): expressionParts.push('savedLens'); - expressionParts.push(`id="${input.id}"`); + expressionParts.push(`id="${input.savedObjectId}"`); if (input.title !== undefined) { expressionParts.push(`title="${input.title}"`); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts index 804d0d849cc7f7..af7b40a9b283d9 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.test.ts @@ -6,12 +6,12 @@ */ import { toExpression } from './map'; -import { MapEmbeddableInput } from '../../../../../../plugins/maps/public/embeddable'; import { fromExpression, Ast } from '@kbn/interpreter/common'; const baseSavedMapInput = { + id: 'elementId', attributes: { title: '' }, - id: 'embeddableId', + savedObjectId: 'embeddableId', filters: [], isLayerTOCOpen: false, refreshConfig: { @@ -23,7 +23,7 @@ const baseSavedMapInput = { describe('toExpression', () => { it('converts to a savedMap expression', () => { - const input: MapEmbeddableInput = { + const input = { ...baseSavedMapInput, }; @@ -33,7 +33,7 @@ describe('toExpression', () => { expect(ast.type).toBe('expression'); expect(ast.chain[0].function).toBe('savedMap'); - expect(ast.chain[0].arguments.id).toStrictEqual([input.id]); + expect(ast.chain[0].arguments.id).toStrictEqual([input.savedObjectId]); expect(ast.chain[0].arguments).not.toHaveProperty('title'); expect(ast.chain[0].arguments).not.toHaveProperty('center'); @@ -41,7 +41,7 @@ describe('toExpression', () => { }); it('includes optional input values', () => { - const input: MapEmbeddableInput = { + const input = { ...baseSavedMapInput, mapCenter: { lat: 1, @@ -73,7 +73,7 @@ describe('toExpression', () => { }); it('includes empty panel title', () => { - const input: MapEmbeddableInput = { + const input = { ...baseSavedMapInput, title: '', }; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts index 3fd6a68a327c60..03746f38b4696c 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/map.ts @@ -5,13 +5,14 @@ * 2.0. */ -import { MapEmbeddableInput } from '../../../../../../plugins/maps/public/embeddable'; +import { MapEmbeddableInput } from '../../../../../../plugins/maps/public'; -export function toExpression(input: MapEmbeddableInput): string { +export function toExpression(input: MapEmbeddableInput & { savedObjectId: string }): string { const expressionParts = [] as string[]; expressionParts.push('savedMap'); - expressionParts.push(`id="${input.id}"`); + + expressionParts.push(`id="${input.savedObjectId}"`); if (input.title !== undefined) { expressionParts.push(`title="${input.title}"`); diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts index c5106b9a102b40..4c61a130f3c95f 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.test.ts @@ -9,7 +9,8 @@ import { toExpression } from './visualization'; import { fromExpression, Ast } from '@kbn/interpreter/common'; const baseInput = { - id: 'embeddableId', + id: 'elementId', + savedObjectId: 'embeddableId', }; describe('toExpression', () => { @@ -24,7 +25,7 @@ describe('toExpression', () => { expect(ast.type).toBe('expression'); expect(ast.chain[0].function).toBe('savedVisualization'); - expect(ast.chain[0].arguments.id).toStrictEqual([input.id]); + expect(ast.chain[0].arguments.id).toStrictEqual([input.savedObjectId]); }); it('includes timerange if given', () => { diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts index bcb73b2081fee5..364d7cd0755db4 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/embeddable/input_type_to_expression/visualization.ts @@ -7,11 +7,11 @@ import { VisualizeInput } from 'src/plugins/visualizations/public'; -export function toExpression(input: VisualizeInput): string { +export function toExpression(input: VisualizeInput & { savedObjectId: string }): string { const expressionParts = [] as string[]; expressionParts.push('savedVisualization'); - expressionParts.push(`id="${input.id}"`); + expressionParts.push(`id="${input.savedObjectId}"`); if (input.title !== undefined) { expressionParts.push(`title="${input.title}"`); diff --git a/x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts b/x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts new file mode 100644 index 00000000000000..e76dedfe63b14a --- /dev/null +++ b/x-pack/plugins/canvas/common/lib/embeddable_dataurl.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { EmbeddableInput } from '../../types'; + +export const encode = (input: Partial) => + Buffer.from(JSON.stringify(input)).toString('base64'); +export const decode = (serializedInput: string) => + JSON.parse(Buffer.from(serializedInput, 'base64').toString()); diff --git a/x-pack/plugins/canvas/i18n/functions/dict/embeddable.ts b/x-pack/plugins/canvas/i18n/functions/dict/embeddable.ts new file mode 100644 index 00000000000000..279f58799e8c00 --- /dev/null +++ b/x-pack/plugins/canvas/i18n/functions/dict/embeddable.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { embeddableFunctionFactory } from '../../../canvas_plugin_src/functions/external/embeddable'; +import { FunctionHelp } from '../function_help'; +import { FunctionFactory } from '../../../types'; + +export const help: FunctionHelp>> = { + help: i18n.translate('xpack.canvas.functions.embeddableHelpText', { + defaultMessage: `Returns an embeddable with the provided configuration`, + }), + args: { + config: i18n.translate('xpack.canvas.functions.embeddable.args.idHelpText', { + defaultMessage: `The base64 encoded embeddable input object`, + }), + type: i18n.translate('xpack.canvas.functions.embeddable.args.typeHelpText', { + defaultMessage: `The embeddable type`, + }), + }, +}; diff --git a/x-pack/plugins/canvas/i18n/functions/function_help.ts b/x-pack/plugins/canvas/i18n/functions/function_help.ts index 5eae785fefa2ea..520d32af1c272c 100644 --- a/x-pack/plugins/canvas/i18n/functions/function_help.ts +++ b/x-pack/plugins/canvas/i18n/functions/function_help.ts @@ -27,6 +27,7 @@ import { help as demodata } from './dict/demodata'; import { help as doFn } from './dict/do'; import { help as dropdownControl } from './dict/dropdown_control'; import { help as eq } from './dict/eq'; +import { help as embeddable } from './dict/embeddable'; import { help as escount } from './dict/escount'; import { help as esdocs } from './dict/esdocs'; import { help as essql } from './dict/essql'; @@ -182,6 +183,7 @@ export const getFunctionHelp = (): FunctionHelpDict => ({ do: doFn, dropdownControl, eq, + embeddable, escount, esdocs, essql, diff --git a/x-pack/plugins/canvas/kibana.json b/x-pack/plugins/canvas/kibana.json index 9c4d1b2179d821..2fd312502a3c74 100644 --- a/x-pack/plugins/canvas/kibana.json +++ b/x-pack/plugins/canvas/kibana.json @@ -25,6 +25,7 @@ "features", "inspector", "presentationUtil", + "visualizations", "uiActions", "share" ], diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx index bf731876bf8c88..57f52fcf21f0f9 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.component.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { FC } from 'react'; +import React, { FC, useCallback } from 'react'; import { EuiFlyout, EuiFlyoutHeader, EuiFlyoutBody, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -27,38 +27,44 @@ const strings = { }; export interface Props { onClose: () => void; - onSelect: (id: string, embeddableType: string) => void; + onSelect: (id: string, embeddableType: string, isByValueEnabled?: boolean) => void; availableEmbeddables: string[]; + isByValueEnabled?: boolean; } -export const AddEmbeddableFlyout: FC = ({ onSelect, availableEmbeddables, onClose }) => { +export const AddEmbeddableFlyout: FC = ({ + onSelect, + availableEmbeddables, + onClose, + isByValueEnabled, +}) => { const embeddablesService = useEmbeddablesService(); const platformService = usePlatformService(); const { getEmbeddableFactories } = embeddablesService; const { getSavedObjects, getUISettings } = platformService; - const onAddPanel = (id: string, savedObjectType: string, name: string) => { - const embeddableFactories = getEmbeddableFactories(); + const onAddPanel = useCallback( + (id: string, savedObjectType: string) => { + const embeddableFactories = getEmbeddableFactories(); + // Find the embeddable type from the saved object type + const found = Array.from(embeddableFactories).find((embeddableFactory) => { + return Boolean( + embeddableFactory.savedObjectMetaData && + embeddableFactory.savedObjectMetaData.type === savedObjectType + ); + }); - // Find the embeddable type from the saved object type - const found = Array.from(embeddableFactories).find((embeddableFactory) => { - return Boolean( - embeddableFactory.savedObjectMetaData && - embeddableFactory.savedObjectMetaData.type === savedObjectType - ); - }); - - const foundEmbeddableType = found ? found.type : 'unknown'; + const foundEmbeddableType = found ? found.type : 'unknown'; - onSelect(id, foundEmbeddableType); - }; + onSelect(id, foundEmbeddableType, isByValueEnabled); + }, + [isByValueEnabled, getEmbeddableFactories, onSelect] + ); const embeddableFactories = getEmbeddableFactories(); const availableSavedObjects = Array.from(embeddableFactories) - .filter((factory) => { - return availableEmbeddables.includes(factory.type); - }) + .filter((factory) => isByValueEnabled || availableEmbeddables.includes(factory.type)) .map((factory) => factory.savedObjectMetaData) .filter>(function ( maybeSavedObjectMetaData diff --git a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx index 770a4cac606b0b..4dc8d963932d8f 100644 --- a/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx +++ b/x-pack/plugins/canvas/public/components/embeddable_flyout/flyout.tsx @@ -8,12 +8,14 @@ import React, { useMemo, useEffect, useCallback } from 'react'; import { createPortal } from 'react-dom'; import { useSelector, useDispatch } from 'react-redux'; +import { encode } from '../../../common/lib/embeddable_dataurl'; import { AddEmbeddableFlyout as Component, Props as ComponentProps } from './flyout.component'; // @ts-expect-error untyped local import { addElement } from '../../state/actions/elements'; import { getSelectedPage } from '../../state/selectors/workpad'; import { EmbeddableTypes } from '../../../canvas_plugin_src/expression_types/embeddable'; import { State } from '../../../types'; +import { useLabsService } from '../../services'; const allowedEmbeddables = { [EmbeddableTypes.map]: (id: string) => { @@ -65,6 +67,9 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ availableEmbeddables, ...restProps }) => { + const labsService = useLabsService(); + const isByValueEnabled = labsService.isProjectEnabled('labs:canvas:byValueEmbeddable'); + const dispatch = useDispatch(); const pageId = useSelector((state) => getSelectedPage(state)); @@ -74,18 +79,27 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ ); const onSelect = useCallback( - (id: string, type: string) => { + (id: string, type: string): void => { const partialElement = { expression: `markdown "Could not find embeddable for type ${type}" | render`, }; - if (allowedEmbeddables[type]) { + + // If by-value is enabled, we'll handle both by-reference and by-value embeddables + // with the new generic `embeddable` function. + // Otherwise we fallback to the embeddable type specific expressions. + if (isByValueEnabled) { + const config = encode({ savedObjectId: id }); + partialElement.expression = `embeddable config="${config}" + type="${type}" +| render`; + } else if (allowedEmbeddables[type]) { partialElement.expression = allowedEmbeddables[type](id); } addEmbeddable(pageId, partialElement); restProps.onClose(); }, - [addEmbeddable, pageId, restProps] + [addEmbeddable, pageId, restProps, isByValueEnabled] ); return ( @@ -93,6 +107,7 @@ export const AddEmbeddablePanel: React.FunctionComponent = ({ {...restProps} availableEmbeddables={availableEmbeddables || []} onSelect={onSelect} + isByValueEnabled={isByValueEnabled} /> ); }; diff --git a/x-pack/plugins/canvas/public/components/hooks/workpad/index.tsx b/x-pack/plugins/canvas/public/components/hooks/workpad/index.tsx index 50d527036560ad..ffd5b095b12e52 100644 --- a/x-pack/plugins/canvas/public/components/hooks/workpad/index.tsx +++ b/x-pack/plugins/canvas/public/components/hooks/workpad/index.tsx @@ -6,3 +6,5 @@ */ export { useDownloadWorkpad, useDownloadRenderedWorkpad } from './use_download_workpad'; + +export { useIncomingEmbeddable } from './use_incoming_embeddable'; diff --git a/x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts b/x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts new file mode 100644 index 00000000000000..2f8e2503ea57ef --- /dev/null +++ b/x-pack/plugins/canvas/public/components/hooks/workpad/use_incoming_embeddable.ts @@ -0,0 +1,86 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect } from 'react'; +import { useDispatch } from 'react-redux'; +import { fromExpression } from '@kbn/interpreter/common'; +import { CANVAS_APP } from '../../../../common/lib'; +import { decode, encode } from '../../../../common/lib/embeddable_dataurl'; +import { CanvasElement, CanvasPage } from '../../../../types'; +import { useEmbeddablesService, useLabsService } from '../../../services'; +// @ts-expect-error unconverted file +import { addElement } from '../../../state/actions/elements'; +// @ts-expect-error unconverted file +import { selectToplevelNodes } from '../../../state/actions/transient'; + +import { + updateEmbeddableExpression, + fetchEmbeddableRenderable, +} from '../../../state/actions/embeddable'; +import { clearValue } from '../../../state/actions/resolved_args'; + +export const useIncomingEmbeddable = (selectedPage: CanvasPage) => { + const embeddablesService = useEmbeddablesService(); + const labsService = useLabsService(); + const dispatch = useDispatch(); + const isByValueEnabled = labsService.isProjectEnabled('labs:canvas:byValueEmbeddable'); + const stateTransferService = embeddablesService.getStateTransfer(); + + // fetch incoming embeddable from state transfer service. + const incomingEmbeddable = stateTransferService.getIncomingEmbeddablePackage(CANVAS_APP, true); + + useEffect(() => { + if (isByValueEnabled && incomingEmbeddable) { + const { embeddableId, input: incomingInput, type } = incomingEmbeddable; + + // retrieve existing element + const originalElement = selectedPage.elements.find( + ({ id }: CanvasElement) => id === embeddableId + ); + + if (originalElement) { + const originalAst = fromExpression(originalElement!.expression); + + const functionIndex = originalAst.chain.findIndex( + ({ function: fn }) => fn === 'embeddable' + ); + + const originalInput = decode( + originalAst.chain[functionIndex].arguments.config[0] as string + ); + + // clear out resolved arg for old embeddable + const argumentPath = [embeddableId, 'expressionRenderable']; + dispatch(clearValue({ path: argumentPath })); + + const updatedInput = { ...originalInput, ...incomingInput }; + + const expression = `embeddable config="${encode(updatedInput)}" + type="${type}" +| render`; + + dispatch( + updateEmbeddableExpression({ + elementId: originalElement.id, + embeddableExpression: expression, + }) + ); + + // update resolved args + dispatch(fetchEmbeddableRenderable(originalElement.id)); + + // select new embeddable element + dispatch(selectToplevelNodes([embeddableId])); + } else { + const expression = `embeddable config="${encode(incomingInput)}" + type="${type}" +| render`; + dispatch(addElement(selectedPage.id, { expression })); + } + } + }, [dispatch, selectedPage, incomingEmbeddable, isByValueEnabled]); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad/workpad.tsx b/x-pack/plugins/canvas/public/components/workpad/workpad.tsx index 622c885b6ef281..7cc077203c7372 100644 --- a/x-pack/plugins/canvas/public/components/workpad/workpad.tsx +++ b/x-pack/plugins/canvas/public/components/workpad/workpad.tsx @@ -27,6 +27,7 @@ import { WorkpadRoutingContext } from '../../routes/workpad'; import { usePlatformService } from '../../services'; import { Workpad as WorkpadComponent, Props } from './workpad.component'; import { State } from '../../../types'; +import { useIncomingEmbeddable } from '../hooks'; type ContainerProps = Pick; @@ -58,6 +59,9 @@ export const Workpad: FC = (props) => { }; }); + const selectedPage = propsFromState.pages[propsFromState.selectedPageNumber - 1]; + useIncomingEmbeddable(selectedPage); + const fetchAllRenderables = useCallback(() => { dispatch(fetchAllRenderablesAction()); }, [dispatch]); diff --git a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss index 4acdca10d61cc2..0ddd44ed8f9a81 100644 --- a/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss +++ b/x-pack/plugins/canvas/public/components/workpad_app/workpad_app.scss @@ -31,7 +31,7 @@ $canvasLayoutFontSize: $euiFontSizeS; .canvasLayout__stageHeader { flex-grow: 0; flex-basis: auto; - padding: $euiSizeS; + padding: $euiSizeS $euiSize; font-size: $canvasLayoutFontSize; border-bottom: $euiBorderThin; background: $euiColorLightestShade; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/__snapshots__/editor_menu.stories.storyshot b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/__snapshots__/editor_menu.stories.storyshot new file mode 100644 index 00000000000000..f4aab0e59e7ee6 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/__snapshots__/editor_menu.stories.storyshot @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots components/WorkpadHeader/EditorMenu dark mode 1`] = ` +
+
+ +
+
+`; + +exports[`Storyshots components/WorkpadHeader/EditorMenu default 1`] = ` +
+
+ +
+
+`; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/editor_menu.stories.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/editor_menu.stories.tsx new file mode 100644 index 00000000000000..01048bc0af3010 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/__stories__/editor_menu.stories.tsx @@ -0,0 +1,107 @@ +/* + * 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 { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import React from 'react'; +import { EmbeddableFactoryDefinition, IEmbeddable } from 'src/plugins/embeddable/public'; +import { BaseVisType, VisTypeAlias } from 'src/plugins/visualizations/public'; +import { EditorMenu } from '../editor_menu.component'; + +const testFactories: EmbeddableFactoryDefinition[] = [ + { + type: 'ml_anomaly_swimlane', + getDisplayName: () => 'Anomaly swimlane', + getIconType: () => '', + getDescription: () => 'Description for anomaly swimlane', + isEditable: () => Promise.resolve(true), + create: () => Promise.resolve({ id: 'swimlane_embeddable' } as IEmbeddable), + grouping: [ + { + id: 'ml', + getDisplayName: () => 'machine learning', + getIconType: () => 'machineLearningApp', + }, + ], + }, + { + type: 'ml_anomaly_chart', + getDisplayName: () => 'Anomaly chart', + getIconType: () => '', + getDescription: () => 'Description for anomaly chart', + isEditable: () => Promise.resolve(true), + create: () => Promise.resolve({ id: 'anomaly_chart_embeddable' } as IEmbeddable), + grouping: [ + { + id: 'ml', + getDisplayName: () => 'machine learning', + getIconType: () => 'machineLearningApp', + }, + ], + }, + { + type: 'log_stream', + getDisplayName: () => 'Log stream', + getIconType: () => '', + getDescription: () => 'Description for log stream', + isEditable: () => Promise.resolve(true), + create: () => Promise.resolve({ id: 'anomaly_chart_embeddable' } as IEmbeddable), + }, +]; + +const testVisTypes: BaseVisType[] = [ + { title: 'TSVB', icon: '', description: 'Description of TSVB', name: 'tsvb' } as BaseVisType, + { + titleInWizard: 'Custom visualization', + title: 'Vega', + icon: '', + description: 'Description of Vega', + name: 'vega', + } as BaseVisType, +]; + +const testVisTypeAliases: VisTypeAlias[] = [ + { + title: 'Lens', + aliasApp: 'lens', + aliasPath: 'path/to/lens', + icon: 'lensApp', + name: 'lens', + description: 'Description of Lens app', + stage: 'production', + }, + { + title: 'Maps', + aliasApp: 'maps', + aliasPath: 'path/to/maps', + icon: 'gisApp', + name: 'maps', + description: 'Description of Maps app', + stage: 'production', + }, +]; + +storiesOf('components/WorkpadHeader/EditorMenu', module) + .add('default', () => ( + action('createNewVisType')} + createNewEmbeddable={() => action('createNewEmbeddable')} + /> + )) + .add('dark mode', () => ( + action('createNewVisType')} + createNewEmbeddable={() => action('createNewEmbeddable')} + /> + )); diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx new file mode 100644 index 00000000000000..e8f762f9731a19 --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.component.tsx @@ -0,0 +1,170 @@ +/* + * 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, { FC } from 'react'; +import { + EuiContextMenu, + EuiContextMenuPanelItemDescriptor, + EuiContextMenuItemIcon, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EmbeddableFactoryDefinition } from '../../../../../../../src/plugins/embeddable/public'; +import { BaseVisType, VisTypeAlias } from '../../../../../../../src/plugins/visualizations/public'; +import { SolutionToolbarPopover } from '../../../../../../../src/plugins/presentation_util/public'; + +const strings = { + getEditorMenuButtonLabel: () => + i18n.translate('xpack.canvas.solutionToolbar.editorMenuButtonLabel', { + defaultMessage: 'Select type', + }), +}; + +interface FactoryGroup { + id: string; + appName: string; + icon: EuiContextMenuItemIcon; + panelId: number; + factories: EmbeddableFactoryDefinition[]; +} + +interface Props { + factories: EmbeddableFactoryDefinition[]; + isDarkThemeEnabled?: boolean; + promotedVisTypes: BaseVisType[]; + visTypeAliases: VisTypeAlias[]; + createNewVisType: (visType?: BaseVisType | VisTypeAlias) => () => void; + createNewEmbeddable: (factory: EmbeddableFactoryDefinition) => () => void; +} + +export const EditorMenu: FC = ({ + factories, + isDarkThemeEnabled, + promotedVisTypes, + visTypeAliases, + createNewVisType, + createNewEmbeddable, +}: Props) => { + const factoryGroupMap: Record = {}; + const ungroupedFactories: EmbeddableFactoryDefinition[] = []; + + let panelCount = 1; + + // Maps factories with a group to create nested context menus for each group type + // and pushes ungrouped factories into a separate array + factories.forEach((factory: EmbeddableFactoryDefinition, index) => { + const { grouping } = factory; + + if (grouping) { + grouping.forEach((group) => { + if (factoryGroupMap[group.id]) { + factoryGroupMap[group.id].factories.push(factory); + } else { + factoryGroupMap[group.id] = { + id: group.id, + appName: group.getDisplayName ? group.getDisplayName({}) : group.id, + icon: (group.getIconType ? group.getIconType({}) : 'empty') as EuiContextMenuItemIcon, + factories: [factory], + panelId: panelCount, + }; + + panelCount++; + } + }); + } else { + ungroupedFactories.push(factory); + } + }); + + const getVisTypeMenuItem = (visType: BaseVisType): EuiContextMenuPanelItemDescriptor => { + const { name, title, titleInWizard, description, icon = 'empty' } = visType; + return { + name: titleInWizard || title, + icon: icon as string, + onClick: createNewVisType(visType), + 'data-test-subj': `visType-${name}`, + toolTipContent: description, + }; + }; + + const getVisTypeAliasMenuItem = ( + visTypeAlias: VisTypeAlias + ): EuiContextMenuPanelItemDescriptor => { + const { name, title, description, icon = 'empty' } = visTypeAlias; + + return { + name: title, + icon, + onClick: createNewVisType(visTypeAlias), + 'data-test-subj': `visType-${name}`, + toolTipContent: description, + }; + }; + + const getEmbeddableFactoryMenuItem = ( + factory: EmbeddableFactoryDefinition + ): EuiContextMenuPanelItemDescriptor => { + const icon = factory?.getIconType ? factory.getIconType() : 'empty'; + + const toolTipContent = factory?.getDescription ? factory.getDescription() : undefined; + + return { + name: factory.getDisplayName(), + icon, + toolTipContent, + onClick: createNewEmbeddable(factory), + 'data-test-subj': `createNew-${factory.type}`, + }; + }; + + const editorMenuPanels = [ + { + id: 0, + items: [ + ...visTypeAliases.map(getVisTypeAliasMenuItem), + ...Object.values(factoryGroupMap).map(({ id, appName, icon, panelId }) => ({ + name: appName, + icon, + panel: panelId, + 'data-test-subj': `canvasEditorMenu-${id}Group`, + })), + ...ungroupedFactories.map(getEmbeddableFactoryMenuItem), + ...promotedVisTypes.map(getVisTypeMenuItem), + ], + }, + ...Object.values(factoryGroupMap).map( + ({ appName, panelId, factories: groupFactories }: FactoryGroup) => ({ + id: panelId, + title: appName, + items: groupFactories.map(getEmbeddableFactoryMenuItem), + }) + ), + ]; + + return ( + + {() => ( + + )} + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx new file mode 100644 index 00000000000000..dad34e6983c5db --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/editor_menu.tsx @@ -0,0 +1,147 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC, useCallback } from 'react'; +import { useLocation } from 'react-router-dom'; +import { trackCanvasUiMetric, METRIC_TYPE } from '../../../../public/lib/ui_metric'; +import { + useEmbeddablesService, + usePlatformService, + useVisualizationsService, +} from '../../../services'; +import { + BaseVisType, + VisGroups, + VisTypeAlias, +} from '../../../../../../../src/plugins/visualizations/public'; +import { + EmbeddableFactoryDefinition, + EmbeddableInput, +} from '../../../../../../../src/plugins/embeddable/public'; +import { CANVAS_APP } from '../../../../common/lib'; +import { encode } from '../../../../common/lib/embeddable_dataurl'; +import { ElementSpec } from '../../../../types'; +import { EditorMenu as Component } from './editor_menu.component'; + +interface Props { + /** + * Handler for adding a selected element to the workpad + */ + addElement: (element: Partial) => void; +} + +export const EditorMenu: FC = ({ addElement }) => { + const embeddablesService = useEmbeddablesService(); + const { pathname, search } = useLocation(); + const platformService = usePlatformService(); + const stateTransferService = embeddablesService.getStateTransfer(); + const visualizationsService = useVisualizationsService(); + const IS_DARK_THEME = platformService.getUISetting('theme:darkMode'); + + const createNewVisType = useCallback( + (visType?: BaseVisType | VisTypeAlias) => () => { + let path = ''; + let appId = ''; + + if (visType) { + if (trackCanvasUiMetric) { + trackCanvasUiMetric(METRIC_TYPE.CLICK, `${visType.name}:create`); + } + + if ('aliasPath' in visType) { + appId = visType.aliasApp; + path = visType.aliasPath; + } else { + appId = 'visualize'; + path = `#/create?type=${encodeURIComponent(visType.name)}`; + } + } else { + appId = 'visualize'; + path = '#/create?'; + } + + stateTransferService.navigateToEditor(appId, { + path, + state: { + originatingApp: CANVAS_APP, + originatingPath: `#/${pathname}${search}`, + }, + }); + }, + [stateTransferService, pathname, search] + ); + + const createNewEmbeddable = useCallback( + (factory: EmbeddableFactoryDefinition) => async () => { + if (trackCanvasUiMetric) { + trackCanvasUiMetric(METRIC_TYPE.CLICK, factory.type); + } + let embeddableInput; + if (factory.getExplicitInput) { + embeddableInput = await factory.getExplicitInput(); + } else { + const newEmbeddable = await factory.create({} as EmbeddableInput); + embeddableInput = newEmbeddable?.getInput(); + } + + if (embeddableInput) { + const config = encode(embeddableInput); + const expression = `embeddable config="${config}" + type="${factory.type}" +| render`; + + addElement({ expression }); + } + }, + [addElement] + ); + + const getVisTypesByGroup = (group: VisGroups): BaseVisType[] => + visualizationsService + .getByGroup(group) + .sort(({ name: a }: BaseVisType | VisTypeAlias, { name: b }: BaseVisType | VisTypeAlias) => { + if (a < b) { + return -1; + } + if (a > b) { + return 1; + } + return 0; + }) + .filter(({ hidden }: BaseVisType) => !hidden); + + const visTypeAliases = visualizationsService + .getAliases() + .sort(({ promotion: a = false }: VisTypeAlias, { promotion: b = false }: VisTypeAlias) => + a === b ? 0 : a ? -1 : 1 + ); + + const factories = embeddablesService + ? Array.from(embeddablesService.getEmbeddableFactories()).filter( + ({ type, isEditable, canCreateNew, isContainerType }) => + isEditable() && + !isContainerType && + canCreateNew() && + !['visualization', 'ml'].some((factoryType) => { + return type.includes(factoryType); + }) + ) + : []; + + const promotedVisTypes = getVisTypesByGroup(VisGroups.PROMOTED); + + return ( + + ); +}; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/index.ts new file mode 100644 index 00000000000000..0f903b1bbbe2ed --- /dev/null +++ b/x-pack/plugins/canvas/public/components/workpad_header/editor_menu/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { EditorMenu } from './editor_menu'; +export { EditorMenu as EditorMenuComponent } from './editor_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx index 8ac581b0866a46..1cfab236d9a9c9 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/element_menu.component.tsx @@ -12,11 +12,11 @@ import { EuiContextMenu, EuiIcon, EuiContextMenuPanelItemDescriptor } from '@ela import { i18n } from '@kbn/i18n'; import { PrimaryActionPopover } from '../../../../../../../src/plugins/presentation_util/public'; import { getId } from '../../../lib/get_id'; -import { ClosePopoverFn } from '../../popover'; import { CONTEXT_MENU_TOP_BORDER_CLASSNAME } from '../../../../common/lib'; import { ElementSpec } from '../../../../types'; import { flattenPanelTree } from '../../../lib/flatten_panel_tree'; import { AssetManager } from '../../asset_manager'; +import { ClosePopoverFn } from '../../popover'; import { SavedElementsModal } from '../../saved_elements_modal'; interface CategorizedElementLists { @@ -112,7 +112,7 @@ const categorizeElementsByType = (elements: ElementSpec[]): { [key: string]: Ele return categories; }; -interface Props { +export interface Props { /** * Dictionary of elements from elements registry */ @@ -120,7 +120,7 @@ interface Props { /** * Handler for adding a selected element to the workpad */ - addElement: (element: ElementSpec) => void; + addElement: (element: Partial) => void; } export const ElementMenu: FunctionComponent = ({ elements, addElement }) => { diff --git a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts index 52c8daece7690a..037bb84b0cdba4 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts +++ b/x-pack/plugins/canvas/public/components/workpad_header/element_menu/index.ts @@ -5,5 +5,4 @@ * 2.0. */ -export { ElementMenu } from './element_menu'; -export { ElementMenu as ElementMenuComponent } from './element_menu.component'; +export { ElementMenu } from './element_menu.component'; diff --git a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx index f031d7c2631991..b84e4faf2925e7 100644 --- a/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx +++ b/x-pack/plugins/canvas/public/components/workpad_header/workpad_header.component.tsx @@ -27,6 +27,7 @@ import { ElementMenu } from './element_menu'; import { ShareMenu } from './share_menu'; import { ViewMenu } from './view_menu'; import { LabsControl } from './labs_control'; +import { EditorMenu } from './editor_menu'; const strings = { getFullScreenButtonAriaLabel: () => @@ -160,24 +161,22 @@ export const WorkpadHeader: FC = ({ + {isWriteable && ( + + + {{ + primaryActionButton: , + quickButtonGroup: , + addFromLibraryButton: , + extraButtons: [], + }} + + + )} - {isWriteable && ( - - - {{ - primaryActionButton: ( - - ), - quickButtonGroup: , - addFromLibraryButton: , - }} - - - )} @@ -192,6 +191,7 @@ export const WorkpadHeader: FC = ({ + diff --git a/x-pack/plugins/canvas/public/plugin.tsx b/x-pack/plugins/canvas/public/plugin.tsx index 5d1f05fdbe8bf5..d2375064603c30 100644 --- a/x-pack/plugins/canvas/public/plugin.tsx +++ b/x-pack/plugins/canvas/public/plugin.tsx @@ -8,6 +8,7 @@ import { BehaviorSubject } from 'rxjs'; import type { SharePluginSetup } from 'src/plugins/share/public'; import { ChartsPluginSetup, ChartsPluginStart } from 'src/plugins/charts/public'; +import { VisualizationsStart } from 'src/plugins/visualizations/public'; import { ReportingStart } from '../../reporting/public'; import { CoreSetup, @@ -63,6 +64,7 @@ export interface CanvasStartDeps { charts: ChartsPluginStart; data: DataPublicPluginStart; presentationUtil: PresentationUtilPluginStart; + visualizations: VisualizationsStart; spaces?: SpacesPluginStart; } @@ -122,7 +124,12 @@ export class CanvasPlugin const { pluginServices } = await import('./services'); pluginServices.setRegistry( - pluginServiceRegistry.start({ coreStart, startPlugins, initContext: this.initContext }) + pluginServiceRegistry.start({ + coreStart, + startPlugins, + appUpdater: this.appUpdater, + initContext: this.initContext, + }) ); // Load application bundle diff --git a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts index 963a69a8f11f08..f117998bbd3eb2 100644 --- a/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts +++ b/x-pack/plugins/canvas/public/routes/workpad/hooks/use_workpad.ts @@ -50,7 +50,7 @@ export const useWorkpad = ( setResolveInfo({ aliasId, outcome, id: workpadId }); // If it's an alias match, we know we are going to redirect so don't even dispatch that we got the workpad - if (outcome !== 'aliasMatch') { + if (storedWorkpad.id !== workpadId && outcome !== 'aliasMatch') { workpad.aliasId = aliasId; dispatch(setAssets(assets)); @@ -61,7 +61,7 @@ export const useWorkpad = ( setError(e as Error | string); } })(); - }, [workpadId, dispatch, setError, loadPages, workpadResolve]); + }, [workpadId, dispatch, setError, loadPages, workpadResolve, storedWorkpad.id]); useEffect(() => { // If the resolved info is not for the current workpad id, bail out diff --git a/x-pack/plugins/canvas/public/services/embeddables.ts b/x-pack/plugins/canvas/public/services/embeddables.ts index 24d7a57e086f2d..26b150b7a53493 100644 --- a/x-pack/plugins/canvas/public/services/embeddables.ts +++ b/x-pack/plugins/canvas/public/services/embeddables.ts @@ -5,8 +5,12 @@ * 2.0. */ -import { EmbeddableFactory } from '../../../../../src/plugins/embeddable/public'; +import { + EmbeddableFactory, + EmbeddableStateTransfer, +} from '../../../../../src/plugins/embeddable/public'; export interface CanvasEmbeddablesService { getEmbeddableFactories: () => IterableIterator; + getStateTransfer: () => EmbeddableStateTransfer; } diff --git a/x-pack/plugins/canvas/public/services/index.ts b/x-pack/plugins/canvas/public/services/index.ts index f4292810b80896..ed55f919e4c767 100644 --- a/x-pack/plugins/canvas/public/services/index.ts +++ b/x-pack/plugins/canvas/public/services/index.ts @@ -17,6 +17,7 @@ import { CanvasNavLinkService } from './nav_link'; import { CanvasNotifyService } from './notify'; import { CanvasPlatformService } from './platform'; import { CanvasReportingService } from './reporting'; +import { CanvasVisualizationsService } from './visualizations'; import { CanvasWorkpadService } from './workpad'; export interface CanvasPluginServices { @@ -28,6 +29,7 @@ export interface CanvasPluginServices { notify: CanvasNotifyService; platform: CanvasPlatformService; reporting: CanvasReportingService; + visualizations: CanvasVisualizationsService; workpad: CanvasWorkpadService; } @@ -44,4 +46,6 @@ export const useNavLinkService = () => (() => pluginServices.getHooks().navLink. export const useNotifyService = () => (() => pluginServices.getHooks().notify.useService())(); export const usePlatformService = () => (() => pluginServices.getHooks().platform.useService())(); export const useReportingService = () => (() => pluginServices.getHooks().reporting.useService())(); +export const useVisualizationsService = () => + (() => pluginServices.getHooks().visualizations.useService())(); export const useWorkpadService = () => (() => pluginServices.getHooks().workpad.useService())(); diff --git a/x-pack/plugins/canvas/public/services/kibana/embeddables.ts b/x-pack/plugins/canvas/public/services/kibana/embeddables.ts index 054b9da7409fbb..8d1a86edab3d89 100644 --- a/x-pack/plugins/canvas/public/services/kibana/embeddables.ts +++ b/x-pack/plugins/canvas/public/services/kibana/embeddables.ts @@ -16,4 +16,5 @@ export type EmbeddablesServiceFactory = KibanaPluginServiceFactory< export const embeddablesServiceFactory: EmbeddablesServiceFactory = ({ startPlugins }) => ({ getEmbeddableFactories: startPlugins.embeddable.getEmbeddableFactories, + getStateTransfer: startPlugins.embeddable.getStateTransfer, }); diff --git a/x-pack/plugins/canvas/public/services/kibana/index.ts b/x-pack/plugins/canvas/public/services/kibana/index.ts index 1eb010e8d6f9da..91767947bc0a65 100644 --- a/x-pack/plugins/canvas/public/services/kibana/index.ts +++ b/x-pack/plugins/canvas/public/services/kibana/index.ts @@ -22,6 +22,7 @@ import { navLinkServiceFactory } from './nav_link'; import { notifyServiceFactory } from './notify'; import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; +import { visualizationsServiceFactory } from './visualizations'; import { workpadServiceFactory } from './workpad'; export { customElementServiceFactory } from './custom_element'; @@ -31,6 +32,7 @@ export { labsServiceFactory } from './labs'; export { notifyServiceFactory } from './notify'; export { platformServiceFactory } from './platform'; export { reportingServiceFactory } from './reporting'; +export { visualizationsServiceFactory } from './visualizations'; export { workpadServiceFactory } from './workpad'; export const pluginServiceProviders: PluginServiceProviders< @@ -45,6 +47,7 @@ export const pluginServiceProviders: PluginServiceProviders< notify: new PluginServiceProvider(notifyServiceFactory), platform: new PluginServiceProvider(platformServiceFactory), reporting: new PluginServiceProvider(reportingServiceFactory), + visualizations: new PluginServiceProvider(visualizationsServiceFactory), workpad: new PluginServiceProvider(workpadServiceFactory), }; diff --git a/x-pack/plugins/canvas/public/services/kibana/visualizations.ts b/x-pack/plugins/canvas/public/services/kibana/visualizations.ts new file mode 100644 index 00000000000000..e319ec1c1f4272 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/kibana/visualizations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { KibanaPluginServiceFactory } from '../../../../../../src/plugins/presentation_util/public'; +import { CanvasStartDeps } from '../../plugin'; +import { CanvasVisualizationsService } from '../visualizations'; + +export type VisualizationsServiceFactory = KibanaPluginServiceFactory< + CanvasVisualizationsService, + CanvasStartDeps +>; + +export const visualizationsServiceFactory: VisualizationsServiceFactory = ({ startPlugins }) => ({ + showNewVisModal: startPlugins.visualizations.showNewVisModal, + getByGroup: startPlugins.visualizations.getByGroup, + getAliases: startPlugins.visualizations.getAliases, +}); diff --git a/x-pack/plugins/canvas/public/services/stubs/embeddables.ts b/x-pack/plugins/canvas/public/services/stubs/embeddables.ts index 173d27563e2b2a..9c2cf4d0650abe 100644 --- a/x-pack/plugins/canvas/public/services/stubs/embeddables.ts +++ b/x-pack/plugins/canvas/public/services/stubs/embeddables.ts @@ -14,4 +14,5 @@ const noop = (..._args: any[]): any => {}; export const embeddablesServiceFactory: EmbeddablesServiceFactory = () => ({ getEmbeddableFactories: noop, + getStateTransfer: noop, }); diff --git a/x-pack/plugins/canvas/public/services/stubs/index.ts b/x-pack/plugins/canvas/public/services/stubs/index.ts index 06a5ff49e9c04e..2216013a29c129 100644 --- a/x-pack/plugins/canvas/public/services/stubs/index.ts +++ b/x-pack/plugins/canvas/public/services/stubs/index.ts @@ -22,6 +22,7 @@ import { navLinkServiceFactory } from './nav_link'; import { notifyServiceFactory } from './notify'; import { platformServiceFactory } from './platform'; import { reportingServiceFactory } from './reporting'; +import { visualizationsServiceFactory } from './visualizations'; import { workpadServiceFactory } from './workpad'; export { customElementServiceFactory } from './custom_element'; @@ -31,6 +32,7 @@ export { navLinkServiceFactory } from './nav_link'; export { notifyServiceFactory } from './notify'; export { platformServiceFactory } from './platform'; export { reportingServiceFactory } from './reporting'; +export { visualizationsServiceFactory } from './visualizations'; export { workpadServiceFactory } from './workpad'; export const pluginServiceProviders: PluginServiceProviders = { @@ -42,6 +44,7 @@ export const pluginServiceProviders: PluginServiceProviders; + +const noop = (..._args: any[]): any => {}; + +export const visualizationsServiceFactory: VisualizationsServiceFactory = () => ({ + showNewVisModal: noop, + getByGroup: noop, + getAliases: noop, +}); diff --git a/x-pack/plugins/canvas/public/services/visualizations.ts b/x-pack/plugins/canvas/public/services/visualizations.ts new file mode 100644 index 00000000000000..c602b1dd39f3d2 --- /dev/null +++ b/x-pack/plugins/canvas/public/services/visualizations.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { VisualizationsStart } from '../../../../../src/plugins/visualizations/public'; + +export interface CanvasVisualizationsService { + showNewVisModal: VisualizationsStart['showNewVisModal']; + getByGroup: VisualizationsStart['getByGroup']; + getAliases: VisualizationsStart['getAliases']; +} diff --git a/x-pack/plugins/canvas/public/state/reducers/embeddable.ts b/x-pack/plugins/canvas/public/state/reducers/embeddable.ts index 4cfdc7f21945f6..092d4300d86b79 100644 --- a/x-pack/plugins/canvas/public/state/reducers/embeddable.ts +++ b/x-pack/plugins/canvas/public/state/reducers/embeddable.ts @@ -40,7 +40,7 @@ export const embeddableReducer = handleActions< const element = pageWithElement.elements.find((elem) => elem.id === elementId); - if (!element) { + if (!element || element.expression === embeddableExpression) { return workpadState; } diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index 4071b891e4c3d7..ebe43ba76a46ac 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -14,6 +14,7 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { BfetchServerSetup } from 'src/plugins/bfetch/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { HomeServerPluginSetup } from 'src/plugins/home/server'; +import { EmbeddableSetup } from 'src/plugins/embeddable/server'; import { ESSQL_SEARCH_STRATEGY } from '../common/lib/constants'; import { ReportingSetup } from '../../reporting/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; @@ -30,6 +31,7 @@ import { CanvasRouteHandlerContext, createWorkpadRouteContext } from './workpad_ interface PluginsSetup { expressions: ExpressionsServerSetup; + embeddable: EmbeddableSetup; features: FeaturesPluginSetup; home: HomeServerPluginSetup; bfetch: BfetchServerSetup; @@ -82,7 +84,12 @@ export class CanvasPlugin implements Plugin { const kibanaIndex = coreSetup.savedObjects.getKibanaIndex(); registerCanvasUsageCollector(plugins.usageCollection, kibanaIndex); - setupInterpreter(expressionsFork); + setupInterpreter(expressionsFork, { + embeddablePersistableStateService: { + extract: plugins.embeddable.extract, + inject: plugins.embeddable.inject, + }, + }); coreSetup.getStartServices().then(([_, depsStart]) => { const strategy = essqlSearchStrategyProvider(); diff --git a/x-pack/plugins/canvas/server/setup_interpreter.ts b/x-pack/plugins/canvas/server/setup_interpreter.ts index 2fe23eb86c086f..849ad79717056c 100644 --- a/x-pack/plugins/canvas/server/setup_interpreter.ts +++ b/x-pack/plugins/canvas/server/setup_interpreter.ts @@ -7,9 +7,15 @@ import { ExpressionsServerSetup } from 'src/plugins/expressions/server'; import { functions } from '../canvas_plugin_src/functions/server'; -import { functions as externalFunctions } from '../canvas_plugin_src/functions/external'; +import { + initFunctions as initExternalFunctions, + InitializeArguments, +} from '../canvas_plugin_src/functions/external'; -export function setupInterpreter(expressions: ExpressionsServerSetup) { +export function setupInterpreter( + expressions: ExpressionsServerSetup, + dependencies: InitializeArguments +) { functions.forEach((f) => expressions.registerFunction(f)); - externalFunctions.forEach((f) => expressions.registerFunction(f)); + initExternalFunctions(dependencies).forEach((f) => expressions.registerFunction(f)); } diff --git a/x-pack/plugins/canvas/types/embeddables.ts b/x-pack/plugins/canvas/types/embeddables.ts new file mode 100644 index 00000000000000..b78efece59d8f8 --- /dev/null +++ b/x-pack/plugins/canvas/types/embeddables.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { TimeRange } from 'src/plugins/data/public'; +import { Filter } from '@kbn/es-query'; +import { EmbeddableInput as Input } from '../../../../src/plugins/embeddable/common/'; + +export type EmbeddableInput = Input & { + timeRange?: TimeRange; + filters?: Filter[]; + savedObjectId?: string; +}; diff --git a/x-pack/plugins/canvas/types/functions.ts b/x-pack/plugins/canvas/types/functions.ts index 2569e0b10685b7..c80102915ed953 100644 --- a/x-pack/plugins/canvas/types/functions.ts +++ b/x-pack/plugins/canvas/types/functions.ts @@ -10,8 +10,8 @@ import { UnwrapPromiseOrReturn } from '@kbn/utility-types'; import { functions as commonFunctions } from '../canvas_plugin_src/functions/common'; import { functions as browserFunctions } from '../canvas_plugin_src/functions/browser'; import { functions as serverFunctions } from '../canvas_plugin_src/functions/server'; -import { functions as externalFunctions } from '../canvas_plugin_src/functions/external'; -import { initFunctions } from '../public/functions'; +import { initFunctions as initExternalFunctions } from '../canvas_plugin_src/functions/external'; +import { initFunctions as initClientFunctions } from '../public/functions'; /** * A `ExpressionFunctionFactory` is a powerful type used for any function that produces @@ -90,9 +90,11 @@ export type FunctionFactory = type CommonFunction = FunctionFactory; type BrowserFunction = FunctionFactory; type ServerFunction = FunctionFactory; -type ExternalFunction = FunctionFactory; +type ExternalFunction = FunctionFactory< + ReturnType extends Array ? U : never +>; type ClientFunctions = FunctionFactory< - ReturnType extends Array ? U : never + ReturnType extends Array ? U : never >; /** diff --git a/x-pack/plugins/canvas/types/index.ts b/x-pack/plugins/canvas/types/index.ts index 09ae1510be6da0..930f3372920884 100644 --- a/x-pack/plugins/canvas/types/index.ts +++ b/x-pack/plugins/canvas/types/index.ts @@ -9,6 +9,7 @@ export * from '../../../../src/plugins/expressions/common'; export * from './assets'; export * from './canvas'; export * from './elements'; +export * from './embeddables'; export * from './filters'; export * from './functions'; export * from './renderers'; diff --git a/x-pack/plugins/cases/common/api/runtime_types.ts b/x-pack/plugins/cases/common/api/runtime_types.ts index 7edc1162c0e818..c807d4b31b7515 100644 --- a/x-pack/plugins/cases/common/api/runtime_types.ts +++ b/x-pack/plugins/cases/common/api/runtime_types.ts @@ -60,7 +60,7 @@ export const decodeOrThrow = const getExcessProps = (props: rt.Props, r: Record): string[] => { const ex: string[] = []; for (const k of Object.keys(r)) { - if (!props.hasOwnProperty(k)) { + if (!Object.prototype.hasOwnProperty.call(props, k)) { ex.push(k); } } @@ -89,5 +89,5 @@ export function excess | rt.PartialType ({ +export const getFormMock = (sampleData: unknown) => ({ ...mockFormHook, submit: () => Promise.resolve({ diff --git a/x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx b/x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx index 59efcf868c9eec..2b43fbf63095e4 100644 --- a/x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx @@ -6,7 +6,7 @@ */ import React from 'react'; -import { EuiBasicTable as _EuiBasicTable } from '@elastic/eui'; +import { EuiBasicTable } from '@elastic/eui'; import styled from 'styled-components'; import { Case, SubCase } from '../../containers/types'; import { CasesColumns } from './columns'; @@ -14,7 +14,7 @@ import { AssociationType } from '../../../common'; type ExpandedRowMap = Record | {}; -const EuiBasicTable: any = _EuiBasicTable; +// @ts-expect-error TS2769 const BasicTable = styled(EuiBasicTable)` thead { display: none; diff --git a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx index a387c5eae38340..f55c871f4922a6 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx @@ -303,7 +303,10 @@ describe('AllCasesGeneric', () => { await waitFor(() => { result.current.map( - (i, key) => i.name != null && !i.hasOwnProperty('actions') && checkIt(`${i.name}`, key) + (i, key) => + i.name != null && + !Object.prototype.hasOwnProperty.call(i, 'actions') && + checkIt(`${i.name}`, key) ); }); }); @@ -378,7 +381,9 @@ describe('AllCasesGeneric', () => { }) ); await waitFor(() => { - result.current.map((i) => i.name != null && !i.hasOwnProperty('actions')); + result.current.map( + (i) => i.name != null && !Object.prototype.hasOwnProperty.call(i, 'actions') + ); expect(wrapper.find(`a[data-test-subj="case-details-link"]`).exists()).toBeFalsy(); }); }); diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx index 4e8334ebceec09..cbfb24f18c97e0 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/index.tsx @@ -87,5 +87,8 @@ export const AllCasesSelectorModal: React.FC = React ); }); + +AllCasesSelectorModal.displayName = 'AllCasesSelectorModal'; + // eslint-disable-next-line import/no-default-export export { AllCasesSelectorModal as default }; diff --git a/x-pack/plugins/cases/public/components/all_cases/table.tsx b/x-pack/plugins/cases/public/components/all_cases/table.tsx index 876007494d276a..3f80fc8f0d7c47 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table.tsx @@ -10,7 +10,7 @@ import { EuiEmptyPrompt, EuiLoadingContent, EuiTableSelectionType, - EuiBasicTable as _EuiBasicTable, + EuiBasicTable, EuiBasicTableProps, } from '@elastic/eui'; import classnames from 'classnames'; @@ -40,12 +40,12 @@ interface CasesTableProps { selection: EuiTableSelectionType; showActions: boolean; sorting: EuiBasicTableProps['sorting']; - tableRef: MutableRefObject<_EuiBasicTable | undefined>; + tableRef: MutableRefObject; tableRowProps: EuiBasicTableProps['rowProps']; userCanCrud: boolean; } -const EuiBasicTable: any = _EuiBasicTable; +// @ts-expect-error TS2769 const BasicTable = styled(EuiBasicTable)` ${({ theme }) => ` .euiTableRow-isExpandedRow.euiTableRow-isSelectable .euiTableCellContent { diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx index 788c6eeb61b32f..cf7962f08db933 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx @@ -242,5 +242,7 @@ export const ConfigureCases: React.FC = React.memo((props) ); }); +ConfigureCases.displayName = 'ConfigureCases'; + // eslint-disable-next-line import/no-default-export export default ConfigureCases; diff --git a/x-pack/plugins/cases/public/components/connectors/connectors_registry.ts b/x-pack/plugins/cases/public/components/connectors/connectors_registry.ts index 2e02cb290c3c84..81747333013481 100644 --- a/x-pack/plugins/cases/public/components/connectors/connectors_registry.ts +++ b/x-pack/plugins/cases/public/components/connectors/connectors_registry.ts @@ -9,8 +9,25 @@ import { i18n } from '@kbn/i18n'; import { CaseConnector, CaseConnectorsRegistry } from './types'; export const createCaseConnectorsRegistry = (): CaseConnectorsRegistry => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any const connectors: Map> = new Map(); + function assertConnectorExists( + connector: CaseConnector | undefined | null, + id: string + ): asserts connector { + if (!connector) { + throw new Error( + i18n.translate('xpack.cases.connecors.get.missingCaseConnectorErrorMessage', { + defaultMessage: 'Object type "{id}" is not registered.', + values: { + id, + }, + }) + ); + } + } + const registry: CaseConnectorsRegistry = { has: (id: string) => connectors.has(id), register: (connector: CaseConnector) => { @@ -28,17 +45,9 @@ export const createCaseConnectorsRegistry = (): CaseConnectorsRegistry => { connectors.set(connector.id, connector); }, get: (id: string): CaseConnector => { - if (!connectors.has(id)) { - throw new Error( - i18n.translate('xpack.cases.connecors.get.missingCaseConnectorErrorMessage', { - defaultMessage: 'Object type "{id}" is not registered.', - values: { - id, - }, - }) - ); - } - return connectors.get(id)!; + const connector = connectors.get(id); + assertConnectorExists(connector, id); + return connector; }, list: () => { return Array.from(connectors).map(([id, connector]) => connector); diff --git a/x-pack/plugins/cases/public/components/create/description.tsx b/x-pack/plugins/cases/public/components/create/description.tsx index e1bd563a3d7986..c1545a42df3f59 100644 --- a/x-pack/plugins/cases/public/components/create/description.tsx +++ b/x-pack/plugins/cases/public/components/create/description.tsx @@ -21,7 +21,7 @@ const DescriptionComponent: React.FC = ({ isLoading }) => { useLensDraftComment(); const { setFieldValue } = useFormContext(); const [{ title, tags }] = useFormData({ watch: ['title', 'tags'] }); - const editorRef = useRef>(); + const editorRef = useRef>(); useEffect(() => { if (draftComment?.commentId === fieldName && editorRef.current) { diff --git a/x-pack/plugins/cases/public/components/create/index.tsx b/x-pack/plugins/cases/public/components/create/index.tsx index d3eaba1ea0bc44..39f10a89290d8f 100644 --- a/x-pack/plugins/cases/public/components/create/index.tsx +++ b/x-pack/plugins/cases/public/components/create/index.tsx @@ -98,5 +98,8 @@ export const CreateCase: React.FC = React.memo((props) => ( )); + +CreateCase.displayName = 'CreateCase'; + // eslint-disable-next-line import/no-default-export export { CreateCase as default }; diff --git a/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx index 4bf25b23403e16..e2067d75e843e9 100644 --- a/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx +++ b/x-pack/plugins/cases/public/components/markdown_editor/editor.tsx @@ -96,4 +96,6 @@ const MarkdownEditorComponent = forwardRef + <> {this.renderSearchBar()} {this.renderListing()} - + ); } @@ -481,16 +481,23 @@ export class SavedObjectFinderUi extends React.Component< {items.map((item) => { const currentSavedObjectMetaData = savedObjectMetaData.find( (metaData) => metaData.type === item.type - )!; + ); + + if (currentSavedObjectMetaData == null) { + return null; + } + const fullName = currentSavedObjectMetaData.getTooltipForSavedObject ? currentSavedObjectMetaData.getTooltipForSavedObject(item.savedObject) - : `${item.title} (${currentSavedObjectMetaData!.name})`; + : `${item.title} (${currentSavedObjectMetaData.name})`; + const iconType = ( currentSavedObjectMetaData || ({ getIconForSavedObject: () => 'document', } as Pick, 'getIconForSavedObject'>) ).getIconForSavedObject(item.savedObject); + return ( => { + const MarkdownLinkProcessingComponent: React.FC = memo((props) => ( + + )); + + MarkdownLinkProcessingComponent.displayName = 'MarkdownLinkProcessingComponent'; + + return MarkdownLinkProcessingComponent; +}; + const MarkdownRendererComponent: React.FC = ({ children, disableLinks }) => { const { processingPlugins, parsingPlugins } = usePlugins(); - const MarkdownLinkProcessingComponent: React.FC = useMemo( - () => (props) => , - [disableLinks] - ); // Deep clone of the processing plugins to prevent affecting the markdown editor. const processingPluginList = cloneDeep(processingPlugins); // This line of code is TS-compatible and it will break if [1][1] change in the future. - processingPluginList[1][1].components.a = MarkdownLinkProcessingComponent; + processingPluginList[1][1].components.a = useMemo( + () => withDisabledLinks(disableLinks), + [disableLinks] + ); return ( = React.memo((props) => { ); }); +RecentCases.displayName = 'RecentCases'; + // eslint-disable-next-line import/no-default-export export { RecentCases as default }; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx b/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx index 2eb44f91190c61..2419ac0d048e9e 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx @@ -396,6 +396,8 @@ const ActionIcon = React.memo<{ ); }); +ActionIcon.displayName = 'ActionIcon'; + export const getActionAttachment = ({ comment, userCanCrud, diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx index 24fc393715f768..95c4f76eae0a20 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx @@ -25,7 +25,7 @@ import * as i18n from './translations'; import { useUpdateComment } from '../../containers/use_update_comment'; import { useCurrentUser } from '../../common/lib/kibana'; -import { AddComment } from '../add_comment'; +import { AddComment, AddCommentRefObject } from '../add_comment'; import { ActionConnector, ActionsCommentRequestRt, @@ -52,7 +52,7 @@ import { getActionAttachment, } from './helpers'; import { UserActionAvatar } from './user_action_avatar'; -import { UserActionMarkdown } from './user_action_markdown'; +import { UserActionMarkdown, UserActionMarkdownRefObject } from './user_action_markdown'; import { UserActionTimestamp } from './user_action_timestamp'; import { UserActionUsername } from './user_action_username'; import { UserActionContentToolbar } from './user_action_content_toolbar'; @@ -131,6 +131,17 @@ const MyEuiCommentList = styled(EuiCommentList)` const DESCRIPTION_ID = 'description'; const NEW_ID = 'newComment'; +const isAddCommentRef = ( + ref: AddCommentRefObject | UserActionMarkdownRefObject | null | undefined +): ref is AddCommentRefObject => { + const commentRef = ref as AddCommentRefObject; + if (commentRef?.addQuote != null) { + return true; + } + + return false; +}; + export const UserActionTree = React.memo( ({ caseServices, @@ -167,7 +178,9 @@ export const UserActionTree = React.memo( const { isLoadingIds, patchComment } = useUpdateComment(); const currentUser = useCurrentUser(); const [manageMarkdownEditIds, setManageMarkdownEditIds] = useState([]); - const commentRefs = useRef>({}); + const commentRefs = useRef< + Record + >({}); const { clearDraftComment, draftComment, hasIncomingLensState, openLensModal } = useLensDraftComment(); @@ -228,8 +241,9 @@ export const UserActionTree = React.memo( const handleManageQuote = useCallback( (quote: string) => { - if (commentRefs.current[NEW_ID]) { - commentRefs.current[NEW_ID].addQuote(quote); + const ref = commentRefs?.current[NEW_ID]; + if (isAddCommentRef(ref)) { + ref.addQuote(quote); } handleOutlineComment('add-comment'); @@ -337,6 +351,8 @@ export const UserActionTree = React.memo( const userActions: EuiCommentProps[] = useMemo( () => caseUserActions.reduce( + // TODO: Decrease complexity. https://github.com/elastic/kibana/issues/115730 + // eslint-disable-next-line complexity (comments, action, index) => { // Comment creation if (action.commentId != null && action.action === 'create') { @@ -664,15 +680,12 @@ export const UserActionTree = React.memo( return prevManageMarkdownEditIds; }); - if ( - commentRefs.current && - commentRefs.current[draftComment.commentId] && - commentRefs.current[draftComment.commentId].editor?.textarea && - commentRefs.current[draftComment.commentId].editor?.toolbar - ) { - commentRefs.current[draftComment.commentId].setComment(draftComment.comment); + const ref = commentRefs?.current?.[draftComment.commentId]; + + if (isAddCommentRef(ref) && ref.editor?.textarea) { + ref.setComment(draftComment.comment); if (hasIncomingLensState) { - openLensModal({ editorRef: commentRefs.current[draftComment.commentId].editor }); + openLensModal({ editorRef: ref.editor }); } else { clearDraftComment(); } diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx index f7a6932b358563..93212d2b11016a 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_markdown.tsx @@ -26,7 +26,7 @@ interface UserActionMarkdownProps { onSaveContent: (content: string) => void; } -interface UserActionMarkdownRefObject { +export interface UserActionMarkdownRefObject { setComment: (newComment: string) => void; } diff --git a/x-pack/plugins/cases/public/components/utils.test.ts b/x-pack/plugins/cases/public/components/utils.test.ts index 86d37d2e5e59e7..e3cc753e757466 100644 --- a/x-pack/plugins/cases/public/components/utils.test.ts +++ b/x-pack/plugins/cases/public/components/utils.test.ts @@ -39,8 +39,49 @@ describe('Utils', () => { }); describe('isDeprecatedConnector', () => { + const connector = { + id: 'test', + actionTypeId: '.webhook', + name: 'Test', + config: { usesTableApi: false }, + secrets: {}, + isPreconfigured: false, + }; + it('returns false if the connector is not defined', () => { expect(isDeprecatedConnector()).toBe(false); }); + + it('returns false if the connector is not ITSM or SecOps', () => { + expect(isDeprecatedConnector(connector)).toBe(false); + }); + + it('returns false if the connector is .servicenow and the usesTableApi=false', () => { + expect(isDeprecatedConnector({ ...connector, actionTypeId: '.servicenow' })).toBe(false); + }); + + it('returns false if the connector is .servicenow-sir and the usesTableApi=false', () => { + expect(isDeprecatedConnector({ ...connector, actionTypeId: '.servicenow-sir' })).toBe(false); + }); + + it('returns true if the connector is .servicenow and the usesTableApi=true', () => { + expect( + isDeprecatedConnector({ + ...connector, + actionTypeId: '.servicenow', + config: { usesTableApi: true }, + }) + ).toBe(true); + }); + + it('returns true if the connector is .servicenow-sir and the usesTableApi=true', () => { + expect( + isDeprecatedConnector({ + ...connector, + actionTypeId: '.servicenow-sir', + config: { usesTableApi: true }, + }) + ).toBe(true); + }); }); }); diff --git a/x-pack/plugins/cases/public/components/utils.ts b/x-pack/plugins/cases/public/components/utils.ts index 3ac48135edae84..82d2682e65fadf 100644 --- a/x-pack/plugins/cases/public/components/utils.ts +++ b/x-pack/plugins/cases/public/components/utils.ts @@ -12,11 +12,6 @@ import { StartPlugins } from '../types'; import { connectorValidator as swimlaneConnectorValidator } from './connectors/swimlane/validator'; import { connectorValidator as servicenowConnectorValidator } from './connectors/servicenow/validator'; import { CaseActionConnector } from './types'; -import { - ENABLE_NEW_SN_ITSM_CONNECTOR, - ENABLE_NEW_SN_SIR_CONNECTOR, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../actions/server/constants/connectors'; export const getConnectorById = ( id: string, @@ -83,24 +78,16 @@ export const isDeprecatedConnector = (connector?: CaseActionConnector): boolean return false; } - if (!ENABLE_NEW_SN_ITSM_CONNECTOR && connector.actionTypeId === '.servicenow') { - return true; + if (connector.actionTypeId === '.servicenow' || connector.actionTypeId === '.servicenow-sir') { + /** + * Connector's prior to the Elastic ServiceNow application + * use the Table API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_TableAPI) + * Connectors after the Elastic ServiceNow application use the + * Import Set API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_ImportSetAPI) + * A ServiceNow connector is considered deprecated if it uses the Table API. + */ + return !!connector.config.usesTableApi; } - if (!ENABLE_NEW_SN_SIR_CONNECTOR && connector.actionTypeId === '.servicenow-sir') { - return true; - } - - /** - * Connector's prior to the Elastic ServiceNow application - * use the Table API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_TableAPI) - * Connectors after the Elastic ServiceNow application use the - * Import Set API (https://developer.servicenow.com/dev.do#!/reference/api/rome/rest/c_ImportSetAPI) - * A ServiceNow connector is considered deprecated if it uses the Table API. - * - * All other connectors do not have the usesTableApi config property - * so the function will always return false for them. - */ - - return !!connector.config.usesTableApi; + return false; }; diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 75e8c8f58705dd..14f617b19db52b 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -87,7 +87,7 @@ export const resolveCase = async ( signal: AbortSignal ): Promise => { const response = await KibanaServices.get().http.fetch( - getCaseDetailsUrl(caseId) + '/resolve', + `${getCaseDetailsUrl(caseId)}/resolve`, { method: 'GET', query: { diff --git a/x-pack/plugins/cases/public/utils/use_mount_appended.ts b/x-pack/plugins/cases/public/utils/use_mount_appended.ts index d43b0455f47da5..48b71e6dbabfed 100644 --- a/x-pack/plugins/cases/public/utils/use_mount_appended.ts +++ b/x-pack/plugins/cases/public/utils/use_mount_appended.ts @@ -5,6 +5,8 @@ * 2.0. */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + // eslint-disable-next-line import/no-extraneous-dependencies import { mount } from 'enzyme'; diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index 3598c5b8956fa4..7a474ff4db402d 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -231,8 +231,10 @@ export class Authorization { ? Array.from(featureCaseOwners) : privileges.kibana.reduce((authorizedOwners, { authorized, privilege }) => { if (authorized && requiredPrivileges.has(privilege)) { - const owner = requiredPrivileges.get(privilege)!; - authorizedOwners.push(owner); + const owner = requiredPrivileges.get(privilege); + if (owner) { + authorizedOwners.push(owner); + } } return authorizedOwners; diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index b84a6bd84c43be..159ff3b41aba91 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -263,7 +263,7 @@ async function getCombinedCase({ id, }), ] - : [Promise.reject('case connector feature is disabled')]), + : [Promise.reject(new Error('case connector feature is disabled'))]), ]); if (subCasePromise.status === 'fulfilled') { diff --git a/x-pack/plugins/cases/server/common/error.ts b/x-pack/plugins/cases/server/common/error.ts index 1b53eb9fdb2181..3478131f65537e 100644 --- a/x-pack/plugins/cases/server/common/error.ts +++ b/x-pack/plugins/cases/server/common/error.ts @@ -8,10 +8,14 @@ import { Boom, isBoom } from '@hapi/boom'; import { Logger } from 'src/core/server'; +export interface HTTPError extends Error { + statusCode: number; +} + /** * Helper class for wrapping errors while preserving the original thrown error. */ -class CaseError extends Error { +export class CaseError extends Error { public readonly wrappedError?: Error; constructor(message?: string, originalError?: Error) { super(message); @@ -51,6 +55,13 @@ export function isCaseError(error: unknown): error is CaseError { return error instanceof CaseError; } +/** + * Type guard for determining if an error is an HTTPError + */ +export function isHTTPError(error: unknown): error is HTTPError { + return (error as HTTPError)?.statusCode != null; +} + /** * Create a CaseError that wraps the original thrown error. This also logs the message that will be placed in the CaseError * if the logger was defined. diff --git a/x-pack/plugins/cases/server/connectors/case/schema.ts b/x-pack/plugins/cases/server/connectors/case/schema.ts index 79d3bf62e8a9e7..b8e46fdf5aa8cc 100644 --- a/x-pack/plugins/cases/server/connectors/case/schema.ts +++ b/x-pack/plugins/cases/server/connectors/case/schema.ts @@ -83,6 +83,7 @@ const SwimlaneFieldsSchema = schema.object({ const NoneFieldsSchema = schema.nullable(schema.object({})); +// eslint-disable-next-line @typescript-eslint/no-explicit-any const ReducedConnectorFieldsSchema: { [x: string]: any } = { [ConnectorTypes.jira]: JiraFieldsSchema, [ConnectorTypes.resilient]: ResilientFieldsSchema, diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts index b09272d0a5505d..706b9f2f23ab5c 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts @@ -8,7 +8,7 @@ import { CaseResponse } from '../../../common'; import { format } from './sir_format'; -describe('ITSM formatter', () => { +describe('SIR formatter', () => { const theCase = { id: 'case-id', connector: { diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts index 9108408c4d089a..02c9fe629f4f81 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts @@ -45,12 +45,16 @@ export const format: ServiceNowSIRFormat = (theCase, alerts) => { if (fieldsToAdd.length > 0) { sirFields = alerts.reduce>((acc, alert) => { + let temp = {}; fieldsToAdd.forEach((alertField) => { const field = get(alertFieldMapping[alertField].alertPath, alert); + if (field && !manageDuplicate[alertFieldMapping[alertField].sirFieldKey].has(field)) { manageDuplicate[alertFieldMapping[alertField].sirFieldKey].add(field); - acc = { + + temp = { ...acc, + ...temp, [alertFieldMapping[alertField].sirFieldKey]: [ ...acc[alertFieldMapping[alertField].sirFieldKey], field, @@ -58,7 +62,8 @@ export const format: ServiceNowSIRFormat = (theCase, alerts) => { }; } }); - return acc; + + return { ...acc, ...temp }; }, sirFields); } diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index bef8d45bd86f66..9bbc7089c033c1 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -126,6 +126,11 @@ export class CasePlugin { }, featuresPluginStart: plugins.features, actionsPluginStart: plugins.actions, + /** + * Lens will be always defined as + * it is declared as required plugin in kibana.json + */ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion lensEmbeddableFactory: this.lensEmbeddableFactory!, }); diff --git a/x-pack/plugins/cases/server/routes/api/utils.test.ts b/x-pack/plugins/cases/server/routes/api/utils.test.ts index 3fce38b27446ef..fd7c038f06bc1c 100644 --- a/x-pack/plugins/cases/server/routes/api/utils.test.ts +++ b/x-pack/plugins/cases/server/routes/api/utils.test.ts @@ -5,8 +5,9 @@ * 2.0. */ -import { wrapError } from './utils'; import { isBoom, boomify } from '@hapi/boom'; +import { HTTPError } from '../../common'; +import { wrapError } from './utils'; describe('Utils', () => { describe('wrapError', () => { @@ -25,7 +26,7 @@ describe('Utils', () => { }); it('it set statusCode to errors status code', () => { - const error = new Error('Something happened') as any; + const error = new Error('Something happened') as HTTPError; error.statusCode = 404; const res = wrapError(error); diff --git a/x-pack/plugins/cases/server/routes/api/utils.ts b/x-pack/plugins/cases/server/routes/api/utils.ts index b2b5417ecae0f2..cb4804aab00549 100644 --- a/x-pack/plugins/cases/server/routes/api/utils.ts +++ b/x-pack/plugins/cases/server/routes/api/utils.ts @@ -9,18 +9,21 @@ import { Boom, boomify, isBoom } from '@hapi/boom'; import { schema } from '@kbn/config-schema'; import { CustomHttpResponseOptions, ResponseError } from 'kibana/server'; -import { isCaseError } from '../../common'; +import { CaseError, isCaseError, HTTPError, isHTTPError } from '../../common'; /** * Transforms an error into the correct format for a kibana response. */ -export function wrapError(error: any): CustomHttpResponseOptions { + +export function wrapError( + error: CaseError | Boom | HTTPError | Error +): CustomHttpResponseOptions { let boom: Boom; if (isCaseError(error)) { boom = error.boomify(); } else { - const options = { statusCode: error.statusCode ?? 500 }; + const options = { statusCode: isHTTPError(error) ? error.statusCode : 500 }; boom = isBoom(error) ? error : boomify(error, options); } diff --git a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts index 3cb013bd2e3fd1..28672160a0737d 100644 --- a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts +++ b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts @@ -4,7 +4,11 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable no-console */ +/* eslint-disable no-process-exit */ + import yargs from 'yargs'; import { ToolingLog } from '@kbn/dev-utils'; import { KbnClient } from '@kbn/test'; diff --git a/x-pack/plugins/cases/server/services/alerts/index.ts b/x-pack/plugins/cases/server/services/alerts/index.ts index 6bb2fb3ee3c560..d8a09fe1baf231 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.ts @@ -48,8 +48,6 @@ function isEmptyAlert(alert: AlertInfo): boolean { } export class AlertService { - constructor() {} - public async updateAlertsStatus({ alerts, scopedClusterClient, logger }: UpdateAlertsStatusArgs) { try { const bucketedAlerts = bucketAlertsByIndexAndStatus(alerts, logger); diff --git a/x-pack/plugins/data_visualizer/kibana.json b/x-pack/plugins/data_visualizer/kibana.json index e63a6b4fa21003..81fc0a2fdfe024 100644 --- a/x-pack/plugins/data_visualizer/kibana.json +++ b/x-pack/plugins/data_visualizer/kibana.json @@ -25,7 +25,8 @@ "kibanaReact", "maps", "esUiShared", - "fieldFormats" + "fieldFormats", + "charts" ], "extraPublicDirs": [ "common" diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx index 6459fc4006ceae..13b68d3b192cc9 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/document_count_content/document_count_chart/document_count_chart.tsx @@ -6,16 +6,13 @@ */ import React, { FC, useCallback, useMemo } from 'react'; - import { i18n } from '@kbn/i18n'; - import { Axis, BarSeries, BrushEndListener, Chart, ElementClickListener, - niceTimeFormatter, Position, ScaleType, Settings, @@ -23,7 +20,9 @@ import { XYBrushEvent, } from '@elastic/charts'; import moment from 'moment'; +import { IUiSettingsClient } from 'kibana/public'; import { useDataVisualizerKibana } from '../../../../kibana_context'; +import { MULTILAYER_TIME_AXIS_STYLE } from '../../../../../../../../../src/plugins/charts/common'; export interface DocumentCountChartPoint { time: number | string; @@ -40,6 +39,16 @@ interface Props { const SPEC_ID = 'document_count'; +function getTimezone(uiSettings: IUiSettingsClient) { + if (uiSettings.isDefault('dateFormat:tz')) { + const detectedTimezone = moment.tz.guess(); + if (detectedTimezone) return detectedTimezone; + else return moment().format('Z'); + } else { + return uiSettings.get('dateFormat:tz', 'Browser'); + } +} + export const DocumentCountChart: FC = ({ width, chartPoints, @@ -48,9 +57,12 @@ export const DocumentCountChart: FC = ({ interval, }) => { const { - services: { data }, + services: { data, uiSettings, fieldFormats }, } = useDataVisualizerKibana(); + const xAxisFormatter = fieldFormats.deserialize({ id: 'date' }); + const useLegacyTimeAxis = uiSettings.get('visualization:useLegacyTimeAxis', false); + const seriesName = i18n.translate( 'xpack.dataVisualizer.dataGrid.field.documentCountChart.seriesLabel', { @@ -63,8 +75,6 @@ export const DocumentCountChart: FC = ({ max: timeRangeLatest, }; - const dateFormatter = niceTimeFormatter([timeRangeEarliest, timeRangeLatest]); - const adjustedChartPoints = useMemo(() => { // Display empty chart when no data in range if (chartPoints.length < 1) return [{ time: timeRangeEarliest, value: 0 }]; @@ -110,6 +120,8 @@ export const DocumentCountChart: FC = ({ timefilterUpdateHandler(range); }; + const timeZone = getTimezone(uiSettings); + return (
= ({ id="bottom" position={Position.Bottom} showOverlappingTicks={true} - tickFormat={dateFormatter} + tickFormat={(value) => xAxisFormatter.convert(value)} + timeAxisLayerCount={useLegacyTimeAxis ? 0 : 2} + style={useLegacyTimeAxis ? {} : MULTILAYER_TIME_AXIS_STYLE} /> = ({ xAccessor="time" yAccessors={['value']} data={adjustedChartPoints} + timeZone={timeZone} />
diff --git a/x-pack/plugins/data_visualizer/public/application/common/components/results_links/results_links.tsx b/x-pack/plugins/data_visualizer/public/application/common/components/results_links/results_links.tsx index f1de0b0b8b8fa2..ed6ab29748a86c 100644 --- a/x-pack/plugins/data_visualizer/public/application/common/components/results_links/results_links.tsx +++ b/x-pack/plugins/data_visualizer/public/application/common/components/results_links/results_links.tsx @@ -58,7 +58,13 @@ export const ResultsLinks: FC = ({ additionalLinks, }) => { const { - services: { fileUpload }, + services: { + fileUpload, + application: { getUrlForApp, capabilities }, + share: { + urlGenerators: { getUrlGenerator }, + }, + }, } = useDataVisualizerKibana(); const [duration, setDuration] = useState({ @@ -72,15 +78,6 @@ export const ResultsLinks: FC = ({ const [indexPatternManagementLink, setIndexPatternManagementLink] = useState(''); const [generatedLinks, setGeneratedLinks] = useState>({}); - const { - services: { - application: { getUrlForApp, capabilities }, - share: { - urlGenerators: { getUrlGenerator }, - }, - }, - } = useDataVisualizerKibana(); - useEffect(() => { let unmounted = false; @@ -137,11 +134,14 @@ export const ResultsLinks: FC = ({ setIndexManagementLink( getUrlForApp('management', { path: '/data/index_management/indices' }) ); - setIndexPatternManagementLink( - getUrlForApp('management', { - path: `/kibana/indexPatterns${createIndexPattern ? `/patterns/${indexPatternId}` : ''}`, - }) - ); + + if (capabilities.indexPatterns.save === true) { + setIndexPatternManagementLink( + getUrlForApp('management', { + path: `/kibana/indexPatterns${createIndexPattern ? `/patterns/${indexPatternId}` : ''}`, + }) + ); + } } return () => { diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js index 054416ad7ba36e..fa437cce292689 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/file_data_visualizer_view/file_data_visualizer_view.js @@ -295,12 +295,7 @@ export class FileDataVisualizerView extends Component {
{mode === MODE.READ && ( <> - {!loading && !loaded && ( - - )} + {!loading && !loaded && } {loading && } @@ -373,6 +368,7 @@ export class FileDataVisualizerView extends Component { savedObjectsClient={this.savedObjectsClient} fileUpload={this.props.fileUpload} resultsLinks={this.props.resultsLinks} + capabilities={this.props.capabilities} /> {bottomBarVisible && ( diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_settings/advanced.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_settings/advanced.tsx index 83e7c556f033f1..23ad2b967bc282 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_settings/advanced.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_settings/advanced.tsx @@ -21,6 +21,7 @@ import { import { CombinedField, CombinedFieldsForm } from '../../../common/components/combined_fields'; import { JsonEditor, EDITOR_MODE } from '../json_editor'; import { FindFileStructureResponse } from '../../../../../../file_upload/common'; +import { CreateDataViewToolTip } from './create_data_view_tooltip'; const EDITOR_HEIGHT = '300px'; interface Props { @@ -42,6 +43,7 @@ interface Props { combinedFields: CombinedField[]; onCombinedFieldsChange(combinedFields: CombinedField[]): void; results: FindFileStructureResponse; + canCreateDataView: boolean; } export const AdvancedSettings: FC = ({ @@ -63,6 +65,7 @@ export const AdvancedSettings: FC = ({ combinedFields, onCombinedFieldsChange, results, + canCreateDataView, }) => { return ( @@ -98,18 +101,20 @@ export const AdvancedSettings: FC = ({ - - } - checked={createIndexPattern === true} - disabled={initialized === true} - onChange={onCreateIndexPatternChange} - /> + + + } + checked={createIndexPattern === true} + disabled={initialized === true || canCreateDataView === false} + onChange={onCreateIndexPatternChange} + /> + diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_settings/create_data_view_tooltip.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_settings/create_data_view_tooltip.tsx new file mode 100644 index 00000000000000..84af5b08b3d495 --- /dev/null +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_settings/create_data_view_tooltip.tsx @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { FC } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { EuiToolTip } from '@elastic/eui'; + +interface Props { + children?: React.ReactElement; + showTooltip: boolean; +} + +export const CreateDataViewToolTip: FC = ({ children, showTooltip }) => { + return ( + + ) : null + } + > + {children} + + ); +}; diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_settings/import_settings.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_settings/import_settings.tsx index 4e36dc42b54a58..c2b9779f3624d0 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_settings/import_settings.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_settings/import_settings.tsx @@ -14,6 +14,7 @@ import { SimpleSettings } from './simple'; import { AdvancedSettings } from './advanced'; import { CombinedField } from '../../../common/components/combined_fields'; import { FindFileStructureResponse } from '../../../../../../file_upload/common'; +import { useDataVisualizerKibana } from '../../../kibana_context'; interface Props { index: string; @@ -56,6 +57,15 @@ export const ImportSettings: FC = ({ onCombinedFieldsChange, results, }) => { + const { + services: { + application: { capabilities }, + }, + } = useDataVisualizerKibana(); + + const canCreateDataView = + capabilities.savedObjectsManagement.edit === true || capabilities.indexPatterns.save === true; + const tabs = [ { id: 'simple-settings', @@ -74,6 +84,7 @@ export const ImportSettings: FC = ({ onCreateIndexPatternChange={onCreateIndexPatternChange} indexNameError={indexNameError} combinedFields={combinedFields} + canCreateDataView={canCreateDataView} /> ), @@ -106,6 +117,7 @@ export const ImportSettings: FC = ({ combinedFields={combinedFields} onCombinedFieldsChange={onCombinedFieldsChange} results={results} + canCreateDataView={canCreateDataView} /> ), diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_settings/simple.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_settings/simple.tsx index 284a5aa3d4f3f0..a080f62f54fc1f 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_settings/simple.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_settings/simple.tsx @@ -14,6 +14,7 @@ import { CombinedField, CombinedFieldsReadOnlyForm, } from '../../../common/components/combined_fields'; +import { CreateDataViewToolTip } from './create_data_view_tooltip'; interface Props { index: string; @@ -23,6 +24,7 @@ interface Props { onCreateIndexPatternChange(): void; indexNameError: string; combinedFields: CombinedField[]; + canCreateDataView: boolean; } export const SimpleSettings: FC = ({ @@ -33,6 +35,7 @@ export const SimpleSettings: FC = ({ onCreateIndexPatternChange, indexNameError, combinedFields, + canCreateDataView, }) => { return ( @@ -69,19 +72,21 @@ export const SimpleSettings: FC = ({ - - } - checked={createIndexPattern === true} - disabled={initialized === true} - onChange={onCreateIndexPatternChange} - data-test-subj="dataVisualizerFileCreateIndexPatternCheckbox" - /> + + + } + checked={createIndexPattern === true} + disabled={initialized === true || canCreateDataView === false} + onChange={onCreateIndexPatternChange} + data-test-subj="dataVisualizerFileCreateIndexPatternCheckbox" + /> + diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_view/import_view.js b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_view/import_view.js index 3b3a11a5dff22e..b65e2c35ff4ff6 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_view/import_view.js +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/components/import_view/import_view.js @@ -76,7 +76,7 @@ export class ImportView extends Component { constructor(props) { super(props); - this.state = getDefaultState(DEFAULT_STATE, this.props.results); + this.state = getDefaultState(DEFAULT_STATE, this.props.results, this.props.capabilities); this.savedObjectsClient = props.savedObjectsClient; } @@ -85,7 +85,7 @@ export class ImportView extends Component { } clickReset = () => { - const state = getDefaultState(this.state, this.props.results); + const state = getDefaultState(this.state, this.props.results, this.props.capabilities); this.setState(state, () => { this.loadIndexPatternNames(); }); @@ -640,7 +640,7 @@ async function createKibanaIndexPattern(indexPatternName, indexPatterns, timeFie } } -function getDefaultState(state, results) { +function getDefaultState(state, results, capabilities) { const indexSettingsString = state.indexSettingsString === '' ? JSON.stringify(DEFAULT_INDEX_SETTINGS, null, 2) @@ -666,6 +666,11 @@ function getDefaultState(state, results) { const timeFieldName = results.timestamp_field; + const createIndexPattern = + capabilities.savedObjectsManagement.edit === false && capabilities.indexPatterns.save === false + ? false + : state.createIndexPattern; + return { ...DEFAULT_STATE, indexSettingsString, @@ -673,6 +678,7 @@ function getDefaultState(state, results) { pipelineString, timeFieldName, combinedFields, + createIndexPattern, }; } diff --git a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/file_data_visualizer.tsx b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/file_data_visualizer.tsx index 3644f7053f1e81..a82a2b7e25d858 100644 --- a/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/file_data_visualizer.tsx +++ b/x-pack/plugins/data_visualizer/public/application/file_data_visualizer/file_data_visualizer.tsx @@ -39,6 +39,7 @@ export const FileDataVisualizer: FC = ({ additionalLinks }) => { http={coreStart.http} fileUpload={fileUpload} resultsLinks={additionalLinks} + capabilities={coreStart.application.capabilities} /> ); diff --git a/x-pack/plugins/data_visualizer/public/plugin.ts b/x-pack/plugins/data_visualizer/public/plugin.ts index df1a5ea406d768..dd1d2acccf8cd7 100644 --- a/x-pack/plugins/data_visualizer/public/plugin.ts +++ b/x-pack/plugins/data_visualizer/public/plugin.ts @@ -22,6 +22,7 @@ import { getFileDataVisualizerComponent, getIndexDataVisualizerComponent } from import { getMaxBytesFormatted } from './application/common/util/get_max_bytes'; import { registerHomeAddData, registerHomeFeatureCatalogue } from './register_home'; import { registerEmbeddables } from './application/index_data_visualizer/embeddables'; +import { FieldFormatsStart } from '../../../../src/plugins/field_formats/public'; export interface DataVisualizerSetupDependencies { home?: HomePublicPluginSetup; @@ -36,6 +37,7 @@ export interface DataVisualizerStartDependencies { share: SharePluginStart; lens?: LensPublicStart; indexPatternFieldEditor?: IndexPatternFieldEditorStart; + fieldFormats: FieldFormatsStart; } export type DataVisualizerPluginSetup = ReturnType; diff --git a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts index 5d4844c3296d70..aa3020a9577f95 100644 --- a/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts +++ b/x-pack/plugins/enterprise_search/common/__mocks__/initial_app_data.ts @@ -7,7 +7,6 @@ export const DEFAULT_INITIAL_APP_DATA = { readOnlyMode: false, - ilmEnabled: true, searchOAuth: { clientId: 'someUID', redirectUrl: 'http://localhost:3002/ws/search_callback', diff --git a/x-pack/plugins/enterprise_search/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts index b0b9eb62748752..8addf17f974768 100644 --- a/x-pack/plugins/enterprise_search/common/types/index.ts +++ b/x-pack/plugins/enterprise_search/common/types/index.ts @@ -16,7 +16,6 @@ import { export interface InitialAppData { readOnlyMode?: boolean; - ilmEnabled?: boolean; searchOAuth?: SearchOAuth; configuredLimits?: ConfiguredLimits; access?: ProductAccess; diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts index f69e3492d26ebb..09b4292a290088 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.test.ts @@ -25,7 +25,6 @@ describe('AppLogic', () => { mount({}, DEFAULT_INITIAL_APP_DATA); expect(AppLogic.values).toEqual({ - ilmEnabled: true, configuredLimits: { engine: { maxDocumentByteSize: 102400, diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts index 90b37e6a4d4ee4..96bf4c062dbaf9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/app_logic.ts @@ -16,7 +16,6 @@ import { ConfiguredLimits, Account, Role } from './types'; import { getRoleAbilities } from './utils/role'; interface AppValues { - ilmEnabled: boolean; configuredLimits: ConfiguredLimits; account: Account; myRole: Role; @@ -41,7 +40,6 @@ export const AppLogic = kea { it('renders', () => { const wrapper = shallow(); expect(wrapper.find(EntSearchLogStream).prop('query')).toEqual( - 'appsearch.search_relevance_suggestions.query: some text and event.kind: event and event.dataset: search-relevance-suggestions and appsearch.search_relevance_suggestions.engine: foo and event.action: curation_suggestion and appsearch.search_relevance_suggestions.suggestion.new_status: automated' + 'appsearch.adaptive_relevance.query: some text and event.kind: event and event.dataset: search-relevance-suggestions and appsearch.adaptive_relevance.engine: foo and event.action: curation_suggestion and appsearch.adaptive_relevance.suggestion.new_status: automated' ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.tsx index debe8f86cfe2bf..7fb91daf2e590f 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/curation/automated_curation_history.tsx @@ -19,12 +19,12 @@ interface Props { export const AutomatedCurationHistory: React.FC = ({ query, engineName }) => { const filters = [ - `appsearch.search_relevance_suggestions.query: ${query}`, + `appsearch.adaptive_relevance.query: ${query}`, 'event.kind: event', 'event.dataset: search-relevance-suggestions', - `appsearch.search_relevance_suggestions.engine: ${engineName}`, + `appsearch.adaptive_relevance.engine: ${engineName}`, 'event.action: curation_suggestion', - 'appsearch.search_relevance_suggestions.suggestion.new_status: automated', + 'appsearch.adaptive_relevance.suggestion.new_status: automated', ]; return ( diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.test.tsx index 044637ff1c8234..36703dc0d0d859 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.test.tsx @@ -29,7 +29,7 @@ describe('AutomatedCurationsHistoryPanel', () => { expect(wrapper.is(DataPanel)).toBe(true); expect(wrapper.find(EntSearchLogStream).prop('query')).toEqual( - 'event.kind: event and event.dataset: search-relevance-suggestions and appsearch.search_relevance_suggestions.engine: some-engine and event.action: curation_suggestion and appsearch.search_relevance_suggestions.suggestion.new_status: automated' + 'event.kind: event and event.dataset: search-relevance-suggestions and appsearch.adaptive_relevance.engine: some-engine and event.action: curation_suggestion and appsearch.adaptive_relevance.suggestion.new_status: automated' ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.tsx index 901609718c8ecc..04f786b1ee1e1b 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/automated_curations_history_panel.tsx @@ -21,9 +21,9 @@ export const AutomatedCurationsHistoryPanel: React.FC = () => { const filters = [ 'event.kind: event', 'event.dataset: search-relevance-suggestions', - `appsearch.search_relevance_suggestions.engine: ${engineName}`, + `appsearch.adaptive_relevance.engine: ${engineName}`, 'event.action: curation_suggestion', - 'appsearch.search_relevance_suggestions.suggestion.new_status: automated', + 'appsearch.adaptive_relevance.suggestion.new_status: automated', ]; return ( @@ -54,7 +54,7 @@ export const AutomatedCurationsHistoryPanel: React.FC = () => { columns={[ { type: 'field', - field: 'appsearch.search_relevance_suggestions.query', + field: 'appsearch.adaptive_relevance.query', header: i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.automatedCurationsHistoryPanel.queryColumnHeader', { defaultMessage: 'Query' } diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.test.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.test.tsx index 28bb317941e1c8..58bf89a36d5ee6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.test.tsx @@ -29,7 +29,7 @@ describe('RejectedCurationsHistoryPanel', () => { expect(wrapper.is(DataPanel)).toBe(true); expect(wrapper.find(EntSearchLogStream).prop('query')).toEqual( - 'event.kind: event and event.dataset: search-relevance-suggestions and appsearch.search_relevance_suggestions.engine: some-engine and event.action: curation_suggestion and appsearch.search_relevance_suggestions.suggestion.new_status: rejected' + 'event.kind: event and event.dataset: search-relevance-suggestions and appsearch.adaptive_relevance.engine: some-engine and event.action: curation_suggestion and appsearch.adaptive_relevance.suggestion.new_status: rejected' ); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.tsx index 275083f91c0fb8..e7d66fc35a5062 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/curations/views/curations_history/components/rejected_curations_history_panel.tsx @@ -21,9 +21,9 @@ export const RejectedCurationsHistoryPanel: React.FC = () => { const filters = [ 'event.kind: event', 'event.dataset: search-relevance-suggestions', - `appsearch.search_relevance_suggestions.engine: ${engineName}`, + `appsearch.adaptive_relevance.engine: ${engineName}`, 'event.action: curation_suggestion', - 'appsearch.search_relevance_suggestions.suggestion.new_status: rejected', + 'appsearch.adaptive_relevance.suggestion.new_status: rejected', ]; return ( @@ -53,7 +53,7 @@ export const RejectedCurationsHistoryPanel: React.FC = () => { columns={[ { type: 'field', - field: 'appsearch.search_relevance_suggestions.query', + field: 'appsearch.adaptive_relevance.query', header: i18n.translate( 'xpack.enterpriseSearch.appSearch.engine.curations.rejectedCurationsHistoryPanel.queryColumnHeader', { defaultMessage: 'Query' } 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 e1e6820204d945..e30b6cec34d18e 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 @@ -83,14 +83,6 @@ export const NoLogging: React.FC = ({ type, disabledAt }) => { ); }; -export const ILMDisabled: React.FC = ({ type }) => ( - -); - export const CustomPolicy: React.FC = ({ type }) => ( { const analytics = LogRetentionOptions.Analytics; const api = LogRetentionOptions.API; - const setLogRetention = (logRetention: object, ilmEnabled: boolean = true) => { + const setLogRetention = (logRetention: object) => { const logRetentionSettings = { disabledAt: null, enabled: true, @@ -30,7 +30,6 @@ describe('LogRetentionMessage', () => { }; setMockValues({ - ilmEnabled, logRetention: { [LogRetentionOptions.API]: logRetentionSettings, [LogRetentionOptions.Analytics]: logRetentionSettings, @@ -155,22 +154,4 @@ describe('LogRetentionMessage', () => { }); }); }); - - describe('when ILM is disabled entirely', () => { - describe('an ILM disabled message renders', () => { - beforeEach(() => { - setLogRetention({}, false); - }); - - it('for analytics', () => { - const wrapper = mountWithIntl(); - expect(wrapper.text()).toEqual("App Search isn't managing analytics log retention."); - }); - - it('for api', () => { - const wrapper = mountWithIntl(); - expect(wrapper.text()).toEqual("App Search isn't managing API log retention."); - }); - }); - }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx index 7d34a2567ba145..c461de72edb889 100644 --- a/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/app_search/components/log_retention/messaging/log_retention_message.tsx @@ -9,18 +9,15 @@ import React from 'react'; import { useValues } from 'kea'; -import { AppLogic } from '../../../app_logic'; import { LogRetentionLogic } from '../log_retention_logic'; import { LogRetentionOptions } from '../types'; -import { NoLogging, ILMDisabled, CustomPolicy, DefaultPolicy } from './constants'; +import { NoLogging, CustomPolicy, DefaultPolicy } from './constants'; interface Props { type: LogRetentionOptions; } export const LogRetentionMessage: React.FC = ({ type }) => { - const { ilmEnabled } = useValues(AppLogic); - const { logRetention } = useValues(LogRetentionLogic); if (!logRetention) return null; @@ -30,9 +27,6 @@ export const LogRetentionMessage: React.FC = ({ type }) => { if (!logRetentionSettings.enabled) { return ; } - if (!ilmEnabled) { - return ; - } if (!logRetentionSettings.retentionPolicy?.isDefault) { return ; } else { diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts index 3f2a038d8bff3c..ba600de298976b 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.test.ts @@ -46,7 +46,6 @@ describe('callEnterpriseSearchConfigAPI', () => { settings: { external_url: 'http://some.vanity.url/', read_only_mode: false, - ilm_enabled: true, is_federated_auth: false, search_oauth: { client_id: 'someUID', @@ -139,7 +138,6 @@ describe('callEnterpriseSearchConfigAPI', () => { }, publicUrl: undefined, readOnlyMode: false, - ilmEnabled: false, searchOAuth: { clientId: undefined, redirectUrl: undefined, diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts index 146b06e4d9a4cc..d652d56c28efe0 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_config_api.ts @@ -74,7 +74,6 @@ export const callEnterpriseSearchConfigAPI = async ({ }, publicUrl: stripTrailingSlash(data?.settings?.external_url), readOnlyMode: !!data?.settings?.read_only_mode, - ilmEnabled: !!data?.settings?.ilm_enabled, searchOAuth: { clientId: data?.settings?.search_oauth?.client_id, redirectUrl: data?.settings?.search_oauth?.redirect_url, diff --git a/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts b/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts index ed5f8e07098d4c..b050a7c798a0bf 100644 --- a/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/fleet/common/types/rest_spec/package_policy.ts @@ -67,6 +67,12 @@ export type DeletePackagePoliciesResponse = Array<{ export interface UpgradePackagePolicyBaseResponse { name?: string; + + // Support generic errors + statusCode?: number; + body?: { + message: string; + }; } export interface UpgradePackagePolicyDryRunResponseItem extends UpgradePackagePolicyBaseResponse { diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx index 5411e6313ebb7d..76a7f0514a8a2c 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agents/agent_details_page/components/agent_logs/index.tsx @@ -56,7 +56,7 @@ export const AgentLogs: React.FunctionComponent(false); useEffect(() => { - const stateStorage = createKbnUrlStateStorage(); + const stateStorage = createKbnUrlStateStorage({ useHashQuery: false, useHash: false }); const { start, stop } = syncState({ storageKey: STATE_STORAGE_KEY, stateContainer: stateContainer as INullableBaseStateContainer, 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 1092b7ac89c072..bf4b1eb00abe09 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 @@ -368,7 +368,7 @@ const AgentPolicySelectionStep = ({ diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.test.ts b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.test.ts index d5d8aa093e300f..f69132d9a64524 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.test.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.test.ts @@ -120,6 +120,17 @@ describe('useMergeEprWithReplacements', () => { ]); }); + test('should filter out apm from package list', () => { + const eprPackages: PackageListItem[] = mockEprPackages([ + { + name: 'apm', + release: 'beta', + }, + ]); + + expect(useMergeEprPackagesWithReplacements(eprPackages, [])).toEqual([]); + }); + test('should consists of all 3 types (ga eprs, replacements for non-ga eprs, replacements without epr equivalent', () => { const eprPackages: PackageListItem[] = mockEprPackages([ { @@ -136,6 +147,10 @@ describe('useMergeEprWithReplacements', () => { name: 'activemq', release: 'beta', }, + { + name: 'apm', + release: 'ga', + }, ]); const replacements: CustomIntegration[] = mockIntegrations([ { diff --git a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.ts b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.ts index 4c59f0ef451235..ff1b51ef19a813 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.ts +++ b/x-pack/plugins/fleet/public/applications/integrations/hooks/use_merge_epr_with_replacements.ts @@ -8,6 +8,7 @@ import type { PackageListItem } from '../../../../common/types/models'; import type { CustomIntegration } from '../../../../../../../src/plugins/custom_integrations/common'; import { filterCustomIntegrations } from '../../../../../../../src/plugins/custom_integrations/public'; +import { FLEET_APM_PACKAGE } from '../../../../common/constants'; // Export this as a utility to find replacements for a package (e.g. in the overview-page for an EPR package) function findReplacementsForEprPackage( @@ -22,12 +23,17 @@ function findReplacementsForEprPackage( } export function useMergeEprPackagesWithReplacements( - eprPackages: PackageListItem[], + rawEprPackages: PackageListItem[], replacements: CustomIntegration[] ): Array { const merged: Array = []; const filteredReplacements = replacements; + // APM EPR-packages should _never_ show. They have special handling. + const eprPackages = rawEprPackages.filter((p) => { + return p.name !== FLEET_APM_PACKAGE; + }); + // Either select replacement or select beat eprPackages.forEach((eprPackage: PackageListItem) => { const hits = findReplacementsForEprPackage( diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx index f1d0717584e2e9..ca932554290bb3 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/available_packages.tsx @@ -221,6 +221,7 @@ export const AvailablePackages: React.FC = memo(() => { if (selectedCategory === '') { return true; } + return c.categories.includes(selectedCategory); }); @@ -255,7 +256,7 @@ export const AvailablePackages: React.FC = memo(() => { defaultMessage: 'Monitor, detect and diagnose complex performance issues from your application.', })} - href={addBasePath('/app/integrations/detail/apm')} + href={addBasePath('/app/home#/tutorial/apm')} icon={} /> diff --git a/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx b/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx index 24d9dc8e2c1004..1b0d90098fa48b 100644 --- a/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx +++ b/x-pack/plugins/fleet/public/components/home_integration/tutorial_module_notice.tsx @@ -13,6 +13,7 @@ import type { TutorialModuleNoticeComponent } from 'src/plugins/home/public'; import { useGetPackages, useLink, useCapabilities } from '../../hooks'; import { pkgKeyFromPackageInfo } from '../../services'; +import { FLEET_APM_PACKAGE } from '../../../common/constants'; const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName }) => { const { getHref } = useLink(); @@ -22,7 +23,7 @@ const TutorialModuleNotice: TutorialModuleNoticeComponent = memo(({ moduleName } const pkgInfo = !isLoading && packagesData?.response && - packagesData.response.find((pkg) => pkg.name === moduleName); + packagesData.response.find((pkg) => pkg.name === moduleName && pkg.name !== FLEET_APM_PACKAGE); // APM needs special handling if (hasIngestManager && pkgInfo) { return ( diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index 58463bfa5569d2..f61890f852798b 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -192,6 +192,8 @@ export const deletePackagePolicyHandler: RequestHandler< } }; +// TODO: Separate the upgrade and dry-run processes into separate endpoints, and address +// duplicate logic in error handling as part of https://github.com/elastic/kibana/issues/63123 export const upgradePackagePolicyHandler: RequestHandler< unknown, unknown, @@ -212,6 +214,16 @@ export const upgradePackagePolicyHandler: RequestHandler< ); body.push(result); } + + const firstFatalError = body.find((item) => item.statusCode && item.statusCode !== 200); + + if (firstFatalError) { + return response.customError({ + statusCode: firstFatalError.statusCode!, + body: { message: firstFatalError.body!.message }, + }); + } + return response.ok({ body, }); @@ -222,6 +234,15 @@ export const upgradePackagePolicyHandler: RequestHandler< request.body.packagePolicyIds, { user } ); + + const firstFatalError = body.find((item) => item.statusCode && item.statusCode !== 200); + + if (firstFatalError) { + return response.customError({ + statusCode: firstFatalError.statusCode!, + body: { message: firstFatalError.body!.message }, + }); + } return response.ok({ body, }); 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 306725ae01953b..e78bc096b8711b 100644 --- a/x-pack/plugins/fleet/server/services/managed_package_policies.ts +++ b/x-pack/plugins/fleet/server/services/managed_package_policies.ts @@ -72,7 +72,10 @@ export const upgradeManagedPackagePolicies = async ( ); if (dryRunResults.hasErrors) { - const errors = dryRunResults.diff?.[1].errors; + const errors = dryRunResults.diff + ? dryRunResults.diff?.[1].errors + : dryRunResults.body?.message; + appContextService .getLogger() .error( diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index 985351c3e981b5..c4ef15f4e7ed9f 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -620,13 +620,6 @@ class PackagePolicyService { success: true, }); } catch (error) { - // We only want to specifically handle validation errors for the new package policy. If a more severe or - // general error is thrown elsewhere during the upgrade process, we want to surface that directly in - // order to preserve any status code mappings, etc that might be included w/ the particular error type - if (!(error instanceof PackagePolicyValidationError)) { - throw error; - } - result.push({ id, success: false, @@ -704,10 +697,6 @@ class PackagePolicyService { hasErrors, }; } catch (error) { - if (!(error instanceof PackagePolicyValidationError)) { - throw error; - } - return { hasErrors: true, ...ingestErrorToResponseOptions(error), diff --git a/x-pack/plugins/infra/common/constants.ts b/x-pack/plugins/infra/common/constants.ts index 1c3aa550f2f629..4c70e34c9899f1 100644 --- a/x-pack/plugins/infra/common/constants.ts +++ b/x-pack/plugins/infra/common/constants.ts @@ -8,7 +8,6 @@ export const DEFAULT_SOURCE_ID = 'default'; export const METRICS_INDEX_PATTERN = 'metrics-*,metricbeat-*'; export const LOGS_INDEX_PATTERN = 'logs-*,filebeat-*,kibana_sample_data_logs*'; -export const TIMESTAMP_FIELD = '@timestamp'; export const METRICS_APP = 'metrics'; export const LOGS_APP = 'logs'; @@ -16,3 +15,9 @@ export const METRICS_FEATURE_ID = 'infrastructure'; export const LOGS_FEATURE_ID = 'logs'; export type InfraFeatureId = typeof METRICS_FEATURE_ID | typeof LOGS_FEATURE_ID; + +export const TIMESTAMP_FIELD = '@timestamp'; +export const TIEBREAKER_FIELD = '_doc'; +export const HOST_FIELD = 'host.name'; +export const CONTAINER_FIELD = 'container.id'; +export const POD_FIELD = 'kubernetes.pod.uid'; diff --git a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts index 79835a0a78f262..395b1527379a9a 100644 --- a/x-pack/plugins/infra/common/http_api/host_details/process_list.ts +++ b/x-pack/plugins/infra/common/http_api/host_details/process_list.ts @@ -14,7 +14,6 @@ const AggValueRT = rt.type({ export const ProcessListAPIRequestRT = rt.type({ hostTerm: rt.record(rt.string, rt.string), - timefield: rt.string, indexPattern: rt.string, to: rt.number, sortBy: rt.type({ @@ -102,7 +101,6 @@ export type ProcessListAPIResponse = rt.TypeOf; export const ProcessListAPIChartRequestRT = rt.type({ hostTerm: rt.record(rt.string, rt.string), - timefield: rt.string, indexPattern: rt.string, to: rt.number, command: rt.string, diff --git a/x-pack/plugins/infra/common/http_api/metrics_api.ts b/x-pack/plugins/infra/common/http_api/metrics_api.ts index c2449707647d74..315a42380397bd 100644 --- a/x-pack/plugins/infra/common/http_api/metrics_api.ts +++ b/x-pack/plugins/infra/common/http_api/metrics_api.ts @@ -10,7 +10,6 @@ import { MetricsUIAggregationRT } from '../inventory_models/types'; import { afterKeyObjectRT } from './metrics_explorer'; export const MetricsAPITimerangeRT = rt.type({ - field: rt.string, from: rt.number, to: rt.number, interval: rt.string, diff --git a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts index 5617bd0954f5d9..de00d521126e36 100644 --- a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts +++ b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts @@ -41,7 +41,6 @@ export const metricsExplorerMetricRT = rt.intersection([ ]); export const timeRangeRT = rt.type({ - field: rt.string, from: rt.number, to: rt.number, interval: rt.string, diff --git a/x-pack/plugins/infra/common/inventory_models/index.ts b/x-pack/plugins/infra/common/inventory_models/index.ts index 6350e76ca7f29c..81f89be8cd6a6a 100644 --- a/x-pack/plugins/infra/common/inventory_models/index.ts +++ b/x-pack/plugins/infra/common/inventory_models/index.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { POD_FIELD, HOST_FIELD, CONTAINER_FIELD } from '../constants'; import { host } from './host'; import { pod } from './pod'; import { awsEC2 } from './aws_ec2'; @@ -30,31 +31,23 @@ export const findInventoryModel = (type: InventoryItemType) => { return model; }; -interface InventoryFields { - host: string; - pod: string; - container: string; - timestamp: string; - tiebreaker: string; -} - const LEGACY_TYPES = ['host', 'pod', 'container']; -const getFieldByType = (type: InventoryItemType, fields: InventoryFields) => { +export const getFieldByType = (type: InventoryItemType) => { switch (type) { case 'pod': - return fields.pod; + return POD_FIELD; case 'host': - return fields.host; + return HOST_FIELD; case 'container': - return fields.container; + return CONTAINER_FIELD; } }; -export const findInventoryFields = (type: InventoryItemType, fields?: InventoryFields) => { +export const findInventoryFields = (type: InventoryItemType) => { const inventoryModel = findInventoryModel(type); - if (fields && LEGACY_TYPES.includes(type)) { - const id = getFieldByType(type, fields) || inventoryModel.fields.id; + if (LEGACY_TYPES.includes(type)) { + const id = getFieldByType(type) || inventoryModel.fields.id; return { ...inventoryModel.fields, id, diff --git a/x-pack/plugins/infra/common/log_sources/log_source_configuration.ts b/x-pack/plugins/infra/common/log_sources/log_source_configuration.ts index ab98ad75b8433f..5d46ce59457da2 100644 --- a/x-pack/plugins/infra/common/log_sources/log_source_configuration.ts +++ b/x-pack/plugins/infra/common/log_sources/log_source_configuration.ts @@ -16,11 +16,6 @@ export const logSourceConfigurationOriginRT = rt.keyof({ export type LogSourceConfigurationOrigin = rt.TypeOf; const logSourceFieldsConfigurationRT = rt.strict({ - container: rt.string, - host: rt.string, - pod: rt.string, - timestamp: rt.string, - tiebreaker: rt.string, message: rt.array(rt.string), }); diff --git a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts b/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts index c6bc10901fcb86..d3459b30a060e3 100644 --- a/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts +++ b/x-pack/plugins/infra/common/log_sources/resolved_log_source_configuration.ts @@ -8,6 +8,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { DataView, DataViewsContract } from '../../../../../src/plugins/data_views/common'; import { ObjectEntries } from '../utility_types'; +import { TIMESTAMP_FIELD, TIEBREAKER_FIELD } from '../constants'; import { ResolveLogSourceConfigurationError } from './errors'; import { LogSourceColumnConfiguration, @@ -61,8 +62,8 @@ const resolveLegacyReference = async ( return { indices: sourceConfiguration.logIndices.indexName, - timestampField: sourceConfiguration.fields.timestamp, - tiebreakerField: sourceConfiguration.fields.tiebreaker, + timestampField: TIMESTAMP_FIELD, + tiebreakerField: TIEBREAKER_FIELD, messageField: sourceConfiguration.fields.message, fields, runtimeMappings: {}, @@ -91,8 +92,8 @@ const resolveKibanaIndexPatternReference = async ( return { indices: indexPattern.title, - timestampField: indexPattern.timeFieldName ?? '@timestamp', - tiebreakerField: '_doc', + timestampField: indexPattern.timeFieldName ?? TIMESTAMP_FIELD, + tiebreakerField: TIEBREAKER_FIELD, messageField: ['message'], fields: indexPattern.fields, runtimeMappings: resolveRuntimeMappings(indexPattern), diff --git a/x-pack/plugins/infra/common/metrics_sources/index.ts b/x-pack/plugins/infra/common/metrics_sources/index.ts index a697c65e5a0aa8..7fae908707a899 100644 --- a/x-pack/plugins/infra/common/metrics_sources/index.ts +++ b/x-pack/plugins/infra/common/metrics_sources/index.ts @@ -6,7 +6,6 @@ */ import * as rt from 'io-ts'; -import { omit } from 'lodash'; import { SourceConfigurationRT, SourceStatusRuntimeType, @@ -22,7 +21,6 @@ export const metricsSourceConfigurationPropertiesRT = rt.strict({ metricAlias: SourceConfigurationRT.props.metricAlias, inventoryDefaultView: SourceConfigurationRT.props.inventoryDefaultView, metricsExplorerDefaultView: SourceConfigurationRT.props.metricsExplorerDefaultView, - fields: rt.strict(omit(SourceConfigurationRT.props.fields.props, 'message')), anomalyThreshold: rt.number, }); @@ -32,9 +30,6 @@ export type MetricsSourceConfigurationProperties = rt.TypeOf< export const partialMetricsSourceConfigurationPropertiesRT = rt.partial({ ...metricsSourceConfigurationPropertiesRT.type.props, - fields: rt.partial({ - ...metricsSourceConfigurationPropertiesRT.type.props.fields.type.props, - }), }); export type PartialMetricsSourceConfigurationProperties = rt.TypeOf< diff --git a/x-pack/plugins/infra/common/source_configuration/source_configuration.ts b/x-pack/plugins/infra/common/source_configuration/source_configuration.ts index 257cccc86595cd..0c30c3d678b2a3 100644 --- a/x-pack/plugins/infra/common/source_configuration/source_configuration.ts +++ b/x-pack/plugins/infra/common/source_configuration/source_configuration.ts @@ -50,12 +50,7 @@ export const sourceConfigurationConfigFilePropertiesRT = rt.type({ sources: rt.type({ default: rt.partial({ fields: rt.partial({ - timestamp: rt.string, message: rt.array(rt.string), - tiebreaker: rt.string, - host: rt.string, - container: rt.string, - pod: rt.string, }), }), }), @@ -113,11 +108,6 @@ export type InfraSourceConfigurationColumn = rt.TypeOf = (props) => { const { setAlertParams, alertParams, errors, metadata } = props; - const { http, notifications } = useKibanaContextForPlugin().services; + const { http, notifications, docLinks } = useKibanaContextForPlugin().services; const { source, createDerivedIndexPattern } = useSourceViaHttp({ sourceId: 'default', fetch: http.fetch, @@ -260,6 +261,14 @@ export const Expressions: React.FC = (props) => { [alertParams.groupBy] ); + const disableNoData = useMemo( + () => alertParams.criteria?.every((c) => c.aggType === Aggregators.COUNT), + [alertParams.criteria] + ); + + // Test to see if any of the group fields in groupBy are already filtered down to a single + // group by the filterQuery. If this is the case, then a groupBy is unnecessary, as it would only + // ever produce one group instance const groupByFilterTestPatterns = useMemo(() => { if (!alertParams.groupBy) return null; const groups = !Array.isArray(alertParams.groupBy) @@ -354,6 +363,7 @@ export const Expressions: React.FC = (props) => { > @@ -361,10 +371,13 @@ export const Expressions: React.FC = (props) => { defaultMessage: "Alert me if there's no data", })}{' '} @@ -456,10 +469,20 @@ export const Expressions: React.FC = (props) => { {redundantFilterGroupBy.join(', ')}, groupCount: redundantFilterGroupBy.length, + filteringAndGroupingLink: ( + + {i18n.translate( + 'xpack.infra.metrics.alertFlyout.alertPerRedundantFilterError.docsLink', + { defaultMessage: 'the docs' } + )} + + ), }} /> @@ -474,16 +497,19 @@ export const Expressions: React.FC = (props) => { defaultMessage: 'Alert me if a group stops reporting data', })}{' '} } - disabled={!hasGroupBy} + disabled={disableNoData || !hasGroupBy} checked={Boolean(hasGroupBy && alertParams.alertOnGroupDisappear)} onChange={(e) => setAlertParams('alertOnGroupDisappear', e.target.checked)} /> @@ -492,6 +518,13 @@ export const Expressions: React.FC = (props) => { ); }; +const docCountNoDataDisabledHelpText = i18n.translate( + 'xpack.infra.metrics.alertFlyout.docCountNoDataDisabledHelpText', + { + defaultMessage: '[This setting is not applicable to the Document Count aggregator.]', + } +); + // required for dynamic import // eslint-disable-next-line import/no-default-export export default Expressions; diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx index ec97d01a1cd6f5..c2c1fa719bb95b 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/components/expression_chart.test.tsx @@ -54,14 +54,6 @@ describe('ExpressionChart', () => { metricAlias: 'metricbeat-*', inventoryDefaultView: 'host', metricsExplorerDefaultView: 'host', - // @ts-ignore - fields: { - timestamp: '@timestamp', - container: 'container.id', - host: 'host.name', - pod: 'kubernetes.pod.uid', - tiebreaker: '_doc', - }, anomalyThreshold: 20, }, }; diff --git a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts index 6021c728d32afa..204fae7dc0f2b5 100644 --- a/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts +++ b/x-pack/plugins/infra/public/containers/logs/log_source/log_source.mock.ts @@ -73,11 +73,6 @@ export const createBasicSourceConfiguration = (sourceId: string): LogSourceConfi }, logColumns: [], fields: { - container: 'CONTAINER_FIELD', - host: 'HOST_FIELD', - pod: 'POD_FIELD', - tiebreaker: 'TIEBREAKER_FIELD', - timestamp: 'TIMESTAMP_FIELD', message: ['MESSAGE_FIELD'], }, name: sourceId, diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx b/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx index 198a99f3948507..22376648ca0033 100644 --- a/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module.tsx @@ -19,7 +19,7 @@ export const useInfraMLModule = ({ moduleDescriptor: ModuleDescriptor; }) => { const { services } = useKibanaContextForPlugin(); - const { spaceId, sourceId, timestampField } = sourceConfiguration; + const { spaceId, sourceId } = sourceConfiguration; const [moduleStatus, dispatchModuleStatus] = useModuleStatus(moduleDescriptor.jobTypes); const [, fetchJobStatus] = useTrackedPromise( @@ -64,7 +64,6 @@ export const useInfraMLModule = ({ indices: selectedIndices, sourceId, spaceId, - timestampField, }, partitionField, }, @@ -91,7 +90,7 @@ export const useInfraMLModule = ({ dispatchModuleStatus({ type: 'failedSetup' }); }, }, - [moduleDescriptor.setUpModule, spaceId, sourceId, timestampField] + [moduleDescriptor.setUpModule, spaceId, sourceId] ); const [cleanUpModuleRequest, cleanUpModule] = useTrackedPromise( diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_configuration.ts b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_configuration.ts index 4c876c1705364c..c258debdddbca5 100644 --- a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_configuration.ts +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_configuration.ts @@ -45,8 +45,7 @@ export const isJobConfigurationOutdated = isSubset( new Set(jobConfiguration.indexPattern.split(',')), new Set(currentSourceConfiguration.indices) - ) && - jobConfiguration.timestampField === currentSourceConfiguration.timestampField + ) ); }; diff --git a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts index ca655f35f74668..9b172a7c82a989 100644 --- a/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts +++ b/x-pack/plugins/infra/public/containers/ml/infra_ml_module_types.ts @@ -49,12 +49,10 @@ export interface ModuleDescriptor { ) => Promise; validateSetupIndices?: ( indices: string[], - timestampField: string, fetch: HttpHandler ) => Promise; validateSetupDatasets?: ( indices: string[], - timestampField: string, startTime: number, endTime: number, fetch: HttpHandler @@ -65,7 +63,6 @@ export interface ModuleSourceConfiguration { indices: string[]; sourceId: string; spaceId: string; - timestampField: string; } interface ManyCategoriesWarningReason { diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module.tsx b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module.tsx index f892ab62ee3d80..f200ab22c043f8 100644 --- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module.tsx +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module.tsx @@ -17,21 +17,18 @@ export const useMetricHostsModule = ({ indexPattern, sourceId, spaceId, - timestampField, }: { indexPattern: string; sourceId: string; spaceId: string; - timestampField: string; }) => { const sourceConfiguration: ModuleSourceConfiguration = useMemo( () => ({ indices: indexPattern.split(','), sourceId, spaceId, - timestampField, }), - [indexPattern, sourceId, spaceId, timestampField] + [indexPattern, sourceId, spaceId] ); const infraMLModule = useInfraMLModule({ diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts index a7ab948d052aab..f87cd78f4ff347 100644 --- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_hosts/module_descriptor.ts @@ -18,6 +18,7 @@ import { MetricsHostsJobType, bucketSpan, } from '../../../../../common/infra_ml'; +import { TIMESTAMP_FIELD } from '../../../../../common/constants'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import MemoryJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_hosts/ml/hosts_memory_usage.json'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -68,7 +69,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) start, end, filter, - moduleSourceConfiguration: { spaceId, sourceId, indices, timestampField }, + moduleSourceConfiguration: { spaceId, sourceId, indices }, partitionField, } = setUpModuleArgs; @@ -93,13 +94,13 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) return { job_id: id, data_description: { - time_field: timestampField, + time_field: TIMESTAMP_FIELD, }, analysis_config, custom_settings: { metrics_source_config: { indexPattern: indexNamePattern, - timestampField, + timestampField: TIMESTAMP_FIELD, bucketSpan, }, }, diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module.tsx b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module.tsx index eadc374434817b..08f4f49058dbe2 100644 --- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module.tsx +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module.tsx @@ -17,21 +17,18 @@ export const useMetricK8sModule = ({ indexPattern, sourceId, spaceId, - timestampField, }: { indexPattern: string; sourceId: string; spaceId: string; - timestampField: string; }) => { const sourceConfiguration: ModuleSourceConfiguration = useMemo( () => ({ indices: indexPattern.split(','), sourceId, spaceId, - timestampField, }), - [indexPattern, sourceId, spaceId, timestampField] + [indexPattern, sourceId, spaceId] ); const infraMLModule = useInfraMLModule({ diff --git a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts index 4c5eb5fd4bf239..388a7dd0a56568 100644 --- a/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts +++ b/x-pack/plugins/infra/public/containers/ml/modules/metrics_k8s/module_descriptor.ts @@ -18,6 +18,7 @@ import { MetricK8sJobType, bucketSpan, } from '../../../../../common/infra_ml'; +import { TIMESTAMP_FIELD } from '../../../../../common/constants'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import MemoryJob from '../../../../../../ml/server/models/data_recognizer/modules/metrics_ui_k8s/ml/k8s_memory_usage.json'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -69,7 +70,7 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) start, end, filter, - moduleSourceConfiguration: { spaceId, sourceId, indices, timestampField }, + moduleSourceConfiguration: { spaceId, sourceId, indices }, partitionField, } = setUpModuleArgs; @@ -93,13 +94,13 @@ const setUpModule = async (setUpModuleArgs: SetUpModuleArgs, fetch: HttpHandler) return { job_id: id, data_description: { - time_field: timestampField, + time_field: TIMESTAMP_FIELD, }, analysis_config, custom_settings: { metrics_source_config: { indexPattern: indexNamePattern, - timestampField, + timestampField: TIMESTAMP_FIELD, bucketSpan, }, }, diff --git a/x-pack/plugins/infra/public/lib/lib.ts b/x-pack/plugins/infra/public/lib/lib.ts index 97a3f8eabbe4ed..a37a9af7d93200 100644 --- a/x-pack/plugins/infra/public/lib/lib.ts +++ b/x-pack/plugins/infra/public/lib/lib.ts @@ -14,7 +14,6 @@ import { SnapshotNodeMetric, SnapshotNodePath, } from '../../common/http_api/snapshot_api'; -import { MetricsSourceConfigurationProperties } from '../../common/metrics_sources'; import { WaffleSortOption } from '../pages/metrics/inventory_view/hooks/use_waffle_options'; export interface InfraWaffleMapNode { @@ -124,7 +123,6 @@ export enum InfraWaffleMapRuleOperator { } export interface InfraWaffleMapOptions { - fields?: Omit | null; formatter: InfraFormatterType; formatTemplate: string; metric: SnapshotMetricInput; diff --git a/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx index f9c80edd2c1995..cfcf8db771b788 100644 --- a/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/link_to_logs.test.tsx @@ -151,7 +151,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'HOST_FIELD: HOST_NAME')"` + `"(language:kuery,query:'host.name: HOST_NAME')"` ); expect(searchParams.get('logPosition')).toEqual(null); }); @@ -172,7 +172,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'(HOST_FIELD: HOST_NAME) and (FILTER_FIELD:FILTER_VALUE)')"` + `"(language:kuery,query:'(host.name: HOST_NAME) and (FILTER_FIELD:FILTER_VALUE)')"` ); expect(searchParams.get('logPosition')).toMatchInlineSnapshot( `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` @@ -193,7 +193,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('OTHER_SOURCE'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'HOST_FIELD: HOST_NAME')"` + `"(language:kuery,query:'host.name: HOST_NAME')"` ); expect(searchParams.get('logPosition')).toEqual(null); }); @@ -229,7 +229,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'CONTAINER_FIELD: CONTAINER_ID')"` + `"(language:kuery,query:'container.id: CONTAINER_ID')"` ); expect(searchParams.get('logPosition')).toEqual(null); }); @@ -250,7 +250,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'(CONTAINER_FIELD: CONTAINER_ID) and (FILTER_FIELD:FILTER_VALUE)')"` + `"(language:kuery,query:'(container.id: CONTAINER_ID) and (FILTER_FIELD:FILTER_VALUE)')"` ); expect(searchParams.get('logPosition')).toMatchInlineSnapshot( `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` @@ -287,7 +287,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'POD_FIELD: POD_UID')"` + `"(language:kuery,query:'kubernetes.pod.uid: POD_UID')"` ); expect(searchParams.get('logPosition')).toEqual(null); }); @@ -306,7 +306,7 @@ describe('LinkToLogsPage component', () => { const searchParams = new URLSearchParams(history.location.search); expect(searchParams.get('sourceId')).toEqual('default'); expect(searchParams.get('logFilter')).toMatchInlineSnapshot( - `"(language:kuery,query:'(POD_FIELD: POD_UID) and (FILTER_FIELD:FILTER_VALUE)')"` + `"(language:kuery,query:'(kubernetes.pod.uid: POD_UID) and (FILTER_FIELD:FILTER_VALUE)')"` ); expect(searchParams.get('logPosition')).toMatchInlineSnapshot( `"(end:'2019-02-20T14:58:09.404Z',position:(tiebreaker:0,time:1550671089404),start:'2019-02-20T12:58:09.404Z',streamLive:!f)"` diff --git a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx index bc8c5699229d81..a8d339cfe979ab 100644 --- a/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx +++ b/x-pack/plugins/infra/public/pages/link_to/redirect_to_node_logs.tsx @@ -34,12 +34,11 @@ export const RedirectToNodeLogs = ({ location, }: RedirectToNodeLogsType) => { const { services } = useKibanaContextForPlugin(); - const { isLoading, loadSource, sourceConfiguration } = useLogSource({ + const { isLoading, loadSource } = useLogSource({ fetch: services.http.fetch, sourceId, indexPatternsService: services.data.indexPatterns, }); - const fields = sourceConfiguration?.configuration.fields; useMount(() => { loadSource(); @@ -57,11 +56,9 @@ export const RedirectToNodeLogs = ({ })} /> ); - } else if (fields == null) { - return null; } - const nodeFilter = `${findInventoryFields(nodeType, fields).id}: ${nodeId}`; + const nodeFilter = `${findInventoryFields(nodeType).id}: ${nodeId}`; const userFilter = getFilterFromLocation(location); const filter = userFilter ? `(${nodeFilter}) and (${userFilter})` : nodeFilter; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx index de0a56c5be73da..f46a379f52d50f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/layout.tsx @@ -18,7 +18,6 @@ import { PageContent } from '../../../../components/page'; import { useWaffleTimeContext } from '../hooks/use_waffle_time'; import { useWaffleFiltersContext } from '../hooks/use_waffle_filters'; import { DEFAULT_LEGEND, useWaffleOptionsContext } from '../hooks/use_waffle_options'; -import { useSourceContext } from '../../../../containers/metrics_source'; import { InfraFormatterType } from '../../../../lib/lib'; import { euiStyled } from '../../../../../../../../src/plugins/kibana_react/common'; import { Toolbar } from './toolbars/toolbar'; @@ -41,7 +40,6 @@ interface Props { export const Layout = React.memo( ({ shouldLoadDefault, currentView, reload, interval, nodes, loading }: Props) => { const [showLoading, setShowLoading] = useState(true); - const { source } = useSourceContext(); const { metric, groupBy, @@ -65,7 +63,6 @@ export const Layout = React.memo( legend: createLegend(legendPalette, legendSteps, legendReverseColors), metric, sort, - fields: source?.configuration?.fields, groupBy, }; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx index 4e28fb4202bdc2..1fcec291fcc296 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/ml/anomaly_detection/anomaly_detection_flyout.tsx @@ -67,13 +67,11 @@ export const AnomalyDetectionFlyout = () => { indexPattern={source?.configuration.metricAlias ?? ''} sourceId={'default'} spaceId={space.id} - timestampField={source?.configuration.fields.timestamp ?? ''} > {screenName === 'home' && ( diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx index b792078c394e99..8b5224068589cf 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/logs.tsx @@ -25,15 +25,13 @@ const TabComponent = (props: TabProps) => { const endTimestamp = props.currentTime; const startTimestamp = endTimestamp - 60 * 60 * 1000; // 60 minutes const { nodeType } = useWaffleOptionsContext(); - const { options, node } = props; + const { node } = props; const throttledTextQuery = useThrottle(textQuery, textQueryThrottleInterval); const filter = useMemo(() => { const query = [ - ...(options.fields != null - ? [`${findInventoryFields(nodeType, options.fields).id}: "${node.id}"`] - : []), + `${findInventoryFields(nodeType).id}: "${node.id}"`, ...(throttledTextQuery !== '' ? [throttledTextQuery] : []), ].join(' and '); @@ -41,7 +39,7 @@ const TabComponent = (props: TabProps) => { language: 'kuery', query, }; - }, [options.fields, nodeType, node.id, throttledTextQuery]); + }, [nodeType, node.id, throttledTextQuery]); const onQueryChange = useCallback((e: React.ChangeEvent) => { setTextQuery(e.target.value); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx index fbb8bd469c1e1f..7ff4720aec01e3 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/metrics/metrics.tsx @@ -71,14 +71,12 @@ const TabComponent = (props: TabProps) => { ]); const { sourceId, createDerivedIndexPattern } = useSourceContext(); const { nodeType, accountId, region, customMetrics } = useWaffleOptionsContext(); - const { currentTime, options, node } = props; + const { currentTime, node } = props; const derivedIndexPattern = useMemo( () => createDerivedIndexPattern('metrics'), [createDerivedIndexPattern] ); - let filter = options.fields - ? `${findInventoryFields(nodeType, options.fields).id}: "${node.id}"` - : ''; + let filter = `${findInventoryFields(nodeType).id}: "${node.id}"`; if (filter) { filter = convertKueryToElasticSearchQuery(filter, derivedIndexPattern); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx index c227a31edc4ab3..2bed7681b8d56f 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/node_details/tabs/processes/index.tsx @@ -17,6 +17,7 @@ import { EuiIconTip, Query, } from '@elastic/eui'; +import { getFieldByType } from '../../../../../../../../common/inventory_models'; import { useProcessList, SortBy, @@ -28,7 +29,7 @@ import { SummaryTable } from './summary_table'; import { ProcessesTable } from './processes_table'; import { parseSearchString } from './parse_search_string'; -const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { +const TabComponent = ({ currentTime, node, nodeType }: TabProps) => { const [searchBarState, setSearchBarState] = useState(Query.MATCH_ALL); const [searchFilter, setSearchFilter] = useState(''); const [sortBy, setSortBy] = useState({ @@ -36,22 +37,17 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { isAscending: false, }); - const timefield = options.fields!.timestamp; - const hostTerm = useMemo(() => { - const field = - options.fields && Reflect.has(options.fields, nodeType) - ? Reflect.get(options.fields, nodeType) - : nodeType; + const field = getFieldByType(nodeType) ?? nodeType; return { [field]: node.name }; - }, [options, node, nodeType]); + }, [node, nodeType]); const { loading, error, response, makeRequest: reload, - } = useProcessList(hostTerm, timefield, currentTime, sortBy, parseSearchString(searchFilter)); + } = useProcessList(hostTerm, currentTime, sortBy, parseSearchString(searchFilter)); const debouncedSearchOnChange = useMemo( () => debounce<(queryText: string) => void>((queryText) => setSearchFilter(queryText), 500), @@ -73,7 +69,7 @@ const TabComponent = ({ currentTime, node, nodeType, options }: TabProps) => { return ( - + { {isAlertFlyoutVisible && ( = withTheme return { label: host.ip, value: node.ip }; } } else { - if (options.fields) { - const { id } = findInventoryFields(nodeType, options.fields); - return { - label: {id}, - value: node.id, - }; - } + const { id } = findInventoryFields(nodeType); + return { + label: {id}, + value: node.id, + }; } return { label: '', value: '' }; - }, [nodeType, node.ip, node.id, options.fields]); + }, [nodeType, node.ip, node.id]); const nodeLogsMenuItemLinkProps = useLinkProps( getNodeLogsUrl({ @@ -184,11 +182,7 @@ export const NodeContextMenu: React.FC = withTheme {flyoutVisible && ( , - timefield: string, to: number, sortBy: SortBy, searchFilter: object @@ -51,7 +50,6 @@ export function useProcessList( 'POST', JSON.stringify({ hostTerm, - timefield, indexPattern, to, sortBy: parsedSortBy, @@ -75,15 +73,11 @@ export function useProcessList( }; } -function useProcessListParams(props: { - hostTerm: Record; - timefield: string; - to: number; -}) { - const { hostTerm, timefield, to } = props; +function useProcessListParams(props: { hostTerm: Record; to: number }) { + const { hostTerm, to } = props; const { createDerivedIndexPattern } = useSourceContext(); const indexPattern = createDerivedIndexPattern('metrics').title; - return { hostTerm, indexPattern, timefield, to }; + return { hostTerm, indexPattern, to }; } const ProcessListContext = createContainter(useProcessListParams); export const [ProcessListContextProvider, useProcessListContext] = ProcessListContext; diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list_row_chart.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list_row_chart.ts index 30d4e5960ba5eb..0d718ffbe210c8 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list_row_chart.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/hooks/use_process_list_row_chart.ts @@ -25,14 +25,13 @@ export function useProcessListRowChart(command: string) { fold(throwErrors(createPlainError), identity) ); }; - const { hostTerm, timefield, indexPattern, to } = useProcessListContext(); + const { hostTerm, indexPattern, to } = useProcessListContext(); const { error, loading, response, makeRequest } = useHTTPRequest( '/api/metrics/process_list/chart', 'POST', JSON.stringify({ hostTerm, - timefield, indexPattern, to, command, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts index dbe45a387891ce..af93f6c0d62cee 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.test.ts @@ -10,13 +10,6 @@ import { InfraWaffleMapOptions, InfraFormatterType } from '../../../../lib/lib'; import { SnapshotMetricType } from '../../../../../common/inventory_models/types'; const options: InfraWaffleMapOptions = { - fields: { - container: 'container.id', - pod: 'kubernetes.pod.uid', - host: 'host.name', - timestamp: '@timestanp', - tiebreaker: '@timestamp', - }, formatter: InfraFormatterType.percent, formatTemplate: '{{value}}', metric: { type: 'cpu' }, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts index 5c02893b867dec..b6fa4fe4273abc 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/create_uptime_link.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { get } from 'lodash'; import { InfraWaffleMapNode, InfraWaffleMapOptions } from '../../../../lib/lib'; import { InventoryItemType } from '../../../../../common/inventory_models/types'; +import { getFieldByType } from '../../../../../common/inventory_models'; import { LinkDescriptor } from '../../../../hooks/use_link_props'; export const createUptimeLink = ( @@ -24,7 +24,7 @@ export const createUptimeLink = ( }, }; } - const field = get(options, ['fields', nodeType], ''); + const field = getFieldByType(nodeType); return { app: 'uptime', hash: '/', diff --git a/x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/get_filtered_metrics.ts b/x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/get_filtered_metrics.ts index 2339319926da81..d1ba4502f37c33 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/get_filtered_metrics.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metric_detail/lib/get_filtered_metrics.ts @@ -8,6 +8,7 @@ import { InfraMetadataFeature } from '../../../../../common/http_api/metadata_api'; import { InventoryMetric } from '../../../../../common/inventory_models/types'; import { metrics } from '../../../../../common/inventory_models/metrics'; +import { TIMESTAMP_FIELD } from '../../../../../common/constants'; export const getFilteredMetrics = ( requiredMetrics: InventoryMetric[], @@ -20,7 +21,7 @@ export const getFilteredMetrics = ( const metricModelCreator = metrics.tsvb[metric]; // We just need to get a dummy version of the model so we can filter // using the `requires` attribute. - const metricModel = metricModelCreator('@timestamp', 'test', '>=1m'); + const metricModel = metricModelCreator(TIMESTAMP_FIELD, 'test', '>=1m'); return metricMetadata.some((m) => m && metricModel.requires.includes(m)); }); }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx index 005dd5cc8c078e..581eec3e824ae0 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/chart_context_menu.tsx @@ -27,6 +27,7 @@ import { import { createTSVBLink } from './helpers/create_tsvb_link'; import { getNodeDetailUrl } from '../../../link_to/redirect_to_node_detail'; import { InventoryItemType } from '../../../../../common/inventory_models/types'; +import { HOST_FIELD, POD_FIELD, CONTAINER_FIELD } from '../../../../../common/constants'; import { useLinkProps } from '../../../../hooks/use_link_props'; export interface Props { @@ -44,13 +45,13 @@ const fieldToNodeType = ( groupBy: string | string[] ): InventoryItemType | undefined => { const fields = Array.isArray(groupBy) ? groupBy : [groupBy]; - if (fields.includes(source.fields.host)) { + if (fields.includes(HOST_FIELD)) { return 'host'; } - if (fields.includes(source.fields.pod)) { + if (fields.includes(POD_FIELD)) { return 'pod'; } - if (fields.includes(source.fields.container)) { + if (fields.includes(CONTAINER_FIELD)) { return 'container'; } }; diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts index a9e65bc30a3c61..472e86200cba35 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.test.ts @@ -79,7 +79,7 @@ describe('createTSVBLink()', () => { app: 'visualize', hash: '/create', search: { - _a: "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))", + _a: "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))", _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))', type: 'metrics', }, @@ -97,7 +97,7 @@ describe('createTSVBLink()', () => { app: 'visualize', hash: '/create', search: { - _a: "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'system.network.name:lo* and host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))", + _a: "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'my-beats-*',filter:(language:kuery,query:'system.network.name:lo* and host.name : \"example-01\"'),id:test-id,index_pattern:'my-beats-*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))", _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))', type: 'metrics', }, @@ -161,7 +161,7 @@ describe('createTSVBLink()', () => { app: 'visualize', hash: '/create', search: { - _a: "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metric*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metric*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:time,type:timeseries),title:example-01,type:metrics))", + _a: "(filters:!(),linked:!f,query:(language:kuery,query:''),uiState:(),vis:(aggs:!(),params:(axis_formatter:number,axis_min:0,axis_position:left,axis_scale:normal,default_index_pattern:'metric*',filter:(language:kuery,query:'host.name : \"example-01\"'),id:test-id,index_pattern:'metric*',interval:auto,series:!((axis_position:right,chart_type:line,color:#6092C0,fill:0,formatter:percent,id:test-id,label:'avg(system.cpu.user.pct)',line_width:2,metrics:!((field:system.cpu.user.pct,id:test-id,type:avg)),point_size:0,separate_axis:0,split_mode:everything,stacked:none,value_template:{{value}})),show_grid:1,show_legend:1,time_field:'@timestamp',type:timeseries),title:example-01,type:metrics))", _g: '(refreshInterval:(pause:!t,value:0),time:(from:now-1h,to:now))', type: 'metrics', }, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts index 84d87ee4ad1b77..5d1f9bafdedaf9 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/components/helpers/create_tsvb_link.ts @@ -8,6 +8,7 @@ import { encode } from 'rison-node'; import uuid from 'uuid'; import { set } from '@elastic/safer-lodash-set'; +import { TIMESTAMP_FIELD } from '../../../../../../common/constants'; import { MetricsSourceConfigurationProperties } from '../../../../../../common/metrics_sources'; import { colorTransformer, Color } from '../../../../../../common/color_palette'; import { MetricsExplorerSeries } from '../../../../../../common/http_api/metrics_explorer'; @@ -169,7 +170,7 @@ export const createTSVBLink = ( series: options.metrics.map(mapMetricToSeries(chartOptions)), show_grid: 1, show_legend: 1, - time_field: (source && source.fields.timestamp) || '@timestamp', + time_field: TIMESTAMP_FIELD, type: 'timeseries', filter: createFilterFromOptions(options, series), }, diff --git a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts index c0d0b15217df3e..788760a0dfe1c5 100644 --- a/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts +++ b/x-pack/plugins/infra/public/pages/metrics/metrics_explorer/hooks/use_metrics_explorer_data.ts @@ -84,7 +84,6 @@ export function useMetricsExplorerData( void 0, timerange: { ...timerange, - field: source.fields.timestamp, from: from.valueOf(), to: to.valueOf(), }, diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts index 44f65b9e8071ab..6843bc631ce27f 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetchers.ts @@ -8,7 +8,7 @@ import { encode } from 'rison-node'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { FetchData, FetchDataParams, LogsFetchDataResponse } from '../../../observability/public'; -import { DEFAULT_SOURCE_ID } from '../../common/constants'; +import { DEFAULT_SOURCE_ID, TIMESTAMP_FIELD } from '../../common/constants'; import { callFetchLogSourceConfigurationAPI } from '../containers/logs/log_source/api/fetch_log_source_configuration'; import { callFetchLogSourceStatusAPI } from '../containers/logs/log_source/api/fetch_log_source_status'; import { InfraClientCoreSetup, InfraClientStartDeps } from '../types'; @@ -30,7 +30,6 @@ interface StatsAggregation { interface LogParams { index: string; - timestampField: string; } type StatsAndSeries = Pick; @@ -63,7 +62,6 @@ export function getLogsOverviewDataFetcher( const { stats, series } = await fetchLogsOverview( { index: resolvedLogSourceConfiguration.indices, - timestampField: resolvedLogSourceConfiguration.timestampField, }, params, data @@ -117,7 +115,7 @@ async function fetchLogsOverview( function buildLogOverviewQuery(logParams: LogParams, params: FetchDataParams) { return { range: { - [logParams.timestampField]: { + [TIMESTAMP_FIELD]: { gt: new Date(params.absoluteTime.start).toISOString(), lte: new Date(params.absoluteTime.end).toISOString(), format: 'strict_date_optional_time', @@ -137,7 +135,7 @@ function buildLogOverviewAggregations(logParams: LogParams, params: FetchDataPar aggs: { series: { date_histogram: { - field: logParams.timestampField, + field: TIMESTAMP_FIELD, fixed_interval: params.intervalString, }, }, diff --git a/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts b/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts index d0349ab20710f4..8a1920f534cd65 100644 --- a/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts +++ b/x-pack/plugins/infra/public/utils/logs_overview_fetches.test.ts @@ -150,7 +150,6 @@ describe('Logs UI Observability Homepage Functions', () => { type: 'index_pattern', indexPatternId: 'test-index-pattern', }, - fields: { timestamp: '@timestamp', tiebreaker: '_doc' }, }, }, } as GetLogSourceConfigurationSuccessResponsePayload); diff --git a/x-pack/plugins/infra/server/deprecations.ts b/x-pack/plugins/infra/server/deprecations.ts index 4c2f5f6a9b3d14..5e016bc0948262 100644 --- a/x-pack/plugins/infra/server/deprecations.ts +++ b/x-pack/plugins/infra/server/deprecations.ts @@ -13,6 +13,13 @@ import { DeprecationsDetails, GetDeprecationsContext, } from 'src/core/server'; +import { + TIMESTAMP_FIELD, + TIEBREAKER_FIELD, + CONTAINER_FIELD, + HOST_FIELD, + POD_FIELD, +} from '../common/constants'; import { InfraSources } from './lib/sources'; const deprecatedFieldMessage = (fieldName: string, defaultValue: string, configNames: string[]) => @@ -28,11 +35,11 @@ const deprecatedFieldMessage = (fieldName: string, defaultValue: string, configN }); const DEFAULT_VALUES = { - timestamp: '@timestamp', - tiebreaker: '_doc', - container: 'container.id', - host: 'host.name', - pod: 'kubernetes.pod.uid', + timestamp: TIMESTAMP_FIELD, + tiebreaker: TIEBREAKER_FIELD, + container: CONTAINER_FIELD, + host: HOST_FIELD, + pod: POD_FIELD, }; const FIELD_DEPRECATION_FACTORIES: Record DeprecationsDetails> = diff --git a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts index 75a86ae654d6c3..7e8f5ebfd5af48 100644 --- a/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/log_entries/kibana_log_entries_adapter.ts @@ -24,6 +24,7 @@ import { import { SortedSearchHit } from '../framework'; import { KibanaFramework } from '../framework/kibana_framework_adapter'; import { ResolvedLogSourceConfiguration } from '../../../../common/log_sources'; +import { TIMESTAMP_FIELD, TIEBREAKER_FIELD } from '../../../../common/constants'; const TIMESTAMP_FORMAT = 'epoch_millis'; @@ -64,8 +65,8 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { : {}; const sort = { - [resolvedLogSourceConfiguration.timestampField]: sortDirection, - [resolvedLogSourceConfiguration.tiebreakerField]: sortDirection, + [TIMESTAMP_FIELD]: sortDirection, + [TIEBREAKER_FIELD]: sortDirection, }; const esQuery = { @@ -83,7 +84,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { ...createFilterClauses(query, highlightQuery), { range: { - [resolvedLogSourceConfiguration.timestampField]: { + [TIMESTAMP_FIELD]: { gte: startTimestamp, lte: endTimestamp, format: TIMESTAMP_FORMAT, @@ -146,7 +147,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { aggregations: { count_by_date: { date_range: { - field: resolvedLogSourceConfiguration.timestampField, + field: TIMESTAMP_FIELD, format: TIMESTAMP_FORMAT, ranges: bucketIntervalStarts.map((bucketIntervalStart) => ({ from: bucketIntervalStart.getTime(), @@ -157,10 +158,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { top_hits_by_key: { top_hits: { size: 1, - sort: [ - { [resolvedLogSourceConfiguration.timestampField]: 'asc' }, - { [resolvedLogSourceConfiguration.tiebreakerField]: 'asc' }, - ], + sort: [{ [TIMESTAMP_FIELD]: 'asc' }, { [TIEBREAKER_FIELD]: 'asc' }], _source: false, }, }, @@ -173,7 +171,7 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { ...createQueryFilterClauses(filterQuery), { range: { - [resolvedLogSourceConfiguration.timestampField]: { + [TIMESTAMP_FIELD]: { gte: startTimestamp, lte: endTimestamp, format: TIMESTAMP_FORMAT, diff --git a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts index 730da9511dc380..e05a5b647ad2b9 100644 --- a/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts +++ b/x-pack/plugins/infra/server/lib/adapters/metrics/kibana_metrics_adapter.ts @@ -8,6 +8,7 @@ import { i18n } from '@kbn/i18n'; import { flatten, get } from 'lodash'; import { KibanaRequest } from 'src/core/server'; +import { TIMESTAMP_FIELD } from '../../../../common/constants'; import { NodeDetailsMetricData } from '../../../../common/http_api/node_details_api'; import { KibanaFramework } from '../framework/kibana_framework_adapter'; import { InfraMetricsAdapter, InfraMetricsRequestOptions } from './adapter_types'; @@ -36,7 +37,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { rawRequest: KibanaRequest ): Promise { const indexPattern = `${options.sourceConfiguration.metricAlias}`; - const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields); + const fields = findInventoryFields(options.nodeType); const nodeField = fields.id; const search = (searchOptions: object) => @@ -122,11 +123,7 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { max: options.timerange.to, }; - const model = createTSVBModel( - options.sourceConfiguration.fields.timestamp, - indexPattern, - options.timerange.interval - ); + const model = createTSVBModel(TIMESTAMP_FIELD, indexPattern, options.timerange.interval); const client = ( opts: CallWithRequestParams @@ -137,7 +134,6 @@ export class KibanaMetricsAdapter implements InfraMetricsAdapter { client, { indexPattern: `${options.sourceConfiguration.metricAlias}`, - timestampField: options.sourceConfiguration.fields.timestamp, timerange: options.timerange, }, model.requires diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/mocks/index.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/mocks/index.ts index 296a540b4a9201..f02dac2139097f 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/mocks/index.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/mocks/index.ts @@ -17,7 +17,6 @@ export const libsMock = { type: 'index_pattern', indexPatternId: 'some-id', }, - fields: { timestamp: '@timestamp' }, }, }); }, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts index 71c18d9f7cf042..8991c884336d30 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts @@ -74,7 +74,6 @@ export const evaluateAlert = { timeSize: 1, } as MetricExpressionParams; - const timefield = '@timestamp'; const groupBy = 'host.doggoname'; const timeframe = { start: moment().subtract(5, 'minutes').valueOf(), @@ -25,7 +24,7 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { }; describe('when passed no filterQuery', () => { - const searchBody = getElasticsearchMetricQuery(expressionParams, timefield, timeframe, groupBy); + const searchBody = getElasticsearchMetricQuery(expressionParams, timeframe, groupBy); test('includes a range filter', () => { expect( searchBody.query.bool.filter.find((filter) => filter.hasOwnProperty('range')) @@ -47,7 +46,6 @@ describe("The Metric Threshold Alert's getElasticsearchMetricQuery", () => { const searchBody = getElasticsearchMetricQuery( expressionParams, - timefield, timeframe, groupBy, filterQuery diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts index 59dc398973f8c1..588b77250e6a64 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/metric_query.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { TIMESTAMP_FIELD } from '../../../../../common/constants'; import { networkTraffic } from '../../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; import { MetricExpressionParams, Aggregators } from '../types'; import { createPercentileAggregation } from './create_percentile_aggregation'; @@ -21,7 +22,6 @@ const getParsedFilterQuery: (filterQuery: string | undefined) => Record { const body = { size: 0, @@ -22,7 +23,7 @@ export const getProcessList = async ( filter: [ { range: { - [timefield]: { + [TIMESTAMP_FIELD]: { gte: to - 60 * 1000, // 1 minute lte: to, }, @@ -47,7 +48,7 @@ export const getProcessList = async ( size: 1, sort: [ { - [timefield]: { + [TIMESTAMP_FIELD]: { order: 'desc', }, }, @@ -93,7 +94,7 @@ export const getProcessList = async ( size: 1, sort: [ { - [timefield]: { + [TIMESTAMP_FIELD]: { order: 'desc', }, }, diff --git a/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts b/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts index 413a97cb7a0583..7ff66a80e967b6 100644 --- a/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts +++ b/x-pack/plugins/infra/server/lib/host_details/process_list_chart.ts @@ -6,6 +6,7 @@ */ import { first } from 'lodash'; +import { TIMESTAMP_FIELD } from '../../../common/constants'; import { ProcessListAPIChartRequest, ProcessListAPIChartQueryAggregation, @@ -17,7 +18,7 @@ import { CMDLINE_FIELD } from './common'; export const getProcessListChart = async ( search: ESSearchClient, - { hostTerm, timefield, indexPattern, to, command }: ProcessListAPIChartRequest + { hostTerm, indexPattern, to, command }: ProcessListAPIChartRequest ) => { const body = { size: 0, @@ -26,7 +27,7 @@ export const getProcessListChart = async ( filter: [ { range: { - [timefield]: { + [TIMESTAMP_FIELD]: { gte: to - 60 * 1000, // 1 minute lte: to, }, @@ -60,7 +61,7 @@ export const getProcessListChart = async ( aggs: { timeseries: { date_histogram: { - field: timefield, + field: TIMESTAMP_FIELD, fixed_interval: '1m', extended_bounds: { min: to - 60 * 15 * 1000, // 15 minutes, diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts index ab50986c3b3d5b..9c0f4313c6bdbc 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_hosts_anomalies.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { TIEBREAKER_FIELD } from '../../../../common/constants'; import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { @@ -20,9 +21,6 @@ import { import { InfluencerFilter } from '../common'; import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; -// TODO: Reassess validity of this against ML docs -const TIEBREAKER_FIELD = '_doc'; - const sortToMlFieldMap = { dataset: 'partition_field_value', anomalyScore: 'record_score', diff --git a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts index 8fb8df5eef3d75..23592aad2e3224 100644 --- a/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts +++ b/x-pack/plugins/infra/server/lib/infra_ml/queries/metrics_k8s_anomalies.ts @@ -6,6 +6,7 @@ */ import * as rt from 'io-ts'; +import { TIEBREAKER_FIELD } from '../../../../common/constants'; import { ANOMALY_THRESHOLD } from '../../../../common/infra_ml'; import { commonSearchSuccessResponseFieldsRT } from '../../../utils/elasticsearch_runtime_types'; import { @@ -20,9 +21,6 @@ import { import { InfluencerFilter } from '../common'; import { Sort, Pagination } from '../../../../common/http_api/infra_ml'; -// TODO: Reassess validity of this against ML docs -const TIEBREAKER_FIELD = '_doc'; - const sortToMlFieldMap = { dataset: 'partition_field_value', anomalyScore: 'record_score', diff --git a/x-pack/plugins/infra/server/lib/metrics/index.ts b/x-pack/plugins/infra/server/lib/metrics/index.ts index d291dbf88b49a7..c4641e265ea555 100644 --- a/x-pack/plugins/infra/server/lib/metrics/index.ts +++ b/x-pack/plugins/infra/server/lib/metrics/index.ts @@ -7,6 +7,7 @@ import { set } from '@elastic/safer-lodash-set'; import { ThrowReporter } from 'io-ts/lib/ThrowReporter'; +import { TIMESTAMP_FIELD } from '../../../common/constants'; import { MetricsAPIRequest, MetricsAPIResponse, afterKeyObjectRT } from '../../../common/http_api'; import { ESSearchClient, @@ -36,7 +37,7 @@ export const query = async ( const filter: Array> = [ { range: { - [options.timerange.field]: { + [TIMESTAMP_FIELD]: { gte: options.timerange.from, lte: options.timerange.to, format: 'epoch_millis', diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts index f6bdfb2de0a299..ee309ad449b2de 100644 --- a/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts +++ b/x-pack/plugins/infra/server/lib/metrics/lib/calculate_interval.ts @@ -21,7 +21,6 @@ export const calculatedInterval = async (search: ESSearchClient, options: Metric search, { indexPattern: options.indexPattern, - timestampField: options.timerange.field, timerange: { from: options.timerange.from, to: options.timerange.to }, }, options.modules diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.test.ts b/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.test.ts index b49560f8c25f6e..8fe22e6f81d716 100644 --- a/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.test.ts +++ b/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.test.ts @@ -13,7 +13,6 @@ const keys = ['example-0']; const options: MetricsAPIRequest = { timerange: { - field: '@timestamp', from: moment('2020-01-01T00:00:00Z').valueOf(), to: moment('2020-01-01T00:00:00Z').add(5, 'minute').valueOf(), interval: '1m', diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.test.ts b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.test.ts index 91bf544b7e48f5..9b92793129d443 100644 --- a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.test.ts +++ b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.test.ts @@ -11,7 +11,6 @@ import { MetricsAPIRequest } from '../../../../common/http_api'; const options: MetricsAPIRequest = { timerange: { - field: '@timestamp', from: moment('2020-01-01T00:00:00Z').valueOf(), to: moment('2020-01-01T01:00:00Z').valueOf(), interval: '>=1m', diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts index 65cd4ebe2d501b..769ccce409e65a 100644 --- a/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts +++ b/x-pack/plugins/infra/server/lib/metrics/lib/create_aggregations.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { TIMESTAMP_FIELD } from '../../../../common/constants'; import { MetricsAPIRequest } from '../../../../common/http_api/metrics_api'; import { calculateDateHistogramOffset } from './calculate_date_histogram_offset'; import { createMetricsAggregations } from './create_metrics_aggregations'; @@ -15,7 +16,7 @@ export const createAggregations = (options: MetricsAPIRequest) => { const histogramAggregation = { histogram: { date_histogram: { - field: options.timerange.field, + field: TIMESTAMP_FIELD, fixed_interval: intervalString, offset: options.alignDataToEnd ? calculateDateHistogramOffset(options.timerange) : '0s', extended_bounds: { diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.test.ts b/x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.test.ts index 27fe491d3964b5..2e2d1736e59259 100644 --- a/x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.test.ts +++ b/x-pack/plugins/infra/server/lib/metrics/lib/create_metrics_aggregations.test.ts @@ -11,7 +11,6 @@ import { createMetricsAggregations } from './create_metrics_aggregations'; const options: MetricsAPIRequest = { timerange: { - field: '@timestamp', from: moment('2020-01-01T00:00:00Z').valueOf(), to: moment('2020-01-01T01:00:00Z').valueOf(), interval: '>=1m', diff --git a/x-pack/plugins/infra/server/lib/sources/defaults.ts b/x-pack/plugins/infra/server/lib/sources/defaults.ts index b6139613cfce35..db262a432b3fcd 100644 --- a/x-pack/plugins/infra/server/lib/sources/defaults.ts +++ b/x-pack/plugins/infra/server/lib/sources/defaults.ts @@ -5,11 +5,7 @@ * 2.0. */ -import { - METRICS_INDEX_PATTERN, - LOGS_INDEX_PATTERN, - TIMESTAMP_FIELD, -} from '../../../common/constants'; +import { METRICS_INDEX_PATTERN, LOGS_INDEX_PATTERN } from '../../../common/constants'; import { InfraSourceConfiguration } from '../../../common/source_configuration/source_configuration'; export const defaultSourceConfiguration: InfraSourceConfiguration = { @@ -21,12 +17,7 @@ export const defaultSourceConfiguration: InfraSourceConfiguration = { indexName: LOGS_INDEX_PATTERN, }, fields: { - container: 'container.id', - host: 'host.name', message: ['message', '@message'], - pod: 'kubernetes.pod.uid', - tiebreaker: '_doc', - timestamp: TIMESTAMP_FIELD, }, inventoryDefaultView: '0', metricsExplorerDefaultView: '0', diff --git a/x-pack/plugins/infra/server/lib/sources/saved_object_references.test.ts b/x-pack/plugins/infra/server/lib/sources/saved_object_references.test.ts index 9f6f9cd284c678..fb550390e25beb 100644 --- a/x-pack/plugins/infra/server/lib/sources/saved_object_references.test.ts +++ b/x-pack/plugins/infra/server/lib/sources/saved_object_references.test.ts @@ -101,12 +101,7 @@ const sourceConfigurationWithIndexPatternReference: InfraSourceConfiguration = { name: 'NAME', description: 'DESCRIPTION', fields: { - container: 'CONTAINER_FIELD', - host: 'HOST_FIELD', message: ['MESSAGE_FIELD'], - pod: 'POD_FIELD', - tiebreaker: 'TIEBREAKER_FIELD', - timestamp: 'TIMESTAMP_FIELD', }, logColumns: [], logIndices: { diff --git a/x-pack/plugins/infra/server/lib/sources/sources.test.ts b/x-pack/plugins/infra/server/lib/sources/sources.test.ts index 904f51d12673fc..396d2c22a100f0 100644 --- a/x-pack/plugins/infra/server/lib/sources/sources.test.ts +++ b/x-pack/plugins/infra/server/lib/sources/sources.test.ts @@ -24,13 +24,6 @@ describe('the InfraSources lib', () => { attributes: { metricAlias: 'METRIC_ALIAS', logIndices: { type: 'index_pattern', indexPatternId: 'log_index_pattern_0' }, - fields: { - container: 'CONTAINER', - host: 'HOST', - pod: 'POD', - tiebreaker: 'TIEBREAKER', - timestamp: 'TIMESTAMP', - }, }, references: [ { @@ -50,13 +43,6 @@ describe('the InfraSources lib', () => { configuration: { metricAlias: 'METRIC_ALIAS', logIndices: { type: 'index_pattern', indexPatternId: 'LOG_INDEX_PATTERN' }, - fields: { - container: 'CONTAINER', - host: 'HOST', - pod: 'POD', - tiebreaker: 'TIEBREAKER', - timestamp: 'TIMESTAMP', - }, }, }); }); @@ -67,12 +53,6 @@ describe('the InfraSources lib', () => { default: { metricAlias: 'METRIC_ALIAS', logIndices: { type: 'index_pattern', indexPatternId: 'LOG_ALIAS' }, - fields: { - host: 'HOST', - pod: 'POD', - tiebreaker: 'TIEBREAKER', - timestamp: 'TIMESTAMP', - }, }, }), }); @@ -82,11 +62,7 @@ describe('the InfraSources lib', () => { version: 'foo', type: infraSourceConfigurationSavedObjectName, updated_at: '2000-01-01T00:00:00.000Z', - attributes: { - fields: { - container: 'CONTAINER', - }, - }, + attributes: {}, references: [], }); @@ -99,13 +75,6 @@ describe('the InfraSources lib', () => { configuration: { metricAlias: 'METRIC_ALIAS', logIndices: { type: 'index_pattern', indexPatternId: 'LOG_ALIAS' }, - fields: { - container: 'CONTAINER', - host: 'HOST', - pod: 'POD', - tiebreaker: 'TIEBREAKER', - timestamp: 'TIMESTAMP', - }, }, }); }); @@ -133,13 +102,6 @@ describe('the InfraSources lib', () => { configuration: { metricAlias: expect.any(String), logIndices: expect.any(Object), - fields: { - container: expect.any(String), - host: expect.any(String), - pod: expect.any(String), - tiebreaker: expect.any(String), - timestamp: expect.any(String), - }, }, }); }); diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 26d9f115405a68..4e655f200d94fc 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -53,12 +53,7 @@ export const config: PluginConfigDescriptor = { schema.object({ fields: schema.maybe( schema.object({ - timestamp: schema.maybe(schema.string()), message: schema.maybe(schema.arrayOf(schema.string())), - tiebreaker: schema.maybe(schema.string()), - host: schema.maybe(schema.string()), - container: schema.maybe(schema.string()), - pod: schema.maybe(schema.string()), }) ), }) diff --git a/x-pack/plugins/infra/server/routes/inventory_metadata/lib/get_cloud_metadata.ts b/x-pack/plugins/infra/server/routes/inventory_metadata/lib/get_cloud_metadata.ts index c721ca75ea9788..5c4ae1981c5cd0 100644 --- a/x-pack/plugins/infra/server/routes/inventory_metadata/lib/get_cloud_metadata.ts +++ b/x-pack/plugins/infra/server/routes/inventory_metadata/lib/get_cloud_metadata.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { TIMESTAMP_FIELD } from '../../../../common/constants'; import { InventoryCloudAccount } from '../../../../common/http_api/inventory_meta_api'; import { InfraMetadataAggregationResponse, @@ -49,7 +50,7 @@ export const getCloudMetadata = async ( must: [ { range: { - [sourceConfiguration.fields.timestamp]: { + [TIMESTAMP_FIELD]: { gte: currentTime - 86400000, // 24 hours ago lte: currentTime, format: 'epoch_millis', diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts index d9da7bce2246fe..126d1485cb702c 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_cloud_metric_metadata.ts @@ -13,6 +13,7 @@ import { import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framework_adapter'; import { InfraSourceConfiguration } from '../../../lib/sources'; import { CLOUD_METRICS_MODULES } from '../../../lib/constants'; +import { TIMESTAMP_FIELD } from '../../../../common/constants'; export interface InfraCloudMetricsAdapterResponse { buckets: InfraMetadataAggregationBucket[]; @@ -36,7 +37,7 @@ export const getCloudMetricsMetadata = async ( { match: { 'cloud.instance.id': instanceId } }, { range: { - [sourceConfiguration.fields.timestamp]: { + [TIMESTAMP_FIELD]: { gte: timeRange.from, lte: timeRange.to, format: 'epoch_millis', diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts index bfa0884bfe1999..1962a24f7d4dbe 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_metric_metadata.ts @@ -15,6 +15,7 @@ import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framewor import { InfraSourceConfiguration } from '../../../lib/sources'; import { findInventoryFields } from '../../../../common/inventory_models'; import { InventoryItemType } from '../../../../common/inventory_models/types'; +import { TIMESTAMP_FIELD } from '../../../../common/constants'; export interface InfraMetricsAdapterResponse { id: string; @@ -30,7 +31,7 @@ export const getMetricMetadata = async ( nodeType: InventoryItemType, timeRange: { from: number; to: number } ): Promise => { - const fields = findInventoryFields(nodeType, sourceConfiguration.fields); + const fields = findInventoryFields(nodeType); const metricQuery = { allow_no_indices: true, ignore_unavailable: true, @@ -45,7 +46,7 @@ export const getMetricMetadata = async ( }, { range: { - [sourceConfiguration.fields.timestamp]: { + [TIMESTAMP_FIELD]: { gte: timeRange.from, lte: timeRange.to, format: 'epoch_millis', diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts index 94becdf6d28117..97a0707a4c2159 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_node_info.ts @@ -15,6 +15,7 @@ import { getPodNodeName } from './get_pod_node_name'; import { CLOUD_METRICS_MODULES } from '../../../lib/constants'; import { findInventoryFields } from '../../../../common/inventory_models'; import { InventoryItemType } from '../../../../common/inventory_models/types'; +import { TIMESTAMP_FIELD } from '../../../../common/constants'; export const getNodeInfo = async ( framework: KibanaFramework, @@ -50,8 +51,7 @@ export const getNodeInfo = async ( } return {}; } - const fields = findInventoryFields(nodeType, sourceConfiguration.fields); - const timestampField = sourceConfiguration.fields.timestamp; + const fields = findInventoryFields(nodeType); const params = { allow_no_indices: true, ignore_unavailable: true, @@ -60,14 +60,14 @@ export const getNodeInfo = async ( body: { size: 1, _source: ['host.*', 'cloud.*', 'agent.*'], - sort: [{ [timestampField]: 'desc' }], + sort: [{ [TIMESTAMP_FIELD]: 'desc' }], query: { bool: { filter: [ { match: { [fields.id]: nodeId } }, { range: { - [timestampField]: { + [TIMESTAMP_FIELD]: { gte: timeRange.from, lte: timeRange.to, format: 'epoch_millis', diff --git a/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts b/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts index 164d94d9f692fc..3afb6a8abcb582 100644 --- a/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts +++ b/x-pack/plugins/infra/server/routes/metadata/lib/get_pod_node_name.ts @@ -10,6 +10,7 @@ import { KibanaFramework } from '../../../lib/adapters/framework/kibana_framewor import { InfraSourceConfiguration } from '../../../lib/sources'; import { findInventoryFields } from '../../../../common/inventory_models'; import type { InfraPluginRequestHandlerContext } from '../../../types'; +import { TIMESTAMP_FIELD } from '../../../../common/constants'; export const getPodNodeName = async ( framework: KibanaFramework, @@ -19,8 +20,7 @@ export const getPodNodeName = async ( nodeType: 'host' | 'pod' | 'container', timeRange: { from: number; to: number } ): Promise => { - const fields = findInventoryFields(nodeType, sourceConfiguration.fields); - const timestampField = sourceConfiguration.fields.timestamp; + const fields = findInventoryFields(nodeType); const params = { allow_no_indices: true, ignore_unavailable: true, @@ -29,7 +29,7 @@ export const getPodNodeName = async ( body: { size: 1, _source: ['kubernetes.node.name'], - sort: [{ [timestampField]: 'desc' }], + sort: [{ [TIMESTAMP_FIELD]: 'desc' }], query: { bool: { filter: [ @@ -37,7 +37,7 @@ export const getPodNodeName = async ( { exists: { field: `kubernetes.node.name` } }, { range: { - [timestampField]: { + [TIMESTAMP_FIELD]: { gte: timeRange.from, lte: timeRange.to, format: 'epoch_millis', diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.test.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.test.ts index 539e9a1fee6ef0..a6848e4f7a2ddd 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.test.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/convert_request_to_metrics_api_options.test.ts @@ -10,7 +10,6 @@ import { convertRequestToMetricsAPIOptions } from './convert_request_to_metrics_ const BASE_REQUEST: MetricsExplorerRequestBody = { timerange: { - field: '@timestamp', from: new Date('2020-01-01T00:00:00Z').getTime(), to: new Date('2020-01-01T01:00:00Z').getTime(), interval: '1m', @@ -22,7 +21,6 @@ const BASE_REQUEST: MetricsExplorerRequestBody = { const BASE_METRICS_UI_OPTIONS: MetricsAPIRequest = { timerange: { - field: '@timestamp', from: new Date('2020-01-01T00:00:00Z').getTime(), to: new Date('2020-01-01T01:00:00Z').getTime(), interval: '1m', diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts index 9ca8c085eac44d..62e99cf8ffd320 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/find_interval_for_metrics.ts @@ -44,7 +44,6 @@ export const findIntervalForMetrics = async ( client, { indexPattern: options.indexPattern, - timestampField: options.timerange.field, timerange: options.timerange, }, modules.filter(Boolean) as string[] diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts index 640d62c3667260..97154a7361c965 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/get_dataset_for_field.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { TIMESTAMP_FIELD } from '../../../../common/constants'; import { ESSearchClient } from '../../../lib/metrics/types'; interface EventDatasetHit { @@ -19,7 +20,7 @@ export const getDatasetForField = async ( client: ESSearchClient, field: string, indexPattern: string, - timerange: { field: string; to: number; from: number } + timerange: { to: number; from: number } ) => { const params = { allow_no_indices: true, @@ -33,7 +34,7 @@ export const getDatasetForField = async ( { exists: { field } }, { range: { - [timerange.field]: { + [TIMESTAMP_FIELD]: { gte: timerange.from, lte: timerange.to, format: 'epoch_millis', @@ -45,7 +46,7 @@ export const getDatasetForField = async ( }, size: 1, _source: ['event.dataset'], - sort: [{ [timerange.field]: { order: 'desc' } }], + sort: [{ [TIMESTAMP_FIELD]: { order: 'desc' } }], }, }; diff --git a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts index a2bf778d5016dd..b2e22752609c1f 100644 --- a/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts +++ b/x-pack/plugins/infra/server/routes/metrics_explorer/lib/query_total_groupings.ts @@ -6,6 +6,7 @@ */ import { isArray } from 'lodash'; +import { TIMESTAMP_FIELD } from '../../../../common/constants'; import { MetricsAPIRequest } from '../../../../common/http_api'; import { ESSearchClient } from '../../../lib/metrics/types'; @@ -26,7 +27,7 @@ export const queryTotalGroupings = async ( let filters: Array> = [ { range: { - [options.timerange.field]: { + [TIMESTAMP_FIELD]: { gte: options.timerange.from, lte: options.timerange.to, format: 'epoch_millis', diff --git a/x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts b/x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts index 7533f2801607ca..ccead528749cdd 100644 --- a/x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts +++ b/x-pack/plugins/infra/server/routes/overview/lib/create_top_nodes_query.ts @@ -7,6 +7,7 @@ import { MetricsSourceConfiguration } from '../../../../common/metrics_sources'; import { TopNodesRequest } from '../../../../common/http_api/overview_api'; +import { TIMESTAMP_FIELD } from '../../../../common/constants'; export const createTopNodesQuery = ( options: TopNodesRequest, @@ -22,7 +23,7 @@ export const createTopNodesQuery = ( filter: [ { range: { - [source.configuration.fields.timestamp]: { + [TIMESTAMP_FIELD]: { gte: options.timerange.from, lte: options.timerange.to, }, @@ -49,7 +50,7 @@ export const createTopNodesQuery = ( { field: 'host.name' }, { field: 'cloud.provider' }, ], - sort: { '@timestamp': 'desc' }, + sort: { [TIMESTAMP_FIELD]: 'desc' }, size: 1, }, }, diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts index 7de63ae59a3296..2931555fc06b0c 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts @@ -42,10 +42,7 @@ export const applyMetadataToLastPath = ( if (firstMetaDoc && lastPath) { // We will need the inventory fields so we can use the field paths to get // the values from the metadata document - const inventoryFields = findInventoryFields( - snapshotRequest.nodeType, - source.configuration.fields - ); + const inventoryFields = findInventoryFields(snapshotRequest.nodeType); // Set the label as the name and fallback to the id OR path.value lastPath.label = (firstMetaDoc[inventoryFields.name] ?? lastPath.value) as string; // If the inventory fields contain an ip address, we need to try and set that diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts index 7473907b7410b8..bf6e51b9fe94fb 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts @@ -25,7 +25,6 @@ const createInterval = async (client: ESSearchClient, options: InfraSnapshotRequ client, { indexPattern: options.sourceConfiguration.metricAlias, - timestampField: options.sourceConfiguration.fields.timestamp, timerange: { from: timerange.from, to: timerange.to }, }, modules, @@ -81,7 +80,6 @@ const aggregationsToModules = async ( async (field) => await getDatasetForField(client, field as string, options.sourceConfiguration.metricAlias, { ...options.timerange, - field: options.sourceConfiguration.fields.timestamp, }) ) ); diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts index f59756e0c5b25d..a3ca2cfd683bb0 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts @@ -16,7 +16,6 @@ import { LogQueryFields } from '../../../services/log_queries/get_log_query_fiel export interface SourceOverrides { indexPattern: string; - timestamp: string; } const transformAndQueryData = async ({ diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.test.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.test.ts index b4e6983a099004..aac5f9e1450220 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.test.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.test.ts @@ -47,12 +47,7 @@ const source: InfraSource = { indexPatternId: 'kibana_index_pattern', }, fields: { - container: 'container.id', - host: 'host.name', message: ['message', '@message'], - pod: 'kubernetes.pod.uid', - tiebreaker: '_doc', - timestamp: '@timestamp', }, inventoryDefaultView: '0', metricsExplorerDefaultView: '0', @@ -80,7 +75,7 @@ const snapshotRequest: SnapshotRequest = { const metricsApiRequest = { indexPattern: 'metrics-*,metricbeat-*', - timerange: { field: '@timestamp', from: 1605705900000, to: 1605706200000, interval: '60s' }, + timerange: { from: 1605705900000, to: 1605706200000, interval: '60s' }, metrics: [ { id: 'cpu', diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts index 3901c8677ae9bf..b7e389cae9126e 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { TIMESTAMP_FIELD } from '../../../../common/constants'; import { findInventoryFields, findInventoryModel } from '../../../../common/inventory_models'; import { MetricsAPIRequest, SnapshotRequest } from '../../../../common/http_api'; import { ESSearchClient } from '../../../lib/metrics/types'; @@ -37,7 +38,6 @@ export const transformRequestToMetricsAPIRequest = async ({ const metricsApiRequest: MetricsAPIRequest = { indexPattern: sourceOverrides?.indexPattern ?? source.configuration.metricAlias, timerange: { - field: sourceOverrides?.timestamp ?? source.configuration.fields.timestamp, from: timeRangeWithIntervalApplied.from, to: timeRangeWithIntervalApplied.to, interval: timeRangeWithIntervalApplied.interval, @@ -69,10 +69,7 @@ export const transformRequestToMetricsAPIRequest = async ({ inventoryModel.nodeFilter?.forEach((f) => filters.push(f)); } - const inventoryFields = findInventoryFields( - snapshotRequest.nodeType, - source.configuration.fields - ); + const inventoryFields = findInventoryFields(snapshotRequest.nodeType); if (snapshotRequest.groupBy) { const groupBy = snapshotRequest.groupBy.map((g) => g.field).filter(Boolean) as string[]; metricsApiRequest.groupBy = [...groupBy, inventoryFields.id]; @@ -86,7 +83,7 @@ export const transformRequestToMetricsAPIRequest = async ({ size: 1, metrics: [{ field: inventoryFields.name }], sort: { - [source.configuration.fields.timestamp]: 'desc', + [TIMESTAMP_FIELD]: 'desc', }, }, }, diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts index b0d2eeb987861b..e48c990d7822fc 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entries_search_strategy.test.ts @@ -289,12 +289,7 @@ const createSourceConfigurationMock = (): InfraSource => ({ }, ], fields: { - pod: 'POD_FIELD', - host: 'HOST_FIELD', - container: 'CONTAINER_FIELD', message: ['MESSAGE_FIELD'], - timestamp: 'TIMESTAMP_FIELD', - tiebreaker: 'TIEBREAKER_FIELD', }, anomalyThreshold: 20, }, diff --git a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts index 1f03878ba6febb..685f11cb00a86e 100644 --- a/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts +++ b/x-pack/plugins/infra/server/services/log_entries/log_entry_search_strategy.test.ts @@ -244,12 +244,7 @@ const createSourceConfigurationMock = (): InfraSource => ({ metricsExplorerDefaultView: 'DEFAULT_VIEW', logColumns: [], fields: { - pod: 'POD_FIELD', - host: 'HOST_FIELD', - container: 'CONTAINER_FIELD', message: ['MESSAGE_FIELD'], - timestamp: 'TIMESTAMP_FIELD', - tiebreaker: 'TIEBREAKER_FIELD', }, anomalyThreshold: 20, }, diff --git a/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts b/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts index 55491db97dbd63..db1696854db838 100644 --- a/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts +++ b/x-pack/plugins/infra/server/services/log_queries/get_log_query_fields.ts @@ -12,7 +12,6 @@ import { KibanaFramework } from '../../lib/adapters/framework/kibana_framework_a export interface LogQueryFields { indexPattern: string; - timestamp: string; } export const createGetLogQueryFields = (sources: InfraSources, framework: KibanaFramework) => { @@ -29,7 +28,6 @@ export const createGetLogQueryFields = (sources: InfraSources, framework: Kibana return { indexPattern: resolvedLogSourceConfiguration.indices, - timestamp: resolvedLogSourceConfiguration.timestampField, }; }; }; diff --git a/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts b/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts index 3357b1a842183f..cb754153c66151 100644 --- a/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts +++ b/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { TIMESTAMP_FIELD } from '../../common/constants'; import { findInventoryModel } from '../../common/inventory_models'; // import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; import { InventoryItemType } from '../../common/inventory_models/types'; @@ -12,7 +13,6 @@ import { ESSearchClient } from '../lib/metrics/types'; interface Options { indexPattern: string; - timestampField: string; timerange: { from: number; to: number; @@ -44,7 +44,7 @@ export const calculateMetricInterval = async ( filter: [ { range: { - [options.timestampField]: { + [TIMESTAMP_FIELD]: { gte: from, lte: options.timerange.to, format: 'epoch_millis', diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx index 427306cb54fb90..250359822e068d 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/workspace_panel/chart_switch.tsx @@ -336,6 +336,7 @@ export const ChartSwitch = memo(function ChartSwitch(props: Props) { {v.selection.dataLoss !== 'nothing' ? ( diff --git a/x-pack/plugins/maps/common/migrations/add_field_meta_options.js b/x-pack/plugins/maps/common/migrations/add_field_meta_options.js index 8143c05913f7bf..33a98c7dbf33ce 100644 --- a/x-pack/plugins/maps/common/migrations/add_field_meta_options.js +++ b/x-pack/plugins/maps/common/migrations/add_field_meta_options.js @@ -18,7 +18,13 @@ export function addFieldMetaOptions({ attributes }) { return attributes; } - const layerList = JSON.parse(attributes.layerListJSON); + let layerList = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layerDescriptor) => { if (isVectorLayer(layerDescriptor) && _.has(layerDescriptor, 'style.properties')) { Object.values(layerDescriptor.style.properties).forEach((stylePropertyDescriptor) => { diff --git a/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts b/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts index e46bf6a1a6e7f0..b43b8979094bb8 100644 --- a/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts +++ b/x-pack/plugins/maps/common/migrations/add_type_to_termjoin.ts @@ -21,7 +21,12 @@ export function addTypeToTermJoin({ return attributes; } - const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + let layerList: LayerDescriptor[] = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } layerList.forEach((layer: LayerDescriptor) => { if (!('joins' in layer)) { diff --git a/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js b/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js index 4e52e88bcc1cd9..2945b9efed958d 100644 --- a/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js +++ b/x-pack/plugins/maps/common/migrations/ems_raster_tile_to_ems_vector_tile.js @@ -23,7 +23,13 @@ export function emsRasterTileToEmsVectorTile({ attributes }) { return attributes; } - const layerList = JSON.parse(attributes.layerListJSON); + let layerList = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layer) => { if (isTileLayer(layer) && isEmsTileSource(layer)) { // Just need to switch layer type to migrate TILE layer to VECTOR_TILE layer diff --git a/x-pack/plugins/maps/common/migrations/join_agg_key.ts b/x-pack/plugins/maps/common/migrations/join_agg_key.ts index e3e5a2fac34f44..726855783be63f 100644 --- a/x-pack/plugins/maps/common/migrations/join_agg_key.ts +++ b/x-pack/plugins/maps/common/migrations/join_agg_key.ts @@ -62,7 +62,13 @@ export function migrateJoinAggKey({ return attributes; } - const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + let layerList = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layerDescriptor: LayerDescriptor) => { if ( layerDescriptor.type === LAYER_TYPE.VECTOR || diff --git a/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js b/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js index 3d06c60d23df79..6dab8595663ed3 100644 --- a/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js +++ b/x-pack/plugins/maps/common/migrations/migrate_symbol_style_descriptor.js @@ -18,7 +18,13 @@ export function migrateSymbolStyleDescriptor({ attributes }) { return attributes; } - const layerList = JSON.parse(attributes.layerListJSON); + let layerList = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layerDescriptor) => { if (!isVectorLayer(layerDescriptor) || !_.has(layerDescriptor, 'style.properties')) { return; diff --git a/x-pack/plugins/maps/common/migrations/move_apply_global_query.js b/x-pack/plugins/maps/common/migrations/move_apply_global_query.js index 2d485400db9cac..b0c7d2031ffa79 100644 --- a/x-pack/plugins/maps/common/migrations/move_apply_global_query.js +++ b/x-pack/plugins/maps/common/migrations/move_apply_global_query.js @@ -22,7 +22,13 @@ export function moveApplyGlobalQueryToSources({ attributes }) { return attributes; } - const layerList = JSON.parse(attributes.layerListJSON); + let layerList = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layerDescriptor) => { const applyGlobalQuery = _.get(layerDescriptor, 'applyGlobalQuery', true); delete layerDescriptor.applyGlobalQuery; diff --git a/x-pack/plugins/maps/common/migrations/move_attribution.ts b/x-pack/plugins/maps/common/migrations/move_attribution.ts index 74258e815439e9..6ab5fb93ca981c 100644 --- a/x-pack/plugins/maps/common/migrations/move_attribution.ts +++ b/x-pack/plugins/maps/common/migrations/move_attribution.ts @@ -18,7 +18,12 @@ export function moveAttribution({ return attributes; } - const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + let layerList: LayerDescriptor[] = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } layerList.forEach((layer: LayerDescriptor) => { const sourceDescriptor = layer.sourceDescriptor as { diff --git a/x-pack/plugins/maps/common/migrations/references.ts b/x-pack/plugins/maps/common/migrations/references.ts index 41d9dc063fe47e..1ced7e06c59cc7 100644 --- a/x-pack/plugins/maps/common/migrations/references.ts +++ b/x-pack/plugins/maps/common/migrations/references.ts @@ -29,7 +29,13 @@ export function extractReferences({ const extractedReferences: SavedObjectReference[] = []; - const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + let layerList: LayerDescriptor[] = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layer, layerIndex) => { // Extract index-pattern references from source descriptor if (layer.sourceDescriptor && 'indexPatternId' in layer.sourceDescriptor) { @@ -92,7 +98,13 @@ export function injectReferences({ return { attributes }; } - const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + let layerList: LayerDescriptor[] = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layer) => { // Inject index-pattern references into source descriptor if (layer.sourceDescriptor && 'indexPatternRefName' in layer.sourceDescriptor) { diff --git a/x-pack/plugins/maps/common/migrations/scaling_type.ts b/x-pack/plugins/maps/common/migrations/scaling_type.ts index 1744b3627b1d6a..5106784a16d0a9 100644 --- a/x-pack/plugins/maps/common/migrations/scaling_type.ts +++ b/x-pack/plugins/maps/common/migrations/scaling_type.ts @@ -24,7 +24,13 @@ export function migrateUseTopHitsToScalingType({ return attributes; } - const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + let layerList: LayerDescriptor[] = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layerDescriptor: LayerDescriptor) => { if (isEsDocumentSource(layerDescriptor)) { const sourceDescriptor = layerDescriptor.sourceDescriptor as ESSearchSourceDescriptor; diff --git a/x-pack/plugins/maps/common/migrations/set_default_auto_fit_to_bounds.ts b/x-pack/plugins/maps/common/migrations/set_default_auto_fit_to_bounds.ts index 36fd3cf8da7e2c..af86b621656839 100644 --- a/x-pack/plugins/maps/common/migrations/set_default_auto_fit_to_bounds.ts +++ b/x-pack/plugins/maps/common/migrations/set_default_auto_fit_to_bounds.ts @@ -16,10 +16,14 @@ export function setDefaultAutoFitToBounds({ return attributes; } - // MapState type is defined in public, no need to bring all of that to common for this migration - const mapState: { settings?: { autoFitToDataBounds: boolean } } = JSON.parse( - attributes.mapStateJSON - ); + // MapState type is defined in public, no need to pull type definition into common for this migration + let mapState: { settings?: { autoFitToDataBounds: boolean } } = {}; + try { + mapState = JSON.parse(attributes.mapStateJSON); + } catch (e) { + throw new Error('Unable to parse attribute mapStateJSON'); + } + if ('settings' in mapState) { mapState.settings!.autoFitToDataBounds = false; } else { diff --git a/x-pack/plugins/maps/common/migrations/set_ems_tms_default_modes.ts b/x-pack/plugins/maps/common/migrations/set_ems_tms_default_modes.ts index 94dbd8741add7f..aab0d6b345428a 100644 --- a/x-pack/plugins/maps/common/migrations/set_ems_tms_default_modes.ts +++ b/x-pack/plugins/maps/common/migrations/set_ems_tms_default_modes.ts @@ -22,7 +22,13 @@ export function setEmsTmsDefaultModes({ return attributes; } - const layerList: LayerDescriptor[] = JSON.parse(attributes.layerListJSON); + let layerList: LayerDescriptor[] = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layerDescriptor: LayerDescriptor) => { if (layerDescriptor.sourceDescriptor?.type === SOURCE_TYPES.EMS_TMS) { const sourceDescriptor = layerDescriptor.sourceDescriptor as EMSTMSSourceDescriptor; diff --git a/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js b/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js index ea7ea6cf91c66e..fa5b5111ff797e 100644 --- a/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js +++ b/x-pack/plugins/maps/common/migrations/top_hits_time_to_sort.js @@ -19,7 +19,13 @@ export function topHitsTimeToSort({ attributes }) { return attributes; } - const layerList = JSON.parse(attributes.layerListJSON); + let layerList = []; + try { + layerList = JSON.parse(attributes.layerListJSON); + } catch (e) { + throw new Error('Unable to parse attribute layerListJSON'); + } + layerList.forEach((layerDescriptor) => { if (isEsDocumentSource(layerDescriptor)) { if (_.has(layerDescriptor, 'sourceDescriptor.topHitsTimeField')) { diff --git a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx index 8fc2d97c4862aa..3825c92f313719 100644 --- a/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx +++ b/x-pack/plugins/maps/public/routes/map_page/map_app/map_app.tsx @@ -40,7 +40,6 @@ import { MapContainer } from '../../../connected_components/map_container'; import { getIndexPatternsFromIds } from '../../../index_pattern_util'; import { getTopNavConfig } from '../top_nav_config'; import { goToSpecifiedPath } from '../../../render_app'; -import { MapSavedObjectAttributes } from '../../../../common/map_saved_object_type'; import { getEditPath, getFullPath, APP_ID } from '../../../../common/constants'; import { getMapEmbeddableDisplayName } from '../../../../common/i18n_getters'; import { @@ -52,11 +51,7 @@ import { unsavedChangesWarning, } from '../saved_map'; import { waitUntilTimeLayersLoad$ } from './wait_until_time_layers_load'; - -interface MapRefreshConfig { - isPaused: boolean; - interval: number; -} +import { RefreshConfig as MapRefreshConfig, SerializedMapState } from '../saved_map'; export interface Props { savedMap: SavedMap; @@ -248,20 +243,14 @@ export class MapApp extends React.Component { updateGlobalState(updatedGlobalState, !this.state.initialized); }; - _initMapAndLayerSettings(mapSavedObjectAttributes: MapSavedObjectAttributes) { + _initMapAndLayerSettings(serializedMapState?: SerializedMapState) { const globalState: MapsGlobalState = getGlobalState(); - let savedObjectFilters = []; - if (mapSavedObjectAttributes.mapStateJSON) { - const mapState = JSON.parse(mapSavedObjectAttributes.mapStateJSON); - if (mapState.filters) { - savedObjectFilters = mapState.filters; - } - } + const savedObjectFilters = serializedMapState?.filters ? serializedMapState.filters : []; const appFilters = this._appStateManager.getFilters() || []; const query = getInitialQuery({ - mapStateJSON: mapSavedObjectAttributes.mapStateJSON, + serializedMapState, appState: this._appStateManager.getAppState(), }); if (query) { @@ -272,14 +261,14 @@ export class MapApp extends React.Component { filters: [..._.get(globalState, 'filters', []), ...appFilters, ...savedObjectFilters], query, time: getInitialTimeFilters({ - mapStateJSON: mapSavedObjectAttributes.mapStateJSON, + serializedMapState, globalState, }), }); this._onRefreshConfigChange( getInitialRefreshConfig({ - mapStateJSON: mapSavedObjectAttributes.mapStateJSON, + serializedMapState, globalState, }) ); @@ -371,7 +360,16 @@ export class MapApp extends React.Component { ); } - this._initMapAndLayerSettings(this.props.savedMap.getAttributes()); + let serializedMapState: SerializedMapState | undefined; + try { + const attributes = this.props.savedMap.getAttributes(); + if (attributes.mapStateJSON) { + serializedMapState = JSON.parse(attributes.mapStateJSON); + } + } catch (e) { + // ignore malformed mapStateJSON, not a critical error for viewing map - map will just use defaults + } + this._initMapAndLayerSettings(serializedMapState); this.setState({ initialized: true }); } diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_query.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_query.ts index 276e89f78ebac8..1a57c09672c049 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_query.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_query.ts @@ -7,23 +7,21 @@ import { getData } from '../../../kibana_services'; import { MapsAppState } from '../url_state'; +import { SerializedMapState } from './types'; export function getInitialQuery({ - mapStateJSON, + serializedMapState, appState = {}, }: { - mapStateJSON?: string; + serializedMapState?: SerializedMapState; appState: MapsAppState; }) { if (appState.query) { return appState.query; } - if (mapStateJSON) { - const mapState = JSON.parse(mapStateJSON); - if (mapState.query) { - return mapState.query; - } + if (serializedMapState?.query) { + return serializedMapState.query; } return getData().query.queryString.getDefaultQuery(); diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_refresh_config.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_refresh_config.ts index b343323cdf7abb..ad5a56dcd4c269 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_refresh_config.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_refresh_config.ts @@ -8,21 +8,19 @@ import { QueryState } from 'src/plugins/data/public'; import { getUiSettings } from '../../../kibana_services'; import { UI_SETTINGS } from '../../../../../../../src/plugins/data/public'; +import { SerializedMapState } from './types'; export function getInitialRefreshConfig({ - mapStateJSON, + serializedMapState, globalState = {}, }: { - mapStateJSON?: string; + serializedMapState?: SerializedMapState; globalState: QueryState; }) { const uiSettings = getUiSettings(); - if (mapStateJSON) { - const mapState = JSON.parse(mapStateJSON); - if (mapState.refreshConfig) { - return mapState.refreshConfig; - } + if (serializedMapState?.refreshConfig) { + return serializedMapState.refreshConfig; } const defaultRefreshConfig = uiSettings.get(UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS); diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_time_filters.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_time_filters.ts index 80c5d70ebacf27..9cb67cefde5471 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_time_filters.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/get_initial_time_filters.ts @@ -7,19 +7,17 @@ import { QueryState } from 'src/plugins/data/public'; import { getUiSettings } from '../../../kibana_services'; +import { SerializedMapState } from './types'; export function getInitialTimeFilters({ - mapStateJSON, + serializedMapState, globalState, }: { - mapStateJSON?: string; + serializedMapState?: SerializedMapState; globalState: QueryState; }) { - if (mapStateJSON) { - const mapState = JSON.parse(mapStateJSON); - if (mapState.timeFilters) { - return mapState.timeFilters; - } + if (serializedMapState?.timeFilters) { + return serializedMapState.timeFilters; } const defaultTime = getUiSettings().get('timepicker:timeDefaults'); diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/index.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/index.ts index 549c9949b9027a..a3e8ef96160bb8 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/index.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +export type { RefreshConfig, SerializedMapState, SerializedUiState } from './types'; export { SavedMap } from './saved_map'; export { getInitialLayersFromUrlParam } from './get_initial_layers_from_url_param'; export { getInitialQuery } from './get_initial_query'; diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts index 004b88a2426237..3cff8d97138303 100644 --- a/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/saved_map.ts @@ -47,6 +47,7 @@ import { getBreadcrumbs } from './get_breadcrumbs'; import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../reducers/ui'; import { createBasemapLayerDescriptor } from '../../../classes/layers/create_basemap_layer_descriptor'; import { whenLicenseInitialized } from '../../../licensed_features'; +import { SerializedMapState, SerializedUiState } from './types'; export class SavedMap { private _attributes: MapSavedObjectAttributes | null = null; @@ -113,9 +114,13 @@ export class SavedMap { if (this._mapEmbeddableInput && this._mapEmbeddableInput.mapSettings !== undefined) { this._store.dispatch(setMapSettings(this._mapEmbeddableInput.mapSettings)); } else if (this._attributes?.mapStateJSON) { - const mapState = JSON.parse(this._attributes.mapStateJSON); - if (mapState.settings) { - this._store.dispatch(setMapSettings(mapState.settings)); + try { + const mapState = JSON.parse(this._attributes.mapStateJSON) as SerializedMapState; + if (mapState.settings) { + this._store.dispatch(setMapSettings(mapState.settings)); + } + } catch (e) { + // ignore malformed mapStateJSON, not a critical error for viewing map - map will just use defaults } } @@ -123,20 +128,28 @@ export class SavedMap { if (this._mapEmbeddableInput && this._mapEmbeddableInput.isLayerTOCOpen !== undefined) { isLayerTOCOpen = this._mapEmbeddableInput.isLayerTOCOpen; } else if (this._attributes?.uiStateJSON) { - const uiState = JSON.parse(this._attributes.uiStateJSON); - if ('isLayerTOCOpen' in uiState) { - isLayerTOCOpen = uiState.isLayerTOCOpen; + try { + const uiState = JSON.parse(this._attributes.uiStateJSON) as SerializedUiState; + if ('isLayerTOCOpen' in uiState) { + isLayerTOCOpen = uiState.isLayerTOCOpen; + } + } catch (e) { + // ignore malformed uiStateJSON, not a critical error for viewing map - map will just use defaults } } this._store.dispatch(setIsLayerTOCOpen(isLayerTOCOpen)); - let openTOCDetails = []; + let openTOCDetails: string[] = []; if (this._mapEmbeddableInput && this._mapEmbeddableInput.openTOCDetails !== undefined) { openTOCDetails = this._mapEmbeddableInput.openTOCDetails; } else if (this._attributes?.uiStateJSON) { - const uiState = JSON.parse(this._attributes.uiStateJSON); - if ('openTOCDetails' in uiState) { - openTOCDetails = uiState.openTOCDetails; + try { + const uiState = JSON.parse(this._attributes.uiStateJSON) as SerializedUiState; + if ('openTOCDetails' in uiState) { + openTOCDetails = uiState.openTOCDetails; + } + } catch (e) { + // ignore malformed uiStateJSON, not a critical error for viewing map - map will just use defaults } } this._store.dispatch(setOpenTOCDetails(openTOCDetails)); @@ -150,19 +163,27 @@ export class SavedMap { }) ); } else if (this._attributes?.mapStateJSON) { - const mapState = JSON.parse(this._attributes.mapStateJSON); - this._store.dispatch( - setGotoWithCenter({ - lat: mapState.center.lat, - lon: mapState.center.lon, - zoom: mapState.zoom, - }) - ); + try { + const mapState = JSON.parse(this._attributes.mapStateJSON) as SerializedMapState; + this._store.dispatch( + setGotoWithCenter({ + lat: mapState.center.lat, + lon: mapState.center.lon, + zoom: mapState.zoom, + }) + ); + } catch (e) { + // ignore malformed mapStateJSON, not a critical error for viewing map - map will just use defaults + } } let layerList: LayerDescriptor[] = []; if (this._attributes.layerListJSON) { - layerList = JSON.parse(this._attributes.layerListJSON); + try { + layerList = JSON.parse(this._attributes.layerListJSON) as LayerDescriptor[]; + } catch (e) { + throw new Error('Malformed saved object: unable to parse layerListJSON'); + } } else { const basemapLayerDescriptor = createBasemapLayerDescriptor(); if (basemapLayerDescriptor) { @@ -413,11 +434,11 @@ export class SavedMap { query: getQuery(state), filters: getFilters(state), settings: getMapSettings(state), - }); + } as SerializedMapState); this._attributes!.uiStateJSON = JSON.stringify({ isLayerTOCOpen: getIsLayerTOCOpen(state), openTOCDetails: getOpenTOCDetails(state), - }); + } as SerializedUiState); } } diff --git a/x-pack/plugins/maps/public/routes/map_page/saved_map/types.ts b/x-pack/plugins/maps/public/routes/map_page/saved_map/types.ts new file mode 100644 index 00000000000000..808007c0755338 --- /dev/null +++ b/x-pack/plugins/maps/public/routes/map_page/saved_map/types.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Query } from 'src/plugins/data/common'; +import { Filter, TimeRange } from '../../../../../../../src/plugins/data/public'; +import { MapCenter } from '../../../../common/descriptor_types'; +import { MapSettings } from '../../../reducers/map'; + +export interface RefreshConfig { + isPaused: boolean; + interval: number; +} + +// parsed contents of mapStateJSON +export interface SerializedMapState { + zoom: number; + center: MapCenter; + timeFilters?: TimeRange; + refreshConfig: RefreshConfig; + query?: Query; + filters: Filter[]; + settings: MapSettings; +} + +// parsed contents of uiStateJSON +export interface SerializedUiState { + isLayerTOCOpen: boolean; + openTOCDetails: string[]; +} diff --git a/x-pack/plugins/maps/server/embeddable_migrations.ts b/x-pack/plugins/maps/server/embeddable_migrations.ts index a49e776d4fe02f..962f5c4fb0d7af 100644 --- a/x-pack/plugins/maps/server/embeddable_migrations.ts +++ b/x-pack/plugins/maps/server/embeddable_migrations.ts @@ -19,15 +19,27 @@ import { setEmsTmsDefaultModes } from '../common/migrations/set_ems_tms_default_ */ export const embeddableMigrations = { '7.14.0': (state: SerializableRecord) => { - return { - ...state, - attributes: moveAttribution(state as { attributes: MapSavedObjectAttributes }), - } as SerializableRecord; + try { + return { + ...state, + attributes: moveAttribution(state as { attributes: MapSavedObjectAttributes }), + } as SerializableRecord; + } catch (e) { + // Do not fail migration for invalid layerListJSON + // Maps application can display invalid layerListJSON error when saved object is viewed + return state; + } }, '8.0.0': (state: SerializableRecord) => { - return { - ...state, - attributes: setEmsTmsDefaultModes(state as { attributes: MapSavedObjectAttributes }), - } as SerializableRecord; + try { + return { + ...state, + attributes: setEmsTmsDefaultModes(state as { attributes: MapSavedObjectAttributes }), + } as SerializableRecord; + } catch (e) { + // Do not fail migration for invalid layerListJSON + // Maps application can display invalid layerListJSON error when saved object is viewed + return state; + } }, }; diff --git a/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.js b/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.js index 5fc15e89297143..6d232468604233 100644 --- a/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.js +++ b/x-pack/plugins/maps/server/saved_objects/saved_object_migrations.js @@ -19,6 +19,14 @@ import { addTypeToTermJoin } from '../../common/migrations/add_type_to_termjoin' import { moveAttribution } from '../../common/migrations/move_attribution'; import { setEmsTmsDefaultModes } from '../../common/migrations/set_ems_tms_default_modes'; +function logMigrationWarning(context, errorMsg, doc) { + context.log.warning( + `map migration failed (${context.migrationVersion}). ${errorMsg}. attributes: ${JSON.stringify( + doc + )}` + ); +} + /* * Embeddables such as Maps, Lens, and Visualize can be embedded by value or by reference on a dashboard. * To ensure that any migrations (>7.12) are run correctly in both cases, @@ -27,95 +35,150 @@ import { setEmsTmsDefaultModes } from '../../common/migrations/set_ems_tms_defau * This is the saved object migration registry. */ export const savedObjectMigrations = { - '7.2.0': (doc) => { - const { attributes, references } = extractReferences(doc); + '7.2.0': (doc, context) => { + try { + const { attributes, references } = extractReferences(doc); - return { - ...doc, - attributes, - references, - }; + return { + ...doc, + attributes, + references, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.4.0': (doc) => { - const attributes = emsRasterTileToEmsVectorTile(doc); + '7.4.0': (doc, context) => { + try { + const attributes = emsRasterTileToEmsVectorTile(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.5.0': (doc) => { - const attributes = topHitsTimeToSort(doc); + '7.5.0': (doc, context) => { + try { + const attributes = topHitsTimeToSort(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.6.0': (doc) => { - const attributesPhase1 = moveApplyGlobalQueryToSources(doc); - const attributesPhase2 = addFieldMetaOptions({ attributes: attributesPhase1 }); + '7.6.0': (doc, context) => { + try { + const attributesPhase1 = moveApplyGlobalQueryToSources(doc); + const attributesPhase2 = addFieldMetaOptions({ attributes: attributesPhase1 }); - return { - ...doc, - attributes: attributesPhase2, - }; + return { + ...doc, + attributes: attributesPhase2, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.7.0': (doc) => { - const attributesPhase1 = migrateSymbolStyleDescriptor(doc); - const attributesPhase2 = migrateUseTopHitsToScalingType({ attributes: attributesPhase1 }); + '7.7.0': (doc, context) => { + try { + const attributesPhase1 = migrateSymbolStyleDescriptor(doc); + const attributesPhase2 = migrateUseTopHitsToScalingType({ attributes: attributesPhase1 }); - return { - ...doc, - attributes: attributesPhase2, - }; + return { + ...doc, + attributes: attributesPhase2, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.8.0': (doc) => { - const attributes = migrateJoinAggKey(doc); + '7.8.0': (doc, context) => { + try { + const attributes = migrateJoinAggKey(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.9.0': (doc) => { - const attributes = removeBoundsFromSavedObject(doc); + '7.9.0': (doc, context) => { + try { + const attributes = removeBoundsFromSavedObject(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.10.0': (doc) => { - const attributes = setDefaultAutoFitToBounds(doc); + '7.10.0': (doc, context) => { + try { + const attributes = setDefaultAutoFitToBounds(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.12.0': (doc) => { - const attributes = addTypeToTermJoin(doc); + '7.12.0': (doc, context) => { + try { + const attributes = addTypeToTermJoin(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '7.14.0': (doc) => { - const attributes = moveAttribution(doc); + '7.14.0': (doc, context) => { + try { + const attributes = moveAttribution(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, - '8.0.0': (doc) => { - const attributes = setEmsTmsDefaultModes(doc); + '8.0.0': (doc, context) => { + try { + const attributes = setEmsTmsDefaultModes(doc); - return { - ...doc, - attributes, - }; + return { + ...doc, + attributes, + }; + } catch (e) { + logMigrationWarning(context, e.message, doc); + return doc; + } }, }; diff --git a/x-pack/plugins/ml/common/constants/trained_models.ts b/x-pack/plugins/ml/common/constants/trained_models.ts new file mode 100644 index 00000000000000..019189ea13c052 --- /dev/null +++ b/x-pack/plugins/ml/common/constants/trained_models.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const DEPLOYMENT_STATE = { + STARTED: 'started', + STARTING: 'starting', + STOPPING: 'stopping', +} as const; + +export type DeploymentState = typeof DEPLOYMENT_STATE[keyof typeof DEPLOYMENT_STATE]; diff --git a/x-pack/plugins/ml/common/types/locator.ts b/x-pack/plugins/ml/common/types/locator.ts index 79db780b791fde..e13dbf7c5b271d 100644 --- a/x-pack/plugins/ml/common/types/locator.ts +++ b/x-pack/plugins/ml/common/types/locator.ts @@ -188,6 +188,10 @@ export interface TrainedModelsQueryState { modelId?: string; } +export interface TrainedModelsNodesQueryState { + nodeId?: string; +} + export type DataFrameAnalyticsUrlState = MLPageState< | typeof ML_PAGES.DATA_FRAME_ANALYTICS_JOBS_MANAGE | typeof ML_PAGES.DATA_FRAME_ANALYTICS_MAP @@ -255,7 +259,8 @@ export type MlLocatorState = | CalendarEditUrlState | FilterEditUrlState | MlGenericUrlState - | TrainedModelsUrlState; + | TrainedModelsUrlState + | TrainedModelsNodesUrlState; export type MlLocatorParams = MlLocatorState & SerializableRecord; @@ -265,3 +270,8 @@ export type TrainedModelsUrlState = MLPageState< typeof ML_PAGES.TRAINED_MODELS_MANAGE, TrainedModelsQueryState | undefined >; + +export type TrainedModelsNodesUrlState = MLPageState< + typeof ML_PAGES.TRAINED_MODELS_NODES, + TrainedModelsNodesQueryState | undefined +>; diff --git a/x-pack/plugins/ml/common/types/trained_models.ts b/x-pack/plugins/ml/common/types/trained_models.ts index 5ad1d85d9feb94..89b8a50846cb36 100644 --- a/x-pack/plugins/ml/common/types/trained_models.ts +++ b/x-pack/plugins/ml/common/types/trained_models.ts @@ -5,9 +5,10 @@ * 2.0. */ -import { DataFrameAnalyticsConfig } from './data_frame_analytics'; -import { FeatureImportanceBaseline, TotalFeatureImportance } from './feature_importance'; -import { XOR } from './common'; +import type { DataFrameAnalyticsConfig } from './data_frame_analytics'; +import type { FeatureImportanceBaseline, TotalFeatureImportance } from './feature_importance'; +import type { XOR } from './common'; +import type { DeploymentState } from '../constants/trained_models'; export interface IngestStats { count: number; @@ -17,8 +18,8 @@ export interface IngestStats { } export interface TrainedModelStat { - model_id: string; - pipeline_count: number; + model_id?: string; + pipeline_count?: number; inference_stats?: { failure_count: number; inference_count: number; @@ -100,6 +101,9 @@ export interface TrainedModelConfigResponse { tags: string[]; version: string; inference_config?: Record; + /** + * Associated pipelines. Extends response from the ES endpoint. + */ pipelines?: Record | null; } @@ -125,7 +129,7 @@ export interface TrainedModelDeploymentStatsResponse { model_size_bytes: number; inference_threads: number; model_threads: number; - state: string; + state: DeploymentState; allocation_status: { target_allocation_count: number; state: string; allocation_count: number }; nodes: Array<{ node: Record< @@ -150,24 +154,35 @@ export interface TrainedModelDeploymentStatsResponse { }>; } +export interface AllocatedModel { + inference_threads: number; + allocation_status: { + target_allocation_count: number; + state: string; + allocation_count: number; + }; + model_id: string; + state: string; + model_threads: number; + model_size_bytes: number; + node: { + average_inference_time_ms: number; + inference_count: number; + routing_state: { + routing_state: string; + reason?: string; + }; + last_access?: number; + }; +} + export interface NodeDeploymentStatsResponse { id: string; name: string; transport_address: string; attributes: Record; roles: string[]; - allocated_models: Array<{ - inference_threads: number; - allocation_status: { - target_allocation_count: number; - state: string; - allocation_count: number; - }; - model_id: string; - state: string; - model_threads: number; - model_size_bytes: number; - }>; + allocated_models: AllocatedModel[]; memory_overview: { machine_memory: { /** Total machine memory in bytes */ diff --git a/x-pack/plugins/ml/public/application/components/anomalies_table/influencers_cell.js b/x-pack/plugins/ml/public/application/components/anomalies_table/influencers_cell.js index 1f2236ad3e6a7a..0059bec2929d02 100644 --- a/x-pack/plugins/ml/public/application/components/anomalies_table/influencers_cell.js +++ b/x-pack/plugins/ml/public/application/components/anomalies_table/influencers_cell.js @@ -12,6 +12,7 @@ import React, { Component } from 'react'; import { EuiLink, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { i18n } from '@kbn/i18n'; +import { blurButtonOnClick } from '../../util/component_utils'; /* * Component for rendering a list of record influencers inside a cell in the anomalies table. @@ -59,13 +60,13 @@ export class InfluencersCell extends Component { + onClick={blurButtonOnClick(() => { influencerFilter( influencer.influencerFieldName, influencer.influencerFieldValue, '+' - ) - } + ); + })} iconType="plusInCircle" aria-label={i18n.translate( 'xpack.ml.anomaliesTable.influencersCell.addFilterAriaLabel', @@ -86,13 +87,13 @@ export class InfluencersCell extends Component { + onClick={blurButtonOnClick(() => { influencerFilter( influencer.influencerFieldName, influencer.influencerFieldValue, '-' - ) - } + ); + })} iconType="minusInCircle" aria-label={i18n.translate( 'xpack.ml.anomaliesTable.influencersCell.removeFilterAriaLabel', diff --git a/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.tsx b/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.tsx index a79c8a63b3bc6d..f4a3b6dbf69c4a 100644 --- a/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.tsx +++ b/x-pack/plugins/ml/public/application/components/entity_cell/entity_cell.tsx @@ -13,6 +13,7 @@ import { i18n } from '@kbn/i18n'; import { EMPTY_FIELD_VALUE_LABEL } from '../../timeseriesexplorer/components/entity_control/entity_control'; import { MLCATEGORY } from '../../../../common/constants/field_types'; import { ENTITY_FIELD_OPERATIONS } from '../../../../common/util/anomaly_utils'; +import { blurButtonOnClick } from '../../util/component_utils'; export type EntityCellFilter = ( entityName: string, @@ -41,7 +42,9 @@ function getAddFilter({ entityName, entityValue, filter }: EntityCellProps) { filter(entityName, entityValue, ENTITY_FIELD_OPERATIONS.ADD)} + onClick={blurButtonOnClick(() => { + filter(entityName, entityValue, ENTITY_FIELD_OPERATIONS.ADD); + })} iconType="plusInCircle" aria-label={i18n.translate('xpack.ml.anomaliesTable.entityCell.addFilterAriaLabel', { defaultMessage: 'Add filter', @@ -66,7 +69,9 @@ function getRemoveFilter({ entityName, entityValue, filter }: EntityCellProps) { filter(entityName, entityValue, ENTITY_FIELD_OPERATIONS.REMOVE)} + onClick={blurButtonOnClick(() => { + filter(entityName, entityValue, ENTITY_FIELD_OPERATIONS.REMOVE); + })} iconType="minusInCircle" aria-label={i18n.translate('xpack.ml.anomaliesTable.entityCell.removeFilterAriaLabel', { defaultMessage: 'Remove filter', diff --git a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx index 78fc10e77b2dae..614db1ba0df9d9 100644 --- a/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx +++ b/x-pack/plugins/ml/public/application/components/navigation_menu/main_tabs.tsx @@ -7,7 +7,7 @@ import React, { FC, useState, useEffect } from 'react'; -import { EuiPageHeader, EuiBetaBadge } from '@elastic/eui'; +import { EuiPageHeader } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { TabId } from './navigation_menu'; import { useMlKibana, useMlLocator, useNavigateToPath } from '../../contexts/kibana'; @@ -57,20 +57,6 @@ function getTabs(disableLinks: boolean): Tab[] { defaultMessage: 'Model Management', }), disabled: disableLinks, - betaTag: ( - - ), }, { id: 'datavisualizer', @@ -201,7 +187,6 @@ export const MainTabs: FC = ({ tabId, disableLinks }) => { }, 'data-test-subj': testSubject + (id === selectedTabId ? ' selected' : ''), isSelected: id === selectedTabId, - append: tab.betaTag, }; })} /> diff --git a/x-pack/plugins/ml/public/application/contexts/kibana/use_field_formatter.ts b/x-pack/plugins/ml/public/application/contexts/kibana/use_field_formatter.ts index 508ce66f40f479..d089a43b3fb393 100644 --- a/x-pack/plugins/ml/public/application/contexts/kibana/use_field_formatter.ts +++ b/x-pack/plugins/ml/public/application/contexts/kibana/use_field_formatter.ts @@ -6,12 +6,26 @@ */ import { useMlKibana } from './kibana_context'; +import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common'; -export function useFieldFormatter(fieldType: 'bytes') { +/** + * Set of reasonable defaults for formatters for the ML app. + */ +const defaultParam = { + [FIELD_FORMAT_IDS.DURATION]: { + inputFormat: 'milliseconds', + outputFormat: 'humanizePrecise', + }, +} as Record; + +export function useFieldFormatter(fieldType: FIELD_FORMAT_IDS) { const { services: { fieldFormats }, } = useMlKibana(); - const fieldFormatter = fieldFormats.deserialize({ id: fieldType }); + const fieldFormatter = fieldFormats.deserialize({ + id: fieldType, + params: defaultParam[fieldType], + }); return fieldFormatter.convert.bind(fieldFormatter); } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx b/x-pack/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx index 93c0291d4f9d4b..90b152453cecbe 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/file_based/file_datavisualizer.tsx @@ -89,7 +89,7 @@ export const FileDataVisualizerPage: FC = () => { }, }); }, - canDisplay: async () => true, + canDisplay: async ({ indexPatternId }) => indexPatternId !== '', }, ], [] diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/entity_filter.tsx b/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/entity_filter.tsx index 2ede9d380f3bf9..66f4052a6952fc 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/entity_filter.tsx +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/components/explorer_chart_label/entity_filter/entity_filter.tsx @@ -12,6 +12,7 @@ import { ENTITY_FIELD_OPERATIONS, EntityFieldOperation, } from '../../../../../../../common/util/anomaly_utils'; +import { blurButtonOnClick } from '../../../../../util/component_utils'; import './_entity_filter.scss'; interface EntityFilterProps { @@ -41,13 +42,13 @@ export const EntityFilter: FC = ({ + onClick={blurButtonOnClick(() => { onFilter({ influencerFieldName, influencerFieldValue, action: ENTITY_FIELD_OPERATIONS.ADD, - }) - } + }); + })} iconType="plusInCircle" aria-label={i18n.translate('xpack.ml.entityFilter.addFilterAriaLabel', { defaultMessage: 'Add filter for {influencerFieldName} {influencerFieldValue}', @@ -66,13 +67,13 @@ export const EntityFilter: FC = ({ + onClick={blurButtonOnClick(() => { onFilter({ influencerFieldName, influencerFieldValue, action: ENTITY_FIELD_OPERATIONS.REMOVE, - }) - } + }); + })} iconType="minusInCircle" aria-label={i18n.translate('xpack.ml.entityFilter.removeFilterAriaLabel', { defaultMessage: 'Remove filter for {influencerFieldName} {influencerFieldValue}', diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/expanded_row.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/expanded_row.tsx index 4b342fe02b4d5f..6dd7db1dbb7b65 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/expanded_row.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import React, { FC, Fragment } from 'react'; +import React, { FC, Fragment, useEffect, useState } from 'react'; +import { omit } from 'lodash'; import { EuiBadge, EuiButtonEmpty, @@ -15,6 +16,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, + EuiListGroup, EuiNotificationBadge, EuiPanel, EuiSpacer, @@ -25,11 +27,13 @@ import { } from '@elastic/eui'; import { EuiDescriptionListProps } from '@elastic/eui/src/components/description_list/description_list'; import { FormattedMessage } from '@kbn/i18n/react'; +import type { EuiListGroupItemProps } from '@elastic/eui/src/components/list_group/list_group_item'; import { ModelItemFull } from './models_list'; -import { useMlKibana } from '../../contexts/kibana'; +import { useMlKibana, useMlLocator } from '../../contexts/kibana'; import { timeFormatter } from '../../../../common/util/date_utils'; import { isDefined } from '../../../../common/types/guards'; import { isPopulatedObject } from '../../../../common'; +import { ML_PAGES } from '../../../../common/constants/locator'; interface ExpandedRowProps { item: ModelItemFull; @@ -85,6 +89,12 @@ export function formatToListItems( } export const ExpandedRow: FC = ({ item }) => { + const mlLocator = useMlLocator(); + + const [deploymentStatsItems, setDeploymentStats] = useState( + [] + ); + const { inference_config: inferenceConfig, stats, @@ -119,6 +129,42 @@ export const ExpandedRow: FC = ({ item }) => { services: { share }, } = useMlKibana(); + useEffect( + function updateDeploymentState() { + (async function () { + const { nodes, ...deploymentStats } = stats.deployment_stats ?? {}; + + if (!isPopulatedObject(deploymentStats)) return; + + const result = formatToListItems(deploymentStats)!; + + const items: EuiListGroupItemProps[] = await Promise.all( + nodes!.map(async (v) => { + const nodeObject = Object.values(v.node)[0]; + const href = await mlLocator!.getUrl({ + page: ML_PAGES.TRAINED_MODELS_NODES, + pageState: { + nodeId: nodeObject.name, + }, + }); + return { + label: nodeObject.name, + href, + }; + }) + ); + + result.push({ + title: 'nodes', + description: , + }); + + setDeploymentStats(result); + })(); + }, + [stats.deployment_stats] + ); + const tabs = [ { id: 'details', @@ -234,164 +280,168 @@ export const ExpandedRow: FC = ({ item }) => { }, ] : []), - { - id: 'stats', - name: ( - - ), - content: ( - <> - - {stats.deployment_stats && ( - <> - - -
- -
-
+ ...(isPopulatedObject(omit(stats, 'pipeline_count')) + ? [ + { + id: 'stats', + name: ( + + ), + content: ( + <> - -
- - - )} - - {stats.inference_stats && ( - - - -
- -
-
- - -
-
- )} - {stats.ingest?.total && ( - - - -
- -
-
- - - - {stats.ingest?.pipelines && ( - <> - + {!!deploymentStatsItems?.length ? ( + <> +
- - {Object.entries(stats.ingest.pipelines).map( - ([pipelineName, { processors, ...pipelineStats }], i) => { - return ( - - - - - -
- {i + 1}. {pipelineName} -
-
-
-
- - - -
- - - - -
- -
-
- - <> - {processors.map((processor) => { - const name = Object.keys(processor)[0]; - const { stats: processorStats } = processor[name]; - return ( - - - - - -
{name}
-
-
-
- - - -
- - -
- ); - })} - -
- ); - } - )} - + + +
+ + + ) : null} + + {stats.inference_stats && ( + + + +
+ +
+
+ + +
+
)} -
-
- )} -
- - ), - }, + {stats.ingest?.total && ( + + + +
+ +
+
+ + + + {stats.ingest?.pipelines && ( + <> + + +
+ +
+
+ + {Object.entries(stats.ingest.pipelines).map( + ([pipelineName, { processors, ...pipelineStats }], i) => { + return ( + + + + + +
+ {i + 1}. {pipelineName} +
+
+
+
+ + + +
+ + + + +
+ +
+
+ + <> + {processors.map((processor) => { + const name = Object.keys(processor)[0]; + const { stats: processorStats } = processor[name]; + return ( + + + + + +
{name}
+
+
+
+ + + +
+ + +
+ ); + })} + +
+ ); + } + )} + + )} +
+
+ )} + + + ), + }, + ] + : []), ...(pipelines && Object.keys(pipelines).length > 0 ? [ { diff --git a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx index 16b9aa760f5351..9c3cc1f93a9cdb 100644 --- a/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/models_management/models_list.tsx @@ -5,19 +5,19 @@ * 2.0. */ -import React, { FC, useState, useCallback, useMemo } from 'react'; -import { groupBy } from 'lodash'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; +import { omit } from 'lodash'; import { - EuiInMemoryTable, + EuiBadge, + EuiButton, + EuiButtonIcon, EuiFlexGroup, EuiFlexItem, - EuiTitle, - EuiButton, + EuiInMemoryTable, + EuiSearchBarProps, EuiSpacer, - EuiButtonIcon, - EuiBadge, + EuiTitle, SearchFilterConfig, - EuiSearchBarProps, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; @@ -48,9 +48,12 @@ import { ListingPageUrlState } from '../../../../common/types/common'; import { usePageUrlState } from '../../util/url_state'; import { ExpandedRow } from './expanded_row'; import { isPopulatedObject } from '../../../../common'; -import { timeFormatter } from '../../../../common/util/date_utils'; import { useTableSettings } from '../../data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings'; import { useToastNotificationService } from '../../services/toast_notification_service'; +import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; +import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common'; +import { useRefresh } from '../../routing/use_refresh'; +import { DEPLOYMENT_STATE } from '../../../../common/constants/trained_models'; type Stats = Omit; @@ -82,11 +85,15 @@ export const ModelsList: FC = () => { } = useMlKibana(); const urlLocator = useMlLocator()!; + const dateFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DATE); + const [pageState, updatePageState] = usePageUrlState( ML_PAGES.TRAINED_MODELS_MANAGE, getDefaultModelsListState() ); + const refresh = useRefresh(); + const searchQueryText = pageState.queryText ?? ''; const canDeleteDataFrameAnalytics = capabilities.ml.canDeleteDataFrameAnalytics as boolean; @@ -121,7 +128,7 @@ export const ModelsList: FC = () => { size: 1000, }); - const newItems = []; + const newItems: ModelItem[] = []; const expandedItemsToRefresh = []; for (const model of response) { @@ -145,6 +152,11 @@ export const ModelsList: FC = () => { } } + // Need to fetch state for 3rd party models to enable/disable actions + await fetchAndPopulateDeploymentStats( + newItems.filter((v) => v.model_type.includes('pytorch')) + ); + setItems(newItems); if (expandedItemsToRefresh.length > 0) { @@ -175,6 +187,13 @@ export const ModelsList: FC = () => { onRefresh: fetchModelsData, }); + useEffect( + function updateOnTimerRefresh() { + fetchModelsData(); + }, + [refresh] + ); + const modelsStats: ModelsBarStats = useMemo(() => { return { total: { @@ -191,8 +210,6 @@ export const ModelsList: FC = () => { * Fetches models stats and update the original object */ const fetchModelsStats = useCallback(async (models: ModelItem[]) => { - const { true: pytorchModels } = groupBy(models, (m) => m.model_type === 'pytorch'); - try { if (models) { const { trained_model_stats: modelsStatsResponse } = @@ -200,19 +217,12 @@ export const ModelsList: FC = () => { for (const { model_id: id, ...stats } of modelsStatsResponse) { const model = models.find((m) => m.model_id === id); - model!.stats = stats; - } - } - - if (pytorchModels) { - const { deployment_stats: deploymentStatsResponse } = - await trainedModelsApiService.getTrainedModelDeploymentStats( - pytorchModels.map((m) => m.model_id) - ); - - for (const { model_id: id, ...stats } of deploymentStatsResponse) { - const model = models.find((m) => m.model_id === id); - model!.stats!.deployment_stats = stats; + if (model) { + model.stats = { + ...(model.stats ?? {}), + ...stats, + }; + } } } @@ -227,6 +237,39 @@ export const ModelsList: FC = () => { } }, []); + /** + * Updates model items with deployment stats; + * + * We have to fetch all deployment stats on each update, + * because for stopped models the API returns 404 response. + */ + const fetchAndPopulateDeploymentStats = useCallback(async (modelItems: ModelItem[]) => { + try { + const { deployment_stats: deploymentStats } = + await trainedModelsApiService.getTrainedModelDeploymentStats('*'); + + for (const deploymentStat of deploymentStats) { + const deployedModel = modelItems.find( + (model) => model.model_id === deploymentStat.model_id + ); + + if (deployedModel) { + deployedModel.stats = { + ...(deployedModel.stats ?? {}), + deployment_stats: omit(deploymentStat, 'model_id'), + }; + } + } + } catch (error) { + displayErrorToast( + error, + i18n.translate('xpack.ml.trainedModels.modelsList.fetchDeploymentStatsErrorMessage', { + defaultMessage: 'Fetch deployment stats failed', + }) + ); + } + }, []); + /** * Unique inference types from models */ @@ -361,12 +404,19 @@ export const ModelsList: FC = () => { description: i18n.translate('xpack.ml.inference.modelsList.startModelAllocationActionLabel', { defaultMessage: 'Start allocation', }), - icon: 'download', + icon: 'play', type: 'icon', isPrimary: true, + enabled: (item) => { + const { state } = item.stats?.deployment_stats ?? {}; + return ( + !isLoading && state !== DEPLOYMENT_STATE.STARTED && state !== DEPLOYMENT_STATE.STARTING + ); + }, available: (item) => item.model_type === 'pytorch', onClick: async (item) => { try { + setIsLoading(true); await trainedModelsApiService.startModelAllocation(item.model_id); displaySuccessToast( i18n.translate('xpack.ml.trainedModels.modelsList.startSuccess', { @@ -376,6 +426,7 @@ export const ModelsList: FC = () => { }, }) ); + await fetchModelsData(); } catch (e) { displayErrorToast( e, @@ -386,6 +437,7 @@ export const ModelsList: FC = () => { }, }) ); + setIsLoading(false); } }, }, @@ -400,9 +452,14 @@ export const ModelsList: FC = () => { type: 'icon', isPrimary: true, available: (item) => item.model_type === 'pytorch', - enabled: (item) => !isPopulatedObject(item.pipelines), + enabled: (item) => + !isLoading && + !isPopulatedObject(item.pipelines) && + isPopulatedObject(item.stats?.deployment_stats) && + item.stats?.deployment_stats?.state !== DEPLOYMENT_STATE.STOPPING, onClick: async (item) => { try { + setIsLoading(true); await trainedModelsApiService.stopModelAllocation(item.model_id); displaySuccessToast( i18n.translate('xpack.ml.trainedModels.modelsList.stopSuccess', { @@ -412,6 +469,8 @@ export const ModelsList: FC = () => { }, }) ); + // Need to fetch model state updates + await fetchModelsData(); } catch (e) { displayErrorToast( e, @@ -422,6 +481,7 @@ export const ModelsList: FC = () => { }, }) ); + setIsLoading(false); } }, }, @@ -521,13 +581,25 @@ export const ModelsList: FC = () => { ), 'data-test-subj': 'mlModelsTableColumnType', }, + { + name: i18n.translate('xpack.ml.trainedModels.modelsList.stateHeader', { + defaultMessage: 'State', + }), + sortable: (item) => item.stats?.deployment_stats?.state, + align: 'left', + render: (model: ModelItem) => { + const state = model.stats?.deployment_stats?.state; + return state ? {state} : null; + }, + 'data-test-subj': 'mlModelsTableColumnDeploymentState', + }, { field: ModelsTableToConfigMapping.createdAt, name: i18n.translate('xpack.ml.trainedModels.modelsList.createdAtHeader', { defaultMessage: 'Created at', }), dataType: 'date', - render: timeFormatter, + render: (v: number) => dateFormatter(v), sortable: true, 'data-test-subj': 'mlModelsTableColumnCreatedAt', }, diff --git a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/allocated_models.tsx b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/allocated_models.tsx new file mode 100644 index 00000000000000..2aad8183b7998f --- /dev/null +++ b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/allocated_models.tsx @@ -0,0 +1,131 @@ +/* + * 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, { FC } from 'react'; +import { EuiBadge, EuiInMemoryTable, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiBasicTableColumn } from '@elastic/eui/src/components/basic_table/basic_table'; +import type { + AllocatedModel, + NodeDeploymentStatsResponse, +} from '../../../../common/types/trained_models'; +import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; +import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common'; + +interface AllocatedModelsProps { + models: NodeDeploymentStatsResponse['allocated_models']; +} + +export const AllocatedModels: FC = ({ models }) => { + const bytesFormatter = useFieldFormatter(FIELD_FORMAT_IDS.BYTES); + const dateFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DATE); + const durationFormatter = useFieldFormatter(FIELD_FORMAT_IDS.DURATION); + + const columns: Array> = [ + { + field: 'model_id', + name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.modelNameHeader', { + defaultMessage: 'Name', + }), + width: '300px', + sortable: true, + truncateText: false, + 'data-test-subj': 'mlAllocatedModelsTableName', + }, + { + name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.modelSizeHeader', { + defaultMessage: 'Size', + }), + width: '100px', + truncateText: true, + 'data-test-subj': 'mlAllocatedModelsTableSize', + render: (v: AllocatedModel) => { + return bytesFormatter(v.model_size_bytes); + }, + }, + { + field: 'state', + name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.modelStateHeader', { + defaultMessage: 'State', + }), + width: '100px', + truncateText: false, + 'data-test-subj': 'mlAllocatedModelsTableState', + }, + { + name: i18n.translate( + 'xpack.ml.trainedModels.nodesList.modelsList.modelAvgInferenceTimeHeader', + { + defaultMessage: 'Avg inference time', + } + ), + width: '100px', + truncateText: false, + 'data-test-subj': 'mlAllocatedModelsTableAvgInferenceTime', + render: (v: AllocatedModel) => { + return v.node.average_inference_time_ms + ? durationFormatter(v.node.average_inference_time_ms) + : '-'; + }, + }, + { + name: i18n.translate( + 'xpack.ml.trainedModels.nodesList.modelsList.modelInferenceCountHeader', + { + defaultMessage: 'Inference count', + } + ), + width: '100px', + 'data-test-subj': 'mlAllocatedModelsTableInferenceCount', + render: (v: AllocatedModel) => { + return v.node.inference_count; + }, + }, + { + name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.modelLastAccessHeader', { + defaultMessage: 'Last access', + }), + width: '200px', + 'data-test-subj': 'mlAllocatedModelsTableInferenceCount', + render: (v: AllocatedModel) => { + return dateFormatter(v.node.last_access); + }, + }, + { + name: i18n.translate('xpack.ml.trainedModels.nodesList.modelsList.modelRoutingStateHeader', { + defaultMessage: 'Routing state', + }), + width: '100px', + 'data-test-subj': 'mlAllocatedModelsTableRoutingState', + render: (v: AllocatedModel) => { + const { routing_state: routingState, reason } = v.node.routing_state; + + return ( + + {routingState} + + ); + }, + }, + ]; + + return ( + + allowNeutralSort={false} + columns={columns} + hasActions={false} + isExpandable={false} + isSelectable={false} + items={models} + itemId={'model_id'} + rowProps={(item) => ({ + 'data-test-subj': `mlAllocatedModelTableRow row-${item.model_id}`, + })} + onTableChange={() => {}} + data-test-subj={'mlNodesTable'} + /> + ); +}; diff --git a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/expanded_row.tsx b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/expanded_row.tsx index a32747185dcc8a..508a5689e1c9b8 100644 --- a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/expanded_row.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/expanded_row.tsx @@ -9,17 +9,15 @@ import React, { FC } from 'react'; import { EuiDescriptionList, EuiFlexGrid, - EuiFlexGroup, EuiFlexItem, - EuiHorizontalRule, EuiPanel, EuiSpacer, - EuiTextColor, EuiTitle, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { NodeItemWithStats } from './nodes_list'; import { formatToListItems } from '../models_management/expanded_row'; +import { AllocatedModels } from './allocated_models'; interface ExpandedRowProps { item: NodeItemWithStats; @@ -55,8 +53,6 @@ export const ExpandedRow: FC = ({ item }) => { listItems={formatToListItems(details)} />
- - @@ -76,10 +72,10 @@ export const ExpandedRow: FC = ({ item }) => { listItems={formatToListItems(attributes)} /> + - - - {allocatedModels.length > 0 ? ( + {allocatedModels.length > 0 ? ( +
@@ -91,34 +87,10 @@ export const ExpandedRow: FC = ({ item }) => { - {allocatedModels.map(({ model_id: modelId, ...rest }) => { - return ( - <> - - - - -
{modelId}
-
-
-
- - - -
- - - - - ); - })} + - ) : null} - + + ) : null} ); diff --git a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/memory_preview_chart.tsx b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/memory_preview_chart.tsx index ba790ba1c25765..dd9b6f82538607 100644 --- a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/memory_preview_chart.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/memory_preview_chart.tsx @@ -8,25 +8,26 @@ import { i18n } from '@kbn/i18n'; import React, { FC, useMemo } from 'react'; import { - Chart, - Settings, - BarSeries, - ScaleType, Axis, + BarSeries, + Chart, Position, + ScaleType, SeriesColorAccessor, + Settings, } from '@elastic/charts'; import { euiPaletteGray } from '@elastic/eui'; import { NodeDeploymentStatsResponse } from '../../../../common/types/trained_models'; import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; import { useCurrentEuiTheme } from '../../components/color_range_legend'; +import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common'; interface MemoryPreviewChartProps { memoryOverview: NodeDeploymentStatsResponse['memory_overview']; } export const MemoryPreviewChart: FC = ({ memoryOverview }) => { - const bytesFormatter = useFieldFormatter('bytes'); + const bytesFormatter = useFieldFormatter(FIELD_FORMAT_IDS.BYTES); const { euiTheme } = useCurrentEuiTheme(); @@ -112,7 +113,7 @@ export const MemoryPreviewChart: FC = ({ memoryOverview tooltip={{ headerFormatter: ({ value }) => i18n.translate('xpack.ml.trainedModels.nodesList.memoryBreakdown', { - defaultMessage: 'Approximate memory breakdown based on the node info', + defaultMessage: 'Approximate memory breakdown', }), }} /> diff --git a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/nodes_list.tsx b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/nodes_list.tsx index 42e51f1ab2971c..b1cc18e698c9de 100644 --- a/x-pack/plugins/ml/public/application/trained_models/nodes_overview/nodes_list.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/nodes_overview/nodes_list.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import React, { FC, useCallback, useMemo, useState } from 'react'; +import React, { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { EuiButtonIcon, EuiFlexGroup, @@ -31,6 +31,8 @@ import { MemoryPreviewChart } from './memory_preview_chart'; import { useFieldFormatter } from '../../contexts/kibana/use_field_formatter'; import { ListingPageUrlState } from '../../../../common/types/common'; import { useToastNotificationService } from '../../services/toast_notification_service'; +import { FIELD_FORMAT_IDS } from '../../../../../../../src/plugins/field_formats/common'; +import { useRefresh } from '../../routing/use_refresh'; export type NodeItem = NodeDeploymentStatsResponse; @@ -47,8 +49,11 @@ export const getDefaultNodesListState = (): ListingPageUrlState => ({ export const NodesList: FC = () => { const trainedModelsApiService = useTrainedModelsApiService(); + + const refresh = useRefresh(); + const { displayErrorToast } = useToastNotificationService(); - const bytesFormatter = useFieldFormatter('bytes'); + const bytesFormatter = useFieldFormatter(FIELD_FORMAT_IDS.BYTES); const [items, setItems] = useState([]); const [isLoading, setIsLoading] = useState(false); const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( @@ -179,6 +184,13 @@ export const NodesList: FC = () => { onRefresh: fetchNodesData, }); + useEffect( + function updateOnTimerRefresh() { + fetchNodesData(); + }, + [refresh] + ); + return ( <> diff --git a/x-pack/plugins/ml/public/application/trained_models/page.tsx b/x-pack/plugins/ml/public/application/trained_models/page.tsx index a6d99ca0fedc0a..54849f3e651df3 100644 --- a/x-pack/plugins/ml/public/application/trained_models/page.tsx +++ b/x-pack/plugins/ml/public/application/trained_models/page.tsx @@ -10,6 +10,7 @@ import React, { FC, Fragment, useMemo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { + EuiBetaBadge, EuiFlexGroup, EuiFlexItem, EuiPage, @@ -21,6 +22,7 @@ import { } from '@elastic/eui'; import { useLocation } from 'react-router-dom'; +import { i18n } from '@kbn/i18n'; import { NavigationMenu } from '../components/navigation_menu'; import { ModelsList } from './models_management'; import { TrainedModelsNavigationBar } from './navigation_bar'; @@ -44,14 +46,35 @@ export const Page: FC = () => { - -

- + + +

+ +

+
+
+ + -

-
+ +
diff --git a/x-pack/plugins/ml/public/application/util/component_utils.ts b/x-pack/plugins/ml/public/application/util/component_utils.ts new file mode 100644 index 00000000000000..764e4f0edd83ba --- /dev/null +++ b/x-pack/plugins/ml/public/application/util/component_utils.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MouseEvent } from 'react'; + +/** + * Removes focus from a button element when clicked, for example to + * ensure a wrapping tooltip is hidden on click. + */ +export const blurButtonOnClick = (callback: Function) => (event: MouseEvent) => { + (event.target as HTMLButtonElement).blur(); + callback(); +}; diff --git a/x-pack/plugins/ml/public/locator/formatters/trained_models.ts b/x-pack/plugins/ml/public/locator/formatters/trained_models.ts index d084c0675769fb..9e1c5d92ce4517 100644 --- a/x-pack/plugins/ml/public/locator/formatters/trained_models.ts +++ b/x-pack/plugins/ml/public/locator/formatters/trained_models.ts @@ -5,8 +5,13 @@ * 2.0. */ -import { TrainedModelsUrlState } from '../../../common/types/locator'; +import type { + TrainedModelsNodesUrlState, + TrainedModelsUrlState, +} from '../../../common/types/locator'; import { ML_PAGES } from '../../../common/constants/locator'; +import type { AppPageState, ListingPageUrlState } from '../../../common/types/common'; +import { setStateToKbnUrl } from '../../../../../../src/plugins/kibana_utils/public'; export function formatTrainedModelsManagementUrl( appBasePath: string, @@ -14,3 +19,31 @@ export function formatTrainedModelsManagementUrl( ): string { return `${appBasePath}/${ML_PAGES.TRAINED_MODELS_MANAGE}`; } + +export function formatTrainedModelsNodesManagementUrl( + appBasePath: string, + mlUrlGeneratorState: TrainedModelsNodesUrlState['pageState'] +): string { + let url = `${appBasePath}/${ML_PAGES.TRAINED_MODELS_NODES}`; + if (mlUrlGeneratorState) { + const { nodeId } = mlUrlGeneratorState; + if (nodeId) { + const nodesListState: Partial = { + queryText: `name:(${nodeId})`, + }; + + const queryState: AppPageState = { + [ML_PAGES.TRAINED_MODELS_NODES]: nodesListState, + }; + + url = setStateToKbnUrl>( + '_a', + queryState, + { useHash: false, storeInHashQuery: false }, + url + ); + } + } + + return url; +} diff --git a/x-pack/plugins/ml/public/locator/ml_locator.ts b/x-pack/plugins/ml/public/locator/ml_locator.ts index 7fa573c3e653d0..c79c93078d04a8 100644 --- a/x-pack/plugins/ml/public/locator/ml_locator.ts +++ b/x-pack/plugins/ml/public/locator/ml_locator.ts @@ -26,7 +26,10 @@ import { formatEditCalendarUrl, formatEditFilterUrl, } from './formatters'; -import { formatTrainedModelsManagementUrl } from './formatters/trained_models'; +import { + formatTrainedModelsManagementUrl, + formatTrainedModelsNodesManagementUrl, +} from './formatters/trained_models'; export type { MlLocatorParams, MlLocator }; @@ -70,6 +73,9 @@ export class MlLocatorDefinition implements LocatorDefinition { case ML_PAGES.TRAINED_MODELS_MANAGE: path = formatTrainedModelsManagementUrl('', params.pageState); break; + case ML_PAGES.TRAINED_MODELS_NODES: + path = formatTrainedModelsNodesManagementUrl('', params.pageState); + break; case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB: case ML_PAGES.ANOMALY_DETECTION_CREATE_JOB_ADVANCED: case ML_PAGES.DATA_VISUALIZER: diff --git a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts index 9e02a93a3c0f1f..1cd9aae79777b8 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts +++ b/x-pack/plugins/ml/server/models/data_recognizer/data_recognizer.ts @@ -293,8 +293,6 @@ export class DataRecognizer { index, size, body: searchBody, - // Ignored indices that are frozen - ignore_throttled: true, }); // @ts-expect-error incorrect search response type diff --git a/x-pack/plugins/monitoring/server/deprecations.ts b/x-pack/plugins/monitoring/server/deprecations.ts index 42868e3fa25847..14c3b9cdc6474b 100644 --- a/x-pack/plugins/monitoring/server/deprecations.ts +++ b/x-pack/plugins/monitoring/server/deprecations.ts @@ -23,27 +23,39 @@ export const deprecations = ({ }: ConfigDeprecationFactory): ConfigDeprecation[] => { return [ // This order matters. The "blanket rename" needs to happen at the end - renameFromRoot('xpack.monitoring.max_bucket_size', 'monitoring.ui.max_bucket_size'), - renameFromRoot('xpack.monitoring.min_interval_seconds', 'monitoring.ui.min_interval_seconds'), + renameFromRoot('xpack.monitoring.max_bucket_size', 'monitoring.ui.max_bucket_size', { + level: 'warning', + }), + renameFromRoot('xpack.monitoring.min_interval_seconds', 'monitoring.ui.min_interval_seconds', { + level: 'warning', + }), renameFromRoot( 'xpack.monitoring.show_license_expiration', - 'monitoring.ui.show_license_expiration' + 'monitoring.ui.show_license_expiration', + { level: 'warning' } ), renameFromRoot( 'xpack.monitoring.ui.container.elasticsearch.enabled', - 'monitoring.ui.container.elasticsearch.enabled' + 'monitoring.ui.container.elasticsearch.enabled', + { level: 'warning' } ), renameFromRoot( 'xpack.monitoring.ui.container.logstash.enabled', - 'monitoring.ui.container.logstash.enabled' + 'monitoring.ui.container.logstash.enabled', + { level: 'warning' } ), - renameFromRoot('xpack.monitoring.elasticsearch', 'monitoring.ui.elasticsearch'), - renameFromRoot('xpack.monitoring.ccs.enabled', 'monitoring.ui.ccs.enabled'), + renameFromRoot('xpack.monitoring.elasticsearch', 'monitoring.ui.elasticsearch', { + level: 'warning', + }), + renameFromRoot('xpack.monitoring.ccs.enabled', 'monitoring.ui.ccs.enabled', { + level: 'warning', + }), renameFromRoot( 'xpack.monitoring.elasticsearch.logFetchCount', - 'monitoring.ui.elasticsearch.logFetchCount' + 'monitoring.ui.elasticsearch.logFetchCount', + { level: 'warning' } ), - renameFromRoot('xpack.monitoring', 'monitoring'), + renameFromRoot('xpack.monitoring', 'monitoring', { level: 'warning' }), (config, fromPath, addDeprecation) => { const emailNotificationsEnabled = get(config, 'cluster_alerts.email_notifications.enabled'); if (emailNotificationsEnabled && !get(config, CLUSTER_ALERTS_ADDRESS_CONFIG_KEY)) { @@ -55,11 +67,14 @@ export const deprecations = ({ `Add [${fromPath}.${CLUSTER_ALERTS_ADDRESS_CONFIG_KEY}] to your kibana configs."`, ], }, + level: 'critical', }); } return config; }, - rename('xpack_api_polling_frequency_millis', 'licensing.api_polling_frequency'), + rename('xpack_api_polling_frequency_millis', 'licensing.api_polling_frequency', { + level: 'warning', + }), // TODO: Add deprecations for "monitoring.ui.elasticsearch.username: elastic" and "monitoring.ui.elasticsearch.username: kibana". // TODO: Add deprecations for using "monitoring.ui.elasticsearch.ssl.certificate" without "monitoring.ui.elasticsearch.ssl.key", and diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index bfb2eedf6deb2a..6b4ea62c167623 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -6,7 +6,11 @@ */ export type { AsDuration, AsPercent } from './utils/formatters'; -export { enableInspectEsQueries, maxSuggestions } from './ui_settings_keys'; +export { + enableInspectEsQueries, + maxSuggestions, + enableComparisonByDefault, +} from './ui_settings_keys'; export const casesFeatureId = 'observabilityCases'; diff --git a/x-pack/plugins/observability/common/ui_settings_keys.ts b/x-pack/plugins/observability/common/ui_settings_keys.ts index 69eb507328719e..4d34e216a017c0 100644 --- a/x-pack/plugins/observability/common/ui_settings_keys.ts +++ b/x-pack/plugins/observability/common/ui_settings_keys.ts @@ -7,3 +7,4 @@ export const enableInspectEsQueries = 'observability:enableInspectEsQueries'; export const maxSuggestions = 'observability:maxSuggestions'; +export const enableComparisonByDefault = 'observability:enableComparisonByDefault'; diff --git a/x-pack/plugins/observability/public/context/has_data_context.test.tsx b/x-pack/plugins/observability/public/context/has_data_context.test.tsx index a586a8bf0bccec..1eb4108b121811 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.test.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.test.tsx @@ -556,7 +556,7 @@ describe('HasDataContextProvider', () => { status: 'success', }, }, - hasAnyData: false, + hasAnyData: true, isAllRequestsComplete: true, forceUpdate: expect.any(String), onRefreshTimeRange: expect.any(Function), diff --git a/x-pack/plugins/observability/public/context/has_data_context.tsx b/x-pack/plugins/observability/public/context/has_data_context.tsx index caed130543accc..b6a45784a53b4b 100644 --- a/x-pack/plugins/observability/public/context/has_data_context.tsx +++ b/x-pack/plugins/observability/public/context/has_data_context.tsx @@ -146,9 +146,12 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode return appStatus !== undefined && appStatus !== FETCH_STATUS.LOADING; }); - const hasAnyData = (Object.keys(hasDataMap) as ObservabilityFetchDataPlugins[]).some( - (app) => hasDataMap[app]?.hasData === true - ); + const hasAnyData = (Object.keys(hasDataMap) as ObservabilityFetchDataPlugins[]).some((app) => { + const appHasData = hasDataMap[app]?.hasData; + return ( + appHasData === true || (Array.isArray(appHasData) && (appHasData as Alert[])?.length > 0) + ); + }); return ( = [ { columnHeaderType: 'not-filtered', - displayAsText: i18n.translate('xpack.observability.alertsTGrid.statusColumnDescription', { - defaultMessage: 'Alert Status', - }), + displayAsText: translations.statusColumnDescription, id: ALERT_STATUS, initialWidth: 110, }, { columnHeaderType: 'not-filtered', - displayAsText: i18n.translate('xpack.observability.alertsTGrid.lastUpdatedColumnDescription', { - defaultMessage: 'Last updated', - }), + displayAsText: translations.lastUpdatedColumnDescription, id: TIMESTAMP, initialWidth: 230, }, { columnHeaderType: 'not-filtered', - displayAsText: i18n.translate('xpack.observability.alertsTGrid.durationColumnDescription', { - defaultMessage: 'Duration', - }), + displayAsText: translations.durationColumnDescription, id: ALERT_DURATION, initialWidth: 116, }, { columnHeaderType: 'not-filtered', - displayAsText: i18n.translate('xpack.observability.alertsTGrid.reasonColumnDescription', { - defaultMessage: 'Reason', - }), + displayAsText: translations.reasonColumnDescription, id: ALERT_REASON, linkField: '*', }, @@ -235,12 +227,14 @@ function ObservabilityActions({ event, casePermissions, appId: observabilityFeatureId, + owner: observabilityFeatureId, onClose: afterCaseSelection, }), timelines.getAddToNewCaseButton({ event, casePermissions, appId: observabilityFeatureId, + owner: observabilityFeatureId, onClose: afterCaseSelection, }), ] @@ -249,73 +243,61 @@ function ObservabilityActions({ ]; }, [afterCaseSelection, casePermissions, timelines, event, statusActionItems, alertPermissions]); - const viewDetailsTextLabel = i18n.translate( - 'xpack.observability.alertsTable.viewDetailsTextLabel', - { - defaultMessage: 'View details', - } - ); - const viewInAppTextLabel = i18n.translate('xpack.observability.alertsTable.viewInAppTextLabel', { - defaultMessage: 'View in app', - }); - const moreActionsTextLabel = i18n.translate( - 'xpack.observability.alertsTable.moreActionsTextLabel', - { - defaultMessage: 'More actions', - } - ); + const actionsToolTip = + actionsMenuItems.length <= 0 + ? translations.notEnoughPermissions + : translations.moreActionsTextLabel; return ( <> - + setFlyoutAlert(alert)} data-test-subj="openFlyoutButton" - aria-label={viewDetailsTextLabel} + aria-label={translations.viewDetailsTextLabel} /> - + - {actionsMenuItems.length > 0 && ( - - - toggleActionsPopover(eventId)} - data-test-subj="alerts-table-row-action-more" - /> - - } - isOpen={openActionsPopoverId === eventId} - closePopover={closeActionsPopover} - panelPaddingSize="none" - anchorPosition="downLeft" - > - - - - )} + + + toggleActionsPopover(eventId)} + data-test-subj="alerts-table-row-action-more" + /> + + } + isOpen={openActionsPopoverId === eventId} + closePopover={closeActionsPopover} + panelPaddingSize="none" + anchorPosition="downLeft" + > + + + ); @@ -363,13 +345,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { id: 'expand', width: 120, headerCellRender: () => { - return ( - - {i18n.translate('xpack.observability.alertsTable.actionsTextLabel', { - defaultMessage: 'Actions', - })} - - ); + return {translations.actionsTextLabel}; }, rowCellRender: (actionProps: ActionProps) => { return ( @@ -390,6 +366,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { const sortDirection: SortDirection = 'desc'; return { appId: observabilityFeatureId, + casesOwner: observabilityFeatureId, casePermissions, type, columns, @@ -400,18 +377,16 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { hasAlertsCrudPermissions, indexNames, itemsPerPageOptions: [10, 25, 50], - loadingText: i18n.translate('xpack.observability.alertsTable.loadingTextLabel', { - defaultMessage: 'loading alerts', - }), - footerText: i18n.translate('xpack.observability.alertsTable.footerTextLabel', { - defaultMessage: 'alerts', - }), + loadingText: translations.loadingTextLabel, + footerText: translations.footerTextLabel, query: { query: `${ALERT_WORKFLOW_STATUS}: ${workflowStatus}${kuery !== '' ? ` and ${kuery}` : ''}`, language: 'kuery', }, renderCellValue: getRenderCellValue({ setFlyoutAlert }), rowRenderers: NO_ROW_RENDER, + // TODO: implement Kibana data view runtime fields in observability + runtimeMappings: {}, start: rangeFrom, setRefetch, sort: [ @@ -424,11 +399,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { filterStatus: workflowStatus as AlertWorkflowStatus, leadingControlColumns, trailingControlColumns, - unit: (totalAlerts: number) => - i18n.translate('xpack.observability.alertsTable.showingAlertsTitle', { - values: { totalAlerts }, - defaultMessage: '{totalAlerts, plural, =1 {alert} other {alerts}}', - }), + unit: (totalAlerts: number) => translations.showingAlertsTitle(totalAlerts), }; }, [ casePermissions, @@ -443,6 +414,7 @@ export function AlertsTableTGrid(props: AlertsTableTGridProps) { leadingControlColumns, deletedEventIds, ]); + const handleFlyoutClose = () => setFlyoutAlert(undefined); const { observabilityRuleTypeRegistry } = usePluginContext(); diff --git a/x-pack/plugins/observability/public/pages/alerts/translations.ts b/x-pack/plugins/observability/public/pages/alerts/translations.ts new file mode 100644 index 00000000000000..4578987e839a05 --- /dev/null +++ b/x-pack/plugins/observability/public/pages/alerts/translations.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const translations = { + viewDetailsTextLabel: i18n.translate('xpack.observability.alertsTable.viewDetailsTextLabel', { + defaultMessage: 'View details', + }), + viewInAppTextLabel: i18n.translate('xpack.observability.alertsTable.viewInAppTextLabel', { + defaultMessage: 'View in app', + }), + moreActionsTextLabel: i18n.translate('xpack.observability.alertsTable.moreActionsTextLabel', { + defaultMessage: 'More actions', + }), + notEnoughPermissions: i18n.translate('xpack.observability.alertsTable.notEnoughPermissions', { + defaultMessage: 'Additional privileges required', + }), + statusColumnDescription: i18n.translate( + 'xpack.observability.alertsTGrid.statusColumnDescription', + { + defaultMessage: 'Alert Status', + } + ), + lastUpdatedColumnDescription: i18n.translate( + 'xpack.observability.alertsTGrid.lastUpdatedColumnDescription', + { + defaultMessage: 'Last updated', + } + ), + durationColumnDescription: i18n.translate( + 'xpack.observability.alertsTGrid.durationColumnDescription', + { + defaultMessage: 'Duration', + } + ), + reasonColumnDescription: i18n.translate( + 'xpack.observability.alertsTGrid.reasonColumnDescription', + { + defaultMessage: 'Reason', + } + ), + actionsTextLabel: i18n.translate('xpack.observability.alertsTable.actionsTextLabel', { + defaultMessage: 'Actions', + }), + loadingTextLabel: i18n.translate('xpack.observability.alertsTable.loadingTextLabel', { + defaultMessage: 'loading alerts', + }), + footerTextLabel: i18n.translate('xpack.observability.alertsTable.footerTextLabel', { + defaultMessage: 'alerts', + }), + showingAlertsTitle: (totalAlerts: number) => + i18n.translate('xpack.observability.alertsTable.showingAlertsTitle', { + values: { totalAlerts }, + defaultMessage: '{totalAlerts, plural, =1 {alert} other {alerts}}', + }), +}; diff --git a/x-pack/plugins/observability/server/index.ts b/x-pack/plugins/observability/server/index.ts index d4d7127d8baee5..d99cf0865c0dd0 100644 --- a/x-pack/plugins/observability/server/index.ts +++ b/x-pack/plugins/observability/server/index.ts @@ -17,7 +17,7 @@ import { unwrapEsResponse, WrappedElasticsearchClientError, } from '../common/utils/unwrap_es_response'; -export { rangeQuery, kqlQuery } from './utils/queries'; +export { rangeQuery, kqlQuery, termQuery } from './utils/queries'; export { getInspectResponse } from '../common/utils/get_inspect_response'; export * from './types'; diff --git a/x-pack/plugins/observability/server/ui_settings.ts b/x-pack/plugins/observability/server/ui_settings.ts index 0bd9f99b5b1454..ad0aa31542e8c7 100644 --- a/x-pack/plugins/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability/server/ui_settings.ts @@ -9,7 +9,11 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { UiSettingsParams } from '../../../../src/core/types'; import { observabilityFeatureId } from '../common'; -import { enableInspectEsQueries, maxSuggestions } from '../common/ui_settings_keys'; +import { + enableComparisonByDefault, + enableInspectEsQueries, + maxSuggestions, +} from '../common/ui_settings_keys'; /** * uiSettings definitions for Observability. @@ -37,4 +41,15 @@ export const uiSettings: Record> = { }), schema: schema.number(), }, + [enableComparisonByDefault]: { + category: [observabilityFeatureId], + name: i18n.translate('xpack.observability.enableComparisonByDefault', { + defaultMessage: 'Comparison feature', + }), + value: true, + description: i18n.translate('xpack.observability.enableComparisonByDefaultDescription', { + defaultMessage: 'Enable the comparison feature on APM UI', + }), + schema: schema.boolean(), + }, }; diff --git a/x-pack/plugins/observability/server/utils/queries.ts b/x-pack/plugins/observability/server/utils/queries.ts index 953c0021636d44..54900cd46ea479 100644 --- a/x-pack/plugins/observability/server/utils/queries.ts +++ b/x-pack/plugins/observability/server/utils/queries.ts @@ -8,6 +8,14 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { fromKueryExpression, toElasticsearchQuery } from '@kbn/es-query'; +export function termQuery(field: T, value: string | undefined) { + if (!value) { + return []; + } + + return [{ term: { [field]: value } as Record }]; +} + export function rangeQuery( start?: number, end?: number, diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx index 72fd79805f9703..6e3de061fd191b 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.test.tsx @@ -5,13 +5,7 @@ * 2.0. */ -import { - fireEvent, - render, - waitFor, - waitForElementToBeRemoved, - within, -} from '@testing-library/react'; +import { render } from '@testing-library/react'; import { createMemoryHistory } from 'history'; import React from 'react'; @@ -22,60 +16,75 @@ import { Providers } from '../api_keys_management_app'; import { apiKeysAPIClientMock } from '../index.mock'; import { APIKeysGridPage } from './api_keys_grid_page'; -jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ - htmlIdGenerator: () => () => `id-${Math.random()}`, -})); +/* + * Note to engineers + * we moved these 4 tests below to "x-pack/test/functional/apps/api_keys/home_page.ts": + * 1-"creates API key when submitting form, redirects back and displays base64" + * 2-"creates API key with optional expiration, redirects back and displays base64" + * 3-"deletes multiple api keys using bulk select" + * 4-"deletes api key using cta button" + * to functional tests to avoid flakyness + */ -jest.setTimeout(15000); +describe('APIKeysGridPage', () => { + // We are spying on the console.error to avoid react to throw error + // in our test "displays error when fetching API keys fails" + // since we are using EuiErrorBoundary and react will console.error any errors + const consoleWarnMock = jest.spyOn(console, 'error').mockImplementation(); -const coreStart = coreMock.createStart(); + const coreStart = coreMock.createStart(); + const apiClientMock = apiKeysAPIClientMock.create(); + const { authc } = securityMock.createSetup(); -const apiClientMock = apiKeysAPIClientMock.create(); -apiClientMock.checkPrivileges.mockResolvedValue({ - areApiKeysEnabled: true, - canManage: true, - isAdmin: true, -}); -apiClientMock.getApiKeys.mockResolvedValue({ - apiKeys: [ - { - creation: 1571322182082, - expiration: 1571408582082, - id: '0QQZ2m0BO2XZwgJFuWTT', - invalidated: false, - name: 'first-api-key', - realm: 'reserved', - username: 'elastic', - }, - { - creation: 1571322182082, - expiration: 1571408582082, - id: 'BO2XZwgJFuWTT0QQZ2m0', - invalidated: false, - name: 'second-api-key', - realm: 'reserved', - username: 'elastic', - }, - ], -}); + beforeEach(() => { + apiClientMock.checkPrivileges.mockClear(); + apiClientMock.getApiKeys.mockClear(); + coreStart.http.get.mockClear(); + coreStart.http.post.mockClear(); + authc.getCurrentUser.mockClear(); -const authc = securityMock.createSetup().authc; -authc.getCurrentUser.mockResolvedValue( - mockAuthenticatedUser({ - username: 'jdoe', - full_name: '', - email: '', - enabled: true, - roles: ['superuser'], - }) -); + apiClientMock.checkPrivileges.mockResolvedValue({ + areApiKeysEnabled: true, + canManage: true, + isAdmin: true, + }); + apiClientMock.getApiKeys.mockResolvedValue({ + apiKeys: [ + { + creation: 1571322182082, + expiration: 1571408582082, + id: '0QQZ2m0BO2XZwgJFuWTT', + invalidated: false, + name: 'first-api-key', + realm: 'reserved', + username: 'elastic', + }, + { + creation: 1571322182082, + expiration: 1571408582082, + id: 'BO2XZwgJFuWTT0QQZ2m0', + invalidated: false, + name: 'second-api-key', + realm: 'reserved', + username: 'elastic', + }, + ], + }); -// FLAKY: https://github.com/elastic/kibana/issues/97085 -describe.skip('APIKeysGridPage', () => { + authc.getCurrentUser.mockResolvedValue( + mockAuthenticatedUser({ + username: 'jdoe', + full_name: '', + email: '', + enabled: true, + roles: ['superuser'], + }) + ); + }); it('loads and displays API keys', async () => { const history = createMemoryHistory({ initialEntries: ['/'] }); - const { getByText } = render( + const { findByText } = render( { ); - await waitForElementToBeRemoved(() => getByText(/Loading API keys/)); - getByText(/first-api-key/); - getByText(/second-api-key/); + expect(await findByText(/Loading API keys/)).not.toBeInTheDocument(); + await findByText(/first-api-key/); + await findByText(/second-api-key/); + }); + + afterAll(() => { + // Let's make sure we restore everything just in case + consoleWarnMock.mockRestore(); }); it('displays callout when API keys are disabled', async () => { @@ -98,7 +112,7 @@ describe.skip('APIKeysGridPage', () => { isAdmin: true, }); - const { getByText } = render( + const { findByText } = render( { ); - await waitForElementToBeRemoved(() => getByText(/Loading API keys/)); - getByText(/API keys not enabled/); + expect(await findByText(/Loading API keys/)).not.toBeInTheDocument(); + await findByText(/API keys not enabled/); }); it('displays error when user does not have required permissions', async () => { @@ -120,7 +134,7 @@ describe.skip('APIKeysGridPage', () => { isAdmin: false, }); - const { getByText } = render( + const { findByText } = render( { ); - await waitForElementToBeRemoved(() => getByText(/Loading API keys/)); - getByText(/You need permission to manage API keys/); + expect(await findByText(/Loading API keys/)).not.toBeInTheDocument(); + await findByText(/You need permission to manage API keys/); }); it('displays error when fetching API keys fails', async () => { apiClientMock.getApiKeys.mockRejectedValueOnce({ - body: { error: 'Internal Server Error', message: '', statusCode: 500 }, - }); - const history = createMemoryHistory({ initialEntries: ['/'] }); - - const { getByText } = render( - - - - ); - - await waitForElementToBeRemoved(() => getByText(/Loading API keys/)); - getByText(/Could not load API keys/); - }); - - it('creates API key when submitting form, redirects back and displays base64', async () => { - const history = createMemoryHistory({ initialEntries: ['/create'] }); - coreStart.http.get.mockResolvedValue([{ name: 'superuser' }]); - coreStart.http.post.mockResolvedValue({ id: '1D', api_key: 'AP1_K3Y' }); - - const { findByRole, findByDisplayValue } = render( - - - - ); - expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role'); - - const dialog = await findByRole('dialog'); - - fireEvent.click(await findByRole('button', { name: 'Create API key' })); - - const alert = await findByRole('alert'); - within(alert).getByText(/Enter a name/i); - - fireEvent.change(await within(dialog).findByLabelText('Name'), { - target: { value: 'Test' }, - }); - - fireEvent.click(await findByRole('button', { name: 'Create API key' })); - - await waitFor(() => { - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/api_key', { - body: JSON.stringify({ name: 'Test' }), - }); - expect(history.location.pathname).toBe('/'); - }); - - await findByDisplayValue(btoa('1D:AP1_K3Y')); - }); - - it('creates API key with optional expiration, redirects back and displays base64', async () => { - const history = createMemoryHistory({ initialEntries: ['/create'] }); - coreStart.http.get.mockResolvedValue([{ name: 'superuser' }]); - coreStart.http.post.mockResolvedValue({ id: '1D', api_key: 'AP1_K3Y' }); - - const { findByRole, findByDisplayValue } = render( - - - - ); - expect(coreStart.http.get).toHaveBeenCalledWith('/api/security/role'); - - const dialog = await findByRole('dialog'); - - fireEvent.change(await within(dialog).findByLabelText('Name'), { - target: { value: 'Test' }, - }); - - fireEvent.click(await within(dialog).findByLabelText('Expire after time')); - - fireEvent.click(await findByRole('button', { name: 'Create API key' })); - - const alert = await findByRole('alert'); - within(alert).getByText(/Enter a valid duration or disable this option\./i); - - fireEvent.change(await within(dialog).findByLabelText('Lifetime (days)'), { - target: { value: '12' }, - }); - - fireEvent.click(await findByRole('button', { name: 'Create API key' })); - - await waitFor(() => { - expect(coreStart.http.post).toHaveBeenLastCalledWith('/internal/security/api_key', { - body: JSON.stringify({ name: 'Test', expiration: '12d' }), - }); - expect(history.location.pathname).toBe('/'); + body: { + error: 'Internal Server Error', + message: 'Internal Server Error', + statusCode: 500, + }, }); - - await findByDisplayValue(btoa('1D:AP1_K3Y')); - }); - - it('deletes api key using cta button', async () => { - const history = createMemoryHistory({ initialEntries: ['/'] }); - - const { findByRole, findAllByLabelText } = render( - - - - ); - - const [deleteButton] = await findAllByLabelText(/Delete/i); - fireEvent.click(deleteButton); - - const dialog = await findByRole('dialog'); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Delete API key' })); - - await waitFor(() => { - expect(apiClientMock.invalidateApiKeys).toHaveBeenLastCalledWith( - [{ id: '0QQZ2m0BO2XZwgJFuWTT', name: 'first-api-key' }], - true - ); - }); - }); - - it('deletes multiple api keys using bulk select', async () => { const history = createMemoryHistory({ initialEntries: ['/'] }); - const { findByRole, findAllByRole } = render( + const { findByText } = render( { ); - const deleteCheckboxes = await findAllByRole('checkbox', { name: 'Select this row' }); - deleteCheckboxes.forEach((checkbox) => fireEvent.click(checkbox)); - fireEvent.click(await findByRole('button', { name: 'Delete API keys' })); - - const dialog = await findByRole('dialog'); - fireEvent.click(await within(dialog).findByRole('button', { name: 'Delete API keys' })); - - await waitFor(() => { - expect(apiClientMock.invalidateApiKeys).toHaveBeenLastCalledWith( - [ - { id: '0QQZ2m0BO2XZwgJFuWTT', name: 'first-api-key' }, - { id: 'BO2XZwgJFuWTT0QQZ2m0', name: 'second-api-key' }, - ], - true - ); - }); + expect(await findByText(/Loading API keys/)).not.toBeInTheDocument(); + await findByText(/Could not load API keys/); }); }); diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx index dcf2a7bfe51653..a4843e4637d8bf 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/api_keys_grid_page.tsx @@ -164,6 +164,7 @@ export class APIKeysGridPage extends Component { {...reactRouterNavigate(this.props.history, '/create')} fill iconType="plusInCircleFilled" + data-test-subj="apiKeysCreatePromptButton" > { {...reactRouterNavigate(this.props.history, '/create')} fill iconType="plusInCircleFilled" + data-test-subj="apiKeysCreateTableButton" > { color: 'danger', onClick: (item) => invalidateApiKeyPrompt([{ id: item.id, name: item.name }], this.onApiKeysInvalidated), + 'data-test-subj': 'apiKeysTableDeleteAction', }, ], }, diff --git a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx index e1ffc3b4b35150..f2fa6f7de468e5 100644 --- a/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx +++ b/x-pack/plugins/security/public/management/api_keys/api_keys_grid/create_api_key_flyout.tsx @@ -202,6 +202,7 @@ export const CreateApiKeyFlyout: FunctionComponent = ({ isInvalid={form.touched.name && !!form.errors.name} inputRef={firstFieldRef} fullWidth + data-test-subj="apiKeyNameInput" /> @@ -258,6 +259,7 @@ export const CreateApiKeyFlyout: FunctionComponent = ({ )} checked={!!form.values.customExpiration} onChange={(e) => form.setValue('customExpiration', e.target.checked)} + data-test-subj="apiKeyCustomExpirationSwitch" /> {form.values.customExpiration && ( <> @@ -284,6 +286,7 @@ export const CreateApiKeyFlyout: FunctionComponent = ({ defaultValue={form.values.expiration} isInvalid={form.touched.expiration && !!form.errors.expiration} fullWidth + data-test-subj="apiKeyCustomExpirationInput" /> diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 2772c3de51065a..5b846751d26df7 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -5,14 +5,20 @@ * 2.0. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ENABLE_ITOM } from '../../actions/server/constants/connectors'; import type { TransformConfigSchema } from './transforms/types'; import { ENABLE_CASE_CONNECTOR } from '../../cases/common'; import { METADATA_TRANSFORMS_PATTERN } from './endpoint/constants'; +/** + * as const + * + * The const assertion ensures that type widening does not occur + * https://mariusschulz.com/blog/literal-type-widening-in-typescript + * Please follow this convention when adding to this file + */ + export const APP_ID = 'securitySolution' as const; -export const APP_UI_ID = 'securitySolutionUI'; +export const APP_UI_ID = 'securitySolutionUI' as const; export const CASES_FEATURE_ID = 'securitySolutionCases' as const; export const SERVER_APP_ID = 'siem' as const; export const APP_NAME = 'Security' as const; @@ -26,6 +32,8 @@ export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz' as const; export const DEFAULT_DARK_MODE = 'theme:darkMode' as const; export const DEFAULT_INDEX_KEY = 'securitySolution:defaultIndex' as const; export const DEFAULT_NUMBER_FORMAT = 'format:number:defaultPattern' as const; +export const DEFAULT_DATA_VIEW_ID = 'security-solution' as const; +export const DEFAULT_TIME_FIELD = '@timestamp' as const; export const DEFAULT_TIME_RANGE = 'timepicker:timeDefaults' as const; export const DEFAULT_REFRESH_RATE_INTERVAL = 'timepicker:refreshIntervalDefaults' as const; export const DEFAULT_APP_TIME_RANGE = 'securitySolution:timeDefaults' as const; @@ -51,7 +59,6 @@ export const DEFAULT_TIMEPICKER_QUICK_RANGES = 'timepicker:quickRanges' as const export const DEFAULT_TRANSFORMS = 'securitySolution:transforms' as const; export const SCROLLING_DISABLED_CLASS_NAME = 'scrolling-disabled' as const; export const GLOBAL_HEADER_HEIGHT = 96 as const; // px -export const GLOBAL_HEADER_HEIGHT_WITH_GLOBAL_BANNER = 128 as const; // px export const FILTERS_GLOBAL_HEIGHT = 109 as const; // px export const FULL_SCREEN_TOGGLED_CLASS_NAME = 'fullScreenToggled' as const; export const NO_ALERT_INDEX = 'no-alert-index-049FC71A-4C2C-446F-9901-37XMC5024C51' as const; @@ -268,6 +275,7 @@ export const TIMELINE_PREPACKAGED_URL = `${TIMELINE_URL}/_prepackaged` as const; export const NOTE_URL = '/api/note' as const; export const PINNED_EVENT_URL = '/api/pinned_event' as const; +export const SOURCERER_API_URL = '/api/sourcerer' as const; /** * Default signals index key for kibana.dev.yml @@ -316,6 +324,7 @@ export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.resilient', '.servicenow', '.servicenow-sir', + '.servicenow-itom', '.slack', '.swimlane', '.teams', @@ -326,11 +335,6 @@ if (ENABLE_CASE_CONNECTOR) { NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.push('.case'); } -// TODO: Remove when ITOM is ready -if (ENABLE_ITOM) { - NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS.push('.servicenow-itom'); -} - export const NOTIFICATION_THROTTLE_NO_ACTIONS = 'no_actions' as const; export const NOTIFICATION_THROTTLE_RULE = 'rule' as const; @@ -355,7 +359,7 @@ export const ELASTIC_NAME = 'estc' as const; export const METADATA_TRANSFORM_STATS_URL = `/api/transform/transforms/${METADATA_TRANSFORMS_PATTERN}/_stats`; -export const RISKY_HOSTS_INDEX_PREFIX = 'ml_host_risk_score_latest_'; +export const RISKY_HOSTS_INDEX_PREFIX = 'ml_host_risk_score_latest_' as const; export const TRANSFORM_STATES = { ABORTING: 'aborting', diff --git a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts index 033e979d2814cc..42c10614975eb3 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/get_query_filter.ts @@ -11,7 +11,7 @@ import type { CreateExceptionListItemSchema, } from '@kbn/securitysolution-io-ts-list-types'; import { buildExceptionFilter } from '@kbn/securitysolution-list-utils'; -import { Filter, EsQueryConfig, IndexPatternBase, buildEsQuery } from '@kbn/es-query'; +import { Filter, EsQueryConfig, DataViewBase, buildEsQuery } from '@kbn/es-query'; import { ESBoolQuery } from '../typed_json'; import { Query, Index, TimestampOverrideOrUndefined } from './schemas/common/schemas'; @@ -24,7 +24,7 @@ export const getQueryFilter = ( lists: Array, excludeExceptions: boolean = true ): ESBoolQuery => { - const indexPattern: IndexPatternBase = { + const indexPattern: DataViewBase = { fields: [], title: index.join(), }; diff --git a/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts b/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts index be5fd3b5c4dc54..86bc11f7a596d8 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/index_fields/index.ts @@ -5,79 +5,17 @@ * 2.0. */ -import type { IFieldSubType } from '@kbn/es-query'; - -import type { - IEsSearchRequest, - IEsSearchResponse, - IIndexPattern, -} from '../../../../../../src/plugins/data/common'; -import type { DocValueFields, Maybe } from '../common'; - -interface FieldInfo { - category: string; - description?: string; - example?: string | number; - format?: string; - name: string; - type?: string; -} - -export interface IndexField { - /** Where the field belong */ - category: string; - /** Example of field's value */ - example?: Maybe; - /** whether the field's belong to an alias index */ - indexes: Array>; - /** The name of the field */ - name: string; - /** The type of the field's values as recognized by Kibana */ - type: string; - /** Whether the field's values can be efficiently searched for */ - searchable: boolean; - /** Whether the field's values can be aggregated */ - aggregatable: boolean; - /** Description of the field */ - description?: Maybe; - format?: Maybe; - /** the elastic type as mapped in the index */ - esTypes?: string[]; - subType?: IFieldSubType; - readFromDocValues: boolean; -} - -export type BeatFields = Record; - -export interface IndexFieldsStrategyRequest extends IEsSearchRequest { - indices: string[]; - onlyCheckIfIndicesExist: boolean; -} - -export interface IndexFieldsStrategyResponse extends IEsSearchResponse { - indexFields: IndexField[]; - indicesExist: string[]; -} - -export interface BrowserField { - aggregatable: boolean; - category: string; - description: string | null; - example: string | number | null; - fields: Readonly>>; - format: string; - indexes: string[]; - name: string; - searchable: boolean; - type: string; - subType?: IFieldSubType; -} - -export type BrowserFields = Readonly>>; - -export const EMPTY_BROWSER_FIELDS = {}; -export const EMPTY_DOCVALUE_FIELD: DocValueFields[] = []; -export const EMPTY_INDEX_PATTERN: IIndexPattern = { - fields: [], - title: '', -}; +export type { + FieldInfo, + IndexField, + BeatFields, + IndexFieldsStrategyRequest, + IndexFieldsStrategyResponse, + BrowserField, + BrowserFields, +} from '../../../../timelines/common'; +export { + EMPTY_BROWSER_FIELDS, + EMPTY_DOCVALUE_FIELD, + EMPTY_INDEX_FIELDS, +} from '../../../../timelines/common'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts index 39f23a63c8afea..d6735b59c229d1 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/events/last_event_time/index.ts @@ -10,6 +10,7 @@ export { LastEventIndexKey } from '../../../../../../timelines/common'; export type { LastTimeDetails, TimelineEventsLastEventTimeStrategyResponse, + TimelineKpiStrategyRequest, TimelineKpiStrategyResponse, TimelineEventsLastEventTimeRequestOptions, } from '../../../../../../timelines/common'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts index 548560ac5cb8cb..2d94a36a937d54 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/timeline/index.ts @@ -5,6 +5,7 @@ * 2.0. */ +import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { IEsSearchRequest } from '../../../../../../src/plugins/data/common'; import { ESQuery } from '../../typed_json'; import { @@ -41,6 +42,7 @@ export interface TimelineRequestBasicOptions extends IEsSearchRequest { defaultIndex: string[]; docValueFields?: DocValueFields[]; factoryQueryType?: TimelineFactoryQueryTypes; + runtimeMappings: MappingRuntimeFields; } export interface TimelineRequestSortField extends SortField { @@ -171,6 +173,7 @@ export interface SortTimelineInput { export interface TimelineInput { columns?: Maybe; dataProviders?: Maybe; + dataViewId?: Maybe; description?: Maybe; eqlOptions?: Maybe; eventType?: Maybe; diff --git a/x-pack/plugins/security_solution/common/test/index.ts b/x-pack/plugins/security_solution/common/test/index.ts index 6d5df76b306a3a..53261d54e84b03 100644 --- a/x-pack/plugins/security_solution/common/test/index.ts +++ b/x-pack/plugins/security_solution/common/test/index.ts @@ -7,12 +7,12 @@ // For the source of these roles please consult the PR these were introduced https://github.com/elastic/kibana/pull/81866#issue-511165754 export enum ROLES { + soc_manager = 'soc_manager', reader = 'reader', t1_analyst = 't1_analyst', t2_analyst = 't2_analyst', hunter = 'hunter', rule_author = 'rule_author', - soc_manager = 'soc_manager', platform_engineer = 'platform_engineer', detections_admin = 'detections_admin', } diff --git a/x-pack/plugins/security_solution/common/types/timeline/index.ts b/x-pack/plugins/security_solution/common/types/timeline/index.ts index c0046f7535db84..60fd126e6fd85c 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/index.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/index.ts @@ -272,6 +272,7 @@ export type TimelineTypeLiteralWithNull = runtimeTypes.TypeOf; +export type TimelineWithoutExternalRefs = Omit; /* * Timeline IDs @@ -719,6 +720,7 @@ export interface TimelineResult { created?: Maybe; createdBy?: Maybe; dataProviders?: Maybe; + dataViewId?: Maybe; dateRange?: Maybe; description?: Maybe; eqlOptions?: Maybe; diff --git a/x-pack/plugins/security_solution/common/types/timeline/store.ts b/x-pack/plugins/security_solution/common/types/timeline/store.ts index 03cf0c39378e57..75cd44ba2b7d79 100644 --- a/x-pack/plugins/security_solution/common/types/timeline/store.ts +++ b/x-pack/plugins/security_solution/common/types/timeline/store.ts @@ -38,19 +38,20 @@ export interface SortColumnTimeline { } export interface TimelinePersistInput { - id: string; + columns: ColumnHeaderOptions[]; dataProviders?: DataProvider[]; + dataViewId: string; dateRange?: { start: string; end: string; }; + defaultColumns?: ColumnHeaderOptions[]; excludedRowRendererIds?: RowRendererId[]; expandedDetail?: TimelineExpandedDetail; filters?: Filter[]; - columns: ColumnHeaderOptions[]; - defaultColumns?: ColumnHeaderOptions[]; - itemsPerPage?: number; + id: string; indexNames: string[]; + itemsPerPage?: number; kqlQuery?: { filterQuery: SerializedFilterQuery | null; }; diff --git a/x-pack/plugins/security_solution/cypress/cypress.json b/x-pack/plugins/security_solution/cypress/cypress.json index 6a9a240af58739..8c27309becf08f 100644 --- a/x-pack/plugins/security_solution/cypress/cypress.json +++ b/x-pack/plugins/security_solution/cypress/cypress.json @@ -12,5 +12,10 @@ "video": false, "videosFolder": "../../../target/kibana-security-solution/cypress/videos", "viewportHeight": 900, - "viewportWidth": 1440 + "viewportWidth": 1440, + "env": { + "protocol": "http", + "hostname": "localhost", + "configport": "5601" + } } diff --git a/x-pack/plugins/security_solution/cypress/downloads/timelines_export.ndjson b/x-pack/plugins/security_solution/cypress/downloads/timelines_export.ndjson new file mode 100644 index 00000000000000..8cf76734ad8767 --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/downloads/timelines_export.ndjson @@ -0,0 +1 @@ +{"savedObjectId":"46cca0e0-2580-11ec-8e56-9dafa0b0343b","version":"WzIyNjIzNCwxXQ==","columns":[{"id":"@timestamp"},{"id":"user.name"},{"id":"event.category"},{"id":"event.action"},{"id":"host.name"}],"kqlMode":"filter","kqlQuery":{"filterQuery":{"kuery":{"expression":"host.name: *","kind":"kuery"}}},"dateRange":{"start":"1514809376000","end":"1577881376000"},"description":"This is the best timeline","title":"Security Timeline","created":1633399341550,"createdBy":"elastic","updated":1633399341550,"updatedBy":"elastic","savedQueryId":null,"dataViewId":null,"timelineType":"default","sort":[],"eventNotes":[],"globalNotes":[],"pinnedEventIds":[]} diff --git a/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts b/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts index 23016ecc512b1f..0337cd3bd6e17d 100644 --- a/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/cases/privileges.spec.ts @@ -18,184 +18,28 @@ import { filterStatusOpen, } from '../../tasks/create_new_case'; import { - constructUrlWithUser, - getEnvAuth, + loginAndWaitForHostDetailsPage, loginWithUserAndWaitForPageWithoutDateRange, + logout, } from '../../tasks/login'; +import { + createUsersAndRoles, + deleteUsersAndRoles, + secAll, + secAllUser, + secReadCasesAllUser, + secReadCasesAll, +} from '../../tasks/privileges'; import { CASES_URL } from '../../urls/navigation'; - -interface User { - username: string; - password: string; - description?: string; - roles: string[]; -} - -interface UserInfo { - username: string; - full_name: string; - email: string; -} - -interface FeaturesPrivileges { - [featureId: string]: string[]; -} - -interface ElasticsearchIndices { - names: string[]; - privileges: string[]; -} - -interface ElasticSearchPrivilege { - cluster?: string[]; - indices?: ElasticsearchIndices[]; -} - -interface KibanaPrivilege { - spaces: string[]; - base?: string[]; - feature?: FeaturesPrivileges; -} - -interface Role { - name: string; - privileges: { - elasticsearch?: ElasticSearchPrivilege; - kibana?: KibanaPrivilege[]; - }; -} - -const secAll: Role = { - name: 'sec_all_role', - privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, - kibana: [ - { - feature: { - siem: ['all'], - securitySolutionCases: ['all'], - actions: ['all'], - actionsSimulators: ['all'], - }, - spaces: ['*'], - }, - ], - }, -}; - -const secAllUser: User = { - username: 'sec_all_user', - password: 'password', - roles: [secAll.name], -}; - -const secReadCasesAll: Role = { - name: 'sec_read_cases_all_role', - privileges: { - elasticsearch: { - indices: [ - { - names: ['*'], - privileges: ['all'], - }, - ], - }, - kibana: [ - { - feature: { - siem: ['read'], - securitySolutionCases: ['all'], - actions: ['all'], - actionsSimulators: ['all'], - }, - spaces: ['*'], - }, - ], - }, -}; - -const secReadCasesAllUser: User = { - username: 'sec_read_cases_all_user', - password: 'password', - roles: [secReadCasesAll.name], -}; - +import { openSourcerer } from '../../tasks/sourcerer'; const usersToCreate = [secAllUser, secReadCasesAllUser]; const rolesToCreate = [secAll, secReadCasesAll]; - -const getUserInfo = (user: User): UserInfo => ({ - username: user.username, - full_name: user.username.replace('_', ' '), - email: `${user.username}@elastic.co`, -}); - -const createUsersAndRoles = (users: User[], roles: Role[]) => { - const envUser = getEnvAuth(); - for (const role of roles) { - cy.log(`Creating role: ${JSON.stringify(role)}`); - cy.request({ - body: role.privileges, - headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, - method: 'PUT', - url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`), - }) - .its('status') - .should('eql', 204); - } - - for (const user of users) { - const userInfo = getUserInfo(user); - cy.log(`Creating user: ${JSON.stringify(user)}`); - cy.request({ - body: { - username: user.username, - password: user.password, - roles: user.roles, - full_name: userInfo.full_name, - email: userInfo.email, - }, - headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, - method: 'POST', - url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`), - }) - .its('status') - .should('eql', 200); - } -}; - -const deleteUsersAndRoles = (users: User[], roles: Role[]) => { - const envUser = getEnvAuth(); - for (const user of users) { - cy.log(`Deleting user: ${JSON.stringify(user)}`); - cy.request({ - headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, - method: 'DELETE', - url: constructUrlWithUser(envUser, `/internal/security/users/${user.username}`), - failOnStatusCode: false, - }) - .its('status') - .should('oneOf', [204, 404]); - } - - for (const role of roles) { - cy.log(`Deleting role: ${JSON.stringify(role)}`); - cy.request({ - headers: { 'kbn-xsrf': 'cypress-creds-via-config' }, - method: 'DELETE', - url: constructUrlWithUser(envUser, `/api/security/role/${role.name}`), - failOnStatusCode: false, - }) - .its('status') - .should('oneOf', [204, 404]); - } +// needed to generate index pattern +const visitSecuritySolution = () => { + loginAndWaitForHostDetailsPage(); + openSourcerer(); + logout(); }; const testCase: TestCaseWithoutTimeline = { @@ -205,11 +49,11 @@ const testCase: TestCaseWithoutTimeline = { reporter: 'elastic', owner: 'securitySolution', }; - describe('Cases privileges', () => { before(() => { cleanKibana(); createUsersAndRoles(usersToCreate, rolesToCreate); + visitSecuritySolution(); }); after(() => { diff --git a/x-pack/plugins/security_solution/cypress/integration/data_sources/create_runtime_field.spec.ts b/x-pack/plugins/security_solution/cypress/integration/data_sources/create_runtime_field.spec.ts new file mode 100644 index 00000000000000..1f2ca36c5a3d7d --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/data_sources/create_runtime_field.spec.ts @@ -0,0 +1,64 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { cleanKibana } from '../../tasks/common'; + +import { loginAndWaitForPage } from '../../tasks/login'; +import { openTimelineUsingToggle } from '../../tasks/security_main'; +import { openTimelineFieldsBrowser, populateTimeline } from '../../tasks/timeline'; + +import { HOSTS_URL, ALERTS_URL } from '../../urls/navigation'; + +import { waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded } from '../../tasks/alerts'; +import { createCustomRuleActivated } from '../../tasks/api_calls/rules'; + +import { getNewRule } from '../../objects/rule'; +import { refreshPage } from '../../tasks/security_header'; +import { waitForAlertsToPopulate } from '../../tasks/create_new_rule'; +import { openEventsViewerFieldsBrowser } from '../../tasks/hosts/events'; + +describe('Create DataView runtime field', () => { + before(() => { + cleanKibana(); + }); + + it('adds field to alert table', () => { + const fieldName = 'field.name.alert.page'; + loginAndWaitForPage(ALERTS_URL); + waitForAlertsPanelToBeLoaded(); + waitForAlertsIndexToBeCreated(); + createCustomRuleActivated(getNewRule()); + refreshPage(); + waitForAlertsToPopulate(500); + openEventsViewerFieldsBrowser(); + + cy.get('[data-test-subj="create-field"]').click(); + cy.get('.indexPatternFieldEditorMaskOverlay').find('[data-test-subj="input"]').type(fieldName); + cy.get('[data-test-subj="fieldSaveButton"]').click(); + + cy.get( + `[data-test-subj="events-viewer-panel"] [data-test-subj="dataGridHeaderCell-${fieldName}"]` + ).should('exist'); + }); + + it('adds field to timeline', () => { + const fieldName = 'field.name.timeline'; + + loginAndWaitForPage(HOSTS_URL); + openTimelineUsingToggle(); + populateTimeline(); + openTimelineFieldsBrowser(); + + cy.get('[data-test-subj="create-field"]').click(); + cy.get('.indexPatternFieldEditorMaskOverlay').find('[data-test-subj="input"]').type(fieldName); + cy.get('[data-test-subj="fieldSaveButton"]').click(); + + cy.get(`[data-test-subj="timeline"] [data-test-subj="header-text-${fieldName}"]`).should( + 'exist' + ); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts b/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts index 26c366e981d440..bd7acc38c10215 100644 --- a/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/data_sources/sourcerer.spec.ts @@ -5,7 +5,10 @@ * 2.0. */ -import { loginAndWaitForPage } from '../../tasks/login'; +import { + loginAndWaitForPage, + loginWithUserAndWaitForPageWithoutDateRange, +} from '../../tasks/login'; import { HOSTS_URL } from '../../urls/navigation'; import { waitForAllHostsToBeLoaded } from '../../tasks/hosts/all_hosts'; @@ -28,20 +31,34 @@ import { openTimelineUsingToggle } from '../../tasks/security_main'; import { populateTimeline } from '../../tasks/timeline'; import { SERVER_SIDE_EVENT_COUNT } from '../../screens/timeline'; import { cleanKibana } from '../../tasks/common'; +import { createUsersAndRoles, secReadCasesAll, secReadCasesAllUser } from '../../tasks/privileges'; +import { TOASTER } from '../../screens/configure_cases'; +const usersToCreate = [secReadCasesAllUser]; +const rolesToCreate = [secReadCasesAll]; // Skipped at the moment as this has flake due to click handler issues. This has been raised with team members // and the code is being re-worked and then these tests will be unskipped -describe.skip('Sourcerer', () => { - before(() => { +describe('Sourcerer', () => { + beforeEach(() => { cleanKibana(); }); - - beforeEach(() => { - cy.clearLocalStorage(); - loginAndWaitForPage(HOSTS_URL); + describe('permissions', () => { + before(() => { + createUsersAndRoles(usersToCreate, rolesToCreate); + }); + it(`role(s) ${secReadCasesAllUser.roles.join()} shows error when user does not have permissions`, () => { + loginWithUserAndWaitForPageWithoutDateRange(HOSTS_URL, secReadCasesAllUser); + cy.get(TOASTER).should('have.text', 'Write role required to generate data'); + }); }); + // Originially written in December 2020, flakey from day1 + // has always been skipped with intentions to fix, see note at top of file + describe.skip('Default scope', () => { + beforeEach(() => { + cy.clearLocalStorage(); + loginAndWaitForPage(HOSTS_URL); + }); - describe('Default scope', () => { it('has SIEM index patterns selected on initial load', () => { openSourcerer(); isSourcererSelection(`auditbeat-*`); @@ -52,7 +69,7 @@ describe.skip('Sourcerer', () => { isSourcererOptions([`metrics-*`, `logs-*`]); }); - it('selected KIP gets added to sourcerer', () => { + it('selected DATA_VIEW gets added to sourcerer', () => { setSourcererOption(`metrics-*`); openSourcerer(); isSourcererSelection(`metrics-*`); @@ -75,8 +92,14 @@ describe.skip('Sourcerer', () => { isNotSourcererSelection(`metrics-*`); }); }); + // Originially written in December 2020, flakey from day1 + // has always been skipped with intentions to fix + describe.skip('Timeline scope', () => { + beforeEach(() => { + cy.clearLocalStorage(); + loginAndWaitForPage(HOSTS_URL); + }); - describe('Timeline scope', () => { const alertPatterns = ['.siem-signals-default']; const rawPatterns = ['auditbeat-*']; const allPatterns = [...alertPatterns, ...rawPatterns]; diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts index 803ff4b4d0d807..033a12dd9de3ee 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/alerts_details.spec.ts @@ -9,6 +9,7 @@ import { ALERT_FLYOUT, CELL_TEXT, JSON_TEXT, TABLE_ROWS } from '../../screens/al import { expandFirstAlert, + refreshAlerts, waitForAlertsIndexToBeCreated, waitForAlertsPanelToBeLoaded, } from '../../tasks/alerts'; @@ -32,6 +33,7 @@ describe('Alert details with unmapped fields', () => { createCustomRuleActivated(getUnmappedRule()); loginAndWaitForPageWithoutDateRange(ALERTS_URL); waitForAlertsPanelToBeLoaded(); + refreshAlerts(); expandFirstAlert(); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts index 10f556a11bf602..171d224cc32d35 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/event_correlation_rule.spec.ts @@ -70,7 +70,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { ALERTS_URL } from '../../urls/navigation'; -describe.skip('Detection rules, EQL', () => { +describe('Detection rules, EQL', () => { const expectedUrls = getEqlRule().referenceUrls.join(''); const expectedFalsePositives = getEqlRule().falsePositivesExamples.join(''); const expectedTags = getEqlRule().tags.join(''); @@ -169,7 +169,7 @@ describe.skip('Detection rules, EQL', () => { }); }); -describe.skip('Detection rules, sequence EQL', () => { +describe('Detection rules, sequence EQL', () => { const expectedNumberOfRules = 1; const expectedNumberOfSequenceAlerts = '1 alert'; diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts index 02621ea49e9068..378de8f0bc5934 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/indicator_match_rule.spec.ts @@ -114,7 +114,7 @@ import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; import { goBackToAllRulesTable } from '../../tasks/rule_details'; import { ALERTS_URL, RULE_CREATION } from '../../urls/navigation'; -import { DEFAULT_THREAT_MATCH_QUERY } from '../../../common/constants'; +const DEFAULT_THREAT_MATCH_QUERY = '@timestamp >= "now-30d"'; describe('indicator match', () => { describe('Detection rules, Indicator Match', () => { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts index ef3d3a82d40bd7..92f9e8180d50c9 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_rules/sorting.spec.ts @@ -34,7 +34,6 @@ import { waitForRuleToChangeStatus, } from '../../tasks/alerts_detection_rules'; import { loginAndWaitForPageWithoutDateRange } from '../../tasks/login'; -import { DEFAULT_RULE_REFRESH_INTERVAL_VALUE } from '../../../common/constants'; import { ALERTS_URL } from '../../urls/navigation'; import { createCustomRule } from '../../tasks/api_calls/rules'; @@ -46,6 +45,8 @@ import { getNewThresholdRule, } from '../../objects/rule'; +const DEFAULT_RULE_REFRESH_INTERVAL_VALUE = 60000; + describe('Alerts detection rules', () => { beforeEach(() => { cleanKibana(); diff --git a/x-pack/plugins/security_solution/cypress/integration/ml/ml_conditional_links.spec.ts b/x-pack/plugins/security_solution/cypress/integration/ml/ml_conditional_links.spec.ts index 89a0d5a660b97b..f9d78ba12a5ea9 100644 --- a/x-pack/plugins/security_solution/cypress/integration/ml/ml_conditional_links.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/ml/ml_conditional_links.spec.ts @@ -98,7 +98,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpNullKqlQuery); cy.url().should( 'include', - 'app/security/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + 'app/security/network/ip/127.0.0.1/source?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -106,7 +106,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkSingleIpKqlQuery); cy.url().should( 'include', - '/app/security/network/ip/127.0.0.1/source?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/network/ip/127.0.0.1/source?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -114,7 +114,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpNullKqlQuery); cy.url().should( 'include', - 'app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + 'app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -122,7 +122,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkMultipleIpKqlQuery); cy.url().should( 'include', - '/app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/network/flows?query=(language:kuery,query:%27((source.ip:%20%22127.0.0.1%22%20or%20destination.ip:%20%22127.0.0.1%22)%20or%20(source.ip:%20%22127.0.0.2%22%20or%20destination.ip:%20%22127.0.0.2%22))%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -130,15 +130,16 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlNetworkNullKqlQuery); cy.url().should( 'include', - '/app/security/network/flows?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/network/flows?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); it('redirects from a $ip$ with a value for the query', () => { loginAndWaitForPageWithoutDateRange(mlNetworkKqlQuery); + cy.url().should( 'include', - '/app/security/network/flows?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + `/app/security/network/flows?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-08-28T11:00:00.000Z%27,kind:absolute,to:%272019-08-28T13:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))` ); }); @@ -146,7 +147,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostNullKqlQuery); cy.url().should( 'include', - '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -154,7 +155,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQueryVariable); cy.url().should( 'include', - '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/hosts/siem-windows/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -162,7 +163,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostSingleHostKqlQuery); cy.url().should( 'include', - '/app/security/hosts/siem-windows/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/hosts/siem-windows/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -170,7 +171,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostMultiHostNullKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -178,7 +179,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostMultiHostKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(host.name:%20%22siem-windows%22%20or%20host.name:%20%22siem-suricata%22)%20and%20((process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22))%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -186,7 +187,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostVariableHostNullKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/hosts/anomalies?timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); @@ -194,7 +195,7 @@ describe('ml conditional links', () => { loginAndWaitForPageWithoutDateRange(mlHostVariableHostKqlQuery); cy.url().should( 'include', - '/app/security/hosts/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:!(%27auditbeat-*%27))' + '/app/security/hosts/anomalies?query=(language:kuery,query:%27(process.name:%20%22conhost.exe%22%20or%20process.name:%20%22sc.exe%22)%27)&timerange=(global:(linkTo:!(timeline),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)),timeline:(linkTo:!(global),timerange:(from:%272019-06-06T06:00:00.000Z%27,kind:absolute,to:%272019-06-07T05:59:59.999Z%27)))&sourcerer=(default:(id:security-solution-default,selectedPatterns:!(%27auditbeat-*%27)))' ); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts index fb41aec91b6c4a..cbff911e5d9826 100644 --- a/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/timelines/creation.spec.ts @@ -121,7 +121,6 @@ describe('Create a timeline from a template', () => { loginAndWaitForPageWithoutDateRange(TIMELINE_TEMPLATES_URL); waitForTimelinesPanelToBeLoaded(); }); - it('Should have the same query and open the timeline modal', () => { selectCustomTemplates(); cy.wait('@timeline', { timeout: 100000 }); @@ -132,5 +131,6 @@ describe('Create a timeline from a template', () => { cy.get(TIMELINE_FLYOUT_WRAPPER).should('have.css', 'visibility', 'visible'); cy.get(TIMELINE_DESCRIPTION).should('have.text', getTimeline().description); cy.get(TIMELINE_QUERY).should('have.text', getTimeline().query); + closeTimeline(); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts b/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts index 73eb141f1ce3d9..28fe1294e6f01a 100644 --- a/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/urls/state.spec.ts @@ -182,11 +182,10 @@ describe('url state', () => { loginAndWaitForPageWithoutDateRange(ABSOLUTE_DATE_RANGE.url); kqlSearch('source.ip: "10.142.0.9" {enter}'); navigateFromHeaderTo(HOSTS); - cy.get(NETWORK).should( 'have.attr', 'href', - `/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))` + `/app/security/network?query=(language:kuery,query:'source.ip:%20%2210.142.0.9%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2019-08-01T20:33:29.186Z')))` ); }); @@ -199,12 +198,12 @@ describe('url state', () => { cy.get(HOSTS).should( 'have.attr', 'href', - `/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` + `/app/security/hosts?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); cy.get(NETWORK).should( 'have.attr', 'href', - `/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` + `/app/security/network?query=(language:kuery,query:'host.name:%20%22siem-kibana%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); cy.get(HOSTS_NAMES).first().should('have.text', 'siem-kibana'); @@ -215,21 +214,21 @@ describe('url state', () => { cy.get(ANOMALIES_TAB).should( 'have.attr', 'href', - "/app/security/hosts/siem-kibana/anomalies?sourcerer=(default:!('auditbeat-*'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')" + "/app/security/hosts/siem-kibana/anomalies?sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))&query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')" ); cy.get(BREADCRUMBS) .eq(1) .should( 'have.attr', 'href', - `/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` + `/app/security/hosts?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); cy.get(BREADCRUMBS) .eq(2) .should( 'have.attr', 'href', - `/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:!(\'auditbeat-*\'))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` + `/app/security/hosts/siem-kibana?query=(language:kuery,query:'agent.type:%20%22auditbeat%22%20')&sourcerer=(default:(id:security-solution-default,selectedPatterns:!('auditbeat-*')))&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')),timeline:(linkTo:!(global),timerange:(from:'2019-08-01T20:03:29.186Z',kind:absolute,to:'2020-01-01T21:33:29.186Z')))` ); }); diff --git a/x-pack/plugins/security_solution/cypress/objects/timeline.ts b/x-pack/plugins/security_solution/cypress/objects/timeline.ts index f3d9bc1b9ef1aa..70b8c1b400d516 100644 --- a/x-pack/plugins/security_solution/cypress/objects/timeline.ts +++ b/x-pack/plugins/security_solution/cypress/objects/timeline.ts @@ -87,6 +87,7 @@ export const expectedExportedTimelineTemplate = ( }, }, }, + dataViewId: timelineTemplateBody.dataViewId, dateRange: { start: timelineTemplateBody.dateRange?.start, end: timelineTemplateBody.dateRange?.end, @@ -127,6 +128,7 @@ export const expectedExportedTimeline = (timelineResponse: Cypress.Response