diff --git a/.ci/end2end.groovy b/.ci/end2end.groovy index 2cdc6d1c297cd..0d9f5c9d92453 100644 --- a/.ci/end2end.groovy +++ b/.ci/end2end.groovy @@ -37,22 +37,31 @@ pipeline { deleteDir() gitCheckout(basedir: "${BASE_DIR}", githubNotifyFirstTimeContributor: false, shallow: false, reference: "/var/lib/jenkins/.git-references/kibana.git") + + // Filter when to run based on the below reasons: + // - On a PRs when: + // - There are changes related to the APM UI project + // - only when the owners of those changes are members of the apm-ui team (new filter) + // - On merges to branches when: + // - There are changes related to the APM UI project + // - FORCE parameter is set to true. script { + def apm_updated = false dir("${BASE_DIR}"){ - def regexps =[ "^x-pack/plugins/apm/.*" ] - env.APM_UPDATED = isGitRegionMatch(patterns: regexps) + apm_updated = isGitRegionMatch(patterns: [ "^x-pack/plugins/apm/.*" ]) + } + if (isPR()) { + def isMember = isMemberOf(user: env.CHANGE_AUTHOR, team: 'apm-ui') + setEnvVar('RUN_APM_E2E', params.FORCE || (apm_updated && isMember)) + } else { + setEnvVar('RUN_APM_E2E', params.FORCE || apm_updated) } } } } stage('Prepare Kibana') { options { skipDefaultCheckout() } - when { - anyOf { - expression { return params.FORCE } - expression { return env.APM_UPDATED != "false" } - } - } + when { expression { return env.RUN_APM_E2E != "false" } } environment { JENKINS_NODE_COOKIE = 'dontKillMe' } @@ -70,12 +79,7 @@ pipeline { } stage('Smoke Tests'){ options { skipDefaultCheckout() } - when { - anyOf { - expression { return params.FORCE } - expression { return env.APM_UPDATED != "false" } - } - } + when { expression { return env.RUN_APM_E2E != "false" } } steps{ notifyTestStatus('Running smoke tests', 'PENDING') dir("${BASE_DIR}"){ diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index bcb4774475849..5efbaba32e00a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -169,6 +169,7 @@ /x-pack/plugins/encrypted_saved_objects/ @elastic/kibana-security /x-pack/plugins/security/ @elastic/kibana-security /x-pack/test/api_integration/apis/security/ @elastic/kibana-security +/x-pack/test/ui_capabilities/ @elastic/kibana-security /x-pack/test/encrypted_saved_objects_api_integration/ @elastic/kibana-security /x-pack/test/functional/apps/security/ @elastic/kibana-security /x-pack/test/kerberos_api_integration/ @elastic/kibana-security diff --git a/.github/ISSUE_TEMPLATE/APM.md b/.github/ISSUE_TEMPLATE/APM.md index 983806f70bc3f..c3abbdd67269d 100644 --- a/.github/ISSUE_TEMPLATE/APM.md +++ b/.github/ISSUE_TEMPLATE/APM.md @@ -2,7 +2,7 @@ name: APM Issue about: Issues related to the APM solution in Kibana labels: Team:apm -title: [APM] +title: "[APM]" --- **Versions** diff --git a/docs/apm/troubleshooting.asciidoc b/docs/apm/troubleshooting.asciidoc index 65f7a378ec244..e00a67f6c78a4 100644 --- a/docs/apm/troubleshooting.asciidoc +++ b/docs/apm/troubleshooting.asciidoc @@ -49,7 +49,7 @@ GET /_template/apm-{version} *Using Logstash, Kafka, etc.* If you're not outputting data directly from APM Server to Elasticsearch (perhaps you're using Logstash or Kafka), then the index template will not be set up automatically. Instead, you'll need to -{apm-server-ref}/_manually_loading_template_configuration.html[load the template manually]. +{apm-server-ref}/configuration-template.html[load the template manually]. *Using a custom index names* This problem can also occur if you've customized the index name that you write APM data to. diff --git a/docs/developer/architecture/security/feature-registration.asciidoc b/docs/developer/architecture/security/feature-registration.asciidoc index 3724624dbb917..3ff83e9db8c43 100644 --- a/docs/developer/architecture/security/feature-registration.asciidoc +++ b/docs/developer/architecture/security/feature-registration.asciidoc @@ -9,13 +9,12 @@ Registering features also gives your plugin access to “UI Capabilities”. The === Registering a feature -Feature registration is controlled via the built-in `xpack_main` plugin. To register a feature, call `xpack_main`'s `registerFeature` function from your plugin's `init` function, and provide the appropriate details: +Feature registration is controlled via the built-in `features` plugin. To register a feature, call `features`'s `registerKibanaFeature` function from your plugin's `setup` lifecycle function, and provide the appropriate details: ["source","javascript"] ----------- -init(server) { - const xpackMainPlugin = server.plugins.xpack_main; - xpackMainPlugin.registerFeature({ +setup(core, { features }) { + features.registerKibanaFeature({ // feature details here. }); } @@ -45,12 +44,12 @@ Registering a feature consists of the following fields. For more information, co |An array of applications this feature enables. Typically, all of your plugin's apps (from `uiExports`) will be included here. |`privileges` (required) -|{kib-repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`FeatureConfig`]. +|{kib-repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`KibanaFeatureConfig`]. |See <> and <> |The set of privileges this feature requires to function. |`subFeatures` (optional) -|{kib-repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`FeatureConfig`]. +|{kib-repo}blob/{branch}/x-pack/plugins/features/common/feature.ts[`KibanaFeatureConfig`]. |See <> |The set of subfeatures that enables finer access control than the `all` and `read` feature privileges. These options are only available in the Gold subscription level and higher. @@ -73,15 +72,17 @@ For a full explanation of fields and options, consult the {kib-repo}blob/{branch === Using UI Capabilities UI Capabilities are available to your public (client) plugin code. These capabilities are read-only, and are used to inform the UI. This object is namespaced by feature id. For example, if your feature id is “foo”, then your UI Capabilities are stored at `uiCapabilities.foo`. -To access capabilities, import them from `ui/capabilities`: +Capabilities can be accessed from your plugin's `start` lifecycle from the `core.application` service: ["source","javascript"] ----------- -import { uiCapabilities } from 'ui/capabilities'; +public start(core) { + const { capabilities } = core.application; -const canUserSave = uiCapabilities.foo.save; -if (canUserSave) { - // show save button + const canUserSave = capabilities.foo.save; + if (canUserSave) { + // show save button + } } ----------- @@ -89,9 +90,8 @@ if (canUserSave) { === Example 1: Canvas Application ["source","javascript"] ----------- -init(server) { - const xpackMainPlugin = server.plugins.xpack_main; - xpackMainPlugin.registerFeature({ +public setup(core, { features }) { + features.registerKibanaFeature({ id: 'canvas', name: 'Canvas', icon: 'canvasApp', @@ -130,11 +130,13 @@ The `all` privilege defines a single “save” UI Capability. To access this in ["source","javascript"] ----------- -import { uiCapabilities } from 'ui/capabilities'; +public start(core) { + const { capabilities } = core.application; -const canUserSave = uiCapabilities.canvas.save; -if (canUserSave) { - // show save button + const canUserSave = capabilities.canvas.save; + if (canUserSave) { + // show save button + } } ----------- @@ -145,9 +147,8 @@ Because the `read` privilege does not define the `save` capability, users with r ["source","javascript"] ----------- -init(server) { - const xpackMainPlugin = server.plugins.xpack_main; - xpackMainPlugin.registerFeature({ +public setup(core, { features }) { + features.registerKibanaFeature({ id: 'dev_tools', name: i18n.translate('xpack.features.devToolsFeatureName', { defaultMessage: 'Dev Tools', @@ -206,9 +207,8 @@ a single "Create Short URLs" subfeature privilege is defined, which allows users ["source","javascript"] ----------- -init(server) { - const xpackMainPlugin = server.plugins.xpack_main; - xpackMainPlugin.registerFeature({ +public setup(core, { features }) { + features.registerKibanaFeature({ { id: 'discover', name: i18n.translate('xpack.features.discoverFeatureName', { diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index b3180a7a03874..275fdf8fb69ad 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -95,7 +95,7 @@ in Kibana, e.g. visualizations. It has the form of a flyout panel. |{kib-repo}blob/{branch}/src/plugins/kibana_legacy/README.md[kibanaLegacy] -|This plugin will contain several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. +|This plugin contains several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. |{kib-repo}blob/{branch}/src/plugins/kibana_react/README.md[kibanaReact] @@ -172,6 +172,10 @@ which also contains the timelion APIs and backend, look at the vis_type_timelion |An API for: +|{kib-repo}blob/{branch}/src/plugins/url_forwarding/README.md[urlForwarding] +|This plugins contains helpers to redirect legacy URLs. It can be used to forward old URLs to their new counterparts. + + |{kib-repo}blob/{branch}/src/plugins/usage_collection/README.md[usageCollection] |Usage Collection allows collecting usage data for other services to consume (telemetry and monitoring). To integrate with the telemetry services for usage collection of your feature, there are 2 steps: @@ -412,10 +416,6 @@ using the CURL scripts in the scripts folder. |This plugin provides shared components and services for use across observability solutions, as well as the observability landing page UI. -|{kib-repo}blob/{branch}/x-pack/plugins/oss_telemetry[ossTelemetry] -|WARNING: Missing README. - - |{kib-repo}blob/{branch}/x-pack/plugins/painless_lab[painlessLab] |WARNING: Missing README. 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 85e1da08b00af..f7b55b0650d8b 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 @@ -10,6 +10,9 @@ readonly links: { readonly dashboard: { readonly drilldowns: string; + readonly drilldownsTriggerPicker: string; + readonly urlDrilldownTemplateSyntax: string; + readonly urlDrilldownVariables: string; }; readonly filebeat: { readonly base: 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 4644dc432bc9a..3f58cf08ee6b6 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 dashboard: {
readonly drilldowns: string;
};
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: 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 scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly visualize: Record<string, string>;
} | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | {
readonly dashboard: {
readonly drilldowns: string;
readonly drilldownsTriggerPicker: string;
readonly urlDrilldownTemplateSyntax: string;
readonly urlDrilldownVariables: string;
};
readonly filebeat: {
readonly base: string;
readonly installation: string;
readonly configuration: string;
readonly elasticsearchOutput: string;
readonly startup: string;
readonly exportedFields: string;
};
readonly auditbeat: {
readonly base: string;
};
readonly metricbeat: {
readonly base: string;
};
readonly heartbeat: {
readonly base: string;
};
readonly logstash: {
readonly base: string;
};
readonly functionbeat: {
readonly base: string;
};
readonly winlogbeat: {
readonly base: string;
};
readonly aggs: {
readonly date_histogram: string;
readonly date_range: 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 scriptedFields: {
readonly scriptFields: string;
readonly scriptAggs: string;
readonly painless: string;
readonly painlessApi: string;
readonly painlessSyntax: string;
readonly luceneExpressions: string;
};
readonly indexPatterns: {
readonly loadingData: string;
readonly introduction: string;
};
readonly addData: string;
readonly kibana: string;
readonly siem: {
readonly guide: string;
readonly gettingStarted: string;
};
readonly query: {
readonly luceneQuerySyntax: string;
readonly queryDsl: string;
readonly kueryQuerySyntax: string;
};
readonly date: {
readonly dateMath: string;
};
readonly management: Record<string, string>;
readonly visualize: Record<string, string>;
} | | diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index 89330d2a86f76..c16600d1d0492 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -28,6 +28,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [SavedObjectsErrorHelpers](./kibana-plugin-core-server.savedobjectserrorhelpers.md) | | | [SavedObjectsRepository](./kibana-plugin-core-server.savedobjectsrepository.md) | | | [SavedObjectsSerializer](./kibana-plugin-core-server.savedobjectsserializer.md) | A serializer that can be used to manually convert [raw](./kibana-plugin-core-server.savedobjectsrawdoc.md) or [sanitized](./kibana-plugin-core-server.savedobjectsanitizeddoc.md) documents to the other kind. | +| [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) | | | [SavedObjectTypeRegistry](./kibana-plugin-core-server.savedobjecttyperegistry.md) | Registry holding information about all the registered [saved object types](./kibana-plugin-core-server.savedobjectstype.md). | ## Enumerations @@ -123,7 +124,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [LoggerFactory](./kibana-plugin-core-server.loggerfactory.md) | The single purpose of LoggerFactory interface is to define a way to retrieve a context-based logger instance. | | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | Provides APIs to plugins for customizing the plugin's logger. | | [LogMeta](./kibana-plugin-core-server.logmeta.md) | Contextual metadata | -| [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | +| [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | APIs to retrieves metrics gathered and exposed by the core platform. | | [NodesVersionCompatibility](./kibana-plugin-core-server.nodesversioncompatibility.md) | | | [OnPostAuthToolkit](./kibana-plugin-core-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. | | [OnPreAuthToolkit](./kibana-plugin-core-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.collectioninterval.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.collectioninterval.md new file mode 100644 index 0000000000000..6f05526b66c83 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.collectioninterval.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) > [collectionInterval](./kibana-plugin-core-server.metricsservicesetup.collectioninterval.md) + +## MetricsServiceSetup.collectionInterval property + +Interval metrics are collected in milliseconds + +Signature: + +```typescript +readonly collectionInterval: number; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md new file mode 100644 index 0000000000000..61107fbf20ad9 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) > [getOpsMetrics$](./kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md) + +## MetricsServiceSetup.getOpsMetrics$ property + +Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) gathered. The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time, based on the `opts.interval` configuration property. + +Signature: + +```typescript +getOpsMetrics$: () => Observable; +``` + +## Example + + +```ts +core.metrics.getOpsMetrics$().subscribe(metrics => { + // do something with the metrics +}) + +``` + diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md index 0bec919797b6f..5fcb1417dea0e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.metricsservicesetup.md @@ -4,8 +4,18 @@ ## MetricsServiceSetup interface +APIs to retrieves metrics gathered and exposed by the core platform. + Signature: ```typescript export interface MetricsServiceSetup ``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [collectionInterval](./kibana-plugin-core-server.metricsservicesetup.collectioninterval.md) | number | Interval metrics are collected in milliseconds | +| [getOpsMetrics$](./kibana-plugin-core-server.metricsservicesetup.getopsmetrics_.md) | () => Observable<OpsMetrics> | Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) gathered. The observable will emit an initial value during core's start phase, and a new value every fixed interval of time, based on the opts.interval configuration property. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.opsmetrics.collected_at.md b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.collected_at.md new file mode 100644 index 0000000000000..25125569b7b38 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.collected_at.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [OpsMetrics](./kibana-plugin-core-server.opsmetrics.md) > [collected\_at](./kibana-plugin-core-server.opsmetrics.collected_at.md) + +## OpsMetrics.collected\_at property + +Time metrics were recorded at. + +Signature: + +```typescript +collected_at: Date; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md index d2d4782385c06..9803c0fbd53cc 100644 --- a/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md +++ b/docs/development/core/server/kibana-plugin-core-server.opsmetrics.md @@ -16,6 +16,7 @@ export interface OpsMetrics | Property | Type | Description | | --- | --- | --- | +| [collected\_at](./kibana-plugin-core-server.opsmetrics.collected_at.md) | Date | Time metrics were recorded at. | | [concurrent\_connections](./kibana-plugin-core-server.opsmetrics.concurrent_connections.md) | OpsServerMetrics['concurrent_connections'] | number of current concurrent connections to the server | | [os](./kibana-plugin-core-server.opsmetrics.os.md) | OpsOsMetrics | OS related metrics | | [process](./kibana-plugin-core-server.opsmetrics.process.md) | OpsProcessMetrics | Process related metrics | diff --git a/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.cpu.md b/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.cpu.md new file mode 100644 index 0000000000000..095c45266a251 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.cpu.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [OpsOsMetrics](./kibana-plugin-core-server.opsosmetrics.md) > [cpu](./kibana-plugin-core-server.opsosmetrics.cpu.md) + +## OpsOsMetrics.cpu property + +cpu cgroup metrics, undefined when not running in a cgroup + +Signature: + +```typescript +cpu?: { + control_group: string; + cfs_period_micros: number; + cfs_quota_micros: number; + stat: { + number_of_elapsed_periods: number; + number_of_times_throttled: number; + time_throttled_nanos: number; + }; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.cpuacct.md b/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.cpuacct.md new file mode 100644 index 0000000000000..140646a0d1a35 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.cpuacct.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [OpsOsMetrics](./kibana-plugin-core-server.opsosmetrics.md) > [cpuacct](./kibana-plugin-core-server.opsosmetrics.cpuacct.md) + +## OpsOsMetrics.cpuacct property + +cpu accounting metrics, undefined when not running in a cgroup + +Signature: + +```typescript +cpuacct?: { + control_group: string; + usage_nanos: number; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.md b/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.md index 5fedb76a9c8d7..8938608531139 100644 --- a/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.md +++ b/docs/development/core/server/kibana-plugin-core-server.opsosmetrics.md @@ -16,6 +16,8 @@ export interface OpsOsMetrics | Property | Type | Description | | --- | --- | --- | +| [cpu](./kibana-plugin-core-server.opsosmetrics.cpu.md) | {
control_group: string;
cfs_period_micros: number;
cfs_quota_micros: number;
stat: {
number_of_elapsed_periods: number;
number_of_times_throttled: number;
time_throttled_nanos: number;
};
} | cpu cgroup metrics, undefined when not running in a cgroup | +| [cpuacct](./kibana-plugin-core-server.opsosmetrics.cpuacct.md) | {
control_group: string;
usage_nanos: number;
} | cpu accounting metrics, undefined when not running in a cgroup | | [distro](./kibana-plugin-core-server.opsosmetrics.distro.md) | string | The os distrib. Only present for linux platforms | | [distroRelease](./kibana-plugin-core-server.opsosmetrics.distrorelease.md) | string | The os distrib release, prefixed by the os distrib. Only present for linux platforms | | [load](./kibana-plugin-core-server.opsosmetrics.load.md) | {
'1m': number;
'5m': number;
'15m': number;
} | cpu load metrics | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md index e079e0fa51aac..d71eda6009284 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.md @@ -17,5 +17,6 @@ export interface SavedObjectsBulkUpdateObject extends PickPartial<T> | The data for a Saved Object is stored as an object in the attributes property. | | [id](./kibana-plugin-core-server.savedobjectsbulkupdateobject.id.md) | string | The ID of this Saved Object, guaranteed to be unique for all objects of the same type | +| [namespace](./kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md) | string | Optional namespace string to use when searching for this object. If this is defined, it will supersede the namespace ID that is in [SavedObjectsBulkUpdateOptions](./kibana-plugin-core-server.savedobjectsbulkupdateoptions.md).Note: the default namespace's string representation is 'default', and its ID representation is undefined. | | [type](./kibana-plugin-core-server.savedobjectsbulkupdateobject.type.md) | string | The type of this Saved Object. Each plugin can define it's own custom Saved Object types. | diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md new file mode 100644 index 0000000000000..544efcd3be909 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsBulkUpdateObject](./kibana-plugin-core-server.savedobjectsbulkupdateobject.md) > [namespace](./kibana-plugin-core-server.savedobjectsbulkupdateobject.namespace.md) + +## SavedObjectsBulkUpdateObject.namespace property + +Optional namespace string to use when searching for this object. If this is defined, it will supersede the namespace ID that is in [SavedObjectsBulkUpdateOptions](./kibana-plugin-core-server.savedobjectsbulkupdateoptions.md). + +Note: the default namespace's string representation is `'default'`, and its ID representation is `undefined`. + +Signature: + +```typescript +namespace?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md index 6ef7b991bb159..650459bfdb435 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.md @@ -16,8 +16,6 @@ export interface SavedObjectsServiceSetup When plugins access the Saved Objects client, a new client is created using the factory provided to `setClientFactory` and wrapped by all wrappers registered through `addClientWrapper`. -All the setup APIs will throw if called after the service has started, and therefor cannot be used from legacy plugin code. Legacy plugins should use the legacy savedObject service until migrated. - ## Example 1 diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md index 57c9e04966c1b..54e01d3110a2d 100644 --- a/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsservicesetup.registertype.md @@ -14,10 +14,6 @@ See the [mappings format](./kibana-plugin-core-server.savedobjectstypemappingdef registerType: (type: SavedObjectsType) => void; ``` -## Remarks - -The type definition is an aggregation of the legacy savedObjects `schema`, `mappings` and `migration` concepts. This API is the single entry point to register saved object types in the new platform. - ## Example diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md new file mode 100644 index 0000000000000..e365dfbcb5142 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) + +## SavedObjectsUtils class + + +Signature: + +```typescript +export declare class SavedObjectsUtils +``` + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [namespaceIdToString](./kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md) | static | (namespace?: string | undefined) => string | Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the undefined namespace ID (which has a namespace string of 'default'). | +| [namespaceStringToId](./kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md) | static | (namespace: string) => string | undefined | Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the 'default' namespace string (which has a namespace ID of undefined). | + diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md new file mode 100644 index 0000000000000..591505892e64f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) > [namespaceIdToString](./kibana-plugin-core-server.savedobjectsutils.namespaceidtostring.md) + +## SavedObjectsUtils.namespaceIdToString property + +Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with the exception of the `undefined` namespace ID (which has a namespace string of `'default'`). + +Signature: + +```typescript +static namespaceIdToString: (namespace?: string | undefined) => string; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md new file mode 100644 index 0000000000000..e052fe493b5ea --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [SavedObjectsUtils](./kibana-plugin-core-server.savedobjectsutils.md) > [namespaceStringToId](./kibana-plugin-core-server.savedobjectsutils.namespacestringtoid.md) + +## SavedObjectsUtils.namespaceStringToId property + +Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with the exception of the `'default'` namespace string (which has a namespace ID of `undefined`). + +Signature: + +```typescript +static namespaceStringToId: (namespace: string) => string | undefined; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md new file mode 100644 index 0000000000000..7475f0e3a4c1c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.dependencies_.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md) + +## StatusServiceSetup.dependencies$ property + +Current status for all plugins this plugin depends on. Each key of the `Record` is a plugin id. + +Signature: + +```typescript +dependencies$: Observable>; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md new file mode 100644 index 0000000000000..6c65e44270a06 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.derivedstatus_.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) + +## StatusServiceSetup.derivedStatus$ property + +The status of this plugin as derived from its dependencies. + +Signature: + +```typescript +derivedStatus$: Observable; +``` + +## Remarks + +By default, plugins inherit this derived status from their dependencies. Calling overrides this default status. + +This may emit multliple times for a single status change event as propagates through the dependency tree + diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md index 3d3b73ccda25f..ba0645be4d26c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.md @@ -12,10 +12,73 @@ API for accessing status of Core and this plugin's dependencies as well as for c export interface StatusServiceSetup ``` +## Remarks + +By default, a plugin inherits it's current status from the most severe status level of any Core services and any plugins that it depends on. This default status is available on the API. + +Plugins may customize their status calculation by calling the API with an Observable. Within this Observable, a plugin may choose to only depend on the status of some of its dependencies, to ignore severe status levels of particular Core services they are not concerned with, or to make its status dependent on other external services. + +## Example 1 + +Customize a plugin's status to only depend on the status of SavedObjects: + +```ts +core.status.set( + core.status.core$.pipe( +. map((coreStatus) => { + return coreStatus.savedObjects; + }) ; + ); +); + +``` + +## Example 2 + +Customize a plugin's status to include an external service: + +```ts +const externalStatus$ = interval(1000).pipe( + switchMap(async () => { + const resp = await fetch(`https://myexternaldep.com/_healthz`); + const body = await resp.json(); + if (body.ok) { + return of({ level: ServiceStatusLevels.available, summary: 'External Service is up'}); + } else { + return of({ level: ServiceStatusLevels.available, summary: 'External Service is unavailable'}); + } + }), + catchError((error) => { + of({ level: ServiceStatusLevels.unavailable, summary: `External Service is down`, meta: { error }}) + }) +); + +core.status.set( + combineLatest([core.status.derivedStatus$, externalStatus$]).pipe( + map(([derivedStatus, externalStatus]) => { + if (externalStatus.level > derivedStatus) { + return externalStatus; + } else { + return derivedStatus; + } + }) + ) +); + +``` + ## Properties | Property | Type | Description | | --- | --- | --- | | [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) | Observable<CoreStatus> | Current status for all Core services. | +| [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md) | Observable<Record<string, ServiceStatus>> | Current status for all plugins this plugin depends on. Each key of the Record is a plugin id. | +| [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) | Observable<ServiceStatus> | The status of this plugin as derived from its dependencies. | | [overall$](./kibana-plugin-core-server.statusservicesetup.overall_.md) | Observable<ServiceStatus> | Overall system status for all of Kibana. | +## Methods + +| Method | Description | +| --- | --- | +| [set(status$)](./kibana-plugin-core-server.statusservicesetup.set.md) | Allows a plugin to specify a custom status dependent on its own criteria. Completely overrides the default inherited status. | + diff --git a/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md new file mode 100644 index 0000000000000..143cd397c40ae --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.statusservicesetup.set.md @@ -0,0 +1,28 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [set](./kibana-plugin-core-server.statusservicesetup.set.md) + +## StatusServiceSetup.set() method + +Allows a plugin to specify a custom status dependent on its own criteria. Completely overrides the default inherited status. + +Signature: + +```typescript +set(status$: Observable): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| status$ | Observable<ServiceStatus> | | + +Returns: + +`void` + +## Remarks + +See the [StatusServiceSetup.derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) API for leveraging the default status calculation that is provided by Core. + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig._constructor_.md new file mode 100644 index 0000000000000..9287a08ff196b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig._constructor_.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [(constructor)](./kibana-plugin-plugins-data-public.aggconfig._constructor_.md) + +## AggConfig.(constructor) + +Constructs a new instance of the `AggConfig` class + +Signature: + +```typescript +constructor(aggConfigs: IAggConfigs, opts: AggConfigOptions); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| aggConfigs | IAggConfigs | | +| opts | AggConfigOptions | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.aggconfigs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.aggconfigs.md new file mode 100644 index 0000000000000..f552bbd2d1cfc --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.aggconfigs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [aggConfigs](./kibana-plugin-plugins-data-public.aggconfig.aggconfigs.md) + +## AggConfig.aggConfigs property + +Signature: + +```typescript +aggConfigs: IAggConfigs; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.brandnew.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.brandnew.md new file mode 100644 index 0000000000000..eb1f3af4c5b01 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.brandnew.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [brandNew](./kibana-plugin-plugins-data-public.aggconfig.brandnew.md) + +## AggConfig.brandNew property + +Signature: + +```typescript +brandNew?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.createfilter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.createfilter.md new file mode 100644 index 0000000000000..7ec0350f65321 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.createfilter.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [createFilter](./kibana-plugin-plugins-data-public.aggconfig.createfilter.md) + +## AggConfig.createFilter() method + +Signature: + +```typescript +createFilter(key: string, params?: {}): any; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| key | string | | +| params | {} | | + +Returns: + +`any` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.enabled.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.enabled.md new file mode 100644 index 0000000000000..82595ee5f5b63 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.enabled.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [enabled](./kibana-plugin-plugins-data-public.aggconfig.enabled.md) + +## AggConfig.enabled property + +Signature: + +```typescript +enabled: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.ensureids.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.ensureids.md new file mode 100644 index 0000000000000..04e0b82187a5f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.ensureids.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [ensureIds](./kibana-plugin-plugins-data-public.aggconfig.ensureids.md) + +## AggConfig.ensureIds() method + +Ensure that all of the objects in the list have ids, the objects and list are modified by reference. + +Signature: + +```typescript +static ensureIds(list: any[]): any[]; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| list | any[] | | + +Returns: + +`any[]` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.fieldistimefield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.fieldistimefield.md new file mode 100644 index 0000000000000..a1fde4dec25b1 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.fieldistimefield.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [fieldIsTimeField](./kibana-plugin-plugins-data-public.aggconfig.fieldistimefield.md) + +## AggConfig.fieldIsTimeField() method + +Signature: + +```typescript +fieldIsTimeField(): boolean | "" | undefined; +``` +Returns: + +`boolean | "" | undefined` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.fieldname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.fieldname.md new file mode 100644 index 0000000000000..2d3acb7f026ff --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.fieldname.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [fieldName](./kibana-plugin-plugins-data-public.aggconfig.fieldname.md) + +## AggConfig.fieldName() method + +Signature: + +```typescript +fieldName(): any; +``` +Returns: + +`any` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getaggparams.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getaggparams.md new file mode 100644 index 0000000000000..f898844ff0273 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getaggparams.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [getAggParams](./kibana-plugin-plugins-data-public.aggconfig.getaggparams.md) + +## AggConfig.getAggParams() method + +Signature: + +```typescript +getAggParams(): import("./param_types/agg").AggParamType[]; +``` +Returns: + +`import("./param_types/agg").AggParamType[]` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getfield.md new file mode 100644 index 0000000000000..1fb6f88c43171 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getfield.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [getField](./kibana-plugin-plugins-data-public.aggconfig.getfield.md) + +## AggConfig.getField() method + +Signature: + +```typescript +getField(): any; +``` +Returns: + +`any` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getfielddisplayname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getfielddisplayname.md new file mode 100644 index 0000000000000..710499cee62dd --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getfielddisplayname.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [getFieldDisplayName](./kibana-plugin-plugins-data-public.aggconfig.getfielddisplayname.md) + +## AggConfig.getFieldDisplayName() method + +Signature: + +```typescript +getFieldDisplayName(): any; +``` +Returns: + +`any` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getindexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getindexpattern.md new file mode 100644 index 0000000000000..ed0e9d0fbb5de --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getindexpattern.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [getIndexPattern](./kibana-plugin-plugins-data-public.aggconfig.getindexpattern.md) + +## AggConfig.getIndexPattern() method + +Signature: + +```typescript +getIndexPattern(): import("../../../public").IndexPattern; +``` +Returns: + +`import("../../../public").IndexPattern` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getkey.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getkey.md new file mode 100644 index 0000000000000..a2a59fcf9ae31 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getkey.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [getKey](./kibana-plugin-plugins-data-public.aggconfig.getkey.md) + +## AggConfig.getKey() method + +Signature: + +```typescript +getKey(bucket: any, key?: string): any; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| bucket | any | | +| key | string | | + +Returns: + +`any` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getparam.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getparam.md new file mode 100644 index 0000000000000..ad4cd2fa175f8 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getparam.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [getParam](./kibana-plugin-plugins-data-public.aggconfig.getparam.md) + +## AggConfig.getParam() method + +Signature: + +```typescript +getParam(key: string): any; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| key | string | | + +Returns: + +`any` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getrequestaggs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getrequestaggs.md new file mode 100644 index 0000000000000..773c2f5a7c0e9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getrequestaggs.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [getRequestAggs](./kibana-plugin-plugins-data-public.aggconfig.getrequestaggs.md) + +## AggConfig.getRequestAggs() method + +Signature: + +```typescript +getRequestAggs(): AggConfig[]; +``` +Returns: + +`AggConfig[]` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getresponseaggs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getresponseaggs.md new file mode 100644 index 0000000000000..cf515e68dcc57 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getresponseaggs.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [getResponseAggs](./kibana-plugin-plugins-data-public.aggconfig.getresponseaggs.md) + +## AggConfig.getResponseAggs() method + +Signature: + +```typescript +getResponseAggs(): AggConfig[]; +``` +Returns: + +`AggConfig[]` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.gettimerange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.gettimerange.md new file mode 100644 index 0000000000000..897a6d8dda3f1 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.gettimerange.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [getTimeRange](./kibana-plugin-plugins-data-public.aggconfig.gettimerange.md) + +## AggConfig.getTimeRange() method + +Signature: + +```typescript +getTimeRange(): import("../../../public").TimeRange | undefined; +``` +Returns: + +`import("../../../public").TimeRange | undefined` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getvalue.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getvalue.md new file mode 100644 index 0000000000000..4fab1af3f6464 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.getvalue.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [getValue](./kibana-plugin-plugins-data-public.aggconfig.getvalue.md) + +## AggConfig.getValue() method + +Signature: + +```typescript +getValue(bucket: any): any; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| bucket | any | | + +Returns: + +`any` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.id.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.id.md new file mode 100644 index 0000000000000..1fa7a5c57e2a8 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.id.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [id](./kibana-plugin-plugins-data-public.aggconfig.id.md) + +## AggConfig.id property + +Signature: + +```typescript +id: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.isfilterable.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.isfilterable.md new file mode 100644 index 0000000000000..a795ab1e91c2c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.isfilterable.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [isFilterable](./kibana-plugin-plugins-data-public.aggconfig.isfilterable.md) + +## AggConfig.isFilterable() method + +Signature: + +```typescript +isFilterable(): boolean; +``` +Returns: + +`boolean` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.makelabel.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.makelabel.md new file mode 100644 index 0000000000000..65923ed0ae889 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.makelabel.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [makeLabel](./kibana-plugin-plugins-data-public.aggconfig.makelabel.md) + +## AggConfig.makeLabel() method + +Signature: + +```typescript +makeLabel(percentageMode?: boolean): any; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| percentageMode | boolean | | + +Returns: + +`any` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.md new file mode 100644 index 0000000000000..ceb90cffbf6ca --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.md @@ -0,0 +1,62 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) + +## AggConfig class + +Signature: + +```typescript +export declare class AggConfig +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(aggConfigs, opts)](./kibana-plugin-plugins-data-public.aggconfig._constructor_.md) | | Constructs a new instance of the AggConfig class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [aggConfigs](./kibana-plugin-plugins-data-public.aggconfig.aggconfigs.md) | | IAggConfigs | | +| [brandNew](./kibana-plugin-plugins-data-public.aggconfig.brandnew.md) | | boolean | | +| [enabled](./kibana-plugin-plugins-data-public.aggconfig.enabled.md) | | boolean | | +| [id](./kibana-plugin-plugins-data-public.aggconfig.id.md) | | string | | +| [params](./kibana-plugin-plugins-data-public.aggconfig.params.md) | | any | | +| [parent](./kibana-plugin-plugins-data-public.aggconfig.parent.md) | | IAggConfigs | | +| [schema](./kibana-plugin-plugins-data-public.aggconfig.schema.md) | | string | | +| [type](./kibana-plugin-plugins-data-public.aggconfig.type.md) | | IAggType | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [createFilter(key, params)](./kibana-plugin-plugins-data-public.aggconfig.createfilter.md) | | | +| [ensureIds(list)](./kibana-plugin-plugins-data-public.aggconfig.ensureids.md) | static | Ensure that all of the objects in the list have ids, the objects and list are modified by reference. | +| [fieldIsTimeField()](./kibana-plugin-plugins-data-public.aggconfig.fieldistimefield.md) | | | +| [fieldName()](./kibana-plugin-plugins-data-public.aggconfig.fieldname.md) | | | +| [getAggParams()](./kibana-plugin-plugins-data-public.aggconfig.getaggparams.md) | | | +| [getField()](./kibana-plugin-plugins-data-public.aggconfig.getfield.md) | | | +| [getFieldDisplayName()](./kibana-plugin-plugins-data-public.aggconfig.getfielddisplayname.md) | | | +| [getIndexPattern()](./kibana-plugin-plugins-data-public.aggconfig.getindexpattern.md) | | | +| [getKey(bucket, key)](./kibana-plugin-plugins-data-public.aggconfig.getkey.md) | | | +| [getParam(key)](./kibana-plugin-plugins-data-public.aggconfig.getparam.md) | | | +| [getRequestAggs()](./kibana-plugin-plugins-data-public.aggconfig.getrequestaggs.md) | | | +| [getResponseAggs()](./kibana-plugin-plugins-data-public.aggconfig.getresponseaggs.md) | | | +| [getTimeRange()](./kibana-plugin-plugins-data-public.aggconfig.gettimerange.md) | | | +| [getValue(bucket)](./kibana-plugin-plugins-data-public.aggconfig.getvalue.md) | | | +| [isFilterable()](./kibana-plugin-plugins-data-public.aggconfig.isfilterable.md) | | | +| [makeLabel(percentageMode)](./kibana-plugin-plugins-data-public.aggconfig.makelabel.md) | | | +| [nextId(list)](./kibana-plugin-plugins-data-public.aggconfig.nextid.md) | static | Calculate the next id based on the ids in this list {array} list - a list of objects with id properties | +| [onSearchRequestStart(searchSource, options)](./kibana-plugin-plugins-data-public.aggconfig.onsearchrequeststart.md) | | Hook for pre-flight logic, see AggType\#onSearchRequestStart | +| [serialize()](./kibana-plugin-plugins-data-public.aggconfig.serialize.md) | | | +| [setParams(from)](./kibana-plugin-plugins-data-public.aggconfig.setparams.md) | | Write the current values to this.params, filling in the defaults as we go | +| [setType(type)](./kibana-plugin-plugins-data-public.aggconfig.settype.md) | | | +| [toDsl(aggConfigs)](./kibana-plugin-plugins-data-public.aggconfig.todsl.md) | | Convert this aggConfig to its dsl syntax.Adds params and adhoc subaggs to a pojo, then returns it | +| [toExpressionAst()](./kibana-plugin-plugins-data-public.aggconfig.toexpressionast.md) | | | +| [toJSON()](./kibana-plugin-plugins-data-public.aggconfig.tojson.md) | | | +| [toSerializedFieldFormat()](./kibana-plugin-plugins-data-public.aggconfig.toserializedfieldformat.md) | | Returns a serialized field format for the field used in this agg. This can be passed to fieldFormats.deserialize to get the field format instance. | +| [write(aggs)](./kibana-plugin-plugins-data-public.aggconfig.write.md) | | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.nextid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.nextid.md new file mode 100644 index 0000000000000..ab524a6d1c4f1 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.nextid.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [nextId](./kibana-plugin-plugins-data-public.aggconfig.nextid.md) + +## AggConfig.nextId() method + +Calculate the next id based on the ids in this list + + {array} list - a list of objects with id properties + +Signature: + +```typescript +static nextId(list: IAggConfig[]): number; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| list | IAggConfig[] | | + +Returns: + +`number` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.onsearchrequeststart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.onsearchrequeststart.md new file mode 100644 index 0000000000000..81df7866560e3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.onsearchrequeststart.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [onSearchRequestStart](./kibana-plugin-plugins-data-public.aggconfig.onsearchrequeststart.md) + +## AggConfig.onSearchRequestStart() method + +Hook for pre-flight logic, see AggType\#onSearchRequestStart + +Signature: + +```typescript +onSearchRequestStart(searchSource: ISearchSource, options?: ISearchOptions): Promise | Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| searchSource | ISearchSource | | +| options | ISearchOptions | | + +Returns: + +`Promise | Promise` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.params.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.params.md new file mode 100644 index 0000000000000..5bdb67f53b519 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.params.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [params](./kibana-plugin-plugins-data-public.aggconfig.params.md) + +## AggConfig.params property + +Signature: + +```typescript +params: any; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.parent.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.parent.md new file mode 100644 index 0000000000000..53d028457a9ae --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.parent.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [parent](./kibana-plugin-plugins-data-public.aggconfig.parent.md) + +## AggConfig.parent property + +Signature: + +```typescript +parent?: IAggConfigs; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.schema.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.schema.md new file mode 100644 index 0000000000000..afbf685951356 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.schema.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [schema](./kibana-plugin-plugins-data-public.aggconfig.schema.md) + +## AggConfig.schema property + +Signature: + +```typescript +schema?: string; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.serialize.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.serialize.md new file mode 100644 index 0000000000000..b0eebdbcc11ec --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.serialize.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [serialize](./kibana-plugin-plugins-data-public.aggconfig.serialize.md) + +## AggConfig.serialize() method + +Signature: + +```typescript +serialize(): AggConfigSerialized; +``` +Returns: + +`AggConfigSerialized` + +Returns a serialized representation of an AggConfig. + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.setparams.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.setparams.md new file mode 100644 index 0000000000000..cb495b7653f8a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.setparams.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [setParams](./kibana-plugin-plugins-data-public.aggconfig.setparams.md) + +## AggConfig.setParams() method + +Write the current values to this.params, filling in the defaults as we go + +Signature: + +```typescript +setParams(from: any): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| from | any | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.settype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.settype.md new file mode 100644 index 0000000000000..0b07186a6ca33 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.settype.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [setType](./kibana-plugin-plugins-data-public.aggconfig.settype.md) + +## AggConfig.setType() method + +Signature: + +```typescript +setType(type: IAggType): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | IAggType | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.todsl.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.todsl.md new file mode 100644 index 0000000000000..ac655c2a88a7b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.todsl.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [toDsl](./kibana-plugin-plugins-data-public.aggconfig.todsl.md) + +## AggConfig.toDsl() method + +Convert this aggConfig to its dsl syntax. + +Adds params and adhoc subaggs to a pojo, then returns it + +Signature: + +```typescript +toDsl(aggConfigs?: IAggConfigs): any; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| aggConfigs | IAggConfigs | | + +Returns: + +`any` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.toexpressionast.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.toexpressionast.md new file mode 100644 index 0000000000000..99001e81fde49 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.toexpressionast.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [toExpressionAst](./kibana-plugin-plugins-data-public.aggconfig.toexpressionast.md) + +## AggConfig.toExpressionAst() method + +Signature: + +```typescript +toExpressionAst(): ExpressionAstFunction | undefined; +``` +Returns: + +`ExpressionAstFunction | undefined` + +Returns an ExpressionAst representing the function for this agg type. + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.tojson.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.tojson.md new file mode 100644 index 0000000000000..aa639aa574076 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.tojson.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [toJSON](./kibana-plugin-plugins-data-public.aggconfig.tojson.md) + +## AggConfig.toJSON() method + +> Warning: This API is now obsolete. +> +> - Use serialize() instead. +> + +Signature: + +```typescript +toJSON(): AggConfigSerialized; +``` +Returns: + +`AggConfigSerialized` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.toserializedfieldformat.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.toserializedfieldformat.md new file mode 100644 index 0000000000000..7a75950f9cc6d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.toserializedfieldformat.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [toSerializedFieldFormat](./kibana-plugin-plugins-data-public.aggconfig.toserializedfieldformat.md) + +## AggConfig.toSerializedFieldFormat() method + +Returns a serialized field format for the field used in this agg. This can be passed to fieldFormats.deserialize to get the field format instance. + +Signature: + +```typescript +toSerializedFieldFormat(): {} | Ensure, SerializableState>; +``` +Returns: + +`{} | Ensure, SerializableState>` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.type.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.type.md new file mode 100644 index 0000000000000..9dc44caee42e8 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.type.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [type](./kibana-plugin-plugins-data-public.aggconfig.type.md) + +## AggConfig.type property + +Signature: + +```typescript +get type(): IAggType; + +set type(type: IAggType); +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.write.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.write.md new file mode 100644 index 0000000000000..f98394b57cac3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfig.write.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) > [write](./kibana-plugin-plugins-data-public.aggconfig.write.md) + +## AggConfig.write() method + +Signature: + +```typescript +write(aggs?: IAggConfigs): Record; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| aggs | IAggConfigs | | + +Returns: + +`Record` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md new file mode 100644 index 0000000000000..c9e08b9712480 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs._constructor_.md @@ -0,0 +1,32 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [(constructor)](./kibana-plugin-plugins-data-public.aggconfigs._constructor_.md) + +## AggConfigs.(constructor) + +Constructs a new instance of the `AggConfigs` class + +Signature: + +```typescript +constructor(indexPattern: IndexPattern, configStates: Pick & Pick<{ + type: string | IAggType; + }, "type"> & Pick<{ + type: string | IAggType; + }, never>, "enabled" | "type" | "schema" | "id" | "params">[] | undefined, opts: AggConfigsOptions); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| indexPattern | IndexPattern | | +| configStates | Pick<Pick<{
type: string;
enabled?: boolean | undefined;
id?: string | undefined;
params?: {} | import("./agg_config").SerializableState | undefined;
schema?: string | undefined;
}, "enabled" | "schema" | "id" | "params"> & Pick<{
type: string | IAggType;
}, "type"> & Pick<{
type: string | IAggType;
}, never>, "enabled" | "type" | "schema" | "id" | "params">[] | undefined | | +| opts | AggConfigsOptions | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.aggs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.aggs.md new file mode 100644 index 0000000000000..0d217e037ecb1 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.aggs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [aggs](./kibana-plugin-plugins-data-public.aggconfigs.aggs.md) + +## AggConfigs.aggs property + +Signature: + +```typescript +aggs: IAggConfig[]; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.byid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.byid.md new file mode 100644 index 0000000000000..14d65ada5e39d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.byid.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [byId](./kibana-plugin-plugins-data-public.aggconfigs.byid.md) + +## AggConfigs.byId() method + +Signature: + +```typescript +byId(id: string): AggConfig | undefined; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| id | string | | + +Returns: + +`AggConfig | undefined` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.byindex.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.byindex.md new file mode 100644 index 0000000000000..5977c81ddaf36 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.byindex.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [byIndex](./kibana-plugin-plugins-data-public.aggconfigs.byindex.md) + +## AggConfigs.byIndex() method + +Signature: + +```typescript +byIndex(index: number): AggConfig; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| index | number | | + +Returns: + +`AggConfig` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.byname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.byname.md new file mode 100644 index 0000000000000..772ba1f074d0d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.byname.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [byName](./kibana-plugin-plugins-data-public.aggconfigs.byname.md) + +## AggConfigs.byName() method + +Signature: + +```typescript +byName(name: string): AggConfig[]; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| name | string | | + +Returns: + +`AggConfig[]` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.byschemaname.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.byschemaname.md new file mode 100644 index 0000000000000..3a7c6a5f89e17 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.byschemaname.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [bySchemaName](./kibana-plugin-plugins-data-public.aggconfigs.byschemaname.md) + +## AggConfigs.bySchemaName() method + +Signature: + +```typescript +bySchemaName(schema: string): AggConfig[]; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| schema | string | | + +Returns: + +`AggConfig[]` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.bytype.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.bytype.md new file mode 100644 index 0000000000000..8bbf85ce4f29b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.bytype.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [byType](./kibana-plugin-plugins-data-public.aggconfigs.bytype.md) + +## AggConfigs.byType() method + +Signature: + +```typescript +byType(type: string): AggConfig[]; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | + +Returns: + +`AggConfig[]` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.bytypename.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.bytypename.md new file mode 100644 index 0000000000000..97f05837493f2 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.bytypename.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [byTypeName](./kibana-plugin-plugins-data-public.aggconfigs.bytypename.md) + +## AggConfigs.byTypeName() method + +Signature: + +```typescript +byTypeName(type: string): AggConfig[]; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| type | string | | + +Returns: + +`AggConfig[]` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.clone.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.clone.md new file mode 100644 index 0000000000000..0206f3c6b4751 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.clone.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [clone](./kibana-plugin-plugins-data-public.aggconfigs.clone.md) + +## AggConfigs.clone() method + +Signature: + +```typescript +clone({ enabledOnly }?: { + enabledOnly?: boolean | undefined; + }): AggConfigs; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| { enabledOnly } | {
enabledOnly?: boolean | undefined;
} | | + +Returns: + +`AggConfigs` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.createaggconfig.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.createaggconfig.md new file mode 100644 index 0000000000000..2ccded7c74e4c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.createaggconfig.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [createAggConfig](./kibana-plugin-plugins-data-public.aggconfigs.createaggconfig.md) + +## AggConfigs.createAggConfig property + +Signature: + +```typescript +createAggConfig: (params: CreateAggConfigParams, { addToAggConfigs }?: { + addToAggConfigs?: boolean | undefined; + }) => T; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getall.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getall.md new file mode 100644 index 0000000000000..091ec1ce416c3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getall.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [getAll](./kibana-plugin-plugins-data-public.aggconfigs.getall.md) + +## AggConfigs.getAll() method + +Signature: + +```typescript +getAll(): AggConfig[]; +``` +Returns: + +`AggConfig[]` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getrequestaggbyid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getrequestaggbyid.md new file mode 100644 index 0000000000000..f375648ca1cb7 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getrequestaggbyid.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [getRequestAggById](./kibana-plugin-plugins-data-public.aggconfigs.getrequestaggbyid.md) + +## AggConfigs.getRequestAggById() method + +Signature: + +```typescript +getRequestAggById(id: string): AggConfig | undefined; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| id | string | | + +Returns: + +`AggConfig | undefined` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getrequestaggs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getrequestaggs.md new file mode 100644 index 0000000000000..f4db6e373f5c3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getrequestaggs.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [getRequestAggs](./kibana-plugin-plugins-data-public.aggconfigs.getrequestaggs.md) + +## AggConfigs.getRequestAggs() method + +Signature: + +```typescript +getRequestAggs(): AggConfig[]; +``` +Returns: + +`AggConfig[]` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresponseaggbyid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresponseaggbyid.md new file mode 100644 index 0000000000000..ab31c74f6000d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresponseaggbyid.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [getResponseAggById](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggbyid.md) + +## AggConfigs.getResponseAggById() method + +Find a response agg by it's id. This may be an agg in the aggConfigs, or one created specifically for a response value + +Signature: + +```typescript +getResponseAggById(id: string): AggConfig | undefined; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| id | string | | + +Returns: + +`AggConfig | undefined` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresponseaggs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresponseaggs.md new file mode 100644 index 0000000000000..47e26bdea9e9c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.getresponseaggs.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [getResponseAggs](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggs.md) + +## AggConfigs.getResponseAggs() method + +Gets the AggConfigs (and possibly ResponseAggConfigs) that represent the values that will be produced when all aggs are run. + +With multi-value metric aggs it is possible for a single agg request to result in multiple agg values, which is why the length of a vis' responseValuesAggs may be different than the vis' aggs + + {array\[AggConfig\]} + +Signature: + +```typescript +getResponseAggs(): AggConfig[]; +``` +Returns: + +`AggConfig[]` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.indexpattern.md new file mode 100644 index 0000000000000..9bd91e185df1e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.indexpattern.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [indexPattern](./kibana-plugin-plugins-data-public.aggconfigs.indexpattern.md) + +## AggConfigs.indexPattern property + +Signature: + +```typescript +indexPattern: IndexPattern; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.jsondataequals.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.jsondataequals.md new file mode 100644 index 0000000000000..d94c3959cd6a2 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.jsondataequals.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [jsonDataEquals](./kibana-plugin-plugins-data-public.aggconfigs.jsondataequals.md) + +## AggConfigs.jsonDataEquals() method + +Data-by-data comparison of this Aggregation Ignores the non-array indexes + +Signature: + +```typescript +jsonDataEquals(aggConfigs: AggConfig[]): boolean; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| aggConfigs | AggConfig[] | | + +Returns: + +`boolean` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md new file mode 100644 index 0000000000000..c0ba1bbeea334 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.md @@ -0,0 +1,48 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) + +## AggConfigs class + +Signature: + +```typescript +export declare class AggConfigs +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(indexPattern, configStates, opts)](./kibana-plugin-plugins-data-public.aggconfigs._constructor_.md) | | Constructs a new instance of the AggConfigs class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [aggs](./kibana-plugin-plugins-data-public.aggconfigs.aggs.md) | | IAggConfig[] | | +| [createAggConfig](./kibana-plugin-plugins-data-public.aggconfigs.createaggconfig.md) | | <T extends AggConfig = AggConfig>(params: CreateAggConfigParams, { addToAggConfigs }?: {
addToAggConfigs?: boolean | undefined;
}) => T | | +| [indexPattern](./kibana-plugin-plugins-data-public.aggconfigs.indexpattern.md) | | IndexPattern | | +| [timeRange](./kibana-plugin-plugins-data-public.aggconfigs.timerange.md) | | TimeRange | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [byId(id)](./kibana-plugin-plugins-data-public.aggconfigs.byid.md) | | | +| [byIndex(index)](./kibana-plugin-plugins-data-public.aggconfigs.byindex.md) | | | +| [byName(name)](./kibana-plugin-plugins-data-public.aggconfigs.byname.md) | | | +| [bySchemaName(schema)](./kibana-plugin-plugins-data-public.aggconfigs.byschemaname.md) | | | +| [byType(type)](./kibana-plugin-plugins-data-public.aggconfigs.bytype.md) | | | +| [byTypeName(type)](./kibana-plugin-plugins-data-public.aggconfigs.bytypename.md) | | | +| [clone({ enabledOnly })](./kibana-plugin-plugins-data-public.aggconfigs.clone.md) | | | +| [getAll()](./kibana-plugin-plugins-data-public.aggconfigs.getall.md) | | | +| [getRequestAggById(id)](./kibana-plugin-plugins-data-public.aggconfigs.getrequestaggbyid.md) | | | +| [getRequestAggs()](./kibana-plugin-plugins-data-public.aggconfigs.getrequestaggs.md) | | | +| [getResponseAggById(id)](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggbyid.md) | | Find a response agg by it's id. This may be an agg in the aggConfigs, or one created specifically for a response value | +| [getResponseAggs()](./kibana-plugin-plugins-data-public.aggconfigs.getresponseaggs.md) | | Gets the AggConfigs (and possibly ResponseAggConfigs) that represent the values that will be produced when all aggs are run.With multi-value metric aggs it is possible for a single agg request to result in multiple agg values, which is why the length of a vis' responseValuesAggs may be different than the vis' aggs {array\[AggConfig\]} | +| [jsonDataEquals(aggConfigs)](./kibana-plugin-plugins-data-public.aggconfigs.jsondataequals.md) | | Data-by-data comparison of this Aggregation Ignores the non-array indexes | +| [onSearchRequestStart(searchSource, options)](./kibana-plugin-plugins-data-public.aggconfigs.onsearchrequeststart.md) | | | +| [setTimeRange(timeRange)](./kibana-plugin-plugins-data-public.aggconfigs.settimerange.md) | | | +| [toDsl(hierarchical)](./kibana-plugin-plugins-data-public.aggconfigs.todsl.md) | | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.onsearchrequeststart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.onsearchrequeststart.md new file mode 100644 index 0000000000000..3ae7af408563c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.onsearchrequeststart.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [onSearchRequestStart](./kibana-plugin-plugins-data-public.aggconfigs.onsearchrequeststart.md) + +## AggConfigs.onSearchRequestStart() method + +Signature: + +```typescript +onSearchRequestStart(searchSource: ISearchSource, options?: ISearchOptions): Promise<[unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown]>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| searchSource | ISearchSource | | +| options | ISearchOptions | | + +Returns: + +`Promise<[unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown]>` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.settimerange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.settimerange.md new file mode 100644 index 0000000000000..77530f02bc9a3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.settimerange.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [setTimeRange](./kibana-plugin-plugins-data-public.aggconfigs.settimerange.md) + +## AggConfigs.setTimeRange() method + +Signature: + +```typescript +setTimeRange(timeRange: TimeRange): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| timeRange | TimeRange | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.timerange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.timerange.md new file mode 100644 index 0000000000000..b4caef6c7f6d2 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.timerange.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [timeRange](./kibana-plugin-plugins-data-public.aggconfigs.timerange.md) + +## AggConfigs.timeRange property + +Signature: + +```typescript +timeRange?: TimeRange; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.todsl.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.todsl.md new file mode 100644 index 0000000000000..055c4113ca3e4 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggconfigs.todsl.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) > [toDsl](./kibana-plugin-plugins-data-public.aggconfigs.todsl.md) + +## AggConfigs.toDsl() method + +Signature: + +```typescript +toDsl(hierarchical?: boolean): Record; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| hierarchical | boolean | | + +Returns: + +`Record` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggsstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggsstart.md new file mode 100644 index 0000000000000..7bdf9d6501203 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.aggsstart.md @@ -0,0 +1,15 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) + +## AggsStart type + +AggsStart represents the actual external contract as AggsCommonStart is only used internally. The difference is that AggsStart includes the typings for the registry with initialized agg types. + +Signature: + +```typescript +export declare type AggsStart = Assign; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autocompletestart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autocompletestart.md new file mode 100644 index 0000000000000..44cee8c32421d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.autocompletestart.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [AutocompleteStart](./kibana-plugin-plugins-data-public.autocompletestart.md) + +## AutocompleteStart type + +\* + +Signature: + +```typescript +export declare type AutocompleteStart = ReturnType; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginsetup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginsetup.md index dba1d79e78682..fc5624aeddce1 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginsetup.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginsetup.md @@ -4,6 +4,8 @@ ## DataPublicPluginSetup interface +Data plugin public Setup contract + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md index 25ce6eaa688f8..10997c94fab06 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md @@ -4,11 +4,10 @@ ## DataPublicPluginStart.actions property +filter creation utilities [DataPublicPluginStartActions](./kibana-plugin-plugins-data-public.datapublicpluginstartactions.md) + Signature: ```typescript -actions: { - createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction; - createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction; - }; +actions: DataPublicPluginStartActions; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.autocomplete.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.autocomplete.md index d2e5aee7d90dd..8a09a10cccb24 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.autocomplete.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.autocomplete.md @@ -4,6 +4,8 @@ ## DataPublicPluginStart.autocomplete property +autocomplete service [AutocompleteStart](./kibana-plugin-plugins-data-public.autocompletestart.md) + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.fieldformats.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.fieldformats.md index dd4b38f64d10b..344044b38f7de 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.fieldformats.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.fieldformats.md @@ -4,6 +4,8 @@ ## DataPublicPluginStart.fieldFormats property +field formats service [FieldFormatsStart](./kibana-plugin-plugins-data-public.fieldformatsstart.md) + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.indexpatterns.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.indexpatterns.md index b3dd6a61760a6..0cf1e3101713d 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.indexpatterns.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.indexpatterns.md @@ -4,6 +4,8 @@ ## DataPublicPluginStart.indexPatterns property +index patterns service [IndexPatternsContract](./kibana-plugin-plugins-data-public.indexpatternscontract.md) + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md index 4f43f10ce089e..7bae0bca701bf 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.md @@ -4,6 +4,8 @@ ## DataPublicPluginStart interface +Data plugin public Start contract + Signature: ```typescript @@ -14,11 +16,11 @@ export interface DataPublicPluginStart | Property | Type | Description | | --- | --- | --- | -| [actions](./kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md) | {
createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction;
createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction;
} | | -| [autocomplete](./kibana-plugin-plugins-data-public.datapublicpluginstart.autocomplete.md) | AutocompleteStart | | -| [fieldFormats](./kibana-plugin-plugins-data-public.datapublicpluginstart.fieldformats.md) | FieldFormatsStart | | -| [indexPatterns](./kibana-plugin-plugins-data-public.datapublicpluginstart.indexpatterns.md) | IndexPatternsContract | | -| [query](./kibana-plugin-plugins-data-public.datapublicpluginstart.query.md) | QueryStart | | -| [search](./kibana-plugin-plugins-data-public.datapublicpluginstart.search.md) | ISearchStart | | -| [ui](./kibana-plugin-plugins-data-public.datapublicpluginstart.ui.md) | {
IndexPatternSelect: React.ComponentType<IndexPatternSelectProps>;
SearchBar: React.ComponentType<StatefulSearchBarProps>;
} | | +| [actions](./kibana-plugin-plugins-data-public.datapublicpluginstart.actions.md) | DataPublicPluginStartActions | filter creation utilities [DataPublicPluginStartActions](./kibana-plugin-plugins-data-public.datapublicpluginstartactions.md) | +| [autocomplete](./kibana-plugin-plugins-data-public.datapublicpluginstart.autocomplete.md) | AutocompleteStart | autocomplete service [AutocompleteStart](./kibana-plugin-plugins-data-public.autocompletestart.md) | +| [fieldFormats](./kibana-plugin-plugins-data-public.datapublicpluginstart.fieldformats.md) | FieldFormatsStart | field formats service [FieldFormatsStart](./kibana-plugin-plugins-data-public.fieldformatsstart.md) | +| [indexPatterns](./kibana-plugin-plugins-data-public.datapublicpluginstart.indexpatterns.md) | IndexPatternsContract | index patterns service [IndexPatternsContract](./kibana-plugin-plugins-data-public.indexpatternscontract.md) | +| [query](./kibana-plugin-plugins-data-public.datapublicpluginstart.query.md) | QueryStart | query service [QueryStart](./kibana-plugin-plugins-data-public.querystart.md) | +| [search](./kibana-plugin-plugins-data-public.datapublicpluginstart.search.md) | ISearchStart | search service [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) | +| [ui](./kibana-plugin-plugins-data-public.datapublicpluginstart.ui.md) | DataPublicPluginStartUi | prewired UI components [DataPublicPluginStartUi](./kibana-plugin-plugins-data-public.datapublicpluginstartui.md) | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.query.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.query.md index a44e250077ed4..16ba5dafbb264 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.query.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.query.md @@ -4,6 +4,8 @@ ## DataPublicPluginStart.query property +query service [QueryStart](./kibana-plugin-plugins-data-public.querystart.md) + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.search.md index eec00e7b13e9d..98832d7ca11d8 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.search.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.search.md @@ -4,6 +4,8 @@ ## DataPublicPluginStart.search property +search service [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.ui.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.ui.md index 9c24216834371..671a1814ac644 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.ui.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstart.ui.md @@ -4,11 +4,10 @@ ## DataPublicPluginStart.ui property +prewired UI components [DataPublicPluginStartUi](./kibana-plugin-plugins-data-public.datapublicpluginstartui.md) + Signature: ```typescript -ui: { - IndexPatternSelect: React.ComponentType; - SearchBar: React.ComponentType; - }; +ui: DataPublicPluginStartUi; ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartactions.createfiltersfromrangeselectaction.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartactions.createfiltersfromrangeselectaction.md new file mode 100644 index 0000000000000..c954e0095cbb6 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartactions.createfiltersfromrangeselectaction.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPublicPluginStartActions](./kibana-plugin-plugins-data-public.datapublicpluginstartactions.md) > [createFiltersFromRangeSelectAction](./kibana-plugin-plugins-data-public.datapublicpluginstartactions.createfiltersfromrangeselectaction.md) + +## DataPublicPluginStartActions.createFiltersFromRangeSelectAction property + +Signature: + +```typescript +createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartactions.createfiltersfromvalueclickaction.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartactions.createfiltersfromvalueclickaction.md new file mode 100644 index 0000000000000..70bd5091f3604 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartactions.createfiltersfromvalueclickaction.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPublicPluginStartActions](./kibana-plugin-plugins-data-public.datapublicpluginstartactions.md) > [createFiltersFromValueClickAction](./kibana-plugin-plugins-data-public.datapublicpluginstartactions.createfiltersfromvalueclickaction.md) + +## DataPublicPluginStartActions.createFiltersFromValueClickAction property + +Signature: + +```typescript +createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartactions.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartactions.md new file mode 100644 index 0000000000000..d44c9e892cb80 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartactions.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPublicPluginStartActions](./kibana-plugin-plugins-data-public.datapublicpluginstartactions.md) + +## DataPublicPluginStartActions interface + +utilities to generate filters from action context + +Signature: + +```typescript +export interface DataPublicPluginStartActions +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [createFiltersFromRangeSelectAction](./kibana-plugin-plugins-data-public.datapublicpluginstartactions.createfiltersfromrangeselectaction.md) | typeof createFiltersFromRangeSelectAction | | +| [createFiltersFromValueClickAction](./kibana-plugin-plugins-data-public.datapublicpluginstartactions.createfiltersfromvalueclickaction.md) | typeof createFiltersFromValueClickAction | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartui.indexpatternselect.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartui.indexpatternselect.md new file mode 100644 index 0000000000000..eac29dc5de70d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartui.indexpatternselect.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPublicPluginStartUi](./kibana-plugin-plugins-data-public.datapublicpluginstartui.md) > [IndexPatternSelect](./kibana-plugin-plugins-data-public.datapublicpluginstartui.indexpatternselect.md) + +## DataPublicPluginStartUi.IndexPatternSelect property + +Signature: + +```typescript +IndexPatternSelect: React.ComponentType; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartui.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartui.md new file mode 100644 index 0000000000000..3d827c0db465b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartui.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPublicPluginStartUi](./kibana-plugin-plugins-data-public.datapublicpluginstartui.md) + +## DataPublicPluginStartUi interface + +Data plugin prewired UI components + +Signature: + +```typescript +export interface DataPublicPluginStartUi +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [IndexPatternSelect](./kibana-plugin-plugins-data-public.datapublicpluginstartui.indexpatternselect.md) | React.ComponentType<IndexPatternSelectProps> | | +| [SearchBar](./kibana-plugin-plugins-data-public.datapublicpluginstartui.searchbar.md) | React.ComponentType<StatefulSearchBarProps> | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartui.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartui.searchbar.md new file mode 100644 index 0000000000000..06339d14cde24 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.datapublicpluginstartui.searchbar.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [DataPublicPluginStartUi](./kibana-plugin-plugins-data-public.datapublicpluginstartui.md) > [SearchBar](./kibana-plugin-plugins-data-public.datapublicpluginstartui.searchbar.md) + +## DataPublicPluginStartUi.SearchBar property + +Signature: + +```typescript +SearchBar: React.ComponentType; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformatsstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformatsstart.md new file mode 100644 index 0000000000000..1a0a08f44451a --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.fieldformatsstart.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [FieldFormatsStart](./kibana-plugin-plugins-data-public.fieldformatsstart.md) + +## FieldFormatsStart type + + +Signature: + +```typescript +export declare type FieldFormatsStart = Omit & { + deserialize: FormatFactory; +}; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md index 337b4b3302cc3..d32e9a955f890 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.getsearchparamsfromrequest.md @@ -9,7 +9,6 @@ ```typescript export declare function getSearchParamsFromRequest(searchRequest: SearchRequest, dependencies: { - esShardTimeout: number; getConfig: GetConfigFn; }): ISearchRequestParams; ``` @@ -19,7 +18,7 @@ export declare function getSearchParamsFromRequest(searchRequest: SearchRequest, | Parameter | Type | Description | | --- | --- | --- | | searchRequest | SearchRequest | | -| dependencies | {
esShardTimeout: number;
getConfig: GetConfigFn;
} | | +| dependencies | {
getConfig: GetConfigFn;
} | | Returns: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md index 2e078e3404fe6..a5bb15c963978 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern._constructor_.md @@ -9,7 +9,7 @@ Constructs a new instance of the `IndexPattern` class Signature: ```typescript -constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); +constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); ``` ## Parameters @@ -17,5 +17,5 @@ constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCach | Parameter | Type | Description | | --- | --- | --- | | id | string | undefined | | -| { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, } | IndexPatternDeps | | +| { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, } | IndexPatternDeps | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md index 4c53af3f8970e..87ce1e258712a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.md @@ -14,7 +14,7 @@ export declare class IndexPattern implements IIndexPattern | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(id, { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | +| [(constructor)(id, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, })](./kibana-plugin-plugins-data-public.indexpattern._constructor_.md) | | Constructs a new instance of the IndexPattern class | ## Properties @@ -29,11 +29,13 @@ export declare class IndexPattern implements IIndexPattern | [id](./kibana-plugin-plugins-data-public.indexpattern.id.md) | | string | | | [intervalName](./kibana-plugin-plugins-data-public.indexpattern.intervalname.md) | | string | undefined | | | [metaFields](./kibana-plugin-plugins-data-public.indexpattern.metafields.md) | | string[] | | +| [originalBody](./kibana-plugin-plugins-data-public.indexpattern.originalbody.md) | | {
[key: string]: any;
} | | | [sourceFilters](./kibana-plugin-plugins-data-public.indexpattern.sourcefilters.md) | | SourceFilter[] | | | [timeFieldName](./kibana-plugin-plugins-data-public.indexpattern.timefieldname.md) | | string | undefined | | | [title](./kibana-plugin-plugins-data-public.indexpattern.title.md) | | string | | | [type](./kibana-plugin-plugins-data-public.indexpattern.type.md) | | string | undefined | | | [typeMeta](./kibana-plugin-plugins-data-public.indexpattern.typemeta.md) | | TypeMeta | | +| [version](./kibana-plugin-plugins-data-public.indexpattern.version.md) | | string | undefined | | ## Methods @@ -60,6 +62,5 @@ export declare class IndexPattern implements IIndexPattern | [prepBody()](./kibana-plugin-plugins-data-public.indexpattern.prepbody.md) | | | | [refreshFields()](./kibana-plugin-plugins-data-public.indexpattern.refreshfields.md) | | | | [removeScriptedField(fieldName)](./kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md) | | | -| [save(saveAttempts)](./kibana-plugin-plugins-data-public.indexpattern.save.md) | | | | [toSpec()](./kibana-plugin-plugins-data-public.indexpattern.tospec.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.originalbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.originalbody.md new file mode 100644 index 0000000000000..4bc3c76afbae9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.originalbody.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [originalBody](./kibana-plugin-plugins-data-public.indexpattern.originalbody.md) + +## IndexPattern.originalBody property + +Signature: + +```typescript +originalBody: { + [key: string]: any; + }; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md index 42c6dd72b8c4e..e902d9c42b082 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.removescriptedfield.md @@ -7,7 +7,7 @@ Signature: ```typescript -removeScriptedField(fieldName: string): Promise; +removeScriptedField(fieldName: string): void; ``` ## Parameters @@ -18,5 +18,5 @@ removeScriptedField(fieldName: string): Promise; Returns: -`Promise` +`void` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.save.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.save.md deleted file mode 100644 index d0b471cc2bc21..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.save.md +++ /dev/null @@ -1,22 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [save](./kibana-plugin-plugins-data-public.indexpattern.save.md) - -## IndexPattern.save() method - -Signature: - -```typescript -save(saveAttempts?: number): Promise; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| saveAttempts | number | | - -Returns: - -`Promise` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.version.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.version.md new file mode 100644 index 0000000000000..99d3bc4e7a04d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.indexpattern.version.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [IndexPattern](./kibana-plugin-plugins-data-public.indexpattern.md) > [version](./kibana-plugin-plugins-data-public.indexpattern.version.md) + +## IndexPattern.version property + +Signature: + +```typescript +version: string | undefined; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.aggs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.aggs.md new file mode 100644 index 0000000000000..ad97820d4d760 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.aggs.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) > [aggs](./kibana-plugin-plugins-data-public.isearchsetup.aggs.md) + +## ISearchSetup.aggs property + +Signature: + +```typescript +aggs: AggsSetup; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md new file mode 100644 index 0000000000000..b68c4d61e4e03 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) + +## ISearchSetup interface + +The setup contract exposed by the Search plugin exposes the search strategy extension point. + +Signature: + +```typescript +export interface ISearchSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [aggs](./kibana-plugin-plugins-data-public.isearchsetup.aggs.md) | AggsSetup | | +| [usageCollector](./kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md) | SearchUsageCollector | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md new file mode 100644 index 0000000000000..908a842974f25 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) > [usageCollector](./kibana-plugin-plugins-data-public.isearchsetup.usagecollector.md) + +## ISearchSetup.usageCollector property + +Signature: + +```typescript +usageCollector?: SearchUsageCollector; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsource.md index 4b9f6e3594dc5..43e10d0bef57a 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsource.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchsource.md @@ -4,7 +4,7 @@ ## ISearchSource type -\* +search source interface Signature: diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.aggs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.aggs.md new file mode 100644 index 0000000000000..993c6bf5a922b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.aggs.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) > [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) + +## ISearchStart.aggs property + +agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) + +Signature: + +```typescript +aggs: AggsStart; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md new file mode 100644 index 0000000000000..cee213fc6e7e3 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) + +## ISearchStart interface + +search service + +Signature: + +```typescript +export interface ISearchStart +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [aggs](./kibana-plugin-plugins-data-public.isearchstart.aggs.md) | AggsStart | agg config sub service [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | +| [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) | ISearchGeneric | low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | +| [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) | ISearchStartSearchSource | high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.search.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.search.md new file mode 100644 index 0000000000000..80e140e9fdd5c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.search.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) > [search](./kibana-plugin-plugins-data-public.isearchstart.search.md) + +## ISearchStart.search property + +low level search [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) + +Signature: + +```typescript +search: ISearchGeneric; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.searchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.searchsource.md new file mode 100644 index 0000000000000..5d4b884b2c25b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstart.searchsource.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) > [searchSource](./kibana-plugin-plugins-data-public.isearchstart.searchsource.md) + +## ISearchStart.searchSource property + +high level search [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) + +Signature: + +```typescript +searchSource: ISearchStartSearchSource; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstartsearchsource.create.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstartsearchsource.create.md new file mode 100644 index 0000000000000..7f6344b82d27c --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstartsearchsource.create.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) > [create](./kibana-plugin-plugins-data-public.isearchstartsearchsource.create.md) + +## ISearchStartSearchSource.create property + +creates [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) based on provided serialized [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) + +Signature: + +```typescript +create: (fields?: SearchSourceFields) => Promise; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstartsearchsource.createempty.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstartsearchsource.createempty.md new file mode 100644 index 0000000000000..b13b5d227c8b4 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstartsearchsource.createempty.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) > [createEmpty](./kibana-plugin-plugins-data-public.isearchstartsearchsource.createempty.md) + +## ISearchStartSearchSource.createEmpty property + +creates empty [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) + +Signature: + +```typescript +createEmpty: () => ISearchSource; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstartsearchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstartsearchsource.md new file mode 100644 index 0000000000000..f10d5bb002a0f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.isearchstartsearchsource.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) + +## ISearchStartSearchSource interface + +high level search service + +Signature: + +```typescript +export interface ISearchStartSearchSource +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [create](./kibana-plugin-plugins-data-public.isearchstartsearchsource.create.md) | (fields?: SearchSourceFields) => Promise<ISearchSource> | creates [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) based on provided serialized [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | +| [createEmpty](./kibana-plugin-plugins-data-public.isearchstartsearchsource.createempty.md) | () => ISearchSource | creates empty [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md index b651480a85899..f51549c81fb62 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.md @@ -8,6 +8,8 @@ | Class | Description | | --- | --- | +| [AggConfig](./kibana-plugin-plugins-data-public.aggconfig.md) | | +| [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) | | | [AggParamType](./kibana-plugin-plugins-data-public.aggparamtype.md) | | | [FieldFormat](./kibana-plugin-plugins-data-public.fieldformat.md) | | | [FilterManager](./kibana-plugin-plugins-data-public.filtermanager.md) | | @@ -18,6 +20,7 @@ | [Plugin](./kibana-plugin-plugins-data-public.plugin.md) | | | [RequestTimeoutError](./kibana-plugin-plugins-data-public.requesttimeouterror.md) | Class used to signify that a request timed out. Useful for applications to conditionally handle this type of error differently than other errors. | | [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) | | +| [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) | \* | | [TimeHistory](./kibana-plugin-plugins-data-public.timehistory.md) | | ## Enumerations @@ -47,8 +50,10 @@ | --- | --- | | [AggParamOption](./kibana-plugin-plugins-data-public.aggparamoption.md) | | | [ApplyGlobalFilterActionContext](./kibana-plugin-plugins-data-public.applyglobalfilteractioncontext.md) | | -| [DataPublicPluginSetup](./kibana-plugin-plugins-data-public.datapublicpluginsetup.md) | | -| [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) | | +| [DataPublicPluginSetup](./kibana-plugin-plugins-data-public.datapublicpluginsetup.md) | Data plugin public Setup contract | +| [DataPublicPluginStart](./kibana-plugin-plugins-data-public.datapublicpluginstart.md) | Data plugin public Start contract | +| [DataPublicPluginStartActions](./kibana-plugin-plugins-data-public.datapublicpluginstartactions.md) | utilities to generate filters from action context | +| [DataPublicPluginStartUi](./kibana-plugin-plugins-data-public.datapublicpluginstartui.md) | Data plugin prewired UI components | | [EsQueryConfig](./kibana-plugin-plugins-data-public.esqueryconfig.md) | | | [FieldFormatConfig](./kibana-plugin-plugins-data-public.fieldformatconfig.md) | | | [FieldMappingSpec](./kibana-plugin-plugins-data-public.fieldmappingspec.md) | | @@ -65,10 +70,14 @@ | [IndexPatternAttributes](./kibana-plugin-plugins-data-public.indexpatternattributes.md) | Use data plugin interface instead | | [IndexPatternTypeMeta](./kibana-plugin-plugins-data-public.indexpatterntypemeta.md) | | | [ISearchOptions](./kibana-plugin-plugins-data-public.isearchoptions.md) | | +| [ISearchSetup](./kibana-plugin-plugins-data-public.isearchsetup.md) | The setup contract exposed by the Search plugin exposes the search strategy extension point. | +| [ISearchStart](./kibana-plugin-plugins-data-public.isearchstart.md) | search service | +| [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md) | high level search service | | [KueryNode](./kibana-plugin-plugins-data-public.kuerynode.md) | | | [OptionedValueProp](./kibana-plugin-plugins-data-public.optionedvalueprop.md) | | | [Query](./kibana-plugin-plugins-data-public.query.md) | | | [QueryState](./kibana-plugin-plugins-data-public.querystate.md) | All query state service state | +| [QueryStateChange](./kibana-plugin-plugins-data-public.querystatechange.md) | | | [QuerySuggestionBasic](./kibana-plugin-plugins-data-public.querysuggestionbasic.md) | \* | | [QuerySuggestionField](./kibana-plugin-plugins-data-public.querysuggestionfield.md) | \* | | [QuerySuggestionGetFnArgs](./kibana-plugin-plugins-data-public.querysuggestiongetfnargs.md) | \* | @@ -78,7 +87,7 @@ | [SavedQueryService](./kibana-plugin-plugins-data-public.savedqueryservice.md) | | | [SearchError](./kibana-plugin-plugins-data-public.searcherror.md) | | | [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) | | -| [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | | +| [SearchSourceFields](./kibana-plugin-plugins-data-public.searchsourcefields.md) | search source fields | | [TabbedAggColumn](./kibana-plugin-plugins-data-public.tabbedaggcolumn.md) | \* | | [TabbedTable](./kibana-plugin-plugins-data-public.tabbedtable.md) | \* | | [TimeRange](./kibana-plugin-plugins-data-public.timerange.md) | | @@ -124,6 +133,8 @@ | [AggConfigOptions](./kibana-plugin-plugins-data-public.aggconfigoptions.md) | | | [AggGroupName](./kibana-plugin-plugins-data-public.agggroupname.md) | | | [AggParam](./kibana-plugin-plugins-data-public.aggparam.md) | | +| [AggsStart](./kibana-plugin-plugins-data-public.aggsstart.md) | AggsStart represents the actual external contract as AggsCommonStart is only used internally. The difference is that AggsStart includes the typings for the registry with initialized agg types. | +| [AutocompleteStart](./kibana-plugin-plugins-data-public.autocompletestart.md) | \* | | [CustomFilter](./kibana-plugin-plugins-data-public.customfilter.md) | | | [EsaggsExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esaggsexpressionfunctiondefinition.md) | | | [EsdslExpressionFunctionDefinition](./kibana-plugin-plugins-data-public.esdslexpressionfunctiondefinition.md) | | @@ -133,6 +144,7 @@ | [FieldFormatId](./kibana-plugin-plugins-data-public.fieldformatid.md) | id type is needed for creating custom converters. | | [FieldFormatsContentType](./kibana-plugin-plugins-data-public.fieldformatscontenttype.md) | \* | | [FieldFormatsGetConfigFn](./kibana-plugin-plugins-data-public.fieldformatsgetconfigfn.md) | | +| [FieldFormatsStart](./kibana-plugin-plugins-data-public.fieldformatsstart.md) | | | [IAggConfig](./kibana-plugin-plugins-data-public.iaggconfig.md) | AggConfig This class represents an aggregation, which is displayed in the left-hand nav of the Visualize app. | | [IAggType](./kibana-plugin-plugins-data-public.iaggtype.md) | | | [IFieldFormat](./kibana-plugin-plugins-data-public.ifieldformat.md) | | @@ -144,12 +156,13 @@ | [InputTimeRange](./kibana-plugin-plugins-data-public.inputtimerange.md) | | | [ISearch](./kibana-plugin-plugins-data-public.isearch.md) | | | [ISearchGeneric](./kibana-plugin-plugins-data-public.isearchgeneric.md) | | -| [ISearchSource](./kibana-plugin-plugins-data-public.isearchsource.md) | \* | +| [ISearchSource](./kibana-plugin-plugins-data-public.isearchsource.md) | search source interface | | [MappingObject](./kibana-plugin-plugins-data-public.mappingobject.md) | | | [MatchAllFilter](./kibana-plugin-plugins-data-public.matchallfilter.md) | | | [ParsedInterval](./kibana-plugin-plugins-data-public.parsedinterval.md) | | | [PhraseFilter](./kibana-plugin-plugins-data-public.phrasefilter.md) | | | [PhrasesFilter](./kibana-plugin-plugins-data-public.phrasesfilter.md) | | +| [QueryStart](./kibana-plugin-plugins-data-public.querystart.md) | | | [QuerySuggestion](./kibana-plugin-plugins-data-public.querysuggestion.md) | \* | | [QuerySuggestionGetFn](./kibana-plugin-plugins-data-public.querysuggestiongetfn.md) | | | [RangeFilter](./kibana-plugin-plugins-data-public.rangefilter.md) | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystart.md new file mode 100644 index 0000000000000..f48a9ee7a79e4 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystart.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStart](./kibana-plugin-plugins-data-public.querystart.md) + +## QueryStart type + +Signature: + +```typescript +export declare type QueryStart = ReturnType; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.appfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.appfilters.md new file mode 100644 index 0000000000000..b358e9477e515 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.appfilters.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStateChange](./kibana-plugin-plugins-data-public.querystatechange.md) > [appFilters](./kibana-plugin-plugins-data-public.querystatechange.appfilters.md) + +## QueryStateChange.appFilters property + +Signature: + +```typescript +appFilters?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.globalfilters.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.globalfilters.md new file mode 100644 index 0000000000000..c395f169c35a5 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.globalfilters.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStateChange](./kibana-plugin-plugins-data-public.querystatechange.md) > [globalFilters](./kibana-plugin-plugins-data-public.querystatechange.globalfilters.md) + +## QueryStateChange.globalFilters property + +Signature: + +```typescript +globalFilters?: boolean; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.md new file mode 100644 index 0000000000000..71fb211da11d2 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystatechange.md @@ -0,0 +1,19 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [QueryStateChange](./kibana-plugin-plugins-data-public.querystatechange.md) + +## QueryStateChange interface + +Signature: + +```typescript +export interface QueryStateChange extends QueryStateChangePartial +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [appFilters](./kibana-plugin-plugins-data-public.querystatechange.appfilters.md) | boolean | | +| [globalFilters](./kibana-plugin-plugins-data-public.querystatechange.globalfilters.md) | boolean | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md index 9f3ed8c1263ba..cf171d9ee9f37 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.querystringinput.md @@ -7,5 +7,5 @@ Signature: ```typescript -QueryStringInput: React.FC> +QueryStringInput: React.FC> ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md index 498691c06285d..d1d20291a6799 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchbar.md @@ -7,7 +7,7 @@ Signature: ```typescript -SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "indicateNoData" | "timeHistory" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "timeHistory" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "indicateNoData" | "onFiltersUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; } ``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor._constructor_.md index 6f5dd1076fb40..4c67639300883 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor._constructor_.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor._constructor_.md @@ -4,12 +4,12 @@ ## SearchInterceptor.(constructor) -This class should be instantiated with a `requestTimeout` corresponding with how many ms after requests are initiated that they should automatically cancel. +Constructs a new instance of the `SearchInterceptor` class Signature: ```typescript -constructor(deps: SearchInterceptorDeps, requestTimeout?: number | undefined); +constructor(deps: SearchInterceptorDeps); ``` ## Parameters @@ -17,5 +17,4 @@ constructor(deps: SearchInterceptorDeps, requestTimeout?: number | undefined); | Parameter | Type | Description | | --- | --- | --- | | deps | SearchInterceptorDeps | | -| requestTimeout | number | undefined | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md deleted file mode 100644 index ef36b3f37b0c7..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md +++ /dev/null @@ -1,17 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [getPendingCount$](./kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md) - -## SearchInterceptor.getPendingCount$() method - -Returns an `Observable` over the current number of pending searches. This could mean that one of the search requests is still in flight, or that it has only received partial responses. - -Signature: - -```typescript -getPendingCount$(): Observable; -``` -Returns: - -`Observable` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md index 32954927504ae..5cee345db6cd2 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.md @@ -14,21 +14,18 @@ export declare class SearchInterceptor | Constructor | Modifiers | Description | | --- | --- | --- | -| [(constructor)(deps, requestTimeout)](./kibana-plugin-plugins-data-public.searchinterceptor._constructor_.md) | | This class should be instantiated with a requestTimeout corresponding with how many ms after requests are initiated that they should automatically cancel. | +| [(constructor)(deps)](./kibana-plugin-plugins-data-public.searchinterceptor._constructor_.md) | | Constructs a new instance of the SearchInterceptor class | ## Properties | Property | Modifiers | Type | Description | | --- | --- | --- | --- | | [deps](./kibana-plugin-plugins-data-public.searchinterceptor.deps.md) | | SearchInterceptorDeps | | -| [requestTimeout](./kibana-plugin-plugins-data-public.searchinterceptor.requesttimeout.md) | | number | undefined | | +| [showTimeoutError](./kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md) | | ((e: Error) => void) & import("lodash").Cancelable | | ## Methods | Method | Modifiers | Description | | --- | --- | --- | -| [getPendingCount$()](./kibana-plugin-plugins-data-public.searchinterceptor.getpendingcount_.md) | | Returns an Observable over the current number of pending searches. This could mean that one of the search requests is still in flight, or that it has only received partial responses. | -| [runSearch(request, signal, strategy)](./kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md) | | | | [search(request, options)](./kibana-plugin-plugins-data-public.searchinterceptor.search.md) | | Searches using the given search method. Overrides the AbortSignal with one that will abort either when cancelPending is called, when the request times out, or when the original AbortSignal is aborted. Updates pendingCount$ when the request is started/finalized. | -| [setupTimers(options)](./kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md) | | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.requesttimeout.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.requesttimeout.md deleted file mode 100644 index 3123433762991..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.requesttimeout.md +++ /dev/null @@ -1,11 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [requestTimeout](./kibana-plugin-plugins-data-public.searchinterceptor.requesttimeout.md) - -## SearchInterceptor.requestTimeout property - -Signature: - -```typescript -protected readonly requestTimeout?: number | undefined; -``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md deleted file mode 100644 index ad1d1dcb59d7b..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md +++ /dev/null @@ -1,24 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [runSearch](./kibana-plugin-plugins-data-public.searchinterceptor.runsearch.md) - -## SearchInterceptor.runSearch() method - -Signature: - -```typescript -protected runSearch(request: IEsSearchRequest, signal: AbortSignal, strategy?: string): Observable; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| request | IEsSearchRequest | | -| signal | AbortSignal | | -| strategy | string | | - -Returns: - -`Observable` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md deleted file mode 100644 index fe35655258b4c..0000000000000 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md +++ /dev/null @@ -1,28 +0,0 @@ - - -[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [setupTimers](./kibana-plugin-plugins-data-public.searchinterceptor.setuptimers.md) - -## SearchInterceptor.setupTimers() method - -Signature: - -```typescript -protected setupTimers(options?: ISearchOptions): { - combinedSignal: AbortSignal; - cleanup: () => void; - }; -``` - -## Parameters - -| Parameter | Type | Description | -| --- | --- | --- | -| options | ISearchOptions | | - -Returns: - -`{ - combinedSignal: AbortSignal; - cleanup: () => void; - }` - diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md new file mode 100644 index 0000000000000..91ecb2821acbf --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptor](./kibana-plugin-plugins-data-public.searchinterceptor.md) > [showTimeoutError](./kibana-plugin-plugins-data-public.searchinterceptor.showtimeouterror.md) + +## SearchInterceptor.showTimeoutError property + +Signature: + +```typescript +protected showTimeoutError: ((e: Error) => void) & import("lodash").Cancelable; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource._constructor_.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource._constructor_.md new file mode 100644 index 0000000000000..00e9050ee8ff9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource._constructor_.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [(constructor)](./kibana-plugin-plugins-data-public.searchsource._constructor_.md) + +## SearchSource.(constructor) + +Constructs a new instance of the `SearchSource` class + +Signature: + +```typescript +constructor(fields: SearchSourceFields | undefined, dependencies: SearchSourceDependencies); +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| fields | SearchSourceFields | undefined | | +| dependencies | SearchSourceDependencies | | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.create.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.create.md new file mode 100644 index 0000000000000..4264c3ff224b1 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.create.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [create](./kibana-plugin-plugins-data-public.searchsource.create.md) + +## SearchSource.create() method + +> Warning: This API is now obsolete. +> +> Don't use. +> + +Signature: + +```typescript +create(): SearchSource; +``` +Returns: + +`SearchSource` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createchild.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createchild.md new file mode 100644 index 0000000000000..0c2e75651b354 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createchild.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [createChild](./kibana-plugin-plugins-data-public.searchsource.createchild.md) + +## SearchSource.createChild() method + +creates a new child search source + +Signature: + +```typescript +createChild(options?: {}): SearchSource; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | {} | | + +Returns: + +`SearchSource` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createcopy.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createcopy.md new file mode 100644 index 0000000000000..1053d31010d00 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.createcopy.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [createCopy](./kibana-plugin-plugins-data-public.searchsource.createcopy.md) + +## SearchSource.createCopy() method + +creates a copy of this search source (without its children) + +Signature: + +```typescript +createCopy(): SearchSource; +``` +Returns: + +`SearchSource` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.destroy.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.destroy.md new file mode 100644 index 0000000000000..8a7cc5ee75d11 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.destroy.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [destroy](./kibana-plugin-plugins-data-public.searchsource.destroy.md) + +## SearchSource.destroy() method + +Completely destroy the SearchSource. {undefined} + +Signature: + +```typescript +destroy(): void; +``` +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md new file mode 100644 index 0000000000000..8fd17e6b1a1d9 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.fetch.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [fetch](./kibana-plugin-plugins-data-public.searchsource.fetch.md) + +## SearchSource.fetch() method + +Fetch this source and reject the returned Promise on error + + +Signature: + +```typescript +fetch(options?: ISearchOptions): Promise>; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| options | ISearchOptions | | + +Returns: + +`Promise>` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfield.md new file mode 100644 index 0000000000000..7c516cc29df15 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfield.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getField](./kibana-plugin-plugins-data-public.searchsource.getfield.md) + +## SearchSource.getField() method + +Gets a single field from the fields + +Signature: + +```typescript +getField(field: K, recurse?: boolean): SearchSourceFields[K]; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| field | K | | +| recurse | boolean | | + +Returns: + +`SearchSourceFields[K]` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md new file mode 100644 index 0000000000000..1980227bee623 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getfields.md @@ -0,0 +1,51 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getFields](./kibana-plugin-plugins-data-public.searchsource.getfields.md) + +## SearchSource.getFields() method + +returns all search source fields + +Signature: + +```typescript +getFields(): { + type?: string | undefined; + query?: import("../..").Query | undefined; + filter?: Filter | Filter[] | (() => Filter | Filter[] | undefined) | undefined; + sort?: Record | Record[] | undefined; + highlight?: any; + highlightAll?: boolean | undefined; + aggs?: any; + from?: number | undefined; + size?: number | undefined; + source?: string | boolean | string[] | undefined; + version?: boolean | undefined; + fields?: string | boolean | string[] | undefined; + index?: import("../..").IndexPattern | undefined; + searchAfter?: import("./types").EsQuerySearchAfter | undefined; + timeout?: string | undefined; + terminate_after?: number | undefined; + }; +``` +Returns: + +`{ + type?: string | undefined; + query?: import("../..").Query | undefined; + filter?: Filter | Filter[] | (() => Filter | Filter[] | undefined) | undefined; + sort?: Record | Record[] | undefined; + highlight?: any; + highlightAll?: boolean | undefined; + aggs?: any; + from?: number | undefined; + size?: number | undefined; + source?: string | boolean | string[] | undefined; + version?: boolean | undefined; + fields?: string | boolean | string[] | undefined; + index?: import("../..").IndexPattern | undefined; + searchAfter?: import("./types").EsQuerySearchAfter | undefined; + timeout?: string | undefined; + terminate_after?: number | undefined; + }` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getid.md new file mode 100644 index 0000000000000..b33410d86ae85 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getid.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getId](./kibana-plugin-plugins-data-public.searchsource.getid.md) + +## SearchSource.getId() method + +returns search source id + +Signature: + +```typescript +getId(): string; +``` +Returns: + +`string` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getownfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getownfield.md new file mode 100644 index 0000000000000..d5a133772264e --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getownfield.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getOwnField](./kibana-plugin-plugins-data-public.searchsource.getownfield.md) + +## SearchSource.getOwnField() method + +Get the field from our own fields, don't traverse up the chain + +Signature: + +```typescript +getOwnField(field: K): SearchSourceFields[K]; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| field | K | | + +Returns: + +`SearchSourceFields[K]` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getparent.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getparent.md new file mode 100644 index 0000000000000..14578f7949ba6 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getparent.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getParent](./kibana-plugin-plugins-data-public.searchsource.getparent.md) + +## SearchSource.getParent() method + +Get the parent of this SearchSource {undefined\|searchSource} + +Signature: + +```typescript +getParent(): SearchSource | undefined; +``` +Returns: + +`SearchSource | undefined` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md new file mode 100644 index 0000000000000..cc50d3f017971 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getSearchRequestBody](./kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md) + +## SearchSource.getSearchRequestBody() method + +Returns body contents of the search request, often referred as query DSL. + +Signature: + +```typescript +getSearchRequestBody(): Promise; +``` +Returns: + +`Promise` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getserializedfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getserializedfields.md new file mode 100644 index 0000000000000..3f58a76b24cd0 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.getserializedfields.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [getSerializedFields](./kibana-plugin-plugins-data-public.searchsource.getserializedfields.md) + +## SearchSource.getSerializedFields() method + +serializes search source fields (which can later be passed to [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md)) + +Signature: + +```typescript +getSerializedFields(): SearchSourceFields; +``` +Returns: + +`SearchSourceFields` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.history.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.history.md new file mode 100644 index 0000000000000..e77c9dac7239f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.history.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [history](./kibana-plugin-plugins-data-public.searchsource.history.md) + +## SearchSource.history property + +Signature: + +```typescript +history: SearchRequest[]; +``` diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md new file mode 100644 index 0000000000000..87346f81b13e2 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.md @@ -0,0 +1,49 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) + +## SearchSource class + +\* + +Signature: + +```typescript +export declare class SearchSource +``` + +## Constructors + +| Constructor | Modifiers | Description | +| --- | --- | --- | +| [(constructor)(fields, dependencies)](./kibana-plugin-plugins-data-public.searchsource._constructor_.md) | | Constructs a new instance of the SearchSource class | + +## Properties + +| Property | Modifiers | Type | Description | +| --- | --- | --- | --- | +| [history](./kibana-plugin-plugins-data-public.searchsource.history.md) | | SearchRequest[] | | + +## Methods + +| Method | Modifiers | Description | +| --- | --- | --- | +| [create()](./kibana-plugin-plugins-data-public.searchsource.create.md) | | | +| [createChild(options)](./kibana-plugin-plugins-data-public.searchsource.createchild.md) | | creates a new child search source | +| [createCopy()](./kibana-plugin-plugins-data-public.searchsource.createcopy.md) | | creates a copy of this search source (without its children) | +| [destroy()](./kibana-plugin-plugins-data-public.searchsource.destroy.md) | | Completely destroy the SearchSource. {undefined} | +| [fetch(options)](./kibana-plugin-plugins-data-public.searchsource.fetch.md) | | Fetch this source and reject the returned Promise on error | +| [getField(field, recurse)](./kibana-plugin-plugins-data-public.searchsource.getfield.md) | | Gets a single field from the fields | +| [getFields()](./kibana-plugin-plugins-data-public.searchsource.getfields.md) | | returns all search source fields | +| [getId()](./kibana-plugin-plugins-data-public.searchsource.getid.md) | | returns search source id | +| [getOwnField(field)](./kibana-plugin-plugins-data-public.searchsource.getownfield.md) | | Get the field from our own fields, don't traverse up the chain | +| [getParent()](./kibana-plugin-plugins-data-public.searchsource.getparent.md) | | Get the parent of this SearchSource {undefined\|searchSource} | +| [getSearchRequestBody()](./kibana-plugin-plugins-data-public.searchsource.getsearchrequestbody.md) | | Returns body contents of the search request, often referred as query DSL. | +| [getSerializedFields()](./kibana-plugin-plugins-data-public.searchsource.getserializedfields.md) | | serializes search source fields (which can later be passed to [ISearchStartSearchSource](./kibana-plugin-plugins-data-public.isearchstartsearchsource.md)) | +| [onRequestStart(handler)](./kibana-plugin-plugins-data-public.searchsource.onrequeststart.md) | | Add a handler that will be notified whenever requests start | +| [serialize()](./kibana-plugin-plugins-data-public.searchsource.serialize.md) | | Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object.The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named kibanaSavedObjectMeta.searchSourceJSON.index and kibanaSavedObjectMeta.searchSourceJSON.filter[<number>].meta.index.Using createSearchSource, the instance can be re-created. | +| [setField(field, value)](./kibana-plugin-plugins-data-public.searchsource.setfield.md) | | sets value to a single search source feild | +| [setFields(newFields)](./kibana-plugin-plugins-data-public.searchsource.setfields.md) | | Internal, do not use. Overrides all search source fields with the new field array. | +| [setParent(parent, options)](./kibana-plugin-plugins-data-public.searchsource.setparent.md) | | Set a searchSource that this source should inherit from | +| [setPreferredSearchStrategyId(searchStrategyId)](./kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md) | | internal, dont use | + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.onrequeststart.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.onrequeststart.md new file mode 100644 index 0000000000000..a9386ddae44e1 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.onrequeststart.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [onRequestStart](./kibana-plugin-plugins-data-public.searchsource.onrequeststart.md) + +## SearchSource.onRequestStart() method + +Add a handler that will be notified whenever requests start + +Signature: + +```typescript +onRequestStart(handler: (searchSource: SearchSource, options?: ISearchOptions) => Promise): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| handler | (searchSource: SearchSource, options?: ISearchOptions) => Promise<unknown> | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md new file mode 100644 index 0000000000000..73ba8eb66040b --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.serialize.md @@ -0,0 +1,27 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [serialize](./kibana-plugin-plugins-data-public.searchsource.serialize.md) + +## SearchSource.serialize() method + +Serializes the instance to a JSON string and a set of referenced objects. Use this method to get a representation of the search source which can be stored in a saved object. + +The references returned by this function can be mixed with other references in the same object, however make sure there are no name-collisions. The references will be named `kibanaSavedObjectMeta.searchSourceJSON.index` and `kibanaSavedObjectMeta.searchSourceJSON.filter[].meta.index`. + +Using `createSearchSource`, the instance can be re-created. + +Signature: + +```typescript +serialize(): { + searchSourceJSON: string; + references: import("../../../../../core/public").SavedObjectReference[]; + }; +``` +Returns: + +`{ + searchSourceJSON: string; + references: import("../../../../../core/public").SavedObjectReference[]; + }` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfield.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfield.md new file mode 100644 index 0000000000000..22619940f1589 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfield.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [setField](./kibana-plugin-plugins-data-public.searchsource.setfield.md) + +## SearchSource.setField() method + +sets value to a single search source feild + +Signature: + +```typescript +setField(field: K, value: SearchSourceFields[K]): this; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| field | K | | +| value | SearchSourceFields[K] | | + +Returns: + +`this` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfields.md new file mode 100644 index 0000000000000..f92ffc0fc991d --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setfields.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [setFields](./kibana-plugin-plugins-data-public.searchsource.setfields.md) + +## SearchSource.setFields() method + +Internal, do not use. Overrides all search source fields with the new field array. + + +Signature: + +```typescript +setFields(newFields: SearchSourceFields): this; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| newFields | SearchSourceFields | | + +Returns: + +`this` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setparent.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setparent.md new file mode 100644 index 0000000000000..19bf10bec210f --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setparent.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [setParent](./kibana-plugin-plugins-data-public.searchsource.setparent.md) + +## SearchSource.setParent() method + +Set a searchSource that this source should inherit from + +Signature: + +```typescript +setParent(parent?: ISearchSource, options?: SearchSourceOptions): this; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| parent | ISearchSource | | +| options | SearchSourceOptions | | + +Returns: + +`this` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md new file mode 100644 index 0000000000000..e3261873ba104 --- /dev/null +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchSource](./kibana-plugin-plugins-data-public.searchsource.md) > [setPreferredSearchStrategyId](./kibana-plugin-plugins-data-public.searchsource.setpreferredsearchstrategyid.md) + +## SearchSource.setPreferredSearchStrategyId() method + +internal, dont use + +Signature: + +```typescript +setPreferredSearchStrategyId(searchStrategyId: string): void; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| searchStrategyId | string | | + +Returns: + +`void` + diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.aggs.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.aggs.md index 743646708b4c6..f6bab8e424857 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.aggs.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.aggs.md @@ -4,6 +4,8 @@ ## SearchSourceFields.aggs property +[AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.filter.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.filter.md index a14d33420a22d..5fd615cc647d2 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.filter.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.filter.md @@ -4,6 +4,8 @@ ## SearchSourceFields.filter property +[Filter](./kibana-plugin-plugins-data-public.filter.md) + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.index.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.index.md index fa1d1a552a560..cf1b1cfa253fd 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.index.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.index.md @@ -4,6 +4,7 @@ ## SearchSourceFields.index property + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md index 7a64af0f8b2b8..d19f1da439cee 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.md @@ -4,6 +4,8 @@ ## SearchSourceFields interface +search source fields + Signature: ```typescript @@ -14,17 +16,17 @@ export interface SearchSourceFields | Property | Type | Description | | --- | --- | --- | -| [aggs](./kibana-plugin-plugins-data-public.searchsourcefields.aggs.md) | any | | +| [aggs](./kibana-plugin-plugins-data-public.searchsourcefields.aggs.md) | any | [AggConfigs](./kibana-plugin-plugins-data-public.aggconfigs.md) | | [fields](./kibana-plugin-plugins-data-public.searchsourcefields.fields.md) | NameList | | -| [filter](./kibana-plugin-plugins-data-public.searchsourcefields.filter.md) | Filter[] | Filter | (() => Filter[] | Filter | undefined) | | +| [filter](./kibana-plugin-plugins-data-public.searchsourcefields.filter.md) | Filter[] | Filter | (() => Filter[] | Filter | undefined) | [Filter](./kibana-plugin-plugins-data-public.filter.md) | | [from](./kibana-plugin-plugins-data-public.searchsourcefields.from.md) | number | | | [highlight](./kibana-plugin-plugins-data-public.searchsourcefields.highlight.md) | any | | | [highlightAll](./kibana-plugin-plugins-data-public.searchsourcefields.highlightall.md) | boolean | | | [index](./kibana-plugin-plugins-data-public.searchsourcefields.index.md) | IndexPattern | | -| [query](./kibana-plugin-plugins-data-public.searchsourcefields.query.md) | Query | | +| [query](./kibana-plugin-plugins-data-public.searchsourcefields.query.md) | Query | [Query](./kibana-plugin-plugins-data-public.query.md) | | [searchAfter](./kibana-plugin-plugins-data-public.searchsourcefields.searchafter.md) | EsQuerySearchAfter | | | [size](./kibana-plugin-plugins-data-public.searchsourcefields.size.md) | number | | -| [sort](./kibana-plugin-plugins-data-public.searchsourcefields.sort.md) | EsQuerySortValue | EsQuerySortValue[] | | +| [sort](./kibana-plugin-plugins-data-public.searchsourcefields.sort.md) | EsQuerySortValue | EsQuerySortValue[] | [EsQuerySortValue](./kibana-plugin-plugins-data-public.esquerysortvalue.md) | | [source](./kibana-plugin-plugins-data-public.searchsourcefields.source.md) | NameList | | | [terminate\_after](./kibana-plugin-plugins-data-public.searchsourcefields.terminate_after.md) | number | | | [timeout](./kibana-plugin-plugins-data-public.searchsourcefields.timeout.md) | string | | diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.query.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.query.md index 687dafce798d1..661ce94a06afb 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.query.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.query.md @@ -4,6 +4,8 @@ ## SearchSourceFields.query property +[Query](./kibana-plugin-plugins-data-public.query.md) + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.sort.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.sort.md index c10f556cef6d6..32f513378e35e 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.sort.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchsourcefields.sort.md @@ -4,6 +4,8 @@ ## SearchSourceFields.sort property +[EsQuerySortValue](./kibana-plugin-plugins-data-public.esquerysortvalue.md) + Signature: ```typescript diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md index e515c3513df6c..6ed20beb396f1 100644 --- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md +++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.ui_settings.md @@ -20,6 +20,7 @@ UI_SETTINGS: { readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests"; readonly COURIER_BATCH_SEARCHES: "courier:batchSearches"; readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen"; + readonly SEARCH_TIMEOUT: "search:timeout"; readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget"; readonly HISTOGRAM_MAX_BARS: "histogram:maxBars"; readonly HISTORY_LIMIT: "history:limit"; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getdefaultsearchparams.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getdefaultsearchparams.md index 9de005c1fd0dd..e718ca42ca30f 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getdefaultsearchparams.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getdefaultsearchparams.md @@ -7,24 +7,26 @@ Signature: ```typescript -export declare function getDefaultSearchParams(config: SharedGlobalConfig): { - timeout: string; +export declare function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient): Promise<{ + maxConcurrentShardRequests: number | undefined; + ignoreThrottled: boolean; ignoreUnavailable: boolean; - restTotalHitsAsInt: boolean; -}; + trackTotalHits: boolean; +}>; ``` ## Parameters | Parameter | Type | Description | | --- | --- | --- | -| config | SharedGlobalConfig | | +| uiSettingsClient | IUiSettingsClient | | Returns: -`{ - timeout: string; +`Promise<{ + maxConcurrentShardRequests: number | undefined; + ignoreThrottled: boolean; ignoreUnavailable: boolean; - restTotalHitsAsInt: boolean; -}` + trackTotalHits: boolean; +}>` diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getshardtimeout.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getshardtimeout.md new file mode 100644 index 0000000000000..d7e2a597ff33d --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.getshardtimeout.md @@ -0,0 +1,30 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [getShardTimeout](./kibana-plugin-plugins-data-server.getshardtimeout.md) + +## getShardTimeout() function + +Signature: + +```typescript +export declare function getShardTimeout(config: SharedGlobalConfig): { + timeout: string; +} | { + timeout?: undefined; +}; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| config | SharedGlobalConfig | | + +Returns: + +`{ + timeout: string; +} | { + timeout?: undefined; +}` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md index 70c32adeab9fd..f5b587d86b349 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.md @@ -26,11 +26,13 @@ | Function | Description | | --- | --- | -| [getDefaultSearchParams(config)](./kibana-plugin-plugins-data-server.getdefaultsearchparams.md) | | +| [getDefaultSearchParams(uiSettingsClient)](./kibana-plugin-plugins-data-server.getdefaultsearchparams.md) | | +| [getShardTimeout(config)](./kibana-plugin-plugins-data-server.getshardtimeout.md) | | | [getTime(indexPattern, timeRange, options)](./kibana-plugin-plugins-data-server.gettime.md) | | | [parseInterval(interval)](./kibana-plugin-plugins-data-server.parseinterval.md) | | | [plugin(initializerContext)](./kibana-plugin-plugins-data-server.plugin.md) | Static code to be shared externally | | [shouldReadFieldFromDocValues(aggregatable, esType)](./kibana-plugin-plugins-data-server.shouldreadfieldfromdocvalues.md) | | +| [toSnakeCase(obj)](./kibana-plugin-plugins-data-server.tosnakecase.md) | | | [usageProvider(core)](./kibana-plugin-plugins-data-server.usageprovider.md) | | ## Interfaces diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md index 2d9104ef894bc..455c5ecdd8195 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.plugin.start.md @@ -8,7 +8,7 @@ ```typescript start(core: CoreStart): { - search: ISearchStart>; + search: ISearchStart>; fieldFormats: { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; @@ -27,7 +27,7 @@ start(core: CoreStart): { Returns: `{ - search: ISearchStart>; + search: ISearchStart>; fieldFormats: { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tosnakecase.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tosnakecase.md new file mode 100644 index 0000000000000..eda9e9c312e59 --- /dev/null +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.tosnakecase.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [toSnakeCase](./kibana-plugin-plugins-data-server.tosnakecase.md) + +## toSnakeCase() function + +Signature: + +```typescript +export declare function toSnakeCase(obj: Record): import("lodash").Dictionary; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| obj | Record<string, any> | | + +Returns: + +`import("lodash").Dictionary` + diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md index e419b64cd43aa..2d4ce75b956df 100644 --- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md +++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.ui_settings.md @@ -20,6 +20,7 @@ UI_SETTINGS: { readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests"; readonly COURIER_BATCH_SEARCHES: "courier:batchSearches"; readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen"; + readonly SEARCH_TIMEOUT: "search:timeout"; readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget"; readonly HISTOGRAM_MAX_BARS: "histogram:maxBars"; readonly HISTORY_LIMIT: "history:limit"; diff --git a/docs/management/advanced-options.asciidoc b/docs/management/advanced-options.asciidoc index a64a0330ae43f..ed20166c87f29 100644 --- a/docs/management/advanced-options.asciidoc +++ b/docs/management/advanced-options.asciidoc @@ -225,6 +225,7 @@ be inconsistent because different shards might be in different refresh states. `search:includeFrozen`:: Includes {ref}/frozen-indices.html[frozen indices] in results. Searching through frozen indices might increase the search time. This setting is off by default. Users must opt-in to include frozen indices. +`search:timeout`:: Change the maximum timeout for a search session or set to 0 to disable the timeout and allow queries to run to completion. [float] [[kibana-siem-settings]] diff --git a/docs/management/index-lifecycle-policies/manage-policy.asciidoc b/docs/management/index-lifecycle-policies/manage-policy.asciidoc index a57af8a33494b..8e2dc96de4b99 100644 --- a/docs/management/index-lifecycle-policies/manage-policy.asciidoc +++ b/docs/management/index-lifecycle-policies/manage-policy.asciidoc @@ -25,4 +25,10 @@ created index. For more information, see {ref}/indices-templates.html[Index temp * *Delete a policy.* You can’t delete a policy that is currently in use or recover a deleted index. +[float] +=== Required permissions + +The `manage_ilm` cluster privilege is required to access *Index lifecycle policies*. + +You can add these privileges in *Stack Management > Security > Roles*. diff --git a/docs/management/managing-ccr.asciidoc b/docs/management/managing-ccr.asciidoc index 67193b3b5a037..9c06e479e28b2 100644 --- a/docs/management/managing-ccr.asciidoc +++ b/docs/management/managing-ccr.asciidoc @@ -20,6 +20,13 @@ image::images/cross-cluster-replication-list-view.png[][Cross-cluster replicatio * The Elasticsearch version of the local cluster must be the same as or newer than the remote cluster. Refer to {ref}/ccr-overview.html[this document] for more information. +[float] +=== Required permissions + +The `manage` and `manage_ccr` cluster privileges are required to access *Cross-Cluster Replication*. + +You can add these privileges in *Stack Management > Security > Roles*. + [float] [[configure-replication]] === Configure replication diff --git a/docs/management/managing-licenses.asciidoc b/docs/management/managing-licenses.asciidoc index 25ae29036f656..b53bda95466dc 100644 --- a/docs/management/managing-licenses.asciidoc +++ b/docs/management/managing-licenses.asciidoc @@ -29,6 +29,13 @@ See {ref}/encrypting-communications.html[Encrypting communications]. {kib} and the {ref}/start-basic.html[start basic API] provide a list of all of the features that will no longer be supported if you revert to a basic license. +[float] +=== Required permissions + +The `manage` cluster privilege is required to access *License Management*. + +You can add this privilege in *Stack Management > Security > Roles*. + [discrete] [[update-license]] === Update your license diff --git a/docs/management/managing-remote-clusters.asciidoc b/docs/management/managing-remote-clusters.asciidoc index 83895838efec6..92e0fa822b056 100644 --- a/docs/management/managing-remote-clusters.asciidoc +++ b/docs/management/managing-remote-clusters.asciidoc @@ -11,6 +11,13 @@ To get started, open the menu, then go to *Stack Management > Data > Remote Clus [role="screenshot"] image::images/remote-clusters-list-view.png[Remote Clusters list view, including Add a remote cluster button] +[float] +=== Required permissions + +The `manage` cluster privilege is required to access *Remote Clusters*. + +You can add this privilege in *Stack Management > Security > Roles*. + [float] [[managing-remote-clusters]] === Add a remote cluster diff --git a/docs/management/rollups/create_and_manage_rollups.asciidoc b/docs/management/rollups/create_and_manage_rollups.asciidoc index 8aa57f50fe94b..e20f384b5ed18 100644 --- a/docs/management/rollups/create_and_manage_rollups.asciidoc +++ b/docs/management/rollups/create_and_manage_rollups.asciidoc @@ -20,6 +20,13 @@ image::images/management_rollup_list.png[][List of currently active rollup jobs] Before using this feature, you should be familiar with how rollups work. {ref}/xpack-rollup.html[Rolling up historical data] is a good source for more detailed information. +[float] +=== Required permissions + +The `manage_rollup` cluster privilege is required to access *Rollup jobs*. + +You can add this privilege in *Stack Management > Security > Roles*. + [float] [[create-and-manage-rollup-job]] === Create a rollup job diff --git a/docs/management/upgrade-assistant/index.asciidoc b/docs/management/upgrade-assistant/index.asciidoc index c5fd6a3a555a1..2b8c2da2ef577 100644 --- a/docs/management/upgrade-assistant/index.asciidoc +++ b/docs/management/upgrade-assistant/index.asciidoc @@ -13,6 +13,14 @@ Before you upgrade, make sure that you are using the latest released minor version of {es} to see the most up-to-date deprecation issues. For example, if you want to upgrade to to 7.0, make sure that you are using 6.8. +[float] +=== Required permissions + +The `manage` cluster privilege is required to access the *Upgrade assistant*. +Additional privileges may be needed to perform certain actions. + +You can add this privilege in *Stack Management > Security > Roles*. + [float] === Reindexing diff --git a/docs/migration/migrate_8_0.asciidoc b/docs/migration/migrate_8_0.asciidoc index b80503750a26e..0cb28ce0fb6e7 100644 --- a/docs/migration/migrate_8_0.asciidoc +++ b/docs/migration/migrate_8_0.asciidoc @@ -115,7 +115,7 @@ URL that it derived from the actual server address and `xpack.security.public` s *Impact:* Any workflow that involved manually clearing generated bundles will have to be updated with the new path. -[float]] +[float] === kibana.keystore has moved from the `data` folder to the `config` folder *Details:* By default, kibana.keystore has moved from the configured `path.data` folder to `/config` for archive distributions and `/etc/kibana` for package distributions. If a pre-existing keystore exists in the data directory that path will continue to be used. @@ -136,6 +136,18 @@ custom roles with {kibana-ref}/kibana-privileges.html[{kib} privileges]. instead be assigned the `kibana_admin` role to maintain their current access level. +[float] +=== `kibana_dashboard_only_user` role has been removed. + +*Details:* The `kibana_dashboard_only_user` role has been removed. +If you wish to restrict access to just the Dashboard feature, create +custom roles with {kibana-ref}/kibana-privileges.html[{kib} privileges]. + +*Impact:* Any users currently assigned the `kibana_dashboard_only_user` role will need to be assigned a custom role which only grants access to the Dashboard feature. + +Granting additional cluster or index privileges may enable certain +**Stack Monitoring** features. + [float] [[breaking_80_reporting_changes]] === Reporting changes diff --git a/docs/setup/settings.asciidoc b/docs/setup/settings.asciidoc index 4a931aabd3646..f03022e9e9f00 100644 --- a/docs/setup/settings.asciidoc +++ b/docs/setup/settings.asciidoc @@ -20,12 +20,12 @@ which may cause a delay before pages start being served. Set to `false` to disable Console. *Default: `true`* | `cpu.cgroup.path.override:` - | Override for cgroup cpu path when mounted in a -manner that is inconsistent with `/proc/self/cgroup`. + | *deprecated* This setting has been renamed to `ops.cGroupOverrides.cpuPath` +and the old name will no longer be supported as of 8.0. | `cpuacct.cgroup.path.override:` - | Override for cgroup cpuacct path when mounted -in a manner that is inconsistent with `/proc/self/cgroup`. + | *deprecated* This setting has been renamed to `ops.cGroupOverrides.cpuAcctPath` +and the old name will no longer be supported as of 8.0. | `csp.rules:` | A https://w3c.github.io/webappsec-csp/[content-security-policy] template @@ -438,6 +438,14 @@ not saved in {es}. *Default: `data`* | Set the interval in milliseconds to sample system and process performance metrics. The minimum value is 100. *Default: `5000`* +| `ops.cGroupOverrides.cpuPath:` + | Override for cgroup cpu path when mounted in a +manner that is inconsistent with `/proc/self/cgroup`. + +| `ops.cGroupOverrides.cpuAcctPath:` + | Override for cgroup cpuacct path when mounted +in a manner that is inconsistent with `/proc/self/cgroup`. + | `server.basePath:` | Enables you to specify a path to mount {kib} at if you are running behind a proxy. Use the `server.rewriteBasePath` setting to tell {kib} diff --git a/docs/user/dashboard/dashboard-drilldown.asciidoc b/docs/user/dashboard/dashboard-drilldown.asciidoc new file mode 100644 index 0000000000000..84701cae2ecc6 --- /dev/null +++ b/docs/user/dashboard/dashboard-drilldown.asciidoc @@ -0,0 +1,76 @@ +[[dashboard-drilldown]] +=== Dashboard drilldown + +The dashboard drilldown allows you to navigate from one dashboard to another dashboard. +For example, you might have a dashboard that shows the overall status of multiple data centers. +You can create a drilldown that navigates from this dashboard to a dashboard +that shows a single data center or server. + +This example shows a dashboard panel that contains a pie chart with a configured dashboard drilldown: + +[role="screenshot"] +image::images/drilldown_on_piechart.gif[Drilldown on pie chart that navigates to another dashboard] + +[float] +[[drilldowns-example]] +==== Try it: Create a dashboard drilldown + +Create the *Host Overview* drilldown shown above. + +*Set up the dashboards* + +. Add the <> data set. + +. Create a new dashboard, called `Host Overview`, and include these visualizations +from the sample data set: ++ +[%hardbreaks] +*[Logs] Heatmap* +*[Logs] Visitors by OS* +*[Logs] Host, Visits, and Bytes Table* +*[Logs] Total Requests and Bytes* ++ +TIP: If you don’t see data for a panel, try changing the time range. + +. Open the *[Logs] Web traffic* dashboard. + +. Set a search and filter. ++ +[%hardbreaks] +Search: `extension.keyword:( “gz” or “css” or “deb”)` +Filter: `geo.src : CN` + + +*Create the drilldown* + + +. In the dashboard menu bar, click *Edit*. + +. In *[Logs] Visitors by OS*, open the panel menu, and then select *Create drilldown*. + +. Pick *Go to dashboard* action. + +. Give the drilldown a name. + +. Select *Host Overview* as the destination dashboard. + +. Keep both filters enabled so that the drilldown carries over the global filters and date range. ++ +Your input should look similar to this: ++ +[role="screenshot"] +image::images/drilldown_create.png[Create drilldown with entries for drilldown name and destination] + +. Click *Create drilldown.* + +. Save the dashboard. ++ +If you don’t save the drilldown, and then navigate away, the drilldown is lost. + +. In *[Logs] Visitors by OS*, click the `win 8` slice of the pie, and then select the name of your drilldown. ++ +[role="screenshot"] +image::images/drilldown_on_panel.png[Drilldown on pie chart that navigates to another dashboard] ++ +You are navigated to your destination dashboard. Verify that the search query, filters, +and time range are carried over. diff --git a/docs/user/dashboard/dashboard.asciidoc b/docs/user/dashboard/dashboard.asciidoc index 0c0151cc3ace2..c8bff91be91a6 100644 --- a/docs/user/dashboard/dashboard.asciidoc +++ b/docs/user/dashboard/dashboard.asciidoc @@ -4,9 +4,9 @@ [partintro] -- -A _dashboard_ is a collection of panels that you use to analyze your data. On a dashboard, you can add a variety of panels that -you can rearrange and tell a story about your data. Panels contain everything you need, including visualizations, -interactive controls, markdown, and more. +A _dashboard_ is a collection of panels that you use to analyze your data. On a dashboard, you can add a variety of panels that +you can rearrange and tell a story about your data. Panels contain everything you need, including visualizations, +interactive controls, markdown, and more. With *Dashboard*s, you can: @@ -18,7 +18,7 @@ With *Dashboard*s, you can: * Create and apply filters to focus on the data you want to display. -* Control who can use your data, and share the dashboard with a small or large audience. +* Control who can use your data, and share the dashboard with a small or large audience. * Generate reports based on your findings. @@ -42,7 +42,7 @@ image::images/dashboard-read-only-badge.png[Example of Dashboard read only acces [[types-of-panels]] == Types of panels -Panels contain everything you need to tell a story about you data, including visualizations, +Panels contain everything you need to tell a story about you data, including visualizations, interactive controls, Markdown, and more. [cols="50, 50"] @@ -50,30 +50,30 @@ interactive controls, Markdown, and more. a| *Area* -Displays data points, connected by a line, where the area between the line and axes are shaded. +Displays data points, connected by a line, where the area between the line and axes are shaded. Use area charts to compare two or more categories over time, and display the magnitude of trends. | image:images/area.png[Area chart] a| *Stacked area* -Displays the evolution of the value of several data groups. The values of each group are displayed -on top of each other. Use stacked area charts to visualize part-to-whole relationships, and to show +Displays the evolution of the value of several data groups. The values of each group are displayed +on top of each other. Use stacked area charts to visualize part-to-whole relationships, and to show how each category contributes to the cumulative total. | image:images/stacked_area.png[Stacked area chart] a| *Bar* -Displays bars side-by-side where each bar represents a category. Use bar charts to compare data across a -large number of categories, display data that includes categories with negative values, and easily identify +Displays bars side-by-side where each bar represents a category. Use bar charts to compare data across a +large number of categories, display data that includes categories with negative values, and easily identify the categories that represent the highest and lowest values. Kibana also supports horizontal bar charts. | image:images/bar.png[Bar chart] a| *Stacked bar* -Displays numeric values across two or more categories. Use stacked bar charts to compare numeric values between +Displays numeric values across two or more categories. Use stacked bar charts to compare numeric values between levels of a categorical value. Kibana also supports stacked horizontal bar charts. | image:images/stacked_bar.png[Stacked area chart] @@ -81,15 +81,15 @@ levels of a categorical value. Kibana also supports stacked horizontal bar chart a| *Line* -Displays data points that are connected by a line. Use line charts to visualize a sequence of values, discover +Displays data points that are connected by a line. Use line charts to visualize a sequence of values, discover trends over time, and forecast future values. | image:images/line.png[Line chart] a| *Pie* -Displays slices that represent a data category, where the slice size is proportional to the quantity it represents. -Use pie charts to show comparisons between multiple categories, illustrate the dominance of one category over others, +Displays slices that represent a data category, where the slice size is proportional to the quantity it represents. +Use pie charts to show comparisons between multiple categories, illustrate the dominance of one category over others, and show percentage or proportional data. | image:images/pie.png[Pie chart] @@ -103,7 +103,7 @@ Similar to the pie chart, but the central circle is removed. Use donut charts wh a| *Tree map* -Relates different segments of your data to the whole. Each rectangle is subdivided into smaller rectangles, or sub branches, based on +Relates different segments of your data to the whole. Each rectangle is subdivided into smaller rectangles, or sub branches, based on its proportion to the whole. Use treemaps to make efficient use of space to show percent total for each category. | image:images/treemap.png[Tree map] @@ -111,7 +111,7 @@ its proportion to the whole. Use treemaps to make efficient use of space to show a| *Heat map* -Displays graphical representations of data where the individual values are represented by colors. Use heat maps when your data set includes +Displays graphical representations of data where the individual values are represented by colors. Use heat maps when your data set includes categorical data. For example, use a heat map to see the flights of origin countries compared to destination countries using the sample flight data. | image:images/heat_map.png[Heat map] @@ -125,7 +125,7 @@ Displays how your metric progresses toward a fixed goal. Use the goal to display a| *Gauge* -Displays your data along a scale that changes color according to where your data falls on the expected scale. Use the gauge to show how metric +Displays your data along a scale that changes color according to where your data falls on the expected scale. Use the gauge to show how metric values relate to reference threshold values, or determine how a specified field is performing versus how it is expected to perform. | image:images/gauge.png[Gauge] @@ -133,7 +133,7 @@ values relate to reference threshold values, or determine how a specified field a| *Metric* -Displays a single numeric value for an aggregation. Use the metric visualization when you have a numeric value that is powerful enough to tell +Displays a single numeric value for an aggregation. Use the metric visualization when you have a numeric value that is powerful enough to tell a story about your data. | image:images/metric.png[Metric] @@ -141,7 +141,7 @@ a story about your data. a| *Data table* -Displays your raw data or aggregation results in a tabular format. Use data tables to display server configuration details, track counts, min, +Displays your raw data or aggregation results in a tabular format. Use data tables to display server configuration details, track counts, min, or max values for a specific field, and monitor the status of key services. | image:images/data_table.png[Data table] @@ -149,7 +149,7 @@ or max values for a specific field, and monitor the status of key services. a| *Tag cloud* -Graphical representations of how frequently a word appears in the source text. Use tag clouds to easily produce a summary of large documents and +Graphical representations of how frequently a word appears in the source text. Use tag clouds to easily produce a summary of large documents and create visual art for a specific topic. | image:images/tag_cloud.png[Tag cloud] @@ -168,16 +168,16 @@ For all your mapping needs, use <>. [[create-panels]] == Create panels -To create a panel, make sure you have {ref}/getting-started-index.html[data indexed into {es}] and an <> -to retrieve the data from {es}. If you aren’t ready to use your own data, {kib} comes with several pre-built dashboards that you can test out. For more information, +To create a panel, make sure you have {ref}/getting-started-index.html[data indexed into {es}] and an <> +to retrieve the data from {es}. If you aren’t ready to use your own data, {kib} comes with several pre-built dashboards that you can test out. For more information, refer to <>. -To begin, click *Create new*, then choose one of the following options on the +To begin, click *Create new*, then choose one of the following options on the *New Visualization* window: -* Click on the type of panel you want to create, then configure the options. +* Click on the type of panel you want to create, then configure the options. -* Select an editor to help you create the panel. +* Select an editor to help you create the panel. [role="screenshot"] image:images/Dashboard_add_new_visualization.png[Example add new visualization to dashboard] @@ -188,19 +188,19 @@ image:images/Dashboard_add_new_visualization.png[Example add new visualization t [[lens]] === Create panels with Lens -*Lens* is the simplest and fastest way to create powerful visualizations of your data. To use *Lens*, you drag and drop as many data fields +*Lens* is the simplest and fastest way to create powerful visualizations of your data. To use *Lens*, you drag and drop as many data fields as you want onto the visualization builder pane, and *Lens* uses heuristics to decide how to apply each field to the visualization. With *Lens*, you can: * Use the automatically generated suggestions to change the visualization type. -* Create visualizations with multiple layers and indices. +* Create visualizations with multiple layers and indices. * Change the aggregation and labels to customize the data. [role="screenshot"] image::images/lens_drag_drop.gif[Drag and drop] -TIP: Drag-and-drop capabilities are available only when *Lens* knows how to use the data. If *Lens* is unable to automatically generate a +TIP: Drag-and-drop capabilities are available only when *Lens* knows how to use the data. If *Lens* is unable to automatically generate a visualization, configure the customization options for your visualization. [float] @@ -220,7 +220,7 @@ To filter the data fields: [[view-data-summaries]] ==== View data summaries -To help you decide exactly the data you want to display, get a quick summary of each field. The summary shows the distribution of +To help you decide exactly the data you want to display, get a quick summary of each field. The summary shows the distribution of values within the specified time range. To view the data field summary information, navigate to the field, then click *i*. @@ -250,10 +250,10 @@ When there is an exclamation point (!) next to a visualization type, *Lens* is u [[customize-the-data]] ==== Customize the data -For each visualization type, you can customize the aggregation and labels. The options available depend on the selected visualization type. +For each visualization type, you can customize the aggregation and labels. The options available depend on the selected visualization type. . Click a data field name in the editor, or click *Drop a field here*. -. Change the options that appear. +. Change the options that appear. + [role="screenshot"] image::images/lens_aggregation_labels.png[Quick function options] @@ -262,7 +262,7 @@ image::images/lens_aggregation_labels.png[Quick function options] [[add-layers-and-indices]] ==== Add layers and indices -To compare and analyze data from different sources, you can visualize multiple data layers and indices. Multiple layers and indices are +To compare and analyze data from different sources, you can visualize multiple data layers and indices. Multiple layers and indices are supported in area, line, and bar charts. To add a layer, click *+*, then drag and drop the data fields for the new layer. @@ -281,7 +281,7 @@ Ready to try out *Lens*? Refer to the <>. [[tsvb]] === Create panels with TSVB -*TSVB* is a time series data visualizer that allows you to use the full power of the Elasticsearch aggregation framework. To use *TSVB*, +*TSVB* is a time series data visualizer that allows you to use the full power of the Elasticsearch aggregation framework. To use *TSVB*, you can combine an infinite number of <> to display your data. With *TSVB*, you can: @@ -295,15 +295,15 @@ image::images/tsvb.png[TSVB UI] [float] [[configure-the-data]] -==== Configure the data +==== Configure the data -With *TSVB*, you can add and display multiple data sets to compare and analyze. {kib} uses many types of <> that you can use to build +With *TSVB*, you can add and display multiple data sets to compare and analyze. {kib} uses many types of <> that you can use to build complex summaries of that data. . Select *Data*. If you are using *Table*, select *Columns*. -. From the *Aggregation* drop down, select the aggregation you want to visualize. +. From the *Aggregation* drop down, select the aggregation you want to visualize. + -If you don’t see any data, change the <>. +If you don’t see any data, change the <>. + To add multiple aggregations, click *+*. . From the *Group by* drop down, select how you want to group or split the data. @@ -315,14 +315,14 @@ When you have more than one aggregation, the last value is displayed, which is i [[change-the-data-display]] ==== Change the data display -To find the best way to display your data, *TSVB* supports several types of panels and charts. +To find the best way to display your data, *TSVB* supports several types of panels and charts. To change the *Time Series* chart type: . Click *Data > Options*. . Select the *Chart type*. -To change the panel type, click on the panel options: +To change the panel type, click on the panel options: [role="screenshot"] image::images/tsvb_change_display.gif[TSVB change the panel type] @@ -331,7 +331,7 @@ image::images/tsvb_change_display.gif[TSVB change the panel type] [[custommize-the-data]] ==== Customize the data -View data in a different <>, and change the data label name and colors. The options available depend on the panel type. +View data in a different <>, and change the data label name and colors. The options available depend on the panel type. To change the index pattern, click *Panel options*, then enter the new *Index Pattern*. @@ -361,7 +361,7 @@ image::images/tsvb_annotations.png[TSVB annotations] [[filter-the-panel]] ==== Filter the panel -The data that displays on the panel is based on the <> and <>. +The data that displays on the panel is based on the <> and <>. You can filter the data on the panels using the <>. Click *Panel options*, then enter the syntax in the *Panel Filter* field. @@ -372,7 +372,7 @@ If you want to ignore filters from all of {kib}, select *Yes* for *Ignore global [[vega]] === Create custom panels with Vega -Build custom visualizations using *Vega* and *Vega-Lite*, backed by one or more data sources including {es}, Elastic Map Service, +Build custom visualizations using *Vega* and *Vega-Lite*, backed by one or more data sources including {es}, Elastic Map Service, URL, or static data. Use the {kib} extensions to embed *Vega* in your dashboard, and add interactive tools. Use *Vega* and *Vega-Lite* when you want to create a visualization for: @@ -405,7 +405,7 @@ For more information about *Vega* and *Vega-Lite*, refer to: [[timelion]] === Create panels with Timelion -*Timelion* is a time series data visualizer that enables you to combine independent data sources within a single visualization. +*Timelion* is a time series data visualizer that enables you to combine independent data sources within a single visualization. *Timelion* is driven by a simple expression language that you use to: @@ -422,9 +422,41 @@ Ready to try out Timelion? For step-by-step tutorials, refer to: * <> * <> +[float] +[[timelion-deprecation]] +==== Timelion app deprecation + +Deprecated since 7.0, the Timelion app will be removed in 8.0. If you have any Timelion worksheets, you must migrate them to a dashboard. + +NOTE: Only the Timelion app is deprecated. {kib} continues to support Timelion +visualizations on dashboards and in Visualize and Canvas. + +To migrate a Timelion worksheet to a dashboard: + +. Open the menu, click **Dashboard**, then click **Create dashboard**. + +. On the dashboard, click **Create New**, then select the Timelion visualization. + +. On a new tab, open the Timelion app, select the chart you want to copy, and copy its expression. ++ +[role="screenshot"] +image::images/timelion-copy-expression.png[] + +. Return to the other tab and paste the copied expression to the *Timelion Expression* field and click **Update**. ++ +[role="screenshot"] +image::images/timelion-vis-paste-expression.png[] + +. Save the new visualization, give it a name, and click **Save and Return**. ++ +Your Timelion visualization will appear on the dashboard. Repeat this for all your charts on each worksheet. ++ +[role="screenshot"] +image::images/timelion-dashboard.png[] + [float] [[save-panels]] -=== Save panels +== Save panels When you’ve finished making changes, save the panels. @@ -436,7 +468,7 @@ When you’ve finished making changes, save the panels. [[add-existing-panels]] == Add existing panels -Add panels that you’ve already created to your dashboard. +Add panels that you’ve already created to your dashboard. On the dashboard, click *Add an existing*, then select the panel you want to add. @@ -445,7 +477,7 @@ When a panel contains a stored query, both queries are applied. [role="screenshot"] image:images/Dashboard_add_visualization.png[Example add visualization to dashboard] -To make changes to the panel, put the dashboard in *Edit* mode, then select the edit option from the panel menu. +To make changes to the panel, put the dashboard in *Edit* mode, then select the edit option from the panel menu. The changes you make appear in every dashboard that uses the panel, except if you edit the panel title. Changes to the panel title appear only on the dashboard where you made the change. [float] @@ -463,6 +495,8 @@ include::edit-dashboards.asciidoc[] include::explore-dashboard-data.asciidoc[] +include::drilldowns.asciidoc[] + include::share-dashboards.asciidoc[] include::tutorials.asciidoc[] diff --git a/docs/user/dashboard/drilldowns.asciidoc b/docs/user/dashboard/drilldowns.asciidoc index 5fca974d58135..85230f1b6f70d 100644 --- a/docs/user/dashboard/drilldowns.asciidoc +++ b/docs/user/dashboard/drilldowns.asciidoc @@ -1,106 +1,51 @@ -[float] [[drilldowns]] -=== Use drilldowns for dashboard actions +== Use drilldowns for dashboard actions Drilldowns, also known as custom actions, allow you to configure a workflow for analyzing and troubleshooting your data. -Using a drilldown, you can navigate from one dashboard to another, +For example, using a drilldown, you can navigate from one dashboard to another, taking the current time range, filters, and other parameters with you, so the context remains the same. You can continue your analysis from a new perspective. -For example, you might have a dashboard that shows the overall status of multiple data centers. -You can create a drilldown that navigates from this dashboard to a dashboard -that shows a single data center or server. - -[float] -[[how-drilldowns-work]] -==== How drilldowns work - -Drilldowns are user-configurable {kib} actions that are stored with the -dashboard metadata. Drilldowns are specific to the dashboard panel -for which you create them—they are not shared across panels. -A panel can have multiple drilldowns. - -This example shows a dashboard panel that contains a pie chart. -Typically, clicking a pie slice applies the current filter. -When a panel has a drilldown, clicking a pie slice opens a menu with -the default action and your drilldowns. Refer to the <> -for instructions on how to create this drilldown. - [role="screenshot"] image::images/drilldown_on_piechart.gif[Drilldown on pie chart that navigates to another dashboard] -Third-party developers can create drilldowns. -Refer to https://github.com/elastic/kibana/tree/master/x-pack/examples/ui_actions_enhanced_examples[this example plugin] -to learn how to code drilldowns. - -[float] -[[create-manage-drilldowns]] -==== Create and manage drilldowns - -Your dashboard must be in *Edit* mode to create a drilldown. -Once a panel has at least one drilldown, the menu also includes a *Manage drilldowns* action -for editing and deleting drilldowns. - -[role="screenshot"] -image::images/drilldown_menu.png[Panel menu with Create drilldown and Manage drilldown actions] +Drilldowns are specific to the dashboard panel for which you create them—they are not shared across panels. A panel can have multiple drilldowns. [float] -[[drilldowns-example]] -==== Try it: Create a drilldown - -This example shows how to create the *Host Overview* drilldown shown earlier in this doc. +[[actions]] +=== Drilldown actions -*Set up the dashboards* +Drilldowns are user-configurable {kib} actions that are stored with the dashboard metadata. +Kibana provides the following types of actions: -. Add the <> data set. +[cols="2"] +|=== -. Create a new dashboard, called `Host Overview`, and include these visualizations -from the sample data set: -+ -[%hardbreaks] -*[Logs] Heatmap* -*[Logs] Visitors by OS* -*[Logs] Host, Visits, and Bytes Table* -*[Logs] Total Requests and Bytes* -+ -TIP: If you don’t see data for a panel, try changing the time range. +a| <> -. Open the *[Logs] Web traffic* dashboard. +| Navigate to a dashboard. -. Set a search and filter. -+ -[%hardbreaks] -Search: `extension.keyword:( “gz” or “css” or “deb”)` -Filter: `geo.src : CN` +a| <> -*Create the drilldown* +| Navigate to external or internal URL. -. In the dashboard menu bar, click *Edit*. +|=== -. In *[Logs] Visitors by OS*, open the panel menu, and then select *Create drilldown*. +[NOTE] +============================================== +Some action types are paid commercial features, while others are free. +For a comparison of the Elastic subscription levels, +see https://www.elastic.co/subscriptions[the subscription page]. +============================================== -. Give the drilldown a name. - -. Select *Host Overview* as the destination dashboard. - -. Keep both filters enabled so that the drilldown carries over the global filters and date range. -+ -Your input should look similar to this: -+ -[role="screenshot"] -image::images/drilldown_create.png[Create drilldown with entries for drilldown name and destination] - -. Click *Create drilldown.* +[float] +[[code-drilldowns]] +=== Code drilldowns +Third-party developers can create drilldowns. +Refer to {kib-repo}blob/{branch}/x-pack/examples/ui_actions_enhanced_examples[this example plugin] +to learn how to code drilldowns. -. Save the dashboard. -+ -If you don’t save the drilldown, and then navigate away, the drilldown is lost. +include::dashboard-drilldown.asciidoc[] +include::url-drilldown.asciidoc[] -. In *[Logs] Visitors by OS*, click the `win 8` slice of the pie, and then select the name of your drilldown. -+ -[role="screenshot"] -image::images/drilldown_on_panel.png[Drilldown on pie chart that navigates to another dashboard] -+ -You are navigated to your destination dashboard. Verify that the search query, filters, -and time range are carried over. diff --git a/docs/user/dashboard/explore-dashboard-data.asciidoc b/docs/user/dashboard/explore-dashboard-data.asciidoc index a0564f5bceb3d..238dfb79e900b 100644 --- a/docs/user/dashboard/explore-dashboard-data.asciidoc +++ b/docs/user/dashboard/explore-dashboard-data.asciidoc @@ -16,5 +16,3 @@ The data that displays depends on the element that you inspect. image:images/Dashboard_inspect.png[Inspect in dashboard] include::explore-underlying-data.asciidoc[] - -include::drilldowns.asciidoc[] diff --git a/docs/user/dashboard/images/drilldown_pick_an_action.png b/docs/user/dashboard/images/drilldown_pick_an_action.png new file mode 100644 index 0000000000000..c99e931e3fbe1 Binary files /dev/null and b/docs/user/dashboard/images/drilldown_pick_an_action.png differ diff --git a/docs/user/dashboard/images/url_drilldown_github.png b/docs/user/dashboard/images/url_drilldown_github.png new file mode 100644 index 0000000000000..d2eaec311948e Binary files /dev/null and b/docs/user/dashboard/images/url_drilldown_github.png differ diff --git a/docs/user/dashboard/images/url_drilldown_go_to_github.gif b/docs/user/dashboard/images/url_drilldown_go_to_github.gif new file mode 100644 index 0000000000000..7cca3f72d5a68 Binary files /dev/null and b/docs/user/dashboard/images/url_drilldown_go_to_github.gif differ diff --git a/docs/user/dashboard/images/url_drilldown_pick_an_action.png b/docs/user/dashboard/images/url_drilldown_pick_an_action.png new file mode 100644 index 0000000000000..c99e931e3fbe1 Binary files /dev/null and b/docs/user/dashboard/images/url_drilldown_pick_an_action.png differ diff --git a/docs/user/dashboard/images/url_drilldown_popup.png b/docs/user/dashboard/images/url_drilldown_popup.png new file mode 100644 index 0000000000000..392edd16ea328 Binary files /dev/null and b/docs/user/dashboard/images/url_drilldown_popup.png differ diff --git a/docs/user/dashboard/images/url_drilldown_trigger_picker.png b/docs/user/dashboard/images/url_drilldown_trigger_picker.png new file mode 100644 index 0000000000000..2fe930f35dce8 Binary files /dev/null and b/docs/user/dashboard/images/url_drilldown_trigger_picker.png differ diff --git a/docs/user/dashboard/images/url_drilldown_url_template.png b/docs/user/dashboard/images/url_drilldown_url_template.png new file mode 100644 index 0000000000000..d8515afe66a80 Binary files /dev/null and b/docs/user/dashboard/images/url_drilldown_url_template.png differ diff --git a/docs/user/dashboard/url-drilldown.asciidoc b/docs/user/dashboard/url-drilldown.asciidoc new file mode 100644 index 0000000000000..16f82477756b7 --- /dev/null +++ b/docs/user/dashboard/url-drilldown.asciidoc @@ -0,0 +1,221 @@ +[[url-drilldown]] +=== URL drilldown + +The URL drilldown allows you to navigate from a dashboard to an internal or external URL. +The destination URL can be dynamic, depending on the dashboard context or user’s interaction with a visualization. + +For example, you might have a dashboard that shows data from a Github repository. +You can create a drilldown that navigates from this dashboard to Github. + +[role="screenshot"] +image:images/url_drilldown_go_to_github.gif[Drilldown on pie chart that navigates to Github] + +NOTE: URL drilldown is available with the https://www.elastic.co/subscriptions[Gold subscription] and higher. + +[float] +[[try-it]] +==== Try it: Create a URL drilldown + +This example shows how to create the "Show on Github" drilldown shown above. + +. Add the <> data set. +. Open the *[Logs] Web traffic* dashboard. This isn’t data from Github, but it should work for demonstration purposes. +. In the dashboard menu bar, click *Edit*. +. In *[Logs] Visitors by OS*, open the panel menu, and then select *Create drilldown*. +. Give the drilldown a name: *Show on Github*. +. Select a drilldown action: *Go to URL*. ++ +[role="screenshot"] +image:images/url_drilldown_pick_an_action.png[Action picker] +. Enter a URL template: ++ +[source, bash] +---- +https://github.com/elastic/kibana/issues?q=is:issue+is:open+{{event.value}} +---- ++ +This example URL navigates to {kib} issues on Github. `{{event.value}}` will be substituted with a value associated with a clicked pie slice. In _preview_ `{{event.value}}` is substituted with a <> value. +[role="screenshot"] +image:images/url_drilldown_url_template.png[URL template input] +. Click *Create drilldown*. +. Save the dashboard. ++ +If you don’t save the drilldown, and then navigate away, the drilldown is lost. + +. In *[Logs] Visitors by OS*, click any slice of the pie, and then select the drilldown *Show on Github*. ++ +[role="screenshot"] +image:images/url_drilldown_popup.png[URL drilldown popup] ++ +You are navigated to the issue list in the {kib} repository. Verify that value from a pie slice you’ve clicked on is carried over to Github. ++ +[role="screenshot"] +image:images/url_drilldown_github.png[Github] + +[float] +[[trigger-picker]] +==== Picking a trigger for a URL drilldown + +Some panels support multiple user interactions (called triggers) for which you can configure a URL drilldown. The list of supported variables in the URL template depends on the trigger you selected. +In the preceding example, you configured a URL drilldown on a pie chart. The only trigger that pie chart supports is clicking on a pie slice, so you didn’t have to pick a trigger. + +However, the sample *[Logs] Unique Visitors vs. Average Bytes* chart supports both clicking on a data point and selecting a range. When you create a URL drilldown for this chart, you have the following choices: + +[role="screenshot"] +image:images/url_drilldown_trigger_picker.png[Trigger picker: Single click and Range selection] + +Variables in the URL template differ per trigger. +For example, *Single click* has `{{event.value}}` and *Range selection* has `{{event.from}}` and `{{event.to}}`. +You can create multiple URL drilldowns per panel and attach them to different triggers. + +[float] +[[templating]] +==== URL templating language + +The URL template input uses Handlebars — a simple templating language. Handlebars templates look like regular text with embedded Handlebars expressions. + +[source, bash] +---- +https://github.com/elastic/kibana/issues?q={{event.value}} +---- + +A Handlebars expression is a `{{`, some contents, followed by a `}}`. When the drilldown is executed, these expressions are replaced by values from the dashboard and interaction context. + +Refer to Handlebars https://handlebarsjs.com/guide/expressions.html#expressions[documentation] to learn about advanced use cases. + +[[helpers]] +In addition to https://handlebarsjs.com/guide/builtin-helpers.html[built-in] Handlebars helpers, you can use the following custom helpers: + + +|=== +|Helper |Use case + +|json +a|Serialize variables in JSON format. + +Example: + +`{{json event}}` + +`{{json event.key event.value}}` + +`{{json filters=context.panel.filters}}` + + +|rison +a|Serialize variables in https://github.com/w33ble/rison-node[rison] format. Rison is a common format for {kib} apps for storing state in the URL. + +Example: + +`{{rison event}}` + +`{{rison event.key event.value}}` + +`{{rison filters=context.panel.filters}}` + + +|date +a|Format dates. Supports relative dates expressions (for example, "now-15d"). Refer to the https://momentjs.com/docs/#/displaying/format/[moment] docs for different formatting options. + +Example: + +`{{ date event.from “YYYY MM DD”}}` + +`{{date “now-15”}}` +|=== + + +[float] +[[variables]] +==== URL template variables + +The URL drilldown template has three sources for variables: + +* *Global* static variables that don’t change depending on the place where the URL drilldown is used or which user interaction executed the drilldown. For example: `{{kibanaUrl}}`. +* *Context* variables that change depending on where the drilldown is created and used. These variables are extracted from a context of a panel on a dashboard. For example, `{{context.panel.filters}}` gives access to filters that applied to the current panel. +* *Event* variables that depend on the trigger context. These variables are dynamically extracted from the interaction context when the drilldown is executed. + +[[values-in-preview]] +A subtle but important difference between *context* and *event* variables is that *context* variables use real values in previews when creating a URL drilldown. +For example, `{{context.panel.filters}}` are previewed with the current filters that applied to a panel. +*Event* variables are extracted during drilldown execution from a user interaction with a panel (for example, from a pie slice that the user clicked on). + +Because there is no user interaction with a panel in preview, there is no interaction context to use in a preview. +To work around this, {kib} provides a sample interaction that relies on a picked <>. +So in a preview, you might notice that `{{event.value}}` is replaced with `{{event.value}}` instead of with a sample from your data. +Such previews can help you make sure that the structure of your URL template is valid. +However, to ensure that the configured URL drilldown works as expected with your data, you have to save the dashboard and test in the panel. + +You can access the full list of variables available for the current panel and selected trigger by clicking *Add variable* in the top-right corner of a URL template input. + +[float] +[[variables-reference]] +==== Variables reference + + +|=== +|Source |Variable |Description + +|*Global* +| kibanaUrl +| {kib} base URL. Useful for creating URL drilldowns that navigate within {kib}. + +| *Context* +| context.panel +| Context provided by current dashboard panel. + +| +| context.panel.id +| ID of a panel. + +| +| context.panel.title +| Title of a panel. + +| +| context.panel.filters +| List of {kib} filters applied to a panel. + +Tip: Use in combination with <> helper for +internal {kib} navigations with carrying over current filters. + +| +| context.panel.query.query +| Current query string. + +| +| context.panel.query.lang +| Current query language. + +| +| context.panel.timeRange.from + +context.panel.timeRange.to +| Current time picker values. + +Tip: Use in combination with <> helper to format date. + +| +| context.panel.timeRange.indexPatternId + +context.panel.timeRange.indexPatternIds +|Index pattern ids used by a panel. + +| +| context.panel.savedObjectId +| ID of saved object behind a panel. + +| *Single click* +| event.value +| Value behind clicked data point. + +| +| event.key +| Field name behind clicked data point + +| +| event.negate +| Boolean, indicating whether clicked data point resulted in negative filter. + +| *Range selection* +| event.from + +event.to +| `from` and `to` values of selected range. Depending on your data, could be either a date or number. + +Tip: Consider using <> helper for date formatting. + +| +| event.key +| Aggregation field behind the selected range, if available. + +|=== diff --git a/docs/user/reporting/reporting-troubleshooting.asciidoc b/docs/user/reporting/reporting-troubleshooting.asciidoc index dc4ffdfebdae9..82f0aa7ca0f19 100644 --- a/docs/user/reporting/reporting-troubleshooting.asciidoc +++ b/docs/user/reporting/reporting-troubleshooting.asciidoc @@ -7,6 +7,7 @@ Having trouble? Here are solutions to common problems you might encounter while using Reporting. +* <> * <> * <> * <> @@ -15,6 +16,11 @@ Having trouble? Here are solutions to common problems you might encounter while * <> * <> +[float] +[[reporting-diagnostics]] +=== Reporting Diagnostics +Reporting comes with a built-in utility to try to automatically find common issues. When Kibana is running, navigate to the Report Listing page, and click the "Run reporting diagnostics..." button. This will open up a diagnostic tool that checks various parts of the Kibana deployment to come up with any relevant recommendations. + [float] [[reporting-troubleshooting-system-dependencies]] === System dependencies diff --git a/examples/alerting_example/server/plugin.ts b/examples/alerting_example/server/plugin.ts index e74cad28f77f4..8e246960937ec 100644 --- a/examples/alerting_example/server/plugin.ts +++ b/examples/alerting_example/server/plugin.ts @@ -38,7 +38,7 @@ export class AlertingExamplePlugin implements Plugin.ts +``` + +### 1. Update the telemetryrc file + +Make sure your collector is not excluded in the `telemetryrc.json` files (located at the root of the kibana project, and another on in the `x-pack` dir). + +```s +[ + { + ... + "exclude": [ + "" + ] + } +] +``` + +Note that the check will fail if the collector in --path is excluded. + +### 2. Type the `fetch` function +1. Make sure the return of the `fetch` function is typed. + +The function `makeUsageCollector` accepts a generic type parameter of the returned type of the `fetch` function. + +``` +interface Usage { + someStat: number; +} + +usageCollection.makeUsageCollector({ + fetch: async () => { + return { + someStat: 3, + } + }, + ... +}) +``` + +The generic type passed to `makeUsageCollector` will automatically unwrap the `Promise` to check for the resolved type. + +### 3. Add a `schema` field + +Add a `schema` field to your collector. After passing the return type of the fetch function to the `makeUsageCollector` generic parameter. It will automaticallly figure out the correct type of the schema based on that provided type. + + +``` +interface Usage { + someStat: number; +} + +usageCollection.makeUsageCollector({ + schema: { + someStat: { + type: 'long' + } + }, + ... +}) +``` + +For full details on writing the `schema` object, check the [Writing the schema](#writing-the-schema) section. + +### 4. Run the telemetry check + +To make sure your changes pass the telemetry check you can run the following: + +``` +node scripts/telemetry_check.js --ignore-stored-json --path=.ts +``` + +### 5. Update the stored json files + +The `--fix` flag will automatically update the persisted json files used by the telemetry team. + +``` +node scripts/telemetry_check.js --fix +``` + +Note that any updates to the stored json files will require a review by the kibana-telemetry team to help us update the telemetry cluster mappings and ensure your changes adhere to our best practices. + + +## Updating the collector schema + +Simply update the fetch function to start returning the updated fields back to our cluster. The update the schema to accomodate these changes. + +Once youre run the changes to both the `fetch` function and the `schema` field run the following command + +``` +node scripts/telemetry_check.js --fix +``` + +The `--fix` flag will automatically update the persisted json files used by the telemetry team. Note that any updates to the stored json files will require a review by the kibana-telemetry team to help us update the telemetry cluster mappings and ensure your changes adhere to our best practices. + + +## Writing the schema + +We've designed the schema object to closely resemble elasticsearch mapping object to reduce any cognitive complexity. + +### Basics + +The function `makeUsageCollector` will automatically translate the returned `Usage` fetch type to the `schema` object. This way you'll have the typescript type checker helping you write the correct corrisponding schema. + +``` +interface Usage { + someStat: number; +} + +usageCollection.makeUsageCollector({ + schema: { + someStat: { + type: 'long' + } + }, + ... +}) +``` + + +### Allowed types + +Any field property in the schema accepts a `type` field. By default the type is `object` which accepts nested properties under it. Currently we accept the following property types: + +``` +AllowedSchemaTypes = + | 'keyword' + | 'text' + | 'number' + | 'boolean' + | 'long' + | 'date' + | 'float'; +``` + + +### Dealing with arrays + +You can optionally define a property to be an array by setting the `isArray` to `true`. Note that the `isArray` property is not currently required. + + +``` +interface Usage { + arrayOfStrings: string[]; + arrayOfObjects: {key: string; value: number; }[]; +} + +usageCollection.makeUsageCollector({ + fetch: () => { + return { + arrayOfStrings: ['item_one', 'item_two'], + arrayOfObjects: [ + { key: 'key_one', value: 13 }, + ] + } + } + schema: { + arrayOfStrings: { + type: 'keyword', + isArray: true, + }, + arrayOfObjects: { + isArray: true, + key: { + type: 'keyword', + }, + value: { + type: 'long', + }, + } + }, + ... +}) +``` + +Be careful adding arrays of objects due to the limitation in correlating the properties inside those objects inside kibana. It is advised to look for an alternative schema based on your use cases. + + +## Schema Restrictions + +We have enforced some restrictions to the schema object to adhere to our telemetry best practices. These practices are derived from the usablity of the sent data in our telemetry cluster. + + +### Root of schema can only be an object + +The root of the schema can only be an object. Currently any property must be nested inside the main schema object. \ No newline at end of file diff --git a/packages/kbn-telemetry-tools/package.json b/packages/kbn-telemetry-tools/package.json index 63a8fcf30335e..4318cbcf2ec4e 100644 --- a/packages/kbn-telemetry-tools/package.json +++ b/packages/kbn-telemetry-tools/package.json @@ -10,12 +10,12 @@ "kbn:watch": "yarn build --watch" }, "devDependencies": { - "lodash": "npm:@elastic/lodash@3.10.1-kibana4", + "lodash": "^4.17.20", "@kbn/dev-utils": "1.0.0", "@kbn/utility-types": "1.0.0", "@types/normalize-path": "^3.0.0", "normalize-path": "^3.0.0", - "@types/lodash": "^3.10.1", + "@types/lodash": "^4.14.159", "moment": "^2.24.0", "typescript": "4.0.2" } diff --git a/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts b/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts index 2f85fd2cdd2a4..87ba68c1bcb27 100644 --- a/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts +++ b/packages/kbn-telemetry-tools/src/cli/run_telemetry_check.ts @@ -35,7 +35,7 @@ import { export function runTelemetryCheck() { run( - async ({ flags: { fix = false, path }, log }) => { + async ({ flags: { fix = false, 'ignore-stored-json': ignoreStoredJson, path }, log }) => { if (typeof fix !== 'boolean') { throw createFailError(`${chalk.white.bgRed(' TELEMETRY ERROR ')} --fix can't have a value`); } @@ -50,6 +50,14 @@ export function runTelemetryCheck() { ); } + if (fix && typeof ignoreStoredJson !== 'undefined') { + throw createFailError( + `${chalk.white.bgRed( + ' TELEMETRY ERROR ' + )} --fix is incompatible with --ignore-stored-json flag.` + ); + } + const list = new Listr([ { title: 'Checking .telemetryrc.json files', @@ -59,11 +67,28 @@ export function runTelemetryCheck() { title: 'Extracting Collectors', task: (context) => new Listr(extractCollectorsTask(context, path), { exitOnError: true }), }, + { + enabled: () => typeof path !== 'undefined', + title: 'Checking collectors in --path are not excluded', + task: ({ roots }: TaskContext) => { + const totalCollections = roots.reduce((acc, root) => { + return acc + (root.parsedCollections?.length || 0); + }, 0); + const collectorsInPath = Array.isArray(path) ? path.length : 1; + + if (totalCollections !== collectorsInPath) { + throw new Error( + 'Collector specified in `path` is excluded; Check the telemetryrc.json files.' + ); + } + }, + }, { title: 'Checking Compatible collector.schema with collector.fetch type', task: (context) => new Listr(checkCompatibleTypesTask(context), { exitOnError: true }), }, { + enabled: (_) => !!ignoreStoredJson, title: 'Checking Matching collector.schema against stored json files', task: (context) => new Listr(checkMatchingSchemasTask(context, !fix), { exitOnError: true }), diff --git a/packages/kbn-telemetry-tools/src/tools/serializer.ts b/packages/kbn-telemetry-tools/src/tools/serializer.ts index d5412f64f3615..7afe828298b4b 100644 --- a/packages/kbn-telemetry-tools/src/tools/serializer.ts +++ b/packages/kbn-telemetry-tools/src/tools/serializer.ts @@ -18,7 +18,7 @@ */ import * as ts from 'typescript'; -import { uniq } from 'lodash'; +import { uniqBy } from 'lodash'; import { getResolvedModuleSourceFile, getIdentifierDeclarationFromSource, @@ -148,7 +148,7 @@ export function getDescriptor(node: ts.Node, program: ts.Program): Descriptor | .map((typeNode) => getDescriptor(typeNode, program)) .filter(discardNullOrUndefined); - const uniqueKinds = uniq(kinds, 'kind'); + const uniqueKinds = uniqBy(kinds, 'kind'); if (uniqueKinds.length !== 1) { throw Error('Mapping does not support conflicting union types.'); diff --git a/packages/kbn-telemetry-tools/src/tools/utils.ts b/packages/kbn-telemetry-tools/src/tools/utils.ts index c1424785b22a5..3d6764117374c 100644 --- a/packages/kbn-telemetry-tools/src/tools/utils.ts +++ b/packages/kbn-telemetry-tools/src/tools/utils.ts @@ -18,7 +18,18 @@ */ import * as ts from 'typescript'; -import { pick, isObject, each, isArray, reduce, isEmpty, merge, transform, isEqual } from 'lodash'; +import { + pick, + pickBy, + isObject, + forEach, + isArray, + reduce, + isEmpty, + merge, + transform, + isEqual, +} from 'lodash'; import * as path from 'path'; import glob from 'glob'; import { readFile, writeFile } from 'fs'; @@ -186,17 +197,17 @@ export function getPropertyValue( } } -export function pickDeep(collection: any, identity: any, thisArg?: any) { - const picked: any = pick(collection, identity, thisArg); - const collections = pick(collection, isObject, thisArg); +export function pickDeep(collection: any, identity: any) { + const picked: any = pick(collection, identity); + const collections = pickBy(collection, isObject); - each(collections, function (item, key) { + forEach(collections, function (item, key) { let object; if (isArray(item)) { object = reduce( item, function (result, value) { - const pickedDeep = pickDeep(value, identity, thisArg); + const pickedDeep = pickDeep(value, identity); if (!isEmpty(pickedDeep)) { result.push(pickedDeep); } @@ -205,7 +216,7 @@ export function pickDeep(collection: any, identity: any, thisArg?: any) { [] as any[] ); } else { - object = pickDeep(item, identity, thisArg); + object = pickDeep(item, identity); } if (!isEmpty(object)) { @@ -230,33 +241,38 @@ export const flattenKeys = (obj: any, keyPath: any[] = []): any => { return { [keyPath.join('.')]: obj }; }; +type ObjectDict = Record; export function difference(actual: any, expected: any) { - function changes(obj: { [key: string]: any }, base: { [key: string]: any }) { - return transform(obj, function (result, value, key) { - if (key && /@@INDEX@@/.test(`${key}`)) { - // The type definition is an Index Signature, fuzzy searching for similar keys - const regexp = new RegExp(`${key}`.replace(/@@INDEX@@/g, '(.+)?')); - const keysInBase = Object.keys(base) - .map((k) => { - const match = k.match(regexp); - return match && match[0]; - }) - .filter((s): s is string => !!s); - - if (keysInBase.length === 0) { - // Mark this key as wrong because we couldn't find any matching keys - result[key] = value; - } - - keysInBase.forEach((k) => { - if (!isEqual(value, base[k])) { - result[k] = isObject(value) && isObject(base[k]) ? changes(value, base[k]) : value; + function changes(obj: ObjectDict, base: ObjectDict) { + return transform( + obj, + function (result, value, key) { + if (key && /@@INDEX@@/.test(`${key}`)) { + // The type definition is an Index Signature, fuzzy searching for similar keys + const regexp = new RegExp(`${key}`.replace(/@@INDEX@@/g, '(.+)?')); + const keysInBase = Object.keys(base) + .map((k) => { + const match = k.match(regexp); + return match && match[0]; + }) + .filter((s): s is string => !!s); + + if (keysInBase.length === 0) { + // Mark this key as wrong because we couldn't find any matching keys + result[key] = value; } - }); - } else if (key && !isEqual(value, base[key])) { - result[key] = isObject(value) && isObject(base[key]) ? changes(value, base[key]) : value; - } - }); + + keysInBase.forEach((k) => { + if (!isEqual(value, base[k])) { + result[k] = isObject(value) && isObject(base[k]) ? changes(value, base[k]) : value; + } + }); + } else if (key && !isEqual(value, base[key])) { + result[key] = isObject(value) && isObject(base[key]) ? changes(value, base[key]) : value; + } + }, + {} as ObjectDict + ); } return changes(actual, expected); } diff --git a/packages/kbn-ui-framework/src/components/local_nav/_local_search.scss b/packages/kbn-ui-framework/src/components/local_nav/_local_search.scss index 130807790e987..740ae664c7f5b 100644 --- a/packages/kbn-ui-framework/src/components/local_nav/_local_search.scss +++ b/packages/kbn-ui-framework/src/components/local_nav/_local_search.scss @@ -26,13 +26,6 @@ border-radius: 0; border-left-width: 0; } - -.kuiLocalSearchAssistedInput { - display: flex; - flex: 1 1 100%; - position: relative; -} - /** * 1. em used for right padding so documentation link and query string * won't overlap if the user increases their default browser font size diff --git a/packages/kbn-ui-shared-deps/package.json b/packages/kbn-ui-shared-deps/package.json index 4b2e88d155245..bbe7b1bc2e8da 100644 --- a/packages/kbn-ui-shared-deps/package.json +++ b/packages/kbn-ui-shared-deps/package.json @@ -9,7 +9,7 @@ "kbn:watch": "node scripts/build --dev --watch" }, "dependencies": { - "@elastic/charts": "21.0.1", + "@elastic/charts": "21.1.2", "@elastic/eui": "28.2.0", "@elastic/numeral": "^2.5.0", "@kbn/i18n": "1.0.0", diff --git a/packages/kbn-ui-shared-deps/webpack.config.js b/packages/kbn-ui-shared-deps/webpack.config.js index c81da4689052a..fa80dfdeef20f 100644 --- a/packages/kbn-ui-shared-deps/webpack.config.js +++ b/packages/kbn-ui-shared-deps/webpack.config.js @@ -32,22 +32,10 @@ exports.getWebpackConfig = ({ dev = false } = {}) => ({ mode: dev ? 'development' : 'production', entry: { 'kbn-ui-shared-deps': './entry.js', - 'kbn-ui-shared-deps.v7.dark': [ - '@elastic/eui/dist/eui_theme_dark.css', - '@elastic/charts/dist/theme_only_dark.css', - ], - 'kbn-ui-shared-deps.v7.light': [ - '@elastic/eui/dist/eui_theme_light.css', - '@elastic/charts/dist/theme_only_light.css', - ], - 'kbn-ui-shared-deps.v8.dark': [ - '@elastic/eui/dist/eui_theme_amsterdam_dark.css', - '@elastic/charts/dist/theme_only_dark.css', - ], - 'kbn-ui-shared-deps.v8.light': [ - '@elastic/eui/dist/eui_theme_amsterdam_light.css', - '@elastic/charts/dist/theme_only_light.css', - ], + 'kbn-ui-shared-deps.v7.dark': ['@elastic/eui/dist/eui_theme_dark.css'], + 'kbn-ui-shared-deps.v7.light': ['@elastic/eui/dist/eui_theme_light.css'], + 'kbn-ui-shared-deps.v8.dark': ['@elastic/eui/dist/eui_theme_amsterdam_dark.css'], + 'kbn-ui-shared-deps.v8.light': ['@elastic/eui/dist/eui_theme_amsterdam_light.css'], }, context: __dirname, devtool: dev ? '#cheap-source-map' : false, diff --git a/packages/kbn-utility-types/package.json b/packages/kbn-utility-types/package.json index a999eb41eb781..d1d7a1c0397cf 100644 --- a/packages/kbn-utility-types/package.json +++ b/packages/kbn-utility-types/package.json @@ -16,7 +16,7 @@ "utility-types": "^3.10.0" }, "devDependencies": { - "del-cli": "^3.0.0", - "tsd": "^0.7.4" + "del-cli": "^3.0.1", + "tsd": "^0.13.1" } } diff --git a/packages/kbn-utility-types/test-d/union_to_intersection.ts b/packages/kbn-utility-types/test-d/union_to_intersection.ts index ba385268475e7..8b49436bdd953 100644 --- a/packages/kbn-utility-types/test-d/union_to_intersection.ts +++ b/packages/kbn-utility-types/test-d/union_to_intersection.ts @@ -17,12 +17,12 @@ * under the License. */ -import { expectType } from 'tsd'; +import { expectAssignable } from 'tsd'; import { UnionToIntersection } from '../index'; type INTERSECTED = UnionToIntersection<{ foo: 'bar' } | { baz: 'qux' }>; -expectType({ +expectAssignable({ foo: 'bar', baz: 'qux', }); diff --git a/packages/kbn-utility-types/test-d/unwrap_observable.ts b/packages/kbn-utility-types/test-d/unwrap_observable.ts index af4fa9abf6ec7..e9791cfd36beb 100644 --- a/packages/kbn-utility-types/test-d/unwrap_observable.ts +++ b/packages/kbn-utility-types/test-d/unwrap_observable.ts @@ -17,9 +17,9 @@ * under the License. */ -import { expectType } from 'tsd'; +import { expectAssignable } from 'tsd'; import { UnwrapObservable, ObservableLike } from '../index'; type STRING = UnwrapObservable>; -expectType('adf'); +expectAssignable('adf'); diff --git a/packages/kbn-utility-types/test-d/unwrap_promise.ts b/packages/kbn-utility-types/test-d/unwrap_promise.ts index 9c4b1bc76b805..b61b24e4b3f15 100644 --- a/packages/kbn-utility-types/test-d/unwrap_promise.ts +++ b/packages/kbn-utility-types/test-d/unwrap_promise.ts @@ -17,11 +17,11 @@ * under the License. */ -import { expectType } from 'tsd'; +import { expectAssignable } from 'tsd'; import { UnwrapPromise } from '../index'; type STRING = UnwrapPromise>; type TUPLE = UnwrapPromise>; -expectType('adf'); -expectType([1, 2]); +expectAssignable('adf'); +expectAssignable([1, 2]); diff --git a/packages/kbn-utility-types/test-d/values.ts b/packages/kbn-utility-types/test-d/values.ts index 9e50cfebde1db..69bee9c3c9655 100644 --- a/packages/kbn-utility-types/test-d/values.ts +++ b/packages/kbn-utility-types/test-d/values.ts @@ -17,22 +17,22 @@ * under the License. */ -import { expectType } from 'tsd'; +import { expectAssignable } from 'tsd'; import { Values } from '../index'; // Arrays type STRING = Values; type ASDF_FOO = Values>; -expectType('adf'); -expectType('asdf'); -expectType('foo'); +expectAssignable('adf'); +expectAssignable('asdf'); +expectAssignable('foo'); // Objects type STRING2 = Values>; type FOO = Values>; type BAR = Values<{ foo: 'bar' }>; -expectType('adf'); -expectType('foo'); -expectType('bar'); +expectAssignable('adf'); +expectAssignable('foo'); +expectAssignable('bar'); diff --git a/rfcs/text/0010_service_status.md b/rfcs/text/0010_service_status.md index ded594930a367..76195c4f1ab89 100644 --- a/rfcs/text/0010_service_status.md +++ b/rfcs/text/0010_service_status.md @@ -137,7 +137,7 @@ interface StatusSetup { * Current status for all dependencies of the current plugin. * Each key of the `Record` is a plugin id. */ - plugins$: Observable>; + dependencies$: Observable>; /** * The status of this plugin as derived from its dependencies. diff --git a/src/legacy/utils/binder.ts b/src/cli/cluster/binder.ts similarity index 100% rename from src/legacy/utils/binder.ts rename to src/cli/cluster/binder.ts diff --git a/src/legacy/utils/binder_for.ts b/src/cli/cluster/binder_for.ts similarity index 100% rename from src/legacy/utils/binder_for.ts rename to src/cli/cluster/binder_for.ts diff --git a/src/cli/cluster/worker.ts b/src/cli/cluster/worker.ts index 097a549187429..c8a8a067d30bf 100644 --- a/src/cli/cluster/worker.ts +++ b/src/cli/cluster/worker.ts @@ -21,7 +21,7 @@ import _ from 'lodash'; import cluster from 'cluster'; import { EventEmitter } from 'events'; -import { BinderFor } from '../../legacy/utils/binder_for'; +import { BinderFor } from './binder_for'; import { fromRoot } from '../../core/server/utils'; const cliPath = fromRoot('src/cli'); diff --git a/src/cli_keystore/add.js b/src/cli_keystore/add.js index 462259ec942dd..232392f34c63b 100644 --- a/src/cli_keystore/add.js +++ b/src/cli_keystore/add.js @@ -18,7 +18,7 @@ */ import { Logger } from '../cli_plugin/lib/logger'; -import { confirm, question } from '../legacy/server/utils'; +import { confirm, question } from './utils'; import { createPromiseFromStreams, createConcatStream } from '../core/server/utils'; /** diff --git a/src/cli_keystore/add.test.js b/src/cli_keystore/add.test.js index b5d5009667eb4..f1adee8879bc2 100644 --- a/src/cli_keystore/add.test.js +++ b/src/cli_keystore/add.test.js @@ -42,7 +42,7 @@ import { PassThrough } from 'stream'; import { Keystore } from '../legacy/server/keystore'; import { add } from './add'; import { Logger } from '../cli_plugin/lib/logger'; -import * as prompt from '../legacy/server/utils/prompt'; +import * as prompt from './utils/prompt'; describe('Kibana keystore', () => { describe('add', () => { diff --git a/src/cli_keystore/create.js b/src/cli_keystore/create.js index 8be1eb36882f1..55fe2c151dec0 100644 --- a/src/cli_keystore/create.js +++ b/src/cli_keystore/create.js @@ -18,7 +18,7 @@ */ import { Logger } from '../cli_plugin/lib/logger'; -import { confirm } from '../legacy/server/utils'; +import { confirm } from './utils'; export async function create(keystore, command, options) { const logger = new Logger(options); diff --git a/src/cli_keystore/create.test.js b/src/cli_keystore/create.test.js index f48b3775ddfff..cb85475eab1cb 100644 --- a/src/cli_keystore/create.test.js +++ b/src/cli_keystore/create.test.js @@ -41,7 +41,7 @@ import sinon from 'sinon'; import { Keystore } from '../legacy/server/keystore'; import { create } from './create'; import { Logger } from '../cli_plugin/lib/logger'; -import * as prompt from '../legacy/server/utils/prompt'; +import * as prompt from './utils/prompt'; describe('Kibana keystore', () => { describe('create', () => { diff --git a/src/legacy/server/utils/index.js b/src/cli_keystore/utils/index.js similarity index 100% rename from src/legacy/server/utils/index.js rename to src/cli_keystore/utils/index.js diff --git a/src/legacy/server/utils/prompt.js b/src/cli_keystore/utils/prompt.js similarity index 100% rename from src/legacy/server/utils/prompt.js rename to src/cli_keystore/utils/prompt.js diff --git a/src/legacy/server/utils/prompt.test.js b/src/cli_keystore/utils/prompt.test.js similarity index 100% rename from src/legacy/server/utils/prompt.test.js rename to src/cli_keystore/utils/prompt.test.js diff --git a/src/core/MIGRATION.md b/src/core/MIGRATION.md index ea0e8d66d58f2..6a21dcb1b0686 100644 --- a/src/core/MIGRATION.md +++ b/src/core/MIGRATION.md @@ -1231,7 +1231,7 @@ import { npStart: { plugins } } from 'ui/new_platform'; | `import 'ui/filter_bar'` | `import { FilterBar } from '../data/public'` | Directive is deprecated. | | `import 'ui/query_bar'` | `import { QueryStringInput } from '../data/public'` | Directives are deprecated. | | `import 'ui/search_bar'` | `import { SearchBar } from '../data/public'` | Directive is deprecated. | -| `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive was moved to `src/plugins/kibana_legacy`. | +| `import 'ui/kbn_top_nav'` | `import { TopNavMenu } from '../navigation/public'` | Directive was removed. | | `ui/saved_objects/components/saved_object_finder` | `import { SavedObjectFinder } from '../saved_objects/public'` | | | `core_plugins/interpreter` | `plugins.data.expressions` | | `ui/courier` | `plugins.data.search` | @@ -1284,7 +1284,7 @@ _See also: [Server's CoreSetup API Docs](/docs/development/core/server/kibana-pl | Legacy Platform | New Platform | Notes | | ---------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | ----- | -| `server.plugins.xpack_main.registerFeature` | [`plugins.features.registerFeature`](x-pack/plugins/features/server/plugin.ts) | | +| `server.plugins.xpack_main.registerFeature` | [`plugins.features.registerKibanaFeature`](x-pack/plugins/features/server/plugin.ts) | | | `server.plugins.xpack_main.feature(pluginID).registerLicenseCheckResultsGenerator` | [`x-pack licensing plugin`](/x-pack/plugins/licensing/README.md) | | #### UI Exports diff --git a/src/core/public/core_app/status/lib/load_status.test.ts b/src/core/public/core_app/status/lib/load_status.test.ts index 3a444a4448467..5a9f982e106a7 100644 --- a/src/core/public/core_app/status/lib/load_status.test.ts +++ b/src/core/public/core_app/status/lib/load_status.test.ts @@ -57,6 +57,7 @@ const mockedResponse: StatusResponse = { ], }, metrics: { + collected_at: new Date('2020-01-01 01:00:00'), collection_interval_in_millis: 1000, os: { platform: 'darwin' as const, diff --git a/src/core/public/core_app/styles/_globals_v7dark.scss b/src/core/public/core_app/styles/_globals_v7dark.scss index 8ac841aab8469..9a4a965d63a38 100644 --- a/src/core/public/core_app/styles/_globals_v7dark.scss +++ b/src/core/public/core_app/styles/_globals_v7dark.scss @@ -3,9 +3,6 @@ // prepended to all .scss imports (from JS, when v7dark theme selected) @import '@elastic/eui/src/themes/eui/eui_colors_dark'; - -@import '@elastic/eui/src/global_styling/functions/index'; -@import '@elastic/eui/src/global_styling/variables/index'; -@import '@elastic/eui/src/global_styling/mixins/index'; +@import '@elastic/eui/src/themes/eui/eui_globals'; @import './mixins'; diff --git a/src/core/public/core_app/styles/_globals_v7light.scss b/src/core/public/core_app/styles/_globals_v7light.scss index 701bbdfe03662..ddb4b5b31fa1f 100644 --- a/src/core/public/core_app/styles/_globals_v7light.scss +++ b/src/core/public/core_app/styles/_globals_v7light.scss @@ -3,9 +3,6 @@ // prepended to all .scss imports (from JS, when v7light theme selected) @import '@elastic/eui/src/themes/eui/eui_colors_light'; - -@import '@elastic/eui/src/global_styling/functions/index'; -@import '@elastic/eui/src/global_styling/variables/index'; -@import '@elastic/eui/src/global_styling/mixins/index'; +@import '@elastic/eui/src/themes/eui/eui_globals'; @import './mixins'; diff --git a/src/core/public/core_app/styles/_globals_v8dark.scss b/src/core/public/core_app/styles/_globals_v8dark.scss index 972365e9e9d0e..9ad9108f350ff 100644 --- a/src/core/public/core_app/styles/_globals_v8dark.scss +++ b/src/core/public/core_app/styles/_globals_v8dark.scss @@ -3,14 +3,6 @@ // prepended to all .scss imports (from JS, when v8dark theme selected) @import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_colors_dark'; - -@import '@elastic/eui/src/global_styling/functions/index'; -@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/functions/index'; - -@import '@elastic/eui/src/global_styling/variables/index'; -@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/variables/index'; - -@import '@elastic/eui/src/global_styling/mixins/index'; -@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/mixins/index'; +@import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_globals'; @import './mixins'; diff --git a/src/core/public/core_app/styles/_globals_v8light.scss b/src/core/public/core_app/styles/_globals_v8light.scss index dc99f4d45082e..a6b2cb84c2062 100644 --- a/src/core/public/core_app/styles/_globals_v8light.scss +++ b/src/core/public/core_app/styles/_globals_v8light.scss @@ -3,14 +3,6 @@ // prepended to all .scss imports (from JS, when v8light theme selected) @import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_colors_light'; - -@import '@elastic/eui/src/global_styling/functions/index'; -@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/functions/index'; - -@import '@elastic/eui/src/global_styling/variables/index'; -@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/variables/index'; - -@import '@elastic/eui/src/global_styling/mixins/index'; -@import '@elastic/eui/src/themes/eui-amsterdam/global_styling/mixins/index'; +@import '@elastic/eui/src/themes/eui-amsterdam/eui_amsterdam_globals'; @import './mixins'; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index fc753517fd940..fae7a272c9635 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -38,6 +38,9 @@ export class DocLinksService { links: { dashboard: { drilldowns: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/drilldowns.html`, + drilldownsTriggerPicker: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url-drilldown.html#trigger-picker`, + urlDrilldownTemplateSyntax: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url-drilldown.html#templating`, + urlDrilldownVariables: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/url-drilldown.html#variables`, }, filebeat: { base: `${ELASTIC_WEBSITE_URL}guide/en/beats/filebeat/${DOC_LINK_VERSION}`, @@ -129,7 +132,7 @@ export class DocLinksService { }, visualize: { guide: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/visualize.html`, - timelionDeprecation: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/timelion.html#timelion-deprecation`, + timelionDeprecation: `${ELASTIC_WEBSITE_URL}guide/en/kibana/${DOC_LINK_VERSION}/dashboard.html#timelion-deprecation`, }, }, }); @@ -143,6 +146,9 @@ export interface DocLinksStart { readonly links: { readonly dashboard: { readonly drilldowns: string; + readonly drilldownsTriggerPicker: string; + readonly urlDrilldownTemplateSyntax: string; + readonly urlDrilldownVariables: string; }; readonly filebeat: { readonly base: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index c473ea67d9bcd..d90b8f780b674 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -490,6 +490,9 @@ export interface DocLinksStart { readonly links: { readonly dashboard: { readonly drilldowns: string; + readonly drilldownsTriggerPicker: string; + readonly urlDrilldownTemplateSyntax: string; + readonly urlDrilldownVariables: string; }; readonly filebeat: { readonly base: string; diff --git a/src/core/public/styles/_base.scss b/src/core/public/styles/_base.scss index 9b06b526fc7dd..427c6b7735435 100644 --- a/src/core/public/styles/_base.scss +++ b/src/core/public/styles/_base.scss @@ -1,4 +1,10 @@ +// Charts themes available app-wide +@import '@elastic/charts/dist/theme'; +@import '@elastic/eui/src/themes/charts/theme'; + +// Grab some nav-specific EUI vars @import '@elastic/eui/src/components/collapsible_nav/variables'; + // Application Layout // chrome-context diff --git a/src/core/server/config/deprecation/core_deprecations.ts b/src/core/server/config/deprecation/core_deprecations.ts index e4e881ab24372..2b8b8e383da24 100644 --- a/src/core/server/config/deprecation/core_deprecations.ts +++ b/src/core/server/config/deprecation/core_deprecations.ts @@ -113,7 +113,7 @@ const mapManifestServiceUrlDeprecation: ConfigDeprecation = (settings, fromPath, return settings; }; -export const coreDeprecationProvider: ConfigDeprecationProvider = ({ unusedFromRoot }) => [ +export const coreDeprecationProvider: ConfigDeprecationProvider = ({ rename, unusedFromRoot }) => [ unusedFromRoot('savedObjects.indexCheckTimeout'), unusedFromRoot('server.xsrf.token'), unusedFromRoot('maps.manifestServiceUrl'), @@ -136,6 +136,8 @@ export const coreDeprecationProvider: ConfigDeprecationProvider = ({ unusedFromR unusedFromRoot('optimize.workers'), unusedFromRoot('optimize.profile'), unusedFromRoot('optimize.validateSyntaxOfNodeModules'), + rename('cpu.cgroup.path.override', 'ops.cGroupOverrides.cpuPath'), + rename('cpuacct.cgroup.path.override', 'ops.cGroupOverrides.cpuAcctPath'), configPathDeprecation, dataPathDeprecation, rewriteBasePathDeprecation, diff --git a/src/core/server/index.ts b/src/core/server/index.ts index c17d3d7546779..d127471348d9f 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -266,9 +266,7 @@ export { SavedObjectUnsanitizedDoc, SavedObjectsRepositoryFactory, SavedObjectsResolveImportErrorsOptions, - SavedObjectsSchema, SavedObjectsSerializer, - SavedObjectsLegacyService, SavedObjectsUpdateOptions, SavedObjectsUpdateResponse, SavedObjectsAddToNamespacesOptions, @@ -295,6 +293,7 @@ export { SavedObjectsTypeManagementDefinition, SavedObjectMigrationMap, SavedObjectMigrationFn, + SavedObjectsUtils, exportSavedObjectsToStream, importSavedObjectsFromStream, resolveSavedObjectsImportErrors, diff --git a/src/core/server/legacy/legacy_service.mock.ts b/src/core/server/legacy/legacy_service.mock.ts index 26ec52185a5d8..c27f5be04d965 100644 --- a/src/core/server/legacy/legacy_service.mock.ts +++ b/src/core/server/legacy/legacy_service.mock.ts @@ -24,13 +24,7 @@ type LegacyServiceMock = jest.Mocked & { legacyId const createDiscoverPluginsMock = (): LegacyServiceDiscoverPlugins => ({ pluginSpecs: [], - uiExports: { - savedObjectSchemas: {}, - savedObjectMappings: [], - savedObjectMigrations: {}, - savedObjectValidations: {}, - savedObjectsManagement: {}, - }, + uiExports: {}, navLinks: [], pluginExtendedConfig: { get: jest.fn(), diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 880011d2e1923..6e6d5cfc24340 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -264,6 +264,7 @@ export class LegacyService implements CoreService { getTypeRegistry: startDeps.core.savedObjects.getTypeRegistry, }, metrics: { + collectionInterval: startDeps.core.metrics.collectionInterval, getOpsMetrics$: startDeps.core.metrics.getOpsMetrics$, }, uiSettings: { asScopedToClient: startDeps.core.uiSettings.asScopedToClient }, @@ -310,6 +311,17 @@ export class LegacyService implements CoreService { status: { core$: setupDeps.core.status.core$, overall$: setupDeps.core.status.overall$, + set: () => { + throw new Error(`core.status.set is unsupported in legacy`); + }, + // @ts-expect-error + get dependencies$() { + throw new Error(`core.status.dependencies$ is unsupported in legacy`); + }, + // @ts-expect-error + get derivedStatus$() { + throw new Error(`core.status.derivedStatus$ is unsupported in legacy`); + }, }, uiSettings: { register: setupDeps.core.uiSettings.register, @@ -341,11 +353,9 @@ export class LegacyService implements CoreService { registerStaticDir: setupDeps.core.http.registerStaticDir, }, hapiServer: setupDeps.core.http.server, - kibanaMigrator: startDeps.core.savedObjects.migrator, uiPlugins: setupDeps.uiPlugins, elasticsearch: setupDeps.core.elasticsearch, rendering: setupDeps.core.rendering, - savedObjectsClientProvider: startDeps.core.savedObjects.clientProvider, legacy: this.legacyInternals, }, logger: this.coreContext.logger, diff --git a/src/core/server/legacy/types.ts b/src/core/server/legacy/types.ts index cf08689a6d0d4..1105308fd44cf 100644 --- a/src/core/server/legacy/types.ts +++ b/src/core/server/legacy/types.ts @@ -24,7 +24,6 @@ import { KibanaRequest, LegacyRequest } from '../http'; import { InternalCoreSetup, InternalCoreStart } from '../internal_types'; import { PluginsServiceSetup, PluginsServiceStart, UiPlugins } from '../plugins'; import { InternalRenderingServiceSetup } from '../rendering'; -import { SavedObjectsLegacyUiExports } from '../types'; /** * @internal @@ -128,13 +127,13 @@ export type LegacyNavLink = Omit; unknown?: [{ pluginSpec: LegacyPluginSpec; type: unknown }]; -}; +} /** * @public diff --git a/src/core/server/metrics/collectors/cgroup.test.ts b/src/core/server/metrics/collectors/cgroup.test.ts new file mode 100644 index 0000000000000..39f917b9f0ba1 --- /dev/null +++ b/src/core/server/metrics/collectors/cgroup.test.ts @@ -0,0 +1,115 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import mockFs from 'mock-fs'; +import { OsCgroupMetricsCollector } from './cgroup'; + +describe('OsCgroupMetricsCollector', () => { + afterEach(() => mockFs.restore()); + + it('returns empty object when no cgroup file present', async () => { + mockFs({ + '/proc/self': { + /** empty directory */ + }, + }); + + const collector = new OsCgroupMetricsCollector({}); + expect(await collector.collect()).toEqual({}); + }); + + it('collects default cgroup data', async () => { + mockFs({ + '/proc/self/cgroup': ` +123:memory:/groupname +123:cpu:/groupname +123:cpuacct:/groupname + `, + '/sys/fs/cgroup/cpuacct/groupname/cpuacct.usage': '111', + '/sys/fs/cgroup/cpu/groupname/cpu.cfs_period_us': '222', + '/sys/fs/cgroup/cpu/groupname/cpu.cfs_quota_us': '333', + '/sys/fs/cgroup/cpu/groupname/cpu.stat': ` +nr_periods 444 +nr_throttled 555 +throttled_time 666 + `, + }); + + const collector = new OsCgroupMetricsCollector({}); + expect(await collector.collect()).toMatchInlineSnapshot(` + Object { + "cpu": Object { + "cfs_period_micros": 222, + "cfs_quota_micros": 333, + "control_group": "/groupname", + "stat": Object { + "number_of_elapsed_periods": 444, + "number_of_times_throttled": 555, + "time_throttled_nanos": 666, + }, + }, + "cpuacct": Object { + "control_group": "/groupname", + "usage_nanos": 111, + }, + } + `); + }); + + it('collects override cgroup data', async () => { + mockFs({ + '/proc/self/cgroup': ` +123:memory:/groupname +123:cpu:/groupname +123:cpuacct:/groupname + `, + '/sys/fs/cgroup/cpuacct/xxcustomcpuacctxx/cpuacct.usage': '111', + '/sys/fs/cgroup/cpu/xxcustomcpuxx/cpu.cfs_period_us': '222', + '/sys/fs/cgroup/cpu/xxcustomcpuxx/cpu.cfs_quota_us': '333', + '/sys/fs/cgroup/cpu/xxcustomcpuxx/cpu.stat': ` +nr_periods 444 +nr_throttled 555 +throttled_time 666 + `, + }); + + const collector = new OsCgroupMetricsCollector({ + cpuAcctPath: 'xxcustomcpuacctxx', + cpuPath: 'xxcustomcpuxx', + }); + expect(await collector.collect()).toMatchInlineSnapshot(` + Object { + "cpu": Object { + "cfs_period_micros": 222, + "cfs_quota_micros": 333, + "control_group": "xxcustomcpuxx", + "stat": Object { + "number_of_elapsed_periods": 444, + "number_of_times_throttled": 555, + "time_throttled_nanos": 666, + }, + }, + "cpuacct": Object { + "control_group": "xxcustomcpuacctxx", + "usage_nanos": 111, + }, + } + `); + }); +}); diff --git a/src/core/server/metrics/collectors/cgroup.ts b/src/core/server/metrics/collectors/cgroup.ts new file mode 100644 index 0000000000000..867ea44dff1ae --- /dev/null +++ b/src/core/server/metrics/collectors/cgroup.ts @@ -0,0 +1,194 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import fs from 'fs'; +import { join as joinPath } from 'path'; +import { MetricsCollector, OpsOsMetrics } from './types'; + +type OsCgroupMetrics = Pick; + +interface OsCgroupMetricsCollectorOptions { + cpuPath?: string; + cpuAcctPath?: string; +} + +export class OsCgroupMetricsCollector implements MetricsCollector { + /** Used to prevent unnecessary file reads on systems not using cgroups. */ + private noCgroupPresent = false; + private cpuPath?: string; + private cpuAcctPath?: string; + + constructor(private readonly options: OsCgroupMetricsCollectorOptions) {} + + public async collect(): Promise { + try { + await this.initializePaths(); + if (this.noCgroupPresent || !this.cpuAcctPath || !this.cpuPath) { + return {}; + } + + const [cpuAcctUsage, cpuFsPeriod, cpuFsQuota, cpuStat] = await Promise.all([ + readCPUAcctUsage(this.cpuAcctPath), + readCPUFsPeriod(this.cpuPath), + readCPUFsQuota(this.cpuPath), + readCPUStat(this.cpuPath), + ]); + + return { + cpuacct: { + control_group: this.cpuAcctPath, + usage_nanos: cpuAcctUsage, + }, + + cpu: { + control_group: this.cpuPath, + cfs_period_micros: cpuFsPeriod, + cfs_quota_micros: cpuFsQuota, + stat: cpuStat, + }, + }; + } catch (err) { + if (err.code === 'ENOENT') { + this.noCgroupPresent = true; + return {}; + } else { + throw err; + } + } + } + + public reset() {} + + private async initializePaths() { + // Perform this setup lazily on the first collect call and then memoize the results. + // Makes the assumption this data doesn't change while the process is running. + if (this.cpuPath && this.cpuAcctPath) { + return; + } + + // Only read the file if both options are undefined. + if (!this.options.cpuPath || !this.options.cpuAcctPath) { + const cgroups = await readControlGroups(); + this.cpuPath = this.options.cpuPath || cgroups[GROUP_CPU]; + this.cpuAcctPath = this.options.cpuAcctPath || cgroups[GROUP_CPUACCT]; + } else { + this.cpuPath = this.options.cpuPath; + this.cpuAcctPath = this.options.cpuAcctPath; + } + + // prevents undefined cgroup paths + if (!this.cpuPath || !this.cpuAcctPath) { + this.noCgroupPresent = true; + } + } +} + +const CONTROL_GROUP_RE = new RegExp('\\d+:([^:]+):(/.*)'); +const CONTROLLER_SEPARATOR_RE = ','; + +const PROC_SELF_CGROUP_FILE = '/proc/self/cgroup'; +const PROC_CGROUP_CPU_DIR = '/sys/fs/cgroup/cpu'; +const PROC_CGROUP_CPUACCT_DIR = '/sys/fs/cgroup/cpuacct'; + +const GROUP_CPUACCT = 'cpuacct'; +const CPUACCT_USAGE_FILE = 'cpuacct.usage'; + +const GROUP_CPU = 'cpu'; +const CPU_FS_PERIOD_US_FILE = 'cpu.cfs_period_us'; +const CPU_FS_QUOTA_US_FILE = 'cpu.cfs_quota_us'; +const CPU_STATS_FILE = 'cpu.stat'; + +async function readControlGroups() { + const data = await fs.promises.readFile(PROC_SELF_CGROUP_FILE); + + return data + .toString() + .split(/\n/) + .reduce((acc, line) => { + const matches = line.match(CONTROL_GROUP_RE); + + if (matches !== null) { + const controllers = matches[1].split(CONTROLLER_SEPARATOR_RE); + controllers.forEach((controller) => { + acc[controller] = matches[2]; + }); + } + + return acc; + }, {} as Record); +} + +async function fileContentsToInteger(path: string) { + const data = await fs.promises.readFile(path); + return parseInt(data.toString(), 10); +} + +function readCPUAcctUsage(controlGroup: string) { + return fileContentsToInteger(joinPath(PROC_CGROUP_CPUACCT_DIR, controlGroup, CPUACCT_USAGE_FILE)); +} + +function readCPUFsPeriod(controlGroup: string) { + return fileContentsToInteger(joinPath(PROC_CGROUP_CPU_DIR, controlGroup, CPU_FS_PERIOD_US_FILE)); +} + +function readCPUFsQuota(controlGroup: string) { + return fileContentsToInteger(joinPath(PROC_CGROUP_CPU_DIR, controlGroup, CPU_FS_QUOTA_US_FILE)); +} + +async function readCPUStat(controlGroup: string) { + const stat = { + number_of_elapsed_periods: -1, + number_of_times_throttled: -1, + time_throttled_nanos: -1, + }; + + try { + const data = await fs.promises.readFile( + joinPath(PROC_CGROUP_CPU_DIR, controlGroup, CPU_STATS_FILE) + ); + return data + .toString() + .split(/\n/) + .reduce((acc, line) => { + const fields = line.split(/\s+/); + + switch (fields[0]) { + case 'nr_periods': + acc.number_of_elapsed_periods = parseInt(fields[1], 10); + break; + + case 'nr_throttled': + acc.number_of_times_throttled = parseInt(fields[1], 10); + break; + + case 'throttled_time': + acc.time_throttled_nanos = parseInt(fields[1], 10); + break; + } + + return acc; + }, stat); + } catch (err) { + if (err.code === 'ENOENT') { + return stat; + } + + throw err; + } +} diff --git a/src/core/server/metrics/collectors/collector.mock.ts b/src/core/server/metrics/collectors/collector.mock.ts new file mode 100644 index 0000000000000..2a942e1fafe63 --- /dev/null +++ b/src/core/server/metrics/collectors/collector.mock.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { MetricsCollector } from './types'; + +const createCollector = (collectReturnValue: any = {}): jest.Mocked> => { + const collector: jest.Mocked> = { + collect: jest.fn().mockResolvedValue(collectReturnValue), + reset: jest.fn(), + }; + + return collector; +}; + +export const metricsCollectorMock = { + create: createCollector, +}; diff --git a/src/core/server/metrics/collectors/index.ts b/src/core/server/metrics/collectors/index.ts index f58ab02e63881..4540cb79be74b 100644 --- a/src/core/server/metrics/collectors/index.ts +++ b/src/core/server/metrics/collectors/index.ts @@ -18,6 +18,6 @@ */ export { OpsProcessMetrics, OpsOsMetrics, OpsServerMetrics, MetricsCollector } from './types'; -export { OsMetricsCollector } from './os'; +export { OsMetricsCollector, OpsMetricsCollectorOptions } from './os'; export { ProcessMetricsCollector } from './process'; export { ServerMetricsCollector } from './server'; diff --git a/src/plugins/kibana_legacy/common/index.ts b/src/core/server/metrics/collectors/os.test.mocks.ts similarity index 77% rename from src/plugins/kibana_legacy/common/index.ts rename to src/core/server/metrics/collectors/os.test.mocks.ts index 9c16d7b273862..ee02b8c802151 100644 --- a/src/plugins/kibana_legacy/common/index.ts +++ b/src/core/server/metrics/collectors/os.test.mocks.ts @@ -17,5 +17,9 @@ * under the License. */ -export * from './kbn_base_url'; -export * from './migrate_legacy_query'; +import { metricsCollectorMock } from './collector.mock'; + +export const cgroupCollectorMock = metricsCollectorMock.create(); +jest.doMock('./cgroup', () => ({ + OsCgroupMetricsCollector: jest.fn(() => cgroupCollectorMock), +})); diff --git a/src/core/server/metrics/collectors/os.test.ts b/src/core/server/metrics/collectors/os.test.ts index 7d5a6da90b7d6..5e52cecb76be3 100644 --- a/src/core/server/metrics/collectors/os.test.ts +++ b/src/core/server/metrics/collectors/os.test.ts @@ -20,6 +20,7 @@ jest.mock('getos', () => (cb: Function) => cb(null, { dist: 'distrib', release: 'release' })); import os from 'os'; +import { cgroupCollectorMock } from './os.test.mocks'; import { OsMetricsCollector } from './os'; describe('OsMetricsCollector', () => { @@ -27,6 +28,8 @@ describe('OsMetricsCollector', () => { beforeEach(() => { collector = new OsMetricsCollector(); + cgroupCollectorMock.collect.mockReset(); + cgroupCollectorMock.reset.mockReset(); }); afterEach(() => { @@ -96,4 +99,9 @@ describe('OsMetricsCollector', () => { '15m': fifteenMinLoad, }); }); + + it('calls the cgroup sub-collector', async () => { + await collector.collect(); + expect(cgroupCollectorMock.collect).toHaveBeenCalled(); + }); }); diff --git a/src/core/server/metrics/collectors/os.ts b/src/core/server/metrics/collectors/os.ts index 59bef9d8ddd2b..eae49278405a9 100644 --- a/src/core/server/metrics/collectors/os.ts +++ b/src/core/server/metrics/collectors/os.ts @@ -21,10 +21,22 @@ import os from 'os'; import getosAsync, { LinuxOs } from 'getos'; import { promisify } from 'util'; import { OpsOsMetrics, MetricsCollector } from './types'; +import { OsCgroupMetricsCollector } from './cgroup'; const getos = promisify(getosAsync); +export interface OpsMetricsCollectorOptions { + cpuPath?: string; + cpuAcctPath?: string; +} + export class OsMetricsCollector implements MetricsCollector { + private readonly cgroupCollector: OsCgroupMetricsCollector; + + constructor(options: OpsMetricsCollectorOptions = {}) { + this.cgroupCollector = new OsCgroupMetricsCollector(options); + } + public async collect(): Promise { const platform = os.platform(); const load = os.loadavg(); @@ -43,20 +55,30 @@ export class OsMetricsCollector implements MetricsCollector { used_in_bytes: os.totalmem() - os.freemem(), }, uptime_in_millis: os.uptime() * 1000, + ...(await this.getDistroStats(platform)), + ...(await this.cgroupCollector.collect()), }; + return metrics; + } + + public reset() {} + + private async getDistroStats( + platform: string + ): Promise> { if (platform === 'linux') { try { const distro = (await getos()) as LinuxOs; - metrics.distro = distro.dist; - metrics.distroRelease = `${distro.dist}-${distro.release}`; + return { + distro: distro.dist, + distroRelease: `${distro.dist}-${distro.release}`, + }; } catch (e) { // ignore errors } } - return metrics; + return {}; } - - public reset() {} } diff --git a/src/core/server/metrics/collectors/types.ts b/src/core/server/metrics/collectors/types.ts index 73e8975a6b362..77ea13a1f0787 100644 --- a/src/core/server/metrics/collectors/types.ts +++ b/src/core/server/metrics/collectors/types.ts @@ -85,6 +85,33 @@ export interface OpsOsMetrics { }; /** the OS uptime */ uptime_in_millis: number; + + /** cpu accounting metrics, undefined when not running in a cgroup */ + cpuacct?: { + /** name of this process's cgroup */ + control_group: string; + /** cpu time used by this process's cgroup */ + usage_nanos: number; + }; + + /** cpu cgroup metrics, undefined when not running in a cgroup */ + cpu?: { + /** name of this process's cgroup */ + control_group: string; + /** the length of the cfs period */ + cfs_period_micros: number; + /** total available run-time within a cfs period */ + cfs_quota_micros: number; + /** current stats on the cfs periods */ + stat: { + /** number of cfs periods that elapsed */ + number_of_elapsed_periods: number; + /** number of times the cgroup has been throttled */ + number_of_times_throttled: number; + /** total amount of time the cgroup has been throttled for */ + time_throttled_nanos: number; + }; + }; } /** diff --git a/src/core/server/metrics/metrics_service.mock.ts b/src/core/server/metrics/metrics_service.mock.ts index 769f6ee2a549a..2af653004a479 100644 --- a/src/core/server/metrics/metrics_service.mock.ts +++ b/src/core/server/metrics/metrics_service.mock.ts @@ -21,20 +21,18 @@ import { MetricsService } from './metrics_service'; import { InternalMetricsServiceSetup, InternalMetricsServiceStart, + MetricsServiceSetup, MetricsServiceStart, } from './types'; const createInternalSetupContractMock = () => { - const setupContract: jest.Mocked = {}; - return setupContract; -}; - -const createStartContractMock = () => { - const startContract: jest.Mocked = { + const setupContract: jest.Mocked = { + collectionInterval: 30000, getOpsMetrics$: jest.fn(), }; - startContract.getOpsMetrics$.mockReturnValue( + setupContract.getOpsMetrics$.mockReturnValue( new BehaviorSubject({ + collected_at: new Date('2020-01-01 01:00:00'), process: { memory: { heap: { total_in_bytes: 1, used_in_bytes: 1, size_limit: 1 }, @@ -56,11 +54,21 @@ const createStartContractMock = () => { concurrent_connections: 1, }) ); + return setupContract; +}; + +const createSetupContractMock = () => { + const startContract: jest.Mocked = createInternalSetupContractMock(); return startContract; }; const createInternalStartContractMock = () => { - const startContract: jest.Mocked = createStartContractMock(); + const startContract: jest.Mocked = createInternalSetupContractMock(); + return startContract; +}; + +const createStartContractMock = () => { + const startContract: jest.Mocked = createInternalSetupContractMock(); return startContract; }; @@ -77,7 +85,7 @@ const createMock = () => { export const metricsServiceMock = { create: createMock, - createSetupContract: createStartContractMock, + createSetupContract: createSetupContractMock, createStartContract: createStartContractMock, createInternalSetupContract: createInternalSetupContractMock, createInternalStartContract: createInternalStartContractMock, diff --git a/src/core/server/metrics/metrics_service.ts b/src/core/server/metrics/metrics_service.ts index f28fb21aaac0d..d4696b3aa9aaf 100644 --- a/src/core/server/metrics/metrics_service.ts +++ b/src/core/server/metrics/metrics_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Subject } from 'rxjs'; +import { ReplaySubject } from 'rxjs'; import { first } from 'rxjs/operators'; import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; @@ -37,26 +37,21 @@ export class MetricsService private readonly logger: Logger; private metricsCollector?: OpsMetricsCollector; private collectInterval?: NodeJS.Timeout; - private metrics$ = new Subject(); + private metrics$ = new ReplaySubject(); + private service?: InternalMetricsServiceSetup; constructor(private readonly coreContext: CoreContext) { this.logger = coreContext.logger.get('metrics'); } public async setup({ http }: MetricsServiceSetupDeps): Promise { - this.metricsCollector = new OpsMetricsCollector(http.server); - return {}; - } - - public async start(): Promise { - if (!this.metricsCollector) { - throw new Error('#setup() needs to be run first'); - } const config = await this.coreContext.configService .atPath(opsConfig.path) .pipe(first()) .toPromise(); + this.metricsCollector = new OpsMetricsCollector(http.server, config.cGroupOverrides); + await this.refreshMetrics(); this.collectInterval = setInterval(() => { @@ -65,9 +60,20 @@ export class MetricsService const metricsObservable = this.metrics$.asObservable(); - return { + this.service = { + collectionInterval: config.interval.asMilliseconds(), getOpsMetrics$: () => metricsObservable, }; + + return this.service; + } + + public async start(): Promise { + if (!this.service) { + throw new Error('#setup() needs to be run first'); + } + + return this.service; } private async refreshMetrics() { diff --git a/src/core/server/metrics/ops_config.ts b/src/core/server/metrics/ops_config.ts index bd6ae5cc5474d..5f3f67e931c38 100644 --- a/src/core/server/metrics/ops_config.ts +++ b/src/core/server/metrics/ops_config.ts @@ -23,6 +23,10 @@ export const opsConfig = { path: 'ops', schema: schema.object({ interval: schema.duration({ defaultValue: '5s' }), + cGroupOverrides: schema.object({ + cpuPath: schema.maybe(schema.string()), + cpuAcctPath: schema.maybe(schema.string()), + }), }), }; diff --git a/src/core/server/metrics/ops_metrics_collector.test.ts b/src/core/server/metrics/ops_metrics_collector.test.ts index 9e76895b14578..7aa3f7cd3baf0 100644 --- a/src/core/server/metrics/ops_metrics_collector.test.ts +++ b/src/core/server/metrics/ops_metrics_collector.test.ts @@ -30,7 +30,7 @@ describe('OpsMetricsCollector', () => { beforeEach(() => { const hapiServer = httpServiceMock.createInternalSetupContract().server; - collector = new OpsMetricsCollector(hapiServer); + collector = new OpsMetricsCollector(hapiServer, {}); mockOsCollector.collect.mockResolvedValue('osMetrics'); }); @@ -51,6 +51,7 @@ describe('OpsMetricsCollector', () => { expect(mockServerCollector.collect).toHaveBeenCalledTimes(1); expect(metrics).toEqual({ + collected_at: expect.any(Date), process: 'processMetrics', os: 'osMetrics', requests: 'serverRequestsMetrics', diff --git a/src/core/server/metrics/ops_metrics_collector.ts b/src/core/server/metrics/ops_metrics_collector.ts index 525515dba1457..af74caa6cb386 100644 --- a/src/core/server/metrics/ops_metrics_collector.ts +++ b/src/core/server/metrics/ops_metrics_collector.ts @@ -21,6 +21,7 @@ import { Server as HapiServer } from 'hapi'; import { ProcessMetricsCollector, OsMetricsCollector, + OpsMetricsCollectorOptions, ServerMetricsCollector, MetricsCollector, } from './collectors'; @@ -31,9 +32,9 @@ export class OpsMetricsCollector implements MetricsCollector { private readonly osCollector: OsMetricsCollector; private readonly serverCollector: ServerMetricsCollector; - constructor(server: HapiServer) { + constructor(server: HapiServer, opsOptions: OpsMetricsCollectorOptions) { this.processCollector = new ProcessMetricsCollector(); - this.osCollector = new OsMetricsCollector(); + this.osCollector = new OsMetricsCollector(opsOptions); this.serverCollector = new ServerMetricsCollector(server); } @@ -44,6 +45,7 @@ export class OpsMetricsCollector implements MetricsCollector { this.serverCollector.collect(), ]); return { + collected_at: new Date(), process, os, ...server, diff --git a/src/core/server/metrics/types.ts b/src/core/server/metrics/types.ts index cbf0acacd6bab..c177b3ed25115 100644 --- a/src/core/server/metrics/types.ts +++ b/src/core/server/metrics/types.ts @@ -20,14 +20,15 @@ import { Observable } from 'rxjs'; import { OpsProcessMetrics, OpsOsMetrics, OpsServerMetrics } from './collectors'; -// eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface MetricsServiceSetup {} /** * APIs to retrieves metrics gathered and exposed by the core platform. * * @public */ -export interface MetricsServiceStart { +export interface MetricsServiceSetup { + /** Interval metrics are collected in milliseconds */ + readonly collectionInterval: number; + /** * Retrieve an observable emitting the {@link OpsMetrics} gathered. * The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time, @@ -42,6 +43,12 @@ export interface MetricsServiceStart { */ getOpsMetrics$: () => Observable; } +/** + * {@inheritdoc MetricsServiceSetup} + * + * @public + */ +export type MetricsServiceStart = MetricsServiceSetup; export type InternalMetricsServiceSetup = MetricsServiceSetup; export type InternalMetricsServiceStart = MetricsServiceStart; @@ -53,6 +60,8 @@ export type InternalMetricsServiceStart = MetricsServiceStart; * @public */ export interface OpsMetrics { + /** Time metrics were recorded at. */ + collected_at: Date; /** Process related metrics */ process: OpsProcessMetrics; /** OS related metrics */ diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts index 64d1256be2f30..836aabf881474 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.test.ts @@ -116,6 +116,16 @@ test('logs warning if pluginId is not in camelCase format', async () => { `); }); +test('does not log pluginId format warning in dist mode', async () => { + mockReadFile.mockImplementation((path, cb) => { + cb(null, Buffer.from(JSON.stringify({ id: 'some_name', version: 'kibana', server: true }))); + }); + + expect(loggingSystemMock.collect(logger).warn).toHaveLength(0); + await parseManifest(pluginPath, { ...packageInfo, dist: true }, logger); + expect(loggingSystemMock.collect(logger).warn.length).toBe(0); +}); + test('return error when plugin version is missing', async () => { mockReadFile.mockImplementation((path, cb) => { cb(null, Buffer.from(JSON.stringify({ id: 'someId' }))); diff --git a/src/core/server/plugins/discovery/plugin_manifest_parser.ts b/src/core/server/plugins/discovery/plugin_manifest_parser.ts index 0d33e266c37db..cfc412cb60b50 100644 --- a/src/core/server/plugins/discovery/plugin_manifest_parser.ts +++ b/src/core/server/plugins/discovery/plugin_manifest_parser.ts @@ -116,7 +116,7 @@ export async function parseManifest( ); } - if (!isCamelCase(manifest.id)) { + if (!packageInfo.dist && !isCamelCase(manifest.id)) { log.warn(`Expect plugin "id" in camelCase, but found: ${manifest.id}`); } diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index fa2659ca130a0..af0b0e19b3227 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -185,6 +185,9 @@ export function createPluginSetupContext( status: { core$: deps.status.core$, overall$: deps.status.overall$, + set: deps.status.plugins.set.bind(null, plugin.name), + dependencies$: deps.status.plugins.getDependenciesStatus$(plugin.name), + derivedStatus$: deps.status.plugins.getDerivedStatus$(plugin.name), }, uiSettings: { register: deps.uiSettings.register, @@ -233,6 +236,7 @@ export function createPluginStartContext( getTypeRegistry: deps.savedObjects.getTypeRegistry, }, metrics: { + collectionInterval: deps.metrics.collectionInterval, getOpsMetrics$: deps.metrics.getOpsMetrics$, }, uiSettings: { diff --git a/src/core/server/plugins/plugins_system.test.ts b/src/core/server/plugins/plugins_system.test.ts index 7af77491df1ab..71ac31db13f92 100644 --- a/src/core/server/plugins/plugins_system.test.ts +++ b/src/core/server/plugins/plugins_system.test.ts @@ -100,15 +100,27 @@ test('getPluginDependencies returns dependency tree of symbols', () => { pluginsSystem.addPlugin(createPlugin('no-dep')); expect(pluginsSystem.getPluginDependencies()).toMatchInlineSnapshot(` - Map { - Symbol(plugin-a) => Array [ - Symbol(no-dep), - ], - Symbol(plugin-b) => Array [ - Symbol(plugin-a), - Symbol(no-dep), - ], - Symbol(no-dep) => Array [], + Object { + "asNames": Map { + "plugin-a" => Array [ + "no-dep", + ], + "plugin-b" => Array [ + "plugin-a", + "no-dep", + ], + "no-dep" => Array [], + }, + "asOpaqueIds": Map { + Symbol(plugin-a) => Array [ + Symbol(no-dep), + ], + Symbol(plugin-b) => Array [ + Symbol(plugin-a), + Symbol(no-dep), + ], + Symbol(no-dep) => Array [], + }, } `); }); diff --git a/src/core/server/plugins/plugins_system.ts b/src/core/server/plugins/plugins_system.ts index f5c1b35d678a3..b2acd9a6fd04b 100644 --- a/src/core/server/plugins/plugins_system.ts +++ b/src/core/server/plugins/plugins_system.ts @@ -20,10 +20,11 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { PluginWrapper } from './plugin'; -import { DiscoveredPlugin, PluginName, PluginOpaqueId } from './types'; +import { DiscoveredPlugin, PluginName } from './types'; import { createPluginSetupContext, createPluginStartContext } from './plugin_context'; import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service'; import { withTimeout } from '../../utils'; +import { PluginDependencies } from '.'; const Sec = 1000; /** @internal */ @@ -45,9 +46,19 @@ export class PluginsSystem { * @returns a ReadonlyMap of each plugin and an Array of its available dependencies * @internal */ - public getPluginDependencies(): ReadonlyMap { - // Return dependency map of opaque ids - return new Map( + public getPluginDependencies(): PluginDependencies { + const asNames = new Map( + [...this.plugins].map(([name, plugin]) => [ + plugin.name, + [ + ...new Set([ + ...plugin.requiredPlugins, + ...plugin.optionalPlugins.filter((optPlugin) => this.plugins.has(optPlugin)), + ]), + ].map((depId) => this.plugins.get(depId)!.name), + ]) + ); + const asOpaqueIds = new Map( [...this.plugins].map(([name, plugin]) => [ plugin.opaqueId, [ @@ -58,6 +69,8 @@ export class PluginsSystem { ].map((depId) => this.plugins.get(depId)!.opaqueId), ]) ); + + return { asNames, asOpaqueIds }; } public async setupPlugins(deps: PluginsServiceSetupDeps) { diff --git a/src/core/server/plugins/types.ts b/src/core/server/plugins/types.ts index eb2a9ca3daf5f..517261b5bc9bb 100644 --- a/src/core/server/plugins/types.ts +++ b/src/core/server/plugins/types.ts @@ -93,6 +93,12 @@ export type PluginName = string; /** @public */ export type PluginOpaqueId = symbol; +/** @internal */ +export interface PluginDependencies { + asNames: ReadonlyMap; + asOpaqueIds: ReadonlyMap; +} + /** * Describes the set of required and optional properties plugin can define in its * mandatory JSON manifest file. diff --git a/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap b/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap deleted file mode 100644 index 7cd0297e57857..0000000000000 --- a/src/core/server/saved_objects/__snapshots__/utils.test.ts.snap +++ /dev/null @@ -1,184 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`convertLegacyTypes converts the legacy mappings using default values if no schemas are specified 1`] = ` -Array [ - Object { - "convertToAliasScript": undefined, - "hidden": false, - "indexPattern": undefined, - "management": undefined, - "mappings": Object { - "properties": Object { - "fieldA": Object { - "type": "text", - }, - }, - }, - "migrations": Object {}, - "name": "typeA", - "namespaceType": "single", - }, - Object { - "convertToAliasScript": undefined, - "hidden": false, - "indexPattern": undefined, - "management": undefined, - "mappings": Object { - "properties": Object { - "fieldB": Object { - "type": "text", - }, - }, - }, - "migrations": Object {}, - "name": "typeB", - "namespaceType": "single", - }, - Object { - "convertToAliasScript": undefined, - "hidden": false, - "indexPattern": undefined, - "management": undefined, - "mappings": Object { - "properties": Object { - "fieldC": Object { - "type": "text", - }, - }, - }, - "migrations": Object {}, - "name": "typeC", - "namespaceType": "single", - }, -] -`; - -exports[`convertLegacyTypes merges everything when all are present 1`] = ` -Array [ - Object { - "convertToAliasScript": undefined, - "hidden": true, - "indexPattern": "myIndex", - "management": undefined, - "mappings": Object { - "properties": Object { - "fieldA": Object { - "type": "text", - }, - }, - }, - "migrations": Object { - "1.0.0": [Function], - "2.0.4": [Function], - }, - "name": "typeA", - "namespaceType": "agnostic", - }, - Object { - "convertToAliasScript": "some alias script", - "hidden": false, - "indexPattern": undefined, - "management": undefined, - "mappings": Object { - "properties": Object { - "anotherFieldB": Object { - "type": "boolean", - }, - "fieldB": Object { - "type": "text", - }, - }, - }, - "migrations": Object {}, - "name": "typeB", - "namespaceType": "single", - }, - Object { - "convertToAliasScript": undefined, - "hidden": false, - "indexPattern": undefined, - "management": undefined, - "mappings": Object { - "properties": Object { - "fieldC": Object { - "type": "text", - }, - }, - }, - "migrations": Object { - "1.5.3": [Function], - }, - "name": "typeC", - "namespaceType": "single", - }, -] -`; - -exports[`convertLegacyTypes merges the mappings and the schema to create the type when schema exists for the type 1`] = ` -Array [ - Object { - "convertToAliasScript": undefined, - "hidden": true, - "indexPattern": "fooBar", - "management": undefined, - "mappings": Object { - "properties": Object { - "fieldA": Object { - "type": "text", - }, - }, - }, - "migrations": Object {}, - "name": "typeA", - "namespaceType": "agnostic", - }, - Object { - "convertToAliasScript": undefined, - "hidden": false, - "indexPattern": "barBaz", - "management": undefined, - "mappings": Object { - "properties": Object { - "fieldB": Object { - "type": "text", - }, - }, - }, - "migrations": Object {}, - "name": "typeB", - "namespaceType": "multiple", - }, - Object { - "convertToAliasScript": undefined, - "hidden": false, - "indexPattern": undefined, - "management": undefined, - "mappings": Object { - "properties": Object { - "fieldC": Object { - "type": "text", - }, - }, - }, - "migrations": Object {}, - "name": "typeC", - "namespaceType": "single", - }, - Object { - "convertToAliasScript": undefined, - "hidden": false, - "indexPattern": "bazQux", - "management": undefined, - "mappings": Object { - "properties": Object { - "fieldD": Object { - "type": "text", - }, - }, - }, - "migrations": Object {}, - "name": "typeD", - "namespaceType": "agnostic", - }, -] -`; diff --git a/src/core/server/saved_objects/index.ts b/src/core/server/saved_objects/index.ts index a294b28753f7b..f2bae29c4743b 100644 --- a/src/core/server/saved_objects/index.ts +++ b/src/core/server/saved_objects/index.ts @@ -19,8 +19,6 @@ export * from './service'; -export { SavedObjectsSchema } from './schema'; - export * from './import'; export { diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts index 4fc94d1992869..4cc4f696d307c 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.test.ts @@ -48,7 +48,6 @@ describe('DocumentMigrator', () => { return { kibanaVersion: '25.2.3', typeRegistry: createRegistry(), - validateDoc: _.noop, log: mockLogger, }; } @@ -60,7 +59,6 @@ describe('DocumentMigrator', () => { name: 'foo', migrations: _.noop as any, }), - validateDoc: _.noop, log: mockLogger, }; expect(() => new DocumentMigrator(invalidDefinition)).toThrow( @@ -77,7 +75,6 @@ describe('DocumentMigrator', () => { bar: (doc) => doc, }, }), - validateDoc: _.noop, log: mockLogger, }; expect(() => new DocumentMigrator(invalidDefinition)).toThrow( @@ -94,7 +91,6 @@ describe('DocumentMigrator', () => { '1.2.3': 23 as any, }, }), - validateDoc: _.noop, log: mockLogger, }; expect(() => new DocumentMigrator(invalidDefinition)).toThrow( @@ -633,27 +629,6 @@ describe('DocumentMigrator', () => { bbb: '3.2.3', }); }); - - test('fails if the validate doc throws', () => { - const migrator = new DocumentMigrator({ - ...testOpts(), - typeRegistry: createRegistry({ - name: 'aaa', - migrations: { - '2.3.4': (d) => set(d, 'attributes.counter', 42), - }, - }), - validateDoc: (d) => { - if ((d.attributes as any).counter === 42) { - throw new Error('Meaningful!'); - } - }, - }); - - const doc = { id: '1', type: 'foo', attributes: {}, migrationVersion: {}, aaa: {} }; - - expect(() => migrator.migrate(doc)).toThrow(/Meaningful/); - }); }); function renameAttr(path: string, newPath: string) { diff --git a/src/core/server/saved_objects/migrations/core/document_migrator.ts b/src/core/server/saved_objects/migrations/core/document_migrator.ts index c50f755fda994..345704fbfd783 100644 --- a/src/core/server/saved_objects/migrations/core/document_migrator.ts +++ b/src/core/server/saved_objects/migrations/core/document_migrator.ts @@ -73,12 +73,9 @@ import { SavedObjectMigrationFn } from '../types'; export type TransformFn = (doc: SavedObjectUnsanitizedDoc) => SavedObjectUnsanitizedDoc; -type ValidateDoc = (doc: SavedObjectUnsanitizedDoc) => void; - interface DocumentMigratorOptions { kibanaVersion: string; typeRegistry: ISavedObjectTypeRegistry; - validateDoc: ValidateDoc; log: Logger; } @@ -113,19 +110,16 @@ export class DocumentMigrator implements VersionedTransformer { * @param {DocumentMigratorOptions} opts * @prop {string} kibanaVersion - The current version of Kibana * @prop {SavedObjectTypeRegistry} typeRegistry - The type registry to get type migrations from - * @prop {ValidateDoc} validateDoc - A function which, given a document throws an error if it is - * not up to date. This is used to ensure we don't let unmigrated documents slip through. * @prop {Logger} log - The migration logger * @memberof DocumentMigrator */ - constructor({ typeRegistry, kibanaVersion, log, validateDoc }: DocumentMigratorOptions) { + constructor({ typeRegistry, kibanaVersion, log }: DocumentMigratorOptions) { validateMigrationDefinition(typeRegistry); this.migrations = buildActiveMigrations(typeRegistry, log); this.transformDoc = buildDocumentTransform({ kibanaVersion, migrations: this.migrations, - validateDoc, }); } @@ -231,21 +225,16 @@ function buildActiveMigrations( * Creates a function which migrates and validates any document that is passed to it. */ function buildDocumentTransform({ - kibanaVersion, migrations, - validateDoc, }: { kibanaVersion: string; migrations: ActiveMigrations; - validateDoc: ValidateDoc; }): TransformFn { return function transformAndValidate(doc: SavedObjectUnsanitizedDoc) { const result = doc.migrationVersion ? applyMigrations(doc, migrations) : markAsUpToDate(doc, migrations); - validateDoc(result); - // In order to keep tests a bit more stable, we won't // tack on an empy migrationVersion to docs that have // no migrations defined. diff --git a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts index df89137a1d798..13f771c16bc67 100644 --- a/src/core/server/saved_objects/migrations/core/index_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/core/index_migrator.test.ts @@ -369,6 +369,30 @@ describe('IndexMigrator', () => { ], }); }); + + test('rejects when the migration function throws an error', async () => { + const { client } = testOpts; + const migrateDoc = jest.fn((doc: SavedObjectUnsanitizedDoc) => { + throw new Error('error migrating document'); + }); + + testOpts.documentMigrator = { + migrationVersion: { foo: '1.2.3' }, + migrate: migrateDoc, + }; + + withIndex(client, { + numOutOfDate: 1, + docs: [ + [{ _id: 'foo:1', _source: { type: 'foo', foo: { name: 'Bar' } } }], + [{ _id: 'foo:2', _source: { type: 'foo', foo: { name: 'Baz' } } }], + ], + }); + + await expect(new IndexMigrator(testOpts).migrate()).rejects.toThrowErrorMatchingInlineSnapshot( + `"error migrating document"` + ); + }); }); function withIndex( diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts index 4c9d2e870a7bb..83dc042d2b96b 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.test.ts @@ -90,4 +90,18 @@ describe('migrateRawDocs', () => { expect(logger.error).toBeCalledTimes(1); }); + + test('rejects when the transform function throws an error', async () => { + const transform = jest.fn((doc: any) => { + throw new Error('error during transform'); + }); + await expect( + migrateRawDocs( + new SavedObjectsSerializer(new SavedObjectTypeRegistry()), + transform, + [{ _id: 'a:b', _source: { type: 'a', a: { name: 'AAA' } } }], + createSavedObjectsMigrationLoggerMock() + ) + ).rejects.toThrowErrorMatchingInlineSnapshot(`"error during transform"`); + }); }); diff --git a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts index 2bdf59d25dc74..5a5048d8ad88f 100644 --- a/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts +++ b/src/core/server/saved_objects/migrations/core/migrate_raw_docs.ts @@ -78,10 +78,14 @@ function transformNonBlocking( ): (doc: SavedObjectUnsanitizedDoc) => Promise { // promises aren't enough to unblock the event loop return (doc: SavedObjectUnsanitizedDoc) => - new Promise((resolve) => { + new Promise((resolve, reject) => { // set immediate is though setImmediate(() => { - resolve(transform(doc)); + try { + resolve(transform(doc)); + } catch (e) { + reject(e); + } }); }); } diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts index cc443093e30a3..7eb2cfefe4620 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.test.ts @@ -134,7 +134,6 @@ const mockOptions = () => { const options: MockedOptions = { logger: loggingSystemMock.create().get(), kibanaVersion: '8.2.3', - savedObjectValidations: {}, typeRegistry: createRegistry([ { name: 'testtype', diff --git a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts index 85b9099308807..18a385c6994b8 100644 --- a/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts +++ b/src/core/server/saved_objects/migrations/kibana/kibana_migrator.ts @@ -28,7 +28,6 @@ import { BehaviorSubject } from 'rxjs'; import { Logger } from '../../../logging'; import { IndexMapping, SavedObjectsTypeMappingDefinitions } from '../../mappings'; import { SavedObjectUnsanitizedDoc, SavedObjectsSerializer } from '../../serialization'; -import { docValidator, PropertyValidators } from '../../validation'; import { buildActiveMappings, IndexMigrator, MigrationResult, MigrationStatus } from '../core'; import { DocumentMigrator, VersionedTransformer } from '../core/document_migrator'; import { MigrationEsClient } from '../core/'; @@ -44,7 +43,6 @@ export interface KibanaMigratorOptions { kibanaConfig: KibanaConfigType; kibanaVersion: string; logger: Logger; - savedObjectValidations: PropertyValidators; } export type IKibanaMigrator = Pick; @@ -80,7 +78,6 @@ export class KibanaMigrator { typeRegistry, kibanaConfig, savedObjectsConfig, - savedObjectValidations, kibanaVersion, logger, }: KibanaMigratorOptions) { @@ -94,7 +91,6 @@ export class KibanaMigrator { this.documentMigrator = new DocumentMigrator({ kibanaVersion, typeRegistry, - validateDoc: docValidator(savedObjectValidations || {}), log: this.log, }); // Building the active mappings (and associated md5sums) is an expensive @@ -124,9 +120,17 @@ export class KibanaMigrator { Array<{ status: string }> > { if (this.migrationResult === undefined || rerun) { - this.status$.next({ status: 'running' }); + // Reruns are only used by CI / EsArchiver. Publishing status updates on reruns results in slowing down CI + // unnecessarily, so we skip it in this case. + if (!rerun) { + this.status$.next({ status: 'running' }); + } + this.migrationResult = this.runMigrationsInternal().then((result) => { - this.status$.next({ status: 'completed', result }); + // Similar to above, don't publish status updates when rerunning in CI. + if (!rerun) { + this.status$.next({ status: 'completed', result }); + } return result; }); } diff --git a/src/core/server/saved_objects/routes/bulk_update.ts b/src/core/server/saved_objects/routes/bulk_update.ts index c112833b29f3f..882213644146a 100644 --- a/src/core/server/saved_objects/routes/bulk_update.ts +++ b/src/core/server/saved_objects/routes/bulk_update.ts @@ -40,6 +40,7 @@ export const registerBulkUpdateRoute = (router: IRouter) => { }) ) ), + namespace: schema.maybe(schema.string({ minLength: 1 })), }) ), }, diff --git a/src/core/server/saved_objects/saved_objects_service.mock.ts b/src/core/server/saved_objects/saved_objects_service.mock.ts index 6f5ecb1eb464b..e3d44c20dd190 100644 --- a/src/core/server/saved_objects/saved_objects_service.mock.ts +++ b/src/core/server/saved_objects/saved_objects_service.mock.ts @@ -26,8 +26,7 @@ import { SavedObjectsServiceSetup, SavedObjectsServiceStart, } from './saved_objects_service'; -import { mockKibanaMigrator } from './migrations/kibana/kibana_migrator.mock'; -import { savedObjectsClientProviderMock } from './service/lib/scoped_client_provider.mock'; + import { savedObjectsRepositoryMock } from './service/lib/repository.mock'; import { savedObjectsClientMock } from './service/saved_objects_client.mock'; import { typeRegistryMock } from './saved_objects_type_registry.mock'; @@ -54,11 +53,7 @@ const createStartContractMock = () => { }; const createInternalStartContractMock = () => { - const internalStartContract: jest.Mocked = { - ...createStartContractMock(), - clientProvider: savedObjectsClientProviderMock.create(), - migrator: mockKibanaMigrator.create(), - }; + const internalStartContract: jest.Mocked = createStartContractMock(); return internalStartContract; }; diff --git a/src/core/server/saved_objects/saved_objects_service.test.ts b/src/core/server/saved_objects/saved_objects_service.test.ts index 8df6a07318c45..d6b30889eba5f 100644 --- a/src/core/server/saved_objects/saved_objects_service.test.ts +++ b/src/core/server/saved_objects/saved_objects_service.test.ts @@ -33,7 +33,6 @@ import { Env } from '../config'; import { configServiceMock } from '../mocks'; import { elasticsearchServiceMock } from '../elasticsearch/elasticsearch_service.mock'; import { elasticsearchClientMock } from '../elasticsearch/client/mocks'; -import { legacyServiceMock } from '../legacy/legacy_service.mock'; import { httpServiceMock } from '../http/http_service.mock'; import { httpServerMock } from '../http/http_server.mocks'; import { SavedObjectsClientFactoryProvider } from './service/lib'; @@ -65,7 +64,6 @@ describe('SavedObjectsService', () => { return { http: httpServiceMock.createInternalSetupContract(), elasticsearch: elasticsearchMock, - legacyPlugins: legacyServiceMock.createDiscoverPlugins(), }; }; @@ -239,8 +237,7 @@ describe('SavedObjectsService', () => { await soService.setup(createSetupDeps()); expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(0); - const startContract = await soService.start(createStartDeps()); - expect(startContract.migrator).toBe(migratorInstanceMock); + await soService.start(createStartDeps()); expect(migratorInstanceMock.runMigrations).toHaveBeenCalledTimes(1); }); diff --git a/src/core/server/saved_objects/saved_objects_service.ts b/src/core/server/saved_objects/saved_objects_service.ts index f05e912b12ad8..5cc59d55a254e 100644 --- a/src/core/server/saved_objects/saved_objects_service.ts +++ b/src/core/server/saved_objects/saved_objects_service.ts @@ -23,12 +23,10 @@ import { CoreService } from '../../types'; import { SavedObjectsClient, SavedObjectsClientProvider, - ISavedObjectsClientProvider, SavedObjectsClientProviderOptions, } from './'; import { KibanaMigrator, IKibanaMigrator } from './migrations'; import { CoreContext } from '../core_context'; -import { LegacyServiceDiscoverPlugins } from '../legacy'; import { ElasticsearchClient, IClusterClient, @@ -49,9 +47,7 @@ import { SavedObjectsClientWrapperFactory, } from './service/lib/scoped_client_provider'; import { Logger } from '../logging'; -import { convertLegacyTypes } from './utils'; import { SavedObjectTypeRegistry, ISavedObjectTypeRegistry } from './saved_objects_type_registry'; -import { PropertyValidators } from './validation'; import { SavedObjectsSerializer } from './serialization'; import { registerRoutes } from './routes'; import { ServiceStatus } from '../status'; @@ -67,9 +63,6 @@ import { createMigrationEsClient } from './migrations/core/'; * the factory provided to `setClientFactory` and wrapped by all wrappers * registered through `addClientWrapper`. * - * All the setup APIs will throw if called after the service has started, and therefor cannot be used - * from legacy plugin code. Legacy plugins should use the legacy savedObject service until migrated. - * * @example * ```ts * import { SavedObjectsClient, CoreSetup } from 'src/core/server'; @@ -155,9 +148,6 @@ export interface SavedObjectsServiceSetup { * } * } * ``` - * - * @remarks The type definition is an aggregation of the legacy savedObjects `schema`, `mappings` and `migration` concepts. - * This API is the single entry point to register saved object types in the new platform. */ registerType: (type: SavedObjectsType) => void; @@ -230,16 +220,7 @@ export interface SavedObjectsServiceStart { getTypeRegistry: () => ISavedObjectTypeRegistry; } -export interface InternalSavedObjectsServiceStart extends SavedObjectsServiceStart { - /** - * @deprecated Exposed only for injecting into Legacy - */ - migrator: IKibanaMigrator; - /** - * @deprecated Exposed only for injecting into Legacy - */ - clientProvider: ISavedObjectsClientProvider; -} +export type InternalSavedObjectsServiceStart = SavedObjectsServiceStart; /** * Factory provided when invoking a {@link SavedObjectsClientFactoryProvider | client factory provider} @@ -271,7 +252,6 @@ export interface SavedObjectsRepositoryFactory { /** @internal */ export interface SavedObjectsSetupDeps { http: InternalHttpServiceSetup; - legacyPlugins: LegacyServiceDiscoverPlugins; elasticsearch: InternalElasticsearchServiceSetup; } @@ -296,9 +276,8 @@ export class SavedObjectsService private clientFactoryProvider?: SavedObjectsClientFactoryProvider; private clientFactoryWrappers: WrappedClientFactoryWrapper[] = []; - private migrator$ = new Subject(); + private migrator$ = new Subject(); private typeRegistry = new SavedObjectTypeRegistry(); - private validations: PropertyValidators = {}; private started = false; constructor(private readonly coreContext: CoreContext) { @@ -310,13 +289,6 @@ export class SavedObjectsService this.setupDeps = setupDeps; - const legacyTypes = convertLegacyTypes( - setupDeps.legacyPlugins.uiExports, - setupDeps.legacyPlugins.pluginExtendedConfig - ); - legacyTypes.forEach((type) => this.typeRegistry.registerType(type)); - this.validations = setupDeps.legacyPlugins.uiExports.savedObjectValidations || {}; - const savedObjectsConfig = await this.coreContext.configService .atPath('savedObjects') .pipe(first()) @@ -471,8 +443,6 @@ export class SavedObjectsService this.started = true; return { - migrator, - clientProvider, getScopedClient: clientProvider.getClient.bind(clientProvider), createScopedRepository: repositoryFactory.createScopedRepository, createInternalRepository: repositoryFactory.createInternalRepository, @@ -488,13 +458,12 @@ export class SavedObjectsService savedObjectsConfig: SavedObjectsMigrationConfigType, client: IClusterClient, migrationsRetryDelay?: number - ): KibanaMigrator { + ): IKibanaMigrator { return new KibanaMigrator({ typeRegistry: this.typeRegistry, logger: this.logger, kibanaVersion: this.coreContext.env.packageInfo.version, savedObjectsConfig, - savedObjectValidations: this.validations, kibanaConfig, client: createMigrationEsClient(client.asInternalUser, this.logger, migrationsRetryDelay), }); diff --git a/src/core/server/saved_objects/schema/schema.test.ts b/src/core/server/saved_objects/schema/schema.test.ts deleted file mode 100644 index f2daa13e43fce..0000000000000 --- a/src/core/server/saved_objects/schema/schema.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { SavedObjectsSchema, SavedObjectsSchemaDefinition } from './schema'; - -describe('#isNamespaceAgnostic', () => { - const expectResult = (expected: boolean, schemaDefinition?: SavedObjectsSchemaDefinition) => { - const schema = new SavedObjectsSchema(schemaDefinition); - const result = schema.isNamespaceAgnostic('foo'); - expect(result).toBe(expected); - }; - - it(`returns false when no schema is defined`, () => { - expectResult(false); - }); - - it(`returns false for unknown types`, () => { - expectResult(false, { bar: {} }); - }); - - it(`returns false for non-namespace-agnostic type`, () => { - expectResult(false, { foo: { isNamespaceAgnostic: false } }); - expectResult(false, { foo: { isNamespaceAgnostic: undefined } }); - }); - - it(`returns true for explicitly namespace-agnostic type`, () => { - expectResult(true, { foo: { isNamespaceAgnostic: true } }); - }); -}); - -describe('#isSingleNamespace', () => { - const expectResult = (expected: boolean, schemaDefinition?: SavedObjectsSchemaDefinition) => { - const schema = new SavedObjectsSchema(schemaDefinition); - const result = schema.isSingleNamespace('foo'); - expect(result).toBe(expected); - }; - - it(`returns true when no schema is defined`, () => { - expectResult(true); - }); - - it(`returns true for unknown types`, () => { - expectResult(true, { bar: {} }); - }); - - it(`returns false for explicitly namespace-agnostic type`, () => { - expectResult(false, { foo: { isNamespaceAgnostic: true } }); - }); - - it(`returns false for explicitly multi-namespace type`, () => { - expectResult(false, { foo: { multiNamespace: true } }); - }); - - it(`returns true for non-namespace-agnostic and non-multi-namespace type`, () => { - expectResult(true, { foo: { isNamespaceAgnostic: false, multiNamespace: false } }); - expectResult(true, { foo: { isNamespaceAgnostic: false, multiNamespace: undefined } }); - expectResult(true, { foo: { isNamespaceAgnostic: undefined, multiNamespace: false } }); - expectResult(true, { foo: { isNamespaceAgnostic: undefined, multiNamespace: undefined } }); - }); -}); - -describe('#isMultiNamespace', () => { - const expectResult = (expected: boolean, schemaDefinition?: SavedObjectsSchemaDefinition) => { - const schema = new SavedObjectsSchema(schemaDefinition); - const result = schema.isMultiNamespace('foo'); - expect(result).toBe(expected); - }; - - it(`returns false when no schema is defined`, () => { - expectResult(false); - }); - - it(`returns false for unknown types`, () => { - expectResult(false, { bar: {} }); - }); - - it(`returns false for explicitly namespace-agnostic type`, () => { - expectResult(false, { foo: { isNamespaceAgnostic: true } }); - }); - - it(`returns false for non-multi-namespace type`, () => { - expectResult(false, { foo: { multiNamespace: false } }); - expectResult(false, { foo: { multiNamespace: undefined } }); - }); - - it(`returns true for non-namespace-agnostic and explicitly multi-namespace type`, () => { - expectResult(true, { foo: { isNamespaceAgnostic: false, multiNamespace: true } }); - expectResult(true, { foo: { isNamespaceAgnostic: undefined, multiNamespace: true } }); - }); -}); diff --git a/src/core/server/saved_objects/schema/schema.ts b/src/core/server/saved_objects/schema/schema.ts deleted file mode 100644 index ba1905158e822..0000000000000 --- a/src/core/server/saved_objects/schema/schema.ts +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { LegacyConfig } from '../../legacy'; - -/** - * @deprecated - * @internal - **/ -interface SavedObjectsSchemaTypeDefinition { - isNamespaceAgnostic?: boolean; - multiNamespace?: boolean; - hidden?: boolean; - indexPattern?: ((config: LegacyConfig) => string) | string; - convertToAliasScript?: string; -} - -/** - * @deprecated - * @internal - **/ -export interface SavedObjectsSchemaDefinition { - [type: string]: SavedObjectsSchemaTypeDefinition; -} - -/** - * @deprecated This is only used by the {@link SavedObjectsLegacyService | legacy savedObjects service} - * @internal - **/ -export class SavedObjectsSchema { - private readonly definition?: SavedObjectsSchemaDefinition; - constructor(schemaDefinition?: SavedObjectsSchemaDefinition) { - this.definition = schemaDefinition; - } - - public isHiddenType(type: string) { - if (this.definition && this.definition.hasOwnProperty(type)) { - return Boolean(this.definition[type].hidden); - } - - return false; - } - - public getIndexForType(config: LegacyConfig, type: string): string | undefined { - if (this.definition != null && this.definition.hasOwnProperty(type)) { - const { indexPattern } = this.definition[type]; - return typeof indexPattern === 'function' ? indexPattern(config) : indexPattern; - } else { - return undefined; - } - } - - public getConvertToAliasScript(type: string): string | undefined { - if (this.definition != null && this.definition.hasOwnProperty(type)) { - return this.definition[type].convertToAliasScript; - } - } - - public isNamespaceAgnostic(type: string) { - // if no plugins have registered a Saved Objects Schema, - // this.schema will be undefined, and no types are namespace agnostic - if (!this.definition) { - return false; - } - - const typeSchema = this.definition[type]; - if (!typeSchema) { - return false; - } - return Boolean(typeSchema.isNamespaceAgnostic); - } - - public isSingleNamespace(type: string) { - // if no plugins have registered a Saved Objects Schema, - // this.schema will be undefined, and all types are namespace isolated - if (!this.definition) { - return true; - } - - const typeSchema = this.definition[type]; - if (!typeSchema) { - return true; - } - return !Boolean(typeSchema.isNamespaceAgnostic) && !Boolean(typeSchema.multiNamespace); - } - - public isMultiNamespace(type: string) { - // if no plugins have registered a Saved Objects Schema, - // this.schema will be undefined, and no types are multi-namespace - if (!this.definition) { - return false; - } - - const typeSchema = this.definition[type]; - if (!typeSchema) { - return false; - } - return !Boolean(typeSchema.isNamespaceAgnostic) && Boolean(typeSchema.multiNamespace); - } -} diff --git a/src/core/server/saved_objects/service/index.ts b/src/core/server/saved_objects/service/index.ts index 9f625b4732e26..c33a9f2f3b157 100644 --- a/src/core/server/saved_objects/service/index.ts +++ b/src/core/server/saved_objects/service/index.ts @@ -17,37 +17,6 @@ * under the License. */ -import { Readable } from 'stream'; -import { SavedObjectsClientProvider } from './lib'; -import { SavedObjectsClient } from './saved_objects_client'; -import { SavedObjectsExportOptions } from '../export'; -import { SavedObjectsImportOptions, SavedObjectsImportResponse } from '../import'; -import { SavedObjectsSchema } from '../schema'; -import { SavedObjectsResolveImportErrorsOptions } from '../import/types'; - -/** - * @internal - * @deprecated - */ -export interface SavedObjectsLegacyService { - // ATTENTION: these types are incomplete - addScopedSavedObjectsClientWrapperFactory: SavedObjectsClientProvider['addClientWrapperFactory']; - setScopedSavedObjectsClientFactory: SavedObjectsClientProvider['setClientFactory']; - getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient']; - SavedObjectsClient: typeof SavedObjectsClient; - types: string[]; - schema: SavedObjectsSchema; - getSavedObjectsRepository(...rest: any[]): any; - importExport: { - objectLimit: number; - importSavedObjects(options: SavedObjectsImportOptions): Promise; - resolveImportErrors( - options: SavedObjectsResolveImportErrorsOptions - ): Promise; - getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise; - }; -} - export { SavedObjectsRepository, SavedObjectsClientProvider, @@ -58,6 +27,7 @@ export { SavedObjectsErrorHelpers, SavedObjectsClientFactory, SavedObjectsClientFactoryProvider, + SavedObjectsUtils, } from './lib'; export * from './saved_objects_client'; diff --git a/src/core/server/saved_objects/service/lib/index.ts b/src/core/server/saved_objects/service/lib/index.ts index e103120388e35..eae8c5ef2e10c 100644 --- a/src/core/server/saved_objects/service/lib/index.ts +++ b/src/core/server/saved_objects/service/lib/index.ts @@ -30,3 +30,5 @@ export { } from './scoped_client_provider'; export { SavedObjectsErrorHelpers } from './errors'; + +export { SavedObjectsUtils } from './utils'; diff --git a/src/core/server/saved_objects/service/lib/repository.test.js b/src/core/server/saved_objects/service/lib/repository.test.js index b1d6028465713..7d30875b90796 100644 --- a/src/core/server/saved_objects/service/lib/repository.test.js +++ b/src/core/server/saved_objects/service/lib/repository.test.js @@ -153,30 +153,35 @@ describe('SavedObjectsRepository', () => { typeRegistry: registry, kibanaVersion: '2.0.0', log: {}, - validateDoc: jest.fn(), }); - const getMockGetResponse = ({ type, id, references, namespace, originId }) => ({ - // NOTE: Elasticsearch returns more fields (_index, _type) but the SavedObjectsRepository method ignores these - found: true, - _id: `${registry.isSingleNamespace(type) && namespace ? `${namespace}:` : ''}${type}:${id}`, - ...mockVersionProps, - _source: { - ...(registry.isSingleNamespace(type) && { namespace }), - ...(registry.isMultiNamespace(type) && { namespaces: [namespace ?? 'default'] }), - ...(originId && { originId }), - type, - [type]: { title: 'Testing' }, - references, - specialProperty: 'specialValue', - ...mockTimestampFields, - }, - }); + const getMockGetResponse = ( + { type, id, references, namespace: objectNamespace, originId }, + namespace + ) => { + const namespaceId = objectNamespace === 'default' ? undefined : objectNamespace ?? namespace; + return { + // NOTE: Elasticsearch returns more fields (_index, _type) but the SavedObjectsRepository method ignores these + found: true, + _id: `${ + registry.isSingleNamespace(type) && namespaceId ? `${namespaceId}:` : '' + }${type}:${id}`, + ...mockVersionProps, + _source: { + ...(registry.isSingleNamespace(type) && { namespace: namespaceId }), + ...(registry.isMultiNamespace(type) && { namespaces: [namespaceId ?? 'default'] }), + ...(originId && { originId }), + type, + [type]: { title: 'Testing' }, + references, + specialProperty: 'specialValue', + ...mockTimestampFields, + }, + }; + }; const getMockMgetResponse = (objects, namespace) => ({ - docs: objects.map((obj) => - obj.found === false ? obj : getMockGetResponse({ ...obj, namespace }) - ), + docs: objects.map((obj) => (obj.found === false ? obj : getMockGetResponse(obj, namespace))), }); expect.extend({ @@ -587,6 +592,16 @@ describe('SavedObjectsRepository', () => { ); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await bulkCreateSuccess([obj1, obj2], { namespace: 'default' }); + const expected = expect.not.objectContaining({ namespace: 'default' }); + const body = [expect.any(Object), expected, expect.any(Object), expected]; + expect(client.bulk).toHaveBeenCalledWith( + expect.objectContaining({ body }), + expect.anything() + ); + }); + it(`doesn't add namespace to request body for any types that are not single-namespace`, async () => { const objects = [ { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, @@ -654,19 +669,19 @@ describe('SavedObjectsRepository', () => { }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - const getId = (type, id) => `${namespace}:${type}:${id}`; + const getId = (type, id) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) await bulkCreateSuccess([obj1, obj2], { namespace }); expectClientCallArgsAction([obj1, obj2], { method: 'create', getId }); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) await bulkCreateSuccess([obj1, obj2]); expectClientCallArgsAction([obj1, obj2], { method: 'create', getId }); }); it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) const objects = [ { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }, { ...obj2, type: MULTI_NAMESPACE_TYPE }, @@ -973,19 +988,25 @@ describe('SavedObjectsRepository', () => { describe('client calls', () => { it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - const getId = (type, id) => `${namespace}:${type}:${id}`; + const getId = (type, id) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) await bulkGetSuccess([obj1, obj2], { namespace }); _expectClientCallArgs([obj1, obj2], { getId }); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) await bulkGetSuccess([obj1, obj2]); _expectClientCallArgs([obj1, obj2], { getId }); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) + await bulkGetSuccess([obj1, obj2], { namespace: 'default' }); + _expectClientCallArgs([obj1, obj2], { getId }); + }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) let objects = [obj1, obj2].map((obj) => ({ ...obj, type: NAMESPACE_AGNOSTIC_TYPE })); await bulkGetSuccess(objects, { namespace }); _expectClientCallArgs(objects, { getId }); @@ -1328,32 +1349,66 @@ describe('SavedObjectsRepository', () => { }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - const getId = (type, id) => `${namespace}:${type}:${id}`; + const getId = (type, id) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) await bulkUpdateSuccess([obj1, obj2], { namespace }); expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); + + jest.clearAllMocks(); + // test again with object namespace string that supersedes the operation's namespace ID + await bulkUpdateSuccess([ + { ...obj1, namespace }, + { ...obj2, namespace }, + ]); + expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) await bulkUpdateSuccess([obj1, obj2]); expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); + + jest.clearAllMocks(); + // test again with object namespace string that supersedes the operation's namespace ID + await bulkUpdateSuccess( + [ + { ...obj1, namespace: 'default' }, + { ...obj2, namespace: 'default' }, + ], + { namespace } + ); + expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); }); - it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + it(`normalizes options.namespace from 'default' to undefined`, async () => { const getId = (type, id) => `${type}:${id}`; - const objects1 = [{ ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }]; - await bulkUpdateSuccess(objects1, { namespace }); - expectClientCallArgsAction(objects1, { method: 'update', getId }); - client.bulk.mockClear(); + await bulkUpdateSuccess([obj1, obj2], { namespace: 'default' }); + expectClientCallArgsAction([obj1, obj2], { method: 'update', getId }); + }); + + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) const overrides = { // bulkUpdate uses a preflight `get` request for multi-namespace saved objects, and specifies that version on `update` // we aren't testing for this here, but we need to include Jest assertions so this test doesn't fail if_primary_term: expect.any(Number), if_seq_no: expect.any(Number), }; - const objects2 = [{ ...obj2, type: MULTI_NAMESPACE_TYPE }]; - await bulkUpdateSuccess(objects2, { namespace }); - expectClientCallArgsAction(objects2, { method: 'update', getId, overrides }, 2); + const _obj1 = { ...obj1, type: NAMESPACE_AGNOSTIC_TYPE }; + const _obj2 = { ...obj2, type: MULTI_NAMESPACE_TYPE }; + + await bulkUpdateSuccess([_obj1], { namespace }); + expectClientCallArgsAction([_obj1], { method: 'update', getId }); + client.bulk.mockClear(); + await bulkUpdateSuccess([_obj2], { namespace }); + expectClientCallArgsAction([_obj2], { method: 'update', getId, overrides }, 2); + + jest.clearAllMocks(); + // test again with object namespace string that supersedes the operation's namespace ID + await bulkUpdateSuccess([{ ..._obj1, namespace }]); + expectClientCallArgsAction([_obj1], { method: 'update', getId }); + client.bulk.mockClear(); + await bulkUpdateSuccess([{ ..._obj2, namespace }]); + expectClientCallArgsAction([_obj2], { method: 'update', getId, overrides }, 2); }); }); @@ -1582,19 +1637,25 @@ describe('SavedObjectsRepository', () => { }); it(`prepends namespace to the id when providing namespace for single-namespace type`, async () => { - const getId = (type, id) => `${namespace}:${type}:${id}`; + const getId = (type, id) => `${namespace}:${type}:${id}`; // test that the raw document ID equals this (e.g., has a namespace prefix) await checkConflictsSuccess([obj1, obj2], { namespace }); _expectClientCallArgs([obj1, obj2], { getId }); }); it(`doesn't prepend namespace to the id when providing no namespace for single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) await checkConflictsSuccess([obj1, obj2]); _expectClientCallArgs([obj1, obj2], { getId }); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) + await checkConflictsSuccess([obj1, obj2], { namespace: 'default' }); + _expectClientCallArgs([obj1, obj2], { getId }); + }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { - const getId = (type, id) => `${type}:${id}`; + const getId = (type, id) => `${type}:${id}`; // test that the raw document ID equals this (e.g., does not have a namespace prefix) // obj3 is multi-namespace, and obj6 is namespace-agnostic await checkConflictsSuccess([obj3, obj6], { namespace }); _expectClientCallArgs([obj3, obj6], { getId }); @@ -1817,6 +1878,16 @@ describe('SavedObjectsRepository', () => { ); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await createSuccess(type, attributes, { id, namespace: 'default' }); + expect(client.create).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + }), + expect.anything() + ); + }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { await createSuccess(NAMESPACE_AGNOSTIC_TYPE, attributes, { id, namespace }); expect(client.create).toHaveBeenCalledWith( @@ -1853,11 +1924,7 @@ describe('SavedObjectsRepository', () => { }); it(`throws when there is a conflict with an existing multi-namespace saved object (get)`, async () => { - const response = getMockGetResponse({ - type: MULTI_NAMESPACE_TYPE, - id, - namespace: 'bar-namespace', - }); + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }, 'bar-namespace'); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); @@ -1960,7 +2027,7 @@ describe('SavedObjectsRepository', () => { const deleteSuccess = async (type, id, options) => { if (registry.isMultiNamespace(type)) { - const mockGetResponse = getMockGetResponse({ type, id, namespace: options?.namespace }); + const mockGetResponse = getMockGetResponse({ type, id }, options?.namespace); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(mockGetResponse) ); @@ -2036,6 +2103,14 @@ describe('SavedObjectsRepository', () => { ); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await deleteSuccess(type, id, { namespace: 'default' }); + expect(client.delete).toHaveBeenCalledWith( + expect.objectContaining({ id: `${type}:${id}` }), + expect.anything() + ); + }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { await deleteSuccess(NAMESPACE_AGNOSTIC_TYPE, id, { namespace }); expect(client.delete).toHaveBeenCalledWith( @@ -2086,7 +2161,7 @@ describe('SavedObjectsRepository', () => { }); it(`throws when the type is multi-namespace and the document exists, but not in this namespace`, async () => { - const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id, namespace }); + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }, namespace); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); @@ -2661,14 +2736,16 @@ describe('SavedObjectsRepository', () => { const originId = 'some-origin-id'; const getSuccess = async (type, id, options, includeOriginId) => { - const response = getMockGetResponse({ - type, - id, - namespace: options?.namespace, - // "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the - // operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response. - ...(includeOriginId && { originId }), - }); + const response = getMockGetResponse( + { + type, + id, + // "includeOriginId" is not an option for the operation; however, if the existing saved object contains an originId attribute, the + // operation will return it in the result. This flag is just used for test purposes to modify the mock cluster call response. + ...(includeOriginId && { originId }), + }, + options?.namespace + ); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); @@ -2703,6 +2780,16 @@ describe('SavedObjectsRepository', () => { ); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await getSuccess(type, id, { namespace: 'default' }); + expect(client.get).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + }), + expect.anything() + ); + }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { await getSuccess(NAMESPACE_AGNOSTIC_TYPE, id, { namespace }); expect(client.get).toHaveBeenCalledWith( @@ -2757,7 +2844,7 @@ describe('SavedObjectsRepository', () => { }); it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { - const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id, namespace }); + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }, namespace); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); @@ -2813,7 +2900,7 @@ describe('SavedObjectsRepository', () => { const incrementCounterSuccess = async (type, id, field, options) => { const isMultiNamespace = registry.isMultiNamespace(type); if (isMultiNamespace) { - const response = getMockGetResponse({ type, id, namespace: options?.namespace }); + const response = getMockGetResponse({ type, id }, options?.namespace); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); @@ -2884,6 +2971,16 @@ describe('SavedObjectsRepository', () => { ); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await incrementCounterSuccess(type, id, field, { namespace: 'default' }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ + id: `${type}:${id}`, + }), + expect.anything() + ); + }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { await incrementCounterSuccess(NAMESPACE_AGNOSTIC_TYPE, id, field, { namespace }); expect(client.update).toHaveBeenCalledWith( @@ -2950,11 +3047,7 @@ describe('SavedObjectsRepository', () => { }); it(`throws when there is a conflict with an existing multi-namespace saved object (get)`, async () => { - const response = getMockGetResponse({ - type: MULTI_NAMESPACE_TYPE, - id, - namespace: 'bar-namespace', - }); + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }, 'bar-namespace'); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); @@ -3247,7 +3340,7 @@ describe('SavedObjectsRepository', () => { expect(client.update).not.toHaveBeenCalled(); }); - it(`throws when type is not namespace-agnostic`, async () => { + it(`throws when type is not multi-namespace`, async () => { const test = async (type) => { const message = `${type} doesn't support multiple namespaces`; await expectBadRequestError(type, id, [namespace1, namespace2], message); @@ -3389,7 +3482,7 @@ describe('SavedObjectsRepository', () => { const updateSuccess = async (type, id, attributes, options, includeOriginId) => { if (registry.isMultiNamespace(type)) { - const mockGetResponse = getMockGetResponse({ type, id, namespace: options?.namespace }); + const mockGetResponse = getMockGetResponse({ type, id }, options?.namespace); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(mockGetResponse) ); @@ -3520,6 +3613,14 @@ describe('SavedObjectsRepository', () => { ); }); + it(`normalizes options.namespace from 'default' to undefined`, async () => { + await updateSuccess(type, id, attributes, { references, namespace: 'default' }); + expect(client.update).toHaveBeenCalledWith( + expect.objectContaining({ id: expect.stringMatching(`${type}:${id}`) }), + expect.anything() + ); + }); + it(`doesn't prepend namespace to the id when not using single-namespace type`, async () => { await updateSuccess(NAMESPACE_AGNOSTIC_TYPE, id, attributes, { namespace }); expect(client.update).toHaveBeenCalledWith( @@ -3590,7 +3691,7 @@ describe('SavedObjectsRepository', () => { }); it(`throws when type is multi-namespace and the document exists, but not in this namespace`, async () => { - const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id, namespace }); + const response = getMockGetResponse({ type: MULTI_NAMESPACE_TYPE, id }, namespace); client.get.mockResolvedValueOnce( elasticsearchClientMock.createSuccessTransportRequestPromise(response) ); diff --git a/src/core/server/saved_objects/service/lib/repository.ts b/src/core/server/saved_objects/service/lib/repository.ts index dd25989725f3e..125f97e7feb11 100644 --- a/src/core/server/saved_objects/service/lib/repository.ts +++ b/src/core/server/saved_objects/service/lib/repository.ts @@ -31,7 +31,7 @@ import { getSearchDsl } from './search_dsl'; import { includedFields } from './included_fields'; import { SavedObjectsErrorHelpers, DecoratedError } from './errors'; import { decodeRequestVersion, encodeVersion, encodeHitVersion } from '../../version'; -import { KibanaMigrator } from '../../migrations'; +import { IKibanaMigrator } from '../../migrations'; import { SavedObjectsSerializer, SavedObjectSanitizedDoc, @@ -67,6 +67,7 @@ import { } from '../../types'; import { SavedObjectTypeRegistry } from '../../saved_objects_type_registry'; import { validateConvertFilterToKueryNode } from './filter_utils'; +import { SavedObjectsUtils } from './utils'; // BEWARE: The SavedObjectClient depends on the implementation details of the SavedObjectsRepository // so any breaking changes to this repository are considered breaking changes to the SavedObjectsClient. @@ -85,7 +86,7 @@ export interface SavedObjectsRepositoryOptions { client: ElasticsearchClient; typeRegistry: SavedObjectTypeRegistry; serializer: SavedObjectsSerializer; - migrator: KibanaMigrator; + migrator: IKibanaMigrator; allowedTypes: string[]; } @@ -120,7 +121,7 @@ export type ISavedObjectsRepository = Pick>, options: SavedObjectsCreateOptions = {} ): Promise> { - const { namespace, overwrite = false, refresh = DEFAULT_REFRESH_SETTING } = options; + const { overwrite = false, refresh = DEFAULT_REFRESH_SETTING } = options; + const namespace = normalizeNamespace(options.namespace); const time = this._getCurrentTime(); let bulkGetRequestIndexCounter = 0; @@ -468,7 +470,7 @@ export class SavedObjectsRepository { return { errors: [] }; } - const { namespace } = options; + const namespace = normalizeNamespace(options.namespace); let bulkGetRequestIndexCounter = 0; const expectedBulkGetResults: Either[] = objects.map((object) => { @@ -551,7 +553,8 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const { namespace, refresh = DEFAULT_REFRESH_SETTING } = options; + const { refresh = DEFAULT_REFRESH_SETTING } = options; + const namespace = normalizeNamespace(options.namespace); const rawId = this._serializer.generateRawId(namespace, type, id); let preflightResult: SavedObjectsRawDoc | undefined; @@ -560,7 +563,7 @@ export class SavedObjectsRepository { preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); const existingNamespaces = getSavedObjectNamespaces(undefined, preflightResult); const remainingNamespaces = existingNamespaces?.filter( - (x) => x !== getNamespaceString(namespace) + (x) => x !== SavedObjectsUtils.namespaceIdToString(namespace) ); if (remainingNamespaces?.length) { @@ -658,7 +661,7 @@ export class SavedObjectsRepository { } `, lang: 'painless', - params: { namespace: getNamespaceString(namespace) }, + params: { namespace }, }, conflicts: 'proceed', ...getSearchDsl(this._mappings, this._registry, { @@ -814,7 +817,7 @@ export class SavedObjectsRepository { objects: SavedObjectsBulkGetObject[] = [], options: SavedObjectsBaseOptions = {} ): Promise> { - const { namespace } = options; + const namespace = normalizeNamespace(options.namespace); if (objects.length === 0) { return { saved_objects: [] }; @@ -884,7 +887,9 @@ export class SavedObjectsRepository { const { originId, updated_at: updatedAt } = doc._source; let namespaces = []; if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = doc._source.namespaces ?? [getNamespaceString(doc._source.namespace)]; + namespaces = doc._source.namespaces ?? [ + SavedObjectsUtils.namespaceIdToString(doc._source.namespace), + ]; } return { @@ -920,7 +925,7 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const { namespace } = options; + const namespace = normalizeNamespace(options.namespace); const { body, statusCode } = await this.client.get>( { @@ -941,7 +946,9 @@ export class SavedObjectsRepository { let namespaces: string[] = []; if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = body._source.namespaces ?? [getNamespaceString(body._source.namespace)]; + namespaces = body._source.namespaces ?? [ + SavedObjectsUtils.namespaceIdToString(body._source.namespace), + ]; } return { @@ -978,7 +985,8 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createGenericNotFoundError(type, id); } - const { version, namespace, references, refresh = DEFAULT_REFRESH_SETTING } = options; + const { version, references, refresh = DEFAULT_REFRESH_SETTING } = options; + const namespace = normalizeNamespace(options.namespace); let preflightResult: SavedObjectsRawDoc | undefined; if (this._registry.isMultiNamespace(type)) { @@ -1016,7 +1024,9 @@ export class SavedObjectsRepository { const { originId } = body.get._source; let namespaces = []; if (!this._registry.isNamespaceAgnostic(type)) { - namespaces = body.get._source.namespaces ?? [getNamespaceString(body.get._source.namespace)]; + namespaces = body.get._source.namespaces ?? [ + SavedObjectsUtils.namespaceIdToString(body.get._source.namespace), + ]; } return { @@ -1060,6 +1070,7 @@ export class SavedObjectsRepository { } const { version, namespace, refresh = DEFAULT_REFRESH_SETTING } = options; + // we do not need to normalize the namespace to its ID format, since it will be converted to a namespace string before being used const rawId = this._serializer.generateRawId(undefined, type, id); const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); @@ -1122,6 +1133,7 @@ export class SavedObjectsRepository { } const { namespace, refresh = DEFAULT_REFRESH_SETTING } = options; + // we do not need to normalize the namespace to its ID format, since it will be converted to a namespace string before being used const rawId = this._serializer.generateRawId(undefined, type, id); const preflightResult = await this.preflightCheckIncludesNamespace(type, id, namespace); @@ -1208,7 +1220,7 @@ export class SavedObjectsRepository { options: SavedObjectsBulkUpdateOptions = {} ): Promise> { const time = this._getCurrentTime(); - const { namespace } = options; + const namespace = normalizeNamespace(options.namespace); let bulkGetRequestIndexCounter = 0; const expectedBulkGetResults: Either[] = objects.map((object) => { @@ -1225,7 +1237,9 @@ export class SavedObjectsRepository { }; } - const { attributes, references, version } = object; + const { attributes, references, version, namespace: objectNamespace } = object; + // `objectNamespace` is a namespace string, while `namespace` is a namespace ID. + // The object namespace string, if defined, will supersede the operation's namespace ID. const documentToSave = { [type]: attributes, @@ -1242,16 +1256,24 @@ export class SavedObjectsRepository { id, version, documentToSave, + objectNamespace, ...(requiresNamespacesCheck && { esRequestIndex: bulkGetRequestIndexCounter++ }), }, }; }); + const getNamespaceId = (objectNamespace?: string) => + objectNamespace !== undefined + ? SavedObjectsUtils.namespaceStringToId(objectNamespace) + : namespace; + const getNamespaceString = (objectNamespace?: string) => + objectNamespace ?? SavedObjectsUtils.namespaceIdToString(namespace); + const bulkGetDocs = expectedBulkGetResults .filter(isRight) .filter(({ value }) => value.esRequestIndex !== undefined) - .map(({ value: { type, id } }) => ({ - _id: this._serializer.generateRawId(namespace, type, id), + .map(({ value: { type, id, objectNamespace } }) => ({ + _id: this._serializer.generateRawId(getNamespaceId(objectNamespace), type, id), _index: this.getIndexForType(type), _source: ['type', 'namespaces'], })); @@ -1276,14 +1298,25 @@ export class SavedObjectsRepository { return expectedBulkGetResult; } - const { esRequestIndex, id, type, version, documentToSave } = expectedBulkGetResult.value; + const { + esRequestIndex, + id, + type, + version, + documentToSave, + objectNamespace, + } = expectedBulkGetResult.value; + let namespaces; let versionProperties; if (esRequestIndex !== undefined) { const indexFound = bulkGetResponse?.statusCode !== 404; const actualResult = indexFound ? bulkGetResponse?.body.docs[esRequestIndex] : undefined; const docFound = indexFound && actualResult.found === true; - if (!docFound || !this.rawDocExistsInNamespace(actualResult, namespace)) { + if ( + !docFound || + !this.rawDocExistsInNamespace(actualResult, getNamespaceId(objectNamespace)) + ) { return { tag: 'Left' as 'Left', error: { @@ -1294,12 +1327,13 @@ export class SavedObjectsRepository { }; } namespaces = actualResult._source.namespaces ?? [ - getNamespaceString(actualResult._source.namespace), + SavedObjectsUtils.namespaceIdToString(actualResult._source.namespace), ]; versionProperties = getExpectedVersionProperties(version, actualResult); } else { if (this._registry.isSingleNamespace(type)) { - namespaces = [getNamespaceString(namespace)]; + // if `objectNamespace` is undefined, fall back to `options.namespace` + namespaces = [getNamespaceString(objectNamespace)]; } versionProperties = getExpectedVersionProperties(version); } @@ -1315,7 +1349,7 @@ export class SavedObjectsRepository { bulkUpdateParams.push( { update: { - _id: this._serializer.generateRawId(namespace, type, id), + _id: this._serializer.generateRawId(getNamespaceId(objectNamespace), type, id), _index: this.getIndexForType(type), ...versionProperties, }, @@ -1401,7 +1435,8 @@ export class SavedObjectsRepository { throw SavedObjectsErrorHelpers.createUnsupportedTypeError(type); } - const { migrationVersion, namespace, refresh = DEFAULT_REFRESH_SETTING } = options; + const { migrationVersion, refresh = DEFAULT_REFRESH_SETTING } = options; + const namespace = normalizeNamespace(options.namespace); const time = this._getCurrentTime(); let savedObjectNamespace; @@ -1495,7 +1530,7 @@ export class SavedObjectsRepository { const savedObject = this._serializer.rawToSavedObject(raw); const { namespace, type } = savedObject; if (this._registry.isSingleNamespace(type)) { - savedObject.namespaces = [getNamespaceString(namespace)]; + savedObject.namespaces = [SavedObjectsUtils.namespaceIdToString(namespace)]; } return omit(savedObject, 'namespace') as SavedObject; } @@ -1518,7 +1553,7 @@ export class SavedObjectsRepository { } const namespaces = raw._source.namespaces; - return namespaces?.includes(getNamespaceString(namespace)) ?? false; + return namespaces?.includes(SavedObjectsUtils.namespaceIdToString(namespace)) ?? false; } /** @@ -1623,14 +1658,6 @@ function getExpectedVersionProperties(version?: string, document?: SavedObjectsR return {}; } -/** - * Returns the string representation of a namespace. - * The default namespace is undefined, and is represented by the string 'default'. - */ -function getNamespaceString(namespace?: string) { - return namespace ?? 'default'; -} - /** * Returns a string array of namespaces for a given saved object. If the saved object is undefined, the result is an array that contains the * current namespace. Value may be undefined if an existing saved object has no namespaces attribute; this should not happen in normal @@ -1646,9 +1673,16 @@ function getSavedObjectNamespaces( if (document) { return document._source?.namespaces; } - return [getNamespaceString(namespace)]; + return [SavedObjectsUtils.namespaceIdToString(namespace)]; } +/** + * Ensure that a namespace is always in its namespace ID representation. + * This allows `'default'` to be used interchangeably with `undefined`. + */ +const normalizeNamespace = (namespace?: string) => + namespace === undefined ? namespace : SavedObjectsUtils.namespaceStringToId(namespace); + /** * Extracts the contents of a decorated error to return the attributes for bulk operations. */ diff --git a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts index ad1a08187dc32..3ff72a86c2f89 100644 --- a/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts +++ b/src/core/server/saved_objects/service/lib/search_dsl/query_params.ts @@ -21,6 +21,7 @@ import { esKuery, KueryNode } from '../../../../../../plugins/data/server'; import { getRootPropertiesObjects, IndexMapping } from '../../../mappings'; import { ISavedObjectTypeRegistry } from '../../../saved_objects_type_registry'; +import { DEFAULT_NAMESPACE_STRING } from '../utils'; /** * Gets the types based on the type. Uses mappings to support @@ -73,7 +74,7 @@ function getFieldsForTypes( */ function getClauseForType( registry: ISavedObjectTypeRegistry, - namespaces: string[] = ['default'], + namespaces: string[] = [DEFAULT_NAMESPACE_STRING], type: string ) { if (namespaces.length === 0) { @@ -88,11 +89,11 @@ function getClauseForType( }; } else if (registry.isSingleNamespace(type)) { const should: Array> = []; - const eligibleNamespaces = namespaces.filter((namespace) => namespace !== 'default'); + const eligibleNamespaces = namespaces.filter((x) => x !== DEFAULT_NAMESPACE_STRING); if (eligibleNamespaces.length > 0) { should.push({ terms: { namespace: eligibleNamespaces } }); } - if (namespaces.includes('default')) { + if (namespaces.includes(DEFAULT_NAMESPACE_STRING)) { should.push({ bool: { must_not: [{ exists: { field: 'namespace' } }] } }); } if (should.length === 0) { @@ -162,9 +163,7 @@ export function getQueryParams({ // would result in no results being returned, as the wildcard is treated as a literal, and not _actually_ as a wildcard. // We had a good discussion around the tradeoffs here: https://github.com/elastic/kibana/pull/67644#discussion_r441055716 const normalizedNamespaces = namespaces - ? Array.from( - new Set(namespaces.map((namespace) => (namespace === '*' ? 'default' : namespace))) - ) + ? Array.from(new Set(namespaces.map((x) => (x === '*' ? DEFAULT_NAMESPACE_STRING : x)))) : undefined; const bool: any = { diff --git a/src/core/server/saved_objects/service/lib/utils.test.ts b/src/core/server/saved_objects/service/lib/utils.test.ts new file mode 100644 index 0000000000000..ea4fa68242bea --- /dev/null +++ b/src/core/server/saved_objects/service/lib/utils.test.ts @@ -0,0 +1,57 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SavedObjectsUtils } from './utils'; + +describe('SavedObjectsUtils', () => { + const { namespaceIdToString, namespaceStringToId } = SavedObjectsUtils; + + describe('#namespaceIdToString', () => { + it('converts `undefined` to default namespace string', () => { + expect(namespaceIdToString(undefined)).toEqual('default'); + }); + + it('leaves other namespace IDs as-is', () => { + expect(namespaceIdToString('foo')).toEqual('foo'); + }); + + it('throws an error when a namespace ID is an empty string', () => { + expect(() => namespaceIdToString('')).toThrowError('namespace cannot be an empty string'); + }); + }); + + describe('#namespaceStringToId', () => { + it('converts default namespace string to `undefined`', () => { + expect(namespaceStringToId('default')).toBeUndefined(); + }); + + it('leaves other namespace strings as-is', () => { + expect(namespaceStringToId('foo')).toEqual('foo'); + }); + + it('throws an error when a namespace string is falsy', () => { + const test = (arg: any) => + expect(() => namespaceStringToId(arg)).toThrowError('namespace must be a non-empty string'); + + test(undefined); + test(null); + test(''); + }); + }); +}); diff --git a/src/core/server/saved_objects/service/lib/utils.ts b/src/core/server/saved_objects/service/lib/utils.ts new file mode 100644 index 0000000000000..6101ad57cc401 --- /dev/null +++ b/src/core/server/saved_objects/service/lib/utils.ts @@ -0,0 +1,53 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const DEFAULT_NAMESPACE_STRING = 'default'; + +/** + * @public + */ +export class SavedObjectsUtils { + /** + * Converts a given saved object namespace ID to its string representation. All namespace IDs have an identical string representation, with + * the exception of the `undefined` namespace ID (which has a namespace string of `'default'`). + * + * @param namespace The namespace ID, which must be either a non-empty string or `undefined`. + */ + public static namespaceIdToString = (namespace?: string) => { + if (namespace === '') { + throw new TypeError('namespace cannot be an empty string'); + } + + return namespace ?? DEFAULT_NAMESPACE_STRING; + }; + + /** + * Converts a given saved object namespace string to its ID representation. All namespace strings have an identical ID representation, with + * the exception of the `'default'` namespace string (which has a namespace ID of `undefined`). + * + * @param namespace The namespace string, which must be non-empty. + */ + public static namespaceStringToId = (namespace: string) => { + if (!namespace) { + throw new TypeError('namespace must be a non-empty string'); + } + + return namespace !== DEFAULT_NAMESPACE_STRING ? namespace : undefined; + }; +} diff --git a/src/core/server/saved_objects/service/saved_objects_client.ts b/src/core/server/saved_objects/service/saved_objects_client.ts index 347c760f841bc..8c96116de49cb 100644 --- a/src/core/server/saved_objects/service/saved_objects_client.ts +++ b/src/core/server/saved_objects/service/saved_objects_client.ts @@ -80,6 +80,13 @@ export interface SavedObjectsBulkUpdateObject type: string; /** {@inheritdoc SavedObjectAttributes} */ attributes: Partial; + /** + * Optional namespace string to use when searching for this object. If this is defined, it will supersede the namespace ID that is in + * {@link SavedObjectsBulkUpdateOptions}. + * + * Note: the default namespace's string representation is `'default'`, and its ID representation is `undefined`. + **/ + namespace?: string; } /** diff --git a/src/core/server/saved_objects/types.ts b/src/core/server/saved_objects/types.ts index 000153cd542fa..50c118ca64ffb 100644 --- a/src/core/server/saved_objects/types.ts +++ b/src/core/server/saved_objects/types.ts @@ -18,9 +18,8 @@ */ import { SavedObjectsClient } from './service/saved_objects_client'; -import { SavedObjectsTypeMappingDefinition, SavedObjectsTypeMappingDefinitions } from './mappings'; +import { SavedObjectsTypeMappingDefinition } from './mappings'; import { SavedObjectMigrationMap } from './migrations'; -import { PropertyValidators } from './validation'; export { SavedObjectsImportResponse, @@ -34,9 +33,6 @@ export { SavedObjectsImportRetry, } from './import/types'; -import { LegacyConfig } from '../legacy'; -import { SavedObjectUnsanitizedDoc } from './serialization'; -import { SavedObjectsMigrationLogger } from './migrations/core/migration_logger'; import { SavedObject } from '../../types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -269,92 +265,3 @@ export interface SavedObjectsTypeManagementDefinition { */ getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string }; } - -/** - * @internal - * @deprecated - */ -export interface SavedObjectsLegacyUiExports { - savedObjectMappings: SavedObjectsLegacyMapping[]; - savedObjectMigrations: SavedObjectsLegacyMigrationDefinitions; - savedObjectSchemas: SavedObjectsLegacySchemaDefinitions; - savedObjectValidations: PropertyValidators; - savedObjectsManagement: SavedObjectsLegacyManagementDefinition; -} - -/** - * @internal - * @deprecated - */ -export interface SavedObjectsLegacyMapping { - pluginId: string; - properties: SavedObjectsTypeMappingDefinitions; -} - -/** - * @internal - * @deprecated Use {@link SavedObjectsTypeManagementDefinition | management definition} when registering - * from new platform plugins - */ -export interface SavedObjectsLegacyManagementDefinition { - [key: string]: SavedObjectsLegacyManagementTypeDefinition; -} - -/** - * @internal - * @deprecated - */ -export interface SavedObjectsLegacyManagementTypeDefinition { - isImportableAndExportable?: boolean; - defaultSearchField?: string; - icon?: string; - getTitle?: (savedObject: SavedObject) => string; - getEditUrl?: (savedObject: SavedObject) => string; - getInAppUrl?: (savedObject: SavedObject) => { path: string; uiCapabilitiesPath: string }; -} - -/** - * @internal - * @deprecated - */ -export interface SavedObjectsLegacyMigrationDefinitions { - [type: string]: SavedObjectLegacyMigrationMap; -} - -/** - * @internal - * @deprecated - */ -export interface SavedObjectLegacyMigrationMap { - [version: string]: SavedObjectLegacyMigrationFn; -} - -/** - * @internal - * @deprecated - */ -export type SavedObjectLegacyMigrationFn = ( - doc: SavedObjectUnsanitizedDoc, - log: SavedObjectsMigrationLogger -) => SavedObjectUnsanitizedDoc; - -/** - * @internal - * @deprecated - */ -interface SavedObjectsLegacyTypeSchema { - isNamespaceAgnostic?: boolean; - /** Cannot be used in conjunction with `isNamespaceAgnostic` */ - multiNamespace?: boolean; - hidden?: boolean; - indexPattern?: ((config: LegacyConfig) => string) | string; - convertToAliasScript?: string; -} - -/** - * @internal - * @deprecated - */ -export interface SavedObjectsLegacySchemaDefinitions { - [type: string]: SavedObjectsLegacyTypeSchema; -} diff --git a/src/core/server/saved_objects/utils.test.ts b/src/core/server/saved_objects/utils.test.ts deleted file mode 100644 index 21229bee489c2..0000000000000 --- a/src/core/server/saved_objects/utils.test.ts +++ /dev/null @@ -1,445 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { legacyServiceMock } from '../legacy/legacy_service.mock'; -import { convertLegacyTypes, convertTypesToLegacySchema } from './utils'; -import { SavedObjectsLegacyUiExports, SavedObjectsType } from './types'; -import { LegacyConfig, SavedObjectMigrationContext } from 'kibana/server'; -import { SavedObjectUnsanitizedDoc } from './serialization'; - -describe('convertLegacyTypes', () => { - let legacyConfig: ReturnType; - - beforeEach(() => { - legacyConfig = legacyServiceMock.createLegacyConfig(); - }); - - it('converts the legacy mappings using default values if no schemas are specified', () => { - const uiExports: SavedObjectsLegacyUiExports = { - savedObjectMappings: [ - { - pluginId: 'pluginA', - properties: { - typeA: { - properties: { - fieldA: { type: 'text' }, - }, - }, - typeB: { - properties: { - fieldB: { type: 'text' }, - }, - }, - }, - }, - { - pluginId: 'pluginB', - properties: { - typeC: { - properties: { - fieldC: { type: 'text' }, - }, - }, - }, - }, - ], - savedObjectMigrations: {}, - savedObjectSchemas: {}, - savedObjectValidations: {}, - savedObjectsManagement: {}, - }; - - const converted = convertLegacyTypes(uiExports, legacyConfig); - expect(converted).toMatchSnapshot(); - }); - - it('merges the mappings and the schema to create the type when schema exists for the type', () => { - const uiExports: SavedObjectsLegacyUiExports = { - savedObjectMappings: [ - { - pluginId: 'pluginA', - properties: { - typeA: { - properties: { - fieldA: { type: 'text' }, - }, - }, - }, - }, - { - pluginId: 'pluginB', - properties: { - typeB: { - properties: { - fieldB: { type: 'text' }, - }, - }, - }, - }, - { - pluginId: 'pluginC', - properties: { - typeC: { - properties: { - fieldC: { type: 'text' }, - }, - }, - }, - }, - { - pluginId: 'pluginD', - properties: { - typeD: { - properties: { - fieldD: { type: 'text' }, - }, - }, - }, - }, - ], - savedObjectMigrations: {}, - savedObjectSchemas: { - typeA: { - indexPattern: 'fooBar', - hidden: true, - isNamespaceAgnostic: true, - }, - typeB: { - indexPattern: 'barBaz', - hidden: false, - multiNamespace: true, - }, - typeD: { - indexPattern: 'bazQux', - hidden: false, - // if both isNamespaceAgnostic and multiNamespace are true, the resulting namespaceType is 'agnostic' - isNamespaceAgnostic: true, - multiNamespace: true, - }, - }, - savedObjectValidations: {}, - savedObjectsManagement: {}, - }; - - const converted = convertLegacyTypes(uiExports, legacyConfig); - expect(converted).toMatchSnapshot(); - }); - - it('invokes indexPattern to retrieve the index when it is a function', () => { - const indexPatternAccessor: (config: LegacyConfig) => string = jest.fn((config) => { - config.get('foo.bar'); - return 'myIndex'; - }); - - const uiExports: SavedObjectsLegacyUiExports = { - savedObjectMappings: [ - { - pluginId: 'pluginA', - properties: { - typeA: { - properties: { - fieldA: { type: 'text' }, - }, - }, - }, - }, - ], - savedObjectMigrations: {}, - savedObjectSchemas: { - typeA: { - indexPattern: indexPatternAccessor, - hidden: true, - isNamespaceAgnostic: true, - }, - }, - savedObjectValidations: {}, - savedObjectsManagement: {}, - }; - - const converted = convertLegacyTypes(uiExports, legacyConfig); - - expect(indexPatternAccessor).toHaveBeenCalledWith(legacyConfig); - expect(legacyConfig.get).toHaveBeenCalledWith('foo.bar'); - expect(converted.length).toEqual(1); - expect(converted[0].indexPattern).toEqual('myIndex'); - }); - - it('import migrations from the uiExports', () => { - const migrationsA = { - '1.0.0': jest.fn(), - '2.0.4': jest.fn(), - }; - const migrationsB = { - '1.5.3': jest.fn(), - }; - - const uiExports: SavedObjectsLegacyUiExports = { - savedObjectMappings: [ - { - pluginId: 'pluginA', - properties: { - typeA: { - properties: { - fieldA: { type: 'text' }, - }, - }, - }, - }, - { - pluginId: 'pluginB', - properties: { - typeB: { - properties: { - fieldC: { type: 'text' }, - }, - }, - }, - }, - ], - savedObjectMigrations: { - typeA: migrationsA, - typeB: migrationsB, - }, - savedObjectSchemas: {}, - savedObjectValidations: {}, - savedObjectsManagement: {}, - }; - - const converted = convertLegacyTypes(uiExports, legacyConfig); - expect(converted.length).toEqual(2); - expect(Object.keys(converted[0]!.migrations!)).toEqual(Object.keys(migrationsA)); - expect(Object.keys(converted[1]!.migrations!)).toEqual(Object.keys(migrationsB)); - }); - - it('converts the migration to the new format', () => { - const legacyMigration = jest.fn(); - const migrationsA = { - '1.0.0': legacyMigration, - }; - - const uiExports: SavedObjectsLegacyUiExports = { - savedObjectMappings: [ - { - pluginId: 'pluginA', - properties: { - typeA: { - properties: { - fieldA: { type: 'text' }, - }, - }, - }, - }, - ], - savedObjectMigrations: { - typeA: migrationsA, - }, - savedObjectSchemas: {}, - savedObjectValidations: {}, - savedObjectsManagement: {}, - }; - - const converted = convertLegacyTypes(uiExports, legacyConfig); - expect(Object.keys(converted[0]!.migrations!)).toEqual(['1.0.0']); - - const migration = converted[0]!.migrations!['1.0.0']!; - - const doc = {} as SavedObjectUnsanitizedDoc; - const context = { log: {} } as SavedObjectMigrationContext; - migration(doc, context); - - expect(legacyMigration).toHaveBeenCalledTimes(1); - expect(legacyMigration).toHaveBeenCalledWith(doc, context.log); - }); - - it('imports type management information', () => { - const uiExports: SavedObjectsLegacyUiExports = { - savedObjectMappings: [ - { - pluginId: 'pluginA', - properties: { - typeA: { - properties: { - fieldA: { type: 'text' }, - }, - }, - }, - }, - { - pluginId: 'pluginB', - properties: { - typeB: { - properties: { - fieldB: { type: 'text' }, - }, - }, - typeC: { - properties: { - fieldC: { type: 'text' }, - }, - }, - }, - }, - ], - savedObjectsManagement: { - typeA: { - isImportableAndExportable: true, - icon: 'iconA', - defaultSearchField: 'searchFieldA', - getTitle: (savedObject) => savedObject.id, - }, - typeB: { - isImportableAndExportable: false, - icon: 'iconB', - getEditUrl: (savedObject) => `/some-url/${savedObject.id}`, - getInAppUrl: (savedObject) => ({ path: 'path', uiCapabilitiesPath: 'ui-path' }), - }, - }, - savedObjectMigrations: {}, - savedObjectSchemas: {}, - savedObjectValidations: {}, - }; - - const converted = convertLegacyTypes(uiExports, legacyConfig); - expect(converted.length).toEqual(3); - const [typeA, typeB, typeC] = converted; - - expect(typeA.management).toEqual({ - importableAndExportable: true, - icon: 'iconA', - defaultSearchField: 'searchFieldA', - getTitle: uiExports.savedObjectsManagement.typeA.getTitle, - }); - - expect(typeB.management).toEqual({ - importableAndExportable: false, - icon: 'iconB', - getEditUrl: uiExports.savedObjectsManagement.typeB.getEditUrl, - getInAppUrl: uiExports.savedObjectsManagement.typeB.getInAppUrl, - }); - - expect(typeC.management).toBeUndefined(); - }); - - it('merges everything when all are present', () => { - const uiExports: SavedObjectsLegacyUiExports = { - savedObjectMappings: [ - { - pluginId: 'pluginA', - properties: { - typeA: { - properties: { - fieldA: { type: 'text' }, - }, - }, - typeB: { - properties: { - fieldB: { type: 'text' }, - anotherFieldB: { type: 'boolean' }, - }, - }, - }, - }, - { - pluginId: 'pluginB', - properties: { - typeC: { - properties: { - fieldC: { type: 'text' }, - }, - }, - }, - }, - ], - savedObjectMigrations: { - typeA: { - '1.0.0': jest.fn(), - '2.0.4': jest.fn(), - }, - typeC: { - '1.5.3': jest.fn(), - }, - }, - savedObjectSchemas: { - typeA: { - indexPattern: jest.fn((config) => { - config.get('foo.bar'); - return 'myIndex'; - }), - hidden: true, - isNamespaceAgnostic: true, - }, - typeB: { - convertToAliasScript: 'some alias script', - hidden: false, - }, - }, - savedObjectValidations: {}, - savedObjectsManagement: {}, - }; - - const converted = convertLegacyTypes(uiExports, legacyConfig); - expect(converted).toMatchSnapshot(); - }); -}); - -describe('convertTypesToLegacySchema', () => { - it('converts types to the legacy schema format', () => { - const types: SavedObjectsType[] = [ - { - name: 'typeA', - hidden: false, - namespaceType: 'agnostic', - mappings: { properties: {} }, - convertToAliasScript: 'some script', - }, - { - name: 'typeB', - hidden: true, - namespaceType: 'single', - indexPattern: 'myIndex', - mappings: { properties: {} }, - }, - { - name: 'typeC', - hidden: false, - namespaceType: 'multiple', - mappings: { properties: {} }, - }, - ]; - expect(convertTypesToLegacySchema(types)).toEqual({ - typeA: { - hidden: false, - isNamespaceAgnostic: true, - multiNamespace: false, - convertToAliasScript: 'some script', - }, - typeB: { - hidden: true, - isNamespaceAgnostic: false, - multiNamespace: false, - indexPattern: 'myIndex', - }, - typeC: { - hidden: false, - isNamespaceAgnostic: false, - multiNamespace: true, - }, - }); - }); -}); diff --git a/src/core/server/saved_objects/utils.ts b/src/core/server/saved_objects/utils.ts deleted file mode 100644 index af7c08d1fbfcc..0000000000000 --- a/src/core/server/saved_objects/utils.ts +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { LegacyConfig } from '../legacy'; -import { SavedObjectMigrationMap } from './migrations'; -import { - SavedObjectsNamespaceType, - SavedObjectsType, - SavedObjectsLegacyUiExports, - SavedObjectLegacyMigrationMap, - SavedObjectsLegacyManagementTypeDefinition, - SavedObjectsTypeManagementDefinition, -} from './types'; -import { SavedObjectsSchemaDefinition } from './schema'; - -/** - * Converts the legacy savedObjects mappings, schema, and migrations - * to actual {@link SavedObjectsType | saved object types} - */ -export const convertLegacyTypes = ( - { - savedObjectMappings = [], - savedObjectMigrations = {}, - savedObjectSchemas = {}, - savedObjectsManagement = {}, - }: SavedObjectsLegacyUiExports, - legacyConfig: LegacyConfig -): SavedObjectsType[] => { - return savedObjectMappings.reduce((types, { properties }) => { - return [ - ...types, - ...Object.entries(properties).map(([type, mappings]) => { - const schema = savedObjectSchemas[type]; - const migrations = savedObjectMigrations[type]; - const management = savedObjectsManagement[type]; - const namespaceType = (schema?.isNamespaceAgnostic - ? 'agnostic' - : schema?.multiNamespace - ? 'multiple' - : 'single') as SavedObjectsNamespaceType; - return { - name: type, - hidden: schema?.hidden ?? false, - namespaceType, - mappings, - indexPattern: - typeof schema?.indexPattern === 'function' - ? schema.indexPattern(legacyConfig) - : schema?.indexPattern, - convertToAliasScript: schema?.convertToAliasScript, - migrations: convertLegacyMigrations(migrations ?? {}), - management: management ? convertLegacyTypeManagement(management) : undefined, - }; - }), - ]; - }, [] as SavedObjectsType[]); -}; - -/** - * Convert {@link SavedObjectsType | saved object types} to the legacy {@link SavedObjectsSchemaDefinition | schema} format - */ -export const convertTypesToLegacySchema = ( - types: SavedObjectsType[] -): SavedObjectsSchemaDefinition => { - return types.reduce((schema, type) => { - return { - ...schema, - [type.name]: { - isNamespaceAgnostic: type.namespaceType === 'agnostic', - multiNamespace: type.namespaceType === 'multiple', - hidden: type.hidden, - indexPattern: type.indexPattern, - convertToAliasScript: type.convertToAliasScript, - }, - }; - }, {} as SavedObjectsSchemaDefinition); -}; - -const convertLegacyMigrations = ( - legacyMigrations: SavedObjectLegacyMigrationMap -): SavedObjectMigrationMap => { - return Object.entries(legacyMigrations).reduce((migrated, [version, migrationFn]) => { - return { - ...migrated, - [version]: (doc, context) => migrationFn(doc, context.log), - }; - }, {} as SavedObjectMigrationMap); -}; - -const convertLegacyTypeManagement = ( - legacyTypeManagement: SavedObjectsLegacyManagementTypeDefinition -): SavedObjectsTypeManagementDefinition => { - return { - importableAndExportable: legacyTypeManagement.isImportableAndExportable, - defaultSearchField: legacyTypeManagement.defaultSearchField, - icon: legacyTypeManagement.icon, - getTitle: legacyTypeManagement.getTitle, - getEditUrl: legacyTypeManagement.getEditUrl, - getInAppUrl: legacyTypeManagement.getInAppUrl, - }; -}; diff --git a/src/core/server/saved_objects/validation/index.ts b/src/core/server/saved_objects/validation/index.ts deleted file mode 100644 index b1b33f91d3fd4..0000000000000 --- a/src/core/server/saved_objects/validation/index.ts +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -/* - * This is the core logic for validating saved object properties. The saved object client - * and migrations consume this in order to validate saved object documents prior to - * persisting them. - */ - -interface SavedObjectDoc { - type: string; - [prop: string]: any; -} - -/** - * A dictionary of property name -> validation function. The property name - * is generally the document's type (e.g. "dashboard"), but will also - * match other properties. - * - * For example, the "acl" and "dashboard" validators both apply to the - * following saved object: { type: "dashboard", attributes: {}, acl: "sdlaj3w" } - * - * @export - * @interface Validators - */ -export interface PropertyValidators { - [prop: string]: ValidateDoc; -} - -export type ValidateDoc = (doc: SavedObjectDoc) => void; - -/** - * Creates a function which uses a dictionary of property validators to validate - * individual saved object documents. - * - * @export - * @param {Validators} validators - * @param {SavedObjectDoc} doc - */ -export function docValidator(validators: PropertyValidators = {}): ValidateDoc { - return function validateDoc(doc: SavedObjectDoc) { - Object.keys(doc) - .concat(doc.type) - .forEach((prop) => { - const validator = validators[prop]; - if (validator) { - validator(doc); - } - }); - }; -} diff --git a/src/core/server/saved_objects/validation/readme.md b/src/core/server/saved_objects/validation/readme.md deleted file mode 100644 index 3b9f17c37fd0b..0000000000000 --- a/src/core/server/saved_objects/validation/readme.md +++ /dev/null @@ -1,63 +0,0 @@ -# Saved Object Validations - -The saved object client supports validation of documents during create / bulkCreate operations. - -This allows us tighter control over what documents get written to the saved object index, and helps us keep the index in a healthy state. - -## Creating validations - -Plugin authors can write their own validations by adding a `validations` property to their uiExports. A validation is nothing more than a dictionary of `{[prop: string]: validationFunction}` where: - -* `prop` - a root-property on a saved object document -* `validationFunction` - a function that takes a document and throws an error if it does not meet expectations. - -## Example - -```js -// In myFanciPlugin... -uiExports: { - validations: { - myProperty(doc) { - if (doc.attributes.someField === undefined) { - throw new Error(`Document ${doc.id} did not define "someField"`); - } - }, - - someOtherProp(doc) { - if (doc.attributes.counter < 0) { - throw new Error(`Document ${doc.id} cannot have a negative counter.`); - } - }, - }, -}, -``` - -In this example, `myFanciPlugin` defines validations for two properties: `myProperty` and `someOtherProp`. - -This means that no other plugin can define validations for myProperty or someOtherProp. - -The `myProperty` validation would run for any doc that has a `type="myProperty"` or for any doc that has a root-level property of `myProperty`. e.g. it would apply to all documents in the following array: - -```js -[ - { - type: 'foo', - attributes: { stuff: 'here' }, - myProperty: 'shazm!', - }, - { - type: 'myProperty', - attributes: { shazm: true }, - }, -]; -``` - -Validating properties other than just 'type' allows us to support potential future saved object scenarios in which plugins might want to annotate other plugin documents, such as a security plugin adding an acl to another document: - -```js -{ - type: 'dashboard', - attributes: { stuff: 'here' }, - acl: '342343', -} -``` diff --git a/src/core/server/saved_objects/validation/validation.test.ts b/src/core/server/saved_objects/validation/validation.test.ts deleted file mode 100644 index 71e220280ba5f..0000000000000 --- a/src/core/server/saved_objects/validation/validation.test.ts +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { docValidator } from './index'; - -describe('docValidator', () => { - test('does not run validators that have no application to the doc', () => { - const validators = { - foo: () => { - throw new Error('Boom!'); - }, - }; - expect(() => docValidator(validators)({ type: 'shoo', bar: 'hi' })).not.toThrow(); - }); - - test('validates the doc type', () => { - const validators = { - foo: () => { - throw new Error('Boom!'); - }, - }; - expect(() => docValidator(validators)({ type: 'foo' })).toThrow(/Boom!/); - }); - - test('validates various props', () => { - const validators = { - a: jest.fn(), - b: jest.fn(), - c: jest.fn(), - }; - docValidator(validators)({ type: 'a', b: 'foo' }); - - expect(validators.c).not.toHaveBeenCalled(); - - expect(validators.a.mock.calls).toEqual([[{ type: 'a', b: 'foo' }]]); - expect(validators.b.mock.calls).toEqual([[{ type: 'a', b: 'foo' }]]); - }); -}); diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 081554cd17f25..ec457704e89c7 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1411,19 +1411,30 @@ export interface LegacyServiceStartDeps { plugins: Record; } -// Warning: (ae-forgotten-export) The symbol "SavedObjectsLegacyUiExports" needs to be exported by the entry point index.d.ts -// // @internal @deprecated (undocumented) -export type LegacyUiExports = SavedObjectsLegacyUiExports & { +export interface LegacyUiExports { + // Warning: (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts + // + // (undocumented) defaultInjectedVarProviders?: VarsProvider[]; + // Warning: (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts + // + // (undocumented) injectedVarsReplacers?: VarsReplacer[]; + // Warning: (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts + // + // (undocumented) navLinkSpecs?: LegacyNavLinkSpec[] | null; + // Warning: (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts + // + // (undocumented) uiAppSpecs?: Array; + // (undocumented) unknown?: [{ pluginSpec: LegacyPluginSpec; type: unknown; }]; -}; +} // Warning: (ae-forgotten-export) The symbol "lifecycleResponseFactory" needs to be exported by the entry point index.d.ts // @@ -1520,10 +1531,10 @@ export interface LogRecord { timestamp: Date; } -// Warning: (ae-missing-release-tag) "MetricsServiceSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) +// @public export interface MetricsServiceSetup { + readonly collectionInterval: number; + getOpsMetrics$: () => Observable; } // @public @deprecated (undocumented) @@ -1610,6 +1621,7 @@ export interface OnPreRoutingToolkit { // @public export interface OpsMetrics { + collected_at: Date; concurrent_connections: OpsServerMetrics['concurrent_connections']; os: OpsOsMetrics; process: OpsProcessMetrics; @@ -1619,6 +1631,20 @@ export interface OpsMetrics { // @public export interface OpsOsMetrics { + cpu?: { + control_group: string; + cfs_period_micros: number; + cfs_quota_micros: number; + stat: { + number_of_elapsed_periods: number; + number_of_times_throttled: number; + time_throttled_nanos: number; + }; + }; + cpuacct?: { + control_group: string; + usage_nanos: number; + }; distro?: string; distroRelease?: string; load: { @@ -2021,6 +2047,7 @@ export interface SavedObjectsBulkResponse { export interface SavedObjectsBulkUpdateObject extends Pick { attributes: Partial; id: string; + namespace?: string; type: string; } @@ -2437,33 +2464,6 @@ export interface SavedObjectsIncrementCounterOptions extends SavedObjectsBaseOpt refresh?: MutatingOperationRefreshSetting; } -// @internal @deprecated (undocumented) -export interface SavedObjectsLegacyService { - // Warning: (ae-forgotten-export) The symbol "SavedObjectsClientProvider" needs to be exported by the entry point index.d.ts - // - // (undocumented) - addScopedSavedObjectsClientWrapperFactory: SavedObjectsClientProvider['addClientWrapperFactory']; - // (undocumented) - getSavedObjectsRepository(...rest: any[]): any; - // (undocumented) - getScopedSavedObjectsClient: SavedObjectsClientProvider['getClient']; - // (undocumented) - importExport: { - objectLimit: number; - importSavedObjects(options: SavedObjectsImportOptions): Promise; - resolveImportErrors(options: SavedObjectsResolveImportErrorsOptions): Promise; - getSortedObjectsForExport(options: SavedObjectsExportOptions): Promise; - }; - // (undocumented) - SavedObjectsClient: typeof SavedObjectsClient; - // (undocumented) - schema: SavedObjectsSchema; - // (undocumented) - setScopedSavedObjectsClientFactory: SavedObjectsClientProvider['setClientFactory']; - // (undocumented) - types: string[]; -} - // @public export interface SavedObjectsMappingProperties { // (undocumented) @@ -2517,10 +2517,10 @@ export class SavedObjectsRepository { bulkUpdate(objects: Array>, options?: SavedObjectsBulkUpdateOptions): Promise>; checkConflicts(objects?: SavedObjectsCheckConflictsObject[], options?: SavedObjectsBaseOptions): Promise; create(type: string, attributes: T, options?: SavedObjectsCreateOptions): Promise>; - // Warning: (ae-forgotten-export) The symbol "KibanaMigrator" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "IKibanaMigrator" needs to be exported by the entry point index.d.ts // // @internal - static createRepository(migrator: KibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; + static createRepository(migrator: IKibanaMigrator, typeRegistry: SavedObjectTypeRegistry, indexName: string, client: ElasticsearchClient, includedHiddenTypes?: string[], injectedConstructor?: any): ISavedObjectsRepository; delete(type: string, id: string, options?: SavedObjectsDeleteOptions): Promise<{}>; deleteByNamespace(namespace: string, options?: SavedObjectsDeleteByNamespaceOptions): Promise; deleteFromNamespaces(type: string, id: string, namespaces: string[], options?: SavedObjectsDeleteFromNamespacesOptions): Promise; @@ -2548,24 +2548,6 @@ export interface SavedObjectsResolveImportErrorsOptions { typeRegistry: ISavedObjectTypeRegistry; } -// @internal @deprecated (undocumented) -export class SavedObjectsSchema { - // Warning: (ae-forgotten-export) The symbol "SavedObjectsSchemaDefinition" needs to be exported by the entry point index.d.ts - constructor(schemaDefinition?: SavedObjectsSchemaDefinition); - // (undocumented) - getConvertToAliasScript(type: string): string | undefined; - // (undocumented) - getIndexForType(config: LegacyConfig, type: string): string | undefined; - // (undocumented) - isHiddenType(type: string): boolean; - // (undocumented) - isMultiNamespace(type: string): boolean; - // (undocumented) - isNamespaceAgnostic(type: string): boolean; - // (undocumented) - isSingleNamespace(type: string): boolean; -} - // @public export class SavedObjectsSerializer { // @internal @@ -2649,6 +2631,12 @@ export interface SavedObjectsUpdateResponse extends Omit string; + static namespaceStringToId: (namespace: string) => string | undefined; +} + // @public export class SavedObjectTypeRegistry { getAllTypes(): SavedObjectsType[]; @@ -2797,10 +2785,17 @@ export type SharedGlobalConfig = RecursiveReadonly<{ // @public export type StartServicesAccessor = () => Promise<[CoreStart, TPluginsStart, TStart]>; +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ServiceStatusSetup" +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ServiceStatusSetup" +// // @public export interface StatusServiceSetup { core$: Observable; + dependencies$: Observable>; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "StatusSetup" + derivedStatus$: Observable; overall$: Observable; + set(status$: Observable): void; } // @public @@ -2888,13 +2883,9 @@ export const validBodyOutput: readonly ["data", "stream"]; // Warnings were encountered during analysis: // // src/core/server/http/router/response.ts:316:3 - (ae-forgotten-export) The symbol "KibanaResponse" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:132:3 - (ae-forgotten-export) The symbol "VarsProvider" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:133:3 - (ae-forgotten-export) The symbol "VarsReplacer" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:134:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:135:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts -// src/core/server/legacy/types.ts:136:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts -// src/core/server/plugins/types.ts:268:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/legacy/types.ts:135:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:272:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:272:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts +// src/core/server/plugins/types.ts:274:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts ``` diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 417f66a2988c2..8bf16d9130ef5 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -49,7 +49,7 @@ const rawConfigService = rawConfigServiceMock.create({}); beforeEach(() => { mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true })); mockPluginsService.discover.mockResolvedValue({ - pluginTree: new Map(), + pluginTree: { asOpaqueIds: new Map(), asNames: new Map() }, uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); }); @@ -98,7 +98,7 @@ test('injects legacy dependency to context#setup()', async () => { [pluginB, [pluginA]], ]); mockPluginsService.discover.mockResolvedValue({ - pluginTree: pluginDependencies, + pluginTree: { asOpaqueIds: pluginDependencies, asNames: new Map() }, uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() }, }); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index cc6d8171e7a03..a02b0f51b559f 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -121,10 +121,13 @@ export class Server { const contextServiceSetup = this.context.setup({ // We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins: - // 1) Can access context from any NP plugin + // 1) Can access context from any KP plugin // 2) Can register context providers that will only be available to other legacy plugins and will not leak into // New Platform plugins. - pluginDependencies: new Map([...pluginTree, [this.legacy.legacyId, [...pluginTree.keys()]]]), + pluginDependencies: new Map([ + ...pluginTree.asOpaqueIds, + [this.legacy.legacyId, [...pluginTree.asOpaqueIds.keys()]], + ]), }); const auditTrailSetup = this.auditTrail.setup(); @@ -142,7 +145,6 @@ export class Server { const savedObjectsSetup = await this.savedObjects.setup({ http: httpSetup, elasticsearch: elasticsearchServiceSetup, - legacyPlugins, }); const uiSettingsSetup = await this.uiSettings.setup({ @@ -154,6 +156,7 @@ export class Server { const statusSetup = await this.status.setup({ elasticsearch: elasticsearchServiceSetup, + pluginDependencies: pluginTree.asNames, savedObjects: savedObjectsSetup, }); diff --git a/src/core/server/status/get_summary_status.test.ts b/src/core/server/status/get_summary_status.test.ts index 7516e82ee784d..d97083162b502 100644 --- a/src/core/server/status/get_summary_status.test.ts +++ b/src/core/server/status/get_summary_status.test.ts @@ -94,6 +94,38 @@ describe('getSummaryStatus', () => { describe('summary', () => { describe('when a single service is at highest level', () => { it('returns all information about that single service', () => { + expect( + getSummaryStatus( + Object.entries({ + s1: degraded, + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + meta: { + custom: { data: 'here' }, + }, + }, + }) + ) + ).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: '[s2]: Lorem ipsum', + detail: 'See the status page for more information', + meta: { + affectedServices: { + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + meta: { + custom: { data: 'here' }, + }, + }, + }, + }, + }); + }); + + it('allows the single service to override the detail and documentationUrl fields', () => { expect( getSummaryStatus( Object.entries({ @@ -115,7 +147,17 @@ describe('getSummaryStatus', () => { detail: 'Vivamus pulvinar sem ac luctus ultrices.', documentationUrl: 'http://helpmenow.com/problem1', meta: { - custom: { data: 'here' }, + affectedServices: { + s2: { + level: ServiceStatusLevels.unavailable, + summary: 'Lorem ipsum', + detail: 'Vivamus pulvinar sem ac luctus ultrices.', + documentationUrl: 'http://helpmenow.com/problem1', + meta: { + custom: { data: 'here' }, + }, + }, + }, }, }); }); diff --git a/src/core/server/status/get_summary_status.ts b/src/core/server/status/get_summary_status.ts index 748a54f0bf8bb..8d97cdbd9b15b 100644 --- a/src/core/server/status/get_summary_status.ts +++ b/src/core/server/status/get_summary_status.ts @@ -23,62 +23,60 @@ import { ServiceStatus, ServiceStatusLevels, ServiceStatusLevel } from './types' * Returns a single {@link ServiceStatus} that summarizes the most severe status level from a group of statuses. * @param statuses */ -export const getSummaryStatus = (statuses: Array<[string, ServiceStatus]>): ServiceStatus => { - const grouped = groupByLevel(statuses); - const highestSeverityLevel = getHighestSeverityLevel(grouped.keys()); - const highestSeverityGroup = grouped.get(highestSeverityLevel)!; +export const getSummaryStatus = ( + statuses: Array<[string, ServiceStatus]>, + { allAvailableSummary = `All services are available` }: { allAvailableSummary?: string } = {} +): ServiceStatus => { + const { highestLevel, highestStatuses } = highestLevelSummary(statuses); - if (highestSeverityLevel === ServiceStatusLevels.available) { + if (highestLevel === ServiceStatusLevels.available) { return { level: ServiceStatusLevels.available, - summary: `All services are available`, + summary: allAvailableSummary, }; - } else if (highestSeverityGroup.size === 1) { - const [serviceName, status] = [...highestSeverityGroup.entries()][0]; + } else if (highestStatuses.length === 1) { + const [serviceName, status] = highestStatuses[0]! as [string, ServiceStatus]; return { ...status, summary: `[${serviceName}]: ${status.summary!}`, + // TODO: include URL to status page + detail: status.detail ?? `See the status page for more information`, + meta: { + affectedServices: { [serviceName]: status }, + }, }; } else { return { - level: highestSeverityLevel, - summary: `[${highestSeverityGroup.size}] services are ${highestSeverityLevel.toString()}`, + level: highestLevel, + summary: `[${highestStatuses.length}] services are ${highestLevel.toString()}`, // TODO: include URL to status page detail: `See the status page for more information`, meta: { - affectedServices: Object.fromEntries([...highestSeverityGroup]), + affectedServices: Object.fromEntries(highestStatuses), }, }; } }; -const groupByLevel = ( - statuses: Array<[string, ServiceStatus]> -): Map> => { - const byLevel = new Map>(); +type StatusPair = [string, ServiceStatus]; - for (const [serviceName, status] of statuses) { - let levelMap = byLevel.get(status.level); - if (!levelMap) { - levelMap = new Map(); - byLevel.set(status.level, levelMap); - } +const highestLevelSummary = ( + statuses: StatusPair[] +): { highestLevel: ServiceStatusLevel; highestStatuses: StatusPair[] } => { + let highestLevel: ServiceStatusLevel = ServiceStatusLevels.available; + let highestStatuses: StatusPair[] = []; - levelMap.set(serviceName, status); + for (const pair of statuses) { + if (pair[1].level === highestLevel) { + highestStatuses.push(pair); + } else if (pair[1].level > highestLevel) { + highestLevel = pair[1].level; + highestStatuses = [pair]; + } } - return byLevel; -}; - -const getHighestSeverityLevel = (levels: Iterable): ServiceStatusLevel => { - const sorted = [...levels].sort((a, b) => { - if (a < b) { - return -1; - } else if (a > b) { - return 1; - } else { - return 0; - } - }); - return sorted[sorted.length - 1] ?? ServiceStatusLevels.available; + return { + highestLevel, + highestStatuses, + }; }; diff --git a/src/core/server/status/plugins_status.test.ts b/src/core/server/status/plugins_status.test.ts new file mode 100644 index 0000000000000..a75dc8c283698 --- /dev/null +++ b/src/core/server/status/plugins_status.test.ts @@ -0,0 +1,338 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { PluginName } from '../plugins'; +import { PluginsStatusService } from './plugins_status'; +import { of, Observable, BehaviorSubject } from 'rxjs'; +import { ServiceStatusLevels, CoreStatus, ServiceStatus } from './types'; +import { first } from 'rxjs/operators'; +import { ServiceStatusLevelSnapshotSerializer } from './test_utils'; + +expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); + +describe('PluginStatusService', () => { + const coreAllAvailable$: Observable = of({ + elasticsearch: { level: ServiceStatusLevels.available, summary: 'elasticsearch avail' }, + savedObjects: { level: ServiceStatusLevels.available, summary: 'savedObjects avail' }, + }); + const coreOneDegraded$: Observable = of({ + elasticsearch: { level: ServiceStatusLevels.available, summary: 'elasticsearch avail' }, + savedObjects: { level: ServiceStatusLevels.degraded, summary: 'savedObjects degraded' }, + }); + const coreOneCriticalOneDegraded$: Observable = of({ + elasticsearch: { level: ServiceStatusLevels.critical, summary: 'elasticsearch critical' }, + savedObjects: { level: ServiceStatusLevels.degraded, summary: 'savedObjects degraded' }, + }); + const pluginDependencies: Map = new Map([ + ['a', []], + ['b', ['a']], + ['c', ['a', 'b']], + ]); + + describe('getDerivedStatus$', () => { + it(`defaults to core's most severe status`, async () => { + const serviceAvailable = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies, + }); + expect(await serviceAvailable.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.available, + summary: 'All dependencies are available', + }); + + const serviceDegraded = new PluginsStatusService({ + core$: coreOneDegraded$, + pluginDependencies, + }); + expect(await serviceDegraded.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.degraded, + summary: '[savedObjects]: savedObjects degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + + const serviceCritical = new PluginsStatusService({ + core$: coreOneCriticalOneDegraded$, + pluginDependencies, + }); + expect(await serviceCritical.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.critical, + summary: '[elasticsearch]: elasticsearch critical', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + }); + + it(`provides a summary status when core and dependencies are at same severity level`, async () => { + const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); + service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a is degraded' })); + expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + }); + + it(`allows dependencies status to take precedence over lower severity core statuses`, async () => { + const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); + service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' })); + expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: '[a]: a is not working', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + }); + + it(`allows core status to take precedence over lower severity dependencies statuses`, async () => { + const service = new PluginsStatusService({ + core$: coreOneCriticalOneDegraded$, + pluginDependencies, + }); + service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' })); + expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.critical, + summary: '[elasticsearch]: elasticsearch critical', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + }); + + it(`allows a severe dependency status to take precedence over a less severe dependency status`, async () => { + const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); + service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a is degraded' })); + service.set('b', of({ level: ServiceStatusLevels.unavailable, summary: 'b is not working' })); + expect(await service.getDerivedStatus$('c').pipe(first()).toPromise()).toEqual({ + level: ServiceStatusLevels.unavailable, + summary: '[b]: b is not working', + detail: 'See the status page for more information', + meta: expect.any(Object), + }); + }); + }); + + describe('getAll$', () => { + it('defaults to empty record if no plugins', async () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies: new Map(), + }); + expect(await service.getAll$().pipe(first()).toPromise()).toEqual({}); + }); + + it('defaults to core status when no plugin statuses are set', async () => { + const serviceAvailable = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies, + }); + expect(await serviceAvailable.getAll$().pipe(first()).toPromise()).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + c: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + }); + + const serviceDegraded = new PluginsStatusService({ + core$: coreOneDegraded$, + pluginDependencies, + }); + expect(await serviceDegraded.getAll$().pipe(first()).toPromise()).toEqual({ + a: { + level: ServiceStatusLevels.degraded, + summary: '[savedObjects]: savedObjects degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + b: { + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + c: { + level: ServiceStatusLevels.degraded, + summary: '[3] services are degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + }); + + const serviceCritical = new PluginsStatusService({ + core$: coreOneCriticalOneDegraded$, + pluginDependencies, + }); + expect(await serviceCritical.getAll$().pipe(first()).toPromise()).toEqual({ + a: { + level: ServiceStatusLevels.critical, + summary: '[elasticsearch]: elasticsearch critical', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + b: { + level: ServiceStatusLevels.critical, + summary: '[2] services are critical', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + c: { + level: ServiceStatusLevels.critical, + summary: '[3] services are critical', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + }); + }); + + it('uses the manually set status level if plugin specifies one', async () => { + const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); + service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a status' })); + + expect(await service.getAll$().pipe(first()).toPromise()).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded + b: { + level: ServiceStatusLevels.degraded, + summary: '[savedObjects]: savedObjects degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + c: { + level: ServiceStatusLevels.degraded, + summary: '[2] services are degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + }); + }); + + it('updates when a new plugin status observable is set', async () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies: new Map([['a', []]]), + }); + const statusUpdates: Array> = []; + const subscription = service + .getAll$() + .subscribe((pluginStatuses) => statusUpdates.push(pluginStatuses)); + + service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a degraded' })); + service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a unavailable' })); + service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a available' })); + subscription.unsubscribe(); + + expect(statusUpdates).toEqual([ + { a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' } }, + { a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } }, + { a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } }, + { a: { level: ServiceStatusLevels.available, summary: 'a available' } }, + ]); + }); + }); + + describe('getDependenciesStatus$', () => { + it('only includes dependencies of specified plugin', async () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies, + }); + expect(await service.getDependenciesStatus$('a').pipe(first()).toPromise()).toEqual({}); + expect(await service.getDependenciesStatus$('b').pipe(first()).toPromise()).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + }); + expect(await service.getDependenciesStatus$('c').pipe(first()).toPromise()).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' }, + }); + }); + + it('uses the manually set status level if plugin specifies one', async () => { + const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies }); + service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a status' })); + + expect(await service.getDependenciesStatus$('c').pipe(first()).toPromise()).toEqual({ + a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded + b: { + level: ServiceStatusLevels.degraded, + summary: '[savedObjects]: savedObjects degraded', + detail: 'See the status page for more information', + meta: expect.any(Object), + }, + }); + }); + + it('throws error if unknown plugin passed', () => { + const service = new PluginsStatusService({ core$: coreAllAvailable$, pluginDependencies }); + expect(() => { + service.getDependenciesStatus$('dont-exist'); + }).toThrowError(); + }); + + it('debounces events in quick succession', async () => { + const service = new PluginsStatusService({ + core$: coreAllAvailable$, + pluginDependencies, + }); + const available: ServiceStatus = { + level: ServiceStatusLevels.available, + summary: 'a available', + }; + const degraded: ServiceStatus = { + level: ServiceStatusLevels.degraded, + summary: 'a degraded', + }; + const pluginA$ = new BehaviorSubject(available); + service.set('a', pluginA$); + + const statusUpdates: Array> = []; + const subscription = service + .getDependenciesStatus$('b') + .subscribe((status) => statusUpdates.push(status)); + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + + pluginA$.next(degraded); + pluginA$.next(available); + pluginA$.next(degraded); + pluginA$.next(available); + pluginA$.next(degraded); + pluginA$.next(available); + pluginA$.next(degraded); + // Waiting for the debounce timeout should cut a new update + await delay(500); + pluginA$.next(available); + await delay(500); + subscription.unsubscribe(); + + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "a": Object { + "level": degraded, + "summary": "a degraded", + }, + }, + Object { + "a": Object { + "level": available, + "summary": "a available", + }, + }, + ] + `); + }); + }); +}); diff --git a/src/core/server/status/plugins_status.ts b/src/core/server/status/plugins_status.ts new file mode 100644 index 0000000000000..113d59b327c11 --- /dev/null +++ b/src/core/server/status/plugins_status.ts @@ -0,0 +1,98 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { BehaviorSubject, Observable, combineLatest, of } from 'rxjs'; +import { map, distinctUntilChanged, switchMap, debounceTime } from 'rxjs/operators'; +import { isDeepStrictEqual } from 'util'; + +import { PluginName } from '../plugins'; +import { ServiceStatus, CoreStatus } from './types'; +import { getSummaryStatus } from './get_summary_status'; + +interface Deps { + core$: Observable; + pluginDependencies: ReadonlyMap; +} + +export class PluginsStatusService { + private readonly pluginStatuses = new Map>(); + private readonly update$ = new BehaviorSubject(true); + constructor(private readonly deps: Deps) {} + + public set(plugin: PluginName, status$: Observable) { + this.pluginStatuses.set(plugin, status$); + this.update$.next(true); // trigger all existing Observables to update from the new source Observable + } + + public getAll$(): Observable> { + return this.getPluginStatuses$([...this.deps.pluginDependencies.keys()]); + } + + public getDependenciesStatus$(plugin: PluginName): Observable> { + const dependencies = this.deps.pluginDependencies.get(plugin); + if (!dependencies) { + throw new Error(`Unknown plugin: ${plugin}`); + } + + return this.getPluginStatuses$(dependencies).pipe( + // Prevent many emissions at once from dependency status resolution from making this too noisy + debounceTime(500) + ); + } + + public getDerivedStatus$(plugin: PluginName): Observable { + return combineLatest([this.deps.core$, this.getDependenciesStatus$(plugin)]).pipe( + map(([coreStatus, pluginStatuses]) => { + return getSummaryStatus( + [...Object.entries(coreStatus), ...Object.entries(pluginStatuses)], + { + allAvailableSummary: `All dependencies are available`, + } + ); + }) + ); + } + + private getPluginStatuses$(plugins: PluginName[]): Observable> { + if (plugins.length === 0) { + return of({}); + } + + return this.update$.pipe( + switchMap(() => { + const pluginStatuses = plugins + .map( + (depName) => + [depName, this.pluginStatuses.get(depName) ?? this.getDerivedStatus$(depName)] as [ + PluginName, + Observable + ] + ) + .map(([pName, status$]) => + status$.pipe(map((status) => [pName, status] as [PluginName, ServiceStatus])) + ); + + return combineLatest(pluginStatuses).pipe( + map((statuses) => Object.fromEntries(statuses)), + distinctUntilChanged(isDeepStrictEqual) + ); + }) + ); + } +} diff --git a/src/core/server/status/status_service.mock.ts b/src/core/server/status/status_service.mock.ts index 47ef8659b4079..42b3eecdca310 100644 --- a/src/core/server/status/status_service.mock.ts +++ b/src/core/server/status/status_service.mock.ts @@ -40,6 +40,9 @@ const createSetupContractMock = () => { const setupContract: jest.Mocked = { core$: new BehaviorSubject(availableCoreStatus), overall$: new BehaviorSubject(available), + set: jest.fn(), + dependencies$: new BehaviorSubject({}), + derivedStatus$: new BehaviorSubject(available), }; return setupContract; @@ -50,6 +53,11 @@ const createInternalSetupContractMock = () => { core$: new BehaviorSubject(availableCoreStatus), overall$: new BehaviorSubject(available), isStatusPageAnonymous: jest.fn().mockReturnValue(false), + plugins: { + set: jest.fn(), + getDependenciesStatus$: jest.fn(), + getDerivedStatus$: jest.fn(), + }, }; return setupContract; diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts index 863fe34e8ecea..dcb1e0a559f5d 100644 --- a/src/core/server/status/status_service.test.ts +++ b/src/core/server/status/status_service.test.ts @@ -34,6 +34,7 @@ describe('StatusService', () => { service = new StatusService(mockCoreContext.create()); }); + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); const available: ServiceStatus = { level: ServiceStatusLevels.available, summary: 'Available', @@ -53,6 +54,7 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, + pluginDependencies: new Map(), }); expect(await setup.core$.pipe(first()).toPromise()).toEqual({ elasticsearch: available, @@ -68,6 +70,7 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, + pluginDependencies: new Map(), }); const subResult1 = await setup.core$.pipe(first()).toPromise(); const subResult2 = await setup.core$.pipe(first()).toPromise(); @@ -96,6 +99,7 @@ describe('StatusService', () => { savedObjects: { status$: savedObjects$, }, + pluginDependencies: new Map(), }); const statusUpdates: CoreStatus[] = []; @@ -158,6 +162,7 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, + pluginDependencies: new Map(), }); expect(await setup.overall$.pipe(first()).toPromise()).toMatchObject({ level: ServiceStatusLevels.degraded, @@ -173,6 +178,7 @@ describe('StatusService', () => { savedObjects: { status$: of(degraded), }, + pluginDependencies: new Map(), }); const subResult1 = await setup.overall$.pipe(first()).toPromise(); const subResult2 = await setup.overall$.pipe(first()).toPromise(); @@ -201,26 +207,95 @@ describe('StatusService', () => { savedObjects: { status$: savedObjects$, }, + pluginDependencies: new Map(), }); const statusUpdates: ServiceStatus[] = []; const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status)); + // Wait for timers to ensure that duplicate events are still filtered out regardless of debouncing. elasticsearch$.next(available); + await delay(500); elasticsearch$.next(available); + await delay(500); elasticsearch$.next({ level: ServiceStatusLevels.available, summary: `Wow another summary`, }); + await delay(500); savedObjects$.next(degraded); + await delay(500); savedObjects$.next(available); + await delay(500); savedObjects$.next(available); + await delay(500); subscription.unsubscribe(); expect(statusUpdates).toMatchInlineSnapshot(` Array [ Object { + "detail": "See the status page for more information", "level": degraded, + "meta": Object { + "affectedServices": Object { + "savedObjects": Object { + "level": degraded, + "summary": "This is degraded!", + }, + }, + }, + "summary": "[savedObjects]: This is degraded!", + }, + Object { + "level": available, + "summary": "All services are available", + }, + ] + `); + }); + + it('debounces events in quick succession', async () => { + const savedObjects$ = new BehaviorSubject(available); + const setup = await service.setup({ + elasticsearch: { + status$: new BehaviorSubject(available), + }, + savedObjects: { + status$: savedObjects$, + }, + pluginDependencies: new Map(), + }); + + const statusUpdates: ServiceStatus[] = []; + const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status)); + + // All of these should debounced into a single `available` status + savedObjects$.next(degraded); + savedObjects$.next(available); + savedObjects$.next(degraded); + savedObjects$.next(available); + savedObjects$.next(degraded); + savedObjects$.next(available); + savedObjects$.next(degraded); + // Waiting for the debounce timeout should cut a new update + await delay(500); + savedObjects$.next(available); + await delay(500); + subscription.unsubscribe(); + + expect(statusUpdates).toMatchInlineSnapshot(` + Array [ + Object { + "detail": "See the status page for more information", + "level": degraded, + "meta": Object { + "affectedServices": Object { + "savedObjects": Object { + "level": degraded, + "summary": "This is degraded!", + }, + }, + }, "summary": "[savedObjects]: This is degraded!", }, Object { diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index aea335e64babf..8fe65eddb61d3 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -18,7 +18,7 @@ */ import { Observable, combineLatest } from 'rxjs'; -import { map, distinctUntilChanged, shareReplay, take } from 'rxjs/operators'; +import { map, distinctUntilChanged, shareReplay, take, debounceTime } from 'rxjs/operators'; import { isDeepStrictEqual } from 'util'; import { CoreService } from '../../types'; @@ -26,13 +26,16 @@ import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { InternalElasticsearchServiceSetup } from '../elasticsearch'; import { InternalSavedObjectsServiceSetup } from '../saved_objects'; +import { PluginName } from '../plugins'; import { config, StatusConfigType } from './status_config'; import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types'; import { getSummaryStatus } from './get_summary_status'; +import { PluginsStatusService } from './plugins_status'; interface SetupDeps { elasticsearch: Pick; + pluginDependencies: ReadonlyMap; savedObjects: Pick; } @@ -40,26 +43,44 @@ export class StatusService implements CoreService { private readonly logger: Logger; private readonly config$: Observable; + private pluginsStatus?: PluginsStatusService; + constructor(coreContext: CoreContext) { this.logger = coreContext.logger.get('status'); this.config$ = coreContext.configService.atPath(config.path); } - public async setup(core: SetupDeps) { + public async setup({ elasticsearch, pluginDependencies, savedObjects }: SetupDeps) { const statusConfig = await this.config$.pipe(take(1)).toPromise(); - const core$ = this.setupCoreStatus(core); - const overall$: Observable = core$.pipe( - map((coreStatus) => { - const summary = getSummaryStatus(Object.entries(coreStatus)); + const core$ = this.setupCoreStatus({ elasticsearch, savedObjects }); + this.pluginsStatus = new PluginsStatusService({ core$, pluginDependencies }); + + const overall$: Observable = combineLatest( + core$, + this.pluginsStatus.getAll$() + ).pipe( + // Prevent many emissions at once from dependency status resolution from making this too noisy + debounceTime(500), + map(([coreStatus, pluginsStatus]) => { + const summary = getSummaryStatus([ + ...Object.entries(coreStatus), + ...Object.entries(pluginsStatus), + ]); this.logger.debug(`Recalculated overall status`, { status: summary }); return summary; }), - distinctUntilChanged(isDeepStrictEqual) + distinctUntilChanged(isDeepStrictEqual), + shareReplay(1) ); return { core$, overall$, + plugins: { + set: this.pluginsStatus.set.bind(this.pluginsStatus), + getDependenciesStatus$: this.pluginsStatus.getDependenciesStatus$.bind(this.pluginsStatus), + getDerivedStatus$: this.pluginsStatus.getDerivedStatus$.bind(this.pluginsStatus), + }, isStatusPageAnonymous: () => statusConfig.allowAnonymous, }; } @@ -68,7 +89,10 @@ export class StatusService implements CoreService { public stop() {} - private setupCoreStatus({ elasticsearch, savedObjects }: SetupDeps): Observable { + private setupCoreStatus({ + elasticsearch, + savedObjects, + }: Pick): Observable { return combineLatest([elasticsearch.status$, savedObjects.status$]).pipe( map(([elasticsearchStatus, savedObjectsStatus]) => ({ elasticsearch: elasticsearchStatus, diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts index 2ecf11deb2960..f884b80316fa8 100644 --- a/src/core/server/status/types.ts +++ b/src/core/server/status/types.ts @@ -19,6 +19,7 @@ import { Observable } from 'rxjs'; import { deepFreeze } from '../../utils'; +import { PluginName } from '../plugins'; /** * The current status of a service at a point in time. @@ -116,6 +117,60 @@ export interface CoreStatus { /** * API for accessing status of Core and this plugin's dependencies as well as for customizing this plugin's status. + * + * @remarks + * By default, a plugin inherits it's current status from the most severe status level of any Core services and any + * plugins that it depends on. This default status is available on the + * {@link ServiceStatusSetup.derivedStatus$ | core.status.derviedStatus$} API. + * + * Plugins may customize their status calculation by calling the {@link ServiceStatusSetup.set | core.status.set} API + * with an Observable. Within this Observable, a plugin may choose to only depend on the status of some of its + * dependencies, to ignore severe status levels of particular Core services they are not concerned with, or to make its + * status dependent on other external services. + * + * @example + * Customize a plugin's status to only depend on the status of SavedObjects: + * ```ts + * core.status.set( + * core.status.core$.pipe( + * . map((coreStatus) => { + * return coreStatus.savedObjects; + * }) ; + * ); + * ); + * ``` + * + * @example + * Customize a plugin's status to include an external service: + * ```ts + * const externalStatus$ = interval(1000).pipe( + * switchMap(async () => { + * const resp = await fetch(`https://myexternaldep.com/_healthz`); + * const body = await resp.json(); + * if (body.ok) { + * return of({ level: ServiceStatusLevels.available, summary: 'External Service is up'}); + * } else { + * return of({ level: ServiceStatusLevels.available, summary: 'External Service is unavailable'}); + * } + * }), + * catchError((error) => { + * of({ level: ServiceStatusLevels.unavailable, summary: `External Service is down`, meta: { error }}) + * }) + * ); + * + * core.status.set( + * combineLatest([core.status.derivedStatus$, externalStatus$]).pipe( + * map(([derivedStatus, externalStatus]) => { + * if (externalStatus.level > derivedStatus) { + * return externalStatus; + * } else { + * return derivedStatus; + * } + * }) + * ) + * ); + * ``` + * * @public */ export interface StatusServiceSetup { @@ -134,9 +189,43 @@ export interface StatusServiceSetup { * only depend on the statuses of {@link StatusServiceSetup.core$ | Core} or their dependencies. */ overall$: Observable; + + /** + * Allows a plugin to specify a custom status dependent on its own criteria. + * Completely overrides the default inherited status. + * + * @remarks + * See the {@link StatusServiceSetup.derivedStatus$} API for leveraging the default status + * calculation that is provided by Core. + */ + set(status$: Observable): void; + + /** + * Current status for all plugins this plugin depends on. + * Each key of the `Record` is a plugin id. + */ + dependencies$: Observable>; + + /** + * The status of this plugin as derived from its dependencies. + * + * @remarks + * By default, plugins inherit this derived status from their dependencies. + * Calling {@link StatusSetup.set} overrides this default status. + * + * This may emit multliple times for a single status change event as propagates + * through the dependency tree + */ + derivedStatus$: Observable; } /** @internal */ -export interface InternalStatusServiceSetup extends StatusServiceSetup { +export interface InternalStatusServiceSetup extends Pick { isStatusPageAnonymous: () => boolean; + // Namespaced under `plugins` key to improve clarity that these are APIs for plugins specifically. + plugins: { + set(plugin: PluginName, status$: Observable): void; + getDependenciesStatus$(plugin: PluginName): Observable>; + getDerivedStatus$(plugin: PluginName): Observable; + }; } diff --git a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts index 61b71f8c5de07..c7d5413ecca56 100644 --- a/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts +++ b/src/core/server/ui_settings/create_or_upgrade_saved_config/integration_tests/create_or_upgrade.test.ts @@ -36,8 +36,6 @@ describe('createOrUpgradeSavedConfig()', () => { let esServer: TestElasticsearchUtils; let kbn: TestKibanaUtils; - let kbnServer: TestKibanaUtils['kbnServer']; - beforeAll(async function () { servers = createTestServers({ adjustTimeout: (t) => { @@ -46,10 +44,8 @@ describe('createOrUpgradeSavedConfig()', () => { }); esServer = await servers.startES(); kbn = await servers.startKibana(); - kbnServer = kbn.kbnServer; - const savedObjects = kbnServer.server.savedObjects; - savedObjectsClient = savedObjects.getScopedSavedObjectsClient( + savedObjectsClient = kbn.coreStart.savedObjects.getScopedClient( httpServerMock.createKibanaRequest() ); diff --git a/src/core/server/ui_settings/integration_tests/lib/servers.ts b/src/core/server/ui_settings/integration_tests/lib/servers.ts index 297deb0233c57..0bdc821f42581 100644 --- a/src/core/server/ui_settings/integration_tests/lib/servers.ts +++ b/src/core/server/ui_settings/integration_tests/lib/servers.ts @@ -68,8 +68,7 @@ export function getServices() { const callCluster = esServer.es.getCallCluster(); - const savedObjects = kbnServer.server.savedObjects; - const savedObjectsClient = savedObjects.getScopedSavedObjectsClient( + const savedObjectsClient = kbn.coreStart.savedObjects.getScopedClient( httpServerMock.createKibanaRequest() ); diff --git a/src/core/test_helpers/kbn_server.ts b/src/core/test_helpers/kbn_server.ts index a494c6aa31d6f..488c4b919d3e4 100644 --- a/src/core/test_helpers/kbn_server.ts +++ b/src/core/test_helpers/kbn_server.ts @@ -32,6 +32,7 @@ import { resolve } from 'path'; import { BehaviorSubject } from 'rxjs'; import supertest from 'supertest'; +import { CoreStart } from 'src/core/server'; import { LegacyAPICaller } from '../server/elasticsearch'; import { CliArgs, Env } from '../server/config'; import { Root } from '../server/root'; @@ -170,6 +171,7 @@ export interface TestElasticsearchUtils { export interface TestKibanaUtils { root: Root; + coreStart: CoreStart; kbnServer: KbnServer; stop: () => Promise; } @@ -289,13 +291,14 @@ export function createTestServers({ const root = createRootWithCorePlugins(kbnSettings); await root.setup(); - await root.start(); + const coreStart = await root.start(); const kbnServer = getKbnServer(root); return { root, kbnServer, + coreStart, stop: async () => await root.shutdown(), }; }, diff --git a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker index d7f137e965327..b02b7cc16ec4a 100755 --- a/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker +++ b/src/dev/build/tasks/os_packages/docker_generator/resources/bin/kibana-docker @@ -18,6 +18,8 @@ kibana_vars=( console.enabled console.proxyConfig console.proxyFilter + ops.cGroupOverrides.cpuPath + ops.cGroupOverrides.cpuAcctPath cpu.cgroup.path.override cpuacct.cgroup.path.override csp.rules @@ -279,4 +281,4 @@ umask 0002 # Therefore, we set this value here so that cgroup statistics are # available for the container this process will run in. -exec /usr/share/kibana/bin/kibana --cpu.cgroup.path.override=/ --cpuacct.cgroup.path.override=/ ${longopts} "$@" +exec /usr/share/kibana/bin/kibana --ops.cGroupOverrides.cpuPath=/ --ops.cGroupOverrides.cpuAcctPath=/ ${longopts} "$@" diff --git a/src/fixtures/stubbed_saved_object_index_pattern.ts b/src/fixtures/stubbed_saved_object_index_pattern.ts index 02e6cb85e341f..44b391f14cf9c 100644 --- a/src/fixtures/stubbed_saved_object_index_pattern.ts +++ b/src/fixtures/stubbed_saved_object_index_pattern.ts @@ -30,6 +30,7 @@ export function stubbedSavedObjectIndexPattern(id: string | null = null) { timeFieldName: 'timestamp', customFormats: '{}', fields: mockLogstashFields, + title: 'title', }, version: 2, }; diff --git a/src/legacy/core_plugins/elasticsearch/index.js b/src/legacy/core_plugins/elasticsearch/index.js index 599886788604b..f90f490d68035 100644 --- a/src/legacy/core_plugins/elasticsearch/index.js +++ b/src/legacy/core_plugins/elasticsearch/index.js @@ -16,18 +16,13 @@ * specific language governing permissions and limitations * under the License. */ -import { first } from 'rxjs/operators'; import { Cluster } from './server/lib/cluster'; import { createProxy } from './server/lib/create_proxy'; export default function (kibana) { - let defaultVars; - return new kibana.Plugin({ require: [], - uiExports: { injectDefaultVars: () => defaultVars }, - async init(server) { // All methods that ES plugin exposes are synchronous so we should get the first // value from all observables here to be able to synchronously return and create @@ -36,16 +31,6 @@ export default function (kibana) { const adminCluster = new Cluster(client); const dataCluster = new Cluster(client); - const esConfig = await server.newPlatform.__internals.elasticsearch.legacy.config$ - .pipe(first()) - .toPromise(); - - defaultVars = { - esRequestTimeout: esConfig.requestTimeout.asMilliseconds(), - esShardTimeout: esConfig.shardTimeout.asMilliseconds(), - esApiVersion: esConfig.apiVersion, - }; - const clusters = new Map(); server.expose('getCluster', (name) => { if (name === 'admin') { diff --git a/src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts b/src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts index e51a355cbc8d2..e1ed2f57375a4 100644 --- a/src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts +++ b/src/legacy/plugin_discovery/plugin_spec/plugin_spec_options.d.ts @@ -18,14 +18,10 @@ */ import { Server } from '../../server/kbn_server'; import { Capabilities } from '../../../core/server'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectsLegacyManagementDefinition } from '../../../core/server/saved_objects/types'; export type InitPluginFunction = (server: Server) => void; export interface UiExports { injectDefaultVars?: (server: Server) => { [key: string]: any }; - savedObjectsManagement?: SavedObjectsLegacyManagementDefinition; - mappings?: unknown; } export interface PluginSpecOptions { diff --git a/src/legacy/plugin_discovery/types.ts b/src/legacy/plugin_discovery/types.ts index 283806f69599a..700ca6fa68c95 100644 --- a/src/legacy/plugin_discovery/types.ts +++ b/src/legacy/plugin_discovery/types.ts @@ -19,11 +19,6 @@ import { Server } from '../server/kbn_server'; import { Capabilities } from '../../core/server'; -// Disable lint errors for imports from src/core/* until SavedObjects migration is complete -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectsSchemaDefinition } from '../../core/server/saved_objects/schema'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { SavedObjectsLegacyManagementDefinition } from '../../core/server/saved_objects/types'; import { AppCategory } from '../../core/types'; /** @@ -70,8 +65,6 @@ export interface LegacyPluginOptions { home: string[]; mappings: any; migrations: any; - savedObjectSchemas: SavedObjectsSchemaDefinition; - savedObjectsManagement: SavedObjectsLegacyManagementDefinition; visTypes: string[]; embeddableActions?: string[]; embeddableFactories?: string[]; diff --git a/src/legacy/server/config/schema.js b/src/legacy/server/config/schema.js index dd65e45659ffc..ce7a500a00dc8 100644 --- a/src/legacy/server/config/schema.js +++ b/src/legacy/server/config/schema.js @@ -49,22 +49,6 @@ export default () => csp: HANDLED_IN_NEW_PLATFORM, - cpu: Joi.object({ - cgroup: Joi.object({ - path: Joi.object({ - override: Joi.string().default(), - }), - }), - }), - - cpuacct: Joi.object({ - cgroup: Joi.object({ - path: Joi.object({ - override: Joi.string().default(), - }), - }), - }), - server: Joi.object({ name: Joi.string().default(os.hostname()), // keep them for BWC, remove when not used in Legacy. @@ -144,6 +128,10 @@ export default () => ops: Joi.object({ interval: Joi.number().default(5000), + cGroupOverrides: Joi.object().keys({ + cpuPath: Joi.string().default(), + cpuAcctPath: Joi.string().default(), + }), }).default(), plugins: Joi.object({ diff --git a/src/legacy/server/kbn_server.d.ts b/src/legacy/server/kbn_server.d.ts index 69fb63fbbd87f..663542618375a 100644 --- a/src/legacy/server/kbn_server.d.ts +++ b/src/legacy/server/kbn_server.d.ts @@ -17,33 +17,24 @@ * under the License. */ -import { ResponseObject, Server } from 'hapi'; -import { UnwrapPromise } from '@kbn/utility-types'; +import { Server } from 'hapi'; import { TelemetryCollectionManagerPluginSetup } from 'src/plugins/telemetry_collection_manager/server'; import { - ConfigService, CoreSetup, CoreStart, - ElasticsearchServiceSetup, EnvironmentMode, LoggerFactory, - SavedObjectsClientContract, - SavedObjectsLegacyService, - SavedObjectsClientProviderOptions, - IUiSettingsClient, PackageInfo, - LegacyRequest, LegacyServiceSetupDeps, - LegacyServiceStartDeps, LegacyServiceDiscoverPlugins, } from '../../core/server'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { LegacyConfig, ILegacyService, ILegacyInternals } from '../../core/server/legacy'; +import { LegacyConfig, ILegacyInternals } from '../../core/server/legacy'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { UiPlugins } from '../../core/server/plugins'; -import { CallClusterWithRequest, ElasticsearchPlugin } from '../core_plugins/elasticsearch'; +import { ElasticsearchPlugin } from '../core_plugins/elasticsearch'; import { UsageCollectionSetup } from '../../plugins/usage_collection/server'; import { HomeServerPluginSetup } from '../../plugins/home/server'; @@ -61,16 +52,9 @@ declare module 'hapi' { interface Server { config: () => KibanaConfig; - savedObjects: SavedObjectsLegacyService; logWithMetadata: (tags: string[], message: string, meta: Record) => void; newPlatform: KbnServer['newPlatform']; } - - interface Request { - getSavedObjectsClient(options?: SavedObjectsClientProviderOptions): SavedObjectsClientContract; - getBasePath(): string; - getUiSettingsService(): IUiSettingsClient; - } } type KbnMixinFunc = (kbnServer: KbnServer, server: Server, config: any) => Promise | void; @@ -86,11 +70,9 @@ export interface KibanaCore { __internals: { elasticsearch: LegacyServiceSetupDeps['core']['elasticsearch']; hapiServer: LegacyServiceSetupDeps['core']['http']['server']; - kibanaMigrator: LegacyServiceStartDeps['core']['savedObjects']['migrator']; legacy: ILegacyInternals; rendering: LegacyServiceSetupDeps['core']['rendering']; uiPlugins: UiPlugins; - savedObjectsClientProvider: LegacyServiceStartDeps['core']['savedObjects']['clientProvider']; }; env: { mode: Readonly; @@ -149,6 +131,3 @@ export default class KbnServer { // Re-export commonly used hapi types. export { Server, Request, ResponseToolkit } from 'hapi'; - -// Re-export commonly accessed api types. -export { SavedObjectsLegacyService, SavedObjectsClient } from 'src/core/server'; diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index 4692262d99bb5..a5eefd140c8fa 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -33,7 +33,6 @@ import pidMixin from './pid'; import configCompleteMixin from './config/complete'; import { optimizeMixin } from '../../optimize'; import * as Plugins from './plugins'; -import { savedObjectsMixin } from './saved_objects/saved_objects_mixin'; import { uiMixin } from '../ui'; import { i18nMixin } from './i18n'; @@ -108,9 +107,6 @@ export default class KbnServer { uiMixin, - // setup saved object routes - savedObjectsMixin, - // setup routes that serve the @kbn/optimizer output optimizeMixin, diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.js b/src/legacy/server/saved_objects/saved_objects_mixin.js deleted file mode 100644 index 96cf2058839cf..0000000000000 --- a/src/legacy/server/saved_objects/saved_objects_mixin.js +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -// Disable lint errors for imports from src/core/server/saved_objects until SavedObjects migration is complete -/* eslint-disable @kbn/eslint/no-restricted-paths */ -import { SavedObjectsSchema } from '../../../core/server/saved_objects/schema'; -import { - SavedObjectsClient, - SavedObjectsRepository, - exportSavedObjectsToStream, - importSavedObjectsFromStream, - resolveSavedObjectsImportErrors, -} from '../../../core/server/saved_objects'; -import { convertTypesToLegacySchema } from '../../../core/server/saved_objects/utils'; - -export function savedObjectsMixin(kbnServer, server) { - const migrator = kbnServer.newPlatform.__internals.kibanaMigrator; - const typeRegistry = kbnServer.newPlatform.start.core.savedObjects.getTypeRegistry(); - const mappings = migrator.getActiveMappings(); - const allTypes = typeRegistry.getAllTypes().map((t) => t.name); - const visibleTypes = typeRegistry.getVisibleTypes().map((t) => t.name); - const schema = new SavedObjectsSchema(convertTypesToLegacySchema(typeRegistry.getAllTypes())); - - server.decorate('server', 'kibanaMigrator', migrator); - - const serializer = kbnServer.newPlatform.start.core.savedObjects.createSerializer(); - - const createRepository = (callCluster, includedHiddenTypes = []) => { - if (typeof callCluster !== 'function') { - throw new TypeError('Repository requires a "callCluster" function to be provided.'); - } - // throw an exception if an extraType is not defined. - includedHiddenTypes.forEach((type) => { - if (!allTypes.includes(type)) { - throw new Error(`Missing mappings for saved objects type '${type}'`); - } - }); - const combinedTypes = visibleTypes.concat(includedHiddenTypes); - const allowedTypes = [...new Set(combinedTypes)]; - - const config = server.config(); - - return new SavedObjectsRepository({ - index: config.get('kibana.index'), - migrator, - mappings, - typeRegistry, - serializer, - allowedTypes, - callCluster, - }); - }; - - const provider = kbnServer.newPlatform.__internals.savedObjectsClientProvider; - - const service = { - types: visibleTypes, - SavedObjectsClient, - SavedObjectsRepository, - getSavedObjectsRepository: createRepository, - getScopedSavedObjectsClient: (...args) => provider.getClient(...args), - setScopedSavedObjectsClientFactory: (...args) => provider.setClientFactory(...args), - addScopedSavedObjectsClientWrapperFactory: (...args) => - provider.addClientWrapperFactory(...args), - importExport: { - objectLimit: server.config().get('savedObjects.maxImportExportSize'), - importSavedObjects: importSavedObjectsFromStream, - resolveImportErrors: resolveSavedObjectsImportErrors, - getSortedObjectsForExport: exportSavedObjectsToStream, - }, - schema, - }; - server.decorate('server', 'savedObjects', service); - - const savedObjectsClientCache = new WeakMap(); - server.decorate('request', 'getSavedObjectsClient', function (options) { - const request = this; - - if (savedObjectsClientCache.has(request)) { - return savedObjectsClientCache.get(request); - } - - const savedObjectsClient = server.savedObjects.getScopedSavedObjectsClient(request, options); - - savedObjectsClientCache.set(request, savedObjectsClient); - return savedObjectsClient; - }); -} diff --git a/src/legacy/server/saved_objects/saved_objects_mixin.test.js b/src/legacy/server/saved_objects/saved_objects_mixin.test.js deleted file mode 100644 index d1d6c052ad589..0000000000000 --- a/src/legacy/server/saved_objects/saved_objects_mixin.test.js +++ /dev/null @@ -1,267 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { savedObjectsMixin } from './saved_objects_mixin'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { mockKibanaMigrator } from '../../../core/server/saved_objects/migrations/kibana/kibana_migrator.mock'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { savedObjectsClientProviderMock } from '../../../core/server/saved_objects/service/lib/scoped_client_provider.mock'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { convertLegacyTypes } from '../../../core/server/saved_objects/utils'; -import { SavedObjectTypeRegistry } from '../../../core/server'; -import { coreMock } from '../../../core/server/mocks'; - -const mockConfig = { - get: jest.fn().mockReturnValue('anything'), -}; - -const savedObjectMappings = [ - { - pluginId: 'testtype', - properties: { - testtype: { - properties: { - name: { type: 'keyword' }, - }, - }, - }, - }, - { - pluginId: 'testtype2', - properties: { - doc1: { - properties: { - name: { type: 'keyword' }, - }, - }, - doc2: { - properties: { - name: { type: 'keyword' }, - }, - }, - }, - }, - { - pluginId: 'secretPlugin', - properties: { - hiddentype: { - properties: { - secret: { type: 'keyword' }, - }, - }, - }, - }, -]; - -const savedObjectSchemas = { - hiddentype: { - hidden: true, - }, - doc1: { - indexPattern: 'other-index', - }, -}; - -const savedObjectTypes = convertLegacyTypes( - { - savedObjectMappings, - savedObjectSchemas, - savedObjectMigrations: {}, - }, - mockConfig -); - -const typeRegistry = new SavedObjectTypeRegistry(); -savedObjectTypes.forEach((type) => typeRegistry.registerType(type)); - -const migrator = mockKibanaMigrator.create({ - types: savedObjectTypes, -}); - -describe('Saved Objects Mixin', () => { - let mockKbnServer; - let mockServer; - const mockCallCluster = jest.fn(); - const stubCallCluster = jest.fn(); - const config = { - 'kibana.index': 'kibana.index', - 'savedObjects.maxImportExportSize': 10000, - }; - const stubConfig = jest.fn((key) => { - return config[key]; - }); - - beforeEach(() => { - const clientProvider = savedObjectsClientProviderMock.create(); - mockServer = { - log: jest.fn(), - route: jest.fn(), - decorate: jest.fn(), - config: () => { - return { - get: stubConfig, - }; - }, - plugins: { - elasticsearch: { - getCluster: () => { - return { - callWithRequest: mockCallCluster, - callWithInternalUser: stubCallCluster, - }; - }, - waitUntilReady: jest.fn(), - }, - }, - }; - - const coreStart = coreMock.createStart(); - coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); - - mockKbnServer = { - newPlatform: { - __internals: { - kibanaMigrator: migrator, - savedObjectsClientProvider: clientProvider, - }, - setup: { - core: coreMock.createSetup(), - }, - start: { - core: coreStart, - }, - }, - server: mockServer, - ready: () => {}, - pluginSpecs: { - some: () => { - return true; - }, - }, - uiExports: { - savedObjectMappings, - savedObjectSchemas, - }, - }; - }); - - describe('Saved object service', () => { - let service; - - beforeEach(async () => { - await savedObjectsMixin(mockKbnServer, mockServer); - const call = mockServer.decorate.mock.calls.filter( - ([objName, methodName]) => objName === 'server' && methodName === 'savedObjects' - ); - service = call[0][2]; - }); - - it('should return all but hidden types', async () => { - expect(service).toBeDefined(); - expect(service.types).toEqual(['testtype', 'doc1', 'doc2']); - }); - - const mockCallEs = jest.fn(); - describe('repository creation', () => { - it('should not allow a repository with an undefined type', () => { - expect(() => { - service.getSavedObjectsRepository(mockCallEs, ['extraType']); - }).toThrow(new Error("Missing mappings for saved objects type 'extraType'")); - }); - - it('should create a repository without hidden types', () => { - const repository = service.getSavedObjectsRepository(mockCallEs); - expect(repository).toBeDefined(); - expect(repository._allowedTypes).toEqual(['testtype', 'doc1', 'doc2']); - }); - - it('should create a repository with a unique list of allowed types', () => { - const repository = service.getSavedObjectsRepository(mockCallEs, ['doc1', 'doc1', 'doc1']); - expect(repository._allowedTypes).toEqual(['testtype', 'doc1', 'doc2']); - }); - - it('should create a repository with extraTypes minus duplicate', () => { - const repository = service.getSavedObjectsRepository(mockCallEs, [ - 'hiddentype', - 'hiddentype', - ]); - expect(repository._allowedTypes).toEqual(['testtype', 'doc1', 'doc2', 'hiddentype']); - }); - - it('should not allow a repository without a callCluster function', () => { - expect(() => { - service.getSavedObjectsRepository({}); - }).toThrow(new Error('Repository requires a "callCluster" function to be provided.')); - }); - }); - - describe('get client', () => { - it('should have a method to get the client', () => { - expect(service).toHaveProperty('getScopedSavedObjectsClient'); - }); - - it('should have a method to set the client factory', () => { - expect(service).toHaveProperty('setScopedSavedObjectsClientFactory'); - }); - - it('should have a method to add a client wrapper factory', () => { - expect(service).toHaveProperty('addScopedSavedObjectsClientWrapperFactory'); - }); - - it('should allow you to set a scoped saved objects client factory', () => { - expect(() => { - service.setScopedSavedObjectsClientFactory({}); - }).not.toThrowError(); - }); - - it('should allow you to add a scoped saved objects client wrapper factory', () => { - expect(() => { - service.addScopedSavedObjectsClientWrapperFactory({}); - }).not.toThrowError(); - }); - }); - - describe('#getSavedObjectsClient', () => { - let getSavedObjectsClient; - - beforeEach(() => { - savedObjectsMixin(mockKbnServer, mockServer); - const call = mockServer.decorate.mock.calls.filter( - ([objName, methodName]) => objName === 'request' && methodName === 'getSavedObjectsClient' - ); - getSavedObjectsClient = call[0][2]; - }); - - it('should be callable', () => { - mockServer.savedObjects = service; - getSavedObjectsClient = getSavedObjectsClient.bind({}); - expect(() => { - getSavedObjectsClient(); - }).not.toThrowError(); - }); - - it('should use cached request object', () => { - mockServer.savedObjects = service; - getSavedObjectsClient = getSavedObjectsClient.bind({ _test: 'me' }); - const savedObjectsClient = getSavedObjectsClient(); - expect(getSavedObjectsClient()).toEqual(savedObjectsClient); - }); - }); - }); -}); diff --git a/src/legacy/server/status/lib/metrics.js b/src/legacy/server/status/lib/metrics.js index 2631b245e72ab..478bf0829b1aa 100644 --- a/src/legacy/server/status/lib/metrics.js +++ b/src/legacy/server/status/lib/metrics.js @@ -116,8 +116,8 @@ export class Metrics { async captureCGroups() { try { const cgroup = await cGroupStats({ - cpuPath: this.config.get('cpu.cgroup.path.override'), - cpuAcctPath: this.config.get('cpuacct.cgroup.path.override'), + cpuPath: this.config.get('ops.cGroupOverrides.cpuPath'), + cpuAcctPath: this.config.get('ops.cGroupOverrides.cpuAcctPath'), }); if (isObject(cgroup)) { diff --git a/src/legacy/ui/ui_exports/__tests__/collect_ui_exports.js b/src/legacy/ui/ui_exports/__tests__/collect_ui_exports.js deleted file mode 100644 index 5b2af9f82333c..0000000000000 --- a/src/legacy/ui/ui_exports/__tests__/collect_ui_exports.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import expect from '@kbn/expect'; - -import { PluginPack } from '../../../plugin_discovery'; - -import { collectUiExports } from '../collect_ui_exports'; - -const specs = new PluginPack({ - path: '/dev/null', - pkg: { - name: 'test', - version: 'kibana', - }, - provider({ Plugin }) { - return [ - new Plugin({ - id: 'test', - uiExports: { - savedObjectSchemas: { - foo: { - isNamespaceAgnostic: true, - }, - }, - }, - }), - new Plugin({ - id: 'test2', - uiExports: { - savedObjectSchemas: { - bar: { - isNamespaceAgnostic: true, - }, - }, - }, - }), - ]; - }, -}).getPluginSpecs(); - -describe('plugin discovery', () => { - describe('collectUiExports()', () => { - it('merges uiExports from all provided plugin specs', () => { - const uiExports = collectUiExports(specs); - - expect(uiExports.savedObjectSchemas).to.eql({ - foo: { - isNamespaceAgnostic: true, - }, - bar: { - isNamespaceAgnostic: true, - }, - }); - }); - - it(`throws an error when migrations and mappings aren't defined in the same plugin`, () => { - const invalidSpecs = new PluginPack({ - path: '/dev/null', - pkg: { - name: 'test', - version: 'kibana', - }, - provider({ Plugin }) { - return [ - new Plugin({ - id: 'test', - uiExports: { - mappings: { - 'test-type': { - properties: {}, - }, - }, - }, - }), - new Plugin({ - id: 'test2', - uiExports: { - migrations: { - 'test-type': { - '1.2.3': (doc) => { - return doc; - }, - }, - }, - }, - }), - ]; - }, - }).getPluginSpecs(); - expect(() => collectUiExports(invalidSpecs)).to.throwError((err) => { - expect(err).to.be.a(Error); - expect(err).to.have.property( - 'message', - 'Migrations and mappings must be defined together in the uiExports of a single plugin. ' + - 'test2 defines migrations for types test-type but does not define their mappings.' - ); - }); - }); - }); -}); diff --git a/src/legacy/ui/ui_render/ui_render_mixin.js b/src/legacy/ui/ui_render/ui_render_mixin.js index cd8dcf5aff71d..e3b7c1e0c3ff9 100644 --- a/src/legacy/ui/ui_render/ui_render_mixin.js +++ b/src/legacy/ui/ui_render/ui_render_mixin.js @@ -193,13 +193,11 @@ export function uiRenderMixin(kbnServer, server, config) { async function renderApp(h) { const app = { getId: () => 'core' }; const { http } = kbnServer.newPlatform.setup.core; - const { - rendering, - legacy, - savedObjectsClientProvider: savedObjects, - } = kbnServer.newPlatform.__internals; + const { savedObjects } = kbnServer.newPlatform.start.core; + const { rendering, legacy } = kbnServer.newPlatform.__internals; + const req = KibanaRequest.from(h.request); const uiSettings = kbnServer.newPlatform.start.core.uiSettings.asScopedToClient( - savedObjects.getClient(h.request) + savedObjects.getScopedClient(req) ); const vars = await legacy.getVars(app.getId(), h.request, { apmConfig: getApmConfig(h.request.path), diff --git a/src/legacy/utils/index.js b/src/legacy/utils/index.js index 529b1ddfd8a4d..e2e2331b3aea6 100644 --- a/src/legacy/utils/index.js +++ b/src/legacy/utils/index.js @@ -17,8 +17,6 @@ * under the License. */ -export { BinderBase } from './binder'; -export { BinderFor } from './binder_for'; export { deepCloneWithBuffers } from './deep_clone_with_buffers'; export { unset } from './unset'; export { IS_KIBANA_DISTRIBUTABLE } from './artifact_type'; diff --git a/src/plugins/dashboard/kibana.json b/src/plugins/dashboard/kibana.json index 1b38c6d124fe1..531074f9fa60b 100644 --- a/src/plugins/dashboard/kibana.json +++ b/src/plugins/dashboard/kibana.json @@ -6,6 +6,7 @@ "embeddable", "inspector", "kibanaLegacy", + "urlForwarding", "navigation", "uiActions", "savedObjects" diff --git a/src/plugins/dashboard/public/application/actions/open_replace_panel_flyout.tsx b/src/plugins/dashboard/public/application/actions/open_replace_panel_flyout.tsx index c676ca052d687..54a294fd2f4ac 100644 --- a/src/plugins/dashboard/public/application/actions/open_replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/application/actions/open_replace_panel_flyout.tsx @@ -60,7 +60,8 @@ export async function openReplacePanelFlyout(options: { /> ), { - 'data-test-subj': 'replacePanelFlyout', + 'data-test-subj': 'dashboardReplacePanel', + ownFocus: true, } ); } diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx b/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx index 0000f63c48c2d..4e228bc1a7a06 100644 --- a/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx +++ b/src/plugins/dashboard/public/application/actions/replace_panel_flyout.tsx @@ -19,16 +19,15 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; -import _ from 'lodash'; -import { EuiFlyout, EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; +import { EuiFlyoutBody, EuiFlyoutHeader, EuiTitle } from '@elastic/eui'; import { NotificationsStart, Toast } from 'src/core/public'; import { DashboardPanelState } from '../embeddable'; import { - IContainer, - IEmbeddable, EmbeddableInput, EmbeddableOutput, EmbeddableStart, + IContainer, + IEmbeddable, SavedObjectEmbeddableInput, } from '../../embeddable_plugin'; @@ -122,7 +121,7 @@ export class ReplacePanelFlyout extends React.Component { const panelToReplace = 'Replace panel ' + this.props.panelToRemove.getTitle() + ' with:'; return ( - + <>

@@ -131,7 +130,7 @@ export class ReplacePanelFlyout extends React.Component { {savedObjectsFinder} - + ); } } diff --git a/src/plugins/dashboard/public/application/application.ts b/src/plugins/dashboard/public/application/application.ts index 21f423d009ee7..b0a5b0472ec47 100644 --- a/src/plugins/dashboard/public/application/application.ts +++ b/src/plugins/dashboard/public/application/application.ts @@ -41,6 +41,7 @@ import { NavigationPublicPluginStart as NavigationStart } from '../../../navigat import { DataPublicPluginStart } from '../../../data/public'; import { SharePluginStart } from '../../../share/public'; import { KibanaLegacyStart, configureAppAngularModule } from '../../../kibana_legacy/public'; +import { UrlForwardingStart } from '../../../url_forwarding/public'; import { SavedObjectLoader, SavedObjectsStart } from '../../../saved_objects/public'; // required for i18nIdDirective @@ -69,8 +70,8 @@ export interface RenderDeps { localStorage: Storage; share?: SharePluginStart; usageCollection?: UsageCollectionSetup; - navigateToDefaultApp: KibanaLegacyStart['navigateToDefaultApp']; - navigateToLegacyKibanaUrl: KibanaLegacyStart['navigateToLegacyKibanaUrl']; + navigateToDefaultApp: UrlForwardingStart['navigateToDefaultApp']; + navigateToLegacyKibanaUrl: UrlForwardingStart['navigateToLegacyKibanaUrl']; scopedHistory: () => ScopedHistory; savedObjects: SavedObjectsStart; restorePreviousUrl: () => void; diff --git a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx index 212b54be9ae04..92d6f2ed91dde 100644 --- a/src/plugins/dashboard/public/application/dashboard_app_controller.tsx +++ b/src/plugins/dashboard/public/application/dashboard_app_controller.tsx @@ -88,8 +88,8 @@ import { AngularHttpError, KibanaLegacyStart, subscribeWithScope, - migrateLegacyQuery, } from '../../../kibana_legacy/public'; +import { migrateLegacyQuery } from './lib/migrate_legacy_query'; export interface DashboardAppControllerDependencies extends RenderDeps { $scope: DashboardAppScope; diff --git a/src/plugins/dashboard/public/application/dashboard_state_manager.ts b/src/plugins/dashboard/public/application/dashboard_state_manager.ts index 5fed38487dc54..910a2b470b2eb 100644 --- a/src/plugins/dashboard/public/application/dashboard_state_manager.ts +++ b/src/plugins/dashboard/public/application/dashboard_state_manager.ts @@ -25,7 +25,7 @@ import { History } from 'history'; import { Filter, Query, TimefilterContract as Timefilter } from 'src/plugins/data/public'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/public'; -import { migrateLegacyQuery } from '../../../kibana_legacy/public'; +import { migrateLegacyQuery } from './lib/migrate_legacy_query'; import { ViewMode } from '../embeddable_plugin'; import { getAppStateDefaults, migrateAppState, getDashboardIdFromUrl } from './lib'; diff --git a/src/plugins/kibana_legacy/common/migrate_legacy_query.ts b/src/plugins/dashboard/public/application/lib/migrate_legacy_query.ts similarity index 100% rename from src/plugins/kibana_legacy/common/migrate_legacy_query.ts rename to src/plugins/dashboard/public/application/lib/migrate_legacy_query.ts diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 0ce6f9489ea02..49584f62215ea 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -33,6 +33,7 @@ import { SavedObjectsClientContract, ScopedHistory, } from 'src/core/public'; +import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwarding/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; import { CONTEXT_MENU_TRIGGER, @@ -125,6 +126,7 @@ interface SetupDependencies { embeddable: EmbeddableSetup; home?: HomePublicPluginSetup; kibanaLegacy: KibanaLegacySetup; + urlForwarding: UrlForwardingSetup; share?: SharePluginSetup; uiActions: UiActionsSetup; usageCollection?: UsageCollectionSetup; @@ -133,6 +135,7 @@ interface SetupDependencies { interface StartDependencies { data: DataPublicPluginStart; kibanaLegacy: KibanaLegacyStart; + urlForwarding: UrlForwardingStart; embeddable: EmbeddableStart; inspector: InspectorStartContract; navigation: NavigationStart; @@ -190,7 +193,16 @@ export class DashboardPlugin public setup( core: CoreSetup, - { share, uiActions, embeddable, home, kibanaLegacy, data, usageCollection }: SetupDependencies + { + share, + uiActions, + embeddable, + home, + kibanaLegacy, + urlForwarding, + data, + usageCollection, + }: SetupDependencies ): Setup { this.dashboardFeatureFlagConfig = this.initializerContext.config.get< DashboardFeatureFlagConfig @@ -311,7 +323,8 @@ export class DashboardPlugin navigation, share: shareStart, data: dataStart, - kibanaLegacy: { dashboardConfig, navigateToDefaultApp, navigateToLegacyKibanaUrl }, + kibanaLegacy: { dashboardConfig }, + urlForwarding: { navigateToDefaultApp, navigateToLegacyKibanaUrl }, savedObjects, } = pluginsStart; @@ -357,7 +370,7 @@ export class DashboardPlugin initAngularBootstrap(); core.application.register(app); - kibanaLegacy.forwardApp( + urlForwarding.forwardApp( DashboardConstants.DASHBOARDS_ID, DashboardConstants.DASHBOARDS_ID, (path) => { @@ -366,7 +379,7 @@ export class DashboardPlugin return `#/list${tail || ''}`; } ); - kibanaLegacy.forwardApp( + urlForwarding.forwardApp( DashboardConstants.DASHBOARD_ID, DashboardConstants.DASHBOARDS_ID, (path) => { diff --git a/src/plugins/data/common/constants.ts b/src/plugins/data/common/constants.ts index 22db1552e4303..43120583bd3a4 100644 --- a/src/plugins/data/common/constants.ts +++ b/src/plugins/data/common/constants.ts @@ -32,6 +32,7 @@ export const UI_SETTINGS = { COURIER_MAX_CONCURRENT_SHARD_REQUESTS: 'courier:maxConcurrentShardRequests', COURIER_BATCH_SEARCHES: 'courier:batchSearches', SEARCH_INCLUDE_FROZEN: 'search:includeFrozen', + SEARCH_TIMEOUT: 'search:timeout', HISTOGRAM_BAR_TARGET: 'histogram:barTarget', HISTOGRAM_MAX_BARS: 'histogram:maxBars', HISTORY_LIMIT: 'history:limit', diff --git a/src/plugins/data/common/field_formats/converters/duration.test.ts b/src/plugins/data/common/field_formats/converters/duration.test.ts index d6205d54bd702..69163842f3498 100644 --- a/src/plugins/data/common/field_formats/converters/duration.test.ts +++ b/src/plugins/data/common/field_formats/converters/duration.test.ts @@ -24,11 +24,16 @@ describe('Duration Format', () => { inputFormat: 'seconds', outputFormat: 'humanize', outputPrecision: undefined, + showSuffix: undefined, fixtures: [ { input: -60, output: 'minus a minute', }, + { + input: 1, + output: 'a few seconds', + }, { input: 60, output: 'a minute', @@ -44,6 +49,7 @@ describe('Duration Format', () => { inputFormat: 'minutes', outputFormat: 'humanize', outputPrecision: undefined, + showSuffix: undefined, fixtures: [ { input: -60, @@ -64,6 +70,7 @@ describe('Duration Format', () => { inputFormat: 'minutes', outputFormat: 'asHours', outputPrecision: undefined, + showSuffix: undefined, fixtures: [ { input: -60, @@ -84,6 +91,7 @@ describe('Duration Format', () => { inputFormat: 'seconds', outputFormat: 'asSeconds', outputPrecision: 0, + showSuffix: undefined, fixtures: [ { input: -60, @@ -104,6 +112,7 @@ describe('Duration Format', () => { inputFormat: 'seconds', outputFormat: 'asSeconds', outputPrecision: 2, + showSuffix: undefined, fixtures: [ { input: -60, @@ -124,15 +133,34 @@ describe('Duration Format', () => { ], }); + testCase({ + inputFormat: 'seconds', + outputFormat: 'asSeconds', + outputPrecision: 0, + showSuffix: true, + fixtures: [ + { + input: -60, + output: '-60 Seconds', + }, + { + input: -32.333, + output: '-32 Seconds', + }, + ], + }); + function testCase({ inputFormat, outputFormat, outputPrecision, + showSuffix, fixtures, }: { inputFormat: string; outputFormat: string; outputPrecision: number | undefined; + showSuffix: boolean | undefined; fixtures: any[]; }) { fixtures.forEach((fixture: Record) => { @@ -143,7 +171,7 @@ describe('Duration Format', () => { outputPrecision ? `, ${outputPrecision} decimals` : '' }`, () => { const duration = new DurationFormat( - { inputFormat, outputFormat, outputPrecision }, + { inputFormat, outputFormat, outputPrecision, showSuffix }, jest.fn() ); expect(duration.convert(input)).toBe(output); diff --git a/src/plugins/data/common/field_formats/converters/duration.ts b/src/plugins/data/common/field_formats/converters/duration.ts index 53c2aba98120e..a3ce3d4dfd795 100644 --- a/src/plugins/data/common/field_formats/converters/duration.ts +++ b/src/plugins/data/common/field_formats/converters/duration.ts @@ -190,6 +190,7 @@ export class DurationFormat extends FieldFormat { const inputFormat = this.param('inputFormat'); const outputFormat = this.param('outputFormat') as keyof Duration; const outputPrecision = this.param('outputPrecision'); + const showSuffix = Boolean(this.param('showSuffix')); const human = this.isHuman(); const prefix = val < 0 && human @@ -200,6 +201,9 @@ export class DurationFormat extends FieldFormat { const duration = parseInputAsDuration(val, inputFormat) as Record; const formatted = duration[outputFormat](); const precise = human ? formatted : formatted.toFixed(outputPrecision); - return prefix + precise; + const type = outputFormats.find(({ method }) => method === outputFormat); + const suffix = showSuffix && type ? ` ${type.text}` : ''; + + return prefix + precise + suffix; }; } diff --git a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap index a0c380ec55bf6..1871627da76de 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap +++ b/src/plugins/data/common/index_patterns/index_patterns/__snapshots__/index_pattern.test.ts.snap @@ -631,7 +631,7 @@ Object { "id": "test-pattern", "sourceFilters": undefined, "timeFieldName": "timestamp", - "title": "test-pattern", + "title": "title", "typeMeta": undefined, "version": 2, } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts index f037a71b508a2..f49897c47d562 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.test.ts @@ -17,7 +17,7 @@ * under the License. */ -import { defaults, map, last, get } from 'lodash'; +import { defaults, map, last } from 'lodash'; import { IndexPattern } from './index_pattern'; @@ -29,7 +29,7 @@ import { stubbedSavedObjectIndexPattern } from '../../../../../fixtures/stubbed_ import { IndexPatternField } from '../fields'; import { fieldFormatsMock } from '../../field_formats/mocks'; -import { FieldFormat } from '../..'; +import { FieldFormat, IndexPatternsService } from '../..'; class MockFieldFormatter {} @@ -116,6 +116,7 @@ function create(id: string, payload?: any): Promise { apiClient, patternCache, fieldFormats: fieldFormatsMock, + indexPatternsService: {} as IndexPatternsService, onNotification: () => {}, onError: () => {}, shortDotsEnable: false, @@ -151,7 +152,6 @@ describe('IndexPattern', () => { expect(indexPattern).toHaveProperty('getNonScriptedFields'); expect(indexPattern).toHaveProperty('addScriptedField'); expect(indexPattern).toHaveProperty('removeScriptedField'); - expect(indexPattern).toHaveProperty('save'); // properties expect(indexPattern).toHaveProperty('fields'); @@ -389,60 +389,4 @@ describe('IndexPattern', () => { }); }); }); - - test('should handle version conflicts', async () => { - setDocsourcePayload(null, { - id: 'foo', - version: 'foo', - attributes: { - title: 'something', - }, - }); - // Create a normal index pattern - const pattern = new IndexPattern('foo', { - savedObjectsClient: savedObjectsClient as any, - apiClient, - patternCache, - fieldFormats: fieldFormatsMock, - onNotification: () => {}, - onError: () => {}, - shortDotsEnable: false, - metaFields: [], - }); - await pattern.init(); - - expect(get(pattern, 'version')).toBe('fooa'); - - // Create the same one - we're going to handle concurrency - const samePattern = new IndexPattern('foo', { - savedObjectsClient: savedObjectsClient as any, - apiClient, - patternCache, - fieldFormats: fieldFormatsMock, - onNotification: () => {}, - onError: () => {}, - shortDotsEnable: false, - metaFields: [], - }); - await samePattern.init(); - - expect(get(samePattern, 'version')).toBe('fooaa'); - - // This will conflict because samePattern did a save (from refreshFields) - // but the resave should work fine - pattern.title = 'foo2'; - await pattern.save(); - - // This should not be able to recover - samePattern.title = 'foo3'; - - let result; - try { - await samePattern.save(); - } catch (err) { - result = err; - } - - expect(result.res.status).toBe(409); - }); }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts index 0558808573580..76f1a5e59d0ee 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_pattern.ts @@ -41,8 +41,8 @@ import { PatternCache } from './_pattern_cache'; import { expandShorthand, FieldMappingSpec, MappingObject } from '../../field_mapping'; import { IndexPatternSpec, TypeMeta, FieldSpec, SourceFilter } from '../types'; import { SerializedFieldFormat } from '../../../../expressions/common'; +import { IndexPatternsService } from '..'; -const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; const savedObjectType = 'index-pattern'; interface IndexPatternDeps { @@ -50,6 +50,7 @@ interface IndexPatternDeps { apiClient: IIndexPatternsApiClient; patternCache: PatternCache; fieldFormats: FieldFormatsStartCommon; + indexPatternsService: IndexPatternsService; onNotification: OnNotification; onError: OnError; shortDotsEnable: boolean; @@ -70,17 +71,18 @@ export class IndexPattern implements IIndexPattern { public flattenHit: any; public metaFields: string[]; - private version: string | undefined; + public version: string | undefined; private savedObjectsClient: SavedObjectsClientCommon; private patternCache: PatternCache; public sourceFilters?: SourceFilter[]; - private originalBody: { [key: string]: any } = {}; + // todo make read only, update via method or factor out + public originalBody: { [key: string]: any } = {}; public fieldsFetcher: any; // probably want to factor out any direct usage and change to private + private indexPatternsService: IndexPatternsService; private shortDotsEnable: boolean = false; private fieldFormats: FieldFormatsStartCommon; private onNotification: OnNotification; private onError: OnError; - private apiClient: IIndexPatternsApiClient; private mapping: MappingObject = expandShorthand({ title: ES_FIELD_TYPES.TEXT, @@ -111,6 +113,7 @@ export class IndexPattern implements IIndexPattern { apiClient, patternCache, fieldFormats, + indexPatternsService, onNotification, onError, shortDotsEnable = false, @@ -121,6 +124,7 @@ export class IndexPattern implements IIndexPattern { this.savedObjectsClient = savedObjectsClient; this.patternCache = patternCache; this.fieldFormats = fieldFormats; + this.indexPatternsService = indexPatternsService; this.onNotification = onNotification; this.onError = onError; @@ -128,7 +132,6 @@ export class IndexPattern implements IIndexPattern { this.metaFields = metaFields; this.fields = fieldList([], this.shortDotsEnable); - this.apiClient = apiClient; this.fieldsFetcher = createFieldsFetcher(this, apiClient, metaFields); this.flattenHit = flattenHitWrapper(this, metaFields); this.formatHit = formatHitProvider( @@ -392,8 +395,6 @@ export class IndexPattern implements IIndexPattern { } else { throw err; } - - await this.save(); } } @@ -402,7 +403,6 @@ export class IndexPattern implements IIndexPattern { if (field) { this.fields.remove(field); } - return this.save(); } async popularizeField(fieldName: string, unit = 1) { @@ -523,92 +523,6 @@ export class IndexPattern implements IIndexPattern { return await _create(potentialDuplicateByTitle.id); } - async save(saveAttempts: number = 0): Promise { - if (!this.id) return; - const body = this.prepBody(); - - const originalChangedKeys: string[] = []; - Object.entries(body).forEach(([key, value]) => { - if (value !== this.originalBody[key]) { - originalChangedKeys.push(key); - } - }); - - return this.savedObjectsClient - .update(savedObjectType, this.id, body, { version: this.version }) - .then((resp) => { - this.id = resp.id; - this.version = resp.version; - }) - .catch((err) => { - if ( - _.get(err, 'res.status') === 409 && - saveAttempts++ < MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS - ) { - const samePattern = new IndexPattern(this.id, { - savedObjectsClient: this.savedObjectsClient, - apiClient: this.apiClient, - patternCache: this.patternCache, - fieldFormats: this.fieldFormats, - onNotification: this.onNotification, - onError: this.onError, - shortDotsEnable: this.shortDotsEnable, - metaFields: this.metaFields, - }); - - return samePattern.init().then(() => { - // What keys changed from now and what the server returned - const updatedBody = samePattern.prepBody(); - - // Build a list of changed keys from the server response - // and ensure we ignore the key if the server response - // is the same as the original response (since that is expected - // if we made a change in that key) - - const serverChangedKeys: string[] = []; - Object.entries(updatedBody).forEach(([key, value]) => { - if (value !== (body as any)[key] && value !== this.originalBody[key]) { - serverChangedKeys.push(key); - } - }); - - let unresolvedCollision = false; - for (const originalKey of originalChangedKeys) { - for (const serverKey of serverChangedKeys) { - if (originalKey === serverKey) { - unresolvedCollision = true; - break; - } - } - } - - if (unresolvedCollision) { - const title = i18n.translate('data.indexPatterns.unableWriteLabel', { - defaultMessage: - 'Unable to write index pattern! Refresh the page to get the most up to date changes for this index pattern.', - }); - - this.onNotification({ title, color: 'danger' }); - throw err; - } - - // Set the updated response on this object - serverChangedKeys.forEach((key) => { - (this as any)[key] = (samePattern as any)[key]; - }); - this.version = samePattern.version; - - // Clear cache - this.patternCache.clear(this.id!); - - // Try the save again - return this.save(saveAttempts); - }); - } - throw err; - }); - } - async _fetchFields() { const fields = await this.fieldsFetcher.fetch(this); const scripted = this.getScriptedFields().map((field) => field.spec); @@ -624,30 +538,37 @@ export class IndexPattern implements IIndexPattern { } refreshFields() { - return this._fetchFields() - .then(() => this.save()) - .catch((err) => { - // https://github.com/elastic/kibana/issues/9224 - // This call will attempt to remap fields from the matching - // ES index which may not actually exist. In that scenario, - // we still want to notify the user that there is a problem - // but we do not want to potentially make any pages unusable - // so do not rethrow the error here - - if (err instanceof IndexPatternMissingIndices) { - this.onNotification({ title: (err as any).message, color: 'danger', iconType: 'alert' }); - return []; - } + return ( + this._fetchFields() + // todo + .then(() => this.indexPatternsService.save(this)) + .catch((err) => { + // https://github.com/elastic/kibana/issues/9224 + // This call will attempt to remap fields from the matching + // ES index which may not actually exist. In that scenario, + // we still want to notify the user that there is a problem + // but we do not want to potentially make any pages unusable + // so do not rethrow the error here + + if (err instanceof IndexPatternMissingIndices) { + this.onNotification({ + title: (err as any).message, + color: 'danger', + iconType: 'alert', + }); + return []; + } - this.onError(err, { - title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', { - defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})', - values: { - id: this.id, - title: this.title, - }, - }), - }); - }); + this.onError(err, { + title: i18n.translate('data.indexPatterns.fetchFieldErrorTitle', { + defaultMessage: 'Error fetching fields for index pattern {title} (ID: {id})', + values: { + id: this.id, + title: this.title, + }, + }), + }); + }) + ); } } diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts index 8223b31042124..c79c7900148ea 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.test.ts @@ -17,28 +17,26 @@ * under the License. */ -import { IndexPatternsService } from './index_patterns'; +import { defaults } from 'lodash'; +import { IndexPatternsService } from '.'; import { fieldFormatsMock } from '../../field_formats/mocks'; -import { - UiSettingsCommon, - IIndexPatternsApiClient, - SavedObjectsClientCommon, - SavedObject, -} from '../types'; +import { stubbedSavedObjectIndexPattern } from '../../../../../fixtures/stubbed_saved_object_index_pattern'; +import { UiSettingsCommon, SavedObjectsClientCommon, SavedObject } from '../types'; + +const createFieldsFetcher = jest.fn().mockImplementation(() => ({ + getFieldsForWildcard: jest.fn().mockImplementation(() => { + return new Promise((resolve) => resolve([])); + }), + every: jest.fn(), +})); const fieldFormats = fieldFormatsMock; -jest.mock('./index_pattern', () => { - class IndexPattern { - init = async () => { - return this; - }; - } +let object: any = {}; - return { - IndexPattern, - }; -}); +function setDocsourcePayload(id: string | null, providedPayload: any) { + object = defaults(providedPayload || {}, stubbedSavedObjectIndexPattern(id)); +} describe('IndexPatterns', () => { let indexPatterns: IndexPatternsService; @@ -53,6 +51,25 @@ describe('IndexPatterns', () => { > ); savedObjectsClient.delete = jest.fn(() => Promise.resolve({}) as Promise); + savedObjectsClient.get = jest.fn().mockImplementation(() => object); + savedObjectsClient.create = jest.fn(); + savedObjectsClient.update = jest + .fn() + .mockImplementation(async (type, id, body, { version }) => { + if (object.version !== version) { + throw new Object({ + res: { + status: 409, + }, + }); + } + object.attributes.title = body.title; + object.version += 'a'; + return { + id: object.id, + version: object.version, + }; + }); indexPatterns = new IndexPatternsService({ uiSettings: ({ @@ -60,7 +77,7 @@ describe('IndexPatterns', () => { getAll: () => {}, } as any) as UiSettingsCommon, savedObjectsClient: (savedObjectsClient as unknown) as SavedObjectsClientCommon, - apiClient: {} as IIndexPatternsApiClient, + apiClient: createFieldsFetcher(), fieldFormats, onNotification: () => {}, onError: () => {}, @@ -70,6 +87,14 @@ describe('IndexPatterns', () => { test('does cache gets for the same id', async () => { const id = '1'; + setDocsourcePayload(id, { + id: 'foo', + version: 'foo', + attributes: { + title: 'something', + }, + }); + const indexPattern = await indexPatterns.get(id); expect(indexPattern).toBeDefined(); @@ -107,4 +132,41 @@ describe('IndexPatterns', () => { await indexPatterns.delete(id); expect(indexPattern).not.toBe(await indexPatterns.get(id)); }); + + test('should handle version conflicts', async () => { + setDocsourcePayload(null, { + id: 'foo', + version: 'foo', + attributes: { + title: 'something', + }, + }); + + // Create a normal index patterns + const pattern = await indexPatterns.make('foo'); + + expect(pattern.version).toBe('fooa'); + + // Create the same one - we're going to handle concurrency + const samePattern = await indexPatterns.make('foo'); + + expect(samePattern.version).toBe('fooaa'); + + // This will conflict because samePattern did a save (from refreshFields) + // but the resave should work fine + pattern.title = 'foo2'; + await indexPatterns.save(pattern); + + // This should not be able to recover + samePattern.title = 'foo3'; + + let result; + try { + await indexPatterns.save(samePattern); + } catch (err) { + result = err; + } + + expect(result.res.status).toBe(409); + }); }); diff --git a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts index fe0d14b2d9c19..88a7e9f6cef4c 100644 --- a/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts +++ b/src/plugins/data/common/index_patterns/index_patterns/index_patterns.ts @@ -17,6 +17,7 @@ * under the License. */ +import { i18n } from '@kbn/i18n'; import { SavedObjectsClientCommon } from '../..'; import { createIndexPatternCache } from '.'; @@ -37,6 +38,8 @@ import { FieldFormatsStartCommon } from '../../field_formats'; import { UI_SETTINGS, SavedObject } from '../../../common'; const indexPatternCache = createIndexPatternCache(); +const MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS = 3; +const savedObjectType = 'index-pattern'; type IndexPatternCachedFieldType = 'id' | 'title'; @@ -181,6 +184,7 @@ export class IndexPatternsService { apiClient: this.apiClient, patternCache: indexPatternCache, fieldFormats: this.fieldFormats, + indexPatternsService: this, onNotification: this.onNotification, onError: this.onError, shortDotsEnable, @@ -191,6 +195,93 @@ export class IndexPatternsService { return indexPattern; } + async save(indexPattern: IndexPattern, saveAttempts: number = 0): Promise { + if (!indexPattern.id) return; + const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); + const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); + + const body = indexPattern.prepBody(); + + const originalChangedKeys: string[] = []; + Object.entries(body).forEach(([key, value]) => { + if (value !== indexPattern.originalBody[key]) { + originalChangedKeys.push(key); + } + }); + + return this.savedObjectsClient + .update(savedObjectType, indexPattern.id, body, { version: indexPattern.version }) + .then((resp) => { + indexPattern.id = resp.id; + indexPattern.version = resp.version; + }) + .catch((err) => { + if (err?.res?.status === 409 && saveAttempts++ < MAX_ATTEMPTS_TO_RESOLVE_CONFLICTS) { + const samePattern = new IndexPattern(indexPattern.id, { + savedObjectsClient: this.savedObjectsClient, + apiClient: this.apiClient, + patternCache: indexPatternCache, + fieldFormats: this.fieldFormats, + indexPatternsService: this, + onNotification: this.onNotification, + onError: this.onError, + shortDotsEnable, + metaFields, + }); + + return samePattern.init().then(() => { + // What keys changed from now and what the server returned + const updatedBody = samePattern.prepBody(); + + // Build a list of changed keys from the server response + // and ensure we ignore the key if the server response + // is the same as the original response (since that is expected + // if we made a change in that key) + + const serverChangedKeys: string[] = []; + Object.entries(updatedBody).forEach(([key, value]) => { + if (value !== (body as any)[key] && value !== indexPattern.originalBody[key]) { + serverChangedKeys.push(key); + } + }); + + let unresolvedCollision = false; + for (const originalKey of originalChangedKeys) { + for (const serverKey of serverChangedKeys) { + if (originalKey === serverKey) { + unresolvedCollision = true; + break; + } + } + } + + if (unresolvedCollision) { + const title = i18n.translate('data.indexPatterns.unableWriteLabel', { + defaultMessage: + 'Unable to write index pattern! Refresh the page to get the most up to date changes for this index pattern.', + }); + + this.onNotification({ title, color: 'danger' }); + throw err; + } + + // Set the updated response on this object + serverChangedKeys.forEach((key) => { + (indexPattern as any)[key] = (samePattern as any)[key]; + }); + indexPattern.version = samePattern.version; + + // Clear cache + indexPatternCache.clear(indexPattern.id!); + + // Try the save again + return this.save(indexPattern, saveAttempts); + }); + } + throw err; + }); + } + async make(id?: string): Promise { const shortDotsEnable = await this.config.get(UI_SETTINGS.SHORT_DOTS_ENABLE); const metaFields = await this.config.get(UI_SETTINGS.META_FIELDS); @@ -200,6 +291,7 @@ export class IndexPatternsService { apiClient: this.apiClient, patternCache: indexPatternCache, fieldFormats: this.fieldFormats, + indexPatternsService: this, onNotification: this.onNotification, onError: this.onError, shortDotsEnable, diff --git a/src/plugins/data/common/search/aggs/types.ts b/src/plugins/data/common/search/aggs/types.ts index dabd653463d4f..aec3dcc9d068c 100644 --- a/src/plugins/data/common/search/aggs/types.ts +++ b/src/plugins/data/common/search/aggs/types.ts @@ -93,7 +93,7 @@ export interface AggsCommonStart { * is only used internally. The difference is that AggsStart includes the * typings for the registry with initialized agg types. * - * @internal + * @public */ export type AggsStart = Assign; diff --git a/src/plugins/data/common/search/es_search/index.ts b/src/plugins/data/common/search/es_search/index.ts index 54757b53b8665..d8f7b5091eb8f 100644 --- a/src/plugins/data/common/search/es_search/index.ts +++ b/src/plugins/data/common/search/es_search/index.ts @@ -17,10 +17,4 @@ * under the License. */ -export { - ISearchRequestParams, - IEsSearchRequest, - IEsSearchResponse, - ES_SEARCH_STRATEGY, - ISearchOptions, -} from './types'; +export * from './types'; diff --git a/src/plugins/data/common/search/es_search/types.ts b/src/plugins/data/common/search/es_search/types.ts index 89faa5b7119c8..81124c1e095f7 100644 --- a/src/plugins/data/common/search/es_search/types.ts +++ b/src/plugins/data/common/search/es_search/types.ts @@ -53,3 +53,6 @@ export interface IEsSearchResponse extends IKibanaSearchResponse { isPartial?: boolean; rawResponse: SearchResponse; } + +export const isEsResponse = (response: any): response is IEsSearchResponse => + response && response.rawResponse; diff --git a/src/plugins/data/common/search/index.ts b/src/plugins/data/common/search/index.ts index 3bfb0ddb89aa9..061974d860246 100644 --- a/src/plugins/data/common/search/index.ts +++ b/src/plugins/data/common/search/index.ts @@ -22,11 +22,4 @@ export * from './es_search'; export * from './expressions'; export * from './tabify'; export * from './types'; - -export { - IEsSearchRequest, - IEsSearchResponse, - ES_SEARCH_STRATEGY, - ISearchRequestParams, - ISearchOptions, -} from './es_search'; +export * from './es_search'; diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json index b4f20ec6225e2..9cb9b1745373a 100644 --- a/src/plugins/data/kibana.json +++ b/src/plugins/data/kibana.json @@ -13,7 +13,6 @@ "usageCollection", "kibanaUtils", "kibanaReact", - "kibanaLegacy", "inspector" ] } diff --git a/src/plugins/data/public/actions/apply_filter_action.ts b/src/plugins/data/public/actions/apply_filter_action.ts index a2621e6ce8802..944da72bd11d1 100644 --- a/src/plugins/data/public/actions/apply_filter_action.ts +++ b/src/plugins/data/public/actions/apply_filter_action.ts @@ -44,6 +44,7 @@ export function createFilterAction( return createAction({ type: ACTION_GLOBAL_APPLY_FILTER, id: ACTION_GLOBAL_APPLY_FILTER, + order: 100, getIconType: () => 'filter', getDisplayName: () => { return i18n.translate('data.filter.applyFilterActionTitle', { diff --git a/src/plugins/data/public/index.ts b/src/plugins/data/public/index.ts index f7b4111df5172..5038af9409316 100644 --- a/src/plugins/data/public/index.ts +++ b/src/plugins/data/public/index.ts @@ -172,7 +172,7 @@ import { } from '../common/field_formats'; import { DateNanosFormat, DateFormat } from './field_formats'; -export { baseFormattersPublic } from './field_formats'; +export { baseFormattersPublic, FieldFormatsStart } from './field_formats'; // Field formats helpers namespace: export const fieldFormats = { @@ -276,6 +276,7 @@ export { QuerySuggestionGetFnArgs, QuerySuggestionBasic, QuerySuggestionField, + AutocompleteStart, } from './autocomplete'; /* @@ -313,6 +314,7 @@ import { export { // aggs + AggConfigSerialized, AggGroupLabels, AggGroupName, AggGroupNames, @@ -337,6 +339,8 @@ export { TabbedTable, } from '../common'; +export type { AggConfigs, AggConfig } from '../common'; + export { // search ES_SEARCH_STRATEGY, @@ -350,6 +354,9 @@ export { IKibanaSearchResponse, injectSearchSourceReferences, ISearch, + ISearchSetup, + ISearchStart, + ISearchStartSearchSource, ISearchGeneric, ISearchSource, parseSearchSourceJSON, @@ -365,6 +372,8 @@ export { EsRawResponseExpressionTypeDefinition, } from './search'; +export type { SearchSource } from './search'; + export { ISearchOptions } from '../common'; // Search namespace @@ -429,8 +438,12 @@ export { TimeHistory, TimefilterContract, TimeHistoryContract, + QueryStateChange, + QueryStart, } from './query'; +export { AggsStart } from './search/aggs'; + export { getTime, // kbn field types @@ -454,7 +467,13 @@ export function plugin(initializerContext: PluginInitializerContext[]; + // (undocumented) + getField(): any; + // (undocumented) + getFieldDisplayName(): any; + // (undocumented) + getIndexPattern(): import("../../../public").IndexPattern; + // (undocumented) + getKey(bucket: any, key?: string): any; + // (undocumented) + getParam(key: string): any; + // (undocumented) + getRequestAggs(): AggConfig[]; + // (undocumented) + getResponseAggs(): AggConfig[]; + // (undocumented) + getTimeRange(): import("../../../public").TimeRange | undefined; + // (undocumented) + getValue(bucket: any): any; + // (undocumented) + id: string; + // (undocumented) + isFilterable(): boolean; + // (undocumented) + makeLabel(percentageMode?: boolean): any; + static nextId(list: IAggConfig[]): number; + onSearchRequestStart(searchSource: ISearchSource_2, options?: ISearchOptions): Promise | Promise; + // (undocumented) + params: any; + // Warning: (ae-incompatible-release-tags) The symbol "parent" is marked as @public, but its signature references "IAggConfigs" which is marked as @internal + // + // (undocumented) + parent?: IAggConfigs; + // (undocumented) + schema?: string; + // Warning: (ae-incompatible-release-tags) The symbol "serialize" is marked as @public, but its signature references "AggConfigSerialized" which is marked as @internal + // + // (undocumented) + serialize(): AggConfigSerialized; + setParams(from: any): void; + // (undocumented) + setType(type: IAggType): void; + // Warning: (ae-incompatible-release-tags) The symbol "toDsl" is marked as @public, but its signature references "IAggConfigs" which is marked as @internal + toDsl(aggConfigs?: IAggConfigs): any; + // (undocumented) + toExpressionAst(): ExpressionAstFunction | undefined; + // Warning: (ae-incompatible-release-tags) The symbol "toJSON" is marked as @public, but its signature references "AggConfigSerialized" which is marked as @internal + // + // @deprecated (undocumented) + toJSON(): AggConfigSerialized; + // Warning: (ae-forgotten-export) The symbol "SerializableState" needs to be exported by the entry point index.d.ts + toSerializedFieldFormat(): {} | Ensure, SerializableState>; + // (undocumented) + get type(): IAggType; + set type(type: IAggType); + // Warning: (ae-incompatible-release-tags) The symbol "write" is marked as @public, but its signature references "IAggConfigs" which is marked as @internal + // + // (undocumented) + write(aggs?: IAggConfigs): Record; +} + +// Warning: (ae-incompatible-release-tags) The symbol "AggConfigOptions" is marked as @public, but its signature references "AggConfigSerialized" which is marked as @internal // Warning: (ae-missing-release-tag) "AggConfigOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -92,6 +174,76 @@ export type AggConfigOptions = Assign; +// Warning: (ae-missing-release-tag) "AggConfigs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class AggConfigs { + // Warning: (ae-forgotten-export) The symbol "AggConfigsOptions" needs to be exported by the entry point index.d.ts + constructor(indexPattern: IndexPattern, configStates: Pick & Pick<{ + type: string | IAggType; + }, "type"> & Pick<{ + type: string | IAggType; + }, never>, "enabled" | "type" | "schema" | "id" | "params">[] | undefined, opts: AggConfigsOptions); + // (undocumented) + aggs: IAggConfig[]; + // (undocumented) + byId(id: string): AggConfig | undefined; + // (undocumented) + byIndex(index: number): AggConfig; + // (undocumented) + byName(name: string): AggConfig[]; + // (undocumented) + bySchemaName(schema: string): AggConfig[]; + // (undocumented) + byType(type: string): AggConfig[]; + // (undocumented) + byTypeName(type: string): AggConfig[]; + // (undocumented) + clone({ enabledOnly }?: { + enabledOnly?: boolean | undefined; + }): AggConfigs; + // Warning: (ae-forgotten-export) The symbol "CreateAggConfigParams" needs to be exported by the entry point index.d.ts + // + // (undocumented) + createAggConfig: (params: CreateAggConfigParams, { addToAggConfigs }?: { + addToAggConfigs?: boolean | undefined; + }) => T; + // (undocumented) + getAll(): AggConfig[]; + // (undocumented) + getRequestAggById(id: string): AggConfig | undefined; + // (undocumented) + getRequestAggs(): AggConfig[]; + getResponseAggById(id: string): AggConfig | undefined; + getResponseAggs(): AggConfig[]; + // (undocumented) + indexPattern: IndexPattern; + jsonDataEquals(aggConfigs: AggConfig[]): boolean; + // (undocumented) + onSearchRequestStart(searchSource: ISearchSource_2, options?: ISearchOptions_2): Promise<[unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown, unknown]>; + // (undocumented) + setTimeRange(timeRange: TimeRange): void; + // (undocumented) + timeRange?: TimeRange; + // (undocumented) + toDsl(hierarchical?: boolean): Record; + } + +// @internal (undocumented) +export type AggConfigSerialized = Ensure<{ + type: string; + enabled?: boolean; + id?: string; + params?: {} | SerializableState; + schema?: string; +}, SerializableState>; + // Warning: (ae-missing-release-tag) "AggGroupLabels" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -127,8 +279,6 @@ export type AggParam = BaseParamType; export interface AggParamOption { // (undocumented) display: string; - // Warning: (ae-forgotten-export) The symbol "AggConfig" needs to be exported by the entry point index.d.ts - // // (undocumented) enabled?(agg: AggConfig): boolean; // (undocumented) @@ -142,10 +292,19 @@ export class AggParamType extends Ba constructor(config: Record); // (undocumented) allowedAggs: string[]; + // Warning: (ae-incompatible-release-tags) The symbol "makeAgg" is marked as @public, but its signature references "AggConfigSerialized" which is marked as @internal + // // (undocumented) makeAgg: (agg: TAggConfig, state?: AggConfigSerialized) => TAggConfig; } +// Warning: (ae-forgotten-export) The symbol "AggsCommonStart" needs to be exported by the entry point index.d.ts +// +// @public +export type AggsStart = Assign; + // Warning: (ae-missing-release-tag) "ApplyGlobalFilterActionContext" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -160,6 +319,11 @@ export interface ApplyGlobalFilterActionContext { timeFieldName?: string; } +// Warning: (ae-forgotten-export) The symbol "AutocompleteService" needs to be exported by the entry point index.d.ts +// +// @public (undocumented) +export type AutocompleteStart = ReturnType; + // Warning: (ae-forgotten-export) The symbol "DateFormat" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "DateNanosFormat" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "baseFormattersPublic" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -200,7 +364,6 @@ export enum BUCKET_TYPES { // @public export const castEsToKbnFieldTypeName: (esType: ES_FIELD_TYPES | string) => KBN_FIELD_TYPES; -// Warning: (ae-forgotten-export) The symbol "QueryStart" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "QuerySetup" needs to be exported by the entry point index.d.ts // Warning: (ae-forgotten-export) The symbol "BaseStateContainer" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "connectToQueryState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -227,7 +390,7 @@ export type CustomFilter = Filter & { // Warning: (ae-missing-release-tag) "DataPublicPluginSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public export interface DataPublicPluginSetup { // Warning: (ae-forgotten-export) The symbol "DataPublicPluginEnhancements" needs to be exported by the entry point index.d.ts // @@ -243,42 +406,47 @@ export interface DataPublicPluginSetup { fieldFormats: FieldFormatsSetup; // (undocumented) query: QuerySetup; - // Warning: (ae-forgotten-export) The symbol "ISearchSetup" needs to be exported by the entry point index.d.ts - // // (undocumented) search: ISearchSetup; } // Warning: (ae-missing-release-tag) "DataPublicPluginStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public export interface DataPublicPluginStart { - // (undocumented) - actions: { - createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction; - createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction; - }; - // Warning: (ae-forgotten-export) The symbol "AutocompleteStart" needs to be exported by the entry point index.d.ts - // - // (undocumented) + actions: DataPublicPluginStartActions; autocomplete: AutocompleteStart; - // Warning: (ae-forgotten-export) The symbol "FieldFormatsStart" needs to be exported by the entry point index.d.ts - // - // (undocumented) fieldFormats: FieldFormatsStart; - // (undocumented) indexPatterns: IndexPatternsContract; - // (undocumented) query: QueryStart; - // Warning: (ae-forgotten-export) The symbol "ISearchStart" needs to be exported by the entry point index.d.ts + search: ISearchStart; + ui: DataPublicPluginStartUi; +} + +// Warning: (ae-missing-release-tag) "DataPublicPluginStartActions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface DataPublicPluginStartActions { + // Warning: (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts // // (undocumented) - search: ISearchStart; + createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction; + // Warning: (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts + // // (undocumented) - ui: { - IndexPatternSelect: React.ComponentType; - SearchBar: React.ComponentType; - }; + createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction; +} + +// Warning: (ae-missing-release-tag) "DataPublicPluginStartUi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export interface DataPublicPluginStartUi { + // Warning: (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts + // + // (undocumented) + IndexPatternSelect: React.ComponentType; + // (undocumented) + SearchBar: React.ComponentType; } // @public (undocumented) @@ -595,6 +763,11 @@ export type FieldFormatsContentType = 'html' | 'text'; // @public (undocumented) export type FieldFormatsGetConfigFn = GetConfigFn; +// @public (undocumented) +export type FieldFormatsStart = Omit & { + deserialize: FormatFactory; +}; + // Warning: (ae-forgotten-export) The symbol "FieldSpec" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "fieldList" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -693,7 +866,6 @@ export const getKbnTypeNames: () => string[]; // // @public (undocumented) export function getSearchParamsFromRequest(searchRequest: SearchRequest, dependencies: { - esShardTimeout: number; getConfig: GetConfigFn; }): ISearchRequestParams; @@ -710,8 +882,6 @@ export function getTime(indexPattern: IIndexPattern | undefined, timeRange: Time // @public export type IAggConfig = AggConfig; -// Warning: (ae-forgotten-export) The symbol "AggConfigs" needs to be exported by the entry point index.d.ts -// // @internal export type IAggConfigs = AggConfigs; @@ -912,7 +1082,7 @@ export type IMetricAggType = MetricAggType; // @public (undocumented) export class IndexPattern implements IIndexPattern { // Warning: (ae-forgotten-export) The symbol "IndexPatternDeps" needs to be exported by the entry point index.d.ts - constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); + constructor(id: string | undefined, { savedObjectsClient, apiClient, patternCache, fieldFormats, indexPatternsService, onNotification, onError, shortDotsEnable, metaFields, }: IndexPatternDeps); // (undocumented) addScriptedField(name: string, script: string, fieldType: string | undefined, lang: string): Promise; // (undocumented) @@ -986,6 +1156,10 @@ export class IndexPattern implements IIndexPattern { // (undocumented) metaFields: string[]; // (undocumented) + originalBody: { + [key: string]: any; + }; + // (undocumented) popularizeField(fieldName: string, unit?: number): Promise; // (undocumented) prepBody(): { @@ -1001,9 +1175,7 @@ export class IndexPattern implements IIndexPattern { // (undocumented) refreshFields(): Promise; // (undocumented) - removeScriptedField(fieldName: string): Promise; - // (undocumented) - save(saveAttempts?: number): Promise; + removeScriptedField(fieldName: string): void; // Warning: (ae-forgotten-export) The symbol "SourceFilter" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -1018,7 +1190,9 @@ export class IndexPattern implements IIndexPattern { type: string | undefined; // (undocumented) typeMeta?: IndexPatternTypeMeta; - } + // (undocumented) + version: string | undefined; +} // Warning: (ae-missing-release-tag) "AggregationRestrictions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1228,11 +1402,40 @@ export interface ISearchOptions { strategy?: string; } -// Warning: (ae-forgotten-export) The symbol "SearchSource" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "ISearchSetup" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public +export interface ISearchSetup { + // Warning: (ae-forgotten-export) The symbol "SearchEnhancements" needs to be exported by the entry point index.d.ts + // + // @internal (undocumented) + __enhance: (enhancements: SearchEnhancements) => void; + // Warning: (ae-forgotten-export) The symbol "AggsSetup" needs to be exported by the entry point index.d.ts + // + // (undocumented) + aggs: AggsSetup; + // Warning: (ae-forgotten-export) The symbol "SearchUsageCollector" needs to be exported by the entry point index.d.ts + // + // (undocumented) + usageCollector?: SearchUsageCollector; +} + +// @public export type ISearchSource = Pick; +// @public +export interface ISearchStart { + aggs: AggsStart; + search: ISearchGeneric; + searchSource: ISearchStartSearchSource; +} + +// @public +export interface ISearchStartSearchSource { + create: (fields?: SearchSourceFields) => Promise; + createEmpty: () => ISearchSource; +} + // Warning: (ae-missing-release-tag) "isFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1447,6 +1650,12 @@ export interface Query { }; } +// Warning: (ae-forgotten-export) The symbol "QueryService" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "QueryStart" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type QueryStart = ReturnType; + // Warning: (ae-missing-release-tag) "QueryState" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -1461,11 +1670,22 @@ export interface QueryState { time?: TimeRange; } +// Warning: (ae-forgotten-export) The symbol "QueryStateChangePartial" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "QueryStateChange" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface QueryStateChange extends QueryStateChangePartial { + // (undocumented) + appFilters?: boolean; + // (undocumented) + globalFilters?: boolean; +} + // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "QueryStringInput" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const QueryStringInput: React.FC>; +export const QueryStringInput: React.FC>; // @public (undocumented) export type QuerySuggestion = QuerySuggestionBasic | QuerySuggestionField; @@ -1678,8 +1898,8 @@ export const search: { // Warning: (ae-missing-release-tag) "SearchBar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "indicateNoData" | "timeHistory" | "onFiltersUpdated">, any> & { - WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; +export const SearchBar: React.ComponentClass, "query" | "isLoading" | "filters" | "onRefresh" | "onRefreshChange" | "refreshInterval" | "indexPatterns" | "dataTestSubj" | "timeHistory" | "customSubmitButton" | "screenTitle" | "showQueryBar" | "showQueryInput" | "showFilterBar" | "showDatePicker" | "showAutoRefreshOnly" | "isRefreshPaused" | "dateRangeFrom" | "dateRangeTo" | "showSaveQuery" | "savedQuery" | "onQueryChange" | "onQuerySubmit" | "onSaved" | "onSavedQueryUpdated" | "onClearSavedQuery" | "indicateNoData" | "onFiltersUpdated">, any> & { + WrappedComponent: React.ComponentType & ReactIntl.InjectedIntlProps>; }; // Warning: (ae-forgotten-export) The symbol "SearchBarOwnProps" needs to be exported by the entry point index.d.ts @@ -1711,32 +1931,28 @@ export interface SearchError { // // @public (undocumented) export class SearchInterceptor { - constructor(deps: SearchInterceptorDeps, requestTimeout?: number | undefined); + constructor(deps: SearchInterceptorDeps); // @internal protected abortController: AbortController; // @internal (undocumented) protected application: CoreStart['application']; // (undocumented) protected readonly deps: SearchInterceptorDeps; - getPendingCount$(): Observable; - // @internal (undocumented) - protected hideToast: () => void; - // @internal - protected longRunningToast?: Toast; // @internal protected pendingCount$: BehaviorSubject; - // (undocumented) - protected readonly requestTimeout?: number | undefined; - // (undocumented) + // @internal (undocumented) protected runSearch(request: IEsSearchRequest, signal: AbortSignal, strategy?: string): Observable; search(request: IEsSearchRequest, options?: ISearchOptions): Observable; - // (undocumented) - protected setupTimers(options?: ISearchOptions): { + // @internal (undocumented) + protected setupAbortSignal({ abortSignal, timeout, }: { + abortSignal?: AbortSignal; + timeout?: number; + }): { combinedSignal: AbortSignal; cleanup: () => void; }; - // @internal (undocumented) - protected showToast: () => void; + // (undocumented) + protected showTimeoutError: ((e: Error) => void) & import("lodash").Cancelable; // @internal protected timeoutSubscriptions: Subscription; } @@ -1753,8 +1969,6 @@ export interface SearchInterceptorDeps { toasts: ToastsSetup; // (undocumented) uiSettings: CoreSetup_2['uiSettings']; - // Warning: (ae-forgotten-export) The symbol "SearchUsageCollector" needs to be exported by the entry point index.d.ts - // // (undocumented) usageCollector?: SearchUsageCollector; } @@ -1762,9 +1976,59 @@ export interface SearchInterceptorDeps { // @internal export type SearchRequest = Record; +// @public (undocumented) +export class SearchSource { + // Warning: (ae-forgotten-export) The symbol "SearchSourceDependencies" needs to be exported by the entry point index.d.ts + constructor(fields: SearchSourceFields | undefined, dependencies: SearchSourceDependencies); + // @deprecated (undocumented) + create(): SearchSource; + createChild(options?: {}): SearchSource; + createCopy(): SearchSource; + destroy(): void; + fetch(options?: ISearchOptions): Promise>; + getField(field: K, recurse?: boolean): SearchSourceFields[K]; + getFields(): { + type?: string | undefined; + query?: import("../..").Query | undefined; + filter?: Filter | Filter[] | (() => Filter | Filter[] | undefined) | undefined; + sort?: Record | Record[] | undefined; + highlight?: any; + highlightAll?: boolean | undefined; + aggs?: any; + from?: number | undefined; + size?: number | undefined; + source?: string | boolean | string[] | undefined; + version?: boolean | undefined; + fields?: string | boolean | string[] | undefined; + index?: import("../..").IndexPattern | undefined; + searchAfter?: import("./types").EsQuerySearchAfter | undefined; + timeout?: string | undefined; + terminate_after?: number | undefined; + }; + getId(): string; + getOwnField(field: K): SearchSourceFields[K]; + getParent(): SearchSource | undefined; + getSearchRequestBody(): Promise; + getSerializedFields(): SearchSourceFields; + // Warning: (ae-incompatible-release-tags) The symbol "history" is marked as @public, but its signature references "SearchRequest" which is marked as @internal + // + // (undocumented) + history: SearchRequest[]; + onRequestStart(handler: (searchSource: SearchSource, options?: ISearchOptions) => Promise): void; + serialize(): { + searchSourceJSON: string; + references: import("../../../../../core/public").SavedObjectReference[]; + }; + setField(field: K, value: SearchSourceFields[K]): this; + setFields(newFields: SearchSourceFields): this; + // Warning: (ae-forgotten-export) The symbol "SearchSourceOptions" needs to be exported by the entry point index.d.ts + setParent(parent?: ISearchSource, options?: SearchSourceOptions): this; + setPreferredSearchStrategyId(searchStrategyId: string): void; +} + // Warning: (ae-missing-release-tag) "SearchSourceFields" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public export interface SearchSourceFields { // (undocumented) aggs?: any; @@ -1778,6 +2042,8 @@ export interface SearchSourceFields { highlight?: any; // (undocumented) highlightAll?: boolean; + // Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "IndexPatternService" + // // (undocumented) index?: IndexPattern; // (undocumented) @@ -1902,6 +2168,7 @@ export const UI_SETTINGS: { readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests"; readonly COURIER_BATCH_SEARCHES: "courier:batchSearches"; readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen"; + readonly SEARCH_TIMEOUT: "search:timeout"; readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget"; readonly HISTOGRAM_MAX_BARS: "histogram:maxBars"; readonly HISTORY_LIMIT: "history:limit"; @@ -1928,6 +2195,8 @@ export const UI_SETTINGS: { // src/plugins/data/common/es_query/filters/match_all_filter.ts:28:3 - (ae-forgotten-export) The symbol "MatchAllFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrase_filter.ts:33:3 - (ae-forgotten-export) The symbol "PhraseFilterMeta" needs to be exported by the entry point index.d.ts // src/plugins/data/common/es_query/filters/phrases_filter.ts:31:3 - (ae-forgotten-export) The symbol "PhrasesFilterMeta" needs to be exported by the entry point index.d.ts +// src/plugins/data/common/search/aggs/types.ts:98:51 - (ae-forgotten-export) The symbol "AggTypesRegistryStart" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/field_formats/field_formats_service.ts:67:3 - (ae-forgotten-export) The symbol "FormatFactory" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FilterLabel" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "FILTERS" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:66:23 - (ae-forgotten-export) The symbol "getDisplayValueFromFilter" needs to be exported by the entry point index.d.ts @@ -1960,25 +2229,22 @@ export const UI_SETTINGS: { // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "getFromSavedObject" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "flattenHitWrapper" needs to be exported by the entry point index.d.ts // src/plugins/data/public/index.ts:234:27 - (ae-forgotten-export) The symbol "formatHitProvider" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:371:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:373:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:374:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:383:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:384:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:385:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:386:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:390:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:391:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/index.ts:398:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:380:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:382:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:383:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:392:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:393:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:394:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:395:1 - (ae-forgotten-export) The symbol "isDateHistogramBucketAggConfig" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:399:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:400:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:403:1 - (ae-forgotten-export) The symbol "parseInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:404:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/public/index.ts:407:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/public/query/state_sync/connect_to_query_state.ts:45:5 - (ae-forgotten-export) The symbol "FilterStateStore" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:62:5 - (ae-forgotten-export) The symbol "createFiltersFromValueClickAction" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:63:5 - (ae-forgotten-export) The symbol "createFiltersFromRangeSelectAction" needs to be exported by the entry point index.d.ts -// src/plugins/data/public/types.ts:71:5 - (ae-forgotten-export) The symbol "IndexPatternSelectProps" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/data/public/search/README.md b/src/plugins/data/public/search/README.md index 33e6d9ab0bd1a..0a123ffa3f1e9 100644 --- a/src/plugins/data/public/search/README.md +++ b/src/plugins/data/public/search/README.md @@ -1,13 +1,23 @@ # search -The `search` plugin provides the ability to register search strategies that take in a request -object, and return a response object, of a given shape. +The `search` service provides you with APIs to query Elasticsearch. -Both client side search strategies can be registered, as well as server side search strategies. +The services are split into two parts: (1) low-level API; and (2) high-level API. -The `search` plugin includes two one concrete client side implementations - - `SYNC_SEARCH_STRATEGY` and `ES_SEARCH_STRATEGY` which uses `SYNC_SEARCH_STRATEGY`. There is also one - default server side search strategy, `ES_SEARCH_STRATEGY`. +## Low-level API - Includes the `esSearch` plugin in order to search for data from Elasticsearch using Elasticsearch -DSL. +With low level API you work directly with elasticsearch DSL + +```typescript +const results = await data.search.search(request, params); +``` + +## High-level API + +Using high-level API you work with Kibana abstractions around Elasticsearch DSL: filters, queries, and aggregations. Provided by the *Search Source* service. + +```typescript +const search = data.search.searchSource.createEmpty(); +search.setField('query', data.query.queryString); +const results = await search.fetch(); +``` diff --git a/src/plugins/data/public/search/collectors/create_usage_collector.test.ts b/src/plugins/data/public/search/collectors/create_usage_collector.test.ts index 315d4678cabf1..9cadb1e796ad6 100644 --- a/src/plugins/data/public/search/collectors/create_usage_collector.test.ts +++ b/src/plugins/data/public/search/collectors/create_usage_collector.test.ts @@ -63,31 +63,4 @@ describe('Search Usage Collector', () => { SEARCH_EVENT_TYPE.QUERIES_CANCELLED ); }); - - test('tracks long popups', async () => { - await usageCollector.trackLongQueryPopupShown(); - expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled(); - expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.LOADED); - expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe( - SEARCH_EVENT_TYPE.LONG_QUERY_POPUP_SHOWN - ); - }); - - test('tracks long popups dismissed', async () => { - await usageCollector.trackLongQueryDialogDismissed(); - expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled(); - expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.CLICK); - expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe( - SEARCH_EVENT_TYPE.LONG_QUERY_DIALOG_DISMISSED - ); - }); - - test('tracks run query beyond timeout', async () => { - await usageCollector.trackLongQueryRunBeyondTimeout(); - expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled(); - expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.CLICK); - expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe( - SEARCH_EVENT_TYPE.LONG_QUERY_RUN_BEYOND_TIMEOUT - ); - }); }); diff --git a/src/plugins/data/public/search/collectors/create_usage_collector.ts b/src/plugins/data/public/search/collectors/create_usage_collector.ts index 321b2c5b99049..187ed90652bb2 100644 --- a/src/plugins/data/public/search/collectors/create_usage_collector.ts +++ b/src/plugins/data/public/search/collectors/create_usage_collector.ts @@ -48,29 +48,5 @@ export const createUsageCollector = ( SEARCH_EVENT_TYPE.QUERIES_CANCELLED ); }, - trackLongQueryPopupShown: async () => { - const currentApp = await getCurrentApp(); - return usageCollection?.reportUiStats( - currentApp!, - METRIC_TYPE.LOADED, - SEARCH_EVENT_TYPE.LONG_QUERY_POPUP_SHOWN - ); - }, - trackLongQueryDialogDismissed: async () => { - const currentApp = await getCurrentApp(); - return usageCollection?.reportUiStats( - currentApp!, - METRIC_TYPE.CLICK, - SEARCH_EVENT_TYPE.LONG_QUERY_DIALOG_DISMISSED - ); - }, - trackLongQueryRunBeyondTimeout: async () => { - const currentApp = await getCurrentApp(); - return usageCollection?.reportUiStats( - currentApp!, - METRIC_TYPE.CLICK, - SEARCH_EVENT_TYPE.LONG_QUERY_RUN_BEYOND_TIMEOUT - ); - }, }; }; diff --git a/src/plugins/data/public/search/collectors/types.ts b/src/plugins/data/public/search/collectors/types.ts index 3e98f901eb0c3..bb7fa1e6ae4a2 100644 --- a/src/plugins/data/public/search/collectors/types.ts +++ b/src/plugins/data/public/search/collectors/types.ts @@ -20,15 +20,9 @@ export enum SEARCH_EVENT_TYPE { QUERY_TIMED_OUT = 'queryTimedOut', QUERIES_CANCELLED = 'queriesCancelled', - LONG_QUERY_POPUP_SHOWN = 'longQueryPopupShown', - LONG_QUERY_DIALOG_DISMISSED = 'longQueryDialogDismissed', - LONG_QUERY_RUN_BEYOND_TIMEOUT = 'longQueryRunBeyondTimeout', } export interface SearchUsageCollector { trackQueryTimedOut: () => Promise; trackQueriesCancelled: () => Promise; - trackLongQueryPopupShown: () => Promise; - trackLongQueryDialogDismissed: () => Promise; - trackLongQueryRunBeyondTimeout: () => Promise; } diff --git a/src/plugins/data/public/search/fetch/get_search_params.test.ts b/src/plugins/data/public/search/fetch/get_search_params.test.ts index 1ecb879b1602d..5e83e1f57bb6d 100644 --- a/src/plugins/data/public/search/fetch/get_search_params.test.ts +++ b/src/plugins/data/public/search/fetch/get_search_params.test.ts @@ -25,44 +25,12 @@ function getConfigStub(config: any = {}): GetConfigFn { } describe('getSearchParams', () => { - test('includes rest_total_hits_as_int', () => { - const config = getConfigStub(); + test('includes custom preference', () => { + const config = getConfigStub({ + [UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE]: 'custom', + [UI_SETTINGS.COURIER_CUSTOM_REQUEST_PREFERENCE]: 'aaa', + }); const searchParams = getSearchParams(config); - expect(searchParams.rest_total_hits_as_int).toBe(true); - }); - - test('includes ignore_unavailable', () => { - const config = getConfigStub(); - const searchParams = getSearchParams(config); - expect(searchParams.ignore_unavailable).toBe(true); - }); - - test('includes ignore_throttled according to search:includeFrozen', () => { - let config = getConfigStub({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: true }); - let searchParams = getSearchParams(config); - expect(searchParams.ignore_throttled).toBe(false); - - config = getConfigStub({ [UI_SETTINGS.SEARCH_INCLUDE_FROZEN]: false }); - searchParams = getSearchParams(config); - expect(searchParams.ignore_throttled).toBe(true); - }); - - test('includes max_concurrent_shard_requests according to courier:maxConcurrentShardRequests', () => { - let config = getConfigStub({ [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 0 }); - let searchParams = getSearchParams(config); - expect(searchParams.max_concurrent_shard_requests).toBe(undefined); - - config = getConfigStub({ [UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS]: 5 }); - searchParams = getSearchParams(config); - expect(searchParams.max_concurrent_shard_requests).toBe(5); - }); - - test('includes timeout according to esShardTimeout if greater than 0', () => { - const config = getConfigStub(); - let searchParams = getSearchParams(config, 0); - expect(searchParams.timeout).toBe(undefined); - - searchParams = getSearchParams(config, 100); - expect(searchParams.timeout).toBe('100ms'); + expect(searchParams.preference).toBe('aaa'); }); }); diff --git a/src/plugins/data/public/search/fetch/get_search_params.ts b/src/plugins/data/public/search/fetch/get_search_params.ts index 5e0395189f647..ed87c4813951c 100644 --- a/src/plugins/data/public/search/fetch/get_search_params.ts +++ b/src/plugins/data/public/search/fetch/get_search_params.ts @@ -22,26 +22,12 @@ import { SearchRequest } from './types'; const sessionId = Date.now(); -export function getSearchParams(getConfig: GetConfigFn, esShardTimeout: number = 0) { +export function getSearchParams(getConfig: GetConfigFn) { return { - rest_total_hits_as_int: true, - ignore_unavailable: true, - ignore_throttled: getIgnoreThrottled(getConfig), - max_concurrent_shard_requests: getMaxConcurrentShardRequests(getConfig), preference: getPreference(getConfig), - timeout: getTimeout(esShardTimeout), }; } -export function getIgnoreThrottled(getConfig: GetConfigFn) { - return !getConfig(UI_SETTINGS.SEARCH_INCLUDE_FROZEN); -} - -export function getMaxConcurrentShardRequests(getConfig: GetConfigFn) { - const maxConcurrentShardRequests = getConfig(UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS); - return maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined; -} - export function getPreference(getConfig: GetConfigFn) { const setRequestPreference = getConfig(UI_SETTINGS.COURIER_SET_REQUEST_PREFERENCE); if (setRequestPreference === 'sessionId') return sessionId; @@ -50,19 +36,15 @@ export function getPreference(getConfig: GetConfigFn) { : undefined; } -export function getTimeout(esShardTimeout: number) { - return esShardTimeout > 0 ? `${esShardTimeout}ms` : undefined; -} - /** @public */ // TODO: Could provide this on runtime contract with dependencies // already wired up. export function getSearchParamsFromRequest( searchRequest: SearchRequest, - dependencies: { esShardTimeout: number; getConfig: GetConfigFn } + dependencies: { getConfig: GetConfigFn } ): ISearchRequestParams { - const { esShardTimeout, getConfig } = dependencies; - const searchParams = getSearchParams(getConfig, esShardTimeout); + const { getConfig } = dependencies; + const searchParams = getSearchParams(getConfig); return { index: searchRequest.index.title || searchRequest.index, diff --git a/src/plugins/data/public/search/fetch/index.ts b/src/plugins/data/public/search/fetch/index.ts index 79cdad1897f9c..4b8511edfc26f 100644 --- a/src/plugins/data/public/search/fetch/index.ts +++ b/src/plugins/data/public/search/fetch/index.ts @@ -18,14 +18,7 @@ */ export * from './types'; -export { - getSearchParams, - getSearchParamsFromRequest, - getPreference, - getTimeout, - getIgnoreThrottled, - getMaxConcurrentShardRequests, -} from './get_search_params'; +export { getSearchParams, getSearchParamsFromRequest, getPreference } from './get_search_params'; export { RequestFailure } from './request_error'; export { handleResponse } from './handle_response'; diff --git a/src/plugins/data/public/search/index.ts b/src/plugins/data/public/search/index.ts index a6a1736ac91da..c1af9699acbb2 100644 --- a/src/plugins/data/public/search/index.ts +++ b/src/plugins/data/public/search/index.ts @@ -19,7 +19,14 @@ export * from './expressions'; -export { ISearch, ISearchGeneric, ISearchSetup, ISearchStart, SearchEnhancements } from './types'; +export { + ISearch, + ISearchGeneric, + ISearchSetup, + ISearchStart, + ISearchStartSearchSource, + SearchEnhancements, +} from './types'; export { IEsSearchResponse, IEsSearchRequest, ES_SEARCH_STRATEGY } from '../../common/search'; diff --git a/src/plugins/data/public/search/legacy/call_client.test.ts b/src/plugins/data/public/search/legacy/call_client.test.ts index 38f3ab200da90..943a02d22088d 100644 --- a/src/plugins/data/public/search/legacy/call_client.test.ts +++ b/src/plugins/data/public/search/legacy/call_client.test.ts @@ -60,7 +60,6 @@ describe('callClient', () => { http: coreMock.createStart().http, legacySearchService: {}, config: { get: jest.fn() }, - esShardTimeout: 0, loadingCount$: new BehaviorSubject(0), } as FetchHandlers; diff --git a/src/plugins/data/public/search/long_query_notification.tsx b/src/plugins/data/public/search/long_query_notification.tsx deleted file mode 100644 index 1db298618fae8..0000000000000 --- a/src/plugins/data/public/search/long_query_notification.tsx +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; -import { ApplicationStart } from 'kibana/public'; -import { toMountPoint } from '../../../kibana_react/public'; - -interface Props { - application: ApplicationStart; -} - -export function getLongQueryNotification(props: Props) { - return toMountPoint(); -} - -export function LongQueryNotification(props: Props) { - return ( -
- - - - - { - await props.application.navigateToApp('management/stack/license_management'); - }} - > - - - - -
- ); -} diff --git a/src/plugins/data/public/search/search_interceptor.test.ts b/src/plugins/data/public/search/search_interceptor.test.ts index da60f39b522ac..7bfa6f0ab1bc5 100644 --- a/src/plugins/data/public/search/search_interceptor.test.ts +++ b/src/plugins/data/public/search/search_interceptor.test.ts @@ -32,15 +32,12 @@ jest.useFakeTimers(); describe('SearchInterceptor', () => { beforeEach(() => { mockCoreSetup = coreMock.createSetup(); - searchInterceptor = new SearchInterceptor( - { - toasts: mockCoreSetup.notifications.toasts, - startServices: mockCoreSetup.getStartServices(), - uiSettings: mockCoreSetup.uiSettings, - http: mockCoreSetup.http, - }, - 1000 - ); + searchInterceptor = new SearchInterceptor({ + toasts: mockCoreSetup.notifications.toasts, + startServices: mockCoreSetup.getStartServices(), + uiSettings: mockCoreSetup.uiSettings, + http: mockCoreSetup.http, + }); }); describe('search', () => { @@ -98,6 +95,39 @@ describe('SearchInterceptor', () => { await flushPromises(); }); + test('Should not timeout if requestTimeout is undefined', async () => { + searchInterceptor = new SearchInterceptor({ + startServices: mockCoreSetup.getStartServices(), + uiSettings: mockCoreSetup.uiSettings, + http: mockCoreSetup.http, + toasts: mockCoreSetup.notifications.toasts, + }); + mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { + return new Promise((resolve, reject) => { + options.signal.addEventListener('abort', () => { + reject(new AbortError()); + }); + + setTimeout(resolve, 5000); + }); + }); + const mockRequest: IEsSearchRequest = { + params: {}, + }; + const response = searchInterceptor.search(mockRequest); + + expect.assertions(1); + const next = jest.fn(); + const complete = () => { + expect(next).toBeCalled(); + }; + response.subscribe({ next, complete }); + + jest.advanceTimersByTime(5000); + + await flushPromises(); + }); + test('Observable should fail if user aborts (test merged signal)', async () => { const abortController = new AbortController(); mockCoreSetup.http.fetch.mockImplementationOnce((options: any) => { @@ -128,7 +158,7 @@ describe('SearchInterceptor', () => { await flushPromises(); }); - test('Immediatelly aborts if passed an aborted abort signal', async (done) => { + test('Immediately aborts if passed an aborted abort signal', async (done) => { const abort = new AbortController(); const mockRequest: IEsSearchRequest = { params: {}, @@ -144,44 +174,4 @@ describe('SearchInterceptor', () => { response.subscribe({ error }); }); }); - - describe('getPendingCount$', () => { - test('should observe the number of pending requests', () => { - const pendingCount$ = searchInterceptor.getPendingCount$(); - const pendingNext = jest.fn(); - pendingCount$.subscribe(pendingNext); - - const mockResponse: any = { result: 200 }; - mockCoreSetup.http.fetch.mockResolvedValue(mockResponse); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest); - - response.subscribe({ - complete: () => { - expect(pendingNext.mock.calls).toEqual([[0], [1], [0]]); - }, - }); - }); - - test('should observe the number of pending requests on error', () => { - const pendingCount$ = searchInterceptor.getPendingCount$(); - const pendingNext = jest.fn(); - pendingCount$.subscribe(pendingNext); - - const mockResponse: any = { result: 500 }; - mockCoreSetup.http.fetch.mockRejectedValue(mockResponse); - const mockRequest: IEsSearchRequest = { - params: {}, - }; - const response = searchInterceptor.search(mockRequest); - - response.subscribe({ - complete: () => { - expect(pendingNext.mock.calls).toEqual([[0], [1], [0]]); - }, - }); - }); - }); }); diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts index c6c03267163c9..888e12a4285b1 100644 --- a/src/plugins/data/public/search/search_interceptor.ts +++ b/src/plugins/data/public/search/search_interceptor.ts @@ -17,27 +17,35 @@ * under the License. */ -import { trimEnd } from 'lodash'; -import { BehaviorSubject, throwError, timer, Subscription, defer, from, Observable } from 'rxjs'; -import { finalize, filter } from 'rxjs/operators'; -import { Toast, CoreStart, ToastsSetup, CoreSetup } from 'kibana/public'; -import { getCombinedSignal, AbortError } from '../../common/utils'; +import { trimEnd, debounce } from 'lodash'; import { + BehaviorSubject, + throwError, + timer, + Subscription, + defer, + from, + Observable, + NEVER, +} from 'rxjs'; +import { catchError, finalize } from 'rxjs/operators'; +import { CoreStart, CoreSetup, ToastsSetup } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { + getCombinedSignal, + AbortError, IEsSearchRequest, IEsSearchResponse, ISearchOptions, ES_SEARCH_STRATEGY, -} from '../../common/search'; -import { getLongQueryNotification } from './long_query_notification'; +} from '../../common'; import { SearchUsageCollector } from './collectors'; -const LONG_QUERY_NOTIFICATION_DELAY = 10000; - export interface SearchInterceptorDeps { - toasts: ToastsSetup; http: CoreSetup['http']; uiSettings: CoreSetup['uiSettings']; startServices: Promise<[CoreStart, any, unknown]>; + toasts: ToastsSetup; usageCollector?: SearchUsageCollector; } @@ -60,49 +68,25 @@ export class SearchInterceptor { */ protected timeoutSubscriptions: Subscription = new Subscription(); - /** - * The current long-running toast (if there is one). - * @internal - */ - protected longRunningToast?: Toast; - /** * @internal */ protected application!: CoreStart['application']; - /** - * This class should be instantiated with a `requestTimeout` corresponding with how many ms after - * requests are initiated that they should automatically cancel. - * @param toasts The `core.notifications.toasts` service - * @param application The `core.application` service - * @param requestTimeout Usually config value `elasticsearch.requestTimeout` + /* + * @internal */ - constructor( - protected readonly deps: SearchInterceptorDeps, - protected readonly requestTimeout?: number - ) { + constructor(protected readonly deps: SearchInterceptorDeps) { this.deps.http.addLoadingCountSource(this.pendingCount$); this.deps.startServices.then(([coreStart]) => { this.application = coreStart.application; }); - - // When search requests go out, a notification is scheduled allowing users to continue the - // request past the timeout. When all search requests complete, we remove the notification. - this.getPendingCount$() - .pipe(filter((count) => count === 0)) - .subscribe(this.hideToast); } /** - * Returns an `Observable` over the current number of pending searches. This could mean that one - * of the search requests is still in flight, or that it has only received partial responses. + * @internal */ - public getPendingCount$() { - return this.pendingCount$.asObservable(); - } - protected runSearch( request: IEsSearchRequest, signal: AbortSignal, @@ -136,10 +120,18 @@ export class SearchInterceptor { return throwError(new AbortError()); } - const { combinedSignal, cleanup } = this.setupTimers(options); + const { combinedSignal, cleanup } = this.setupAbortSignal({ + abortSignal: options?.abortSignal, + }); this.pendingCount$.next(this.pendingCount$.getValue() + 1); return this.runSearch(request, combinedSignal, options?.strategy).pipe( + catchError((e: any) => { + if (e.body?.attributes?.error === 'Request timed out') { + this.showTimeoutError(e); + } + return throwError(e); + }), finalize(() => { this.pendingCount$.next(this.pendingCount$.getValue() - 1); cleanup(); @@ -148,19 +140,26 @@ export class SearchInterceptor { }); } - protected setupTimers(options?: ISearchOptions) { + /** + * @internal + */ + protected setupAbortSignal({ + abortSignal, + timeout, + }: { + abortSignal?: AbortSignal; + timeout?: number; + }) { // Schedule this request to automatically timeout after some interval const timeoutController = new AbortController(); const { signal: timeoutSignal } = timeoutController; - const timeout$ = timer(this.requestTimeout); + const timeout$ = timeout ? timer(timeout) : NEVER; const subscription = timeout$.subscribe(() => { timeoutController.abort(); + this.showTimeoutError(new AbortError()); }); this.timeoutSubscriptions.add(subscription); - // Schedule the notification to allow users to cancel or wait beyond the timeout - const notificationSubscription = timer(LONG_QUERY_NOTIFICATION_DELAY).subscribe(this.showToast); - // Get a combined `AbortSignal` that will be aborted whenever the first of the following occurs: // 1. The user manually aborts (via `cancelPending`) // 2. The request times out @@ -168,13 +167,12 @@ export class SearchInterceptor { const signals = [ this.abortController.signal, timeoutSignal, - ...(options?.abortSignal ? [options.abortSignal] : []), + ...(abortSignal ? [abortSignal] : []), ]; const combinedSignal = getCombinedSignal(signals); const cleanup = () => { this.timeoutSubscriptions.remove(subscription); - notificationSubscription.unsubscribe(); }; combinedSignal.addEventListener('abort', cleanup); @@ -185,36 +183,23 @@ export class SearchInterceptor { }; } - /** - * @internal - */ - protected showToast = () => { - if (this.longRunningToast) return; - this.longRunningToast = this.deps.toasts.addInfo( - { - title: 'Your query is taking a while', - text: getLongQueryNotification({ - application: this.application, + // Right now we are debouncing but we will hook this up with background sessions to show only one + // error notification per session. + protected showTimeoutError = debounce( + (e: Error) => { + this.deps.toasts.addError(e, { + title: 'Timed out', + toastMessage: i18n.translate('data.search.upgradeLicense', { + defaultMessage: + 'One or more queries timed out. With our free Basic tier, your queries never time out.', }), - }, - { - toastLifeTimeMs: 1000000, - } - ); - }; - - /** - * @internal - */ - protected hideToast = () => { - if (this.longRunningToast) { - this.deps.toasts.remove(this.longRunningToast); - delete this.longRunningToast; - if (this.deps.usageCollector) { - this.deps.usageCollector.trackLongQueryDialogDismissed(); - } + }); + }, + 60000, + { + leading: true, } - }; + ); } export type ISearchInterceptor = PublicMethodsOf; diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts index a49d2ef0956ff..6b73761c5a437 100644 --- a/src/plugins/data/public/search/search_service.ts +++ b/src/plugins/data/public/search/search_service.ts @@ -52,26 +52,19 @@ export class SearchService implements Plugin { { http, getStartServices, injectedMetadata, notifications, uiSettings }: CoreSetup, { expressions, usageCollection }: SearchServiceSetupDependencies ): ISearchSetup { - const esRequestTimeout = injectedMetadata.getInjectedVar('esRequestTimeout') as number; - this.usageCollector = createUsageCollector(getStartServices, usageCollection); /** * A global object that intercepts all searches and provides convenience methods for cancelling * all pending search requests, as well as getting the number of pending search requests. - * TODO: Make this modular so that apps can opt in/out of search collection, or even provide - * their own search collector instances */ - this.searchInterceptor = new SearchInterceptor( - { - toasts: notifications.toasts, - http, - uiSettings, - startServices: getStartServices(), - usageCollector: this.usageCollector!, - }, - esRequestTimeout - ); + this.searchInterceptor = new SearchInterceptor({ + toasts: notifications.toasts, + http, + uiSettings, + startServices: getStartServices(), + usageCollector: this.usageCollector!, + }); expressions.registerFunction(esdsl); expressions.registerType(esRawResponse); @@ -101,8 +94,6 @@ export class SearchService implements Plugin { const searchSourceDependencies: SearchSourceDependencies = { getConfig: uiSettings.get.bind(uiSettings), - // TODO: we don't need this, apply on the server - esShardTimeout: injectedMetadata.getInjectedVar('esShardTimeout') as number, search, http, loadingCount$, @@ -112,7 +103,13 @@ export class SearchService implements Plugin { aggs: this.aggsService.start({ fieldFormats, uiSettings }), search, searchSource: { + /** + * creates searchsource based on serialized search source fields + */ create: createSearchSource(indexPatterns, searchSourceDependencies), + /** + * creates an enpty search source + */ createEmpty: () => { return new SearchSource({}, searchSourceDependencies); }, diff --git a/src/plugins/data/public/search/search_source/create_search_source.test.ts b/src/plugins/data/public/search/search_source/create_search_source.test.ts index 2820aab67ea3a..bc1c7c06c8806 100644 --- a/src/plugins/data/public/search/search_source/create_search_source.test.ts +++ b/src/plugins/data/public/search/search_source/create_search_source.test.ts @@ -35,7 +35,6 @@ describe('createSearchSource', () => { dependencies = { getConfig: jest.fn(), search: jest.fn(), - esShardTimeout: 30000, http: coreMock.createStart().http, loadingCount$: new BehaviorSubject(0), }; diff --git a/src/plugins/data/public/search/search_source/create_search_source.ts b/src/plugins/data/public/search/search_source/create_search_source.ts index 4c44f4d62d469..242fbd73fe42b 100644 --- a/src/plugins/data/public/search/search_source/create_search_source.ts +++ b/src/plugins/data/public/search/search_source/create_search_source.ts @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -import { migrateLegacyQuery } from '../../../../kibana_legacy/common'; +import { migrateLegacyQuery } from './migrate_legacy_query'; import { SearchSource, SearchSourceDependencies } from './search_source'; import { IndexPatternsContract } from '../../index_patterns/index_patterns'; import { SearchSourceFields } from './types'; diff --git a/src/plugins/data/public/search/search_source/migrate_legacy_query.ts b/src/plugins/data/public/search/search_source/migrate_legacy_query.ts new file mode 100644 index 0000000000000..8d9b50d5a66b2 --- /dev/null +++ b/src/plugins/data/public/search/search_source/migrate_legacy_query.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { has } from 'lodash'; +import { Query } from 'src/plugins/data/public'; + +/** + * Creates a standardized query object from old queries that were either strings or pure ES query DSL + * + * @param query - a legacy query, what used to be stored in SearchSource's query property + * @return Object + */ + +export function migrateLegacyQuery(query: Query | { [key: string]: any } | string): Query { + // Lucene was the only option before, so language-less queries are all lucene + if (!has(query, 'language')) { + return { query, language: 'lucene' }; + } + + return query as Query; +} diff --git a/src/plugins/data/public/search/search_source/mocks.ts b/src/plugins/data/public/search/search_source/mocks.ts index bc3e287d9fe80..adf53bee33fe1 100644 --- a/src/plugins/data/public/search/search_source/mocks.ts +++ b/src/plugins/data/public/search/search_source/mocks.ts @@ -53,7 +53,6 @@ export const searchSourceMock = { export const createSearchSourceMock = (fields?: SearchSourceFields) => new SearchSource(fields, { getConfig: uiSettingsServiceMock.createStartContract().get, - esShardTimeout: 30000, search: jest.fn(), http: httpServiceMock.createStartContract(), loadingCount$: new BehaviorSubject(0), diff --git a/src/plugins/data/public/search/search_source/search_source.test.ts b/src/plugins/data/public/search/search_source/search_source.test.ts index a8baed9faa84d..282a33e6d01f7 100644 --- a/src/plugins/data/public/search/search_source/search_source.test.ts +++ b/src/plugins/data/public/search/search_source/search_source.test.ts @@ -68,7 +68,6 @@ describe('SearchSource', () => { searchSourceDependencies = { getConfig: jest.fn(), search: mockSearchMethod, - esShardTimeout: 30000, http: coreMock.createStart().http, loadingCount$: new BehaviorSubject(0), }; diff --git a/src/plugins/data/public/search/search_source/search_source.ts b/src/plugins/data/public/search/search_source/search_source.ts index eec2d9b50eafe..a39898e6a9f52 100644 --- a/src/plugins/data/public/search/search_source/search_source.ts +++ b/src/plugins/data/public/search/search_source/search_source.ts @@ -118,7 +118,6 @@ export interface SearchSourceDependencies { getConfig: GetConfigFn; search: ISearchGeneric; http: HttpStart; - esShardTimeout: number; loadingCount$: BehaviorSubject; } @@ -144,15 +143,19 @@ export class SearchSource { * PUBLIC API *****/ + /** + * internal, dont use + * @param searchStrategyId + */ setPreferredSearchStrategyId(searchStrategyId: string) { this.searchStrategyId = searchStrategyId; } - setFields(newFields: SearchSourceFields) { - this.fields = newFields; - return this; - } - + /** + * sets value to a single search source feild + * @param field: field name + * @param value: value for the field + */ setField(field: K, value: SearchSourceFields[K]) { if (value == null) { delete this.fields[field]; @@ -162,16 +165,33 @@ export class SearchSource { return this; } + /** + * Internal, do not use. Overrides all search source fields with the new field array. + * + * @private + * @param newFields New field array. + */ + setFields(newFields: SearchSourceFields) { + this.fields = newFields; + return this; + } + + /** + * returns search source id + */ getId() { return this.id; } + /** + * returns all search source fields + */ getFields() { return { ...this.fields }; } /** - * Get fields from the fields + * Gets a single field from the fields */ getField(field: K, recurse = true): SearchSourceFields[K] { if (!recurse || this.fields[field] !== void 0) { @@ -188,10 +208,16 @@ export class SearchSource { return this.getField(field, false); } + /** + * @deprecated Don't use. + */ create() { return new SearchSource({}, this.dependencies); } + /** + * creates a copy of this search source (without its children) + */ createCopy() { const newSearchSource = new SearchSource({}, this.dependencies); newSearchSource.setFields({ ...this.fields }); @@ -202,6 +228,10 @@ export class SearchSource { return newSearchSource; } + /** + * creates a new child search source + * @param options + */ createChild(options = {}) { const childSearchSource = new SearchSource({}, this.dependencies); childSearchSource.setParent(this, options); @@ -228,43 +258,6 @@ export class SearchSource { return this.parent; } - /** - * Run a search using the search service - * @return {Observable>} - */ - private fetch$(searchRequest: SearchRequest, options: ISearchOptions) { - const { search, esShardTimeout, getConfig } = this.dependencies; - - const params = getSearchParamsFromRequest(searchRequest, { - esShardTimeout, - getConfig, - }); - - return search({ params, indexType: searchRequest.indexType }, options).pipe( - map(({ rawResponse }) => handleResponse(searchRequest, rawResponse)) - ); - } - - /** - * Run a search using the search service - * @return {Promise>} - */ - private async legacyFetch(searchRequest: SearchRequest, options: ISearchOptions) { - const { http, getConfig, loadingCount$ } = this.dependencies; - - return await fetchSoon( - searchRequest, - { - ...(this.searchStrategyId && { searchStrategyId: this.searchStrategyId }), - ...options, - }, - { - http, - config: { get: getConfig }, - loadingCount$, - } - ); - } /** * Fetch this source and reject the returned Promise on error * @@ -303,6 +296,9 @@ export class SearchSource { this.requestStartHandlers.push(handler); } + /** + * Returns body contents of the search request, often referred as query DSL. + */ async getSearchRequestBody() { const searchRequest = await this.flatten(); return searchRequest.body; @@ -320,6 +316,43 @@ export class SearchSource { * PRIVATE APIS ******/ + /** + * Run a search using the search service + * @return {Observable>} + */ + private fetch$(searchRequest: SearchRequest, options: ISearchOptions) { + const { search, getConfig } = this.dependencies; + + const params = getSearchParamsFromRequest(searchRequest, { + getConfig, + }); + + return search({ params, indexType: searchRequest.indexType }, options).pipe( + map(({ rawResponse }) => handleResponse(searchRequest, rawResponse)) + ); + } + + /** + * Run a search using the search service + * @return {Promise>} + */ + private async legacyFetch(searchRequest: SearchRequest, options: ISearchOptions) { + const { http, getConfig, loadingCount$ } = this.dependencies; + + return await fetchSoon( + searchRequest, + { + ...(this.searchStrategyId && { searchStrategyId: this.searchStrategyId }), + ...options, + }, + { + http, + config: { get: getConfig }, + loadingCount$, + } + ); + } + /** * Called by requests of this search source when they are started * @param options @@ -482,6 +515,9 @@ export class SearchSource { return searchRequest; } + /** + * serializes search source fields (which can later be passed to {@link ISearchStartSearchSource}) + */ public getSerializedFields() { const { filter: originalFilters, ...searchSourceFields } = omit(this.getFields(), [ 'sort', @@ -533,5 +569,8 @@ export class SearchSource { } } -/** @public **/ +/** + * search source interface + * @public + */ export type ISearchSource = Pick; diff --git a/src/plugins/data/public/search/search_source/types.ts b/src/plugins/data/public/search/search_source/types.ts index c2f8701a64fa3..0882aa9a2ceec 100644 --- a/src/plugins/data/public/search/search_source/types.ts +++ b/src/plugins/data/public/search/search_source/types.ts @@ -34,19 +34,37 @@ export interface SortDirectionNumeric { export type EsQuerySortValue = Record; +/** + * search source fields + */ export interface SearchSourceFields { type?: string; + /** + * {@link Query} + */ query?: Query; + /** + * {@link Filter} + */ filter?: Filter[] | Filter | (() => Filter[] | Filter | undefined); + /** + * {@link EsQuerySortValue} + */ sort?: EsQuerySortValue | EsQuerySortValue[]; highlight?: any; highlightAll?: boolean; + /** + * {@link AggConfigs} + */ aggs?: any; from?: number; size?: number; source?: NameList; version?: boolean; fields?: NameList; + /** + * {@link IndexPatternService} + */ index?: IndexPattern; searchAfter?: EsQuerySearchAfter; timeout?: string; diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts index cec5c63294e96..83a542269046f 100644 --- a/src/plugins/data/public/search/types.ts +++ b/src/plugins/data/public/search/types.ts @@ -62,13 +62,42 @@ export interface ISearchSetup { __enhance: (enhancements: SearchEnhancements) => void; } +/** + * high level search service + * @public + */ +export interface ISearchStartSearchSource { + /** + * creates {@link SearchSource} based on provided serialized {@link SearchSourceFields} + * @param fields + */ + create: (fields?: SearchSourceFields) => Promise; + /** + * creates empty {@link SearchSource} + */ + createEmpty: () => ISearchSource; +} +/** + * search service + * @public + */ export interface ISearchStart { + /** + * agg config sub service + * {@link AggsStart} + * + */ aggs: AggsStart; + /** + * low level search + * {@link ISearchGeneric} + */ search: ISearchGeneric; - searchSource: { - create: (fields?: SearchSourceFields) => Promise; - createEmpty: () => ISearchSource; - }; + /** + * high level search + * {@link ISearchStartSearchSource} + */ + searchSource: ISearchStartSearchSource; } export { SEARCH_EVENT_TYPE } from './collectors'; diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts index bffc10642eb47..7b5d79aff24ef 100644 --- a/src/plugins/data/public/types.ts +++ b/src/plugins/data/public/types.ts @@ -46,6 +46,9 @@ export interface DataStartDependencies { uiActions: UiActionsStart; } +/** + * Data plugin public Setup contract + */ export interface DataPublicPluginSetup { autocomplete: AutocompleteSetup; search: ISearchSetup; @@ -57,20 +60,61 @@ export interface DataPublicPluginSetup { __enhance: (enhancements: DataPublicPluginEnhancements) => void; } +/** + * Data plugin prewired UI components + */ +export interface DataPublicPluginStartUi { + IndexPatternSelect: React.ComponentType; + SearchBar: React.ComponentType; +} + +/** + * utilities to generate filters from action context + */ +export interface DataPublicPluginStartActions { + createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction; + createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction; +} + +/** + * Data plugin public Start contract + */ export interface DataPublicPluginStart { - actions: { - createFiltersFromValueClickAction: typeof createFiltersFromValueClickAction; - createFiltersFromRangeSelectAction: typeof createFiltersFromRangeSelectAction; - }; + /** + * filter creation utilities + * {@link DataPublicPluginStartActions} + */ + actions: DataPublicPluginStartActions; + /** + * autocomplete service + * {@link AutocompleteStart} + */ autocomplete: AutocompleteStart; + /** + * index patterns service + * {@link IndexPatternsContract} + */ indexPatterns: IndexPatternsContract; + /** + * search service + * {@link ISearchStart} + */ search: ISearchStart; + /** + * field formats service + * {@link FieldFormatsStart} + */ fieldFormats: FieldFormatsStart; + /** + * query service + * {@link QueryStart} + */ query: QueryStart; - ui: { - IndexPatternSelect: React.ComponentType; - SearchBar: React.ComponentType; - }; + /** + * prewired UI components + * {@link DataPublicPluginStartUi} + */ + ui: DataPublicPluginStartUi; } export interface IDataPluginServices extends Partial { diff --git a/src/plugins/data/public/ui/query_string_input/_query_bar.scss b/src/plugins/data/public/ui/query_string_input/_query_bar.scss index 00895ec49003b..1ff24c61954e7 100644 --- a/src/plugins/data/public/ui/query_string_input/_query_bar.scss +++ b/src/plugins/data/public/ui/query_string_input/_query_bar.scss @@ -8,30 +8,37 @@ border-right: none !important; } +.kbnQueryBar__textareaWrap { + overflow: visible !important; // Override EUI form control + display: flex; + flex: 1 1 100%; + position: relative; +} + .kbnQueryBar__textarea { z-index: $euiZContentMenu; resize: none !important; // When in the group, it will autosize - height: $euiSizeXXL; + height: $euiFormControlHeight; // Unlike most inputs within layout control groups, the text area still needs a border. // These adjusts help it sit above the control groups shadow to line up correctly. - padding-top: $euiSizeS + 3px !important; - transform: translateY(-2px); - padding: $euiSizeS - 1px; + padding: $euiSizeS; + padding-top: $euiSizeS + 3px; + transform: translateY(-1px) translateX(-1px); - &:not(:focus) { + &:not(:focus):not(:invalid) { @include euiYScrollWithShadows; + } + + &:not(:focus) { white-space: nowrap; overflow-y: hidden; overflow-x: hidden; - border: none; - box-shadow: none; } // When focused, let it scroll &:focus { overflow-x: auto; overflow-y: auto; - width: calc(100% + 1px); // To overtake the group's fake border white-space: normal; } } diff --git a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx index 0bfac2a07a7eb..f159cac664a9e 100644 --- a/src/plugins/data/public/ui/query_string_input/query_string_input.tsx +++ b/src/plugins/data/public/ui/query_string_input/query_string_input.tsx @@ -19,6 +19,7 @@ import React, { Component, RefObject, createRef } from 'react'; import { i18n } from '@kbn/i18n'; + import classNames from 'classnames'; import { EuiTextArea, @@ -63,6 +64,7 @@ interface Props { dataTestSubj?: string; size?: SuggestionsListSize; className?: string; + isInvalid?: boolean; } interface State { @@ -591,6 +593,7 @@ export class QueryStringInputUI extends Component { 'euiFormControlLayout euiFormControlLayout--group kbnQueryBar__wrap', this.props.className ); + return (
{this.props.prepend} @@ -607,7 +610,7 @@ export class QueryStringInputUI extends Component { >
{ } role="textbox" data-test-subj={this.props.dataTestSubj || 'queryInput'} + isInvalid={this.props.isInvalid} > {this.getQueryString()} diff --git a/src/plugins/data/public/ui/typeahead/constants.ts b/src/plugins/data/public/ui/typeahead/constants.ts index 08f9bd23e16f3..0e28891a14535 100644 --- a/src/plugins/data/public/ui/typeahead/constants.ts +++ b/src/plugins/data/public/ui/typeahead/constants.ts @@ -33,4 +33,4 @@ export const SUGGESTIONS_LIST_REQUIRED_BOTTOM_SPACE = 250; * A distance in px to display suggestions list right under the query input without a gap * @public */ -export const SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET = 2; +export const SUGGESTIONS_LIST_REQUIRED_TOP_OFFSET = 1; diff --git a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx index dc7c55374f1d5..50ed9e9542d36 100644 --- a/src/plugins/data/public/ui/typeahead/suggestions_component.tsx +++ b/src/plugins/data/public/ui/typeahead/suggestions_component.tsx @@ -154,6 +154,7 @@ export class SuggestionsComponent extends Component { const StyledSuggestionsListDiv = styled.div` ${(props: { queryBarRect: DOMRect; verticalListPosition: string }) => ` position: absolute; + z-index: 4001; left: ${props.queryBarRect.left}px; width: ${props.queryBarRect.width}px; ${props.verticalListPosition}`} diff --git a/src/plugins/data/server/index.ts b/src/plugins/data/server/index.ts index 71ed83290e697..03baff4910309 100644 --- a/src/plugins/data/server/index.ts +++ b/src/plugins/data/server/index.ts @@ -212,8 +212,11 @@ export { ISearchStrategy, ISearchSetup, ISearchStart, + toSnakeCase, getDefaultSearchParams, + getShardTimeout, getTotalLoaded, + shimHitsTotal, usageProvider, SearchUsage, } from './search'; diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts index c34c3a310814c..504ce728481f0 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.test.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.test.ts @@ -36,7 +36,14 @@ describe('ES search strategy', () => { }, }); const mockContext = { - core: { elasticsearch: { client: { asCurrentUser: { search: mockApiCaller } } } }, + core: { + uiSettings: { + client: { + get: () => {}, + }, + }, + elasticsearch: { client: { asCurrentUser: { search: mockApiCaller } } }, + }, }; const mockConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; @@ -59,14 +66,13 @@ describe('ES search strategy', () => { expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toEqual({ ...params, - timeout: '0ms', - ignoreUnavailable: true, - restTotalHitsAsInt: true, + ignore_unavailable: true, + track_total_hits: true, }); }); it('calls the API caller with overridden defaults', async () => { - const params = { index: 'logstash-*', ignoreUnavailable: false, timeout: '1000ms' }; + const params = { index: 'logstash-*', ignore_unavailable: false, timeout: '1000ms' }; const esSearch = await esSearchStrategyProvider(mockConfig$, mockLogger); await esSearch.search((mockContext as unknown) as RequestHandlerContext, { params }); @@ -74,7 +80,7 @@ describe('ES search strategy', () => { expect(mockApiCaller).toBeCalled(); expect(mockApiCaller.mock.calls[0][0]).toEqual({ ...params, - restTotalHitsAsInt: true, + track_total_hits: true, }); }); diff --git a/src/plugins/data/server/search/es_search/es_search_strategy.ts b/src/plugins/data/server/search/es_search/es_search_strategy.ts index eabbf3e3e2600..e2ed500689cfa 100644 --- a/src/plugins/data/server/search/es_search/es_search_strategy.ts +++ b/src/plugins/data/server/search/es_search/es_search_strategy.ts @@ -22,7 +22,8 @@ import { SearchResponse } from 'elasticsearch'; import { Observable } from 'rxjs'; import { ApiResponse } from '@elastic/elasticsearch'; import { SearchUsage } from '../collectors/usage'; -import { ISearchStrategy, getDefaultSearchParams, getTotalLoaded } from '..'; +import { toSnakeCase } from './to_snake_case'; +import { ISearchStrategy, getDefaultSearchParams, getTotalLoaded, getShardTimeout } from '..'; export const esSearchStrategyProvider = ( config$: Observable, @@ -33,7 +34,7 @@ export const esSearchStrategyProvider = ( search: async (context, request, options) => { logger.debug(`search ${request.params?.index}`); const config = await config$.pipe(first()).toPromise(); - const defaultParams = getDefaultSearchParams(config); + const uiSettingsClient = await context.core.uiSettings.client; // Only default index pattern type is supported here. // See data_enhanced for other type support. @@ -41,16 +42,21 @@ export const esSearchStrategyProvider = ( throw new Error(`Unsupported index pattern type ${request.indexType}`); } - const params = { + // ignoreThrottled is not supported in OSS + const { ignoreThrottled, ...defaultParams } = await getDefaultSearchParams(uiSettingsClient); + + const params = toSnakeCase({ ...defaultParams, + ...getShardTimeout(config), ...request.params, - }; + }); try { - const esResponse = (await context.core.elasticsearch.client.asCurrentUser.search( - params - )) as ApiResponse>; - const rawResponse = esResponse.body; + // Temporary workaround until https://github.com/elastic/elasticsearch-js/issues/1297 + const promise = context.core.elasticsearch.client.asCurrentUser.search(params); + if (options?.abortSignal) + options.abortSignal.addEventListener('abort', () => promise.abort()); + const { body: rawResponse } = (await promise) as ApiResponse>; if (usage) usage.trackSuccess(rawResponse.took); diff --git a/src/plugins/data/server/search/es_search/get_default_search_params.ts b/src/plugins/data/server/search/es_search/get_default_search_params.ts index b2341ccc0f3c8..13607fce51670 100644 --- a/src/plugins/data/server/search/es_search/get_default_search_params.ts +++ b/src/plugins/data/server/search/es_search/get_default_search_params.ts @@ -17,12 +17,28 @@ * under the License. */ -import { SharedGlobalConfig } from '../../../../../core/server'; +import { SharedGlobalConfig, IUiSettingsClient } from '../../../../../core/server'; +import { UI_SETTINGS } from '../../../common/constants'; -export function getDefaultSearchParams(config: SharedGlobalConfig) { +export function getShardTimeout(config: SharedGlobalConfig) { + const timeout = config.elasticsearch.shardTimeout.asMilliseconds(); + return timeout + ? { + timeout: `${timeout}ms`, + } + : {}; +} + +export async function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient) { + const ignoreThrottled = !(await uiSettingsClient.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN)); + const maxConcurrentShardRequests = await uiSettingsClient.get( + UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS + ); return { - timeout: `${config.elasticsearch.shardTimeout.asMilliseconds()}ms`, + maxConcurrentShardRequests: + maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined, + ignoreThrottled, ignoreUnavailable: true, // Don't fail if the index/indices don't exist - restTotalHitsAsInt: true, // Get the number of hits as an int rather than a range + trackTotalHits: true, }; } diff --git a/src/plugins/data/server/search/es_search/index.ts b/src/plugins/data/server/search/es_search/index.ts index 20006b70730d8..1bd17fc986168 100644 --- a/src/plugins/data/server/search/es_search/index.ts +++ b/src/plugins/data/server/search/es_search/index.ts @@ -17,7 +17,9 @@ * under the License. */ -export { ES_SEARCH_STRATEGY, IEsSearchRequest, IEsSearchResponse } from '../../../common/search'; export { esSearchStrategyProvider } from './es_search_strategy'; -export { getDefaultSearchParams } from './get_default_search_params'; +export * from './get_default_search_params'; export { getTotalLoaded } from './get_total_loaded'; +export * from './to_snake_case'; + +export { ES_SEARCH_STRATEGY, IEsSearchRequest, IEsSearchResponse } from '../../../common'; diff --git a/src/legacy/utils/path_contains.js b/src/plugins/data/server/search/es_search/to_snake_case.ts similarity index 83% rename from src/legacy/utils/path_contains.js rename to src/plugins/data/server/search/es_search/to_snake_case.ts index 60d05c1099554..74f156274cbc6 100644 --- a/src/legacy/utils/path_contains.js +++ b/src/plugins/data/server/search/es_search/to_snake_case.ts @@ -17,8 +17,8 @@ * under the License. */ -import { relative } from 'path'; +import { mapKeys, snakeCase } from 'lodash'; -export default function pathContains(root, child) { - return relative(child, root).slice(0, 2) !== '..'; +export function toSnakeCase(obj: Record) { + return mapKeys(obj, (value, key) => snakeCase(key)); } diff --git a/src/plugins/data/server/search/index.ts b/src/plugins/data/server/search/index.ts index 8a74c51f52f51..b671ed806510b 100644 --- a/src/plugins/data/server/search/index.ts +++ b/src/plugins/data/server/search/index.ts @@ -19,8 +19,10 @@ export { ISearchStrategy, ISearchSetup, ISearchStart, SearchEnhancements } from './types'; -export { getDefaultSearchParams, getTotalLoaded } from './es_search'; +export * from './es_search'; export { usageProvider, SearchUsage } from './collectors'; export * from './aggs'; + +export { shimHitsTotal } from './routes'; diff --git a/src/plugins/data/server/search/routes/index.ts b/src/plugins/data/server/search/routes/index.ts index 2217890ff778e..a290f08f9b843 100644 --- a/src/plugins/data/server/search/routes/index.ts +++ b/src/plugins/data/server/search/routes/index.ts @@ -19,3 +19,4 @@ export * from './msearch'; export * from './search'; +export * from './shim_hits_total'; diff --git a/src/plugins/data/server/search/routes/msearch.test.ts b/src/plugins/data/server/search/routes/msearch.test.ts index 0a52cf23c5472..3a7d67c31b8be 100644 --- a/src/plugins/data/server/search/routes/msearch.test.ts +++ b/src/plugins/data/server/search/routes/msearch.test.ts @@ -48,7 +48,7 @@ describe('msearch route', () => { }); it('handler calls /_msearch with the given request', async () => { - const response = { id: 'yay' }; + const response = { id: 'yay', body: { responses: [{ hits: { total: 5 } }] } }; const mockClient = { transport: { request: jest.fn().mockResolvedValue(response) } }; const mockContext = { core: { @@ -73,7 +73,7 @@ describe('msearch route', () => { expect(mockClient.transport.request.mock.calls[0][0].method).toBe('GET'); expect(mockClient.transport.request.mock.calls[0][0].path).toBe('/_msearch'); expect(mockClient.transport.request.mock.calls[0][0].body).toEqual( - convertRequestBody(mockBody as any, { timeout: '0ms' }) + convertRequestBody(mockBody as any, {}) ); expect(mockResponse.ok).toBeCalled(); expect(mockResponse.ok.mock.calls[0][0]).toEqual({ diff --git a/src/plugins/data/server/search/routes/msearch.ts b/src/plugins/data/server/search/routes/msearch.ts index efb40edd90d58..e1ddb06e4fb6f 100644 --- a/src/plugins/data/server/search/routes/msearch.ts +++ b/src/plugins/data/server/search/routes/msearch.ts @@ -20,10 +20,11 @@ import { first } from 'rxjs/operators'; import { schema } from '@kbn/config-schema'; +import { SearchResponse } from 'elasticsearch'; import { IRouter } from 'src/core/server'; -import { UI_SETTINGS } from '../../../common'; import { SearchRouteDependencies } from '../search_service'; -import { getDefaultSearchParams } from '..'; +import { shimHitsTotal } from './shim_hits_total'; +import { getShardTimeout, getDefaultSearchParams, toSnakeCase } from '..'; interface MsearchHeaders { index: string; @@ -96,30 +97,31 @@ export function registerMsearchRoute(router: IRouter, deps: SearchRouteDependenc // get shardTimeout const config = await deps.globalConfig$.pipe(first()).toPromise(); - const { timeout } = getDefaultSearchParams(config); + const timeout = getShardTimeout(config); - const body = convertRequestBody(request.body, { timeout }); + const body = convertRequestBody(request.body, timeout); + + // trackTotalHits is not supported by msearch + const { trackTotalHits, ...defaultParams } = await getDefaultSearchParams( + context.core.uiSettings.client + ); try { - const ignoreThrottled = !(await context.core.uiSettings.client.get( - UI_SETTINGS.SEARCH_INCLUDE_FROZEN - )); - const maxConcurrentShardRequests = await context.core.uiSettings.client.get( - UI_SETTINGS.COURIER_MAX_CONCURRENT_SHARD_REQUESTS - ); const response = await client.transport.request({ method: 'GET', path: '/_msearch', body, - querystring: { - rest_total_hits_as_int: true, - ignore_throttled: ignoreThrottled, - max_concurrent_shard_requests: - maxConcurrentShardRequests > 0 ? maxConcurrentShardRequests : undefined, - }, + querystring: toSnakeCase(defaultParams), }); - return res.ok({ body: response }); + return res.ok({ + body: { + ...response, + body: { + responses: response.body.responses?.map((r: SearchResponse) => shimHitsTotal(r)), + }, + }, + }); } catch (err) { return res.customError({ statusCode: err.statusCode || 500, diff --git a/src/plugins/data/server/search/routes/search.ts b/src/plugins/data/server/search/routes/search.ts index 4340285583489..b5d5ec283767d 100644 --- a/src/plugins/data/server/search/routes/search.ts +++ b/src/plugins/data/server/search/routes/search.ts @@ -21,6 +21,8 @@ import { schema } from '@kbn/config-schema'; import { IRouter } from 'src/core/server'; import { getRequestAbortedSignal } from '../../lib'; import { SearchRouteDependencies } from '../search_service'; +import { shimHitsTotal } from './shim_hits_total'; +import { isEsResponse } from '../../../common'; export function registerSearchRoute( router: IRouter, @@ -56,7 +58,17 @@ export function registerSearchRoute( strategy, } ); - return res.ok({ body: response }); + + return res.ok({ + body: { + ...response, + ...(isEsResponse(response) + ? { + rawResponse: shimHitsTotal(response.rawResponse), + } + : {}), + }, + }); } catch (err) { return res.customError({ statusCode: err.statusCode || 500, diff --git a/x-pack/plugins/data_enhanced/server/search/shim_hits_total.test.ts b/src/plugins/data/server/search/routes/shim_hits_total.test.ts similarity index 54% rename from x-pack/plugins/data_enhanced/server/search/shim_hits_total.test.ts rename to src/plugins/data/server/search/routes/shim_hits_total.test.ts index 61740b97299da..0f24735386121 100644 --- a/x-pack/plugins/data_enhanced/server/search/shim_hits_total.test.ts +++ b/src/plugins/data/server/search/routes/shim_hits_total.test.ts @@ -1,7 +1,20 @@ /* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ import { shimHitsTotal } from './shim_hits_total'; diff --git a/src/plugins/data/server/search/routes/shim_hits_total.ts b/src/plugins/data/server/search/routes/shim_hits_total.ts new file mode 100644 index 0000000000000..5f95b21358978 --- /dev/null +++ b/src/plugins/data/server/search/routes/shim_hits_total.ts @@ -0,0 +1,33 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { SearchResponse } from 'elasticsearch'; + +/** + * Temporary workaround until https://github.com/elastic/kibana/issues/26356 is addressed. + * Since we are setting `track_total_hits` in the request, `hits.total` will be an object + * containing the `value`. + * + * @internal + */ +export function shimHitsTotal(response: SearchResponse) { + const total = (response.hits?.total as any)?.value ?? response.hits?.total; + const hits = { ...response.hits, total }; + return { ...response, hits }; +} diff --git a/src/plugins/data/server/search/types.ts b/src/plugins/data/server/search/types.ts index b2b958454de48..aefdac2ab639f 100644 --- a/src/plugins/data/server/search/types.ts +++ b/src/plugins/data/server/search/types.ts @@ -20,7 +20,7 @@ import { RequestHandlerContext } from '../../../../core/server'; import { ISearchOptions } from '../../common/search'; import { AggsSetup, AggsStart } from './aggs'; -import { SearchUsage } from './collectors/usage'; +import { SearchUsage } from './collectors'; import { IEsSearchRequest, IEsSearchResponse } from './es_search'; export interface SearchEnhancements { diff --git a/src/plugins/data/server/server.api.md b/src/plugins/data/server/server.api.md index a4f5f590e1774..cd0369a5c4551 100644 --- a/src/plugins/data/server/server.api.md +++ b/src/plugins/data/server/server.api.md @@ -446,14 +446,25 @@ export interface Filter { query?: any; } -// Warning: (ae-forgotten-export) The symbol "SharedGlobalConfig" needs to be exported by the entry point index.d.ts +// Warning: (ae-forgotten-export) The symbol "IUiSettingsClient" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "getDefaultSearchParams" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function getDefaultSearchParams(config: SharedGlobalConfig): { - timeout: string; +export function getDefaultSearchParams(uiSettingsClient: IUiSettingsClient): Promise<{ + maxConcurrentShardRequests: number | undefined; + ignoreThrottled: boolean; ignoreUnavailable: boolean; - restTotalHitsAsInt: boolean; + trackTotalHits: boolean; +}>; + +// Warning: (ae-forgotten-export) The symbol "SharedGlobalConfig" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "getShardTimeout" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function getShardTimeout(config: SharedGlobalConfig): { + timeout: string; +} | { + timeout?: undefined; }; // Warning: (ae-missing-release-tag) "getTime" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -873,7 +884,7 @@ export class Plugin implements Plugin_2>; + search: ISearchStart>; fieldFormats: { fieldFormatServiceFactory: (uiSettings: import("../../../core/server").IUiSettingsClient) => Promise; }; @@ -989,6 +1000,33 @@ export interface SearchUsage { trackSuccess(duration: number): Promise; } +// @internal +export function shimHitsTotal(response: SearchResponse): { + hits: { + total: any; + max_score: number; + hits: { + _index: string; + _type: string; + _id: string; + _score: number; + _source: any; + _version?: number | undefined; + _explanation?: import("elasticsearch").Explanation | undefined; + fields?: any; + highlight?: any; + inner_hits?: any; + matched_queries?: string[] | undefined; + sort?: string[] | undefined; + }[]; + }; + took: number; + timed_out: boolean; + _scroll_id?: string | undefined; + _shards: import("elasticsearch").ShardsResponse; + aggregations?: any; +}; + // Warning: (ae-missing-release-tag) "shouldReadFieldFromDocValues" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1027,6 +1065,11 @@ export interface TimeRange { to: string; } +// Warning: (ae-missing-release-tag) "toSnakeCase" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function toSnakeCase(obj: Record): import("lodash").Dictionary; + // Warning: (ae-missing-release-tag) "UI_SETTINGS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1043,6 +1086,7 @@ export const UI_SETTINGS: { readonly COURIER_MAX_CONCURRENT_SHARD_REQUESTS: "courier:maxConcurrentShardRequests"; readonly COURIER_BATCH_SEARCHES: "courier:batchSearches"; readonly SEARCH_INCLUDE_FROZEN: "search:includeFrozen"; + readonly SEARCH_TIMEOUT: "search:timeout"; readonly HISTOGRAM_BAR_TARGET: "histogram:barTarget"; readonly HISTOGRAM_MAX_BARS: "histogram:maxBars"; readonly HISTORY_LIMIT: "history:limit"; @@ -1091,19 +1135,19 @@ export function usageProvider(core: CoreSetup_2): SearchUsage; // src/plugins/data/server/index.ts:101:26 - (ae-forgotten-export) The symbol "TruncateFormat" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isFilterable" needs to be exported by the entry point index.d.ts // src/plugins/data/server/index.ts:127:27 - (ae-forgotten-export) The symbol "isNestedField" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:222:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:222:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:222:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:222:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:224:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:225:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:234:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:235:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:236:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:240:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:241:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:245:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts -// src/plugins/data/server/index.ts:248:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:225:20 - (ae-forgotten-export) The symbol "getRequestInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:225:20 - (ae-forgotten-export) The symbol "getResponseInspectorStats" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:225:20 - (ae-forgotten-export) The symbol "tabifyAggResponse" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:225:20 - (ae-forgotten-export) The symbol "tabifyGetColumns" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:227:1 - (ae-forgotten-export) The symbol "CidrMask" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:228:1 - (ae-forgotten-export) The symbol "dateHistogramInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:237:1 - (ae-forgotten-export) The symbol "InvalidEsCalendarIntervalError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:238:1 - (ae-forgotten-export) The symbol "InvalidEsIntervalFormatError" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:239:1 - (ae-forgotten-export) The symbol "Ipv4Address" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:243:1 - (ae-forgotten-export) The symbol "isValidEsInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:244:1 - (ae-forgotten-export) The symbol "isValidInterval" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:248:1 - (ae-forgotten-export) The symbol "propFilter" needs to be exported by the entry point index.d.ts +// src/plugins/data/server/index.ts:251:1 - (ae-forgotten-export) The symbol "toAbsoluteDates" needs to be exported by the entry point index.d.ts // src/plugins/data/server/plugin.ts:88:66 - (ae-forgotten-export) The symbol "DataEnhancements" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) diff --git a/src/plugins/dev_tools/kibana.json b/src/plugins/dev_tools/kibana.json index d83cabd0f0817..f1c6c9ecf87e6 100644 --- a/src/plugins/dev_tools/kibana.json +++ b/src/plugins/dev_tools/kibana.json @@ -3,5 +3,5 @@ "version": "kibana", "server": false, "ui": true, - "requiredPlugins": ["kibanaLegacy"] + "requiredPlugins": ["urlForwarding"] } diff --git a/src/plugins/dev_tools/public/plugin.ts b/src/plugins/dev_tools/public/plugin.ts index 45fa3634bc87e..fcc6a57361a94 100644 --- a/src/plugins/dev_tools/public/plugin.ts +++ b/src/plugins/dev_tools/public/plugin.ts @@ -24,7 +24,7 @@ import { i18n } from '@kbn/i18n'; import { sortBy } from 'lodash'; import { AppNavLinkStatus, DEFAULT_APP_CATEGORIES } from '../../../core/public'; -import { KibanaLegacySetup } from '../../kibana_legacy/public'; +import { UrlForwardingSetup } from '../../url_forwarding/public'; import { CreateDevToolArgs, DevToolApp, createDevToolApp } from './dev_tool'; import './index.scss'; @@ -51,7 +51,7 @@ export class DevToolsPlugin implements Plugin { return sortBy([...this.devTools.values()], 'order'); } - public setup(coreSetup: CoreSetup, { kibanaLegacy }: { kibanaLegacy: KibanaLegacySetup }) { + public setup(coreSetup: CoreSetup, { urlForwarding }: { urlForwarding: UrlForwardingSetup }) { const { application: applicationSetup, getStartServices } = coreSetup; applicationSetup.register({ @@ -75,7 +75,7 @@ export class DevToolsPlugin implements Plugin { }, }); - kibanaLegacy.forwardApp('dev_tools', 'dev_tools'); + urlForwarding.forwardApp('dev_tools', 'dev_tools'); return { register: (devToolArgs: CreateDevToolArgs) => { diff --git a/src/plugins/discover/kibana.json b/src/plugins/discover/kibana.json index 041f362bf0623..1a23f6deb5fa5 100644 --- a/src/plugins/discover/kibana.json +++ b/src/plugins/discover/kibana.json @@ -9,6 +9,7 @@ "embeddable", "inspector", "kibanaLegacy", + "urlForwarding", "navigation", "uiActions", "visualizations" diff --git a/src/plugins/discover/public/application/angular/discover_state.ts b/src/plugins/discover/public/application/angular/discover_state.ts index ff8fb9f80a723..ac0dc054485f0 100644 --- a/src/plugins/discover/public/application/angular/discover_state.ts +++ b/src/plugins/discover/public/application/angular/discover_state.ts @@ -28,7 +28,7 @@ import { withNotifyOnErrors, } from '../../../../kibana_utils/public'; import { esFilters, Filter, Query } from '../../../../data/public'; -import { migrateLegacyQuery } from '../../../../kibana_legacy/public'; +import { migrateLegacyQuery } from '../helpers/migrate_legacy_query'; export interface AppState { /** diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts index a32af8fe43dc1..4db1d2b175d0b 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts +++ b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.test.ts @@ -58,6 +58,11 @@ describe('docTable', function () { expect(getSort([['foo', 'bar']], indexPattern)).toEqual([]); expect(getSort([{ foo: 'bar' }], indexPattern)).toEqual([]); }); + + test('should convert a legacy sort to an array of objects', function () { + expect(getSort(['foo', 'desc'], indexPattern)).toEqual([{ foo: 'desc' }]); + expect(getSort(['foo', 'asc'], indexPattern)).toEqual([{ foo: 'asc' }]); + }); }); describe('getSortArray function', function () { diff --git a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts index c28519692318e..73ae691529e2b 100644 --- a/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts +++ b/src/plugins/discover/public/application/angular/doc_table/lib/get_sort.ts @@ -46,6 +46,12 @@ function createSortObject( } } +export function isLegacySort(sort: SortPair[] | SortPair): sort is SortPair { + return ( + sort.length === 2 && typeof sort[0] === 'string' && (sort[1] === 'desc' || sort[1] === 'asc') + ); +} + /** * Take a sorting array and make it into an object * @param {array} sort two dimensional array [[fieldToSort, directionToSort]] @@ -53,8 +59,12 @@ function createSortObject( * @param {object} indexPattern used for determining default sort * @returns Array<{object}> an array of sort objects */ -export function getSort(sort: SortPair[], indexPattern: IndexPattern): SortPairObj[] { +export function getSort(sort: SortPair[] | SortPair, indexPattern: IndexPattern): SortPairObj[] { if (Array.isArray(sort)) { + if (isLegacySort(sort)) { + // To stay compatible with legacy sort, which just supported a single sort field + return [{ [sort[0]]: sort[1] }]; + } return sort .map((sortPair: SortPair) => createSortObject(sortPair, indexPattern)) .filter((sortPairObj) => typeof sortPairObj === 'object') as SortPairObj[]; diff --git a/src/plugins/discover/public/application/angular/redirect.ts b/src/plugins/discover/public/application/angular/redirect.ts index bfa2f07f852e9..d3fb47f329d4b 100644 --- a/src/plugins/discover/public/application/angular/redirect.ts +++ b/src/plugins/discover/public/application/angular/redirect.ts @@ -24,10 +24,10 @@ getAngularModule().config(($routeProvider: any) => { const path = window.location.hash.substr(1); getUrlTracker().restorePreviousUrl(); $rootScope.$applyAsync(() => { - const { kibanaLegacy } = getServices(); - const { navigated } = kibanaLegacy.navigateToLegacyKibanaUrl(path); + const { urlForwarding } = getServices(); + const { navigated } = urlForwarding.navigateToLegacyKibanaUrl(path); if (!navigated) { - kibanaLegacy.navigateToDefaultApp(); + urlForwarding.navigateToDefaultApp(); } }); // prevent angular from completing the navigation diff --git a/src/plugins/discover/public/application/helpers/migrate_legacy_query.ts b/src/plugins/discover/public/application/helpers/migrate_legacy_query.ts new file mode 100644 index 0000000000000..8d9b50d5a66b2 --- /dev/null +++ b/src/plugins/discover/public/application/helpers/migrate_legacy_query.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { has } from 'lodash'; +import { Query } from 'src/plugins/data/public'; + +/** + * Creates a standardized query object from old queries that were either strings or pure ES query DSL + * + * @param query - a legacy query, what used to be stored in SearchSource's query property + * @return Object + */ + +export function migrateLegacyQuery(query: Query | { [key: string]: any } | string): Query { + // Lucene was the only option before, so language-less queries are all lucene + if (!has(query, 'language')) { + return { query, language: 'lucene' }; + } + + return query as Query; +} diff --git a/src/plugins/discover/public/build_services.ts b/src/plugins/discover/public/build_services.ts index 75c83e30d80ad..12562d8571a25 100644 --- a/src/plugins/discover/public/build_services.ts +++ b/src/plugins/discover/public/build_services.ts @@ -43,6 +43,7 @@ import { DiscoverStartPlugins } from './plugin'; import { createSavedSearchesLoader, SavedSearch } from './saved_searches'; import { getHistory } from './kibana_services'; import { KibanaLegacyStart } from '../../kibana_legacy/public'; +import { UrlForwardingStart } from '../../url_forwarding/public'; export interface DiscoverServices { addBasePath: (path: string) => string; @@ -59,6 +60,7 @@ export interface DiscoverServices { metadata: { branch: string }; share?: SharePluginStart; kibanaLegacy: KibanaLegacyStart; + urlForwarding: UrlForwardingStart; timefilter: TimefilterContract; toastNotifications: ToastsStart; getSavedSearchById: (id: string) => Promise; @@ -100,6 +102,7 @@ export async function buildServices( }, share: plugins.share, kibanaLegacy: plugins.kibanaLegacy, + urlForwarding: plugins.urlForwarding, timefilter: plugins.data.query.timefilter.timefilter, toastNotifications: core.notifications.toasts, uiSettings: core.uiSettings, diff --git a/src/plugins/discover/public/plugin.ts b/src/plugins/discover/public/plugin.ts index 015f4267646c1..b6960c8a20abf 100644 --- a/src/plugins/discover/public/plugin.ts +++ b/src/plugins/discover/public/plugin.ts @@ -37,6 +37,7 @@ import { NavigationPublicPluginStart as NavigationStart } from 'src/plugins/navi import { SharePluginStart, SharePluginSetup, UrlGeneratorContract } from 'src/plugins/share/public'; import { VisualizationsStart, VisualizationsSetup } from 'src/plugins/visualizations/public'; import { KibanaLegacySetup, KibanaLegacyStart } from 'src/plugins/kibana_legacy/public'; +import { UrlForwardingSetup, UrlForwardingStart } from 'src/plugins/url_forwarding/public'; import { HomePublicPluginSetup } from 'src/plugins/home/public'; import { Start as InspectorPublicPluginStart } from 'src/plugins/inspector/public'; import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public'; @@ -119,6 +120,7 @@ export interface DiscoverSetupPlugins { uiActions: UiActionsSetup; embeddable: EmbeddableSetup; kibanaLegacy: KibanaLegacySetup; + urlForwarding: UrlForwardingSetup; home?: HomePublicPluginSetup; visualizations: VisualizationsSetup; data: DataPublicPluginSetup; @@ -135,6 +137,7 @@ export interface DiscoverStartPlugins { data: DataPublicPluginStart; share?: SharePluginStart; kibanaLegacy: KibanaLegacyStart; + urlForwarding: UrlForwardingStart; inspector: InspectorPublicPluginStart; visualizations: VisualizationsStart; } @@ -267,13 +270,13 @@ export class DiscoverPlugin }, }); - plugins.kibanaLegacy.forwardApp('doc', 'discover', (path) => { + plugins.urlForwarding.forwardApp('doc', 'discover', (path) => { return `#${path}`; }); - plugins.kibanaLegacy.forwardApp('context', 'discover', (path) => { + plugins.urlForwarding.forwardApp('context', 'discover', (path) => { return `#${path}`; }); - plugins.kibanaLegacy.forwardApp('discover', 'discover', (path) => { + plugins.urlForwarding.forwardApp('discover', 'discover', (path) => { const [, id, tail] = /discover\/([^\?]+)(.*)/.exec(path) || []; if (!id) { return `#${path.replace('/discover', '') || '/'}`; diff --git a/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts b/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts index 1cdb5af00e748..3460203aac29c 100644 --- a/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts +++ b/src/plugins/embeddable/public/lib/actions/apply_filter_action.ts @@ -42,6 +42,7 @@ export function createFilterAction(): ActionByType { return createAction({ type: ACTION_APPLY_FILTER, id: ACTION_APPLY_FILTER, + order: 100, getIconType: () => 'filter', getDisplayName: () => { return i18n.translate('embeddableApi.actions.applyFilterActionTitle', { diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx index 0d6fd122ad22c..7a42ed7fad427 100644 --- a/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx +++ b/src/plugins/es_ui_shared/public/request/use_request.test.helpers.tsx @@ -106,7 +106,7 @@ export const createUseRequestHelpers = (): UseRequestHelpers => { }; const TestComponent = ({ requestConfig }: { requestConfig: UseRequestConfig }) => { - const { isInitialRequest, isLoading, error, data, sendRequest } = useRequest( + const { isInitialRequest, isLoading, error, data, resendRequest } = useRequest( httpClient as HttpSetup, requestConfig ); @@ -115,7 +115,7 @@ export const createUseRequestHelpers = (): UseRequestHelpers => { hookResult.isLoading = isLoading; hookResult.error = error; hookResult.data = data; - hookResult.sendRequest = sendRequest; + hookResult.resendRequest = resendRequest; return null; }; diff --git a/src/plugins/es_ui_shared/public/request/use_request.test.ts b/src/plugins/es_ui_shared/public/request/use_request.test.ts index f7902218d9314..2a639f93b47b4 100644 --- a/src/plugins/es_ui_shared/public/request/use_request.test.ts +++ b/src/plugins/es_ui_shared/public/request/use_request.test.ts @@ -102,7 +102,7 @@ describe('useRequest hook', () => { setupSuccessRequest(); expect(hookResult.isInitialRequest).toBe(true); - hookResult.sendRequest(); + hookResult.resendRequest(); await completeRequest(); expect(hookResult.isInitialRequest).toBe(false); }); @@ -148,7 +148,7 @@ describe('useRequest hook', () => { expect(hookResult.error).toBe(getErrorResponse().error); act(() => { - hookResult.sendRequest(); + hookResult.resendRequest(); }); expect(hookResult.isLoading).toBe(true); expect(hookResult.error).toBe(getErrorResponse().error); @@ -183,7 +183,7 @@ describe('useRequest hook', () => { expect(hookResult.data).toBe(getSuccessResponse().data); act(() => { - hookResult.sendRequest(); + hookResult.resendRequest(); }); expect(hookResult.isLoading).toBe(true); expect(hookResult.data).toBe(getSuccessResponse().data); @@ -215,7 +215,7 @@ describe('useRequest hook', () => { }); describe('callbacks', () => { - describe('sendRequest', () => { + describe('resendRequest', () => { it('sends the request', async () => { const { setupSuccessRequest, completeRequest, hookResult, getSendRequestSpy } = helpers; setupSuccessRequest(); @@ -224,7 +224,7 @@ describe('useRequest hook', () => { expect(getSendRequestSpy().callCount).toBe(1); await act(async () => { - hookResult.sendRequest(); + hookResult.resendRequest(); await completeRequest(); }); expect(getSendRequestSpy().callCount).toBe(2); @@ -239,17 +239,17 @@ describe('useRequest hook', () => { await advanceTime(REQUEST_TIME); expect(getSendRequestSpy().callCount).toBe(1); act(() => { - hookResult.sendRequest(); + hookResult.resendRequest(); }); // The manual request resolves, and we'll send yet another one... await advanceTime(REQUEST_TIME); expect(getSendRequestSpy().callCount).toBe(2); act(() => { - hookResult.sendRequest(); + hookResult.resendRequest(); }); - // At this point, we've moved forward 3s. The poll is set at 2s. If sendRequest didn't + // At this point, we've moved forward 3s. The poll is set at 2s. If resendRequest didn't // reset the poll, the request call count would be 4, not 3. await advanceTime(REQUEST_TIME); expect(getSendRequestSpy().callCount).toBe(3); @@ -291,14 +291,14 @@ describe('useRequest hook', () => { const HALF_REQUEST_TIME = REQUEST_TIME * 0.5; setupSuccessRequest({ pollIntervalMs: REQUEST_TIME }); - // Before the original request resolves, we make a manual sendRequest call. + // Before the original request resolves, we make a manual resendRequest call. await advanceTime(HALF_REQUEST_TIME); expect(getSendRequestSpy().callCount).toBe(0); act(() => { - hookResult.sendRequest(); + hookResult.resendRequest(); }); - // The original quest resolves but it's been marked as outdated by the the manual sendRequest + // The original quest resolves but it's been marked as outdated by the the manual resendRequest // call "interrupts", so data is left undefined. await advanceTime(HALF_REQUEST_TIME); expect(getSendRequestSpy().callCount).toBe(1); diff --git a/src/plugins/es_ui_shared/public/request/use_request.ts b/src/plugins/es_ui_shared/public/request/use_request.ts index 481843bf40e88..e04f84a67b8a3 100644 --- a/src/plugins/es_ui_shared/public/request/use_request.ts +++ b/src/plugins/es_ui_shared/public/request/use_request.ts @@ -20,11 +20,7 @@ import { useEffect, useCallback, useState, useRef, useMemo } from 'react'; import { HttpSetup } from '../../../../../src/core/public'; -import { - sendRequest as sendStatelessRequest, - SendRequestConfig, - SendRequestResponse, -} from './send_request'; +import { sendRequest, SendRequestConfig } from './send_request'; export interface UseRequestConfig extends SendRequestConfig { pollIntervalMs?: number; @@ -37,7 +33,7 @@ export interface UseRequestResponse { isLoading: boolean; error: E | null; data?: D | null; - sendRequest: () => Promise>; + resendRequest: () => void; } export const useRequest = ( @@ -80,7 +76,7 @@ export const useRequest = ( /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [path, method, queryStringified, bodyStringified]); - const sendRequest = useCallback(async () => { + const resendRequest = useCallback(async () => { // If we're on an interval, this allows us to reset it if the user has manually requested the // data, to avoid doubled-up requests. clearPollInterval(); @@ -91,7 +87,7 @@ export const useRequest = ( // "old" error/data or loading state when a new request is in-flight. setIsLoading(true); - const response = await sendStatelessRequest(httpClient, requestBody); + const response = await sendRequest(httpClient, requestBody); const { data: serializedResponseData, error: responseError } = response; const isOutdatedRequest = requestId !== requestCountRef.current; @@ -99,7 +95,7 @@ export const useRequest = ( // Ignore outdated or irrelevant data. if (isOutdatedRequest || isUnmounted) { - return { data: null, error: null }; + return; } setError(responseError); @@ -112,8 +108,6 @@ export const useRequest = ( } // Setting isLoading to false also acts as a signal for scheduling the next poll request. setIsLoading(false); - - return { data: serializedResponseData, error: responseError }; }, [requestBody, httpClient, deserializer, clearPollInterval]); const scheduleRequest = useCallback(() => { @@ -121,19 +115,19 @@ export const useRequest = ( clearPollInterval(); if (pollIntervalMs) { - pollIntervalIdRef.current = setTimeout(sendRequest, pollIntervalMs); + pollIntervalIdRef.current = setTimeout(resendRequest, pollIntervalMs); } - }, [pollIntervalMs, sendRequest, clearPollInterval]); + }, [pollIntervalMs, resendRequest, clearPollInterval]); - // Send the request on component mount and whenever the dependencies of sendRequest() change. + // Send the request on component mount and whenever the dependencies of resendRequest() change. useEffect(() => { - sendRequest(); - }, [sendRequest]); + resendRequest(); + }, [resendRequest]); // Schedule the next poll request when the previous one completes. useEffect(() => { // When a request completes, attempt to schedule the next one. Note that we aren't re-scheduling - // a request whenever sendRequest's dependencies change. isLoading isn't set to false until the + // a request whenever resendRequest's dependencies change. isLoading isn't set to false until the // initial request has completed, so we won't schedule a request on mount. if (!isLoading) { scheduleRequest(); @@ -156,6 +150,6 @@ export const useRequest = ( isLoading, error, data, - sendRequest, // Gives the user the ability to manually request data + resendRequest, // Gives the user the ability to manually request data }; }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts index 3688421964d2e..d0ac0d4ab28c3 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/components/use_array.ts @@ -17,18 +17,25 @@ * under the License. */ -import { useState, useEffect, useRef, useCallback } from 'react'; +import { useEffect, useRef, useCallback, useMemo } from 'react'; +import { FormHook, FieldConfig } from '../types'; +import { getFieldValidityAndErrorMessage } from '../helpers'; import { useFormContext } from '../form_context'; +import { useField, InternalFieldConfig } from '../hooks'; interface Props { path: string; initialNumberOfItems?: number; readDefaultValueOnForm?: boolean; + validations?: FieldConfig['validations']; children: (args: { items: ArrayItem[]; + error: string | null; addItem: () => void; removeItem: (id: number) => void; + moveItem: (sourceIdx: number, destinationIdx: number) => void; + form: FormHook; }) => JSX.Element; } @@ -56,32 +63,62 @@ export interface ArrayItem { export const UseArray = ({ path, initialNumberOfItems, + validations, readDefaultValueOnForm = true, children, }: Props) => { - const didMountRef = useRef(false); - const form = useFormContext(); - const defaultValues = readDefaultValueOnForm && (form.getFieldDefaultValue(path) as any[]); + const isMounted = useRef(false); const uniqueId = useRef(0); - const getInitialItemsFromValues = (values: any[]): ArrayItem[] => - values.map((_, index) => ({ + const form = useFormContext(); + const { getFieldDefaultValue } = form; + + const getNewItemAtIndex = useCallback( + (index: number): ArrayItem => ({ id: uniqueId.current++, path: `${path}[${index}]`, - isNew: false, - })); + isNew: true, + }), + [path] + ); + + const fieldDefaultValue = useMemo(() => { + const defaultValues = readDefaultValueOnForm + ? (getFieldDefaultValue(path) as any[]) + : undefined; - const getNewItemAtIndex = (index: number): ArrayItem => ({ - id: uniqueId.current++, - path: `${path}[${index}]`, - isNew: true, - }); + const getInitialItemsFromValues = (values: any[]): ArrayItem[] => + values.map((_, index) => ({ + id: uniqueId.current++, + path: `${path}[${index}]`, + isNew: false, + })); - const initialState = defaultValues - ? getInitialItemsFromValues(defaultValues) - : new Array(initialNumberOfItems).fill('').map((_, i) => getNewItemAtIndex(i)); + return defaultValues + ? getInitialItemsFromValues(defaultValues) + : new Array(initialNumberOfItems).fill('').map((_, i) => getNewItemAtIndex(i)); + }, [path, initialNumberOfItems, readDefaultValueOnForm, getFieldDefaultValue, getNewItemAtIndex]); - const [items, setItems] = useState(initialState); + // Create a new hook field with the "hasValue" set to false so we don't use its value to build the final form data. + // Apart from that the field behaves like a normal field and is hooked into the form validation lifecycle. + const fieldConfigBase: FieldConfig & InternalFieldConfig = { + defaultValue: fieldDefaultValue, + errorDisplayDelay: 0, + isIncludedInOutput: false, + }; + + const fieldConfig: FieldConfig & InternalFieldConfig = validations + ? { validations, ...fieldConfigBase } + : fieldConfigBase; + + const field = useField(form, path, fieldConfig); + const { setValue, value, isChangingValue, errors } = field; + + // Derived state from the field + const error = useMemo(() => { + const { errorMessage } = getFieldValidityAndErrorMessage({ isChangingValue, errors }); + return errorMessage; + }, [isChangingValue, errors]); const updatePaths = useCallback( (_rows: ArrayItem[]) => { @@ -96,29 +133,51 @@ export const UseArray = ({ [path] ); - const addItem = () => { - setItems((previousItems) => { + const addItem = useCallback(() => { + setValue((previousItems) => { const itemIndex = previousItems.length; return [...previousItems, getNewItemAtIndex(itemIndex)]; }); - }; + }, [setValue, getNewItemAtIndex]); - const removeItem = (id: number) => { - setItems((previousItems) => { - const updatedItems = previousItems.filter((item) => item.id !== id); - return updatePaths(updatedItems); - }); - }; + const removeItem = useCallback( + (id: number) => { + setValue((previousItems) => { + const updatedItems = previousItems.filter((item) => item.id !== id); + return updatePaths(updatedItems); + }); + }, + [setValue, updatePaths] + ); - useEffect(() => { - if (didMountRef.current) { - setItems((prev) => { - return updatePaths(prev); + const moveItem = useCallback( + (sourceIdx: number, destinationIdx: number) => { + setValue((previousItems) => { + const nextItems = [...previousItems]; + const removed = nextItems.splice(sourceIdx, 1)[0]; + nextItems.splice(destinationIdx, 0, removed); + return updatePaths(nextItems); }); - } else { - didMountRef.current = true; + }, + [setValue, updatePaths] + ); + + useEffect(() => { + if (!isMounted.current) { + return; } - }, [path, updatePaths]); - return children({ items, addItem, removeItem }); + setValue((prev) => { + return updatePaths(prev); + }); + }, [path, updatePaths, setValue]); + + useEffect(() => { + isMounted.current = true; + return () => { + isMounted.current = false; + }; + }, []); + + return children({ items: value, error, form, addItem, removeItem, moveItem }); }; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/helpers.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/helpers.ts index 7ea42f81b43cb..a148dc543542b 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/helpers.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/helpers.ts @@ -19,9 +19,10 @@ import { FieldHook } from './types'; -export const getFieldValidityAndErrorMessage = ( - field: FieldHook -): { isInvalid: boolean; errorMessage: string | null } => { +export const getFieldValidityAndErrorMessage = (field: { + isChangingValue: FieldHook['isChangingValue']; + errors: FieldHook['errors']; +}): { isInvalid: boolean; errorMessage: string | null } => { const isInvalid = !field.isChangingValue && field.errors.length > 0; const errorMessage = !field.isChangingValue && field.errors.length ? field.errors[0].message : null; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts index 45c11dd6272e4..aa9610dd85ae3 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/index.ts @@ -17,6 +17,6 @@ * under the License. */ -export { useField } from './use_field'; +export { useField, InternalFieldConfig } from './use_field'; export { useForm } from './use_form'; export { useFormData } from './use_form_data'; diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts index f01c7226ea4ce..bb4aae6eccae8 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_field.ts @@ -22,16 +22,22 @@ import { useMemo, useState, useEffect, useRef, useCallback } from 'react'; import { FormHook, FieldHook, FieldConfig, FieldValidateResponse, ValidationError } from '../types'; import { FIELD_TYPES, VALIDATION_TYPES } from '../constants'; +export interface InternalFieldConfig { + initialValue?: T; + isIncludedInOutput?: boolean; +} + export const useField = ( form: FormHook, path: string, - config: FieldConfig & { initialValue?: T } = {}, + config: FieldConfig & InternalFieldConfig = {}, valueChangeListener?: (value: T) => void ) => { const { type = FIELD_TYPES.TEXT, defaultValue = '', // The value to use a fallback mecanism when no initial value is passed initialValue = config.defaultValue ?? '', // The value explicitly passed + isIncludedInOutput = true, label = '', labelAppend = '', helpText = '', @@ -201,7 +207,7 @@ export const useField = ( validationTypeToValidate, }: { formData: any; - value: unknown; + value: T; validationTypeToValidate?: string; }): ValidationError[] | Promise => { if (!validations) { @@ -234,7 +240,7 @@ export const useField = ( } inflightValidation.current = validator({ - value: (valueToValidate as unknown) as string, + value: valueToValidate, errors: validationErrors, form: { getFormData, getFields }, formData, @@ -280,7 +286,7 @@ export const useField = ( } const validationResult = validator({ - value: (valueToValidate as unknown) as string, + value: valueToValidate, errors: validationErrors, form: { getFormData, getFields }, formData, @@ -388,9 +394,15 @@ export const useField = ( */ const setValue: FieldHook['setValue'] = useCallback( (newValue) => { - const formattedValue = formatInputValue(newValue); - setStateValue(formattedValue); - return formattedValue; + setStateValue((prev) => { + let formattedValue: T; + if (typeof newValue === 'function') { + formattedValue = formatInputValue((newValue as Function)(prev)); + } else { + formattedValue = formatInputValue(newValue); + } + return formattedValue; + }); }, [formatInputValue] ); @@ -496,6 +508,7 @@ export const useField = ( clearErrors, validate, reset, + __isIncludedInOutput: isIncludedInOutput, __serializeValue: serializeValue, }; }, [ @@ -511,6 +524,7 @@ export const useField = ( isValidating, isValidated, isChangingValue, + isIncludedInOutput, onChange, getErrorsMessages, setValue, diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts index 7b72a9eeacf7b..b390c17d3c2ff 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/hooks/use_form.ts @@ -95,19 +95,25 @@ export function useForm( const fieldsToArray = useCallback(() => Object.values(fieldsRefs.current), []); - const stripEmptyFields = useCallback( - (fields: FieldsMap): FieldsMap => { - if (formOptions.stripEmptyFields) { - return Object.entries(fields).reduce((acc, [key, field]) => { - if (typeof field.value !== 'string' || field.value.trim() !== '') { - acc[key] = field; - } + const getFieldsForOutput = useCallback( + (fields: FieldsMap, opts: { stripEmptyFields: boolean }): FieldsMap => { + return Object.entries(fields).reduce((acc, [key, field]) => { + if (!field.__isIncludedInOutput) { return acc; - }, {} as FieldsMap); - } - return fields; + } + + if (opts.stripEmptyFields) { + const isFieldEmpty = typeof field.value === 'string' && field.value.trim() === ''; + if (isFieldEmpty) { + return acc; + } + } + + acc[key] = field; + return acc; + }, {} as FieldsMap); }, - [formOptions] + [] ); const updateFormDataAt: FormHook['__updateFormDataAt'] = useCallback( @@ -133,8 +139,10 @@ export function useForm( const getFormData: FormHook['getFormData'] = useCallback( (getDataOptions: Parameters['getFormData']>[0] = { unflatten: true }) => { if (getDataOptions.unflatten) { - const nonEmptyFields = stripEmptyFields(fieldsRefs.current); - const fieldsValue = mapFormFields(nonEmptyFields, (field) => field.__serializeValue()); + const fieldsToOutput = getFieldsForOutput(fieldsRefs.current, { + stripEmptyFields: formOptions.stripEmptyFields, + }); + const fieldsValue = mapFormFields(fieldsToOutput, (field) => field.__serializeValue()); return serializer ? (serializer(unflattenObject(fieldsValue)) as T) : (unflattenObject(fieldsValue) as T); @@ -148,7 +156,7 @@ export function useForm( {} as T ); }, - [stripEmptyFields, serializer] + [getFieldsForOutput, formOptions.stripEmptyFields, serializer] ); const getErrors: FormHook['getErrors'] = useCallback(() => { diff --git a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts index 4b343ec5e9f2e..18b8f478f7c0e 100644 --- a/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts +++ b/src/plugins/es_ui_shared/static/forms/hook_form_lib/types.ts @@ -108,15 +108,18 @@ export interface FieldHook { errorCode?: string; }) => string | null; onChange: (event: ChangeEvent<{ name?: string; value: string; checked?: boolean }>) => void; - setValue: (value: T) => T; + setValue: (value: T | ((prevValue: T) => T)) => void; setErrors: (errors: ValidationError[]) => void; clearErrors: (type?: string | string[]) => void; validate: (validateData?: { formData?: any; - value?: unknown; + value?: T; validationType?: string; }) => FieldValidateResponse | Promise; reset: (options?: { resetValue?: boolean; defaultValue?: T }) => unknown | undefined; + // Flag to indicate if the field value will be included in the form data outputted + // when calling form.getFormData(); + __isIncludedInOutput: boolean; __serializeValue: (rawValue?: unknown) => unknown; } @@ -127,7 +130,7 @@ export interface FieldConfig { readonly helpText?: string | ReactNode; readonly type?: HTMLInputElement['type']; readonly defaultValue?: ValueType; - readonly validations?: Array>; + readonly validations?: Array>; readonly formatters?: FormatterFunc[]; readonly deserializer?: SerializerFunc; readonly serializer?: SerializerFunc; @@ -163,8 +166,8 @@ export interface ValidationFuncArg { errors: readonly ValidationError[]; } -export type ValidationFunc = ( - data: ValidationFuncArg +export type ValidationFunc = ( + data: ValidationFuncArg ) => ValidationError | void | undefined | Promise | void | undefined>; export interface FieldValidateResponse { @@ -184,8 +187,8 @@ type FormatterFunc = (value: any, formData: FormData) => unknown; // string | number | boolean | string[] ... type FieldValue = unknown; -export interface ValidationConfig { - validator: ValidationFunc; +export interface ValidationConfig { + validator: ValidationFunc; type?: string; /** * By default all validation are blockers, which means that if they fail, the field is invalid. diff --git a/src/plugins/expressions/common/expression_types/specs/boolean.ts b/src/plugins/expressions/common/expression_types/specs/boolean.ts index adbdeafc34fd2..73b0b98eaaf06 100644 --- a/src/plugins/expressions/common/expression_types/specs/boolean.ts +++ b/src/plugins/expressions/common/expression_types/specs/boolean.ts @@ -41,7 +41,6 @@ export const boolean: ExpressionTypeDefinition<'boolean', boolean> = { }, datatable: (value): Datatable => ({ type: 'datatable', - meta: {}, columns: [{ id: 'value', name: 'value', meta: { type: name } }], rows: [{ value }], }), diff --git a/src/plugins/expressions/common/expression_types/specs/datatable.ts b/src/plugins/expressions/common/expression_types/specs/datatable.ts index dd3c653878de7..c201e99faeb03 100644 --- a/src/plugins/expressions/common/expression_types/specs/datatable.ts +++ b/src/plugins/expressions/common/expression_types/specs/datatable.ts @@ -52,7 +52,10 @@ export type DatatableRow = Record; export interface DatatableColumnMeta { type: DatatableColumnType; field?: string; + index?: string; params?: SerializableState; + source?: string; + sourceParams?: SerializableState; } /** * This type represents the shape of a column in a `Datatable`. @@ -63,17 +66,11 @@ export interface DatatableColumn { meta: DatatableColumnMeta; } -export interface DatatableMeta { - type?: string; - source?: string; -} - /** * A `Datatable` in Canvas is a unique structure that represents tabulated data. */ export interface Datatable { type: typeof name; - meta?: DatatableMeta; columns: DatatableColumn[]; rows: DatatableRow[]; } diff --git a/src/plugins/expressions/common/expression_types/specs/num.ts b/src/plugins/expressions/common/expression_types/specs/num.ts index 041747f39740b..d208a9dcf73c8 100644 --- a/src/plugins/expressions/common/expression_types/specs/num.ts +++ b/src/plugins/expressions/common/expression_types/specs/num.ts @@ -73,7 +73,6 @@ export const num: ExpressionTypeDefinition<'num', ExpressionValueNum> = { }, datatable: ({ value }): Datatable => ({ type: 'datatable', - meta: {}, columns: [{ id: 'value', name: 'value', meta: { type: 'number' } }], rows: [{ value }], }), diff --git a/src/plugins/expressions/common/expression_types/specs/number.ts b/src/plugins/expressions/common/expression_types/specs/number.ts index c5fdacf3408a1..c30d3fe943d42 100644 --- a/src/plugins/expressions/common/expression_types/specs/number.ts +++ b/src/plugins/expressions/common/expression_types/specs/number.ts @@ -55,7 +55,6 @@ export const number: ExpressionTypeDefinition = { }, datatable: (value): Datatable => ({ type: 'datatable', - meta: {}, columns: [{ id: 'value', name: 'value', meta: { type: 'number' } }], rows: [{ value }], }), diff --git a/src/plugins/expressions/common/expression_types/specs/string.ts b/src/plugins/expressions/common/expression_types/specs/string.ts index 3d52707279bfc..0869e21e455f7 100644 --- a/src/plugins/expressions/common/expression_types/specs/string.ts +++ b/src/plugins/expressions/common/expression_types/specs/string.ts @@ -40,7 +40,6 @@ export const string: ExpressionTypeDefinition = { }, datatable: (value): Datatable => ({ type: 'datatable', - meta: {}, columns: [{ id: 'value', name: 'value', meta: { type: 'string' } }], rows: [{ value }], }), diff --git a/src/plugins/home/kibana.json b/src/plugins/home/kibana.json index 74bd3625ca964..81bfc57a00363 100644 --- a/src/plugins/home/kibana.json +++ b/src/plugins/home/kibana.json @@ -3,7 +3,7 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["data", "kibanaLegacy"], + "requiredPlugins": ["data", "urlForwarding"], "optionalPlugins": ["usageCollection", "telemetry"], "requiredBundles": [ "kibanaReact" diff --git a/src/plugins/home/public/application/components/home_app.js b/src/plugins/home/public/application/components/home_app.js index 90e549c873436..69cd68d553d03 100644 --- a/src/plugins/home/public/application/components/home_app.js +++ b/src/plugins/home/public/application/components/home_app.js @@ -32,8 +32,8 @@ import { useMount } from 'react-use'; const RedirectToDefaultApp = () => { useMount(() => { - const { kibanaLegacy } = getServices(); - kibanaLegacy.navigateToDefaultApp(); + const { urlForwarding } = getServices(); + urlForwarding.navigateToDefaultApp(); }); return null; }; diff --git a/src/plugins/home/public/application/components/welcome.tsx b/src/plugins/home/public/application/components/welcome.tsx index cacb507009c70..404185de3d2ea 100644 --- a/src/plugins/home/public/application/components/welcome.tsx +++ b/src/plugins/home/public/application/components/welcome.tsx @@ -76,7 +76,7 @@ export class Welcome extends React.Component { componentDidMount() { const { telemetry } = this.props; this.services.trackUiMetric(METRIC_TYPE.LOADED, 'welcomeScreenMount'); - if (telemetry) { + if (telemetry?.telemetryService.userCanChangeSettings) { telemetry.telemetryNotifications.setOptedInNoticeSeen(); } document.addEventListener('keydown', this.hideOnEsc); @@ -88,7 +88,7 @@ export class Welcome extends React.Component { private renderTelemetryEnabledOrDisabledText = () => { const { telemetry } = this.props; - if (!telemetry) { + if (!telemetry || !telemetry.telemetryService.userCanChangeSettings) { return null; } diff --git a/src/plugins/home/public/application/kibana_services.ts b/src/plugins/home/public/application/kibana_services.ts index 8bd651d038128..74b2bf8d4f6a4 100644 --- a/src/plugins/home/public/application/kibana_services.ts +++ b/src/plugins/home/public/application/kibana_services.ts @@ -29,7 +29,7 @@ import { } from 'kibana/public'; import { UiStatsMetricType } from '@kbn/analytics'; import { TelemetryPluginStart } from '../../../telemetry/public'; -import { KibanaLegacyStart } from '../../../kibana_legacy/public'; +import { UrlForwardingStart } from '../../../url_forwarding/public'; import { TutorialService } from '../services/tutorials'; import { FeatureCatalogueRegistry } from '../services/feature_catalogue'; import { EnvironmentService } from '../services/environment'; @@ -41,7 +41,7 @@ export interface HomeKibanaServices { chrome: ChromeStart; application: ApplicationStart; uiSettings: IUiSettingsClient; - kibanaLegacy: KibanaLegacyStart; + urlForwarding: UrlForwardingStart; homeConfig: ConfigSchema; featureCatalogue: FeatureCatalogueRegistry; http: HttpStart; diff --git a/src/plugins/home/public/plugin.test.ts b/src/plugins/home/public/plugin.test.ts index 0ebba06e6bea9..7b56c6ec89b77 100644 --- a/src/plugins/home/public/plugin.test.ts +++ b/src/plugins/home/public/plugin.test.ts @@ -20,7 +20,7 @@ import { registryMock, environmentMock, tutorialMock } from './plugin.test.mocks'; import { HomePublicPlugin } from './plugin'; import { coreMock } from '../../../core/public/mocks'; -import { kibanaLegacyPluginMock } from '../../kibana_legacy/public/mocks'; +import { urlForwardingPluginMock } from '../../url_forwarding/public/mocks'; const mockInitializerContext = coreMock.createPluginInitializerContext(); @@ -37,7 +37,7 @@ describe('HomePublicPlugin', () => { const setup = await new HomePublicPlugin(mockInitializerContext).setup( coreMock.createSetup() as any, { - kibanaLegacy: kibanaLegacyPluginMock.createSetupContract(), + urlForwarding: urlForwardingPluginMock.createSetupContract(), } ); expect(setup).toHaveProperty('featureCatalogue'); @@ -56,7 +56,7 @@ describe('HomePublicPlugin', () => { const setup = await new HomePublicPlugin(mockInitializerContext).setup( coreMock.createSetup() as any, { - kibanaLegacy: kibanaLegacyPluginMock.createSetupContract(), + urlForwarding: urlForwardingPluginMock.createSetupContract(), } ); expect(setup).toHaveProperty('featureCatalogue'); @@ -73,7 +73,7 @@ describe('HomePublicPlugin', () => { const setup = await new HomePublicPlugin(mockInitializerContext).setup( coreMock.createSetup() as any, { - kibanaLegacy: kibanaLegacyPluginMock.createSetupContract(), + urlForwarding: urlForwardingPluginMock.createSetupContract(), } ); expect(setup).toHaveProperty('featureCatalogue'); @@ -84,7 +84,7 @@ describe('HomePublicPlugin', () => { const setup = await new HomePublicPlugin(mockInitializerContext).setup( coreMock.createSetup() as any, { - kibanaLegacy: kibanaLegacyPluginMock.createSetupContract(), + urlForwarding: urlForwardingPluginMock.createSetupContract(), } ); expect(setup).toHaveProperty('environment'); @@ -95,7 +95,7 @@ describe('HomePublicPlugin', () => { const setup = await new HomePublicPlugin(mockInitializerContext).setup( coreMock.createSetup() as any, { - kibanaLegacy: kibanaLegacyPluginMock.createSetupContract(), + urlForwarding: urlForwardingPluginMock.createSetupContract(), } ); expect(setup).toHaveProperty('tutorials'); diff --git a/src/plugins/home/public/plugin.ts b/src/plugins/home/public/plugin.ts index ba2f537e7c5de..b62ceae3d0d37 100644 --- a/src/plugins/home/public/plugin.ts +++ b/src/plugins/home/public/plugin.ts @@ -41,19 +41,19 @@ import { setServices } from './application/kibana_services'; import { DataPublicPluginStart } from '../../data/public'; import { TelemetryPluginStart } from '../../telemetry/public'; import { UsageCollectionSetup } from '../../usage_collection/public'; -import { KibanaLegacySetup, KibanaLegacyStart } from '../../kibana_legacy/public'; +import { UrlForwardingSetup, UrlForwardingStart } from '../../url_forwarding/public'; import { AppNavLinkStatus } from '../../../core/public'; import { PLUGIN_ID, HOME_APP_BASE_PATH } from '../common/constants'; export interface HomePluginStartDependencies { data: DataPublicPluginStart; telemetry?: TelemetryPluginStart; - kibanaLegacy: KibanaLegacyStart; + urlForwarding: UrlForwardingStart; } export interface HomePluginSetupDependencies { usageCollection?: UsageCollectionSetup; - kibanaLegacy: KibanaLegacySetup; + urlForwarding: UrlForwardingSetup; } export class HomePublicPlugin @@ -67,7 +67,7 @@ export class HomePublicPlugin public setup( core: CoreSetup, - { kibanaLegacy, usageCollection }: HomePluginSetupDependencies + { urlForwarding, usageCollection }: HomePluginSetupDependencies ): HomePublicPluginSetup { core.application.register({ id: PLUGIN_ID, @@ -79,7 +79,7 @@ export class HomePublicPlugin : () => {}; const [ coreStart, - { telemetry, data, kibanaLegacy: kibanaLegacyStart }, + { telemetry, data, urlForwarding: urlForwardingStart }, ] = await core.getStartServices(); setServices({ trackUiMetric, @@ -97,7 +97,7 @@ export class HomePublicPlugin getBasePath: core.http.basePath.get, indexPatternService: data.indexPatterns, environmentService: this.environmentService, - kibanaLegacy: kibanaLegacyStart, + urlForwarding: urlForwardingStart, homeConfig: this.initializerContext.config.get(), tutorialService: this.tutorialService, featureCatalogue: this.featuresCatalogueRegistry, @@ -109,7 +109,7 @@ export class HomePublicPlugin return await renderApp(params.element, coreStart, params.history); }, }); - kibanaLegacy.forwardApp('home', 'home'); + urlForwarding.forwardApp('home', 'home'); const featureCatalogue = { ...this.featuresCatalogueRegistry.setup() }; @@ -170,7 +170,7 @@ export class HomePublicPlugin public start( { application: { capabilities, currentAppId$ }, http }: CoreStart, - { kibanaLegacy }: HomePluginStartDependencies + { urlForwarding }: HomePluginStartDependencies ) { this.featuresCatalogueRegistry.start({ capabilities }); @@ -184,7 +184,7 @@ export class HomePublicPlugin if (appId === 'home') { // ...navigate to default app set by `kibana.defaultAppId`. // This doesn't do anything as along as the default settings are kept. - kibanaLegacy.navigateToDefaultApp({ overwriteHash: false }); + urlForwarding.navigateToDefaultApp({ overwriteHash: false }); } }); } diff --git a/src/plugins/index_pattern_management/kibana.json b/src/plugins/index_pattern_management/kibana.json index d0ad6a96065c3..6c3025485bbd7 100644 --- a/src/plugins/index_pattern_management/kibana.json +++ b/src/plugins/index_pattern_management/kibana.json @@ -3,6 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["management", "data", "kibanaLegacy"], + "requiredPlugins": ["management", "data", "urlForwarding"], "requiredBundles": ["kibanaReact", "kibanaUtils"] } diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx index 22bc78ee0538e..13be9ca6c9c25 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/create_edit_field/create_edit_field.tsx @@ -44,7 +44,7 @@ const newFieldPlaceholder = i18n.translate( export const CreateEditField = withRouter( ({ indexPattern, mode, fieldName, history }: CreateEditFieldProps) => { - const { uiSettings, chrome, notifications } = useKibana< + const { uiSettings, chrome, notifications, data } = useKibana< IndexPatternManagmentContext >().services; const spec = @@ -96,6 +96,7 @@ export const CreateEditField = withRouter( indexPattern={indexPattern} spec={spec} services={{ + saveIndexPattern: data.indexPatterns.save.bind(data.indexPatterns), redirectAway, }} /> diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx index a0eecef66ff93..d09836019b0bc 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/edit_index_pattern.tsx @@ -234,7 +234,13 @@ export const EditIndexPattern = withRouter( )} - +
); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx index ed50317aed6a0..84469a7e1fbd9 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_field_table.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { ScriptedFieldsTable } from '../scripted_fields_table'; -import { IIndexPattern } from '../../../../../../plugins/data/common/index_patterns'; +import { IIndexPattern, IndexPattern } from '../../../../../../plugins/data/common/index_patterns'; jest.mock('@elastic/eui', () => ({ EuiTitle: 'eui-title', @@ -54,7 +54,7 @@ const helpers = { const getIndexPatternMock = (mockedFields: any = {}) => ({ ...mockedFields } as IIndexPattern); describe('ScriptedFieldsTable', () => { - let indexPattern: IIndexPattern; + let indexPattern: IndexPattern; beforeEach(() => { indexPattern = getIndexPatternMock({ @@ -62,7 +62,7 @@ describe('ScriptedFieldsTable', () => { { name: 'ScriptedField', lang: 'painless', script: 'x++' }, { name: 'JustATest', lang: 'painless', script: 'z++' }, ], - }); + }) as IndexPattern; }); test('should render normally', async () => { @@ -71,6 +71,7 @@ describe('ScriptedFieldsTable', () => { indexPattern={indexPattern} helpers={helpers} painlessDocLink={'painlessDoc'} + saveIndexPattern={async () => {}} /> ); @@ -88,6 +89,7 @@ describe('ScriptedFieldsTable', () => { indexPattern={indexPattern} helpers={helpers} painlessDocLink={'painlessDoc'} + saveIndexPattern={async () => {}} /> ); @@ -105,15 +107,18 @@ describe('ScriptedFieldsTable', () => { test('should filter based on the lang filter', async () => { const component = shallow( [ - { name: 'ScriptedField', lang: 'painless', script: 'x++' }, - { name: 'JustATest', lang: 'painless', script: 'z++' }, - { name: 'Bad', lang: 'somethingElse', script: 'z++' }, - ], - })} + indexPattern={ + getIndexPatternMock({ + getScriptedFields: () => [ + { name: 'ScriptedField', lang: 'painless', script: 'x++' }, + { name: 'JustATest', lang: 'painless', script: 'z++' }, + { name: 'Bad', lang: 'somethingElse', script: 'z++' }, + ], + }) as IndexPattern + } painlessDocLink={'painlessDoc'} helpers={helpers} + saveIndexPattern={async () => {}} /> ); @@ -131,11 +136,14 @@ describe('ScriptedFieldsTable', () => { test('should hide the table if there are no scripted fields', async () => { const component = shallow( [], - })} + indexPattern={ + getIndexPatternMock({ + getScriptedFields: () => [], + }) as IndexPattern + } painlessDocLink={'painlessDoc'} helpers={helpers} + saveIndexPattern={async () => {}} /> ); @@ -153,6 +161,7 @@ describe('ScriptedFieldsTable', () => { indexPattern={indexPattern} helpers={helpers} painlessDocLink={'painlessDoc'} + saveIndexPattern={async () => {}} /> ); @@ -168,12 +177,15 @@ describe('ScriptedFieldsTable', () => { const removeScriptedField = jest.fn(); const component = shallow( {}} /> ); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx index 532af2757915b..08cc90faf75fa 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/scripted_fields_table/scripted_fields_table.tsx @@ -27,10 +27,10 @@ import { import { Table, Header, CallOuts, DeleteScritpedFieldConfirmationModal } from './components'; import { ScriptedFieldItem } from './types'; -import { IIndexPattern } from '../../../../../../plugins/data/public'; +import { IndexPattern, DataPublicPluginStart } from '../../../../../../plugins/data/public'; interface ScriptedFieldsTableProps { - indexPattern: IIndexPattern; + indexPattern: IndexPattern; fieldFilter?: string; scriptedFieldLanguageFilter?: string; helpers: { @@ -39,6 +39,7 @@ interface ScriptedFieldsTableProps { }; onRemoveField?: () => void; painlessDocLink: string; + saveIndexPattern: DataPublicPluginStart['indexPatterns']['save']; } interface ScriptedFieldsTableState { @@ -68,7 +69,7 @@ export class ScriptedFieldsTable extends Component< } fetchFields = async () => { - const fields = await this.props.indexPattern.getScriptedFields(); + const fields = await (this.props.indexPattern.getScriptedFields() as ScriptedFieldItem[]); const deprecatedLangsInUse = []; const deprecatedLangs = getDeprecatedScriptingLanguages(); @@ -121,10 +122,11 @@ export class ScriptedFieldsTable extends Component< }; deleteField = () => { - const { indexPattern, onRemoveField } = this.props; + const { indexPattern, onRemoveField, saveIndexPattern } = this.props; const { fieldToDelete } = this.state; - indexPattern.removeScriptedField(fieldToDelete); + indexPattern.removeScriptedField(fieldToDelete!.name); + saveIndexPattern(indexPattern); if (onRemoveField) { onRemoveField(); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/__snapshots__/source_filters_table.test.tsx.snap b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/__snapshots__/source_filters_table.test.tsx.snap index a7b73624c4665..6a2b208c47987 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/__snapshots__/source_filters_table.test.tsx.snap +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/__snapshots__/source_filters_table.test.tsx.snap @@ -14,17 +14,6 @@ exports[`SourceFiltersTable should add a filter 1`] = ` fieldWildcardMatcher={[Function]} indexPattern={ Object { - "save": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, "sourceFilters": Array [ Object { "value": "tim*", @@ -108,17 +97,6 @@ exports[`SourceFiltersTable should remove a filter 1`] = ` fieldWildcardMatcher={[Function]} indexPattern={ Object { - "save": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, "sourceFilters": Array [ Object { "clientId": 2, @@ -279,17 +257,6 @@ exports[`SourceFiltersTable should update a filter 1`] = ` fieldWildcardMatcher={[Function]} indexPattern={ Object { - "save": [MockFunction] { - "calls": Array [ - Array [], - ], - "results": Array [ - Object { - "type": "return", - "value": undefined, - }, - ], - }, "sourceFilters": Array [ Object { "clientId": 1, diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.test.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.test.tsx index fa048af7c7a70..395e1f3744e94 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.test.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { SourceFiltersTable } from './source_filters_table'; -import { IIndexPattern } from 'src/plugins/data/public'; +import { IndexPattern } from 'src/plugins/data/public'; jest.mock('@elastic/eui', () => ({ EuiButton: 'eui-button', @@ -52,7 +52,7 @@ const getIndexPatternMock = (mockedFields: any = {}) => ({ sourceFilters: [{ value: 'time*' }, { value: 'nam*' }, { value: 'age*' }], ...mockedFields, - } as IIndexPattern); + } as IndexPattern); describe('SourceFiltersTable', () => { test('should render normally', () => { @@ -61,6 +61,7 @@ describe('SourceFiltersTable', () => { indexPattern={getIndexPatternMock()} fieldWildcardMatcher={() => {}} filterFilter={''} + saveIndexPattern={async () => {}} /> ); @@ -73,6 +74,7 @@ describe('SourceFiltersTable', () => { indexPattern={getIndexPatternMock()} fieldWildcardMatcher={() => {}} filterFilter={''} + saveIndexPattern={async () => {}} /> ); @@ -88,6 +90,7 @@ describe('SourceFiltersTable', () => { })} filterFilter={''} fieldWildcardMatcher={() => {}} + saveIndexPattern={async () => {}} /> ); @@ -98,11 +101,14 @@ describe('SourceFiltersTable', () => { test('should show a delete modal', () => { const component = shallow( {}} + saveIndexPattern={async () => {}} /> ); @@ -112,15 +118,17 @@ describe('SourceFiltersTable', () => { }); test('should remove a filter', async () => { - const save = jest.fn(); + const saveIndexPattern = jest.fn(async () => {}); const component = shallow( {}} + saveIndexPattern={saveIndexPattern} /> ); @@ -129,47 +137,49 @@ describe('SourceFiltersTable', () => { await component.instance().deleteFilter(); component.update(); // We are not calling `.setState` directly so we need to re-render - expect(save).toBeCalled(); + expect(saveIndexPattern).toBeCalled(); expect(component).toMatchSnapshot(); }); test('should add a filter', async () => { - const save = jest.fn(); + const saveIndexPattern = jest.fn(async () => {}); const component = shallow( {}} + saveIndexPattern={saveIndexPattern} /> ); await component.instance().onAddFilter('na*'); component.update(); // We are not calling `.setState` directly so we need to re-render - expect(save).toBeCalled(); + expect(saveIndexPattern).toBeCalled(); expect(component).toMatchSnapshot(); }); test('should update a filter', async () => { - const save = jest.fn(); + const saveIndexPattern = jest.fn(async () => {}); const component = shallow( {}} + saveIndexPattern={saveIndexPattern} /> ); await component.instance().saveFilter({ clientId: 'tim*', value: 'ti*' }); component.update(); // We are not calling `.setState` directly so we need to re-render - expect(save).toBeCalled(); + expect(saveIndexPattern).toBeCalled(); expect(component).toMatchSnapshot(); }); }); diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx index e5c753886ea9f..b00648f124716 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/source_filters_table/source_filters_table.tsx @@ -22,14 +22,15 @@ import { createSelector } from 'reselect'; import { EuiSpacer } from '@elastic/eui'; import { AddFilter, Table, Header, DeleteFilterConfirmationModal } from './components'; -import { IIndexPattern } from '../../../../../../plugins/data/public'; +import { IndexPattern, DataPublicPluginStart } from '../../../../../../plugins/data/public'; import { SourceFiltersTableFilter } from './types'; export interface SourceFiltersTableProps { - indexPattern: IIndexPattern; + indexPattern: IndexPattern; filterFilter: string; fieldWildcardMatcher: Function; onAddOrRemoveFilter?: Function; + saveIndexPattern: DataPublicPluginStart['indexPatterns']['save']; } export interface SourceFiltersTableState { @@ -104,7 +105,7 @@ export class SourceFiltersTable extends Component< }; deleteFilter = async () => { - const { indexPattern, onAddOrRemoveFilter } = this.props; + const { indexPattern, onAddOrRemoveFilter, saveIndexPattern } = this.props; const { filterToDelete, filters } = this.state; indexPattern.sourceFilters = filters.filter((filter) => { @@ -112,7 +113,7 @@ export class SourceFiltersTable extends Component< }); this.setState({ isSaving: true }); - await indexPattern.save(); + await saveIndexPattern(indexPattern); if (onAddOrRemoveFilter) { onAddOrRemoveFilter(); @@ -124,12 +125,12 @@ export class SourceFiltersTable extends Component< }; onAddFilter = async (value: string) => { - const { indexPattern, onAddOrRemoveFilter } = this.props; + const { indexPattern, onAddOrRemoveFilter, saveIndexPattern } = this.props; indexPattern.sourceFilters = [...(indexPattern.sourceFilters || []), { value }]; this.setState({ isSaving: true }); - await indexPattern.save(); + await saveIndexPattern(indexPattern); if (onAddOrRemoveFilter) { onAddOrRemoveFilter(); @@ -140,7 +141,7 @@ export class SourceFiltersTable extends Component< }; saveFilter = async ({ clientId, value }: SourceFiltersTableFilter) => { - const { indexPattern } = this.props; + const { indexPattern, saveIndexPattern } = this.props; const { filters } = this.state; indexPattern.sourceFilters = filters.map((filter) => { @@ -155,7 +156,7 @@ export class SourceFiltersTable extends Component< }); this.setState({ isSaving: true }); - await indexPattern.save(); + await saveIndexPattern(indexPattern); this.updateFilters(); this.setState({ isSaving: false }); }; diff --git a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx index 3bc9cd34f2984..101399ef02b73 100644 --- a/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx +++ b/src/plugins/index_pattern_management/public/components/edit_index_pattern/tabs/tabs.tsx @@ -35,6 +35,7 @@ import { IndexPattern, IndexPatternField, UI_SETTINGS, + DataPublicPluginStart, } from '../../../../../../plugins/data/public'; import { useKibana } from '../../../../../../plugins/kibana_react/public'; import { IndexPatternManagmentContext } from '../../../types'; @@ -48,6 +49,7 @@ import { getTabs, getPath, convertToEuiSelectOption } from './utils'; interface TabsProps extends Pick { indexPattern: IndexPattern; fields: IndexPatternField[]; + saveIndexPattern: DataPublicPluginStart['indexPatterns']['save']; } const searchAriaLabel = i18n.translate( @@ -71,7 +73,7 @@ const filterPlaceholder = i18n.translate( } ); -export function Tabs({ indexPattern, fields, history, location }: TabsProps) { +export function Tabs({ indexPattern, saveIndexPattern, fields, history, location }: TabsProps) { const { uiSettings, indexPatternManagementStart, docLinks } = useKibana< IndexPatternManagmentContext >().services; @@ -191,6 +193,7 @@ export function Tabs({ indexPattern, fields, history, location }: TabsProps) { + + + } + onChange={[Function]} + /> + {!(format as DurationFormat).isHuman() ? ( - - } - isInvalid={!!error} - error={hasDecimalError ? error : null} - > - { - this.onChange({ outputPrecision: e.target.value ? Number(e.target.value) : null }); - }} + <> + + } isInvalid={!!error} - /> - + error={hasDecimalError ? error : null} + > + { + this.onChange({ + outputPrecision: e.target.value ? Number(e.target.value) : null, + }); + }} + isInvalid={!!error} + /> + + + + } + checked={Boolean(formatParams.showSuffix)} + onChange={(e) => { + this.onChange({ showSuffix: !formatParams.showSuffix }); + }} + /> + + ) : null} diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx index b0385a61a72ac..23f52475d413d 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.test.tsx @@ -94,6 +94,8 @@ const field = { format: new Format(), }; +const services = { redirectAway: () => {}, saveIndexPattern: async () => {} }; + describe('FieldEditor', () => { let indexPattern: IndexPattern; @@ -122,7 +124,7 @@ describe('FieldEditor', () => { { indexPattern, spec: (field as unknown) as IndexPatternField, - services: { redirectAway: () => {} }, + services, }, mockContext ); @@ -151,7 +153,7 @@ describe('FieldEditor', () => { { indexPattern, spec: (testField as unknown) as IndexPatternField, - services: { redirectAway: () => {} }, + services, }, mockContext ); @@ -181,7 +183,7 @@ describe('FieldEditor', () => { { indexPattern, spec: (testField as unknown) as IndexPatternField, - services: { redirectAway: () => {} }, + services, }, mockContext ); @@ -198,7 +200,7 @@ describe('FieldEditor', () => { { indexPattern, spec: (testField as unknown) as IndexPatternField, - services: { redirectAway: () => {} }, + services, }, mockContext ); @@ -223,7 +225,7 @@ describe('FieldEditor', () => { { indexPattern, spec: (testField as unknown) as IndexPatternField, - services: { redirectAway: () => {} }, + services, }, mockContext ); diff --git a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx index 6a3f632a9582e..4857a402cc4b2 100644 --- a/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx +++ b/src/plugins/index_pattern_management/public/components/field_editor/field_editor.tsx @@ -133,6 +133,7 @@ export interface FieldEdiorProps { spec: IndexPatternField['spec']; services: { redirectAway: () => void; + saveIndexPattern: DataPublicPluginStart['indexPatterns']['save']; }; } @@ -757,23 +758,18 @@ export class FieldEditor extends PureComponent { - const { redirectAway } = this.props.services; + const { redirectAway, saveIndexPattern } = this.props.services; const { indexPattern } = this.props; const { spec } = this.state; - const remove = indexPattern.removeScriptedField(spec.name); - - if (remove) { - remove.then(() => { - const message = i18n.translate('indexPatternManagement.deleteField.deletedHeader', { - defaultMessage: "Deleted '{fieldName}'", - values: { fieldName: spec.name }, - }); - this.context.services.notifications.toasts.addSuccess(message); - redirectAway(); + indexPattern.removeScriptedField(spec.name); + saveIndexPattern(indexPattern).then(() => { + const message = i18n.translate('indexPatternManagement.deleteField.deletedHeader', { + defaultMessage: "Deleted '{fieldName}'", + values: { fieldName: spec.name }, }); - } else { + this.context.services.notifications.toasts.addSuccess(message); redirectAway(); - } + }); }; saveField = async () => { @@ -803,7 +799,7 @@ export class FieldEditor extends PureComponent { const message = i18n.translate('indexPatternManagement.deleteField.savedHeader', { defaultMessage: "Saved '{fieldName}'", diff --git a/src/plugins/index_pattern_management/public/mocks.ts b/src/plugins/index_pattern_management/public/mocks.ts index 6a9ef23e3732e..24aea961764a9 100644 --- a/src/plugins/index_pattern_management/public/mocks.ts +++ b/src/plugins/index_pattern_management/public/mocks.ts @@ -20,7 +20,7 @@ import { PluginInitializerContext } from 'src/core/public'; import { coreMock } from '../../../core/public/mocks'; import { managementPluginMock } from '../../management/public/mocks'; -import { kibanaLegacyPluginMock } from '../../kibana_legacy/public/mocks'; +import { urlForwardingPluginMock } from '../../url_forwarding/public/mocks'; import { dataPluginMock } from '../../data/public/mocks'; import { IndexPatternManagementSetup, @@ -65,7 +65,7 @@ const createInstance = async () => { const setup = plugin.setup(coreMock.createSetup(), { management: managementPluginMock.createSetupContract(), - kibanaLegacy: kibanaLegacyPluginMock.createSetupContract(), + urlForwarding: urlForwardingPluginMock.createSetupContract(), }); const doStart = () => plugin.start(coreMock.createStart(), { diff --git a/src/plugins/index_pattern_management/public/plugin.ts b/src/plugins/index_pattern_management/public/plugin.ts index ee1e00fcafd98..cfe0a23eb14dd 100644 --- a/src/plugins/index_pattern_management/public/plugin.ts +++ b/src/plugins/index_pattern_management/public/plugin.ts @@ -20,7 +20,7 @@ import { i18n } from '@kbn/i18n'; import { PluginInitializerContext, CoreSetup, CoreStart, Plugin } from 'src/core/public'; import { DataPublicPluginStart } from 'src/plugins/data/public'; -import { KibanaLegacySetup } from '../../kibana_legacy/public'; +import { UrlForwardingSetup } from '../../url_forwarding/public'; import { IndexPatternManagementService, IndexPatternManagementServiceSetup, @@ -31,7 +31,7 @@ import { ManagementSetup } from '../../management/public'; export interface IndexPatternManagementSetupDependencies { management: ManagementSetup; - kibanaLegacy: KibanaLegacySetup; + urlForwarding: UrlForwardingSetup; } export interface IndexPatternManagementStartDependencies { @@ -62,7 +62,7 @@ export class IndexPatternManagementPlugin public setup( core: CoreSetup, - { management, kibanaLegacy }: IndexPatternManagementSetupDependencies + { management, urlForwarding }: IndexPatternManagementSetupDependencies ) { const kibanaSection = management.sections.section.kibana; @@ -73,8 +73,8 @@ export class IndexPatternManagementPlugin const newAppPath = `management/kibana/${IPM_APP_ID}`; const legacyPatternsPath = 'management/kibana/index_patterns'; - kibanaLegacy.forwardApp('management/kibana/index_pattern', newAppPath, (path) => '/create'); - kibanaLegacy.forwardApp(legacyPatternsPath, newAppPath, (path) => { + urlForwarding.forwardApp('management/kibana/index_pattern', newAppPath, (path) => '/create'); + urlForwarding.forwardApp(legacyPatternsPath, newAppPath, (path) => { const pathInApp = path.substr(legacyPatternsPath.length + 1); return pathInApp && `/patterns${pathInApp}`; }); diff --git a/src/plugins/input_control_vis/public/vis_controller.tsx b/src/plugins/input_control_vis/public/vis_controller.tsx index e4310960851ca..faea98b792291 100644 --- a/src/plugins/input_control_vis/public/vis_controller.tsx +++ b/src/plugins/input_control_vis/public/vis_controller.tsx @@ -18,8 +18,10 @@ */ import React from 'react'; +import { isEqual } from 'lodash'; import { render, unmountComponentAtNode } from 'react-dom'; +import { Subscription } from 'rxjs'; import { I18nStart } from 'kibana/public'; import { InputControlVis } from './components/vis/input_control_vis'; import { getControlFactory } from './control/control_factory'; @@ -34,11 +36,13 @@ import { VisParams, Vis } from '../../visualizations/public'; export const createInputControlVisController = (deps: InputControlVisDependencies) => { return class InputControlVisController { private I18nContext?: I18nStart['Context']; + private isLoaded = false; controls: Array; queryBarUpdateHandler: () => void; filterManager: FilterManager; updateSubsciption: any; + timeFilterSubscription: Subscription; visParams?: VisParams; constructor(public el: Element, public vis: Vis) { @@ -50,19 +54,32 @@ export const createInputControlVisController = (deps: InputControlVisDependencie this.updateSubsciption = this.filterManager .getUpdates$() .subscribe(this.queryBarUpdateHandler); + this.timeFilterSubscription = deps.data.query.timefilter.timefilter + .getTimeUpdate$() + .subscribe(() => { + if (this.visParams?.useTimeFilter) { + this.isLoaded = false; + } + }); } async render(visData: any, visParams: VisParams) { - this.visParams = visParams; - this.controls = []; - this.controls = await this.initControls(); - const [{ i18n }] = await deps.core.getStartServices(); - this.I18nContext = i18n.Context; + if (!this.I18nContext) { + const [{ i18n }] = await deps.core.getStartServices(); + this.I18nContext = i18n.Context; + } + if (!this.isLoaded || !isEqual(visParams, this.visParams)) { + this.visParams = visParams; + this.controls = []; + this.controls = await this.initControls(); + this.isLoaded = true; + } this.drawVis(); } destroy() { this.updateSubsciption.unsubscribe(); + this.timeFilterSubscription.unsubscribe(); unmountComponentAtNode(this.el); this.controls.forEach((control) => control.destroy()); } diff --git a/src/plugins/kibana_legacy/README.md b/src/plugins/kibana_legacy/README.md index 82bf3270589db..d66938cca6d13 100644 --- a/src/plugins/kibana_legacy/README.md +++ b/src/plugins/kibana_legacy/README.md @@ -1,6 +1,7 @@ # kibana-legacy -This plugin will contain several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. +This plugin contains several helpers and services to integrate pieces of the legacy Kibana app with the new Kibana platform. -Currently, the only service offered is the ability to register apps which are rendered in the legacy "kibana" plugin. +This plugin will be removed once all parts of legacy Kibana are removed from other plugins. +All of this plugin should be considered deprecated. New code should never integrate with the services provided from this plugin. \ No newline at end of file diff --git a/src/plugins/kibana_legacy/kibana.json b/src/plugins/kibana_legacy/kibana.json index 79264d95dcc27..e96b4859a36d0 100644 --- a/src/plugins/kibana_legacy/kibana.json +++ b/src/plugins/kibana_legacy/kibana.json @@ -2,6 +2,5 @@ "id": "kibanaLegacy", "version": "kibana", "server": true, - "ui": true, - "extraPublicDirs": ["common", "common/kbn_base_url"] + "ui": true } diff --git a/src/plugins/kibana_legacy/public/index.ts b/src/plugins/kibana_legacy/public/index.ts index 27b940b0a456b..030dfd585fefb 100644 --- a/src/plugins/kibana_legacy/public/index.ts +++ b/src/plugins/kibana_legacy/public/index.ts @@ -24,7 +24,6 @@ export const plugin = (initializerContext: PluginInitializerContext) => new KibanaLegacyPlugin(initializerContext); export * from './plugin'; -export { kbnBaseUrl, migrateLegacyQuery } from '../common'; export { initAngularBootstrap } from './angular_bootstrap'; export { PaginateDirectiveProvider, PaginateControlsDirectiveProvider } from './paginate/paginate'; diff --git a/src/plugins/kibana_legacy/public/mocks.ts b/src/plugins/kibana_legacy/public/mocks.ts index a3cdb2106523c..f3aa015b6000b 100644 --- a/src/plugins/kibana_legacy/public/mocks.ts +++ b/src/plugins/kibana_legacy/public/mocks.ts @@ -22,12 +22,9 @@ import { KibanaLegacyPlugin } from './plugin'; export type Setup = jest.Mocked>; export type Start = jest.Mocked>; -const createSetupContract = (): Setup => ({ - forwardApp: jest.fn(), -}); +const createSetupContract = (): Setup => ({}); const createStartContract = (): Start => ({ - getForwards: jest.fn(), config: { defaultAppId: 'home', }, @@ -35,8 +32,6 @@ const createStartContract = (): Start => ({ turnHideWriteControlsOn: jest.fn(), getHideWriteControls: jest.fn(), }, - navigateToDefaultApp: jest.fn(), - navigateToLegacyKibanaUrl: jest.fn(), loadFontAwesome: jest.fn(), }); diff --git a/src/plugins/kibana_legacy/public/plugin.ts b/src/plugins/kibana_legacy/public/plugin.ts index 59ce88c07f4f4..8e62411fc34e9 100644 --- a/src/plugins/kibana_legacy/public/plugin.ts +++ b/src/plugins/kibana_legacy/public/plugin.ts @@ -18,78 +18,18 @@ */ import { PluginInitializerContext, CoreStart, CoreSetup } from 'kibana/public'; -import { Subscription } from 'rxjs'; import { ConfigSchema } from '../config'; import { getDashboardConfig } from './dashboard_config'; -import { navigateToDefaultApp } from './navigate_to_default_app'; -import { createLegacyUrlForwardApp } from './forward_app'; import { injectHeaderStyle } from './utils/inject_header_style'; -import { navigateToLegacyKibanaUrl } from './forward_app/navigate_to_legacy_kibana_url'; - -export interface ForwardDefinition { - legacyAppId: string; - newAppId: string; - rewritePath: (legacyPath: string) => string; -} export class KibanaLegacyPlugin { - private forwardDefinitions: ForwardDefinition[] = []; - private currentAppId: string | undefined; - private currentAppIdSubscription: Subscription | undefined; - constructor(private readonly initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup<{}, KibanaLegacyStart>) { - core.application.register(createLegacyUrlForwardApp(core, this.forwardDefinitions)); - return { - /** - * Forwards URLs within the legacy `kibana` app to a new platform application. - * - * @param legacyAppId The name of the old app to forward URLs from - * @param newAppId The name of the new app that handles the URLs now - * @param rewritePath Function to rewrite the legacy sub path of the app to the new path in the core app. - * If none is provided, it will just strip the prefix of the legacyAppId away - * - * path into the new path - * - * Example usage: - * ``` - * kibanaLegacy.forwardApp( - * 'old', - * 'new', - * path => { - * const [, id] = /old/item\/(.*)$/.exec(path) || []; - * if (!id) { - * return '#/home'; - * } - * return '#/items/${id}'; - * } - * ); - * ``` - * This will cause the following redirects: - * - * * app/kibana#/old/ -> app/new#/home - * * app/kibana#/old/item/123 -> app/new#/items/123 - * - */ - forwardApp: ( - legacyAppId: string, - newAppId: string, - rewritePath?: (legacyPath: string) => string - ) => { - this.forwardDefinitions.push({ - legacyAppId, - newAppId, - rewritePath: rewritePath || ((path) => `#${path.replace(`/${legacyAppId}`, '') || '/'}`), - }); - }, - }; + return {}; } public start({ application, http: { basePath }, uiSettings }: CoreStart) { - this.currentAppIdSubscription = application.currentAppId$.subscribe((currentAppId) => { - this.currentAppId = currentAppId; - }); injectHeaderStyle(uiSettings); return { /** @@ -97,31 +37,6 @@ export class KibanaLegacyPlugin { * @deprecated */ dashboardConfig: getDashboardConfig(!application.capabilities.dashboard.showWriteControls), - /** - * Navigates to the app defined as kibana.defaultAppId. - * This takes redirects into account and uses the right mechanism to navigate. - */ - navigateToDefaultApp: ( - { overwriteHash }: { overwriteHash: boolean } = { overwriteHash: true } - ) => { - navigateToDefaultApp( - this.initializerContext.config.get().defaultAppId, - this.forwardDefinitions, - application, - basePath, - this.currentAppId, - overwriteHash - ); - }, - /** - * Resolves the provided hash using the registered forwards and navigates to the target app. - * If a navigation happened, `{ navigated: true }` will be returned. - * If no matching forward is found, `{ navigated: false }` will be returned. - * @param hash - */ - navigateToLegacyKibanaUrl: (hash: string) => { - return navigateToLegacyKibanaUrl(hash, this.forwardDefinitions, basePath, application); - }, /** * Loads the font-awesome icon font. Should be removed once the last consumer has migrated to EUI * @deprecated @@ -129,11 +44,6 @@ export class KibanaLegacyPlugin { loadFontAwesome: async () => { await import('./font_awesome'); }, - /** - * @deprecated - * Just exported for wiring up with legacy platform, should not be used. - */ - getForwards: () => this.forwardDefinitions, /** * @deprecated * Just exported for wiring up with dashboard mode, should not be used. @@ -141,12 +51,6 @@ export class KibanaLegacyPlugin { config: this.initializerContext.config.get(), }; } - - public stop() { - if (this.currentAppIdSubscription) { - this.currentAppIdSubscription.unsubscribe(); - } - } } export type KibanaLegacySetup = ReturnType; diff --git a/src/plugins/kibana_legacy/public/utils/index.ts b/src/plugins/kibana_legacy/public/utils/index.ts index a32cd5e40a047..590a75ffeed9e 100644 --- a/src/plugins/kibana_legacy/public/utils/index.ts +++ b/src/plugins/kibana_legacy/public/utils/index.ts @@ -18,7 +18,6 @@ */ export * from './system_api'; -export * from './normalize_path'; // @ts-ignore export { KbnAccessibleClickProvider } from './kbn_accessible_click'; // @ts-ignore diff --git a/src/plugins/kibana_legacy/server/index.ts b/src/plugins/kibana_legacy/server/index.ts index 3ddcac1517f74..c447f44c16a89 100644 --- a/src/plugins/kibana_legacy/server/index.ts +++ b/src/plugins/kibana_legacy/server/index.ts @@ -50,8 +50,6 @@ export const config: PluginConfigDescriptor = { ], }; -export { kbnBaseUrl, migrateLegacyQuery } from '../common'; - class Plugin { public setup(core: CoreSetup) {} diff --git a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts index 6efe872553583..2e79cdaa7fc6b 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/application_usage/schema.ts @@ -66,6 +66,7 @@ export const applicationUsageSchema = { csm: commonSchema, canvas: commonSchema, dashboard_mode: commonSchema, // It's a forward app so we'll likely never report it + enterpriseSearch: commonSchema, appSearch: commonSchema, workplaceSearch: commonSchema, graph: commonSchema, diff --git a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts index 359d3a396665d..a527d4d03c6fc 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/index.test.ts @@ -39,6 +39,7 @@ describe('telemetry_ops_stats', () => { const callCluster = jest.fn(); const metric: OpsMetrics = { + collected_at: new Date('2020-01-01 01:00:00'), process: { memory: { heap: { diff --git a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/ops_stats_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/ops_stats_collector.ts index 6e8b71d675f7b..d3be601540582 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ops_stats/ops_stats_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ops_stats/ops_stats_collector.ts @@ -18,13 +18,13 @@ */ import { Observable } from 'rxjs'; -import { cloneDeep } from 'lodash'; +import { cloneDeep, omit } from 'lodash'; import moment from 'moment'; import { OpsMetrics } from 'kibana/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { KIBANA_STATS_TYPE } from '../../../common/constants'; -interface OpsStatsMetrics extends Omit { +interface OpsStatsMetrics extends Omit { timestamp: string; response_times: { average: number; @@ -52,9 +52,9 @@ export function getOpsStatsCollector( // @ts-expect-error delete metrics.requests.statusCodes; lastMetrics = { - ...metrics, + ...omit(metrics, ['collected_at']), response_times: responseTimes, - timestamp: moment.utc().toISOString(), + timestamp: moment.utc(metrics.collected_at).toISOString(), }; }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts index fca685ef4b805..d6f40a2a6867f 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/index.test.ts @@ -73,6 +73,11 @@ describe('telemetry_ui_metric', () => { { id: 'testAppName:testKeyName1', attributes: { count: 3 } }, { id: 'testAppName:testKeyName2', attributes: { count: 5 } }, { id: 'testAppName2:testKeyName3', attributes: { count: 1 } }, + { + id: + 'kibana-user_agent:Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:80.0) Gecko/20100101 Firefox/80.0', + attributes: { count: 1 }, + }, ], total: 3, } as any; @@ -86,6 +91,12 @@ describe('telemetry_ui_metric', () => { { key: 'testKeyName2', value: 5 }, ], testAppName2: [{ key: 'testKeyName3', value: 1 }], + 'kibana-user_agent': [ + { + key: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:80.0) Gecko/20100101 Firefox/80.0', + value: 1, + }, + ], }); }); }); diff --git a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts index ec2f1bfdfc25f..46768813b1970 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/ui_metric/telemetry_ui_metric_collector.ts @@ -66,9 +66,9 @@ export function registerUiMetricUsageCollector( attributes: { count }, } = rawUiMetric; - const [appName, metricType] = id.split(':'); + const [appName, ...metricType] = id.split(':'); - const pair = { key: metricType, value: count }; + const pair = { key: metricType.join(':'), value: count }; return { ...accum, [appName]: [...(accum[appName] || []), pair], diff --git a/src/plugins/management/kibana.json b/src/plugins/management/kibana.json index 1a9e6be46bd55..6c8574f024229 100644 --- a/src/plugins/management/kibana.json +++ b/src/plugins/management/kibana.json @@ -3,7 +3,6 @@ "version": "kibana", "server": true, "ui": true, - "requiredPlugins": ["kibanaLegacy"], "optionalPlugins": ["home"], "requiredBundles": ["kibanaReact", "kibanaUtils", "home"] } diff --git a/src/plugins/management/public/plugin.ts b/src/plugins/management/public/plugin.ts index cd65b7adfeadd..794bbc0d0613b 100644 --- a/src/plugins/management/public/plugin.ts +++ b/src/plugins/management/public/plugin.ts @@ -18,6 +18,7 @@ */ import { i18n } from '@kbn/i18n'; +import { BehaviorSubject } from 'rxjs'; import { ManagementSetup, ManagementStart } from './types'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public'; import { @@ -27,6 +28,9 @@ import { DEFAULT_APP_CATEGORIES, PluginInitializerContext, AppMountParameters, + AppUpdater, + AppStatus, + AppNavLinkStatus, } from '../../../core/public'; import { @@ -41,6 +45,8 @@ interface ManagementSetupDependencies { export class ManagementPlugin implements Plugin { private readonly managementSections = new ManagementSectionsService(); + private readonly appUpdater = new BehaviorSubject(() => ({})); + constructor(private initializerContext: PluginInitializerContext) {} public setup(core: CoreSetup, { home }: ManagementSetupDependencies) { @@ -70,6 +76,7 @@ export class ManagementPlugin implements Plugin section.getAppsEnabled().length > 0); + + if (!hasAnyEnabledApps) { + this.appUpdater.next(() => { + return { + status: AppStatus.inaccessible, + navLinkStatus: AppNavLinkStatus.hidden, + }; + }); + } + return {}; } } diff --git a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx index a1a40b49cc8f0..b284c60bac5de 100644 --- a/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx +++ b/src/plugins/navigation/public/top_nav_menu/top_nav_menu.tsx @@ -23,39 +23,44 @@ import classNames from 'classnames'; import { MountPoint } from '../../../../core/public'; import { MountPointPortal } from '../../../kibana_react/public'; -import { StatefulSearchBarProps, DataPublicPluginStart } from '../../../data/public'; +import { + StatefulSearchBarProps, + DataPublicPluginStart, + SearchBarProps, +} from '../../../data/public'; import { TopNavMenuData } from './top_nav_menu_data'; import { TopNavMenuItem } from './top_nav_menu_item'; -export type TopNavMenuProps = StatefulSearchBarProps & { - config?: TopNavMenuData[]; - showSearchBar?: boolean; - showQueryBar?: boolean; - showQueryInput?: boolean; - showDatePicker?: boolean; - showFilterBar?: boolean; - data?: DataPublicPluginStart; - className?: string; - /** - * If provided, the menu part of the component will be rendered as a portal inside the given mount point. - * - * This is meant to be used with the `setHeaderActionMenu` core API. - * - * @example - * ```ts - * export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => { - * const topNavConfig = ...; // TopNavMenuProps - * return ( - * - * - * - * - * ) - * } - * ``` - */ - setMenuMountPoint?: (menuMount: MountPoint | undefined) => void; -}; +export type TopNavMenuProps = StatefulSearchBarProps & + Omit & { + config?: TopNavMenuData[]; + showSearchBar?: boolean; + showQueryBar?: boolean; + showQueryInput?: boolean; + showDatePicker?: boolean; + showFilterBar?: boolean; + data?: DataPublicPluginStart; + className?: string; + /** + * If provided, the menu part of the component will be rendered as a portal inside the given mount point. + * + * This is meant to be used with the `setHeaderActionMenu` core API. + * + * @example + * ```ts + * export renderApp = ({ element, history, setHeaderActionMenu }: AppMountParameters) => { + * const topNavConfig = ...; // TopNavMenuProps + * return ( + * + * + * + * + * ) + * } + * ``` + */ + setMenuMountPoint?: (menuMount: MountPoint | undefined) => void; + }; /* * Top Nav Menu is a convenience wrapper component for: diff --git a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx index 3b9efbee22ba6..9cdef8b9392bb 100644 --- a/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx +++ b/src/plugins/saved_objects/public/save_modal/saved_object_save_modal.tsx @@ -294,7 +294,7 @@ export class SavedObjectSaveModal extends React.Component id="savedObjects.saveModal.duplicateTitleDescription" defaultMessage="Saving '{title}' creates a duplicate title." values={{ - title: this.props.title, + title: this.state.title, }} />

diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap index 9ad82723c1161..0a330d074fd42 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/flyout.test.tsx.snap @@ -122,6 +122,8 @@ exports[`Flyout conflicts should allow conflict resolution 1`] = ` grow={false} > @@ -420,6 +422,8 @@ exports[`Flyout legacy conflicts should allow conflict resolution 1`] = ` grow={false} > @@ -553,7 +557,7 @@ exports[`Flyout should render import step 1`] = ` hasEmptyLabelSpace={false} label={ @@ -603,6 +607,8 @@ exports[`Flyout should render import step 1`] = ` grow={false} > diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/header.test.tsx.snap b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/header.test.tsx.snap index 642a5030e4ec0..038e1aaf2d8f5 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/header.test.tsx.snap +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/__snapshots__/header.test.tsx.snap @@ -92,7 +92,7 @@ exports[`Header should render normally 1`] = ` color="subdued" > diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx index 32462e1e2184d..cc9d2ed160241 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.test.tsx @@ -267,6 +267,10 @@ describe('Flyout', () => { expect(component.state('status')).toBe('success'); expect(component.find('EuiFlyout ImportSummary')).toMatchSnapshot(); + const cancelButton = await component.find( + 'EuiButtonEmpty[data-test-subj="importSavedObjectsCancelBtn"]' + ); + expect(cancelButton.prop('disabled')).toBe(true); }); }); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx index eddca18f9e283..47d82077294cc 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/flyout.tsx @@ -729,7 +729,7 @@ export class Flyout extends Component { label={ } > @@ -756,7 +756,7 @@ export class Flyout extends Component { } renderFooter() { - const { status } = this.state; + const { isLegacyFile, status } = this.state; const { done, close } = this.props; let confirmButton; @@ -773,7 +773,7 @@ export class Flyout extends Component { } else if (this.hasUnmatchedReferences) { confirmButton = ( { } else { confirmButton = ( { return ( - +

diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx index ac8099893d00e..4000d620465a8 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_mode_control.tsx @@ -51,8 +51,7 @@ const createNewCopiesDisabled = { tooltip: i18n.translate( 'savedObjectsManagement.objectsTable.importModeControl.createNewCopies.disabledText', { - defaultMessage: - 'Check if each object was previously copied or imported into the destination space.', + defaultMessage: 'Check if objects were previously copied or imported.', } ), }; @@ -64,21 +63,23 @@ const createNewCopiesEnabled = { ), tooltip: i18n.translate( 'savedObjectsManagement.objectsTable.importModeControl.createNewCopies.enabledText', - { defaultMessage: 'All imported objects will be created with new random IDs.' } + { + defaultMessage: 'Use this option to create one or more copies of the object.', + } ), }; const overwriteEnabled = { id: 'overwriteEnabled', label: i18n.translate( 'savedObjectsManagement.objectsTable.importModeControl.overwrite.enabledLabel', - { defaultMessage: 'Automatically try to overwrite conflicts' } + { defaultMessage: 'Automatically overwrite conflicts' } ), }; const overwriteDisabled = { id: 'overwriteDisabled', label: i18n.translate( 'savedObjectsManagement.objectsTable.importModeControl.overwrite.disabledLabel', - { defaultMessage: 'Request action when conflict occurs' } + { defaultMessage: 'Request action on conflict' } ), }; const importOptionsTitle = i18n.translate( diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.test.tsx index ed65131b0fc6b..20ac5a903ef22 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.test.tsx @@ -70,7 +70,7 @@ describe('ImportSummary', () => { const wrapper = shallowWithI18nProvider(); expect(findHeader(wrapper).childAt(0).props()).toEqual( - expect.objectContaining({ values: { importCount: 1 } }) + expect.not.objectContaining({ values: expect.anything() }) // no importCount for singular ); const countCreated = findCountCreated(wrapper); expect(countCreated).toHaveLength(1); @@ -90,7 +90,7 @@ describe('ImportSummary', () => { const wrapper = shallowWithI18nProvider(); expect(findHeader(wrapper).childAt(0).props()).toEqual( - expect.objectContaining({ values: { importCount: 1 } }) + expect.not.objectContaining({ values: expect.anything() }) // no importCount for singular ); expect(findCountCreated(wrapper)).toHaveLength(0); const countOverwritten = findCountOverwritten(wrapper); @@ -110,7 +110,7 @@ describe('ImportSummary', () => { const wrapper = shallowWithI18nProvider(); expect(findHeader(wrapper).childAt(0).props()).toEqual( - expect.objectContaining({ values: { importCount: 1 } }) + expect.not.objectContaining({ values: expect.anything() }) // no importCount for singular ); expect(findCountCreated(wrapper)).toHaveLength(0); expect(findCountOverwritten(wrapper)).toHaveLength(0); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx index 7949f7d18d350..e2ce3c3695b17 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/import_summary.tsx @@ -141,7 +141,7 @@ const getCountIndicators = (importItems: ImportItem[]) => { ); }; -const getStatusIndicator = ({ outcome, errorMessage }: ImportItem) => { +const getStatusIndicator = ({ outcome, errorMessage = 'Error' }: ImportItem) => { switch (outcome) { case 'created': return ( @@ -168,8 +168,8 @@ const getStatusIndicator = ({ outcome, errorMessage }: ImportItem) => { type={'alert'} color={'danger'} content={i18n.translate('savedObjectsManagement.importSummary.errorOutcomeLabel', { - defaultMessage: 'Error{message}', - values: { message: errorMessage ? `: ${errorMessage}` : '' }, + defaultMessage: '{errorMessage}', + values: { errorMessage }, })} /> ); @@ -194,11 +194,18 @@ export const ImportSummary = ({ failedImports, successfulImports }: ImportSummar } >

- + {importItems.length === 1 ? ( + + ) : ( + + )}

diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.test.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.test.tsx index c93bc9e5038df..7576b62552aa2 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.test.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.test.tsx @@ -40,7 +40,7 @@ describe('OverwriteModal', () => { const wrapper = shallowWithI18nProvider(); expect(wrapper.find('p').text()).toMatchInlineSnapshot( - `"\\"baz\\" conflicts with an existing object, are you sure you want to overwrite it?"` + `"\\"baz\\" conflicts with an existing object. Overwrite it?"` ); expect(wrapper.find('EuiSuperSelect')).toHaveLength(0); }); @@ -82,7 +82,7 @@ describe('OverwriteModal', () => { const wrapper = shallowWithI18nProvider(); expect(wrapper.find('p').text()).toMatchInlineSnapshot( - `"\\"baz\\" conflicts with multiple existing objects, do you want to overwrite one of them?"` + `"\\"baz\\" conflicts with multiple existing objects. Overwrite one?"` ); expect(wrapper.find('EuiSuperSelect')).toHaveLength(1); }); diff --git a/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx index dbe95161cbeae..bf27d407fbe94 100644 --- a/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx +++ b/src/plugins/saved_objects_management/public/management_section/objects_table/components/overwrite_modal.tsx @@ -98,15 +98,13 @@ export const OverwriteModal = ({ conflict, onFinish }: OverwriteModalProps) => { const bodyText = error.type === 'conflict' ? i18n.translate('savedObjectsManagement.objectsTable.overwriteModal.body.conflict', { - defaultMessage: - '"{title}" conflicts with an existing object, are you sure you want to overwrite it?', + defaultMessage: '"{title}" conflicts with an existing object. Overwrite it?', values: { title }, }) : i18n.translate( 'savedObjectsManagement.objectsTable.overwriteModal.body.ambiguousConflict', { - defaultMessage: - '"{title}" conflicts with multiple existing objects, do you want to overwrite one of them?', + defaultMessage: '"{title}" conflicts with multiple existing objects. Overwrite one?', values: { title }, } ); diff --git a/src/plugins/telemetry/public/mocks.ts b/src/plugins/telemetry/public/mocks.ts index dd7e5a4cc4ce3..5f38b27144d02 100644 --- a/src/plugins/telemetry/public/mocks.ts +++ b/src/plugins/telemetry/public/mocks.ts @@ -48,6 +48,7 @@ export function mockTelemetryService({ banner: true, allowChangingOptInStatus: true, telemetryNotifyUserAboutOptInDefault: true, + userCanChangeSettings: true, ...configOverride, }; diff --git a/src/plugins/telemetry/public/plugin.ts b/src/plugins/telemetry/public/plugin.ts index 3846e7cb96a19..9fefa2ebdd02e 100644 --- a/src/plugins/telemetry/public/plugin.ts +++ b/src/plugins/telemetry/public/plugin.ts @@ -25,6 +25,7 @@ import { PluginInitializerContext, SavedObjectsClientContract, SavedObjectsBatchResponse, + ApplicationStart, } from '../../../core/public'; import { TelemetrySender, TelemetryService, TelemetryNotifications } from './services'; @@ -61,6 +62,7 @@ export interface TelemetryPluginConfig { optInStatusUrl: string; sendUsageFrom: 'browser' | 'server'; telemetryNotifyUserAboutOptInDefault?: boolean; + userCanChangeSettings?: boolean; } export class TelemetryPlugin implements Plugin { @@ -69,6 +71,7 @@ export class TelemetryPlugin implements Plugin) { this.currentKibanaVersion = initializerContext.env.packageInfo.version; @@ -91,6 +94,9 @@ export class TelemetryPlugin implements Plugin { expect(telemetryService.setUserHasSeenNotice).toBeCalledTimes(1); }); }); + +describe('shouldShowOptedInNoticeBanner', () => { + it("should return true because a banner hasn't been shown, the notice hasn't been seen and the user has privileges to edit saved objects", () => { + const telemetryService = mockTelemetryService(); + telemetryService.getUserShouldSeeOptInNotice = jest.fn().mockReturnValue(true); + const telemetryNotifications = mockTelemetryNotifications({ telemetryService }); + expect(telemetryNotifications.shouldShowOptedInNoticeBanner()).toBe(true); + }); + + it('should return false because the banner is already on screen', () => { + const telemetryService = mockTelemetryService(); + telemetryService.getUserShouldSeeOptInNotice = jest.fn().mockReturnValue(true); + const telemetryNotifications = mockTelemetryNotifications({ telemetryService }); + telemetryNotifications['optedInNoticeBannerId'] = 'bruce-banner'; + expect(telemetryNotifications.shouldShowOptedInNoticeBanner()).toBe(false); + }); + + it("should return false because the banner has already been seen or the user doesn't have privileges to change saved objects", () => { + const telemetryService = mockTelemetryService(); + telemetryService.getUserShouldSeeOptInNotice = jest.fn().mockReturnValue(false); + const telemetryNotifications = mockTelemetryNotifications({ telemetryService }); + expect(telemetryNotifications.shouldShowOptedInNoticeBanner()).toBe(false); + }); +}); diff --git a/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts index bf25bb592db82..fc44a4db7cf5e 100644 --- a/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts +++ b/src/plugins/telemetry/public/services/telemetry_notifications/telemetry_notifications.ts @@ -39,9 +39,9 @@ export class TelemetryNotifications { } public shouldShowOptedInNoticeBanner = (): boolean => { - const userHasSeenOptedInNotice = this.telemetryService.getUserHasSeenOptedInNotice(); + const userShouldSeeOptInNotice = this.telemetryService.getUserShouldSeeOptInNotice(); const bannerOnScreen = typeof this.optedInNoticeBannerId !== 'undefined'; - return !bannerOnScreen && userHasSeenOptedInNotice; + return !bannerOnScreen && userShouldSeeOptInNotice; }; public renderOptedInNoticeBanner = (): void => { diff --git a/src/plugins/telemetry/public/services/telemetry_service.test.ts b/src/plugins/telemetry/public/services/telemetry_service.test.ts index 16faa0cfc7536..655bbfe746c2a 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.test.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.test.ts @@ -184,15 +184,15 @@ describe('TelemetryService', () => { describe('setUserHasSeenNotice', () => { it('should hit the API and change the config', async () => { const telemetryService = mockTelemetryService({ - config: { telemetryNotifyUserAboutOptInDefault: undefined }, + config: { telemetryNotifyUserAboutOptInDefault: undefined, userCanChangeSettings: true }, }); expect(telemetryService.userHasSeenOptedInNotice).toBe(undefined); - expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(false); + expect(telemetryService.getUserShouldSeeOptInNotice()).toBe(false); await telemetryService.setUserHasSeenNotice(); expect(telemetryService['http'].put).toBeCalledTimes(1); expect(telemetryService.userHasSeenOptedInNotice).toBe(true); - expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(true); + expect(telemetryService.getUserShouldSeeOptInNotice()).toBe(true); }); it('should show a toast notification if the request fail', async () => { @@ -207,12 +207,33 @@ describe('TelemetryService', () => { }); expect(telemetryService.userHasSeenOptedInNotice).toBe(undefined); - expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(false); + expect(telemetryService.getUserShouldSeeOptInNotice()).toBe(false); await telemetryService.setUserHasSeenNotice(); expect(telemetryService['http'].put).toBeCalledTimes(1); expect(telemetryService['notifications'].toasts.addError).toBeCalledTimes(1); expect(telemetryService.userHasSeenOptedInNotice).toBe(false); - expect(telemetryService.getUserHasSeenOptedInNotice()).toBe(false); + expect(telemetryService.getUserShouldSeeOptInNotice()).toBe(false); + }); + }); + + describe('getUserShouldSeeOptInNotice', () => { + it('returns whether the user can update the telemetry config (has SavedObjects access)', () => { + const telemetryService = mockTelemetryService({ + config: { userCanChangeSettings: undefined }, + }); + expect(telemetryService.config.userCanChangeSettings).toBe(undefined); + expect(telemetryService.userCanChangeSettings).toBe(false); + expect(telemetryService.getUserShouldSeeOptInNotice()).toBe(false); + + telemetryService.userCanChangeSettings = false; + expect(telemetryService.config.userCanChangeSettings).toBe(false); + expect(telemetryService.userCanChangeSettings).toBe(false); + expect(telemetryService.getUserShouldSeeOptInNotice()).toBe(false); + + telemetryService.userCanChangeSettings = true; + expect(telemetryService.config.userCanChangeSettings).toBe(true); + expect(telemetryService.userCanChangeSettings).toBe(true); + expect(telemetryService.getUserShouldSeeOptInNotice()).toBe(true); }); }); }); diff --git a/src/plugins/telemetry/public/services/telemetry_service.ts b/src/plugins/telemetry/public/services/telemetry_service.ts index 6d87a74197fe5..c807aa9e1d35e 100644 --- a/src/plugins/telemetry/public/services/telemetry_service.ts +++ b/src/plugins/telemetry/public/services/telemetry_service.ts @@ -87,9 +87,25 @@ export class TelemetryService { return telemetryUrl; }; - public getUserHasSeenOptedInNotice = () => { - return this.config.telemetryNotifyUserAboutOptInDefault || false; - }; + /** + * Returns if an user should be shown the notice about Opt-In/Out telemetry. + * The decision is made based on whether any user has already dismissed the message or + * the user can't actually change the settings (in which case, there's no point on bothering them) + */ + public getUserShouldSeeOptInNotice(): boolean { + return ( + (this.config.telemetryNotifyUserAboutOptInDefault && this.config.userCanChangeSettings) ?? + false + ); + } + + public get userCanChangeSettings() { + return this.config.userCanChangeSettings ?? false; + } + + public set userCanChangeSettings(userCanChangeSettings: boolean) { + this.config = { ...this.config, userCanChangeSettings }; + } public getIsOptedIn = () => { return this.isOptedIn; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index acd575badbe5b..5bce03a292760 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -414,6 +414,34 @@ } } }, + "enterpriseSearch": { + "properties": { + "clicks_total": { + "type": "long" + }, + "clicks_7_days": { + "type": "long" + }, + "clicks_30_days": { + "type": "long" + }, + "clicks_90_days": { + "type": "long" + }, + "minutes_on_screen_total": { + "type": "float" + }, + "minutes_on_screen_7_days": { + "type": "float" + }, + "minutes_on_screen_30_days": { + "type": "float" + }, + "minutes_on_screen_90_days": { + "type": "float" + } + } + }, "appSearch": { "properties": { "clicks_total": { diff --git a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap index dd4ee61fd1148..ab29656c557c2 100644 --- a/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap +++ b/src/plugins/telemetry_management_section/public/components/__snapshots__/telemetry_management_section.test.tsx.snap @@ -228,7 +228,6 @@ exports[`TelemetryManagementSectionComponent renders null because allowChangingO "getIsOptedIn": [Function], "getOptInStatusUrl": [Function], "getTelemetryUrl": [Function], - "getUserHasSeenOptedInNotice": [Function], "http": Object { "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { @@ -430,7 +429,6 @@ exports[`TelemetryManagementSectionComponent renders null because query does not "getIsOptedIn": [Function], "getOptInStatusUrl": [Function], "getTelemetryUrl": [Function], - "getUserHasSeenOptedInNotice": [Function], "http": Object { "addLoadingCountSource": [MockFunction], "anonymousPaths": Object { diff --git a/src/plugins/timelion/server/plugin.ts b/src/plugins/timelion/server/plugin.ts index fe77ebeb0866d..d5a5ec4640d4b 100644 --- a/src/plugins/timelion/server/plugin.ts +++ b/src/plugins/timelion/server/plugin.ts @@ -42,7 +42,7 @@ const showWarningMessageIfTimelionSheetWasFound = (core: CoreStart, logger: Logg ({ total }) => total && logger.warn( - 'Deprecated since 7.0, the Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard. See https://www.elastic.co/guide/en/kibana/master/timelion.html#timelion-deprecation.' + 'Deprecated since 7.0, the Timelion app will be removed in 8.0. To continue using your Timelion worksheets, migrate them to a dashboard. See https://www.elastic.co/guide/en/kibana/master/dashboard.html#timelion-deprecation.' ) ); }; diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts new file mode 100644 index 0000000000000..a513bb3c95f24 --- /dev/null +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.test.ts @@ -0,0 +1,81 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { buildContextMenuForActions } from './build_eui_context_menu_panels'; +import { Action, createAction } from '../actions'; + +const createTestAction = ({ + type, + dispayName, + order, +}: { + type: string; + dispayName: string; + order: number; +}) => + createAction({ + type: type as any, // mapping doesn't matter for this test + getDisplayName: () => dispayName, + order, + execute: async () => {}, + }); + +test('contextMenu actions sorting: order, type, displayName', async () => { + const actions: Action[] = [ + createTestAction({ + order: 100, + type: '1', + dispayName: 'a', + }), + createTestAction({ + order: 100, + type: '1', + dispayName: 'b', + }), + createTestAction({ + order: 0, + type: '2', + dispayName: 'c', + }), + createTestAction({ + order: 0, + type: '2', + dispayName: 'd', + }), + createTestAction({ + order: 0, + type: '3', + dispayName: 'aa', + }), + ].sort(() => 0.5 - Math.random()); + + const result = await buildContextMenuForActions({ + actions: actions.map((action) => ({ action, context: {}, trigger: '' as any })), + }); + + expect(result.items?.map((item) => item.name as string)).toMatchInlineSnapshot(` + Array [ + "a", + "b", + "c", + "d", + "aa", + ] + `); +}); diff --git a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx index b44a07273f4a9..3be1ec781cef6 100644 --- a/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx +++ b/src/plugins/ui_actions/public/context_menu/build_eui_context_menu_panels.tsx @@ -20,6 +20,7 @@ import * as React from 'react'; import { EuiContextMenuPanelDescriptor, EuiContextMenuPanelItemDescriptor } from '@elastic/eui'; import _ from 'lodash'; +import sortBy from 'lodash/sortBy'; import { i18n } from '@kbn/i18n'; import { uiToReactComponent } from '../../../kibana_react/public'; import { Action } from '../actions'; @@ -46,11 +47,11 @@ interface ActionWithContext { export async function buildContextMenuForActions({ actions, title = defaultTitle, - closeMenu, + closeMenu = () => {}, }: { actions: ActionWithContext[]; title?: string; - closeMenu: () => void; + closeMenu?: () => void; }): Promise { const menuItems = await buildEuiContextMenuPanelItems({ actions, @@ -74,6 +75,13 @@ async function buildEuiContextMenuPanelItems({ actions: ActionWithContext[]; closeMenu: () => void; }) { + actions = sortBy( + actions, + (a) => -1 * (a.action.order ?? 0), + (a) => a.action.type, + (a) => a.action.getDisplayName({ ...a.context, trigger: a.trigger }) + ); + const items: EuiContextMenuPanelItemDescriptor[] = new Array(actions.length); const promises = actions.map(async ({ action, context, trigger }, index) => { const isCompatible = await action.isCompatible({ diff --git a/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx b/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx index 8fff231a867bf..a4cfe172dd109 100644 --- a/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx +++ b/src/plugins/ui_actions/public/tests/test_samples/hello_world_action.tsx @@ -18,7 +18,7 @@ */ import React from 'react'; -import { EuiFlyout, EuiFlexGroup, EuiFlexItem, EuiBadge } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiBadge, EuiFlyoutBody } from '@elastic/eui'; import { CoreStart } from 'src/core/public'; import { createAction, ActionByType } from '../../actions'; import { toMountPoint, reactToUiComponent } from '../../../../kibana_react/public'; @@ -49,14 +49,11 @@ export function createHelloWorldAction( getIconType: () => 'lock', MenuItem: UiMenuItem, execute: async () => { - const flyoutSession = overlays.openFlyout( - toMountPoint( - flyoutSession && flyoutSession.close()}> - Hello World, I am a hello world action! - - ), + overlays.openFlyout( + toMountPoint(Hello World, I am a hello world action!), { 'data-test-subj': 'helloWorldAction', + ownFocus: true, } ); }, diff --git a/src/plugins/url_forwarding/README.md b/src/plugins/url_forwarding/README.md new file mode 100644 index 0000000000000..5c5501cc019f9 --- /dev/null +++ b/src/plugins/url_forwarding/README.md @@ -0,0 +1,3 @@ +# url-forwarding + +This plugins contains helpers to redirect legacy URLs. It can be used to forward old URLs to their new counterparts. diff --git a/src/plugins/url_forwarding/kibana.json b/src/plugins/url_forwarding/kibana.json new file mode 100644 index 0000000000000..4f534c1219b34 --- /dev/null +++ b/src/plugins/url_forwarding/kibana.json @@ -0,0 +1,7 @@ +{ + "id": "urlForwarding", + "version": "kibana", + "server": false, + "ui": true, + "requiredPlugins": ["kibanaLegacy"] +} diff --git a/src/plugins/kibana_legacy/public/forward_app/forward_app.ts b/src/plugins/url_forwarding/public/forward_app/forward_app.ts similarity index 94% rename from src/plugins/kibana_legacy/public/forward_app/forward_app.ts rename to src/plugins/url_forwarding/public/forward_app/forward_app.ts index b425091dfbcd9..967b18769ebc6 100644 --- a/src/plugins/kibana_legacy/public/forward_app/forward_app.ts +++ b/src/plugins/url_forwarding/public/forward_app/forward_app.ts @@ -20,10 +20,10 @@ import { App, AppMountParameters, CoreSetup } from 'kibana/public'; import { AppNavLinkStatus } from '../../../../core/public'; import { navigateToLegacyKibanaUrl } from './navigate_to_legacy_kibana_url'; -import { ForwardDefinition, KibanaLegacyStart } from '../plugin'; +import { ForwardDefinition, UrlForwardingStart } from '../plugin'; export const createLegacyUrlForwardApp = ( - core: CoreSetup<{}, KibanaLegacyStart>, + core: CoreSetup<{}, UrlForwardingStart>, forwards: ForwardDefinition[] ): App => ({ id: 'kibana', diff --git a/src/plugins/kibana_legacy/public/forward_app/index.ts b/src/plugins/url_forwarding/public/forward_app/index.ts similarity index 100% rename from src/plugins/kibana_legacy/public/forward_app/index.ts rename to src/plugins/url_forwarding/public/forward_app/index.ts diff --git a/src/plugins/kibana_legacy/public/forward_app/navigate_to_legacy_kibana_url.test.ts b/src/plugins/url_forwarding/public/forward_app/navigate_to_legacy_kibana_url.test.ts similarity index 100% rename from src/plugins/kibana_legacy/public/forward_app/navigate_to_legacy_kibana_url.test.ts rename to src/plugins/url_forwarding/public/forward_app/navigate_to_legacy_kibana_url.test.ts diff --git a/src/plugins/kibana_legacy/public/forward_app/navigate_to_legacy_kibana_url.ts b/src/plugins/url_forwarding/public/forward_app/navigate_to_legacy_kibana_url.ts similarity index 96% rename from src/plugins/kibana_legacy/public/forward_app/navigate_to_legacy_kibana_url.ts rename to src/plugins/url_forwarding/public/forward_app/navigate_to_legacy_kibana_url.ts index 1df991f66747c..1677b01e7aa4f 100644 --- a/src/plugins/kibana_legacy/public/forward_app/navigate_to_legacy_kibana_url.ts +++ b/src/plugins/url_forwarding/public/forward_app/navigate_to_legacy_kibana_url.ts @@ -19,7 +19,7 @@ import { ApplicationStart, IBasePath } from 'kibana/public'; import { ForwardDefinition } from '../index'; -import { normalizePath } from '../utils/normalize_path'; +import { normalizePath } from './normalize_path'; export const navigateToLegacyKibanaUrl = ( path: string, diff --git a/src/plugins/kibana_legacy/public/utils/normalize_path.ts b/src/plugins/url_forwarding/public/forward_app/normalize_path.ts similarity index 100% rename from src/plugins/kibana_legacy/public/utils/normalize_path.ts rename to src/plugins/url_forwarding/public/forward_app/normalize_path.ts diff --git a/src/core/server/saved_objects/schema/index.ts b/src/plugins/url_forwarding/public/index.ts similarity index 85% rename from src/core/server/saved_objects/schema/index.ts rename to src/plugins/url_forwarding/public/index.ts index d30bbb8d34cd3..5fc3f0bea4d3e 100644 --- a/src/core/server/saved_objects/schema/index.ts +++ b/src/plugins/url_forwarding/public/index.ts @@ -17,4 +17,8 @@ * under the License. */ -export { SavedObjectsSchema, SavedObjectsSchemaDefinition } from './schema'; +import { UrlForwardingPlugin } from './plugin'; + +export const plugin = () => new UrlForwardingPlugin(); + +export * from './plugin'; diff --git a/src/plugins/url_forwarding/public/mocks.ts b/src/plugins/url_forwarding/public/mocks.ts new file mode 100644 index 0000000000000..5e32d9b1896bc --- /dev/null +++ b/src/plugins/url_forwarding/public/mocks.ts @@ -0,0 +1,38 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { UrlForwardingPlugin } from './plugin'; + +export type Setup = jest.Mocked>; +export type Start = jest.Mocked>; + +const createSetupContract = (): Setup => ({ + forwardApp: jest.fn(), +}); + +const createStartContract = (): Start => ({ + getForwards: jest.fn(), + navigateToDefaultApp: jest.fn(), + navigateToLegacyKibanaUrl: jest.fn(), +}); + +export const urlForwardingPluginMock = { + createSetupContract, + createStartContract, +}; diff --git a/src/plugins/kibana_legacy/public/navigate_to_default_app.ts b/src/plugins/url_forwarding/public/navigate_to_default_app.ts similarity index 100% rename from src/plugins/kibana_legacy/public/navigate_to_default_app.ts rename to src/plugins/url_forwarding/public/navigate_to_default_app.ts diff --git a/src/plugins/url_forwarding/public/plugin.ts b/src/plugins/url_forwarding/public/plugin.ts new file mode 100644 index 0000000000000..8ef23fb2c840e --- /dev/null +++ b/src/plugins/url_forwarding/public/plugin.ts @@ -0,0 +1,134 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { CoreStart, CoreSetup } from 'kibana/public'; +import { KibanaLegacyStart } from 'src/plugins/kibana_legacy/public'; +import { Subscription } from 'rxjs'; +import { navigateToDefaultApp } from './navigate_to_default_app'; +import { createLegacyUrlForwardApp } from './forward_app'; +import { navigateToLegacyKibanaUrl } from './forward_app/navigate_to_legacy_kibana_url'; + +export interface ForwardDefinition { + legacyAppId: string; + newAppId: string; + rewritePath: (legacyPath: string) => string; +} + +export class UrlForwardingPlugin { + private forwardDefinitions: ForwardDefinition[] = []; + private currentAppId: string | undefined; + private currentAppIdSubscription: Subscription | undefined; + + public setup(core: CoreSetup<{}, UrlForwardingStart>) { + core.application.register(createLegacyUrlForwardApp(core, this.forwardDefinitions)); + return { + /** + * Forwards URLs within the legacy `kibana` app to a new platform application. + * + * @param legacyAppId The name of the old app to forward URLs from + * @param newAppId The name of the new app that handles the URLs now + * @param rewritePath Function to rewrite the legacy sub path of the app to the new path in the core app. + * If none is provided, it will just strip the prefix of the legacyAppId away + * + * path into the new path + * + * Example usage: + * ``` + * urlForwarding.forwardApp( + * 'old', + * 'new', + * path => { + * const [, id] = /old/item\/(.*)$/.exec(path) || []; + * if (!id) { + * return '#/home'; + * } + * return '#/items/${id}'; + * } + * ); + * ``` + * This will cause the following redirects: + * + * * app/kibana#/old/ -> app/new#/home + * * app/kibana#/old/item/123 -> app/new#/items/123 + * + */ + forwardApp: ( + legacyAppId: string, + newAppId: string, + rewritePath?: (legacyPath: string) => string + ) => { + this.forwardDefinitions.push({ + legacyAppId, + newAppId, + rewritePath: rewritePath || ((path) => `#${path.replace(`/${legacyAppId}`, '') || '/'}`), + }); + }, + }; + } + + public start( + { application, http: { basePath }, uiSettings }: CoreStart, + { kibanaLegacy }: { kibanaLegacy: KibanaLegacyStart } + ) { + this.currentAppIdSubscription = application.currentAppId$.subscribe((currentAppId) => { + this.currentAppId = currentAppId; + }); + return { + /** + * Navigates to the app defined as kibana.defaultAppId. + * This takes redirects into account and uses the right mechanism to navigate. + */ + navigateToDefaultApp: ( + { overwriteHash }: { overwriteHash: boolean } = { overwriteHash: true } + ) => { + navigateToDefaultApp( + kibanaLegacy.config.defaultAppId, + this.forwardDefinitions, + application, + basePath, + this.currentAppId, + overwriteHash + ); + }, + /** + * Resolves the provided hash using the registered forwards and navigates to the target app. + * If a navigation happened, `{ navigated: true }` will be returned. + * If no matching forward is found, `{ navigated: false }` will be returned. + * @param hash + */ + navigateToLegacyKibanaUrl: (hash: string) => { + return navigateToLegacyKibanaUrl(hash, this.forwardDefinitions, basePath, application); + }, + /** + * @deprecated + * Just exported for wiring up with legacy platform, should not be used. + */ + getForwards: () => this.forwardDefinitions, + }; + } + + public stop() { + if (this.currentAppIdSubscription) { + this.currentAppIdSubscription.unsubscribe(); + } + } +} + +export type UrlForwardingSetup = ReturnType; +export type UrlForwardingStart = ReturnType; diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js index 4b5aab85cfc4e..c5fc4b7b93269 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/es.test.js @@ -100,9 +100,17 @@ describe('es', () => { expect(agg.time_buckets.date_histogram.time_zone).to.equal('Etc/UTC'); }); - it('sets the field and interval', () => { + it('sets the field', () => { expect(agg.time_buckets.date_histogram.field).to.equal('@timestamp'); - expect(agg.time_buckets.date_histogram.interval).to.equal('1y'); + }); + + it('sets the interval for calendar_interval correctly', () => { + expect(agg.time_buckets.date_histogram).to.have.property('calendar_interval', '1y'); + }); + + it('sets the interval for fixed_interval correctly', () => { + const a = createDateAgg({ timefield: '@timestamp', interval: '24h' }, tlConfig); + expect(a.time_buckets.date_histogram).to.have.property('fixed_interval', '24h'); }); it('sets min_doc_count to 0', () => { diff --git a/src/plugins/vis_type_timelion/server/series_functions/es/lib/create_date_agg.js b/src/plugins/vis_type_timelion/server/series_functions/es/lib/create_date_agg.js index 904fe69cbc57c..b36f37ac5cc9d 100644 --- a/src/plugins/vis_type_timelion/server/series_functions/es/lib/create_date_agg.js +++ b/src/plugins/vis_type_timelion/server/series_functions/es/lib/create_date_agg.js @@ -19,6 +19,8 @@ import _ from 'lodash'; import { buildAggBody } from './agg_body'; +import { search } from '../../../../../../plugins/data/server'; +const { dateHistogramInterval } = search.aggs; export default function createDateAgg(config, tlConfig, scriptedFields) { const dateAgg = { @@ -26,13 +28,13 @@ export default function createDateAgg(config, tlConfig, scriptedFields) { meta: { type: 'time_buckets' }, date_histogram: { field: config.timefield, - interval: config.interval, time_zone: tlConfig.time.timezone, extended_bounds: { min: tlConfig.time.from, max: tlConfig.time.to, }, min_doc_count: 0, + ...dateHistogramInterval(config.interval), }, }, }; diff --git a/src/plugins/vis_type_vega/public/data_model/search_api.ts b/src/plugins/vis_type_vega/public/data_model/search_api.ts index 8a1541ecae0d4..4ea25af549249 100644 --- a/src/plugins/vis_type_vega/public/data_model/search_api.ts +++ b/src/plugins/vis_type_vega/public/data_model/search_api.ts @@ -51,9 +51,6 @@ export class SearchAPI { searchRequests.map((request) => { const requestId = request.name; const params = getSearchParamsFromRequest(request, { - esShardTimeout: this.dependencies.injectedMetadata.getInjectedVar( - 'esShardTimeout' - ) as number, getConfig: this.dependencies.uiSettings.get.bind(this.dependencies.uiSettings), }); diff --git a/src/plugins/vis_type_vega/public/plugin.ts b/src/plugins/vis_type_vega/public/plugin.ts index 00c6b2e3c8d5b..4b8ff8e2cb43a 100644 --- a/src/plugins/vis_type_vega/public/plugin.ts +++ b/src/plugins/vis_type_vega/public/plugin.ts @@ -78,7 +78,6 @@ export class VegaPlugin implements Plugin, void> { ) { setInjectedVars({ enableExternalUrls: this.initializerContext.config.get().enableExternalUrls, - esShardTimeout: core.injectedMetadata.getInjectedVar('esShardTimeout') as number, emsTileLayerId: core.injectedMetadata.getInjectedVar('emsTileLayerId', true), }); setUISettings(core.uiSettings); diff --git a/src/plugins/vis_type_vega/public/services.ts b/src/plugins/vis_type_vega/public/services.ts index acd02a6dd42f8..dfb2c96e9f894 100644 --- a/src/plugins/vis_type_vega/public/services.ts +++ b/src/plugins/vis_type_vega/public/services.ts @@ -48,7 +48,6 @@ export const [getSavedObjects, setSavedObjects] = createGetterSetter('InjectedVars'); diff --git a/src/plugins/vis_type_vega/public/vega_visualization.test.js b/src/plugins/vis_type_vega/public/vega_visualization.test.js index 0912edf9503a6..1bf625af76207 100644 --- a/src/plugins/vis_type_vega/public/vega_visualization.test.js +++ b/src/plugins/vis_type_vega/public/vega_visualization.test.js @@ -82,7 +82,6 @@ describe('VegaVisualizations', () => { setInjectedVars({ emsTileLayerId: {}, enableExternalUrls: true, - esShardTimeout: 10000, }); setData(dataPluginStart); setSavedObjects(coreStart.savedObjects); diff --git a/src/plugins/visualizations/server/plugin.ts b/src/plugins/visualizations/server/plugin.ts index 993612d22ebfd..7502968a33654 100644 --- a/src/plugins/visualizations/server/plugin.ts +++ b/src/plugins/visualizations/server/plugin.ts @@ -19,6 +19,8 @@ import { i18n } from '@kbn/i18n'; import { schema } from '@kbn/config-schema'; +import { Observable } from 'rxjs'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { PluginInitializerContext, CoreSetup, @@ -32,16 +34,19 @@ import { VISUALIZE_ENABLE_LABS_SETTING } from '../common/constants'; import { visualizationSavedObjectType } from './saved_objects'; import { VisualizationsPluginSetup, VisualizationsPluginStart } from './types'; +import { registerVisualizationsCollector } from './usage_collector'; export class VisualizationsPlugin implements Plugin { private readonly logger: Logger; + private readonly config: Observable<{ kibana: { index: string } }>; constructor(initializerContext: PluginInitializerContext) { this.logger = initializerContext.logger.get(); + this.config = initializerContext.config.legacy.globalConfig$; } - public setup(core: CoreSetup) { + public setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) { this.logger.debug('visualizations: Setup'); core.savedObjects.registerType(visualizationSavedObjectType); @@ -61,6 +66,10 @@ export class VisualizationsPlugin }, }); + if (plugins.usageCollection) { + registerVisualizationsCollector(plugins.usageCollection, this.config); + } + return {}; } diff --git a/src/plugins/visualizations/server/usage_collector/get_past_days.test.ts b/src/plugins/visualizations/server/usage_collector/get_past_days.test.ts new file mode 100644 index 0000000000000..7ef3009de9e5c --- /dev/null +++ b/src/plugins/visualizations/server/usage_collector/get_past_days.test.ts @@ -0,0 +1,35 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; +import { getPastDays } from './get_past_days'; + +describe('getPastDays', () => { + test('Returns 2 days that have passed from the current date', () => { + const pastDate = moment().subtract(2, 'days').startOf('day').toString(); + + expect(getPastDays(pastDate)).toEqual(2); + }); + + test('Returns 30 days that have passed from the current date', () => { + const pastDate = moment().subtract(30, 'days').startOf('day').toString(); + + expect(getPastDays(pastDate)).toEqual(30); + }); +}); diff --git a/src/plugins/visualizations/server/usage_collector/get_past_days.ts b/src/plugins/visualizations/server/usage_collector/get_past_days.ts new file mode 100644 index 0000000000000..5fa68d80de111 --- /dev/null +++ b/src/plugins/visualizations/server/usage_collector/get_past_days.ts @@ -0,0 +1,25 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export const getPastDays = (dateString: string): number => { + const date = new Date(dateString); + const today = new Date(); + const diff = Math.abs(date.getTime() - today.getTime()); + return Math.trunc(diff / (1000 * 60 * 60 * 24)); +}; diff --git a/src/plugins/visualizations/server/usage_collector/get_usage_collector.test.ts b/src/plugins/visualizations/server/usage_collector/get_usage_collector.test.ts new file mode 100644 index 0000000000000..4a8e4b70ae070 --- /dev/null +++ b/src/plugins/visualizations/server/usage_collector/get_usage_collector.test.ts @@ -0,0 +1,195 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import moment from 'moment'; +import { of } from 'rxjs'; + +import { LegacyAPICaller } from 'src/core/server'; +import { getUsageCollector } from './get_usage_collector'; + +const defaultMockSavedObjects = [ + { + _id: 'visualization:coolviz-123', + _source: { + type: 'visualization', + visualization: { visState: '{"type": "shell_beads"}' }, + updated_at: moment().subtract(7, 'days').startOf('day').toString(), + }, + }, +]; + +const enlargedMockSavedObjects = [ + // default space + { + _id: 'visualization:coolviz-123', + _source: { + type: 'visualization', + visualization: { visState: '{"type": "cave_painting"}' }, + updated_at: moment().subtract(7, 'days').startOf('day').toString(), + }, + }, + { + _id: 'visualization:coolviz-456', + _source: { + type: 'visualization', + visualization: { visState: '{"type": "printing_press"}' }, + updated_at: moment().subtract(20, 'days').startOf('day').toString(), + }, + }, + { + _id: 'meat:visualization:coolviz-789', + _source: { + type: 'visualization', + visualization: { visState: '{"type": "floppy_disk"}' }, + updated_at: moment().subtract(2, 'months').startOf('day').toString(), + }, + }, + // meat space + { + _id: 'meat:visualization:coolviz-789', + _source: { + type: 'visualization', + visualization: { visState: '{"type": "cave_painting"}' }, + updated_at: moment().subtract(89, 'days').startOf('day').toString(), + }, + }, + { + _id: 'meat:visualization:coolviz-789', + _source: { + type: 'visualization', + visualization: { visState: '{"type": "cuneiform"}' }, + updated_at: moment().subtract(5, 'months').startOf('day').toString(), + }, + }, + { + _id: 'meat:visualization:coolviz-789', + _source: { + type: 'visualization', + visualization: { visState: '{"type": "cuneiform"}' }, + updated_at: moment().subtract(2, 'days').startOf('day').toString(), + }, + }, + { + _id: 'meat:visualization:coolviz-789', + _source: { + type: 'visualization', + visualization: { visState: '{"type": "floppy_disk"}' }, + updated_at: moment().subtract(7, 'days').startOf('day').toString(), + }, + }, + // cyber space + { + _id: 'cyber:visualization:coolviz-789', + _source: { + type: 'visualization', + visualization: { visState: '{"type": "floppy_disk"}' }, + updated_at: moment().subtract(7, 'months').startOf('day').toString(), + }, + }, + { + _id: 'cyber:visualization:coolviz-789', + _source: { + type: 'visualization', + visualization: { visState: '{"type": "floppy_disk"}' }, + updated_at: moment().subtract(3, 'days').startOf('day').toString(), + }, + }, + { + _id: 'cyber:visualization:coolviz-123', + _source: { + type: 'visualization', + visualization: { visState: '{"type": "cave_painting"}' }, + updated_at: moment().subtract(15, 'days').startOf('day').toString(), + }, + }, +]; + +describe('Visualizations usage collector', () => { + const configMock = of({ kibana: { index: '' } }); + const usageCollector = getUsageCollector(configMock); + const getMockCallCluster = (hits: unknown[]) => + (() => Promise.resolve({ hits: { hits } }) as unknown) as LegacyAPICaller; + + test('Should fit the shape', () => { + expect(usageCollector.type).toBe('visualization_types'); + expect(usageCollector.isReady()).toBe(true); + expect(usageCollector.fetch).toEqual(expect.any(Function)); + }); + + test('Summarizes visualizations response data', async () => { + const result = await usageCollector.fetch(getMockCallCluster(defaultMockSavedObjects)); + + expect(result).toMatchObject({ + shell_beads: { + spaces_avg: 1, + spaces_max: 1, + spaces_min: 1, + total: 1, + saved_7_days_total: 1, + saved_30_days_total: 1, + saved_90_days_total: 1, + }, + }); + }); + + test('Summarizes visualizations response data per Space', async () => { + const expectedStats = { + cave_painting: { + total: 3, + spaces_min: 1, + spaces_max: 1, + spaces_avg: 1, + saved_7_days_total: 1, + saved_30_days_total: 2, + saved_90_days_total: 3, + }, + printing_press: { + total: 1, + spaces_min: 1, + spaces_max: 1, + spaces_avg: 1, + saved_7_days_total: 0, + saved_30_days_total: 1, + saved_90_days_total: 1, + }, + cuneiform: { + total: 2, + spaces_min: 2, + spaces_max: 2, + spaces_avg: 2, + saved_7_days_total: 1, + saved_30_days_total: 1, + saved_90_days_total: 1, + }, + floppy_disk: { + total: 4, + spaces_min: 2, + spaces_max: 2, + spaces_avg: 2, + saved_7_days_total: 2, + saved_30_days_total: 2, + saved_90_days_total: 3, + }, + }; + + const result = await usageCollector.fetch(getMockCallCluster(enlargedMockSavedObjects)); + + expect(result).toMatchObject(expectedStats); + }); +}); diff --git a/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts b/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts new file mode 100644 index 0000000000000..165c3ee649868 --- /dev/null +++ b/src/plugins/visualizations/server/usage_collector/get_usage_collector.ts @@ -0,0 +1,107 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; +import { countBy, get, groupBy, mapValues, max, min, values } from 'lodash'; +import { first } from 'rxjs/operators'; +import { SearchResponse } from 'elasticsearch'; + +import { LegacyAPICaller } from 'src/core/server'; +import { getPastDays } from './get_past_days'; + +const VIS_USAGE_TYPE = 'visualization_types'; + +type ESResponse = SearchResponse<{ visualization: { visState: string } }>; + +interface VisSummary { + type: string; + space: string; + past_days: number; +} + +/* + * Parse the response data into telemetry payload + */ +async function getStats(callCluster: LegacyAPICaller, index: string) { + const searchParams = { + size: 10000, // elasticsearch index.max_result_window default value + index, + ignoreUnavailable: true, + filterPath: [ + 'hits.hits._id', + 'hits.hits._source.visualization', + 'hits.hits._source.updated_at', + ], + body: { + query: { + bool: { filter: { term: { type: 'visualization' } } }, + }, + }, + }; + const esResponse: ESResponse = await callCluster('search', searchParams); + const size = get(esResponse, 'hits.hits.length'); + if (size < 1) { + return; + } + + // `map` to get the raw types + const visSummaries: VisSummary[] = esResponse.hits.hits.map((hit) => { + const spacePhrases = hit._id.split(':'); + const lastUpdated: string = get(hit, '_source.updated_at'); + const space = spacePhrases.length === 3 ? spacePhrases[0] : 'default'; // if in a custom space, the format of a saved object ID is space:type:id + const visualization = get(hit, '_source.visualization', { visState: '{}' }); + const visState: { type?: string } = JSON.parse(visualization.visState); + return { + type: visState.type || '_na_', + space, + past_days: getPastDays(lastUpdated), + }; + }); + + // organize stats per type + const visTypes = groupBy(visSummaries, 'type'); + + // get the final result + return mapValues(visTypes, (curr) => { + const total = curr.length; + const spacesBreakdown = countBy(curr, 'space'); + const spaceCounts: number[] = values(spacesBreakdown); + + return { + total, + spaces_min: min(spaceCounts), + spaces_max: max(spaceCounts), + spaces_avg: total / spaceCounts.length, + saved_7_days_total: curr.filter((c) => c.past_days <= 7).length, + saved_30_days_total: curr.filter((c) => c.past_days <= 30).length, + saved_90_days_total: curr.filter((c) => c.past_days <= 90).length, + }; + }); +} + +export function getUsageCollector(config: Observable<{ kibana: { index: string } }>) { + return { + type: VIS_USAGE_TYPE, + isReady: () => true, + fetch: async (callCluster: LegacyAPICaller) => { + const index = (await config.pipe(first()).toPromise()).kibana.index; + return await getStats(callCluster, index); + }, + }; +} diff --git a/src/plugins/visualizations/server/usage_collector/index.ts b/src/plugins/visualizations/server/usage_collector/index.ts new file mode 100644 index 0000000000000..90ee65bb6ad2a --- /dev/null +++ b/src/plugins/visualizations/server/usage_collector/index.ts @@ -0,0 +1,31 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { Observable } from 'rxjs'; + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { getUsageCollector } from './get_usage_collector'; + +export function registerVisualizationsCollector( + collectorSet: UsageCollectionSetup, + config: Observable<{ kibana: { index: string } }> +): void { + const collector = collectorSet.makeUsageCollector(getUsageCollector(config)); + collectorSet.registerCollector(collector); +} diff --git a/src/plugins/visualize/kibana.json b/src/plugins/visualize/kibana.json index 29fcd30184cb2..318a1562efdfe 100644 --- a/src/plugins/visualize/kibana.json +++ b/src/plugins/visualize/kibana.json @@ -5,7 +5,7 @@ "ui": true, "requiredPlugins": [ "data", - "kibanaLegacy", + "urlForwarding", "navigation", "savedObjects", "visualizations", diff --git a/src/plugins/visualize/public/application/components/visualize_no_match.tsx b/src/plugins/visualize/public/application/components/visualize_no_match.tsx index 7776c5e8ce486..98f22f25c666e 100644 --- a/src/plugins/visualize/public/application/components/visualize_no_match.tsx +++ b/src/plugins/visualize/public/application/components/visualize_no_match.tsx @@ -34,7 +34,7 @@ export const VisualizeNoMatch = () => { useEffect(() => { services.restorePreviousUrl(); - const { navigated } = services.kibanaLegacy.navigateToLegacyKibanaUrl( + const { navigated } = services.urlForwarding.navigateToLegacyKibanaUrl( services.history.location.pathname ); diff --git a/src/plugins/visualize/public/application/types.ts b/src/plugins/visualize/public/application/types.ts index 0a12dbc22a744..4bdd19113dddc 100644 --- a/src/plugins/visualize/public/application/types.ts +++ b/src/plugins/visualize/public/application/types.ts @@ -43,7 +43,7 @@ import { import { SharePluginStart } from 'src/plugins/share/public'; import { SavedObjectsStart, SavedObject } from 'src/plugins/saved_objects/public'; import { EmbeddableStart } from 'src/plugins/embeddable/public'; -import { KibanaLegacyStart } from 'src/plugins/kibana_legacy/public'; +import { UrlForwardingStart } from 'src/plugins/url_forwarding/public'; import { DashboardStart } from '../../../dashboard/public'; export type PureVisState = SavedVisState; @@ -95,7 +95,7 @@ export interface VisualizeServices extends CoreStart { embeddable: EmbeddableStart; history: History; kbnUrlStateStorage: IKbnUrlStateStorage; - kibanaLegacy: KibanaLegacyStart; + urlForwarding: UrlForwardingStart; pluginInitializerContext: PluginInitializerContext; chrome: ChromeStart; data: DataPublicPluginStart; diff --git a/src/plugins/visualize/public/application/utils/migrate_legacy_query.ts b/src/plugins/visualize/public/application/utils/migrate_legacy_query.ts new file mode 100644 index 0000000000000..8d9b50d5a66b2 --- /dev/null +++ b/src/plugins/visualize/public/application/utils/migrate_legacy_query.ts @@ -0,0 +1,37 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { has } from 'lodash'; +import { Query } from 'src/plugins/data/public'; + +/** + * Creates a standardized query object from old queries that were either strings or pure ES query DSL + * + * @param query - a legacy query, what used to be stored in SearchSource's query property + * @return Object + */ + +export function migrateLegacyQuery(query: Query | { [key: string]: any } | string): Query { + // Lucene was the only option before, so language-less queries are all lucene + if (!has(query, 'language')) { + return { query, language: 'lucene' }; + } + + return query as Query; +} diff --git a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx index 935d4b26c98c9..24381ecfc9e2d 100644 --- a/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx +++ b/src/plugins/visualize/public/application/utils/use/use_visualize_app_state.tsx @@ -24,7 +24,7 @@ import { EventEmitter } from 'events'; import { i18n } from '@kbn/i18n'; import { MarkdownSimple, toMountPoint } from '../../../../../kibana_react/public'; -import { migrateLegacyQuery } from '../../../../../kibana_legacy/public'; +import { migrateLegacyQuery } from '../migrate_legacy_query'; import { esFilters, connectToQueryState } from '../../../../../data/public'; import { VisualizeServices, diff --git a/src/plugins/visualize/public/plugin.ts b/src/plugins/visualize/public/plugin.ts index 7e5cafd3ceecc..95d5343d5d695 100644 --- a/src/plugins/visualize/public/plugin.ts +++ b/src/plugins/visualize/public/plugin.ts @@ -40,7 +40,7 @@ import { import { DataPublicPluginStart, DataPublicPluginSetup, esFilters } from '../../data/public'; import { NavigationPublicPluginStart as NavigationStart } from '../../navigation/public'; import { SharePluginStart, SharePluginSetup } from '../../share/public'; -import { KibanaLegacySetup, KibanaLegacyStart } from '../../kibana_legacy/public'; +import { UrlForwardingSetup, UrlForwardingStart } from '../../url_forwarding/public'; import { VisualizationsStart } from '../../visualizations/public'; import { VisualizeConstants } from './application/visualize_constants'; import { FeatureCatalogueCategory, HomePublicPluginSetup } from '../../home/public'; @@ -66,7 +66,7 @@ export interface VisualizePluginStartDependencies { share?: SharePluginStart; visualizations: VisualizationsStart; embeddable: EmbeddableStart; - kibanaLegacy: KibanaLegacyStart; + urlForwarding: UrlForwardingStart; savedObjects: SavedObjectsStart; dashboard: DashboardStart; uiActions: UiActionsStart; @@ -74,7 +74,7 @@ export interface VisualizePluginStartDependencies { export interface VisualizePluginSetupDependencies { home?: HomePublicPluginSetup; - kibanaLegacy: KibanaLegacySetup; + urlForwarding: UrlForwardingSetup; data: DataPublicPluginSetup; share?: SharePluginSetup; } @@ -90,7 +90,7 @@ export class VisualizePlugin public async setup( core: CoreSetup, - { home, kibanaLegacy, data, share }: VisualizePluginSetupDependencies + { home, urlForwarding, data, share }: VisualizePluginSetupDependencies ) { const { appMounted, @@ -177,7 +177,7 @@ export class VisualizePlugin useHash: coreStart.uiSettings.get('state:storeInSessionStorage'), ...withNotifyOnErrors(coreStart.notifications.toasts), }), - kibanaLegacy: pluginsStart.kibanaLegacy, + urlForwarding: pluginsStart.urlForwarding, pluginInitializerContext: this.initializerContext, chrome: coreStart.chrome, data: pluginsStart.data, @@ -209,7 +209,7 @@ export class VisualizePlugin }, }); - kibanaLegacy.forwardApp('visualize', 'visualize'); + urlForwarding.forwardApp('visualize', 'visualize'); if (home) { home.featureCatalogue.register({ diff --git a/tasks/config/run.js b/tasks/config/run.js index 132b51765b3ed..148be6ea8afaa 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -154,12 +154,6 @@ module.exports = function () { args: ['scripts/test_hardening.js'], }), - test_package_safer_lodash_set: scriptWithGithubChecks({ - title: '@elastic/safer-lodash-set tests', - cmd: YARN, - args: ['--cwd', 'packages/elastic-safer-lodash-set', 'test'], - }), - apiIntegrationTests: scriptWithGithubChecks({ title: 'API integration tests', cmd: NODE, diff --git a/tasks/jenkins.js b/tasks/jenkins.js index adfb6f0f46868..90efadf41c435 100644 --- a/tasks/jenkins.js +++ b/tasks/jenkins.js @@ -38,7 +38,6 @@ module.exports = function (grunt) { 'run:test_jest_integration', 'run:test_projects', 'run:test_hardening', - 'run:test_package_safer_lodash_set', 'run:apiIntegrationTests', ]); }; diff --git a/test/api_integration/apis/saved_objects/migrations.ts b/test/api_integration/apis/saved_objects/migrations.ts index 9997d9710e212..99a58620b17f5 100644 --- a/test/api_integration/apis/saved_objects/migrations.ts +++ b/test/api_integration/apis/saved_objects/migrations.ts @@ -379,14 +379,12 @@ async function migrateIndex({ index, migrations, mappingProperties, - validateDoc, obsoleteIndexTemplatePattern, }: { esClient: ElasticsearchClient; index: string; migrations: Record; mappingProperties: SavedObjectsTypeMappingDefinitions; - validateDoc?: (doc: any) => void; obsoleteIndexTemplatePattern?: string; }) { const typeRegistry = new SavedObjectTypeRegistry(); @@ -396,7 +394,6 @@ async function migrateIndex({ const documentMigrator = new DocumentMigrator({ kibanaVersion: '99.9.9', typeRegistry, - validateDoc: validateDoc || _.noop, log: getLogMock(), }); diff --git a/test/api_integration/apis/stats/stats.js b/test/api_integration/apis/stats/stats.js index a40427fea8b94..0972f0ebebf0c 100644 --- a/test/api_integration/apis/stats/stats.js +++ b/test/api_integration/apis/stats/stats.js @@ -55,7 +55,12 @@ const assertStatsAndMetrics = (body) => { export default function ({ getService }) { const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + describe('kibana stats api', () => { + before('make sure there are some saved objects', () => esArchiver.load('saved_objects/basic')); + after('cleanup saved objects changes', () => esArchiver.unload('saved_objects/basic')); + describe('basic', () => { it('should return the stats without cluster_uuid with no query string params', () => { return supertest diff --git a/test/api_integration/apis/telemetry/telemetry_local.js b/test/api_integration/apis/telemetry/telemetry_local.js index 8b10f412fae27..d2d61705b763d 100644 --- a/test/api_integration/apis/telemetry/telemetry_local.js +++ b/test/api_integration/apis/telemetry/telemetry_local.js @@ -38,8 +38,12 @@ function flatKeys(source) { export default function ({ getService }) { const supertest = getService('supertest'); const es = getService('es'); + const esArchiver = getService('esArchiver'); describe('/api/telemetry/v2/clusters/_stats', () => { + before('make sure there are some saved objects', () => esArchiver.load('saved_objects/basic')); + after('cleanup saved objects changes', () => esArchiver.unload('saved_objects/basic')); + before('create some telemetry-data tracked indices', async () => { return es.indices.create({ index: 'filebeat-telemetry_tests_logs' }); }); diff --git a/test/common/services/security/test_user.ts b/test/common/services/security/test_user.ts index 104094f5b6fb5..83eac78621a53 100644 --- a/test/common/services/security/test_user.ts +++ b/test/common/services/security/test_user.ts @@ -65,9 +65,9 @@ export async function createTestUserService( } return new (class TestUser { - async restoreDefaults() { + async restoreDefaults(shouldRefreshBrowser: boolean = true) { if (isEnabled()) { - await this.setRoles(config.get('security.defaultRoles')); + await this.setRoles(config.get('security.defaultRoles'), shouldRefreshBrowser); } } diff --git a/test/functional/apps/management/_create_index_pattern_wizard.js b/test/functional/apps/management/_create_index_pattern_wizard.js index 9760527371408..8b11a02099f61 100644 --- a/test/functional/apps/management/_create_index_pattern_wizard.js +++ b/test/functional/apps/management/_create_index_pattern_wizard.js @@ -66,6 +66,18 @@ export default function ({ getService, getPageObjects }) { await PageObjects.settings.createIndexPattern('alias1', false); }); + + after(async () => { + await es.transport.request({ + path: '/_aliases', + method: 'POST', + body: { actions: [{ remove: { index: 'blogs', alias: 'alias1' } }] }, + }); + await es.transport.request({ + path: '/blogs', + method: 'DELETE', + }); + }); }); }); } diff --git a/test/functional/services/index.ts b/test/functional/services/index.ts index 4c97d672bae2e..057ae0bd13b6e 100644 --- a/test/functional/services/index.ts +++ b/test/functional/services/index.ts @@ -42,6 +42,7 @@ import { FilterBarProvider } from './filter_bar'; import { FlyoutProvider } from './flyout'; import { GlobalNavProvider } from './global_nav'; import { InspectorProvider } from './inspector'; +import { ManagementMenuProvider } from './management'; import { QueryBarProvider } from './query_bar'; import { RemoteProvider } from './remote'; import { RenderableProvider } from './renderable'; @@ -91,4 +92,5 @@ export const services = { savedQueryManagementComponent: SavedQueryManagementComponentProvider, elasticChart: ElasticChartProvider, supertest: KibanaSupertestProvider, + managementMenu: ManagementMenuProvider, }; diff --git a/src/plugins/kibana_legacy/common/kbn_base_url.ts b/test/functional/services/management/index.ts similarity index 93% rename from src/plugins/kibana_legacy/common/kbn_base_url.ts rename to test/functional/services/management/index.ts index 69711626750ea..54cd229a8e858 100644 --- a/src/plugins/kibana_legacy/common/kbn_base_url.ts +++ b/test/functional/services/management/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export const kbnBaseUrl = '/app/kibana'; +export { ManagementMenuProvider } from './management_menu'; diff --git a/test/functional/services/management/management_menu.ts b/test/functional/services/management/management_menu.ts new file mode 100644 index 0000000000000..9aed490bc6998 --- /dev/null +++ b/test/functional/services/management/management_menu.ts @@ -0,0 +1,51 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { FtrProviderContext } from 'test/functional/ftr_provider_context'; + +export function ManagementMenuProvider({ getService }: FtrProviderContext) { + const find = getService('find'); + + class ManagementMenu { + public async getSections() { + const sectionsElements = await find.allByCssSelector( + '.mgtSideBarNav > .euiSideNav__content > .euiSideNavItem' + ); + + const sections = []; + + for (const el of sectionsElements) { + const sectionId = await (await el.findByClassName('euiSideNavItemButton')).getAttribute( + 'data-test-subj' + ); + const sectionLinks = await Promise.all( + (await el.findAllByCssSelector('.euiSideNavItem > a.euiSideNavItemButton')).map((item) => + item.getAttribute('data-test-subj') + ) + ); + + sections.push({ sectionId, sectionLinks }); + } + + return sections; + } + } + + return new ManagementMenu(); +} diff --git a/test/plugin_functional/plugins/index_patterns/server/plugin.ts b/test/plugin_functional/plugins/index_patterns/server/plugin.ts index d6a4fdd67b0a1..1c85f226623cb 100644 --- a/test/plugin_functional/plugins/index_patterns/server/plugin.ts +++ b/test/plugin_functional/plugins/index_patterns/server/plugin.ts @@ -78,7 +78,7 @@ export class IndexPatternsTestPlugin const id = (req.params as Record).id; const service = await data.indexPatterns.indexPatternsServiceFactory(req); const ip = await service.get(id); - await ip.save(); + await service.save(ip); return res.ok(); } ); diff --git a/test/scripts/test/safer_lodash_set.sh b/test/scripts/test/safer_lodash_set.sh deleted file mode 100755 index 4d7f9c28210d1..0000000000000 --- a/test/scripts/test/safer_lodash_set.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -source src/dev/ci_setup/setup_env.sh - -yarn run grunt run:test_package_safer_lodash_set diff --git a/vars/tasks.groovy b/vars/tasks.groovy index 52641ce31f0be..edd2c0aa47401 100644 --- a/vars/tasks.groovy +++ b/vars/tasks.groovy @@ -34,7 +34,6 @@ def test() { kibanaPipeline.scriptTask('Jest Unit Tests', 'test/scripts/test/jest_unit.sh'), kibanaPipeline.scriptTask('API Integration Tests', 'test/scripts/test/api_integration.sh'), - kibanaPipeline.scriptTask('@elastic/safer-lodash-set Tests', 'test/scripts/test/safer_lodash_set.sh'), kibanaPipeline.scriptTask('X-Pack SIEM cyclic dependency', 'test/scripts/test/xpack_siem_cyclic_dependency.sh'), kibanaPipeline.scriptTask('X-Pack List cyclic dependency', 'test/scripts/test/xpack_list_cyclic_dependency.sh'), kibanaPipeline.scriptTask('X-Pack Jest Unit Tests', 'test/scripts/test/xpack_jest_unit.sh'), diff --git a/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts b/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts index f4363a8e57b37..c2ec5662ad12e 100644 --- a/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts +++ b/x-pack/legacy/plugins/xpack_main/server/xpack_main.d.ts @@ -5,7 +5,7 @@ */ import KbnServer from 'src/legacy/server/kbn_server'; -import { Feature, FeatureConfig } from '../../../../plugins/features/server'; +import { KibanaFeature } from '../../../../plugins/features/server'; import { XPackInfo, XPackInfoOptions } from './lib/xpack_info'; export { XPackFeature } from './lib/xpack_info'; diff --git a/x-pack/package.json b/x-pack/package.json index 899eca1095923..1e2fa4d7ee550 100644 --- a/x-pack/package.json +++ b/x-pack/package.json @@ -195,7 +195,7 @@ "jsdom": "13.1.0", "jsondiffpatch": "0.4.1", "jsts": "^1.6.2", - "kea": "2.2.0-rc.4", + "kea": "^2.2.0", "loader-utils": "^1.2.3", "lz-string": "^1.4.4", "madge": "3.4.4", @@ -207,7 +207,7 @@ "mocha": "^7.1.1", "mocha-junit-reporter": "^1.23.1", "mochawesome": "^4.1.0", - "mochawesome-merge": "^2.0.1", + "mochawesome-merge": "^4.1.0", "mustache": "^2.3.0", "mutation-observer": "^1.0.3", "node-fetch": "^2.6.0", @@ -268,7 +268,7 @@ "vinyl-fs": "^3.0.3", "whatwg-fetch": "^3.0.0", "xml-crypto": "^1.4.0", - "yargs": "4.8.1" + "yargs": "^15.4.1" }, "dependencies": { "@babel/core": "^7.11.1", diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 868f6f180cc91..c55b21b2f9029 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -19,7 +19,7 @@ Table of Contents - [Usage](#usage) - [Kibana Actions Configuration](#kibana-actions-configuration) - [Configuration Options](#configuration-options) - - [Adding Built-in Action Types to allowedHosts](#adding-built-in-action-types-to-hosts-allow-list) + - [Adding Built-in Action Types to allowedHosts](#adding-built-in-action-types-to-allowedhosts) - [Configuration Utilities](#configuration-utilities) - [Action types](#action-types) - [Methods](#methods) @@ -74,13 +74,21 @@ Table of Contents - [`secrets`](#secrets-7) - [`params`](#params-7) - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-1) + - [`subActionParams (issueTypes)`](#subactionparams-issuetypes) + - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2) - [IBM Resilient](#ibm-resilient) - [`config`](#config-8) - [`secrets`](#secrets-8) - [`params`](#params-8) - - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-2) + - [`subActionParams (pushToService)`](#subactionparams-pushtoservice-3) - [Command Line Utility](#command-line-utility) - [Developing New Action Types](#developing-new-action-types) + - [licensing](#licensing) + - [plugin location](#plugin-location) + - [documentation](#documentation) + - [tests](#tests) + - [action type config and secrets](#action-type-config-and-secrets) + - [user interface](#user-interface) ## Terminology @@ -103,12 +111,12 @@ Implemented under the [Actions Config](./server/actions_config.ts). Built-In-Actions are configured using the _xpack.actions_ namespoace under _kibana.yml_, and have the following configuration options: -| Namespaced Key | Description | Type | -| -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | -| _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean | -| _xpack.actions._**allowedHosts** | Which _hostnames_ are allowed for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array | -| _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array | -| _xpack.actions._**preconfigured** | A object of action id / preconfigured actions. Default: `{}` | Array | +| Namespaced Key | Description | Type | +| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| _xpack.actions._**enabled** | Feature toggle which enabled Actions in Kibana. | boolean | +| _xpack.actions._**allowedHosts** | Which _hostnames_ are allowed for the Built-In-Action? This list should contain hostnames of every external service you wish to interact with using Webhooks, Email or any other built in Action. Note that you may use the string "\*" in place of a specific hostname to enable Kibana to target any URL, but keep in mind the potential use of such a feature to execute [SSRF](https://www.owasp.org/index.php/Server_Side_Request_Forgery) attacks from your server. | Array | +| _xpack.actions._**enabledActionTypes** | A list of _actionTypes_ id's that are enabled. A "\*" may be used as an element to indicate all registered actionTypes should be enabled. The actionTypes registered for Kibana are `.server-log`, `.slack`, `.email`, `.index`, `.pagerduty`, `.webhook`. Default: `["*"]` | Array | +| _xpack.actions._**preconfigured** | A object of action id / preconfigured actions. Default: `{}` | Array | #### Adding Built-in Action Types to allowedHosts @@ -120,14 +128,14 @@ Uniquely, the _PagerDuty Action Type_ has been configured to support the service This module provides a Utilities for interacting with the configuration. -| Method | Arguments | Description | Return Type | -| ------------------------- | ------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | -| isUriAllowed | _uri_: The URI you wish to validate is allowed | Validates whether the URI is allowed. This checks the configuration and validates that the hostname of the URI is in the list of allowed Hosts and returns `true` if it is allowed. If the configuration says that all URI's are allowed (using an "\*") then it will always return `true`. | Boolean | -| isHostnameAllowed | _hostname_: The Hostname you wish to validate is allowed | Validates whether the Hostname is allowed. This checks the configuration and validates that the hostname is in the list of allowed Hosts and returns `true` if it is allowed. If the configuration says that all Hostnames are allowed (using an "\*") then it will always return `true`. | Boolean | -| isActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Returns true if the actionType is enabled, otherwise false. | Boolean | -| ensureUriAllowed | _uri_: The URI you wish to validate is allowed | Validates whether the URI is allowed. This checks the configuration and validates that the hostname of the URI is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all URI's are allowed (using an "\*") then it will never throw. | No return value, throws if URI isn't allowed | -| ensureHostnameAllowed | _hostname_: The Hostname you wish to validate is allowed | Validates whether the Hostname is allowed. This checks the configuration and validates that the hostname is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all Hostnames are allowed (using an "\*") then it will never throw | No return value, throws if Hostname isn't allowed . | -| ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled | +| Method | Arguments | Description | Return Type | +| ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | +| isUriAllowed | _uri_: The URI you wish to validate is allowed | Validates whether the URI is allowed. This checks the configuration and validates that the hostname of the URI is in the list of allowed Hosts and returns `true` if it is allowed. If the configuration says that all URI's are allowed (using an "\*") then it will always return `true`. | Boolean | +| isHostnameAllowed | _hostname_: The Hostname you wish to validate is allowed | Validates whether the Hostname is allowed. This checks the configuration and validates that the hostname is in the list of allowed Hosts and returns `true` if it is allowed. If the configuration says that all Hostnames are allowed (using an "\*") then it will always return `true`. | Boolean | +| isActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Returns true if the actionType is enabled, otherwise false. | Boolean | +| ensureUriAllowed | _uri_: The URI you wish to validate is allowed | Validates whether the URI is allowed. This checks the configuration and validates that the hostname of the URI is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all URI's are allowed (using an "\*") then it will never throw. | No return value, throws if URI isn't allowed | +| ensureHostnameAllowed | _hostname_: The Hostname you wish to validate is allowed | Validates whether the Hostname is allowed. This checks the configuration and validates that the hostname is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all Hostnames are allowed (using an "\*") then it will never throw | No return value, throws if Hostname isn't allowed . | +| ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled | ## Action types @@ -323,15 +331,17 @@ const result = await actionsClient.execute({ Kibana ships with a set of built-in action types: -| Type | Id | Description | -| ------------------------- | ------------- | ------------------------------------------------------------------ | -| [Server log](#server-log) | `.server-log` | Logs messages to the Kibana log using Kibana's logger | -| [Email](#email) | `.email` | Sends an email using SMTP | -| [Slack](#slack) | `.slack` | Posts a message to a slack channel | -| [Index](#index) | `.index` | Indexes document(s) into Elasticsearch | -| [Webhook](#webhook) | `.webhook` | Send a payload to a web service using HTTP POST or PUT | -| [PagerDuty](#pagerduty) | `.pagerduty` | Trigger, resolve, or acknowlege an incident to a PagerDuty service | -| [ServiceNow](#servicenow) | `.servicenow` | Create or update an incident to a ServiceNow instance | +| Type | Id | Description | +| ------------------------------- | ------------- | ------------------------------------------------------------------ | +| [Server log](#server-log) | `.server-log` | Logs messages to the Kibana log using Kibana's logger | +| [Email](#email) | `.email` | Sends an email using SMTP | +| [Slack](#slack) | `.slack` | Posts a message to a slack channel | +| [Index](#index) | `.index` | Indexes document(s) into Elasticsearch | +| [Webhook](#webhook) | `.webhook` | Send a payload to a web service using HTTP POST or PUT | +| [PagerDuty](#pagerduty) | `.pagerduty` | Trigger, resolve, or acknowlege an incident to a PagerDuty service | +| [ServiceNow](#servicenow) | `.servicenow` | Create or update an incident to a ServiceNow instance | +| [Jira](#jira) | `.jira` | Create or update an issue to a Jira instance | +| [IBM Resilient](#ibm-resilient) | `.resilient` | Create or update an incident to a IBM Resilient instance | --- @@ -442,7 +452,7 @@ The config and params properties are modelled after the [Watcher Index Action](h | index | The Elasticsearch index to index into. | string _(optional)_ | | doc_id | The optional \_id of the document. | string _(optional)_ | | execution_time_field | The field that will store/index the action execution time. | string _(optional)_ | -| refresh | Setting of the refresh policy for the write request. | boolean _(optional)_ | +| refresh | Setting of the refresh policy for the write request. | boolean _(optional)_ | ### `secrets` @@ -450,9 +460,9 @@ This action type has no `secrets` properties. ### `params` -| Property | Description | Type | -| --------- | ---------------------------------------- | ------------------- | -| documents | JSON object that describes the [document](https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-index.html#getting-started-batch-processing). | object[] | +| Property | Description | Type | +| --------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | +| documents | JSON object that describes the [document](https://www.elastic.co/guide/en/elasticsearch/reference/current/getting-started-index.html#getting-started-batch-processing). | object[] | --- @@ -529,10 +539,10 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a ### `config` -| Property | Description | Type | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -| apiUrl | ServiceNow instance URL. | string | -| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the ServiceNow field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'short_description', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in ServiceNow and will be overwrite on each update. | object | +| Property | Description | Type | +| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------- | +| apiUrl | ServiceNow instance URL. | string | +| incidentConfiguration | Optional property and specific to **Cases only**. If defined, the object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the ServiceNow field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'short_description', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in ServiceNow and will be overwrite on each update. | object _(optional)_ | ### `secrets` @@ -550,13 +560,17 @@ The ServiceNow action uses the [V2 Table API](https://developer.servicenow.com/a #### `subActionParams (pushToService)` -| Property | Description | Type | -| ----------- | -------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| caseId | The case id | string | -| title | The title of the case | string _(optional)_ | -| description | The description of the case | string _(optional)_ | -| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | -| externalId | The id of the incident in ServiceNow . If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| Property | Description | Type | +| ------------- | ------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| savedObjectId | The id of the saved object. | string | +| title | The title of the incident. | string _(optional)_ | +| description | The description of the incident. | string _(optional)_ | +| comment | A comment. | string _(optional)_ | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | +| externalId | The id of the incident in ServiceNow. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| severity | The name of the severity in ServiceNow. | string _(optional)_ | +| urgency | The name of the urgency in ServiceNow. | string _(optional)_ | +| impact | The name of the impact in ServiceNow. | string _(optional)_ | --- @@ -568,34 +582,47 @@ The Jira action uses the [V2 API](https://developer.atlassian.com/cloud/jira/pla ### `config` -| Property | Description | Type | -| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | -| apiUrl | Jira instance URL. | string | -| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in Jira and will be overwrite on each update. | object | +| Property | Description | Type | +| --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | +| apiUrl | Jira instance URL. | string | +| incidentConfiguration | Optional property and specific to **Cases only**. if defined, the object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in Jira and will be overwrite on each update. | object _(optional)_ | ### `secrets` -| Property | Description | Type | -| -------- | --------------------------------------- | ------ | -| email | email for HTTP Basic authentication | string | -| apiToken | API token for HTTP Basic authentication | string | +| Property | Description | Type | +| -------- | ----------------------------------------------------- | ------ | +| email | email (or username) for HTTP Basic authentication | string | +| apiToken | API token (or password) for HTTP Basic authentication | string | ### `params` -| Property | Description | Type | -| --------------- | ------------------------------------------------------------------------------------ | ------ | -| subAction | The sub action to perform. It can be `pushToService`, `handshake`, and `getIncident` | string | -| subActionParams | The parameters of the sub action | object | +| Property | Description | Type | +| --------------- | ----------------------------------------------------------------------------------------------------------------------- | ------ | +| subAction | The sub action to perform. It can be `pushToService`, `handshake`, `getIncident`, `issueTypes`, and `fieldsByIssueType` | string | +| subActionParams | The parameters of the sub action | object | + +#### `subActionParams (pushToService)` + +| Property | Description | Type | +| ------------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- | +| savedObjectId | The id of the saved object | string | +| title | The title of the issue | string _(optional)_ | +| description | The description of the issue | string _(optional)_ | +| externalId | The id of the issue in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| issueType | The id of the issue type in Jira. | string _(optional)_ | +| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | +| labels | An array of labels. | string[] _(optional)_ | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | + +#### `subActionParams (issueTypes)` + +No parameters for `issueTypes` sub-action. Provide an empty object `{}`. #### `subActionParams (pushToService)` -| Property | Description | Type | -| ----------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- | -| caseId | The case id | string | -| title | The title of the case | string _(optional)_ | -| description | The description of the case | string _(optional)_ | -| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | -| externalId | The id of the incident in Jira. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| Property | Description | Type | +| -------- | -------------------------------- | ------ | +| id | The id of the issue type in Jira | string | ## IBM Resilient @@ -603,10 +630,10 @@ ID: `.resilient` ### `config` -| Property | Description | Type | -| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------ | -| apiUrl | IBM Resilient instance URL. | string | -| casesConfiguration | Case configuration object. The object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in IBM Resilient and will be overwrite on each update. | object | +| Property | Description | Type | +| --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------ | +| apiUrl | IBM Resilient instance URL. | string | +| incidentConfiguration | Optional property and specific to **Cases only**. If defined, the object should contain an attribute called `mapping`. A `mapping` is an array of objects. Each mapping object should be of the form `{ source: string, target: string, actionType: string }`. `source` is the Case field. `target` is the Jira field where `source` will be mapped to. `actionType` can be one of `nothing`, `overwrite` or `append`. For example the `{ source: 'title', target: 'summary', actionType: 'overwrite' }` record, inside mapping array, means that the title of a case will be mapped to the short description of an incident in IBM Resilient and will be overwrite on each update. | object | ### `secrets` @@ -624,13 +651,15 @@ ID: `.resilient` #### `subActionParams (pushToService)` -| Property | Description | Type | -| ----------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| caseId | The case id | string | -| title | The title of the case | string _(optional)_ | -| description | The description of the case | string _(optional)_ | -| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | -| externalId | The id of the incident in IBM Resilient. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| Property | Description | Type | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- | +| savedObjectId | The id of the saved object | string | +| title | The title of the incident | string _(optional)_ | +| description | The description of the incident | string _(optional)_ | +| comments | The comments of the incident. A comment is of the form `{ commentId: string, version: string, comment: string }` | object[] _(optional)_ | +| externalId | The id of the incident in IBM Resilient. If presented the incident will be update. Otherwise a new incident will be created. | string _(optional)_ | +| incidentTypes | An array with the ids of IBM Resilient incident types. | number[] _(optional)_ | +| severityCode | IBM Resilient id of the severity code. | number _(optional)_ | # Command Line Utility @@ -660,30 +689,30 @@ Consider working with the alerting team on early structure /design feedback of n ## licensing -Currently actions are licensed as "basic" if the action only interacts with the stack, eg the server log and es index actions. Other actions are at least "gold" level. +Currently actions are licensed as "basic" if the action only interacts with the stack, eg the server log and es index actions. Other actions are at least "gold" level. ## plugin location -Currently actions that are licensed as "basic" **MUST** be implemented in the actions plugin, other actions can be implemented in any other plugin that pre-reqs the actions plugin. If the new action is generic across the stack, it probably belongs in the actions plugin, but if your action is very specific to a plugin/solution, it might be easiest to implement it in the plugin/solution. Keep in mind that if Kibana is run without the plugin being enabled, any actions defined in that plugin will not run, nor will those actions be available via APIs or UI. +Currently actions that are licensed as "basic" **MUST** be implemented in the actions plugin, other actions can be implemented in any other plugin that pre-reqs the actions plugin. If the new action is generic across the stack, it probably belongs in the actions plugin, but if your action is very specific to a plugin/solution, it might be easiest to implement it in the plugin/solution. Keep in mind that if Kibana is run without the plugin being enabled, any actions defined in that plugin will not run, nor will those actions be available via APIs or UI. -Actions that take URLs or hostnames should check that those values are allowed. The allowed host list utilities are currently internal to the actions plugin, and so such actions will need to be implemented in the actions plugin. Longer-term, we will expose these utilities so they can be used by alerts implemented in other plugins; see [issue #64659](https://github.com/elastic/kibana/issues/64659). +Actions that take URLs or hostnames should check that those values are allowed. The allowed host list utilities are currently internal to the actions plugin, and so such actions will need to be implemented in the actions plugin. Longer-term, we will expose these utilities so they can be used by alerts implemented in other plugins; see [issue #64659](https://github.com/elastic/kibana/issues/64659). ## documentation -You should also create some asciidoc for the new action type. An entry should be made in the action type index - [`docs/user/alerting/action-types.asciidoc`](../../../docs/user/alerting/action-types.asciidoc) which points to a new document for the action type that should be in the directory [`docs/user/alerting/action-types`](../../../docs/user/alerting/action-types). +You should also create some asciidoc for the new action type. An entry should be made in the action type index - [`docs/user/alerting/action-types.asciidoc`](../../../docs/user/alerting/action-types.asciidoc) which points to a new document for the action type that should be in the directory [`docs/user/alerting/action-types`](../../../docs/user/alerting/action-types). ## tests -The action type should have both jest tests and functional tests. For functional tests, if your action interacts with a 3rd party service via HTTP, you may be able to create a simulator for your service, to test with. See the existing functional test servers in the directory [`x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server`](../../test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server) +The action type should have both jest tests and functional tests. For functional tests, if your action interacts with a 3rd party service via HTTP, you may be able to create a simulator for your service, to test with. See the existing functional test servers in the directory [`x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server`](../../test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server) ## action type config and secrets -Action types must define `config` and `secrets` which are used to create connectors. This data should be described with `@kbn/config-schema` object schemas, and you **MUST NOT** use `schema.maybe()` to define properties. +Action types must define `config` and `secrets` which are used to create connectors. This data should be described with `@kbn/config-schema` object schemas, and you **MUST NOT** use `schema.maybe()` to define properties. -This is due to the fact that the structures are persisted in saved objects, which performs partial updates on the persisted data. If a property value is already persisted, but an update either doesn't include the property, or sets it to `undefined`, the persisted value will not be changed. Beyond this being a semantic error in general, it also ends up invalidating the encryption used to save secrets, and will render the secrets will not be able to be unencrypted later. +This is due to the fact that the structures are persisted in saved objects, which performs partial updates on the persisted data. If a property value is already persisted, but an update either doesn't include the property, or sets it to `undefined`, the persisted value will not be changed. Beyond this being a semantic error in general, it also ends up invalidating the encryption used to save secrets, and will render the secrets will not be able to be unencrypted later. -Instead of `schema.maybe()`, use `schema.nullable()`, which is the same as `schema.maybe()` except that when passed an `undefined` value, the object returned from the validation will be set to `null`. The resulting type will be `property-type | null`, whereas with `schema.maybe()` it would be `property-type | undefined`. +Instead of `schema.maybe()`, use `schema.nullable()`, which is the same as `schema.maybe()` except that when passed an `undefined` value, the object returned from the validation will be set to `null`. The resulting type will be `property-type | null`, whereas with `schema.maybe()` it would be `property-type | undefined`. ## user interface -In order to make this action usable in the Kibana UI, you will need to provide all the UI editing aspects of the action. The existing action type user interfaces are defined in [`x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types`](../triggers_actions_ui/public/application/components/builtin_action_types). For more information, see the [UI documentation](../triggers_actions_ui/README.md#create-and-register-new-action-type-ui). +In order to make this action usable in the Kibana UI, you will need to provide all the UI editing aspects of the action. The existing action type user interfaces are defined in [`x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types`](../triggers_actions_ui/public/application/components/builtin_action_types). For more information, see the [UI documentation](../triggers_actions_ui/README.md#create-and-register-new-action-type-ui). diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts index a48124cdbcb6a..14573161b8d5d 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.test.ts @@ -85,7 +85,9 @@ describe('ensureAuthorized', () => { await actionsAuthorization.ensureAuthorized('create', 'myType'); expect(authorization.actions.savedObject.get).toHaveBeenCalledWith('action', 'create'); - expect(checkPrivileges).toHaveBeenCalledWith(mockAuthorizationAction('action', 'create')); + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: mockAuthorizationAction('action', 'create'), + }); expect(auditLogger.actionsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.actionsAuthorizationFailure).not.toHaveBeenCalled(); @@ -131,10 +133,12 @@ describe('ensureAuthorized', () => { ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create' ); - expect(checkPrivileges).toHaveBeenCalledWith([ - mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'), - mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'), - ]); + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: [ + mockAuthorizationAction(ACTION_SAVED_OBJECT_TYPE, 'get'), + mockAuthorizationAction(ACTION_TASK_PARAMS_SAVED_OBJECT_TYPE, 'create'), + ], + }); expect(auditLogger.actionsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.actionsAuthorizationFailure).not.toHaveBeenCalled(); diff --git a/x-pack/plugins/actions/server/authorization/actions_authorization.ts b/x-pack/plugins/actions/server/authorization/actions_authorization.ts index da5a5a1cdc3eb..3ba798ddf1715 100644 --- a/x-pack/plugins/actions/server/authorization/actions_authorization.ts +++ b/x-pack/plugins/actions/server/authorization/actions_authorization.ts @@ -42,11 +42,11 @@ export class ActionsAuthorization { const { authorization } = this; if (authorization?.mode?.useRbacForRequest(this.request)) { const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested, username } = await checkPrivileges( - operationAlias[operation] + const { hasAllRequested, username } = await checkPrivileges({ + kibana: operationAlias[operation] ? operationAlias[operation](authorization) - : authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation) - ); + : authorization.actions.savedObject.get(ACTION_SAVED_OBJECT_TYPE, operation), + }); if (hasAllRequested) { this.auditLogger.actionsAuthorizationSuccess(username, operation, actionTypeId); } else { diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/api.ts b/x-pack/plugins/actions/server/builtin_action_types/case/api.ts deleted file mode 100644 index de4b7edaed3da..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/case/api.ts +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - ExternalServiceApi, - ExternalServiceParams, - PushToServiceResponse, - GetIncidentApiHandlerArgs, - HandshakeApiHandlerArgs, - PushToServiceApiHandlerArgs, -} from './types'; -import { prepareFieldsForTransformation, transformFields, transformComments } from './utils'; - -const handshakeHandler = async ({ - externalService, - mapping, - params, -}: HandshakeApiHandlerArgs) => {}; -const getIncidentHandler = async ({ - externalService, - mapping, - params, -}: GetIncidentApiHandlerArgs) => {}; - -const pushToServiceHandler = async ({ - externalService, - mapping, - params, -}: PushToServiceApiHandlerArgs): Promise => { - const { externalId, comments } = params; - const updateIncident = externalId ? true : false; - const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; - let currentIncident: ExternalServiceParams | undefined; - let res: PushToServiceResponse; - - if (externalId) { - currentIncident = await externalService.getIncident(externalId); - } - - const fields = prepareFieldsForTransformation({ - externalCase: params.externalCase, - mapping, - defaultPipes, - }); - - const incident = transformFields({ - params, - fields, - currentIncident, - }); - - if (updateIncident) { - res = await externalService.updateIncident({ incidentId: externalId, incident }); - } else { - res = await externalService.createIncident({ incident }); - } - - if ( - comments && - Array.isArray(comments) && - comments.length > 0 && - mapping.get('comments')?.actionType !== 'nothing' - ) { - const commentsTransformed = transformComments(comments, ['informationAdded']); - - res.comments = []; - for (const currentComment of commentsTransformed) { - const comment = await externalService.createComment({ - incidentId: res.id, - comment: currentComment, - field: mapping.get('comments')?.target ?? 'comments', - }); - res.comments = [ - ...(res.comments ?? []), - { - commentId: comment.commentId, - pushedDate: comment.pushedDate, - }, - ]; - } - } - - return res; -}; - -export const api: ExternalServiceApi = { - handshake: handshakeHandler, - pushToService: pushToServiceHandler, - getIncident: getIncidentHandler, -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts index f47686c911ff0..5a23eb89339e6 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/schema.ts @@ -18,36 +18,18 @@ export const MapRecordSchema = schema.object({ actionType: MappingActionType, }); -export const CaseConfigurationSchema = schema.object({ +export const IncidentConfigurationSchema = schema.object({ mapping: schema.arrayOf(MapRecordSchema), }); -export const ExternalIncidentServiceConfiguration = { - apiUrl: schema.string(), - casesConfiguration: CaseConfigurationSchema, -}; - -export const ExternalIncidentServiceConfigurationSchema = schema.object( - ExternalIncidentServiceConfiguration -); - -export const ExternalIncidentServiceSecretConfiguration = { - password: schema.string(), - username: schema.string(), -}; - -export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( - ExternalIncidentServiceSecretConfiguration -); - export const UserSchema = schema.object({ fullName: schema.nullable(schema.string()), username: schema.nullable(schema.string()), }); -const EntityInformation = { - createdAt: schema.string(), - createdBy: UserSchema, +export const EntityInformation = { + createdAt: schema.nullable(schema.string()), + createdBy: schema.nullable(UserSchema), updatedAt: schema.nullable(schema.string()), updatedBy: schema.nullable(UserSchema), }; @@ -59,40 +41,3 @@ export const CommentSchema = schema.object({ comment: schema.string(), ...EntityInformation, }); - -export const ExecutorSubActionSchema = schema.oneOf([ - schema.literal('getIncident'), - schema.literal('pushToService'), - schema.literal('handshake'), -]); - -export const ExecutorSubActionPushParamsSchema = schema.object({ - savedObjectId: schema.string(), - title: schema.string(), - description: schema.nullable(schema.string()), - comments: schema.nullable(schema.arrayOf(CommentSchema)), - externalId: schema.nullable(schema.string()), - ...EntityInformation, -}); - -export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ - externalId: schema.string(), -}); - -// Reserved for future implementation -export const ExecutorSubActionHandshakeParamsSchema = schema.object({}); - -export const ExecutorParamsSchema = schema.oneOf([ - schema.object({ - subAction: schema.literal('getIncident'), - subActionParams: ExecutorSubActionGetIncidentParamsSchema, - }), - schema.object({ - subAction: schema.literal('handshake'), - subActionParams: ExecutorSubActionHandshakeParamsSchema, - }), - schema.object({ - subAction: schema.literal('pushToService'), - subActionParams: ExecutorSubActionPushParamsSchema, - }), -]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts index 1030e3d9c5d8e..73d8297c638df 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/types.ts @@ -4,74 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -// This will have to remain `any` until we can extend connectors with generics -// and circular dependencies eliminated. -/* eslint-disable @typescript-eslint/no-explicit-any */ - import { TypeOf } from '@kbn/config-schema'; -import { Logger } from '../../../../../../src/core/server'; - import { - ExternalIncidentServiceConfigurationSchema, - ExternalIncidentServiceSecretConfigurationSchema, - ExecutorParamsSchema, - CaseConfigurationSchema, + IncidentConfigurationSchema, MapRecordSchema, CommentSchema, - ExecutorSubActionPushParamsSchema, - ExecutorSubActionGetIncidentParamsSchema, - ExecutorSubActionHandshakeParamsSchema, + EntityInformationSchema, } from './schema'; -import { LicenseType } from '../../../../../legacy/common/constants'; - -export interface AnyParams { - [index: string]: string | number | object | undefined | null; -} - -export type ExternalIncidentServiceConfiguration = TypeOf< - typeof ExternalIncidentServiceConfigurationSchema ->; -export type ExternalIncidentServiceSecretConfiguration = TypeOf< - typeof ExternalIncidentServiceSecretConfigurationSchema ->; - -export type ExecutorParams = TypeOf; -export type ExecutorSubActionPushParams = TypeOf; -export type ExecutorSubActionGetIncidentParams = TypeOf< - typeof ExecutorSubActionGetIncidentParamsSchema ->; - -export type ExecutorSubActionHandshakeParams = TypeOf< - typeof ExecutorSubActionHandshakeParamsSchema ->; - -export type CaseConfiguration = TypeOf; +export type IncidentConfiguration = TypeOf; export type MapRecord = TypeOf; export type Comment = TypeOf; - -export interface ExternalServiceConfiguration { - id: string; - name: string; - minimumLicenseRequired: LicenseType; -} - -export interface ExternalServiceCredentials { - config: Record; - secrets: Record; -} - -export interface ExternalServiceValidation { - config: (configurationUtilities: any, configObject: any) => void; - secrets: (configurationUtilities: any, secrets: any) => void; -} - -export interface ExternalServiceIncidentResponse { - id: string; - title: string; - url: string; - pushedDate: string; -} +export type EntityInformation = TypeOf; export interface ExternalServiceCommentResponse { commentId: string; @@ -79,69 +23,6 @@ export interface ExternalServiceCommentResponse { externalCommentId?: string; } -export interface ExternalServiceParams { - [index: string]: any; -} - -export interface ExternalService { - getIncident: (id: string) => Promise; - createIncident: (params: ExternalServiceParams) => Promise; - updateIncident: (params: ExternalServiceParams) => Promise; - createComment: (params: ExternalServiceParams) => Promise; -} - -export interface PushToServiceApiParams extends ExecutorSubActionPushParams { - externalCase: Record; -} - -export interface ExternalServiceApiHandlerArgs { - externalService: ExternalService; - mapping: Map; -} - -export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { - params: PushToServiceApiParams; -} - -export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs { - params: ExecutorSubActionGetIncidentParams; -} - -export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { - params: ExecutorSubActionHandshakeParams; -} - -export interface PushToServiceResponse extends ExternalServiceIncidentResponse { - comments?: ExternalServiceCommentResponse[]; -} - -export interface ExternalServiceApi { - handshake: (args: HandshakeApiHandlerArgs) => Promise; - pushToService: (args: PushToServiceApiHandlerArgs) => Promise; - getIncident: (args: GetIncidentApiHandlerArgs) => Promise; -} - -export interface CreateExternalServiceBasicArgs { - api: ExternalServiceApi; - createExternalService: ( - credentials: ExternalServiceCredentials, - logger: Logger, - proxySettings?: any - ) => ExternalService; - logger: Logger; -} - -export interface CreateExternalServiceArgs extends CreateExternalServiceBasicArgs { - config: ExternalServiceConfiguration; - validate: ExternalServiceValidation; - validationSchema: { config: any; secrets: any }; -} - -export interface CreateActionTypeArgs { - configurationUtilities: any; - executor?: any; -} - export interface PipedField { key: string; value: string; @@ -149,16 +30,10 @@ export interface PipedField { pipes: string[]; } -export interface PrepareFieldsForTransformArgs { - externalCase: Record; - mapping: Map; - defaultPipes?: string[]; -} - -export interface TransformFieldsArgs { - params: PushToServiceApiParams; +export interface TransformFieldsArgs { + params: P; fields: PipedField[]; - currentIncident?: ExternalServiceParams; + currentIncident?: S; } export interface TransformerArgs { @@ -167,3 +42,13 @@ export interface TransformerArgs { user?: string; previousValue?: string; } + +export interface AnyParams { + [index: string]: string | number | object | undefined | null; +} + +export interface PrepareFieldsForTransformArgs { + externalCase: Record; + mapping: Map; + defaultPipes?: string[]; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts index 2e3cee3946d61..600e18eb5daff 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.test.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { normalizeMapping, buildMap, @@ -14,7 +16,23 @@ import { } from './utils'; import { SUPPORTED_SOURCE_FIELDS } from './constants'; -import { Comment, MapRecord, PushToServiceApiParams } from './types'; +import { Comment, MapRecord } from './types'; + +interface Entity { + createdAt: string | null; + createdBy: { fullName: string; username: string } | null; + updatedAt: string | null; + updatedBy: { fullName: string; username: string } | null; +} + +interface PushToServiceApiParams extends Entity { + savedObjectId: string; + title: string; + description: string | null; + externalId: string | null; + externalObject: Record; + comments: Comment[]; +} const mapping: MapRecord[] = [ { source: 'title', target: 'short_description', actionType: 'overwrite' }, @@ -22,7 +40,6 @@ const mapping: MapRecord[] = [ { source: 'comments', target: 'comments', actionType: 'append' }, ]; -// eslint-disable-next-line @typescript-eslint/no-explicit-any const finalMapping: Map = new Map(); finalMapping.set('title', { @@ -61,7 +78,7 @@ const fullParams: PushToServiceApiParams = { updatedAt: null, updatedBy: null, externalId: null, - externalCase: { + externalObject: { short_description: 'a title', description: 'a description', }, @@ -154,7 +171,7 @@ describe('mapParams', () => { describe('prepareFieldsForTransformation', () => { test('prepare fields with defaults', () => { const res = prepareFieldsForTransformation({ - externalCase: fullParams.externalCase, + externalCase: fullParams.externalObject, mapping: finalMapping, }); expect(res).toEqual([ @@ -175,7 +192,7 @@ describe('prepareFieldsForTransformation', () => { test('prepare fields with default pipes', () => { const res = prepareFieldsForTransformation({ - externalCase: fullParams.externalCase, + externalCase: fullParams.externalObject, mapping: finalMapping, defaultPipes: ['myTestPipe'], }); @@ -199,11 +216,15 @@ describe('prepareFieldsForTransformation', () => { describe('transformFields', () => { test('transform fields for creation correctly', () => { const fields = prepareFieldsForTransformation({ - externalCase: fullParams.externalCase, + externalCase: fullParams.externalObject, mapping: finalMapping, }); - const res = transformFields({ + const res = transformFields< + PushToServiceApiParams, + {}, + { short_description: string; description: string } + >({ params: fullParams, fields, }); @@ -216,12 +237,16 @@ describe('transformFields', () => { test('transform fields for update correctly', () => { const fields = prepareFieldsForTransformation({ - externalCase: fullParams.externalCase, + externalCase: fullParams.externalObject, mapping: finalMapping, defaultPipes: ['informationUpdated'], }); - const res = transformFields({ + const res = transformFields< + PushToServiceApiParams, + {}, + { short_description: string; description: string } + >({ params: { ...fullParams, updatedAt: '2020-03-15T08:34:53.450Z', @@ -245,12 +270,16 @@ describe('transformFields', () => { test('add newline character to description', () => { const fields = prepareFieldsForTransformation({ - externalCase: fullParams.externalCase, + externalCase: fullParams.externalObject, mapping: finalMapping, defaultPipes: ['informationUpdated'], }); - const res = transformFields({ + const res = transformFields< + PushToServiceApiParams, + {}, + { short_description: string; description: string } + >({ params: fullParams, fields, currentIncident: { @@ -263,11 +292,15 @@ describe('transformFields', () => { test('append username if fullname is undefined when create', () => { const fields = prepareFieldsForTransformation({ - externalCase: fullParams.externalCase, + externalCase: fullParams.externalObject, mapping: finalMapping, }); - const res = transformFields({ + const res = transformFields< + PushToServiceApiParams, + {}, + { short_description: string; description: string } + >({ params: { ...fullParams, createdBy: { fullName: '', username: 'elastic' }, @@ -283,12 +316,16 @@ describe('transformFields', () => { test('append username if fullname is undefined when update', () => { const fields = prepareFieldsForTransformation({ - externalCase: fullParams.externalCase, + externalCase: fullParams.externalObject, mapping: finalMapping, defaultPipes: ['informationUpdated'], }); - const res = transformFields({ + const res = transformFields< + PushToServiceApiParams, + {}, + { short_description: string; description: string } + >({ params: { ...fullParams, updatedAt: '2020-03-15T08:34:53.450Z', diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts index d895bf386a367..3d51f5e826279 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/case/utils.ts @@ -4,30 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ -import { curry, flow, get } from 'lodash'; -import { schema } from '@kbn/config-schema'; - -import { ActionTypeExecutorOptions, ActionTypeExecutorResult, ActionType } from '../../types'; - -import { ExecutorParamsSchema } from './schema'; -import { - ExternalIncidentServiceConfiguration, - ExternalIncidentServiceSecretConfiguration, -} from './types'; +import { flow, get } from 'lodash'; import { - CreateExternalServiceArgs, - CreateActionTypeArgs, - ExecutorParams, MapRecord, - AnyParams, - CreateExternalServiceBasicArgs, - PrepareFieldsForTransformArgs, - PipedField, TransformFieldsArgs, Comment, - ExecutorSubActionPushParams, - PushToServiceResponse, + EntityInformation, + PipedField, + AnyParams, + PrepareFieldsForTransformArgs, } from './types'; import { transformers } from './transformers'; @@ -51,10 +37,7 @@ export const buildMap = (mapping: MapRecord[]): Map => { }, new Map()); }; -export const mapParams = ( - params: Partial, - mapping: Map -): AnyParams => { +export const mapParams = (params: T, mapping: Map): AnyParams => { return Object.keys(params).reduce((prev: AnyParams, curr: string): AnyParams => { const field = mapping.get(curr); if (field) { @@ -64,89 +47,6 @@ export const mapParams = ( }, {}); }; -export const createConnectorExecutor = ({ - api, - createExternalService, - logger, -}: CreateExternalServiceBasicArgs) => async ( - execOptions: ActionTypeExecutorOptions< - ExternalIncidentServiceConfiguration, - ExternalIncidentServiceSecretConfiguration, - ExecutorParams - > -): Promise> => { - const { actionId, config, params, secrets } = execOptions; - const { subAction, subActionParams } = params; - let data = {}; - - const res: ActionTypeExecutorResult = { - status: 'ok', - actionId, - }; - - const externalService = createExternalService( - { - config, - secrets, - }, - logger, - execOptions.proxySettings - ); - - if (!api[subAction]) { - throw new Error('[Action][ExternalService] Unsupported subAction type.'); - } - - if (subAction !== 'pushToService') { - throw new Error('[Action][ExternalService] subAction not implemented.'); - } - - if (subAction === 'pushToService') { - const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; - const { comments, externalId, ...restParams } = pushToServiceParams; - - const mapping = buildMap(config.casesConfiguration.mapping); - const externalCase = mapParams(restParams, mapping); - - data = await api.pushToService({ - externalService, - mapping, - params: { ...pushToServiceParams, externalCase }, - }); - } - - return { - ...res, - data, - }; -}; - -export const createConnector = ({ - api, - config, - validate, - createExternalService, - validationSchema, - logger, -}: CreateExternalServiceArgs) => { - return ({ - configurationUtilities, - executor = createConnectorExecutor({ api, createExternalService, logger }), - }: CreateActionTypeArgs): ActionType => ({ - ...config, - validate: { - config: schema.object(validationSchema.config, { - validate: curry(validate.config)(configurationUtilities), - }), - secrets: schema.object(validationSchema.secrets, { - validate: curry(validate.secrets)(configurationUtilities), - }), - params: ExecutorParamsSchema, - }, - executor, - }); -}; - export const prepareFieldsForTransformation = ({ externalCase, mapping, @@ -165,11 +65,15 @@ export const prepareFieldsForTransformation = ({ }); }; -export const transformFields = ({ +export const transformFields = < + P extends EntityInformation, + S extends Record, + R extends {} +>({ params, fields, currentIncident, -}: TransformFieldsArgs): Record => { +}: TransformFieldsArgs): R => { return fields.reduce((prev, cur) => { const transform = flow(...cur.pipes.map((p) => transformers[p])); return { @@ -177,18 +81,11 @@ export const transformFields = ({ [cur.key]: transform({ value: cur.value, date: params.updatedAt ?? params.createdAt, - user: - (params.updatedBy != null - ? params.updatedBy.fullName - ? params.updatedBy.fullName - : params.updatedBy.username - : params.createdBy.fullName - ? params.createdBy.fullName - : params.createdBy.username) ?? '', + user: getEntity(params), previousValue: currentIncident ? currentIncident[cur.key] : '', }).value, }; - }, {}); + }, {} as R); }; export const transformComments = (comments: Comment[], pipes: string[]): Comment[] => { @@ -197,18 +94,18 @@ export const transformComments = (comments: Comment[], pipes: string[]): Comment comment: flow(...pipes.map((p) => transformers[p]))({ value: c.comment, date: c.updatedAt ?? c.createdAt, - user: - (c.updatedBy != null - ? c.updatedBy.fullName - ? c.updatedBy.fullName - : c.updatedBy.username - : c.createdBy.fullName - ? c.createdBy.fullName - : c.createdBy.username) ?? '', + user: getEntity(c), }).value, })); }; -export const getErrorMessage = (connector: string, msg: string) => { - return `[Action][${connector}]: ${msg}`; -}; +export const getEntity = (entity: EntityInformation): string => + (entity.updatedBy != null + ? entity.updatedBy.fullName + ? entity.updatedBy.fullName + : entity.updatedBy.username + : entity.createdBy != null + ? entity.createdBy.fullName + ? entity.createdBy.fullName + : entity.createdBy.username + : '') ?? ''; diff --git a/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts deleted file mode 100644 index 08e8a8be6a3e6..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/case/validators.ts +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isEmpty } from 'lodash'; - -import { ActionsConfigurationUtilities } from '../../actions_config'; -import { - ExternalIncidentServiceConfiguration, - ExternalIncidentServiceSecretConfiguration, -} from './types'; - -import * as i18n from './translations'; - -export const validateCommonConfig = ( - configurationUtilities: ActionsConfigurationUtilities, - configObject: ExternalIncidentServiceConfiguration -) => { - try { - if (isEmpty(configObject.casesConfiguration.mapping)) { - return i18n.MAPPING_EMPTY; - } - - configurationUtilities.ensureUriAllowed(configObject.apiUrl); - } catch (allowListError) { - return i18n.WHITE_LISTED_ERROR(allowListError.message); - } -}; - -export const validateCommonSecrets = ( - configurationUtilities: ActionsConfigurationUtilities, - secrets: ExternalIncidentServiceSecretConfiguration -) => {}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts index bcfb82077d286..4495c37f758ee 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.test.ts @@ -4,15 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -import { api } from '../case/api'; +import { Logger } from '../../../../../../src/core/server'; import { externalServiceMock, mapping, apiParams } from './mocks'; -import { ExternalService } from '../case/types'; +import { ExternalService } from './types'; +import { api } from './api'; +let mockedLogger: jest.Mocked; describe('api', () => { let externalService: jest.Mocked; beforeEach(() => { externalService = externalServiceMock.create(); + jest.clearAllMocks(); }); afterEach(() => { @@ -20,10 +23,15 @@ describe('api', () => { }); describe('pushToService', () => { - describe('create incident', () => { + describe('create incident - cases', () => { test('it creates an incident', async () => { const params = { ...apiParams, externalId: null }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-1', @@ -45,7 +53,12 @@ describe('api', () => { test('it creates an incident without comments', async () => { const params = { ...apiParams, externalId: null, comments: [] }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-1', @@ -57,7 +70,7 @@ describe('api', () => { test('it calls createIncident correctly', async () => { const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.createIncident).toHaveBeenCalledWith({ incident: { @@ -69,9 +82,25 @@ describe('api', () => { expect(externalService.updateIncident).not.toHaveBeenCalled(); }); + test('it calls createIncident correctly without mapping', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + + expect(externalService.createIncident).toHaveBeenCalledWith({ + incident: { + description: 'Incident description', + summary: 'Incident title', + issueType: '10006', + labels: ['kibana', 'elastic'], + priority: 'High', + }, + }); + expect(externalService.updateIncident).not.toHaveBeenCalled(); + }); + test('it calls createComment correctly', async () => { const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { incidentId: 'incident-1', @@ -89,7 +118,6 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', }); expect(externalService.createComment).toHaveBeenNthCalledWith(2, { @@ -108,14 +136,59 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', + }); + }); + + test('it calls createComment correctly without mapping', async () => { + const params = { ...apiParams, externalId: null }; + await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, }); }); }); describe('update incident', () => { test('it updates an incident', async () => { - const res = await api.pushToService({ externalService, mapping, params: apiParams }); + const res = await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-1', @@ -137,7 +210,12 @@ describe('api', () => { test('it updates an incident without comments', async () => { const params = { ...apiParams, comments: [] }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + logger: mockedLogger, + }); expect(res).toEqual({ id: 'incident-1', @@ -149,7 +227,7 @@ describe('api', () => { test('it calls updateIncident correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', @@ -162,9 +240,26 @@ describe('api', () => { expect(externalService.createIncident).not.toHaveBeenCalled(); }); + test('it calls updateIncident correctly without mapping', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + + expect(externalService.updateIncident).toHaveBeenCalledWith({ + incidentId: 'incident-3', + incident: { + description: 'Incident description', + summary: 'Incident title', + issueType: '10006', + labels: ['kibana', 'elastic'], + priority: 'High', + }, + }); + expect(externalService.createIncident).not.toHaveBeenCalled(); + }); + test('it calls createComment correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { incidentId: 'incident-1', @@ -182,7 +277,6 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', }); expect(externalService.createComment).toHaveBeenNthCalledWith(2, { @@ -201,7 +295,87 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', + }); + }); + + test('it calls createComment correctly without mapping', async () => { + const params = { ...apiParams }; + await api.pushToService({ externalService, mapping: null, params, logger: mockedLogger }); + expect(externalService.createComment).toHaveBeenCalledTimes(2); + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-1', + comment: 'A comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + incidentId: 'incident-1', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment', + createdAt: '2020-04-27T10:59:46.202Z', + createdBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + updatedAt: '2020-04-27T10:59:46.202Z', + updatedBy: { + fullName: 'Elastic User', + username: 'elastic', + }, + }, + }); + }); + }); + + describe('issueTypes', () => { + test('it returns the issue types correctly', async () => { + const res = await api.issueTypes({ + externalService, + params: {}, + }); + expect(res).toEqual([ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ]); + }); + }); + + describe('fieldsByIssueType', () => { + test('it returns the fields correctly', async () => { + const res = await api.fieldsByIssueType({ + externalService, + params: { id: '10006' }, + }); + expect(res).toEqual({ + summary: { allowedValues: [], defaultValue: {} }, + priority: { + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { name: 'Medium', id: '3' }, + }, }); }); }); @@ -228,7 +402,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -260,7 +439,12 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -291,7 +475,12 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -324,7 +513,12 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: {}, @@ -352,7 +546,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -382,7 +581,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -414,7 +618,12 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -445,7 +654,12 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -478,7 +692,12 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -509,7 +728,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.createComment).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts index 3db66e5884af4..a64eb7a2036ca 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/api.ts @@ -4,4 +4,134 @@ * you may not use this file except in compliance with the Elastic License. */ -export { api } from '../case/api'; +import { + ExternalServiceParams, + PushToServiceApiHandlerArgs, + HandshakeApiHandlerArgs, + GetIncidentApiHandlerArgs, + ExternalServiceApi, + Incident, + GetFieldsByIssueTypeHandlerArgs, + GetIssueTypesHandlerArgs, + PushToServiceApiParams, + PushToServiceResponse, +} from './types'; + +// TODO: to remove, need to support Case +import { prepareFieldsForTransformation, transformFields, transformComments } from '../case/utils'; + +const handshakeHandler = async ({ + externalService, + mapping, + params, +}: HandshakeApiHandlerArgs) => {}; + +const getIncidentHandler = async ({ + externalService, + mapping, + params, +}: GetIncidentApiHandlerArgs) => {}; + +const getIssueTypesHandler = async ({ externalService }: GetIssueTypesHandlerArgs) => { + const res = await externalService.getIssueTypes(); + return res; +}; + +const getFieldsByIssueTypeHandler = async ({ + externalService, + params, +}: GetFieldsByIssueTypeHandlerArgs) => { + const { id } = params; + const res = await externalService.getFieldsByIssueType(id); + return res; +}; + +const pushToServiceHandler = async ({ + externalService, + mapping, + params, + logger, +}: PushToServiceApiHandlerArgs): Promise => { + const { externalId, comments } = params; + const updateIncident = externalId ? true : false; + const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; + let currentIncident: ExternalServiceParams | undefined; + let res: PushToServiceResponse; + + if (externalId) { + try { + currentIncident = await externalService.getIncident(externalId); + } catch (ex) { + logger.debug( + `Retrieving Incident by id ${externalId} from Jira failed with exception: ${ex}` + ); + } + } + + let incident: Incident; + // TODO: should be removed later but currently keep it for the Case implementation support + if (mapping) { + const fields = prepareFieldsForTransformation({ + externalCase: params.externalObject, + mapping, + defaultPipes, + }); + + incident = transformFields({ + params, + fields, + currentIncident, + }); + } else { + const { title, description, priority, labels, issueType } = params; + incident = { summary: title, description, priority, labels, issueType }; + } + + if (externalId != null) { + res = await externalService.updateIncident({ + incidentId: externalId, + incident, + }); + } else { + res = await externalService.createIncident({ + incident: { + ...incident, + }, + }); + } + + if (comments && Array.isArray(comments) && comments.length > 0) { + if (mapping && mapping.get('comments')?.actionType === 'nothing') { + return res; + } + + const commentsTransformed = mapping + ? transformComments(comments, ['informationAdded']) + : comments; + + res.comments = []; + for (const currentComment of commentsTransformed) { + const comment = await externalService.createComment({ + incidentId: res.id, + comment: currentComment, + }); + res.comments = [ + ...(res.comments ?? []), + { + commentId: comment.commentId, + pushedDate: comment.pushedDate, + }, + ]; + } + } + + return res; +}; + +export const api: ExternalServiceApi = { + handshake: handshakeHandler, + pushToService: pushToServiceHandler, + getIncident: getIncidentHandler, + issueTypes: getIssueTypesHandler, + fieldsByIssueType: getFieldsByIssueTypeHandler, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts deleted file mode 100644 index 54f28e447010a..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/config.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ExternalServiceConfiguration } from '../case/types'; -import * as i18n from './translations'; - -export const config: ExternalServiceConfiguration = { - id: '.jira', - name: i18n.NAME, - minimumLicenseRequired: 'gold', -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index 66be0bad02d7b..d3346557f3684 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -4,33 +4,138 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger } from '../../../../../../src/core/server'; -import { createConnector } from '../case/utils'; -import { ActionType } from '../../types'; +import { curry } from 'lodash'; +import { schema } from '@kbn/config-schema'; -import { api } from './api'; -import { config } from './config'; import { validate } from './validators'; -import { createExternalService } from './service'; -import { JiraSecretConfiguration, JiraPublicConfiguration } from './schema'; +import { + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, + ExecutorParamsSchema, +} from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; +import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; +import { createExternalService } from './service'; +import { api } from './api'; +import { + ExecutorParams, + ExecutorSubActionPushParams, + JiraPublicConfigurationType, + JiraSecretConfigurationType, + JiraExecutorResultData, + ExecutorSubActionGetFieldsByIssueTypeParams, + ExecutorSubActionGetIssueTypesParams, +} from './types'; +import * as i18n from './translations'; +import { Logger } from '../../../../../../src/core/server'; -export function getActionType({ - logger, - configurationUtilities, -}: { +// TODO: to remove, need to support Case +import { buildMap, mapParams } from '../case/utils'; + +interface GetActionTypeParams { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; -}): ActionType { - return createConnector({ - api, - config, - validate, - createExternalService, - validationSchema: { - config: JiraPublicConfiguration, - secrets: JiraSecretConfiguration, +} + +const supportedSubActions: string[] = ['pushToService', 'issueTypes', 'fieldsByIssueType']; + +// action type definition +export function getActionType( + params: GetActionTypeParams +): ActionType< + JiraPublicConfigurationType, + JiraSecretConfigurationType, + ExecutorParams, + JiraExecutorResultData | {} +> { + const { logger, configurationUtilities } = params; + return { + id: '.jira', + minimumLicenseRequired: 'gold', + name: i18n.NAME, + validate: { + config: schema.object(ExternalIncidentServiceConfiguration, { + validate: curry(validate.config)(configurationUtilities), + }), + secrets: schema.object(ExternalIncidentServiceSecretConfiguration, { + validate: curry(validate.secrets)(configurationUtilities), + }), + params: ExecutorParamsSchema, + }, + executor: curry(executor)({ logger }), + }; +} + +// action executor +async function executor( + { logger }: { logger: Logger }, + execOptions: ActionTypeExecutorOptions< + JiraPublicConfigurationType, + JiraSecretConfigurationType, + ExecutorParams + > +): Promise> { + const { actionId, config, params, secrets } = execOptions; + const { subAction, subActionParams } = params as ExecutorParams; + let data: JiraExecutorResultData | null = null; + + const externalService = createExternalService( + { + config, + secrets, }, logger, - })({ configurationUtilities }); + execOptions.proxySettings + ); + + if (!api[subAction]) { + const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (!supportedSubActions.includes(subAction)) { + const errorMessage = `[Action][ExternalService] subAction ${subAction} not implemented.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (subAction === 'pushToService') { + const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; + + const { comments, externalId, ...restParams } = pushToServiceParams; + const incidentConfiguration = config.incidentConfiguration; + const mapping = incidentConfiguration ? buildMap(incidentConfiguration.mapping) : null; + const externalObject = + config.incidentConfiguration && mapping + ? mapParams(restParams as ExecutorSubActionPushParams, mapping) + : {}; + + data = await api.pushToService({ + externalService, + mapping, + params: { ...pushToServiceParams, externalObject }, + logger, + }); + + logger.debug(`response push to service for incident id: ${data.id}`); + } + + if (subAction === 'issueTypes') { + const getIssueTypesParams = subActionParams as ExecutorSubActionGetIssueTypesParams; + data = await api.issueTypes({ + externalService, + params: getIssueTypesParams, + }); + } + + if (subAction === 'fieldsByIssueType') { + const getFieldsByIssueTypeParams = subActionParams as ExecutorSubActionGetFieldsByIssueTypeParams; + data = await api.fieldsByIssueType({ + externalService, + params: getFieldsByIssueTypeParams, + }); + } + + return { status: 'ok', data: data ?? {}, actionId }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts index 709d490a5227f..53f8d43ebc2d8 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/mocks.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ExternalService, - PushToServiceApiParams, - ExecutorSubActionPushParams, - MapRecord, -} from '../case/types'; +import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types'; + +import { MapRecord } from '../case/types'; const createMock = (): jest.Mocked => { const service = { @@ -40,6 +37,30 @@ const createMock = (): jest.Mocked => { }) ), createComment: jest.fn(), + findIncidents: jest.fn(), + getCapabilities: jest.fn(), + getIssueTypes: jest.fn().mockImplementation(() => [ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ]), + getFieldsByIssueType: jest.fn().mockImplementation(() => ({ + summary: { allowedValues: [], defaultValue: {} }, + priority: { + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { name: 'Medium', id: '3' }, + }, + })), }; service.createComment.mockImplementationOnce(() => @@ -96,6 +117,9 @@ const executorParams: ExecutorSubActionPushParams = { updatedBy: { fullName: 'Elastic User', username: 'elastic' }, title: 'Incident title', description: 'Incident description', + labels: ['kibana', 'elastic'], + priority: 'High', + issueType: '10006', comments: [ { commentId: 'case-comment-1', @@ -118,7 +142,7 @@ const executorParams: ExecutorSubActionPushParams = { const apiParams: PushToServiceApiParams = { ...executorParams, - externalCase: { summary: 'Incident title', description: 'Incident description' }, + externalObject: { summary: 'Incident title', description: 'Incident description' }, }; export { externalServiceMock, mapping, executorParams, apiParams }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index 9c831e75d91c1..9fee465e72efc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -5,18 +5,81 @@ */ import { schema } from '@kbn/config-schema'; -import { ExternalIncidentServiceConfiguration } from '../case/schema'; +import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '../case/schema'; -export const JiraPublicConfiguration = { +export const ExternalIncidentServiceConfiguration = { + apiUrl: schema.string(), projectKey: schema.string(), - ...ExternalIncidentServiceConfiguration, + // TODO: to remove - set it optional for the current stage to support Case Jira implementation + incidentConfiguration: schema.nullable(IncidentConfigurationSchema), + isCaseOwned: schema.nullable(schema.boolean()), }; -export const JiraPublicConfigurationSchema = schema.object(JiraPublicConfiguration); +export const ExternalIncidentServiceConfigurationSchema = schema.object( + ExternalIncidentServiceConfiguration +); -export const JiraSecretConfiguration = { +export const ExternalIncidentServiceSecretConfiguration = { email: schema.string(), apiToken: schema.string(), }; -export const JiraSecretConfigurationSchema = schema.object(JiraSecretConfiguration); +export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( + ExternalIncidentServiceSecretConfiguration +); + +export const ExecutorSubActionSchema = schema.oneOf([ + schema.literal('getIncident'), + schema.literal('pushToService'), + schema.literal('handshake'), + schema.literal('issueTypes'), + schema.literal('fieldsByIssueType'), +]); + +export const ExecutorSubActionPushParamsSchema = schema.object({ + savedObjectId: schema.string(), + title: schema.string(), + description: schema.nullable(schema.string()), + externalId: schema.nullable(schema.string()), + issueType: schema.nullable(schema.string()), + priority: schema.nullable(schema.string()), + labels: schema.nullable(schema.arrayOf(schema.string())), + // TODO: modify later to string[] - need for support Case schema + comments: schema.nullable(schema.arrayOf(CommentSchema)), + ...EntityInformation, +}); + +export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ + externalId: schema.string(), +}); + +// Reserved for future implementation +export const ExecutorSubActionHandshakeParamsSchema = schema.object({}); +export const ExecutorSubActionGetCapabilitiesParamsSchema = schema.object({}); +export const ExecutorSubActionGetIssueTypesParamsSchema = schema.object({}); +export const ExecutorSubActionGetFieldsByIssueTypeParamsSchema = schema.object({ + id: schema.string(), +}); + +export const ExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('getIncident'), + subActionParams: ExecutorSubActionGetIncidentParamsSchema, + }), + schema.object({ + subAction: schema.literal('handshake'), + subActionParams: ExecutorSubActionHandshakeParamsSchema, + }), + schema.object({ + subAction: schema.literal('pushToService'), + subActionParams: ExecutorSubActionPushParamsSchema, + }), + schema.object({ + subAction: schema.literal('issueTypes'), + subActionParams: ExecutorSubActionGetIssueTypesParamsSchema, + }), + schema.object({ + subAction: schema.literal('fieldsByIssueType'), + subActionParams: ExecutorSubActionGetFieldsByIssueTypeParamsSchema, + }), +]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index 547595b4c183f..2439c507c3328 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -8,11 +8,15 @@ import axios from 'axios'; import { createExternalService } from './service'; import * as utils from '../lib/axios_utils'; -import { ExternalService } from '../case/types'; +import { ExternalService } from './types'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; const logger = loggingSystemMock.create().get() as jest.Mocked; +interface ResponseError extends Error { + response?: { data: { errors: Record } }; +} + jest.mock('axios'); jest.mock('../lib/axios_utils', () => { const originalUtils = jest.requireActual('../lib/axios_utils'); @@ -25,6 +29,72 @@ jest.mock('../lib/axios_utils', () => { axios.create = jest.fn(() => axios); const requestMock = utils.request as jest.Mock; +const issueTypesResponse = { + data: { + projects: [ + { + issuetypes: [ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ], + }, + ], + }, +}; + +const fieldsResponse = { + data: { + projects: [ + { + issuetypes: [ + { + id: '10006', + name: 'Task', + fields: { + summary: { fieldId: 'summary' }, + priority: { + fieldId: 'priority', + allowedValues: [ + { + name: 'Highest', + id: '1', + }, + { + name: 'High', + id: '2', + }, + { + name: 'Medium', + id: '3', + }, + { + name: 'Low', + id: '4', + }, + { + name: 'Lowest', + id: '5', + }, + ], + defaultValue: { + name: 'Medium', + id: '3', + }, + }, + }, + }, + ], + }, + ], + }, +}; + describe('Jira service', () => { let service: ExternalService; @@ -116,19 +186,24 @@ describe('Jira service', () => { test('it should throw an error', async () => { requestMock.mockImplementation(() => { - throw new Error('An error has occurred'); + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { summary: 'Required field' } } }; + throw error; }); expect(service.getIncident('1')).rejects.toThrow( - 'Unable to get incident with id 1. Error: An error has occurred' + '[Action][Jira]: Unable to get incident with id 1. Error: An error has occurred Reason: Required field' ); }); }); describe('createIncident', () => { test('it creates the incident correctly', async () => { - // The response from Jira when creating an issue contains only the key and the id. - // The service makes two calls when creating an issue. One to create and one to get - // the created incident with all the necessary fields. + /* The response from Jira when creating an issue contains only the key and the id. + The function makes the following calls when creating an issue: + 1. Get issueTypes to set a default ONLY when incident.issueType is missing + 2. Create the issue. + 3. Get the created issue with all the necessary fields. + */ requestMock.mockImplementationOnce(() => ({ data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, })); @@ -138,7 +213,13 @@ describe('Jira service', () => { })); const res = await service.createIncident({ - incident: { summary: 'title', description: 'desc' }, + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: '10006', + priority: 'High', + }, }); expect(res).toEqual({ @@ -149,6 +230,68 @@ describe('Jira service', () => { }); }); + test('it creates the incident correctly without issue type', async () => { + /* The response from Jira when creating an issue contains only the key and the id. + The function makes the following calls when creating an issue: + 1. Get issueTypes to set a default ONLY when incident.issueType is missing + 2. Create the issue. + 3. Get the created issue with all the necessary fields. + */ + // getIssueType mocks + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + // getIssueType mocks + requestMock.mockImplementationOnce(() => issueTypesResponse); + + requestMock.mockImplementationOnce(() => ({ + data: { id: '1', key: 'CK-1', fields: { summary: 'title', description: 'description' } }, + })); + + requestMock.mockImplementationOnce(() => ({ + data: { id: '1', key: 'CK-1', fields: { created: '2020-04-27T10:59:46.202Z' } }, + })); + + const res = await service.createIncident({ + incident: { + summary: 'title', + description: 'desc', + labels: [], + priority: 'High', + issueType: null, + }, + }); + + expect(res).toEqual({ + title: 'CK-1', + id: '1', + pushedDate: '2020-04-27T10:59:46.202Z', + url: 'https://siem-kibana.atlassian.net/browse/CK-1', + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue', + logger, + method: 'post', + data: { + fields: { + summary: 'title', + description: 'desc', + project: { key: 'CK' }, + issuetype: { id: '10006' }, + labels: [], + priority: { name: 'High' }, + }, + }, + }); + }); + test('it should call request with correct arguments', async () => { requestMock.mockImplementation(() => ({ data: { @@ -159,7 +302,13 @@ describe('Jira service', () => { })); await service.createIncident({ - incident: { summary: 'title', description: 'desc' }, + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: '10006', + priority: 'High', + }, }); expect(requestMock).toHaveBeenCalledWith({ @@ -172,7 +321,9 @@ describe('Jira service', () => { summary: 'title', description: 'desc', project: { key: 'CK' }, - issuetype: { name: 'Task' }, + issuetype: { id: '10006' }, + labels: [], + priority: { name: 'High' }, }, }, }); @@ -180,14 +331,24 @@ describe('Jira service', () => { test('it should throw an error', async () => { requestMock.mockImplementation(() => { - throw new Error('An error has occurred'); + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { summary: 'Required field' } } }; + throw error; }); expect( service.createIncident({ - incident: { summary: 'title', description: 'desc' }, + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: '10006', + priority: 'High', + }, }) - ).rejects.toThrow('[Action][Jira]: Unable to create incident. Error: An error has occurred'); + ).rejects.toThrow( + '[Action][Jira]: Unable to create incident. Error: An error has occurred. Reason: Required field' + ); }); }); @@ -203,7 +364,13 @@ describe('Jira service', () => { const res = await service.updateIncident({ incidentId: '1', - incident: { summary: 'title', description: 'desc' }, + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: '10006', + priority: 'High', + }, }); expect(res).toEqual({ @@ -225,7 +392,13 @@ describe('Jira service', () => { await service.updateIncident({ incidentId: '1', - incident: { summary: 'title', description: 'desc' }, + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: '10006', + priority: 'High', + }, }); expect(requestMock).toHaveBeenCalledWith({ @@ -233,22 +406,39 @@ describe('Jira service', () => { logger, method: 'put', url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/1', - data: { fields: { summary: 'title', description: 'desc' } }, + data: { + fields: { + summary: 'title', + description: 'desc', + labels: [], + priority: { name: 'High' }, + issuetype: { id: '10006' }, + project: { key: 'CK' }, + }, + }, }); }); test('it should throw an error', async () => { requestMock.mockImplementation(() => { - throw new Error('An error has occurred'); + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { summary: 'Required field' } } }; + throw error; }); expect( service.updateIncident({ incidentId: '1', - incident: { summary: 'title', description: 'desc' }, + incident: { + summary: 'title', + description: 'desc', + labels: [], + issueType: '10006', + priority: 'High', + }, }) ).rejects.toThrow( - '[Action][Jira]: Unable to update incident with id 1. Error: An error has occurred' + '[Action][Jira]: Unable to update incident with id 1. Error: An error has occurred. Reason: Required field' ); }); }); @@ -265,8 +455,14 @@ describe('Jira service', () => { const res = await service.createComment({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'comments', + comment: { + comment: 'comment', + commentId: 'comment-1', + createdBy: null, + createdAt: null, + updatedAt: null, + updatedBy: null, + }, }); expect(res).toEqual({ @@ -287,8 +483,14 @@ describe('Jira service', () => { await service.createComment({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'my_field', + comment: { + comment: 'comment', + commentId: 'comment-1', + createdBy: null, + createdAt: null, + updatedAt: null, + updatedBy: null, + }, }); expect(requestMock).toHaveBeenCalledWith({ @@ -302,18 +504,416 @@ describe('Jira service', () => { test('it should throw an error', async () => { requestMock.mockImplementation(() => { - throw new Error('An error has occurred'); + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { summary: 'Required field' } } }; + throw error; }); expect( service.createComment({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'comments', + comment: { + comment: 'comment', + commentId: 'comment-1', + createdBy: null, + createdAt: null, + updatedAt: null, + updatedBy: null, + }, }) ).rejects.toThrow( - '[Action][Jira]: Unable to create comment at incident with id 1. Error: An error has occurred' + '[Action][Jira]: Unable to create comment at incident with id 1. Error: An error has occurred. Reason: Required field' + ); + }); + }); + + describe('getCapabilities', () => { + test('it should return the capabilities', async () => { + requestMock.mockImplementation(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + const res = await service.getCapabilities(); + expect(res).toEqual({ + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + await service.getCapabilities(); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + logger, + method: 'get', + url: 'https://siem-kibana.atlassian.net/rest/capabilities', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { capabilities: 'Could not get capabilities' } } }; + throw error; + }); + + expect(service.getCapabilities()).rejects.toThrow( + '[Action][Jira]: Unable to get capabilities. Error: An error has occurred. Reason: Could not get capabilities' ); }); }); + + describe('getIssueTypes', () => { + describe('Old API', () => { + test('it should return the issue types', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + requestMock.mockImplementationOnce(() => issueTypesResponse); + + const res = await service.getIssueTypes(); + + expect(res).toEqual([ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ]); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + requestMock.mockImplementationOnce(() => issueTypesResponse); + + await service.getIssueTypes(); + + expect(requestMock).toHaveBeenLastCalledWith({ + axios, + logger, + method: 'get', + url: + 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta?projectKeys=CK&expand=projects.issuetypes.fields', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + requestMock.mockImplementation(() => { + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } }; + throw error; + }); + + expect(service.getIssueTypes()).rejects.toThrow( + '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types' + ); + }); + }); + describe('New API', () => { + test('it should return the issue types', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + })); + + requestMock.mockImplementationOnce(() => ({ + data: { + values: issueTypesResponse.data.projects[0].issuetypes, + }, + })); + + const res = await service.getIssueTypes(); + + expect(res).toEqual([ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ]); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + })); + + requestMock.mockImplementationOnce(() => ({ + data: { + values: issueTypesResponse.data.projects[0].issuetypes, + }, + })); + + await service.getIssueTypes(); + + expect(requestMock).toHaveBeenLastCalledWith({ + axios, + logger, + method: 'get', + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + })); + + requestMock.mockImplementation(() => { + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } }; + throw error; + }); + + expect(service.getIssueTypes()).rejects.toThrow( + '[Action][Jira]: Unable to get issue types. Error: An error has occurred. Reason: Could not get issue types' + ); + }); + }); + }); + + describe('getFieldsByIssueType', () => { + describe('Old API', () => { + test('it should return the fields', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + requestMock.mockImplementationOnce(() => fieldsResponse); + + const res = await service.getFieldsByIssueType('10006'); + + expect(res).toEqual({ + priority: { + allowedValues: [ + { id: '1', name: 'Highest' }, + { id: '2', name: 'High' }, + { id: '3', name: 'Medium' }, + { id: '4', name: 'Low' }, + { id: '5', name: 'Lowest' }, + ], + defaultValue: { id: '3', name: 'Medium' }, + }, + summary: { allowedValues: [], defaultValue: {} }, + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + requestMock.mockImplementationOnce(() => fieldsResponse); + + await service.getFieldsByIssueType('10006'); + + expect(requestMock).toHaveBeenLastCalledWith({ + axios, + logger, + method: 'get', + url: + 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta?projectKeys=CK&issuetypeIds=10006&expand=projects.issuetypes.fields', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + navigation: 'https://siem-kibana.atlassian.net/rest/capabilities/navigation', + }, + }, + })); + + requestMock.mockImplementation(() => { + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { fields: 'Could not get fields' } } }; + throw error; + }); + + expect(service.getFieldsByIssueType('10006')).rejects.toThrow( + '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get fields' + ); + }); + }); + + describe('New API', () => { + test('it should return the fields', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + })); + + requestMock.mockImplementationOnce(() => ({ + data: { + values: [ + { fieldId: 'summary' }, + { + fieldId: 'priority', + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { + name: 'Medium', + id: '3', + }, + }, + ], + }, + })); + + const res = await service.getFieldsByIssueType('10006'); + + expect(res).toEqual({ + priority: { + allowedValues: [{ id: '3', name: 'Medium' }], + defaultValue: { id: '3', name: 'Medium' }, + }, + summary: { allowedValues: [], defaultValue: {} }, + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + })); + + requestMock.mockImplementationOnce(() => ({ + data: { + values: [ + { fieldId: 'summary' }, + { + fieldId: 'priority', + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { + name: 'Medium', + id: '3', + }, + }, + ], + }, + })); + + await service.getFieldsByIssueType('10006'); + + expect(requestMock).toHaveBeenLastCalledWith({ + axios, + logger, + method: 'get', + url: 'https://siem-kibana.atlassian.net/rest/api/2/issue/createmeta/CK/issuetypes/10006', + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementationOnce(() => ({ + data: { + capabilities: { + 'list-project-issuetypes': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-project-issuetypes', + 'list-issuetype-fields': + 'https://siem-kibana.atlassian.net/rest/capabilities/list-issuetype-fields', + }, + }, + })); + + requestMock.mockImplementation(() => { + const error: ResponseError = new Error('An error has occurred'); + error.response = { data: { errors: { issuetypes: 'Could not get issue types' } } }; + throw error; + }); + + expect(service.getFieldsByIssueType('10006')).rejects.toThrow( + '[Action][Jira]: Unable to get fields. Error: An error has occurred. Reason: Could not get issue types' + ); + }); + }); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts index aec73cfb375ed..84b6e70d2a100 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.ts @@ -6,14 +6,20 @@ import axios from 'axios'; -import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; import { Logger } from '../../../../../../src/core/server'; import { + ExternalServiceCredentials, + ExternalService, + CreateIncidentParams, + UpdateIncidentParams, JiraPublicConfigurationType, JiraSecretConfigurationType, - CreateIncidentRequest, - UpdateIncidentRequest, - CreateCommentRequest, + Fields, + CreateCommentParams, + Incident, + ResponseError, + ExternalServiceCommentResponse, + ExternalServiceIncidentResponse, } from './types'; import * as i18n from './translations'; @@ -22,11 +28,12 @@ import { ProxySettings } from '../../types'; const VERSION = '2'; const BASE_URL = `rest/api/${VERSION}`; -const INCIDENT_URL = `issue`; -const COMMENT_URL = `comment`; +const CAPABILITIES_URL = `rest/capabilities`; const VIEW_INCIDENT_URL = `browse`; +const createMetaCapabilities = ['list-project-issuetypes', 'list-issuetype-fields']; + export const createExternalService = ( { config, secrets }: ExternalServiceCredentials, logger: Logger, @@ -39,8 +46,13 @@ export const createExternalService = ( throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); } - const incidentUrl = `${url}/${BASE_URL}/${INCIDENT_URL}`; - const commentUrl = `${incidentUrl}/{issueId}/${COMMENT_URL}`; + const incidentUrl = `${url}/${BASE_URL}/issue`; + const capabilitiesUrl = `${url}/${CAPABILITIES_URL}`; + const commentUrl = `${incidentUrl}/{issueId}/comment`; + const getIssueTypesOldAPIURL = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&expand=projects.issuetypes.fields`; + const getIssueTypeFieldsOldAPIURL = `${url}/${BASE_URL}/issue/createmeta?projectKeys=${projectKey}&issuetypeIds={issueTypeId}&expand=projects.issuetypes.fields`; + const getIssueTypesUrl = `${url}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes`; + const getIssueTypeFieldsUrl = `${url}/${BASE_URL}/issue/createmeta/${projectKey}/issuetypes/{issueTypeId}`; const axiosInstance = axios.create({ auth: { username: email, password: apiToken }, }); @@ -52,6 +64,60 @@ export const createExternalService = ( const getCommentsURL = (issueId: string) => { return commentUrl.replace('{issueId}', issueId); }; + const createGetIssueTypeFieldsUrl = (uri: string, issueTypeId: string) => { + return uri.replace('{issueTypeId}', issueTypeId); + }; + + const createFields = (key: string, incident: Incident): Fields => { + let fields: Fields = { + summary: incident.summary, + project: { key }, + }; + + if (incident.issueType) { + fields = { ...fields, issuetype: { id: incident.issueType } }; + } + + if (incident.description) { + fields = { ...fields, description: incident.description }; + } + + if (incident.labels) { + fields = { ...fields, labels: incident.labels }; + } + + if (incident.priority) { + fields = { ...fields, priority: { name: incident.priority } }; + } + + return fields; + }; + + const createErrorMessage = (errors: ResponseError) => { + return Object.entries(errors).reduce((errorMessage, [, value]) => { + const msg = errorMessage.length > 0 ? `${errorMessage} ${value}` : value; + return msg; + }, ''); + }; + + const hasSupportForNewAPI = (capabilities: { capabilities?: {} }) => + createMetaCapabilities.every((c) => Object.keys(capabilities?.capabilities ?? {}).includes(c)); + + const normalizeIssueTypes = (issueTypes: Array<{ id: string; name: string }>) => + issueTypes.map((type) => ({ id: type.id, name: type.name })); + + const normalizeFields = (fields: { + [key: string]: { allowedValues?: Array<{}>; defaultValue?: {} }; + }) => + Object.keys(fields ?? {}).reduce((fieldsAcc, fieldKey) => { + return { + ...fieldsAcc, + [fieldKey]: { + allowedValues: fields[fieldKey]?.allowedValues ?? [], + defaultValue: fields[fieldKey]?.defaultValue ?? {}, + }, + }; + }, {}); const getIncident = async (id: string) => { try { @@ -67,23 +133,46 @@ export const createExternalService = ( return { ...rest, ...fields }; } catch (error) { throw new Error( - getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}`) + getErrorMessage( + i18n.NAME, + `Unable to get incident with id ${id}. Error: ${ + error.message + } Reason: ${createErrorMessage(error.response?.data?.errors ?? {})}` + ) ); } }; - const createIncident = async ({ incident }: ExternalServiceParams) => { - // The response from Jira when creating an issue contains only the key and the id. - // The function makes two calls when creating an issue. One to create the issue and one to get - // the created issue with all the necessary fields. + const createIncident = async ({ + incident, + }: CreateIncidentParams): Promise => { + /* The response from Jira when creating an issue contains only the key and the id. + The function makes the following calls when creating an issue: + 1. Get issueTypes to set a default ONLY when incident.issueType is missing + 2. Create the issue. + 3. Get the created issue with all the necessary fields. + */ + + let issueType = incident.issueType; + + if (!incident.issueType) { + const issueTypes = await getIssueTypes(); + issueType = issueTypes[0]?.id ?? ''; + } + + const fields = createFields(projectKey, { + ...incident, + issueType, + }); + try { - const res = await request({ + const res = await request({ axios: axiosInstance, url: `${incidentUrl}`, logger, method: 'post', data: { - fields: { ...incident, project: { key: projectKey }, issuetype: { name: 'Task' } }, + fields, }, proxySettings, }); @@ -98,23 +187,38 @@ export const createExternalService = ( }; } catch (error) { throw new Error( - getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}`) + getErrorMessage( + i18n.NAME, + `Unable to create incident. Error: ${error.message}. Reason: ${createErrorMessage( + error.response?.data?.errors ?? {} + )}` + ) ); } }; - const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { + const updateIncident = async ({ + incidentId, + incident, + }: UpdateIncidentParams): Promise => { + const incidentWithoutNullValues = Object.entries(incident).reduce( + (obj, [key, value]) => (value != null ? { ...obj, [key]: value } : obj), + {} as Incident + ); + + const fields = createFields(projectKey, incidentWithoutNullValues); + try { - await request({ + await request({ axios: axiosInstance, method: 'put', url: `${incidentUrl}/${incidentId}`, logger, - data: { fields: { ...incident } }, + data: { fields }, proxySettings, }); - const updatedIncident = await getIncident(incidentId); + const updatedIncident = await getIncident(incidentId as string); return { title: updatedIncident.key, @@ -126,15 +230,20 @@ export const createExternalService = ( throw new Error( getErrorMessage( i18n.NAME, - `Unable to update incident with id ${incidentId}. Error: ${error.message}` + `Unable to update incident with id ${incidentId}. Error: ${ + error.message + }. Reason: ${createErrorMessage(error.response?.data?.errors ?? {})}` ) ); } }; - const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => { + const createComment = async ({ + incidentId, + comment, + }: CreateCommentParams): Promise => { try { - const res = await request({ + const res = await request({ axios: axiosInstance, method: 'post', url: getCommentsURL(incidentId), @@ -152,7 +261,118 @@ export const createExternalService = ( throw new Error( getErrorMessage( i18n.NAME, - `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` + `Unable to create comment at incident with id ${incidentId}. Error: ${ + error.message + }. Reason: ${createErrorMessage(error.response?.data?.errors ?? {})}` + ) + ); + } + }; + + const getCapabilities = async () => { + try { + const res = await request({ + axios: axiosInstance, + method: 'get', + url: capabilitiesUrl, + logger, + proxySettings, + }); + + return { ...res.data }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to get capabilities. Error: ${error.message}. Reason: ${createErrorMessage( + error.response?.data?.errors ?? {} + )}` + ) + ); + } + }; + + const getIssueTypes = async () => { + const capabilitiesResponse = await getCapabilities(); + const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); + + try { + if (!supportsNewAPI) { + const res = await request({ + axios: axiosInstance, + method: 'get', + url: getIssueTypesOldAPIURL, + logger, + proxySettings, + }); + + const issueTypes = res.data.projects[0]?.issuetypes ?? []; + return normalizeIssueTypes(issueTypes); + } else { + const res = await request({ + axios: axiosInstance, + method: 'get', + url: getIssueTypesUrl, + logger, + proxySettings, + }); + + const issueTypes = res.data.values; + return normalizeIssueTypes(issueTypes); + } + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to get issue types. Error: ${error.message}. Reason: ${createErrorMessage( + error.response?.data?.errors ?? {} + )}` + ) + ); + } + }; + + const getFieldsByIssueType = async (issueTypeId: string) => { + const capabilitiesResponse = await getCapabilities(); + const supportsNewAPI = hasSupportForNewAPI(capabilitiesResponse); + + try { + if (!supportsNewAPI) { + const res = await request({ + axios: axiosInstance, + method: 'get', + url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsOldAPIURL, issueTypeId), + logger, + proxySettings, + }); + + const fields = res.data.projects[0]?.issuetypes[0]?.fields || {}; + return normalizeFields(fields); + } else { + const res = await request({ + axios: axiosInstance, + method: 'get', + url: createGetIssueTypeFieldsUrl(getIssueTypeFieldsUrl, issueTypeId), + logger, + proxySettings, + }); + + const fields = res.data.values.reduce( + (acc: { [x: string]: {} }, value: { fieldId: string }) => ({ + ...acc, + [value.fieldId]: { ...value }, + }), + {} + ); + return normalizeFields(fields); + } + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to get fields. Error: ${error.message}. Reason: ${createErrorMessage( + error.response?.data?.errors ?? {} + )}` ) ); } @@ -163,5 +383,8 @@ export const createExternalService = ( createIncident, updateIncident, createComment, + getCapabilities, + getIssueTypes, + getFieldsByIssueType, }; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts index dae0d75952e11..0e71de813eb5d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/translations.ts @@ -9,3 +9,19 @@ import { i18n } from '@kbn/i18n'; export const NAME = i18n.translate('xpack.actions.builtin.case.jiraTitle', { defaultMessage: 'Jira', }); + +export const ALLOWED_HOSTS_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.jira.configuration.apiAllowedHostsError', { + defaultMessage: 'error configuring connector action: {message}', + values: { + message, + }, + }); + +// TODO: remove when Case mappings will be removed +export const MAPPING_EMPTY = i18n.translate( + 'xpack.actions.builtin.jira.configuration.emptyMapping', + { + defaultMessage: '[incidentConfiguration.mapping]: expected non-empty but got empty', + } +); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index 8d9c6b92abb3b..6fe7c62976f22 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -4,29 +4,169 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { TypeOf } from '@kbn/config-schema'; -import { JiraPublicConfigurationSchema, JiraSecretConfigurationSchema } from './schema'; +import { + ExternalIncidentServiceConfigurationSchema, + ExternalIncidentServiceSecretConfigurationSchema, + ExecutorParamsSchema, + ExecutorSubActionPushParamsSchema, + ExecutorSubActionGetIncidentParamsSchema, + ExecutorSubActionHandshakeParamsSchema, + ExecutorSubActionGetCapabilitiesParamsSchema, + ExecutorSubActionGetIssueTypesParamsSchema, + ExecutorSubActionGetFieldsByIssueTypeParamsSchema, +} from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { IncidentConfigurationSchema } from '../case/schema'; +import { Comment } from '../case/types'; +import { Logger } from '../../../../../../src/core/server'; + +export type JiraPublicConfigurationType = TypeOf; +export type JiraSecretConfigurationType = TypeOf< + typeof ExternalIncidentServiceSecretConfigurationSchema +>; + +export type ExecutorParams = TypeOf; +export type ExecutorSubActionPushParams = TypeOf; + +export type IncidentConfiguration = TypeOf; + +export interface ExternalServiceCredentials { + config: Record; + secrets: Record; +} + +export interface ExternalServiceValidation { + config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void; + secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void; +} + +export interface ExternalServiceIncidentResponse { + id: string; + title: string; + url: string; + pushedDate: string; +} + +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: string; + externalCommentId?: string; +} + +export type ExternalServiceParams = Record; + +export type Incident = Pick< + ExecutorSubActionPushParams, + 'description' | 'priority' | 'labels' | 'issueType' +> & { summary: string }; + +export interface CreateIncidentParams { + incident: Incident; +} + +export interface UpdateIncidentParams { + incidentId: string; + incident: Incident; +} + +export interface CreateCommentParams { + incidentId: string; + comment: Comment; +} -export type JiraPublicConfigurationType = TypeOf; -export type JiraSecretConfigurationType = TypeOf; +export type GetIssueTypesResponse = Array<{ id: string; name: string }>; +export type GetFieldsByIssueTypeResponse = Record< + string, + { allowedValues: Array<{}>; defaultValue: {} } +>; -interface CreateIncidentBasicRequestArgs { - summary: string; - description: string; +export interface ExternalService { + getIncident: (id: string) => Promise; + createIncident: (params: CreateIncidentParams) => Promise; + updateIncident: (params: UpdateIncidentParams) => Promise; + createComment: (params: CreateCommentParams) => Promise; + getCapabilities: () => Promise; + getIssueTypes: () => Promise; + getFieldsByIssueType: (issueTypeId: string) => Promise; } -interface CreateIncidentRequestArgs extends CreateIncidentBasicRequestArgs { - project: { key: string }; - issuetype: { name: string }; + +export interface PushToServiceApiParams extends ExecutorSubActionPushParams { + externalObject: Record; +} + +export type ExecutorSubActionGetIncidentParams = TypeOf< + typeof ExecutorSubActionGetIncidentParamsSchema +>; + +export type ExecutorSubActionHandshakeParams = TypeOf< + typeof ExecutorSubActionHandshakeParamsSchema +>; + +export type ExecutorSubActionGetCapabilitiesParams = TypeOf< + typeof ExecutorSubActionGetCapabilitiesParamsSchema +>; + +export type ExecutorSubActionGetIssueTypesParams = TypeOf< + typeof ExecutorSubActionGetIssueTypesParamsSchema +>; + +export type ExecutorSubActionGetFieldsByIssueTypeParams = TypeOf< + typeof ExecutorSubActionGetFieldsByIssueTypeParamsSchema +>; + +export interface ExternalServiceApiHandlerArgs { + externalService: ExternalService; + mapping: Map | null; +} + +export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: PushToServiceApiParams; + logger: Logger; } -export interface CreateIncidentRequest { - fields: CreateIncidentRequestArgs; +export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionGetIncidentParams; } -export interface UpdateIncidentRequest { - fields: Partial; +export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionHandshakeParams; } -export interface CreateCommentRequest { - body: string; +export interface GetIssueTypesHandlerArgs { + externalService: ExternalService; + params: ExecutorSubActionGetIssueTypesParams; +} + +export interface GetFieldsByIssueTypeHandlerArgs { + externalService: ExternalService; + params: ExecutorSubActionGetFieldsByIssueTypeParams; +} + +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} + +export interface ExternalServiceApi { + handshake: (args: HandshakeApiHandlerArgs) => Promise; + pushToService: (args: PushToServiceApiHandlerArgs) => Promise; + getIncident: (args: GetIncidentApiHandlerArgs) => Promise; + issueTypes: (args: GetIssueTypesHandlerArgs) => Promise; + fieldsByIssueType: ( + args: GetFieldsByIssueTypeHandlerArgs + ) => Promise; +} + +export type JiraExecutorResultData = + | PushToServiceResponse + | GetIssueTypesResponse + | GetFieldsByIssueTypeResponse; + +export interface Fields { + [key: string]: string | string[] | { name: string } | { key: string } | { id: string }; +} +export interface ResponseError { + [k: string]: string; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts index 7226071392bc6..58a3e27247fae 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/validators.ts @@ -4,8 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateCommonConfig, validateCommonSecrets } from '../case/validators'; -import { ExternalServiceValidation } from '../case/types'; +import { isEmpty } from 'lodash'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { + JiraPublicConfigurationType, + JiraSecretConfigurationType, + ExternalServiceValidation, +} from './types'; + +import * as i18n from './translations'; + +export const validateCommonConfig = ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: JiraPublicConfigurationType +) => { + if ( + configObject.incidentConfiguration !== null && + isEmpty(configObject.incidentConfiguration.mapping) + ) { + return i18n.MAPPING_EMPTY; + } + + try { + configurationUtilities.ensureUriAllowed(configObject.apiUrl); + } catch (allowedListError) { + return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message); + } +}; + +export const validateCommonSecrets = ( + configurationUtilities: ActionsConfigurationUtilities, + secrets: JiraSecretConfigurationType +) => {}; export const validate: ExternalServiceValidation = { config: validateCommonConfig, diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts index 734f6be382629..e974fedd0775b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.test.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { api } from '../case/api'; +import { Logger } from '../../../../../../src/core/server'; +import { api } from './api'; import { externalServiceMock, mapping, apiParams } from './mocks'; -import { ExternalService } from '../case/types'; +import { ExternalService } from './types'; + +let mockedLogger: jest.Mocked; describe('api', () => { let externalService: jest.Mocked; @@ -23,7 +26,12 @@ describe('api', () => { describe('create incident', () => { test('it creates an incident', async () => { const params = { ...apiParams, externalId: null }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + logger: mockedLogger, + }); expect(res).toEqual({ id: '1', @@ -45,7 +53,12 @@ describe('api', () => { test('it creates an incident without comments', async () => { const params = { ...apiParams, externalId: null, comments: [] }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + logger: mockedLogger, + }); expect(res).toEqual({ id: '1', @@ -57,7 +70,7 @@ describe('api', () => { test('it calls createIncident correctly', async () => { const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.createIncident).toHaveBeenCalledWith({ incident: { @@ -71,7 +84,7 @@ describe('api', () => { test('it calls createComment correctly', async () => { const params = { ...apiParams, externalId: null }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { incidentId: '1', @@ -89,7 +102,6 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', }); expect(externalService.createComment).toHaveBeenNthCalledWith(2, { @@ -108,14 +120,18 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', }); }); }); describe('update incident', () => { test('it updates an incident', async () => { - const res = await api.pushToService({ externalService, mapping, params: apiParams }); + const res = await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(res).toEqual({ id: '1', @@ -137,7 +153,12 @@ describe('api', () => { test('it updates an incident without comments', async () => { const params = { ...apiParams, comments: [] }; - const res = await api.pushToService({ externalService, mapping, params }); + const res = await api.pushToService({ + externalService, + mapping, + params, + logger: mockedLogger, + }); expect(res).toEqual({ id: '1', @@ -149,7 +170,7 @@ describe('api', () => { test('it calls updateIncident correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', @@ -164,7 +185,7 @@ describe('api', () => { test('it calls createComment correctly', async () => { const params = { ...apiParams }; - await api.pushToService({ externalService, mapping, params }); + await api.pushToService({ externalService, mapping, params, logger: mockedLogger }); expect(externalService.createComment).toHaveBeenCalledTimes(2); expect(externalService.createComment).toHaveBeenNthCalledWith(1, { incidentId: '1', @@ -182,7 +203,6 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', }); expect(externalService.createComment).toHaveBeenNthCalledWith(2, { @@ -201,11 +221,52 @@ describe('api', () => { username: 'elastic', }, }, - field: 'comments', }); }); }); + describe('incidentTypes', () => { + test('it returns the incident types correctly', async () => { + const res = await api.incidentTypes({ + externalService, + params: {}, + }); + expect(res).toEqual([ + { + id: 17, + name: 'Communication error (fax; email)', + }, + { + id: 1001, + name: 'Custom type', + }, + ]); + }); + }); + + describe('severity', () => { + test('it returns the severity correctly', async () => { + const res = await api.severity({ + externalService, + params: { id: '10006' }, + }); + expect(res).toEqual([ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, + ]); + }); + }); + describe('mapping variations', () => { test('overwrite & append', async () => { mapping.set('title', { @@ -228,7 +289,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -260,7 +326,12 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -291,7 +362,12 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -324,7 +400,12 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: {}, @@ -352,7 +433,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -382,7 +468,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -414,7 +505,12 @@ describe('api', () => { actionType: 'nothing', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -445,7 +541,12 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -478,7 +579,12 @@ describe('api', () => { actionType: 'append', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.updateIncident).toHaveBeenCalledWith({ incidentId: 'incident-3', incident: { @@ -509,7 +615,12 @@ describe('api', () => { actionType: 'overwrite', }); - await api.pushToService({ externalService, mapping, params: apiParams }); + await api.pushToService({ + externalService, + mapping, + params: apiParams, + logger: mockedLogger, + }); expect(externalService.createComment).not.toHaveBeenCalled(); }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts index 3db66e5884af4..af3984bf5f0fa 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/api.ts @@ -4,4 +4,129 @@ * you may not use this file except in compliance with the Elastic License. */ -export { api } from '../case/api'; +import { + ExternalServiceParams, + PushToServiceApiHandlerArgs, + HandshakeApiHandlerArgs, + GetIncidentApiHandlerArgs, + ExternalServiceApi, + Incident, + GetIncidentTypesHandlerArgs, + GetSeverityHandlerArgs, + PushToServiceApiParams, + PushToServiceResponse, +} from './types'; + +// TODO: to remove, need to support Case +import { transformFields, prepareFieldsForTransformation, transformComments } from '../case/utils'; + +const handshakeHandler = async ({ + externalService, + mapping, + params, +}: HandshakeApiHandlerArgs) => {}; + +const getIncidentHandler = async ({ + externalService, + mapping, + params, +}: GetIncidentApiHandlerArgs) => {}; + +const getIncidentTypesHandler = async ({ externalService }: GetIncidentTypesHandlerArgs) => { + const res = await externalService.getIncidentTypes(); + return res; +}; + +const getSeverityHandler = async ({ externalService }: GetSeverityHandlerArgs) => { + const res = await externalService.getSeverity(); + return res; +}; + +const pushToServiceHandler = async ({ + externalService, + mapping, + params, + logger, +}: PushToServiceApiHandlerArgs): Promise => { + const { externalId, comments } = params; + const updateIncident = externalId ? true : false; + const defaultPipes = updateIncident ? ['informationUpdated'] : ['informationCreated']; + let currentIncident: ExternalServiceParams | undefined; + let res: PushToServiceResponse; + + if (externalId) { + try { + currentIncident = await externalService.getIncident(externalId); + } catch (ex) { + logger.debug( + `Retrieving Incident by id ${externalId} from IBM Resilient was failed with exception: ${ex}` + ); + } + } + + let incident: Incident; + // TODO: should be removed later but currently keep it for the Case implementation support + if (mapping) { + const fields = prepareFieldsForTransformation({ + externalCase: params.externalObject, + mapping, + defaultPipes, + }); + + incident = transformFields({ + params, + fields, + currentIncident, + }); + } else { + const { title, description, incidentTypes, severityCode } = params; + incident = { name: title, description, incidentTypes, severityCode }; + } + + if (externalId != null) { + res = await externalService.updateIncident({ + incidentId: externalId, + incident, + }); + } else { + res = await externalService.createIncident({ + incident: { + ...incident, + }, + }); + } + + if (comments && Array.isArray(comments) && comments.length > 0) { + if (mapping && mapping.get('comments')?.actionType === 'nothing') { + return res; + } + const commentsTransformed = mapping + ? transformComments(comments, ['informationAdded']) + : comments; + + res.comments = []; + for (const currentComment of commentsTransformed) { + const comment = await externalService.createComment({ + incidentId: res.id, + comment: currentComment, + }); + res.comments = [ + ...(res.comments ?? []), + { + commentId: comment.commentId, + pushedDate: comment.pushedDate, + }, + ]; + } + } + + return res; +}; + +export const api: ExternalServiceApi = { + handshake: handshakeHandler, + pushToService: pushToServiceHandler, + getIncident: getIncidentHandler, + incidentTypes: getIncidentTypesHandler, + severity: getSeverityHandler, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/config.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/config.ts deleted file mode 100644 index 4ce9417bfa9a1..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/config.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ExternalServiceConfiguration } from '../case/types'; -import * as i18n from './translations'; - -export const config: ExternalServiceConfiguration = { - id: '.resilient', - name: i18n.NAME, - minimumLicenseRequired: 'platinum', -}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts index 1e9cb15589702..53285a2a350af 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/index.ts @@ -4,33 +4,139 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Logger } from '../../../../../../src/core/server'; -import { createConnector } from '../case/utils'; +import { curry } from 'lodash'; +import { schema } from '@kbn/config-schema'; -import { api } from './api'; -import { config } from './config'; import { validate } from './validators'; -import { createExternalService } from './service'; -import { ResilientSecretConfiguration, ResilientPublicConfiguration } from './schema'; +import { + ExternalIncidentServiceConfiguration, + ExternalIncidentServiceSecretConfiguration, + ExecutorParamsSchema, +} from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { ActionType } from '../../types'; +import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; +import { createExternalService } from './service'; +import { api } from './api'; +import { + ExecutorParams, + ExecutorSubActionPushParams, + ResilientPublicConfigurationType, + ResilientSecretConfigurationType, + ResilientExecutorResultData, + ExecutorSubActionGetIncidentTypesParams, + ExecutorSubActionGetSeverityParams, +} from './types'; +import * as i18n from './translations'; +import { Logger } from '../../../../../../src/core/server'; -export function getActionType({ - logger, - configurationUtilities, -}: { +// TODO: to remove, need to support Case +import { buildMap, mapParams } from '../case/utils'; + +interface GetActionTypeParams { logger: Logger; configurationUtilities: ActionsConfigurationUtilities; -}): ActionType { - return createConnector({ - api, - config, - validate, - createExternalService, - validationSchema: { - config: ResilientPublicConfiguration, - secrets: ResilientSecretConfiguration, +} + +const supportedSubActions: string[] = ['pushToService', 'incidentTypes', 'severity']; + +// action type definition +export function getActionType( + params: GetActionTypeParams +): ActionType< + ResilientPublicConfigurationType, + ResilientSecretConfigurationType, + ExecutorParams, + ResilientExecutorResultData | {} +> { + const { logger, configurationUtilities } = params; + return { + id: '.resilient', + minimumLicenseRequired: 'platinum', + name: i18n.NAME, + validate: { + config: schema.object(ExternalIncidentServiceConfiguration, { + validate: curry(validate.config)(configurationUtilities), + }), + secrets: schema.object(ExternalIncidentServiceSecretConfiguration, { + validate: curry(validate.secrets)(configurationUtilities), + }), + params: ExecutorParamsSchema, + }, + executor: curry(executor)({ logger }), + }; +} + +// action executor +async function executor( + { logger }: { logger: Logger }, + execOptions: ActionTypeExecutorOptions< + ResilientPublicConfigurationType, + ResilientSecretConfigurationType, + ExecutorParams + > +): Promise> { + const { actionId, config, params, secrets } = execOptions; + const { subAction, subActionParams } = params as ExecutorParams; + let data: ResilientExecutorResultData | null = null; + + const externalService = createExternalService( + { + config, + secrets, }, logger, - })({ configurationUtilities }); + execOptions.proxySettings + ); + + if (!api[subAction]) { + const errorMessage = `[Action][ExternalService] Unsupported subAction type ${subAction}.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (!supportedSubActions.includes(subAction)) { + const errorMessage = `[Action][ExternalService] subAction ${subAction} not implemented.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (subAction === 'pushToService') { + const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; + + const { comments, externalId, ...restParams } = pushToServiceParams; + const mapping = config.incidentConfiguration + ? buildMap(config.incidentConfiguration.mapping) + : null; + const externalObject = + config.incidentConfiguration && mapping + ? mapParams(restParams as ExecutorSubActionPushParams, mapping) + : {}; + + data = await api.pushToService({ + externalService, + mapping, + params: { ...pushToServiceParams, externalObject }, + logger, + }); + + logger.debug(`response push to service for incident id: ${data.id}`); + } + + if (subAction === 'incidentTypes') { + const incidentTypesParams = subActionParams as ExecutorSubActionGetIncidentTypesParams; + data = await api.incidentTypes({ + externalService, + params: incidentTypesParams, + }); + } + + if (subAction === 'severity') { + const severityParams = subActionParams as ExecutorSubActionGetSeverityParams; + data = await api.severity({ + externalService, + params: severityParams, + }); + } + + return { status: 'ok', data: data ?? {}, actionId }; } diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts index bba9c58bf28c9..2e841728159a3 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/mocks.ts @@ -4,12 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ExternalService, - PushToServiceApiParams, - ExecutorSubActionPushParams, - MapRecord, -} from '../case/types'; +import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types'; + +import { MapRecord } from '../case/types'; const createMock = (): jest.Mocked => { const service = { @@ -40,6 +37,25 @@ const createMock = (): jest.Mocked => { }) ), createComment: jest.fn(), + findIncidents: jest.fn(), + getIncidentTypes: jest.fn().mockImplementation(() => [ + { id: 17, name: 'Communication error (fax; email)' }, + { id: 1001, name: 'Custom type' }, + ]), + getSeverity: jest.fn().mockImplementation(() => [ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, + ]), }; service.createComment.mockImplementationOnce(() => @@ -96,6 +112,8 @@ const executorParams: ExecutorSubActionPushParams = { updatedBy: { fullName: 'Elastic User', username: 'elastic' }, title: 'Incident title', description: 'Incident description', + incidentTypes: [1001], + severityCode: 6, comments: [ { commentId: 'case-comment-1', @@ -118,7 +136,58 @@ const executorParams: ExecutorSubActionPushParams = { const apiParams: PushToServiceApiParams = { ...executorParams, - externalCase: { name: 'Incident title', description: 'Incident description' }, + externalObject: { name: 'Incident title', description: 'Incident description' }, }; -export { externalServiceMock, mapping, executorParams, apiParams }; +const incidentTypes = [ + { + value: 17, + label: 'Communication error (fax; email)', + enabled: true, + properties: null, + uuid: '4a8d22f7-d89e-4403-85c7-2bafe3b7f2ae', + hidden: false, + default: false, + }, + { + value: 1001, + label: 'Custom type', + enabled: true, + properties: null, + uuid: '3b51c8c2-9758-48f8-b013-bd141f1d2ec9', + hidden: false, + default: false, + }, +]; + +const severity = [ + { + value: 4, + label: 'Low', + enabled: true, + properties: null, + uuid: '97cae239-963d-4e36-be34-07e47ef2cc86', + hidden: false, + default: true, + }, + { + value: 5, + label: 'Medium', + enabled: true, + properties: null, + uuid: 'c2c354c9-6d1e-4a48-82e5-bd5dc5068339', + hidden: false, + default: false, + }, + { + value: 6, + label: 'High', + enabled: true, + properties: null, + uuid: '93e5c99c-563b-48b9-80a3-9572307622d8', + hidden: false, + default: false, + }, +]; + +export { externalServiceMock, mapping, executorParams, apiParams, incidentTypes, severity }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts index c13de2b27e2b9..151f703dcc07e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts @@ -5,18 +5,77 @@ */ import { schema } from '@kbn/config-schema'; -import { ExternalIncidentServiceConfiguration } from '../case/schema'; +import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '../case/schema'; -export const ResilientPublicConfiguration = { +export const ExternalIncidentServiceConfiguration = { + apiUrl: schema.string(), orgId: schema.string(), - ...ExternalIncidentServiceConfiguration, + // TODO: to remove - set it optional for the current stage to support Case implementation + incidentConfiguration: schema.nullable(IncidentConfigurationSchema), + isCaseOwned: schema.nullable(schema.boolean()), }; -export const ResilientPublicConfigurationSchema = schema.object(ResilientPublicConfiguration); +export const ExternalIncidentServiceConfigurationSchema = schema.object( + ExternalIncidentServiceConfiguration +); -export const ResilientSecretConfiguration = { +export const ExternalIncidentServiceSecretConfiguration = { apiKeyId: schema.string(), apiKeySecret: schema.string(), }; -export const ResilientSecretConfigurationSchema = schema.object(ResilientSecretConfiguration); +export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( + ExternalIncidentServiceSecretConfiguration +); + +export const ExecutorSubActionSchema = schema.oneOf([ + schema.literal('getIncident'), + schema.literal('pushToService'), + schema.literal('handshake'), + schema.literal('incidentTypes'), + schema.literal('severity'), +]); + +export const ExecutorSubActionPushParamsSchema = schema.object({ + savedObjectId: schema.string(), + title: schema.string(), + description: schema.nullable(schema.string()), + externalId: schema.nullable(schema.string()), + incidentTypes: schema.nullable(schema.arrayOf(schema.number())), + severityCode: schema.nullable(schema.number()), + // TODO: remove later - need for support Case push multiple comments + comments: schema.nullable(schema.arrayOf(CommentSchema)), + ...EntityInformation, +}); + +export const ExecutorSubActionGetIncidentParamsSchema = schema.object({ + externalId: schema.string(), +}); + +// Reserved for future implementation +export const ExecutorSubActionHandshakeParamsSchema = schema.object({}); +export const ExecutorSubActionGetIncidentTypesParamsSchema = schema.object({}); +export const ExecutorSubActionGetSeverityParamsSchema = schema.object({}); + +export const ExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('getIncident'), + subActionParams: ExecutorSubActionGetIncidentParamsSchema, + }), + schema.object({ + subAction: schema.literal('handshake'), + subActionParams: ExecutorSubActionHandshakeParamsSchema, + }), + schema.object({ + subAction: schema.literal('pushToService'), + subActionParams: ExecutorSubActionPushParamsSchema, + }), + schema.object({ + subAction: schema.literal('incidentTypes'), + subActionParams: ExecutorSubActionGetIncidentTypesParamsSchema, + }), + schema.object({ + subAction: schema.literal('severity'), + subActionParams: ExecutorSubActionGetSeverityParamsSchema, + }), +]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts index a9271671f68b9..86ea352625a5b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.test.ts @@ -8,9 +8,11 @@ import axios from 'axios'; import { createExternalService, getValueTextContent, formatUpdateRequest } from './service'; import * as utils from '../lib/axios_utils'; -import { ExternalService } from '../case/types'; +import { ExternalService } from './types'; import { Logger } from '../../../../../../src/core/server'; import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { incidentTypes, severity } from './mocks'; + const logger = loggingSystemMock.create().get() as jest.Mocked; jest.mock('axios'); @@ -41,6 +43,8 @@ const mockIncidentUpdate = (withUpdateError = false) => { format: 'html', content: 'description', }, + incident_type_ids: [1001, 16, 12], + severity_code: 6, }, })); @@ -246,7 +250,12 @@ describe('IBM Resilient service', () => { })); const res = await service.createIncident({ - incident: { name: 'title', description: 'desc' }, + incident: { + name: 'title', + description: 'desc', + incidentTypes: [1001], + severityCode: 6, + }, }); expect(res).toEqual({ @@ -269,12 +278,18 @@ describe('IBM Resilient service', () => { })); await service.createIncident({ - incident: { name: 'title', description: 'desc' }, + incident: { + name: 'title', + description: 'desc', + incidentTypes: [1001], + severityCode: 6, + }, }); expect(requestMock).toHaveBeenCalledWith({ axios, - url: 'https://resilient.elastic.co/rest/orgs/201/incidents', + url: + 'https://resilient.elastic.co/rest/orgs/201/incidents?text_content_output_format=objects_convert', logger, method: 'post', data: { @@ -284,6 +299,8 @@ describe('IBM Resilient service', () => { content: 'desc', }, discovered_date: TIMESTAMP, + incident_type_ids: [{ id: 1001 }], + severity_code: { id: 6 }, }, }); }); @@ -295,7 +312,12 @@ describe('IBM Resilient service', () => { expect( service.createIncident({ - incident: { name: 'title', description: 'desc' }, + incident: { + name: 'title', + description: 'desc', + incidentTypes: [1001], + severityCode: 6, + }, }) ).rejects.toThrow( '[Action][IBM Resilient]: Unable to create incident. Error: An error has occurred' @@ -308,7 +330,12 @@ describe('IBM Resilient service', () => { mockIncidentUpdate(); const res = await service.updateIncident({ incidentId: '1', - incident: { name: 'title_updated', description: 'desc_updated' }, + incident: { + name: 'title', + description: 'desc', + incidentTypes: [1001], + severityCode: 6, + }, }); expect(res).toEqual({ @@ -324,7 +351,12 @@ describe('IBM Resilient service', () => { await service.updateIncident({ incidentId: '1', - incident: { name: 'title_updated', description: 'desc_updated' }, + incident: { + name: 'title_updated', + description: 'desc_updated', + incidentTypes: [1001], + severityCode: 5, + }, }); // Incident update makes three calls to the API. @@ -356,6 +388,28 @@ describe('IBM Resilient service', () => { }, }, }, + { + field: { + name: 'incident_type_ids', + }, + old_value: { + ids: [1001, 16, 12], + }, + new_value: { + ids: [1001], + }, + }, + { + field: { + name: 'severity_code', + }, + old_value: { + id: 6, + }, + new_value: { + id: 5, + }, + }, ], }, }); @@ -367,7 +421,12 @@ describe('IBM Resilient service', () => { expect( service.updateIncident({ incidentId: '1', - incident: { name: 'title', description: 'desc' }, + incident: { + name: 'title', + description: 'desc', + incidentTypes: [1001], + severityCode: 5, + }, }) ).rejects.toThrow( '[Action][IBM Resilient]: Unable to update incident with id 1. Error: An error has occurred' @@ -386,8 +445,14 @@ describe('IBM Resilient service', () => { const res = await service.createComment({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'comments', + comment: { + comment: 'comment', + commentId: 'comment-1', + createdBy: null, + createdAt: null, + updatedAt: null, + updatedBy: null, + }, }); expect(res).toEqual({ @@ -407,8 +472,14 @@ describe('IBM Resilient service', () => { await service.createComment({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'my_field', + comment: { + comment: 'comment', + commentId: 'comment-1', + createdBy: null, + createdAt: null, + updatedAt: null, + updatedBy: null, + }, }); expect(requestMock).toHaveBeenCalledWith({ @@ -434,12 +505,82 @@ describe('IBM Resilient service', () => { expect( service.createComment({ incidentId: '1', - comment: { comment: 'comment', commentId: 'comment-1' }, - field: 'comments', + comment: { + comment: 'comment', + commentId: 'comment-1', + createdBy: null, + createdAt: null, + updatedAt: null, + updatedBy: null, + }, }) ).rejects.toThrow( '[Action][IBM Resilient]: Unable to create comment at incident with id 1. Error: An error has occurred' ); }); }); + + describe('getIncidentTypes', () => { + test('it creates the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { + values: incidentTypes, + }, + })); + + const res = await service.getIncidentTypes(); + + expect(res).toEqual([ + { id: 17, name: 'Communication error (fax; email)' }, + { id: 1001, name: 'Custom type' }, + ]); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect(service.getIncidentTypes()).rejects.toThrow( + '[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.' + ); + }); + }); + + describe('getSeverity', () => { + test('it creates the incident correctly', async () => { + requestMock.mockImplementation(() => ({ + data: { + values: severity, + }, + })); + + const res = await service.getSeverity(); + + expect(res).toEqual([ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, + ]); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + expect(service.getIncidentTypes()).rejects.toThrow( + '[Action][IBM Resilient]: Unable to get incident types. Error: An error has occurred.' + ); + }); + }); }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts index b2150081f2c89..4bf1453641e42 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/service.ts @@ -5,44 +5,56 @@ */ import axios from 'axios'; +import { omitBy, isNil } from 'lodash/fp'; import { Logger } from '../../../../../../src/core/server'; -import { ExternalServiceCredentials, ExternalService, ExternalServiceParams } from '../case/types'; import { + ExternalServiceCredentials, + ExternalService, + ExternalServiceParams, + CreateCommentParams, + UpdateIncidentParams, + CreateIncidentParams, + CreateIncidentData, ResilientPublicConfigurationType, ResilientSecretConfigurationType, - CreateIncidentRequest, UpdateIncidentRequest, - CreateCommentRequest, - UpdateFieldText, - UpdateFieldTextArea, + GetValueTextContentResponse, } from './types'; import * as i18n from './translations'; import { getErrorMessage, request } from '../lib/axios_utils'; import { ProxySettings } from '../../types'; -const BASE_URL = `rest`; -const INCIDENT_URL = `incidents`; -const COMMENT_URL = `comments`; - const VIEW_INCIDENT_URL = `#incidents`; export const getValueTextContent = ( field: string, - value: string -): UpdateFieldText | UpdateFieldTextArea => { + value: string | number | number[] +): GetValueTextContentResponse => { if (field === 'description') { return { textarea: { format: 'html', - content: value, + content: value as string, }, }; } + if (field === 'incidentTypes') { + return { + ids: value as number[], + }; + } + + if (field === 'severityCode') { + return { + id: value as number, + }; + } + return { - text: value, + text: value as string, }; }; @@ -51,11 +63,30 @@ export const formatUpdateRequest = ({ newIncident, }: ExternalServiceParams): UpdateIncidentRequest => { return { - changes: Object.keys(newIncident).map((key) => ({ - field: { name: key }, - old_value: getValueTextContent(key, oldIncident[key]), - new_value: getValueTextContent(key, newIncident[key]), - })), + changes: Object.keys(newIncident as Record).map((key) => { + let name = key; + + if (key === 'incidentTypes') { + name = 'incident_type_ids'; + } + + if (key === 'severityCode') { + name = 'severity_code'; + } + + return { + field: { name }, + // TODO: Fix ugly casting + old_value: getValueTextContent( + key, + (oldIncident as Record)[name] as string + ), + new_value: getValueTextContent( + key, + (newIncident as Record)[key] as string + ), + }; + }), }; }; @@ -72,8 +103,12 @@ export const createExternalService = ( } const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; - const incidentUrl = `${urlWithoutTrailingSlash}/${BASE_URL}/orgs/${orgId}/${INCIDENT_URL}`; - const commentUrl = `${incidentUrl}/{inc_id}/${COMMENT_URL}`; + const orgUrl = `${urlWithoutTrailingSlash}/rest/orgs/${orgId}`; + const incidentUrl = `${orgUrl}/incidents`; + const commentUrl = `${incidentUrl}/{inc_id}/comments`; + const incidentFieldsUrl = `${orgUrl}/types/incident/fields`; + const incidentTypesUrl = `${incidentFieldsUrl}/incident_type_ids`; + const severityUrl = `${incidentFieldsUrl}/severity_code`; const axiosInstance = axios.create({ auth: { username: apiKeyId, password: apiKeySecret }, }); @@ -101,26 +136,48 @@ export const createExternalService = ( return { ...res.data, description: res.data.description?.content ?? '' }; } catch (error) { throw new Error( - getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}`) + getErrorMessage(i18n.NAME, `Unable to get incident with id ${id}. Error: ${error.message}.`) ); } }; - const createIncident = async ({ incident }: ExternalServiceParams) => { + const createIncident = async ({ incident }: CreateIncidentParams) => { + let data: CreateIncidentData = { + name: incident.name, + discovered_date: Date.now(), + }; + + if (incident.description) { + data = { + ...data, + description: { + format: 'html', + content: incident.description ?? '', + }, + }; + } + + if (incident.incidentTypes) { + data = { + ...data, + incident_type_ids: incident.incidentTypes.map((id) => ({ id })), + }; + } + + if (incident.severityCode) { + data = { + ...data, + severity_code: { id: incident.severityCode }, + }; + } + try { - const res = await request({ + const res = await request({ axios: axiosInstance, - url: `${incidentUrl}`, + url: `${incidentUrl}?text_content_output_format=objects_convert`, method: 'post', logger, - data: { - ...incident, - description: { - format: 'html', - content: incident.description ?? '', - }, - discovered_date: Date.now(), - }, + data, proxySettings, }); @@ -132,17 +189,20 @@ export const createExternalService = ( }; } catch (error) { throw new Error( - getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}`) + getErrorMessage(i18n.NAME, `Unable to create incident. Error: ${error.message}.`) ); } }; - const updateIncident = async ({ incidentId, incident }: ExternalServiceParams) => { + const updateIncident = async ({ incidentId, incident }: UpdateIncidentParams) => { try { const latestIncident = await getIncident(incidentId); - const data = formatUpdateRequest({ oldIncident: latestIncident, newIncident: incident }); - const res = await request({ + // Remove null or undefined values. Allowing null values sets the field in IBM Resilient to empty. + const newIncident = omitBy(isNil, incident); + const data = formatUpdateRequest({ oldIncident: latestIncident, newIncident }); + + const res = await request({ axios: axiosInstance, method: 'patch', url: `${incidentUrl}/${incidentId}`, @@ -173,9 +233,9 @@ export const createExternalService = ( } }; - const createComment = async ({ incidentId, comment, field }: ExternalServiceParams) => { + const createComment = async ({ incidentId, comment }: CreateCommentParams) => { try { - const res = await request({ + const res = await request({ axios: axiosInstance, method: 'post', url: getCommentsURL(incidentId), @@ -193,16 +253,62 @@ export const createExternalService = ( throw new Error( getErrorMessage( i18n.NAME, - `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}` + `Unable to create comment at incident with id ${incidentId}. Error: ${error.message}.` ) ); } }; + const getIncidentTypes = async () => { + try { + const res = await request({ + axios: axiosInstance, + method: 'get', + url: incidentTypesUrl, + logger, + proxySettings, + }); + + const incidentTypes = res.data?.values ?? []; + return incidentTypes.map((type: { value: string; label: string }) => ({ + id: type.value, + name: type.label, + })); + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to get incident types. Error: ${error.message}.`) + ); + } + }; + + const getSeverity = async () => { + try { + const res = await request({ + axios: axiosInstance, + method: 'get', + url: severityUrl, + logger, + proxySettings, + }); + + const incidentTypes = res.data?.values ?? []; + return incidentTypes.map((type: { value: string; label: string }) => ({ + id: type.value, + name: type.label, + })); + } catch (error) { + throw new Error( + getErrorMessage(i18n.NAME, `Unable to get severity. Error: ${error.message}.`) + ); + } + }; + return { getIncident, createIncident, updateIncident, createComment, + getIncidentTypes, + getSeverity, }; }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts index d952838d5a2b3..8c6ce9902da81 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/translations.ts @@ -9,3 +9,19 @@ import { i18n } from '@kbn/i18n'; export const NAME = i18n.translate('xpack.actions.builtin.case.resilientTitle', { defaultMessage: 'IBM Resilient', }); + +export const ALLOWED_HOSTS_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.configuration.apiAllowedHostsError', { + defaultMessage: 'error configuring connector action: {message}', + values: { + message, + }, + }); + +// TODO: remove when Case mappings will be removed +export const MAPPING_EMPTY = i18n.translate( + 'xpack.actions.builtin.servicenow.configuration.emptyMapping', + { + defaultMessage: '[incidentConfiguration.mapping]: expected non-empty but got empty', + } +); diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts index 6869e2ff3a105..ed622ee473b65 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/types.ts @@ -4,29 +4,175 @@ * you may not use this file except in compliance with the Elastic License. */ +/* eslint-disable @typescript-eslint/no-explicit-any */ + import { TypeOf } from '@kbn/config-schema'; -import { ResilientPublicConfigurationSchema, ResilientSecretConfigurationSchema } from './schema'; +import { + ExternalIncidentServiceConfigurationSchema, + ExternalIncidentServiceSecretConfigurationSchema, + ExecutorParamsSchema, + ExecutorSubActionPushParamsSchema, + ExecutorSubActionGetIncidentParamsSchema, + ExecutorSubActionHandshakeParamsSchema, + ExecutorSubActionGetIncidentTypesParamsSchema, + ExecutorSubActionGetSeverityParamsSchema, +} from './schema'; + +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { Logger } from '../../../../../../src/core/server'; + +import { IncidentConfigurationSchema } from '../case/schema'; +import { Comment } from '../case/types'; + +export type ResilientPublicConfigurationType = TypeOf< + typeof ExternalIncidentServiceConfigurationSchema +>; +export type ResilientSecretConfigurationType = TypeOf< + typeof ExternalIncidentServiceSecretConfigurationSchema +>; + +export type ExecutorParams = TypeOf; +export type ExecutorSubActionPushParams = TypeOf; + +export type IncidentConfiguration = TypeOf; + +export interface ExternalServiceCredentials { + config: Record; + secrets: Record; +} + +export interface ExternalServiceValidation { + config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void; + secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void; +} + +export interface ExternalServiceIncidentResponse { + id: string; + title: string; + url: string; + pushedDate: string; +} + +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: string; + externalCommentId?: string; +} -export type ResilientPublicConfigurationType = TypeOf; -export type ResilientSecretConfigurationType = TypeOf; +export type ExternalServiceParams = Record; -interface CreateIncidentBasicRequestArgs { +export type Incident = Pick< + ExecutorSubActionPushParams, + 'description' | 'incidentTypes' | 'severityCode' +> & { name: string; - description: string; - discovered_date: number; +}; + +export interface CreateIncidentParams { + incident: Incident; +} + +export interface UpdateIncidentParams { + incidentId: string; + incident: Incident; +} + +export interface CreateCommentParams { + incidentId: string; + comment: Comment; +} + +export type GetIncidentTypesResponse = Array<{ id: string; name: string }>; +export type GetSeverityResponse = Array<{ id: string; name: string }>; + +export interface ExternalService { + getIncident: (id: string) => Promise; + createIncident: (params: CreateIncidentParams) => Promise; + updateIncident: (params: UpdateIncidentParams) => Promise; + createComment: (params: CreateCommentParams) => Promise; + getIncidentTypes: () => Promise; + getSeverity: () => Promise; } -interface Comment { - text: { format: string; content: string }; +export interface PushToServiceApiParams extends ExecutorSubActionPushParams { + externalObject: Record; } -interface CreateIncidentRequestArgs extends CreateIncidentBasicRequestArgs { - comments?: Comment[]; +export type ExecutorSubActionGetIncidentTypesParams = TypeOf< + typeof ExecutorSubActionGetIncidentTypesParamsSchema +>; + +export type ExecutorSubActionGetSeverityParams = TypeOf< + typeof ExecutorSubActionGetSeverityParamsSchema +>; + +export interface ExternalServiceApiHandlerArgs { + externalService: ExternalService; + mapping: Map | null; } +export type ExecutorSubActionGetIncidentParams = TypeOf< + typeof ExecutorSubActionGetIncidentParamsSchema +>; + +export type ExecutorSubActionHandshakeParams = TypeOf< + typeof ExecutorSubActionHandshakeParamsSchema +>; + +export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: PushToServiceApiParams; + logger: Logger; +} + +export interface GetIncidentApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionGetIncidentParams; +} + +export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: ExecutorSubActionHandshakeParams; +} + +export interface GetIncidentTypesHandlerArgs { + externalService: ExternalService; + params: ExecutorSubActionGetIncidentTypesParams; +} + +export interface GetSeverityHandlerArgs { + externalService: ExternalService; + params: ExecutorSubActionGetSeverityParams; +} + +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} + +export interface ExternalServiceApi { + handshake: (args: HandshakeApiHandlerArgs) => Promise; + pushToService: (args: PushToServiceApiHandlerArgs) => Promise; + getIncident: (args: GetIncidentApiHandlerArgs) => Promise; + incidentTypes: (args: GetIncidentTypesHandlerArgs) => Promise; + severity: (args: GetSeverityHandlerArgs) => Promise; +} + +export type ResilientExecutorResultData = + | PushToServiceResponse + | GetIncidentTypesResponse + | GetSeverityResponse; + export interface UpdateFieldText { text: string; } +export interface UpdateFieldText { + text: string; +} + +export interface UpdateIdsField { + ids: number[]; +} + +export interface UpdateIdField { + id: number; +} export interface UpdateFieldTextArea { textarea: { format: 'html' | 'text'; content: string }; @@ -34,13 +180,24 @@ export interface UpdateFieldTextArea { interface UpdateField { field: { name: string }; - old_value: UpdateFieldText | UpdateFieldTextArea; - new_value: UpdateFieldText | UpdateFieldTextArea; + old_value: UpdateFieldText | UpdateFieldTextArea | UpdateIdsField | UpdateIdField; + new_value: UpdateFieldText | UpdateFieldTextArea | UpdateIdsField | UpdateIdField; } -export type CreateIncidentRequest = CreateIncidentRequestArgs; -export type CreateCommentRequest = Comment; - export interface UpdateIncidentRequest { changes: UpdateField[]; } + +export type GetValueTextContentResponse = + | UpdateFieldText + | UpdateFieldTextArea + | UpdateIdsField + | UpdateIdField; + +export interface CreateIncidentData { + name: string; + discovered_date: number; + description?: { format: string; content: string }; + incident_type_ids?: Array<{ id: number }>; + severity_code?: { id: number }; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts index 7226071392bc6..a50e868cdda3d 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/validators.ts @@ -4,8 +4,38 @@ * you may not use this file except in compliance with the Elastic License. */ -import { validateCommonConfig, validateCommonSecrets } from '../case/validators'; -import { ExternalServiceValidation } from '../case/types'; +import { isEmpty } from 'lodash'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { + ResilientPublicConfigurationType, + ResilientSecretConfigurationType, + ExternalServiceValidation, +} from './types'; + +import * as i18n from './translations'; + +export const validateCommonConfig = ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: ResilientPublicConfigurationType +) => { + if ( + configObject.incidentConfiguration !== null && + isEmpty(configObject.incidentConfiguration.mapping) + ) { + return i18n.MAPPING_EMPTY; + } + + try { + configurationUtilities.ensureUriAllowed(configObject.apiUrl); + } catch (allowedListError) { + return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message); + } +}; + +export const validateCommonSecrets = ( + configurationUtilities: ActionsConfigurationUtilities, + secrets: ResilientSecretConfigurationType +) => {}; export const validate: ExternalServiceValidation = { config: validateCommonConfig, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts index 0bb096ecd0f62..7a68781bb9a75 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.test.ts @@ -91,7 +91,7 @@ describe('api', () => { expect(externalService.updateIncident).not.toHaveBeenCalled(); }); - test('it calls updateIncident correctly', async () => { + test('it calls updateIncident correctly when creating an incident and having comments', async () => { const params = { ...apiParams, externalId: null }; await api.pushToService({ externalService, @@ -103,7 +103,7 @@ describe('api', () => { expect(externalService.updateIncident).toHaveBeenCalledTimes(2); expect(externalService.updateIncident).toHaveBeenNthCalledWith(1, { incident: { - comments: 'A comment', + comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', description: 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', short_description: @@ -114,7 +114,7 @@ describe('api', () => { expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { incident: { - comments: 'Another comment', + comments: 'Another comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', description: 'Incident description (created at 2020-03-13T08:34:53.450Z by Elastic User)', short_description: @@ -215,7 +215,7 @@ describe('api', () => { expect(externalService.updateIncident).toHaveBeenNthCalledWith(2, { incident: { - comments: 'A comment', + comments: 'A comment (added at 2020-03-13T08:34:53.450Z by Elastic User)', description: 'Incident description (updated at 2020-03-13T08:34:53.450Z by Elastic User)', short_description: diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts index 3281832941558..455a71517fb4a 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/api.ts @@ -3,19 +3,19 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { flow } from 'lodash'; import { ExternalServiceParams, PushToServiceApiHandlerArgs, HandshakeApiHandlerArgs, GetIncidentApiHandlerArgs, ExternalServiceApi, + PushToServiceApiParams, + PushToServiceResponse, + Incident, } from './types'; // TODO: to remove, need to support Case -import { transformers } from '../case/transformers'; -import { PushToServiceResponse, TransformFieldsArgs } from './case_types'; -import { prepareFieldsForTransformation } from '../case/utils'; +import { transformFields, transformComments, prepareFieldsForTransformation } from '../case/utils'; const handshakeHandler = async ({ externalService, @@ -60,7 +60,7 @@ const pushToServiceHandler = async ({ defaultPipes, }); - incident = transformFields({ + incident = transformFields({ params, fields, currentIncident, @@ -92,9 +92,10 @@ const pushToServiceHandler = async ({ mapping.get('comments')?.actionType !== 'nothing' ) { res.comments = []; + const commentsTransformed = transformComments(comments, ['informationAdded']); const fieldsKey = mapping.get('comments')?.target ?? 'comments'; - for (const currentComment of comments) { + for (const currentComment of commentsTransformed) { await externalService.updateIncident({ incidentId: res.id, incident: { @@ -114,32 +115,6 @@ const pushToServiceHandler = async ({ return res; }; -export const transformFields = ({ - params, - fields, - currentIncident, -}: TransformFieldsArgs): Record => { - return fields.reduce((prev, cur) => { - const transform = flow(...cur.pipes.map((p) => transformers[p])); - return { - ...prev, - [cur.key]: transform({ - value: cur.value, - date: params.updatedAt ?? params.createdAt, - user: - (params.updatedBy != null - ? params.updatedBy.fullName - ? params.updatedBy.fullName - : params.updatedBy.username - : params.createdBy.fullName - ? params.createdBy.fullName - : params.createdBy.username) ?? '', - previousValue: currentIncident ? currentIncident[cur.key] : '', - }).value, - }; - }, {}); -}; - export const api: ExternalServiceApi = { handshake: handshakeHandler, pushToService: pushToServiceHandler, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_shema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_shema.ts deleted file mode 100644 index 2df8c8156cde8..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_shema.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { schema } from '@kbn/config-schema'; - -export const MappingActionType = schema.oneOf([ - schema.literal('nothing'), - schema.literal('overwrite'), - schema.literal('append'), -]); - -export const MapRecordSchema = schema.object({ - source: schema.string(), - target: schema.string(), - actionType: MappingActionType, -}); - -export const IncidentConfigurationSchema = schema.object({ - mapping: schema.arrayOf(MapRecordSchema), -}); - -export const EntityInformation = { - createdAt: schema.maybe(schema.string()), - createdBy: schema.maybe(schema.any()), - updatedAt: schema.nullable(schema.string()), - updatedBy: schema.nullable(schema.any()), -}; - -export const CommentSchema = schema.object({ - commentId: schema.string(), - comment: schema.string(), - ...EntityInformation, -}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts deleted file mode 100644 index 49b85f9254af9..0000000000000 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/case_types.ts +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { TypeOf } from '@kbn/config-schema'; -import { - ExecutorSubActionGetIncidentParamsSchema, - ExecutorSubActionHandshakeParamsSchema, -} from './schema'; -import { IncidentConfigurationSchema, MapRecordSchema } from './case_shema'; -import { - PushToServiceApiParams, - ExternalServiceIncidentResponse, - ExternalServiceParams, -} from './types'; - -export interface CreateCommentRequest { - [key: string]: string; -} - -export type IncidentConfiguration = TypeOf; -export type MapRecord = TypeOf; - -export interface ExternalServiceCommentResponse { - commentId: string; - pushedDate: string; - externalCommentId?: string; -} - -export type ExecutorSubActionGetIncidentParams = TypeOf< - typeof ExecutorSubActionGetIncidentParamsSchema ->; - -export type ExecutorSubActionHandshakeParams = TypeOf< - typeof ExecutorSubActionHandshakeParamsSchema ->; - -export interface PushToServiceResponse extends ExternalServiceIncidentResponse { - comments?: ExternalServiceCommentResponse[]; -} - -export interface PipedField { - key: string; - value: string; - actionType: string; - pipes: string[]; -} - -export interface TransformFieldsArgs { - params: PushToServiceApiParams; - fields: PipedField[]; - currentIncident?: ExternalServiceParams; -} - -export interface TransformerArgs { - value: string; - date?: string; - user?: string; - previousValue?: string; -} diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts index 3addbe7c54dac..41a577918b18e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/index.ts @@ -24,11 +24,11 @@ import { ExecutorSubActionPushParams, ServiceNowPublicConfigurationType, ServiceNowSecretConfigurationType, + PushToServiceResponse, } from './types'; // TODO: to remove, need to support Case import { buildMap, mapParams } from '../case/utils'; -import { PushToServiceResponse } from './case_types'; interface GetActionTypeParams { logger: Logger; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts index 5f22fcd4fdc85..f34e9714b22ce 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/mocks.ts @@ -5,7 +5,7 @@ */ import { ExternalService, PushToServiceApiParams, ExecutorSubActionPushParams } from './types'; -import { MapRecord } from './case_types'; +import { MapRecord } from '../case/types'; const createMock = (): jest.Mocked => { const service = { diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index 82afebaaee445..9896d4175954c 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -5,7 +5,7 @@ */ import { schema } from '@kbn/config-schema'; -import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from './case_shema'; +import { CommentSchema, EntityInformation, IncidentConfigurationSchema } from '../case/schema'; export const ExternalIncidentServiceConfiguration = { apiUrl: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts index 05c7d805a1852..7cc97a241c4bc 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/translations.ts @@ -10,8 +10,8 @@ export const NAME = i18n.translate('xpack.actions.builtin.servicenowTitle', { defaultMessage: 'ServiceNow', }); -export const WHITE_LISTED_ERROR = (message: string) => - i18n.translate('xpack.actions.builtin.configuration.apiWhitelistError', { +export const ALLOWED_HOSTS_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.configuration.apiAllowedHostsError', { defaultMessage: 'error configuring connector action: {message}', values: { message, diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts index 0db9b6642ea5c..a6a0ac946fe96 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/types.ts @@ -16,8 +16,8 @@ import { ExecutorSubActionHandshakeParamsSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; -import { IncidentConfigurationSchema } from './case_shema'; -import { PushToServiceResponse } from './case_types'; +import { ExternalServiceCommentResponse } from '../case/types'; +import { IncidentConfigurationSchema } from '../case/schema'; import { Logger } from '../../../../../../src/core/server'; export type ServiceNowPublicConfigurationType = TypeOf< @@ -52,6 +52,9 @@ export interface ExternalServiceIncidentResponse { url: string; pushedDate: string; } +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} export type ExternalServiceParams = Record; @@ -79,6 +82,13 @@ export type ExecutorSubActionHandshakeParams = TypeOf< typeof ExecutorSubActionHandshakeParamsSchema >; +export type Incident = Pick< + ExecutorSubActionPushParams, + 'description' | 'severity' | 'urgency' | 'impact' +> & { + short_description: string; +}; + export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { params: PushToServiceApiParams; secrets: Record; diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts index 6eec3b8d63b86..87bbfd9c7ea95 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/validators.ts @@ -27,8 +27,8 @@ export const validateCommonConfig = ( try { configurationUtilities.ensureUriAllowed(configObject.apiUrl); - } catch (allowListError) { - return i18n.WHITE_LISTED_ERROR(allowListError.message); + } catch (allowedListError) { + return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message); } }; diff --git a/x-pack/plugins/actions/server/plugin.ts b/x-pack/plugins/actions/server/plugin.ts index a6c5899281658..592ca93ef5a16 100644 --- a/x-pack/plugins/actions/server/plugin.ts +++ b/x-pack/plugins/actions/server/plugin.ts @@ -159,7 +159,7 @@ export class ActionsPlugin implements Plugin, Plugi ); } - plugins.features.registerFeature(ACTIONS_FEATURE); + plugins.features.registerKibanaFeature(ACTIONS_FEATURE); setupSavedObjects(core.savedObjects, plugins.encryptedSavedObjects); this.eventLogService = plugins.eventLog; diff --git a/x-pack/plugins/alerting_builtins/server/plugin.test.ts b/x-pack/plugins/alerting_builtins/server/plugin.test.ts index 15ad066523502..629c02d923071 100644 --- a/x-pack/plugins/alerting_builtins/server/plugin.test.ts +++ b/x-pack/plugins/alerting_builtins/server/plugin.test.ts @@ -43,7 +43,7 @@ describe('AlertingBuiltins Plugin', () => { "name": "Index threshold", } `); - expect(featuresSetup.registerFeature).toHaveBeenCalledWith(BUILT_IN_ALERTS_FEATURE); + expect(featuresSetup.registerKibanaFeature).toHaveBeenCalledWith(BUILT_IN_ALERTS_FEATURE); }); it('should return a service in the expected shape', async () => { diff --git a/x-pack/plugins/alerting_builtins/server/plugin.ts b/x-pack/plugins/alerting_builtins/server/plugin.ts index 41871c01bfb50..48e5c41cbe637 100644 --- a/x-pack/plugins/alerting_builtins/server/plugin.ts +++ b/x-pack/plugins/alerting_builtins/server/plugin.ts @@ -27,7 +27,7 @@ export class AlertingBuiltinsPlugin implements Plugin { core: CoreSetup, { alerts, features }: AlertingBuiltinsDeps ): Promise { - features.registerFeature(BUILT_IN_ALERTS_FEATURE); + features.registerKibanaFeature(BUILT_IN_ALERTS_FEATURE); registerBuiltInAlertTypes({ service: this.service, diff --git a/x-pack/plugins/alerts/README.md b/x-pack/plugins/alerts/README.md index 6307e463af853..62058d47cbd44 100644 --- a/x-pack/plugins/alerts/README.md +++ b/x-pack/plugins/alerts/README.md @@ -306,7 +306,7 @@ In addition, when users are inside your feature you might want to grant them acc You can control all of these abilities by assigning privileges to the Alerting Framework from within your own feature, for example: ```typescript -features.registerFeature({ +features.registerKibanaFeature({ id: 'my-application-id', name: 'My Application', app: [], @@ -348,7 +348,7 @@ In this example we can see the following: It's important to note that any role can be granted a mix of `all` and `read` privileges accross multiple type, for example: ```typescript -features.registerFeature({ +features.registerKibanaFeature({ id: 'my-application-id', name: 'My Application', app: [], diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts index b164d27ded648..c2506381b9df9 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.test.ts @@ -6,7 +6,10 @@ import { KibanaRequest } from 'kibana/server'; import { alertTypeRegistryMock } from '../alert_type_registry.mock'; import { securityMock } from '../../../../plugins/security/server/mocks'; -import { PluginStartContract as FeaturesStartContract, Feature } from '../../../features/server'; +import { + PluginStartContract as FeaturesStartContract, + KibanaFeature, +} from '../../../features/server'; import { featuresPluginMock } from '../../../features/server/mocks'; import { AlertsAuthorization, @@ -41,7 +44,7 @@ function mockSecurity() { } function mockFeature(appName: string, typeName?: string) { - return new Feature({ + return new KibanaFeature({ id: appName, name: appName, app: [], @@ -84,7 +87,7 @@ function mockFeature(appName: string, typeName?: string) { } function mockFeatureWithSubFeature(appName: string, typeName: string) { - return new Feature({ + return new KibanaFeature({ id: appName, name: appName, app: [], @@ -174,7 +177,7 @@ beforeEach(() => { async executor() {}, producer: 'myApp', })); - features.getFeatures.mockReturnValue([ + features.getKibanaFeatures.mockReturnValue([ myAppFeature, myOtherAppFeature, myAppWithSubFeature, @@ -255,7 +258,7 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: true, - privileges: [], + privileges: { kibana: [] }, }); await alertAuthorization.ensureAuthorized('myType', 'myApp', WriteOperations.Create); @@ -263,9 +266,9 @@ describe('AlertsAuthorization', () => { expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); - expect(checkPrivileges).toHaveBeenCalledWith([ - mockAuthorizationAction('myType', 'myApp', 'create'), - ]); + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: [mockAuthorizationAction('myType', 'myApp', 'create')], + }); expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); @@ -298,7 +301,7 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: true, - privileges: [], + privileges: { kibana: [] }, }); await alertAuthorization.ensureAuthorized('myType', 'alerts', WriteOperations.Create); @@ -306,9 +309,9 @@ describe('AlertsAuthorization', () => { expect(alertTypeRegistry.get).toHaveBeenCalledWith('myType'); expect(authorization.actions.alerting.get).toHaveBeenCalledWith('myType', 'myApp', 'create'); - expect(checkPrivileges).toHaveBeenCalledWith([ - mockAuthorizationAction('myType', 'myApp', 'create'), - ]); + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: [mockAuthorizationAction('myType', 'myApp', 'create')], + }); expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); @@ -332,7 +335,7 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: true, - privileges: [], + privileges: { kibana: [] }, }); const alertAuthorization = new AlertsAuthorization({ @@ -354,10 +357,12 @@ describe('AlertsAuthorization', () => { 'myOtherApp', 'create' ); - expect(checkPrivileges).toHaveBeenCalledWith([ - mockAuthorizationAction('myType', 'myOtherApp', 'create'), - mockAuthorizationAction('myType', 'myApp', 'create'), - ]); + expect(checkPrivileges).toHaveBeenCalledWith({ + kibana: [ + mockAuthorizationAction('myType', 'myOtherApp', 'create'), + mockAuthorizationAction('myType', 'myApp', 'create'), + ], + }); expect(auditLogger.alertsAuthorizationSuccess).toHaveBeenCalledTimes(1); expect(auditLogger.alertsAuthorizationFailure).not.toHaveBeenCalled(); @@ -390,16 +395,18 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myType', 'myApp', 'create'), - authorized: true, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'create'), + authorized: true, + }, + ], + }, }); await expect( @@ -439,16 +446,18 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myType', 'myApp', 'create'), - authorized: false, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'create'), + authorized: false, + }, + ], + }, }); await expect( @@ -488,16 +497,18 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myType', 'myApp', 'create'), - authorized: false, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myType', 'myApp', 'create'), + authorized: false, + }, + ], + }, }); await expect( @@ -592,7 +603,7 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: true, - privileges: [], + privileges: { kibana: [] }, }); const alertAuthorization = new AlertsAuthorization({ @@ -621,24 +632,26 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), - authorized: false, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + ], + }, }); const alertAuthorization = new AlertsAuthorization({ @@ -680,24 +693,26 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), - authorized: true, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), + authorized: true, + }, + ], + }, }); const alertAuthorization = new AlertsAuthorization({ @@ -728,32 +743,34 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('mySecondAppAlertType', 'myApp', 'find'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('mySecondAppAlertType', 'myOtherApp', 'find'), - authorized: true, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'find'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('mySecondAppAlertType', 'myApp', 'find'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('mySecondAppAlertType', 'myOtherApp', 'find'), + authorized: true, + }, + ], + }, }); const alertAuthorization = new AlertsAuthorization({ @@ -903,24 +920,26 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), - authorized: true, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: true, + }, + ], + }, }); const alertAuthorization = new AlertsAuthorization({ @@ -989,16 +1008,18 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + ], + }, }); const alertAuthorization = new AlertsAuthorization({ @@ -1048,40 +1069,42 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'get'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'get'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'get'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'get'), - authorized: true, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'get'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'get'), + authorized: true, + }, + ], + }, }); const alertAuthorization = new AlertsAuthorization({ @@ -1158,24 +1181,26 @@ describe('AlertsAuthorization', () => { checkPrivileges.mockResolvedValueOnce({ username: 'some-user', hasAllRequested: false, - privileges: [ - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), - authorized: true, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), - authorized: false, - }, - { - privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), - authorized: false, - }, - ], + privileges: { + kibana: [ + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myOtherAppAlertType', 'myOtherApp', 'create'), + authorized: true, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myApp', 'create'), + authorized: false, + }, + { + privilege: mockAuthorizationAction('myAppAlertType', 'myOtherApp', 'create'), + authorized: false, + }, + ], + }, }); const alertAuthorization = new AlertsAuthorization({ diff --git a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts index b362a50c9f10b..9dda006c1eb8e 100644 --- a/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts +++ b/x-pack/plugins/alerts/server/authorization/alerts_authorization.ts @@ -82,7 +82,7 @@ export class AlertsAuthorization { (disabledFeatures) => new Set( features - .getFeatures() + .getKibanaFeatures() .filter( ({ id, alerting }) => // ignore features which are disabled in the user's space @@ -133,20 +133,21 @@ export class AlertsAuthorization { const shouldAuthorizeConsumer = consumer !== ALERTS_FEATURE_ID; const checkPrivileges = authorization.checkPrivilegesDynamicallyWithRequest(this.request); - const { hasAllRequested, username, privileges } = await checkPrivileges( - shouldAuthorizeConsumer && consumer !== alertType.producer - ? [ - // check for access at consumer level - requiredPrivilegesByScope.consumer, - // check for access at producer level - requiredPrivilegesByScope.producer, - ] - : [ - // skip consumer privilege checks under `alerts` as all alert types can - // be created under `alerts` if you have producer level privileges - requiredPrivilegesByScope.producer, - ] - ); + const { hasAllRequested, username, privileges } = await checkPrivileges({ + kibana: + shouldAuthorizeConsumer && consumer !== alertType.producer + ? [ + // check for access at consumer level + requiredPrivilegesByScope.consumer, + // check for access at producer level + requiredPrivilegesByScope.producer, + ] + : [ + // skip consumer privilege checks under `alerts` as all alert types can + // be created under `alerts` if you have producer level privileges + requiredPrivilegesByScope.producer, + ], + }); if (!isAvailableConsumer) { /** @@ -177,7 +178,7 @@ export class AlertsAuthorization { ); } else { const authorizedPrivileges = map( - privileges.filter((privilege) => privilege.authorized), + privileges.kibana.filter((privilege) => privilege.authorized), 'privilege' ); const unauthorizedScopes = mapValues( @@ -341,9 +342,9 @@ export class AlertsAuthorization { } } - const { username, hasAllRequested, privileges } = await checkPrivileges([ - ...privilegeToAlertType.keys(), - ]); + const { username, hasAllRequested, privileges } = await checkPrivileges({ + kibana: [...privilegeToAlertType.keys()], + }); return { username, @@ -352,7 +353,7 @@ export class AlertsAuthorization { ? // has access to all features this.augmentWithAuthorizedConsumers(alertTypes, await this.allPossibleConsumers) : // only has some of the required privileges - privileges.reduce((authorizedAlertTypes, { authorized, privilege }) => { + privileges.kibana.reduce((authorizedAlertTypes, { authorized, privilege }) => { if (authorized && privilegeToAlertType.has(privilege)) { const [ alertType, diff --git a/x-pack/plugins/alerts/server/plugin.test.ts b/x-pack/plugins/alerts/server/plugin.test.ts index e65d195290259..026aa0c5238dc 100644 --- a/x-pack/plugins/alerts/server/plugin.test.ts +++ b/x-pack/plugins/alerts/server/plugin.test.ts @@ -12,7 +12,7 @@ import { taskManagerMock } from '../../task_manager/server/mocks'; import { eventLogServiceMock } from '../../event_log/server/event_log_service.mock'; import { KibanaRequest, CoreSetup } from 'kibana/server'; import { featuresPluginMock } from '../../features/server/mocks'; -import { Feature } from '../../features/server'; +import { KibanaFeature } from '../../features/server'; describe('Alerting Plugin', () => { describe('setup()', () => { @@ -159,8 +159,8 @@ describe('Alerting Plugin', () => { function mockFeatures() { const features = featuresPluginMock.createSetup(); - features.getFeatures.mockReturnValue([ - new Feature({ + features.getKibanaFeatures.mockReturnValue([ + new KibanaFeature({ id: 'appName', name: 'appName', app: [], diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index f93df9a01dea2..8218eefe738f0 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -478,7 +478,7 @@ exports[`Transaction TRANSACTION_TIME_TO_FIRST_BYTE 1`] = `undefined`; exports[`Transaction TRANSACTION_TYPE 1`] = `"transaction type"`; -exports[`Transaction TRANSACTION_URL 1`] = `undefined`; +exports[`Transaction TRANSACTION_URL 1`] = `"http://www.elastic.co"`; exports[`Transaction URL_FULL 1`] = `"http://www.elastic.co"`; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.test.ts b/x-pack/plugins/apm/common/anomaly_detection.test.ts similarity index 74% rename from x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.test.ts rename to x-pack/plugins/apm/common/anomaly_detection.test.ts index 52b7d54236db6..21963b5300f83 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.test.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getSeverity, severity } from './getSeverity'; +import { getSeverity, Severity } from './anomaly_detection'; describe('getSeverity', () => { describe('when score is undefined', () => { @@ -15,25 +15,25 @@ describe('getSeverity', () => { describe('when score < 25', () => { it('returns warning', () => { - expect(getSeverity(10)).toEqual(severity.warning); + expect(getSeverity(10)).toEqual(Severity.warning); }); }); describe('when score is between 25 and 50', () => { it('returns minor', () => { - expect(getSeverity(40)).toEqual(severity.minor); + expect(getSeverity(40)).toEqual(Severity.minor); }); }); describe('when score is between 50 and 75', () => { it('returns major', () => { - expect(getSeverity(60)).toEqual(severity.major); + expect(getSeverity(60)).toEqual(Severity.major); }); }); describe('when score is 75 or more', () => { it('returns critical', () => { - expect(getSeverity(100)).toEqual(severity.critical); + expect(getSeverity(100)).toEqual(Severity.critical); }); }); }); diff --git a/x-pack/plugins/apm/common/anomaly_detection.ts b/x-pack/plugins/apm/common/anomaly_detection.ts index 07270b572a4be..5d80ee6381267 100644 --- a/x-pack/plugins/apm/common/anomaly_detection.ts +++ b/x-pack/plugins/apm/common/anomaly_detection.ts @@ -5,6 +5,7 @@ */ import { i18n } from '@kbn/i18n'; +import { EuiTheme } from '../../../legacy/common/eui_styled_components'; export interface ServiceAnomalyStats { transactionType?: string; @@ -13,6 +14,82 @@ export interface ServiceAnomalyStats { jobId?: string; } +export enum Severity { + critical = 'critical', + major = 'major', + minor = 'minor', + warning = 'warning', +} + +// TODO: Replace with `getSeverity` from: +// https://github.com/elastic/kibana/blob/0f964f66916480f2de1f4b633e5afafc08cf62a0/x-pack/plugins/ml/common/util/anomaly_utils.ts#L129 +export function getSeverity(score?: number) { + if (typeof score !== 'number') { + return undefined; + } else if (score < 25) { + return Severity.warning; + } else if (score >= 25 && score < 50) { + return Severity.minor; + } else if (score >= 50 && score < 75) { + return Severity.major; + } else if (score >= 75) { + return Severity.critical; + } else { + return undefined; + } +} + +export function getSeverityColor(theme: EuiTheme, severity?: Severity) { + switch (severity) { + case Severity.warning: + return theme.eui.euiColorVis0; + case Severity.minor: + case Severity.major: + return theme.eui.euiColorVis5; + case Severity.critical: + return theme.eui.euiColorVis9; + default: + return; + } +} + +export function getSeverityLabel(severity?: Severity) { + switch (severity) { + case Severity.critical: + return i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.critical', + { + defaultMessage: 'Critical', + } + ); + + case Severity.major: + case Severity.minor: + return i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.warning', + { + defaultMessage: 'Warning', + } + ); + + case Severity.warning: + return i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.healthy', + { + defaultMessage: 'Healthy', + } + ); + + default: + return i18n.translate( + 'xpack.apm.servicesTable.serviceHealthStatus.unknown', + { + defaultMessage: 'Unknown', + } + ); + } +} + export const ML_ERRORS = { INVALID_LICENSE: i18n.translate( 'xpack.apm.anomaly_detection.error.invalid_license', diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index b322abeb3d597..e1a279714d308 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -97,7 +97,7 @@ export const POD_NAME = 'kubernetes.pod.name'; export const CLIENT_GEO_COUNTRY_ISO_CODE = 'client.geo.country_iso_code'; // RUM Labels -export const TRANSACTION_URL = 'transaction.page.url'; +export const TRANSACTION_URL = 'url.full'; export const CLIENT_GEO = 'client.geo'; export const USER_AGENT_DEVICE = 'user_agent.device.name'; export const USER_AGENT_OS = 'user_agent.os.name'; diff --git a/x-pack/plugins/apm/common/service_map.test.ts b/x-pack/plugins/apm/common/service_map.test.ts index 346403efc46ae..31f439a7aaec9 100644 --- a/x-pack/plugins/apm/common/service_map.test.ts +++ b/x-pack/plugins/apm/common/service_map.test.ts @@ -8,7 +8,7 @@ import { License } from '../../licensing/common/license'; import * as serviceMap from './service_map'; describe('service map helpers', () => { - describe('isValidPlatinumLicense', () => { + describe('isActivePlatinumLicense', () => { describe('with an expired license', () => { it('returns false', () => { const license = new License({ @@ -22,7 +22,7 @@ describe('service map helpers', () => { signature: 'test signature', }); - expect(serviceMap.isValidPlatinumLicense(license)).toEqual(false); + expect(serviceMap.isActivePlatinumLicense(license)).toEqual(false); }); }); @@ -39,7 +39,7 @@ describe('service map helpers', () => { signature: 'test signature', }); - expect(serviceMap.isValidPlatinumLicense(license)).toEqual(false); + expect(serviceMap.isActivePlatinumLicense(license)).toEqual(false); }); }); @@ -56,7 +56,7 @@ describe('service map helpers', () => { signature: 'test signature', }); - expect(serviceMap.isValidPlatinumLicense(license)).toEqual(true); + expect(serviceMap.isActivePlatinumLicense(license)).toEqual(true); }); }); @@ -73,7 +73,7 @@ describe('service map helpers', () => { signature: 'test signature', }); - expect(serviceMap.isValidPlatinumLicense(license)).toEqual(true); + expect(serviceMap.isActivePlatinumLicense(license)).toEqual(true); }); }); @@ -90,7 +90,7 @@ describe('service map helpers', () => { signature: 'test signature', }); - expect(serviceMap.isValidPlatinumLicense(license)).toEqual(true); + expect(serviceMap.isActivePlatinumLicense(license)).toEqual(true); }); }); }); diff --git a/x-pack/plugins/apm/common/service_map.ts b/x-pack/plugins/apm/common/service_map.ts index 7f46fc685d9ca..1dc4d598cd2ee 100644 --- a/x-pack/plugins/apm/common/service_map.ts +++ b/x-pack/plugins/apm/common/service_map.ts @@ -46,7 +46,7 @@ export interface ServiceNodeStats { avgErrorRate: number | null; } -export function isValidPlatinumLicense(license: ILicense) { +export function isActivePlatinumLicense(license: ILicense) { return license.isActive && license.hasAtLeast('platinum'); } diff --git a/x-pack/plugins/apm/dev_docs/updating_functional_tests_archives.md b/x-pack/plugins/apm/dev_docs/updating_functional_tests_archives.md new file mode 100644 index 0000000000000..467090fb3c91b --- /dev/null +++ b/x-pack/plugins/apm/dev_docs/updating_functional_tests_archives.md @@ -0,0 +1,8 @@ +### Updating functional tests archives + +Some of our API tests use an archive generated by the [`esarchiver`](https://www.elastic.co/guide/en/kibana/current/development-functional-tests.html) script. Updating the main archive (`apm_8.0.0`) is a scripted process, where a 30m snapshot is downloaded from a cluster running the [APM Integration Testing server](https://github.com/elastic/apm-integration-testing). The script will copy the generated archives into the `fixtures/es_archiver` folders of our test suites (currently `basic` and `trial`). It will also generate a file that contains metadata about the archive, that can be imported to get the time range of the snapshot. + +Usage: +`node x-pack/plugins/apm/scripts/create-functional-tests-archive --es-url=https://admin:changeme@localhost:9200 --kibana-url=https://localhost:5601` + + diff --git a/x-pack/plugins/apm/e2e/cypress/integration/apm.feature b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature index 285615108266b..82d896c5ba17e 100644 --- a/x-pack/plugins/apm/e2e/cypress/integration/apm.feature +++ b/x-pack/plugins/apm/e2e/cypress/integration/apm.feature @@ -1,4 +1,4 @@ -Feature: APM +KibanaFeature: APM Scenario: Transaction duration charts Given a user browses the APM UI application diff --git a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts index c1402bbd035f4..66d604a663fbf 100644 --- a/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts +++ b/x-pack/plugins/apm/e2e/cypress/support/step_definitions/apm.ts @@ -26,7 +26,7 @@ When(`the user inspects the opbeans-node service`, () => { }); Then(`should redirect to correct path with correct params`, () => { - cy.url().should('contain', `/app/apm#/services/opbeans-node/transactions`); + cy.url().should('contain', `/app/apm/services/opbeans-node/transactions`); cy.url().should('contain', `transactionType=request`); }); diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx index 970365779a0a2..f27a3d56aab55 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ChartWrapper/index.tsx @@ -26,11 +26,14 @@ interface Props { * aria-label for accessibility */ 'aria-label'?: string; + + maxWidth?: string; } export function ChartWrapper({ loading = false, height = '100%', + maxWidth, children, ...rest }: Props) { @@ -43,6 +46,7 @@ export function ChartWrapper({ height, opacity, transition: 'opacity 0.2s', + ...(maxWidth ? { maxWidth } : {}), }} {...(rest as HTMLAttributes)} > @@ -52,7 +56,12 @@ export function ChartWrapper({ diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx index 9f9ffdf7168b8..213126ba4bf81 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/Charts/VisitorBreakdownChart.tsx @@ -14,7 +14,7 @@ import { PartitionLayout, Settings, } from '@elastic/charts'; -import euiLightVars from '@elastic/eui/dist/eui_theme_light.json'; +import styled from 'styled-components'; import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT, @@ -22,6 +22,10 @@ import { import { useUiSetting$ } from '../../../../../../../../src/plugins/kibana_react/public'; import { ChartWrapper } from '../ChartWrapper'; +const StyleChart = styled.div` + height: 100%; +`; + interface Props { options?: Array<{ count: number; @@ -32,65 +36,47 @@ interface Props { export function VisitorBreakdownChart({ options }: Props) { const [darkMode] = useUiSetting$('theme:darkMode'); + const euiChartTheme = darkMode + ? EUI_CHARTS_THEME_DARK + : EUI_CHARTS_THEME_LIGHT; + return ( - - - - d.count as number} - valueGetter="percent" - percentFormatter={(d: number) => - `${Math.round((d + Number.EPSILON) * 100) / 100}%` - } - layers={[ - { - groupByRollup: (d: Datum) => d.name, - nodeLabel: (d: Datum) => d, - // fillLabel: { textInvertible: true }, - shape: { - fillColor: (d) => { - const clrs = [ - euiLightVars.euiColorVis1_behindText, - euiLightVars.euiColorVis0_behindText, - euiLightVars.euiColorVis2_behindText, - euiLightVars.euiColorVis3_behindText, - euiLightVars.euiColorVis4_behindText, - euiLightVars.euiColorVis5_behindText, - euiLightVars.euiColorVis6_behindText, - euiLightVars.euiColorVis7_behindText, - euiLightVars.euiColorVis8_behindText, - euiLightVars.euiColorVis9_behindText, - ]; - return clrs[d.sortIndex]; + + + + + d.count as number} + valueGetter="percent" + percentFormatter={(d: number) => + `${Math.round((d + Number.EPSILON) * 100) / 100}%` + } + layers={[ + { + groupByRollup: (d: Datum) => d.name, + shape: { + fillColor: (d) => + euiChartTheme.theme.colors?.vizColors?.[d.sortIndex]!, }, }, - }, - ]} - config={{ - partitionLayout: PartitionLayout.sunburst, - linkLabel: { - maxCount: 32, - fontSize: 14, - }, - fontFamily: 'Arial', - margin: { top: 0, bottom: 0, left: 0, right: 0 }, - minFontSize: 1, - idealFontSizeJump: 1.1, - outerSizeRatio: 0.9, // - 0.5 * Math.random(), - emptySizeRatio: 0, - circlePadding: 4, - }} - /> - + ]} + config={{ + partitionLayout: PartitionLayout.sunburst, + linkLabel: { maximumSection: Infinity, maxCount: 0 }, + margin: { top: 0, bottom: 0, left: 0, right: 0 }, + outerSizeRatio: 1, // - 0.5 * Math.random(), + circlePadding: 4, + clockwiseSectors: false, + }} + /> + + ); } diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx index b2132c50dc6bc..f54a54211359c 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/ClientMetrics/index.tsx @@ -45,7 +45,7 @@ export function ClientMetrics() { <>{numeral(data?.pageViews?.value).format('0 a') ?? '-'} diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx index d23e16b3a5b38..f05c07e8512ac 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/RumDashboard.tsx @@ -49,15 +49,18 @@ export function RumDashboard() { - - - + + + - + + + + - - - + + + diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx index c19e2cd4a3742..e18875f32ff72 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/VisitorBreakdown/index.tsx @@ -5,9 +5,9 @@ */ import React from 'react'; -import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui'; +import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui'; import { VisitorBreakdownChart } from '../Charts/VisitorBreakdownChart'; -import { VisitorBreakdownLabel } from '../translations'; +import { I18LABELS, VisitorBreakdownLabel } from '../translations'; import { useFetcher } from '../../../../hooks/useFetcher'; import { useUrlParams } from '../../../../hooks/useUrlParams'; @@ -37,27 +37,24 @@ export function VisitorBreakdown() { return ( <> - +

{VisitorBreakdownLabel}

+ - - -

Browser

-
-
- - - -

Operating System

+ +

{I18LABELS.browser}

+ +
- - -

Device

+ +

{I18LABELS.operatingSystem}

+ +
diff --git a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts index 042e138793f11..660ed5a92a0e6 100644 --- a/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts +++ b/x-pack/plugins/apm/public/components/app/RumDashboard/translations.ts @@ -55,6 +55,15 @@ export const I18LABELS = { coreWebVitals: i18n.translate('xpack.apm.rum.filterGroup.coreWebVitals', { defaultMessage: 'Core web vitals', }), + browser: i18n.translate('xpack.apm.rum.visitorBreakdown.browser', { + defaultMessage: 'Browser', + }), + operatingSystem: i18n.translate( + 'xpack.apm.rum.visitorBreakdown.operatingSystem', + { + defaultMessage: 'Operating system', + } + ), }; export const VisitorBreakdownLabel = i18n.translate( diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx index b3d19e1aab2cc..5699d0b56219b 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/AnomalyDetection.tsx @@ -18,10 +18,13 @@ import { useTheme } from '../../../../hooks/useTheme'; import { fontSize, px } from '../../../../style/variables'; import { asInteger, asDuration } from '../../../../utils/formatters'; import { MLJobLink } from '../../../shared/Links/MachineLearningLinks/MLJobLink'; -import { getSeverityColor, popoverWidth } from '../cytoscapeOptions'; +import { popoverWidth } from '../cytoscapeOptions'; import { TRANSACTION_REQUEST } from '../../../../../common/transaction_types'; -import { ServiceAnomalyStats } from '../../../../../common/anomaly_detection'; -import { getSeverity } from './getSeverity'; +import { + getSeverity, + getSeverityColor, + ServiceAnomalyStats, +} from '../../../../../common/anomaly_detection'; const HealthStatusTitle = styled(EuiTitle)` display: inline; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.ts deleted file mode 100644 index f4eb2033e9231..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/Popover/getSeverity.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export enum severity { - critical = 'critical', - major = 'major', - minor = 'minor', - warning = 'warning', -} - -// TODO: Replace with `getSeverity` from: -// https://github.com/elastic/kibana/blob/0f964f66916480f2de1f4b633e5afafc08cf62a0/x-pack/plugins/ml/common/util/anomaly_utils.ts#L129 -export function getSeverity(score?: number) { - if (typeof score !== 'number') { - return undefined; - } else if (score < 25) { - return severity.warning; - } else if (score >= 25 && score < 50) { - return severity.minor; - } else if (score >= 50 && score < 75) { - return severity.major; - } else if (score >= 75) { - return severity.critical; - } else { - return undefined; - } -} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts index 9fedcc70bbbcf..1ac7157cc2aad 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/cytoscapeOptions.ts @@ -11,25 +11,15 @@ import { } from '../../../../common/elasticsearch_fieldnames'; import { EuiTheme } from '../../../../../observability/public'; import { defaultIcon, iconForNode } from './icons'; -import { ServiceAnomalyStats } from '../../../../common/anomaly_detection'; -import { severity, getSeverity } from './Popover/getSeverity'; +import { + getSeverity, + getSeverityColor, + ServiceAnomalyStats, + Severity, +} from '../../../../common/anomaly_detection'; export const popoverWidth = 280; -export function getSeverityColor(theme: EuiTheme, nodeSeverity?: string) { - switch (nodeSeverity) { - case severity.warning: - return theme.eui.euiColorVis0; - case severity.minor: - case severity.major: - return theme.eui.euiColorVis5; - case severity.critical: - return theme.eui.euiColorVis9; - default: - return; - } -} - function getNodeSeverity(el: cytoscape.NodeSingular) { const serviceAnomalyStats: ServiceAnomalyStats | undefined = el.data( 'serviceAnomalyStats' @@ -60,7 +50,7 @@ const getBorderStyle: cytoscape.Css.MapperFunction< cytoscape.Css.LineStyle > = (el: cytoscape.NodeSingular) => { const nodeSeverity = getNodeSeverity(el); - if (nodeSeverity === severity.critical) { + if (nodeSeverity === Severity.critical) { return 'double'; } else { return 'solid'; @@ -70,9 +60,9 @@ const getBorderStyle: cytoscape.Css.MapperFunction< function getBorderWidth(el: cytoscape.NodeSingular) { const nodeSeverity = getNodeSeverity(el); - if (nodeSeverity === severity.minor || nodeSeverity === severity.major) { + if (nodeSeverity === Severity.minor || nodeSeverity === Severity.major) { return 4; - } else if (nodeSeverity === severity.critical) { + } else if (nodeSeverity === Severity.critical) { return 8; } else { return 4; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts index 2f4cc0d39d71c..c85cf85d38702 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/icons.ts @@ -5,7 +5,6 @@ */ import cytoscape from 'cytoscape'; -import { getNormalizedAgentName } from '../../../../common/agent_name'; import { AGENT_NAME, SPAN_SUBTYPE, @@ -13,29 +12,22 @@ import { } from '../../../../common/elasticsearch_fieldnames'; import awsIcon from './icons/aws.svg'; import cassandraIcon from './icons/cassandra.svg'; -import darkIcon from './icons/dark.svg'; import databaseIcon from './icons/database.svg'; import defaultIconImport from './icons/default.svg'; import documentsIcon from './icons/documents.svg'; -import dotNetIcon from './icons/dot-net.svg'; import elasticsearchIcon from './icons/elasticsearch.svg'; import globeIcon from './icons/globe.svg'; -import goIcon from './icons/go.svg'; import graphqlIcon from './icons/graphql.svg'; import grpcIcon from './icons/grpc.svg'; import handlebarsIcon from './icons/handlebars.svg'; -import javaIcon from './icons/java.svg'; import kafkaIcon from './icons/kafka.svg'; import mongodbIcon from './icons/mongodb.svg'; import mysqlIcon from './icons/mysql.svg'; -import nodeJsIcon from './icons/nodejs.svg'; -import phpIcon from './icons/php.svg'; import postgresqlIcon from './icons/postgresql.svg'; -import pythonIcon from './icons/python.svg'; import redisIcon from './icons/redis.svg'; -import rubyIcon from './icons/ruby.svg'; -import rumJsIcon from './icons/rumjs.svg'; import websocketIcon from './icons/websocket.svg'; +import javaIcon from '../../shared/AgentIcon/icons/java.svg'; +import { getAgentIcon } from '../../shared/AgentIcon/get_agent_icon'; export const defaultIcon = defaultIconImport; @@ -74,23 +66,6 @@ const typeIcons: { [key: string]: { [key: string]: string } } = { }, }; -const agentIcons: { [key: string]: string } = { - dark: darkIcon, - dotnet: dotNetIcon, - go: goIcon, - java: javaIcon, - 'js-base': rumJsIcon, - nodejs: nodeJsIcon, - php: phpIcon, - python: pythonIcon, - ruby: rubyIcon, -}; - -function getAgentIcon(agentName?: string) { - const normalizedAgentName = getNormalizedAgentName(agentName); - return normalizedAgentName && agentIcons[normalizedAgentName]; -} - function getSpanIcon(type?: string, subtype?: string) { if (!type) { return; diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dark.svg b/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dark.svg deleted file mode 100644 index 9ae4b31c1a0d6..0000000000000 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dark.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx index 83fab95bc91c9..cb5a57e9ab9fb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceMap/index.tsx @@ -9,7 +9,7 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { useTheme } from '../../../hooks/useTheme'; import { invalidLicenseMessage, - isValidPlatinumLicense, + isActivePlatinumLicense, } from '../../../../common/service_map'; import { useFetcher } from '../../../hooks/useFetcher'; import { useLicense } from '../../../hooks/useLicense'; @@ -36,7 +36,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { const { data = { elements: [] } } = useFetcher(() => { // When we don't have a license or a valid license, don't make the request. - if (!license || !isValidPlatinumLicense(license)) { + if (!license || !isActivePlatinumLicense(license)) { return; } @@ -66,7 +66,7 @@ export function ServiceMap({ serviceName }: ServiceMapProps) { return null; } - return isValidPlatinumLicense(license) ? ( + return isActivePlatinumLicense(license) ? (
+ {getSeverityLabel(severity)} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx new file mode 100644 index 0000000000000..dd632db0f15fe --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/MLCallout.tsx @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiButton } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGrid } from '@elastic/eui'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { APMLink } from '../../../shared/Links/apm/APMLink'; + +export function MLCallout({ onDismiss }: { onDismiss: () => void }) { + return ( + +

+ {i18n.translate('xpack.apm.serviceOverview.mlNudgeMessage.content', { + defaultMessage: `Our integration with ML anomaly detection will enable you to see your services' health status`, + })} +

+ + + + + {i18n.translate( + 'xpack.apm.serviceOverview.mlNudgeMessage.learnMoreButton', + { + defaultMessage: `Learn more`, + } + )} + + + + + onDismiss()}> + {i18n.translate( + 'xpack.apm.serviceOverview.mlNudgeMessage.dismissButton', + { + defaultMessage: `Dismiss message`, + } + )} + + + +
+ ); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/ServiceListMetric.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/ServiceListMetric.tsx new file mode 100644 index 0000000000000..c94c94d4a0b72 --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/ServiceListMetric.tsx @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; + +import React from 'react'; +import { useTheme } from '../../../../hooks/useTheme'; +import { useUrlParams } from '../../../../hooks/useUrlParams'; +import { getEmptySeries } from '../../../shared/charts/CustomPlot/getEmptySeries'; +import { SparkPlot } from '../../../shared/charts/SparkPlot'; + +export function ServiceListMetric({ + color, + series, + valueLabel, +}: { + color: 'euiColorVis1' | 'euiColorVis0' | 'euiColorVis7'; + series?: Array<{ x: number; y: number | null }>; + valueLabel: React.ReactNode; +}) { + const theme = useTheme(); + + const { + urlParams: { start, end }, + } = useUrlParams(); + + const colorValue = theme.eui[color]; + + return ( + + + + + + {valueLabel} + + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js index 927779b571fd8..519d74827097b 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/List.test.js @@ -15,34 +15,62 @@ describe('ServiceOverview -> List', () => { mockMoment(); }); - it('should render empty state', () => { + it('renders empty state', () => { const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); - it('should render with data', () => { + it('renders with data', () => { const wrapper = shallow(); expect(wrapper).toMatchSnapshot(); }); - it('should render columns correctly', () => { + it('renders columns correctly', () => { const service = { serviceName: 'opbeans-python', agentName: 'python', - transactionsPerMinute: 86.93333333333334, - errorsPerMinute: 12.6, - avgResponseTime: 91535.42944785276, + transactionsPerMinute: { + value: 86.93333333333334, + timeseries: [], + }, + errorsPerMinute: { + value: 12.6, + timeseries: [], + }, + avgResponseTime: { + value: 91535.42944785276, + timeseries: [], + }, environments: ['test'], }; const renderedColumns = SERVICE_COLUMNS.map((c) => c.render(service[c.field], service) ); + expect(renderedColumns[0]).toMatchSnapshot(); - expect(renderedColumns.slice(2)).toEqual([ - 'python', - '92 ms', - '86.9 tpm', - '12.6 err.', - ]); + }); + + describe('without ML data', () => { + it('does not render health column', () => { + const wrapper = shallow( + + ); + + const columns = wrapper.props().columns; + + expect(columns[0].field).not.toBe('severity'); + }); + }); + + describe('with ML data', () => { + it('renders health column', () => { + const wrapper = shallow( + + ); + + const columns = wrapper.props().columns; + + expect(columns[0].field).toBe('severity'); + }); }); }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap index 146f6f58031bb..da3f6ae89940a 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/__snapshots__/List.test.js.snap @@ -1,21 +1,8 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`ServiceOverview -> List should render columns correctly 1`] = ` - - - opbeans-python - - -`; +exports[`ServiceOverview -> List renders columns correctly 1`] = ``; -exports[`ServiceOverview -> List should render empty state 1`] = ` +exports[`ServiceOverview -> List renders empty state 1`] = ` List should render empty state 1`] = ` "name": "Environment", "render": [Function], "sortable": true, - "width": "20%", - }, - Object { - "field": "agentName", - "name": "Agent", - "render": [Function], - "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "avgResponseTime", "name": "Avg. response time", "render": [Function], "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "transactionsPerMinute", "name": "Trans. per minute", "render": [Function], "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "errorsPerMinute", - "name": "Errors per minute", + "name": "Error rate %", "render": [Function], "sortable": true, + "width": "160px", }, ] } initialPageSize={50} - initialSortField="serviceName" + initialSortDirection="desc" + initialSortField="severity" items={Array []} + sortFn={[Function]} /> `; -exports[`ServiceOverview -> List should render with data 1`] = ` +exports[`ServiceOverview -> List renders with data 1`] = ` List should render with data 1`] = ` "name": "Environment", "render": [Function], "sortable": true, - "width": "20%", - }, - Object { - "field": "agentName", - "name": "Agent", - "render": [Function], - "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "avgResponseTime", "name": "Avg. response time", "render": [Function], "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "transactionsPerMinute", "name": "Trans. per minute", "render": [Function], "sortable": true, + "width": "160px", }, Object { + "align": "left", "dataType": "number", "field": "errorsPerMinute", - "name": "Errors per minute", + "name": "Error rate %", "render": [Function], "sortable": true, + "width": "160px", }, ] } initialPageSize={50} - initialSortField="serviceName" + initialSortDirection="desc" + initialSortField="severity" items={ Array [ Object { @@ -125,19 +115,35 @@ exports[`ServiceOverview -> List should render with data 1`] = ` "environments": Array [ "test", ], - "errorsPerMinute": 46.06666666666667, + "errorsPerMinute": Object { + "timeseries": Array [], + "value": 46.06666666666667, + }, "serviceName": "opbeans-node", - "transactionsPerMinute": 0, + "transactionsPerMinute": Object { + "timeseries": Array [], + "value": 0, + }, }, Object { "agentName": "python", - "avgResponseTime": 91535.42944785276, + "avgResponseTime": Object { + "timeseries": Array [], + "value": 91535.42944785276, + }, "environments": Array [], - "errorsPerMinute": 12.6, + "errorsPerMinute": Object { + "timeseries": Array [], + "value": 12.6, + }, "serviceName": "opbeans-python", - "transactionsPerMinute": 86.93333333333334, + "transactionsPerMinute": Object { + "timeseries": Array [], + "value": 86.93333333333334, + }, }, ] } + sortFn={[Function]} /> `; diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json index 2379d27407e04..7f24ad8b0d308 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/__test__/props.json @@ -3,17 +3,34 @@ { "serviceName": "opbeans-node", "agentName": "nodejs", - "transactionsPerMinute": 0, - "errorsPerMinute": 46.06666666666667, + "transactionsPerMinute": { + "value": 0, + "timeseries": [] + }, + "errorsPerMinute": { + "value": 46.06666666666667, + "timeseries": [] + }, "avgResponseTime": null, - "environments": ["test"] + "environments": [ + "test" + ] }, { "serviceName": "opbeans-python", "agentName": "python", - "transactionsPerMinute": 86.93333333333334, - "errorsPerMinute": 12.6, - "avgResponseTime": 91535.42944785276, + "transactionsPerMinute": { + "value": 86.93333333333334, + "timeseries": [] + }, + "errorsPerMinute": { + "value": 12.6, + "timeseries": [] + }, + "avgResponseTime": { + "value": 91535.42944785276, + "timeseries": [] + }, "environments": [] } ] diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx index 90cc9af45273e..ce256137481cb 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/ServiceList/index.tsx @@ -4,24 +4,34 @@ * you may not use this file except in compliance with the Elastic License. */ -import { EuiToolTip } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; import styled from 'styled-components'; +import { ValuesType } from 'utility-types'; +import { orderBy } from 'lodash'; +import { asPercent } from '../../../../../common/utils/formatters'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { ServiceListAPIResponse } from '../../../../../server/lib/services/get_services'; import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { fontSizes, truncate } from '../../../../style/variables'; +import { fontSizes, px, truncate, unit } from '../../../../style/variables'; import { asDecimal, asMillisecondDuration } from '../../../../utils/formatters'; -import { ManagedTable } from '../../../shared/ManagedTable'; +import { ManagedTable, ITableColumn } from '../../../shared/ManagedTable'; import { EnvironmentBadge } from '../../../shared/EnvironmentBadge'; import { TransactionOverviewLink } from '../../../shared/Links/apm/TransactionOverviewLink'; +import { AgentIcon } from '../../../shared/AgentIcon'; +import { Severity } from '../../../../../common/anomaly_detection'; +import { HealthBadge } from './HealthBadge'; +import { ServiceListMetric } from './ServiceListMetric'; interface Props { items: ServiceListAPIResponse['items']; noItemsMessage?: React.ReactNode; + displayHealthStatus: boolean; } +type ServiceListItem = ValuesType; + function formatNumber(value: number) { if (value === 0) { return '0'; @@ -41,7 +51,18 @@ const AppLink = styled(TransactionOverviewLink)` ${truncate('100%')}; `; -export const SERVICE_COLUMNS = [ +export const SERVICE_COLUMNS: Array> = [ + { + field: 'severity', + name: i18n.translate('xpack.apm.servicesTable.healthColumnLabel', { + defaultMessage: 'Health', + }), + width: px(unit * 6), + sortable: true, + render: (_, { severity }) => { + return ; + }, + }, { field: 'serviceName', name: i18n.translate('xpack.apm.servicesTable.nameColumnLabel', { @@ -49,9 +70,24 @@ export const SERVICE_COLUMNS = [ }), width: '40%', sortable: true, - render: (serviceName: string) => ( - - {formatString(serviceName)} + render: (_, { serviceName, agentName }) => ( + + + {agentName && ( + + + + )} + + + {formatString(serviceName)} + + + ), }, @@ -60,20 +96,12 @@ export const SERVICE_COLUMNS = [ name: i18n.translate('xpack.apm.servicesTable.environmentColumnLabel', { defaultMessage: 'Environment', }), - width: '20%', + width: px(unit * 10), sortable: true, - render: (environments: string[]) => ( - + render: (_, { environments }) => ( + ), }, - { - field: 'agentName', - name: i18n.translate('xpack.apm.servicesTable.agentColumnLabel', { - defaultMessage: 'Agent', - }), - sortable: true, - render: (agentName: string) => formatString(agentName), - }, { field: 'avgResponseTime', name: i18n.translate('xpack.apm.servicesTable.avgResponseTimeColumnLabel', { @@ -81,7 +109,15 @@ export const SERVICE_COLUMNS = [ }), sortable: true, dataType: 'number', - render: (time: number) => asMillisecondDuration(time), + render: (_, { avgResponseTime }) => ( + + ), + align: 'left', + width: px(unit * 10), }, { field: 'transactionsPerMinute', @@ -93,39 +129,107 @@ export const SERVICE_COLUMNS = [ ), sortable: true, dataType: 'number', - render: (value: number) => - `${formatNumber(value)} ${i18n.translate( - 'xpack.apm.servicesTable.transactionsPerMinuteUnitLabel', - { - defaultMessage: 'tpm', - } - )}`, + render: (_, { transactionsPerMinute }) => ( + + ), + align: 'left', + width: px(unit * 10), }, { field: 'errorsPerMinute', - name: i18n.translate('xpack.apm.servicesTable.errorsPerMinuteColumnLabel', { - defaultMessage: 'Errors per minute', + name: i18n.translate('xpack.apm.servicesTable.transactionErrorRate', { + defaultMessage: 'Error rate %', }), sortable: true, dataType: 'number', - render: (value: number) => - `${formatNumber(value)} ${i18n.translate( - 'xpack.apm.servicesTable.errorsPerMinuteUnitLabel', - { - defaultMessage: 'err.', - } - )}`, + render: (_, { transactionErrorRate }) => { + const value = transactionErrorRate?.value; + + const valueLabel = + value !== null && value !== undefined ? asPercent(value, 1) : ''; + + return ( + + ); + }, + align: 'left', + width: px(unit * 10), }, ]; -export function ServiceList({ items, noItemsMessage }: Props) { +const SEVERITY_ORDER = [ + Severity.warning, + Severity.minor, + Severity.major, + Severity.critical, +]; + +export function ServiceList({ + items, + displayHealthStatus, + noItemsMessage, +}: Props) { + const columns = displayHealthStatus + ? SERVICE_COLUMNS + : SERVICE_COLUMNS.filter((column) => column.field !== 'severity'); + return ( { + // For severity, sort items by severity first, then by TPM + + return sortField === 'severity' + ? orderBy( + itemsToSort, + [ + (item) => { + return item.severity + ? SEVERITY_ORDER.indexOf(item.severity) + : -1; + }, + (item) => item.transactionsPerMinute?.value ?? 0, + ], + [sortDirection, sortDirection] + ) + : orderBy( + itemsToSort, + (item) => { + switch (sortField) { + case 'avgResponseTime': + return item.avgResponseTime?.value ?? 0; + case 'transactionsPerMinute': + return item.transactionsPerMinute?.value ?? 0; + case 'transactionErrorRate': + return item.transactionErrorRate?.value ?? 0; + + default: + return item[sortField as keyof typeof item]; + } + }, + sortDirection + ); + }} /> ); } diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx index e417138076af9..e4ba1e36378d9 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/ServiceOverview.test.tsx @@ -6,49 +6,53 @@ import { render, wait, waitForElement } from '@testing-library/react'; import { CoreStart } from 'kibana/public'; +import { merge } from 'lodash'; import React, { FunctionComponent, ReactChild } from 'react'; +import { MemoryRouter } from 'react-router-dom'; import { createKibanaReactContext } from 'src/plugins/kibana_react/public'; import { ServiceOverview } from '..'; +import { EuiThemeProvider } from '../../../../../../observability/public'; import { ApmPluginContextValue } from '../../../../context/ApmPluginContext'; import { mockApmPluginContextValue, MockApmPluginContextWrapper, } from '../../../../context/ApmPluginContext/MockApmPluginContext'; +import * as useAnomalyDetectionJobs from '../../../../hooks/useAnomalyDetectionJobs'; import { FETCH_STATUS } from '../../../../hooks/useFetcher'; import * as useLocalUIFilters from '../../../../hooks/useLocalUIFilters'; import * as urlParamsHooks from '../../../../hooks/useUrlParams'; import { SessionStorageMock } from '../../../../services/__test__/SessionStorageMock'; -import { MemoryRouter } from 'react-router-dom'; const KibanaReactContext = createKibanaReactContext({ usageCollection: { reportUiStats: () => {} }, } as Partial); +const addWarning = jest.fn(); +const httpGet = jest.fn(); + function wrapper({ children }: { children: ReactChild }) { + const mockPluginContext = (merge({}, mockApmPluginContextValue, { + core: { + http: { + get: httpGet, + }, + notifications: { + toasts: { + addWarning, + }, + }, + }, + }) as unknown) as ApmPluginContextValue; + return ( - - - {children} - - + + + + {children} + + + ); } @@ -59,9 +63,6 @@ function renderServiceOverview() { }); } -const addWarning = jest.fn(); -const httpGet = jest.fn(); - describe('Service Overview -> View', () => { beforeEach(() => { // @ts-expect-error @@ -83,6 +84,17 @@ describe('Service Overview -> View', () => { clearValues: () => null, status: FETCH_STATUS.SUCCESS, }); + + jest + .spyOn(useAnomalyDetectionJobs, 'useAnomalyDetectionJobs') + .mockReturnValue({ + status: FETCH_STATUS.SUCCESS, + data: { + jobs: [], + hasLegacyJobs: false, + }, + refetch: () => undefined, + }); }); afterEach(() => { @@ -102,6 +114,7 @@ describe('Service Overview -> View', () => { errorsPerMinute: 200, avgResponseTime: 300, environments: ['test', 'dev'], + severity: 1, }, { serviceName: 'My Go Service', @@ -110,6 +123,7 @@ describe('Service Overview -> View', () => { errorsPerMinute: 500, avgResponseTime: 600, environments: [], + severity: 10, }, ], }); @@ -198,4 +212,57 @@ describe('Service Overview -> View', () => { expect(addWarning).not.toHaveBeenCalled(); }); }); + + describe('when ML data is not found', () => { + it('does not render the health column', async () => { + httpGet.mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [ + { + serviceName: 'My Python Service', + agentName: 'python', + transactionsPerMinute: 100, + errorsPerMinute: 200, + avgResponseTime: 300, + environments: ['test', 'dev'], + }, + ], + }); + + const { queryByText } = renderServiceOverview(); + + // wait for requests to be made + await wait(() => expect(httpGet).toHaveBeenCalledTimes(1)); + + expect(queryByText('Health')).toBeNull(); + }); + }); + + describe('when ML data is found', () => { + it('renders the health column', async () => { + httpGet.mockResolvedValueOnce({ + hasLegacyData: false, + hasHistoricalData: true, + items: [ + { + serviceName: 'My Python Service', + agentName: 'python', + transactionsPerMinute: 100, + errorsPerMinute: 200, + avgResponseTime: 300, + environments: ['test', 'dev'], + severity: 1, + }, + ], + }); + + const { queryAllByText } = renderServiceOverview(); + + // wait for requests to be made + await wait(() => expect(httpGet).toHaveBeenCalledTimes(1)); + + expect(queryAllByText('Health').length).toBeGreaterThan(1); + }); + }); }); diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap index 6d447887627bf..b56f7d6820274 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/__test__/__snapshots__/ServiceOverview.test.tsx.snap @@ -7,7 +7,7 @@ NodeList [ >
- Name + Health
- - My Go Service - + + Unknown + +
- Environment + Name
+ > + + + +
- Agent + Environment
- go + + + + test + + + + + + + dev + + +
- 0.6 ms +
+
+
+
+
+
+
+
+
+ N/A +
+
+
+
+
+
+ 0 ms +
+
- 400.0 tpm +
+
+
+
+
+
+
+
+
+ N/A +
+
+
+
+
+
+ 0 tpm +
+
- Errors per minute + Error rate %
- 500.0 err. +
+
+
+
+
+
+
+
+
+ N/A +
+
+
+
+
+
+
, @@ -247,87 +423,91 @@ NodeList [ >
- Name + Health
- - My Python Service - + + Unknown + +
- Environment + Name
- - - test - - - - - - +
+
- dev - - + + My Go Service + +
+
- Agent + Environment
- python -
+ />
- 0.3 ms +
+
+
+
+
+
+
+
+
+ N/A +
+
+
+
+
+
+ 0 ms +
+
- 100.0 tpm +
+
+
+
+
+
+
+
+
+ N/A +
+
+
+
+
+
+ 0 tpm +
+
- Errors per minute + Error rate %
- 200.0 err. +
+
+
+
+
+
+
+
+
+ N/A +
+
+
+
+
+
+
, diff --git a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx index 7146e471a7f82..d9d2cffb67620 100644 --- a/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/ServiceOverview/index.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import React, { useEffect, useMemo } from 'react'; import url from 'url'; import { toMountPoint } from '../../../../../../../src/plugins/kibana_react/public'; -import { useFetcher } from '../../../hooks/useFetcher'; +import { useFetcher, FETCH_STATUS } from '../../../hooks/useFetcher'; import { NoServicesMessage } from './NoServicesMessage'; import { ServiceList } from './ServiceList'; import { useUrlParams } from '../../../hooks/useUrlParams'; @@ -18,8 +18,11 @@ import { useTrackPageview } from '../../../../../observability/public'; import { Projection } from '../../../../common/projections'; import { LocalUIFilters } from '../../shared/LocalUIFilters'; import { useApmPluginContext } from '../../../hooks/useApmPluginContext'; +import { MLCallout } from './ServiceList/MLCallout'; +import { useLocalStorage } from '../../../hooks/useLocalStorage'; +import { useAnomalyDetectionJobs } from '../../../hooks/useAnomalyDetectionJobs'; -const initalData = { +const initialData = { items: [], hasHistoricalData: true, hasLegacyData: false, @@ -33,7 +36,7 @@ export function ServiceOverview() { urlParams: { start, end }, uiFilters, } = useUrlParams(); - const { data = initalData, status } = useFetcher( + const { data = initialData, status } = useFetcher( (callApmApi) => { if (start && end) { return callApmApi({ @@ -93,6 +96,26 @@ export function ServiceOverview() { [] ); + const { + data: anomalyDetectionJobsData, + status: anomalyDetectionJobsStatus, + } = useAnomalyDetectionJobs(); + + const [userHasDismissedCallout, setUserHasDismissedCallout] = useLocalStorage( + 'apm.userHasDismissedServiceInventoryMlCallout', + false + ); + + const canCreateJob = !!core.application.capabilities.ml?.canCreateJob; + + const displayMlCallout = + anomalyDetectionJobsStatus === FETCH_STATUS.SUCCESS && + !anomalyDetectionJobsData?.jobs.length && + canCreateJob && + !userHasDismissedCallout; + + const displayHealthStatus = data.items.some((item) => 'severity' in item); + return ( <> @@ -101,17 +124,27 @@ export function ServiceOverview() { - - + {displayMlCallout ? ( + + setUserHasDismissedCallout(true)} /> + + ) : null} + + + + } /> - } - /> - + + + diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts new file mode 100644 index 0000000000000..2475eecee8e34 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/get_agent_icon.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getNormalizedAgentName } from '../../../../common/agent_name'; +import dotNetIcon from './icons/dot-net.svg'; +import goIcon from './icons/go.svg'; +import javaIcon from './icons/java.svg'; +import nodeJsIcon from './icons/nodejs.svg'; +import phpIcon from './icons/php.svg'; +import pythonIcon from './icons/python.svg'; +import rubyIcon from './icons/ruby.svg'; +import rumJsIcon from './icons/rumjs.svg'; + +const agentIcons: { [key: string]: string } = { + dotnet: dotNetIcon, + go: goIcon, + java: javaIcon, + 'js-base': rumJsIcon, + nodejs: nodeJsIcon, + php: phpIcon, + python: pythonIcon, + ruby: rubyIcon, +}; + +export function getAgentIcon(agentName?: string) { + const normalizedAgentName = getNormalizedAgentName(agentName); + return normalizedAgentName && agentIcons[normalizedAgentName]; +} diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/dot-net.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/dot-net.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/dot-net.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/go.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/go.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/go.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/go.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/java.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/java.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/java.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/java.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/nodejs.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/nodejs.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/nodejs.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/php.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/php.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/php.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/python.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/python.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/python.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/python.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/ruby.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/ruby.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/ruby.svg diff --git a/x-pack/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg b/x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs.svg similarity index 100% rename from x-pack/plugins/apm/public/components/app/ServiceMap/icons/rumjs.svg rename to x-pack/plugins/apm/public/components/shared/AgentIcon/icons/rumjs.svg diff --git a/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx b/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx new file mode 100644 index 0000000000000..5646fc05bd28f --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/AgentIcon/index.tsx @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; +import { getAgentIcon } from './get_agent_icon'; +import { px } from '../../../style/variables'; + +interface Props { + agentName: AgentName; +} + +export function AgentIcon(props: Props) { + const { agentName } = props; + + const icon = getAgentIcon(agentName); + + return {agentName}; +} diff --git a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx index 9fe52aab83641..9db563a0f6ba8 100644 --- a/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/ManagedTable/index.tsx @@ -33,9 +33,22 @@ interface Props { hidePerPageOptions?: boolean; noItemsMessage?: React.ReactNode; sortItems?: boolean; + sortFn?: ( + items: T[], + sortField: string, + sortDirection: 'asc' | 'desc' + ) => T[]; pagination?: boolean; } +function defaultSortFn( + items: T[], + sortField: string, + sortDirection: 'asc' | 'desc' +) { + return orderBy(items, sortField, sortDirection); +} + function UnoptimizedManagedTable(props: Props) { const history = useHistory(); const { @@ -48,6 +61,7 @@ function UnoptimizedManagedTable(props: Props) { hidePerPageOptions = true, noItemsMessage, sortItems = true, + sortFn = defaultSortFn, pagination = true, } = props; @@ -62,11 +76,11 @@ function UnoptimizedManagedTable(props: Props) { const renderedItems = useMemo(() => { const sortedItems = sortItems - ? orderBy(items, sortField, sortDirection as 'asc' | 'desc') + ? sortFn(items, sortField, sortDirection as 'asc' | 'desc') : items; return sortedItems.slice(page * pageSize, (page + 1) * pageSize); - }, [page, pageSize, sortField, sortDirection, items, sortItems]); + }, [page, pageSize, sortField, sortDirection, items, sortItems, sortFn]); const sort = useMemo(() => { return { diff --git a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx b/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx index fcbdb900368ea..5bddfc67200b1 100644 --- a/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx +++ b/x-pack/plugins/apm/public/components/shared/TransactionDurationAnomalyAlertTrigger/SelectAnomalySeverity.tsx @@ -8,9 +8,11 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiText } from '@elastic/eui'; -import { getSeverityColor } from '../../app/ServiceMap/cytoscapeOptions'; +import { + getSeverityColor, + Severity, +} from '../../../../common/anomaly_detection'; import { useTheme } from '../../../hooks/useTheme'; -import { severity as Severity } from '../../app/ServiceMap/Popover/getSeverity'; type SeverityScore = 0 | 25 | 50 | 75; const ANOMALY_SCORES: SeverityScore[] = [0, 25, 50, 75]; diff --git a/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx new file mode 100644 index 0000000000000..18b914afea995 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/SparkPlot/index.tsx @@ -0,0 +1,66 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { ScaleType, Chart, Settings, AreaSeries } from '@elastic/charts'; +import { EuiIcon } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiText } from '@elastic/eui'; +import { px } from '../../../../style/variables'; +import { useChartTheme } from '../../../../../../observability/public'; +import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; + +interface Props { + color: string; + series: Array<{ x: number; y: number | null }>; +} + +export function SparkPlot(props: Props) { + const { series, color } = props; + const chartTheme = useChartTheme(); + + const isEmpty = series.every((point) => point.y === null); + + if (isEmpty) { + return ( + + + + + + + {NOT_AVAILABLE_LABEL} + + + + ); + } + + return ( + + + + + ); +} diff --git a/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts b/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts new file mode 100644 index 0000000000000..56c58bc82967b --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/useAnomalyDetectionJobs.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useFetcher } from './useFetcher'; + +export function useAnomalyDetectionJobs() { + return useFetcher( + (callApmApi) => + callApmApi({ + pathname: `/api/apm/settings/anomaly-detection`, + }), + [], + { showToastOnError: false } + ); +} diff --git a/x-pack/plugins/apm/public/hooks/useLocalStorage.ts b/x-pack/plugins/apm/public/hooks/useLocalStorage.ts new file mode 100644 index 0000000000000..cf37b45045f4d --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/useLocalStorage.ts @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState, useEffect } from 'react'; + +export function useLocalStorage(key: string, defaultValue: T) { + const [item, setItem] = useState(getFromStorage()); + + function getFromStorage() { + const storedItem = window.localStorage.getItem(key); + + let toStore: T = defaultValue; + + if (storedItem !== null) { + try { + toStore = JSON.parse(storedItem) as T; + } catch (err) { + window.localStorage.removeItem(key); + // eslint-disable-next-line no-console + console.log(`Unable to decode: ${key}`); + } + } + + return toStore; + } + + const updateFromStorage = () => { + const storedItem = getFromStorage(); + setItem(storedItem); + }; + + const saveToStorage = (value: T) => { + if (value === undefined) { + window.localStorage.removeItem(key); + } else { + window.localStorage.setItem(key, JSON.stringify(value)); + updateFromStorage(); + } + }; + + useEffect(() => { + window.addEventListener('storage', (event: StorageEvent) => { + if (event.key === key) { + updateFromStorage(); + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return [item, saveToStorage] as const; +} diff --git a/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts index c3cf363cbec05..ef85112918712 100644 --- a/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts +++ b/x-pack/plugins/apm/scripts/aggregate-latency-metrics/index.ts @@ -4,15 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Client } from '@elastic/elasticsearch'; import { argv } from 'yargs'; import pLimit from 'p-limit'; import pRetry from 'p-retry'; -import { parse, format } from 'url'; import { set } from '@elastic/safer-lodash-set'; import { uniq, without, merge, flatten } from 'lodash'; import * as histogram from 'hdr-histogram-js'; -import { ESSearchResponse } from '../../typings/elasticsearch'; import { HOST_NAME, SERVICE_NAME, @@ -28,6 +25,8 @@ import { } from '../../common/elasticsearch_fieldnames'; import { stampLogger } from '../shared/stamp-logger'; import { createOrUpdateIndex } from '../shared/create-or-update-index'; +import { parseIndexUrl } from '../shared/parse_index_url'; +import { ESClient, getEsClient } from '../shared/get_es_client'; // This script will try to estimate how many latency metric documents // will be created based on the available transaction documents. @@ -125,41 +124,18 @@ export async function aggregateLatencyMetrics() { const source = String(argv.source ?? ''); const dest = String(argv.dest ?? ''); - function getClientOptionsFromIndexUrl( - url: string - ): { node: string; index: string } { - const parsed = parse(url); - const { pathname, ...rest } = parsed; + const sourceOptions = parseIndexUrl(source); - return { - node: format(rest), - index: pathname!.replace('/', ''), - }; - } - - const sourceOptions = getClientOptionsFromIndexUrl(source); - - const sourceClient = new Client({ - node: sourceOptions.node, - ssl: { - rejectUnauthorized: false, - }, - requestTimeout: 120000, - }); + const sourceClient = getEsClient({ node: sourceOptions.node }); - let destClient: Client | undefined; + let destClient: ESClient | undefined; let destOptions: { node: string; index: string } | undefined; const uploadMetrics = !!dest; if (uploadMetrics) { - destOptions = getClientOptionsFromIndexUrl(dest); - destClient = new Client({ - node: destOptions.node, - ssl: { - rejectUnauthorized: false, - }, - }); + destOptions = parseIndexUrl(dest); + destClient = getEsClient({ node: destOptions.node }); const mappings = ( await sourceClient.indices.getMapping({ @@ -298,10 +274,9 @@ export async function aggregateLatencyMetrics() { }, }; - const response = (await sourceClient.search(params)) - .body as ESSearchResponse; + const response = await sourceClient.search(params); - const { aggregations } = response; + const { aggregations } = response.body; if (!aggregations) { return buckets; @@ -333,10 +308,9 @@ export async function aggregateLatencyMetrics() { }, }; - const response = (await sourceClient.search(params)) - .body as ESSearchResponse; + const response = await sourceClient.search(params); - return response.hits.total.value; + return response.body.hits.total.value; } const [buckets, numberOfTransactionDocuments] = await Promise.all([ diff --git a/x-pack/plugins/apm/scripts/create-functional-tests-archive.js b/x-pack/plugins/apm/scripts/create-functional-tests-archive.js new file mode 100644 index 0000000000000..6b3473dc2ac0a --- /dev/null +++ b/x-pack/plugins/apm/scripts/create-functional-tests-archive.js @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// compile typescript on the fly +// eslint-disable-next-line import/no-extraneous-dependencies +require('@babel/register')({ + extensions: ['.js', '.ts'], + plugins: ['@babel/plugin-proposal-optional-chaining'], + presets: [ + '@babel/typescript', + ['@babel/preset-env', { targets: { node: 'current' } }], + ], +}); + +require('./create-functional-tests-archive/index.ts'); diff --git a/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts b/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts new file mode 100644 index 0000000000000..723ff03dc4995 --- /dev/null +++ b/x-pack/plugins/apm/scripts/create-functional-tests-archive/index.ts @@ -0,0 +1,143 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { argv } from 'yargs'; +import { execSync } from 'child_process'; +import moment from 'moment'; +import path from 'path'; +import fs from 'fs'; +import { stampLogger } from '../shared/stamp-logger'; + +async function run() { + stampLogger(); + + const archiveName = 'apm_8.0.0'; + + // include important APM data and ML data + const indices = + 'apm-*-transaction,apm-*-span,apm-*-error,apm-*-metric,.ml-anomalies*,.ml-config'; + + const esUrl = argv['es-url'] as string | undefined; + + if (!esUrl) { + throw new Error('--es-url is not set'); + } + const kibanaUrl = argv['kibana-url'] as string | undefined; + + if (!kibanaUrl) { + throw new Error('--kibana-url is not set'); + } + const gte = moment().subtract(1, 'hour').toISOString(); + const lt = moment(gte).add(30, 'minutes').toISOString(); + + // eslint-disable-next-line no-console + console.log(`Archiving from ${gte} to ${lt}...`); + + // APM data uses '@timestamp' (ECS), ML data uses 'timestamp' + + const rangeQueries = [ + { + range: { + '@timestamp': { + gte, + lt, + }, + }, + }, + { + range: { + timestamp: { + gte, + lt, + }, + }, + }, + ]; + + // some of the data is timeless/content + const query = { + bool: { + should: [ + ...rangeQueries, + { + bool: { + must_not: [ + { + exists: { + field: '@timestamp', + }, + }, + { + exists: { + field: 'timestamp', + }, + }, + ], + }, + }, + ], + minimum_should_match: 1, + }, + }; + + const root = path.join(__dirname, '../../../../..'); + const commonDir = path.join(root, 'x-pack/test/apm_api_integration/common'); + const archivesDir = path.join(commonDir, 'fixtures/es_archiver'); + + // create the archive + + execSync( + `node scripts/es_archiver save ${archiveName} ${indices} --dir=${archivesDir} --kibana-url=${kibanaUrl} --es-url=${esUrl} --query='${JSON.stringify( + query + )}'`, + { + cwd: root, + stdio: 'inherit', + } + ); + + const currentConfig = {}; + + // get the current metadata and extend/override metadata for the new archive + const configFilePath = path.join(commonDir, 'archives_metadata.ts'); + + try { + Object.assign(currentConfig, (await import(configFilePath)).default); + } catch (error) { + // do nothing + } + + const newConfig = { + ...currentConfig, + [archiveName]: { + start: gte, + end: lt, + }, + }; + + fs.writeFileSync( + configFilePath, + `export default ${JSON.stringify(newConfig, null, 2)}`, + { encoding: 'utf-8' } + ); + + // run ESLint on the generated metadata files + + execSync('node scripts/eslint **/*/archives_metadata.ts --fix', { + cwd: root, + stdio: 'inherit', + }); +} + +run() + .then(() => { + process.exit(0); + }) + .catch((err) => { + // eslint-disable-next-line no-console + console.log(err); + process.exit(1); + }); diff --git a/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts b/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts index 6d44e12fb00a2..01fa5b0509bcd 100644 --- a/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts +++ b/x-pack/plugins/apm/scripts/shared/create-or-update-index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Client } from '@elastic/elasticsearch'; +import { ESClient } from './get_es_client'; export async function createOrUpdateIndex({ client, @@ -12,7 +12,7 @@ export async function createOrUpdateIndex({ indexName, template, }: { - client: Client; + client: ESClient; clear: boolean; indexName: string; template: any; diff --git a/x-pack/plugins/apm/scripts/shared/get_es_client.ts b/x-pack/plugins/apm/scripts/shared/get_es_client.ts new file mode 100644 index 0000000000000..86dfd92190fdf --- /dev/null +++ b/x-pack/plugins/apm/scripts/shared/get_es_client.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Client } from '@elastic/elasticsearch'; +import { ApiKeyAuth, BasicAuth } from '@elastic/elasticsearch/lib/pool'; +import { ESSearchResponse, ESSearchRequest } from '../../typings/elasticsearch'; + +export type ESClient = ReturnType; + +export function getEsClient({ + node, + auth, +}: { + node: string; + auth?: BasicAuth | ApiKeyAuth; +}) { + const client = new Client({ + node, + ssl: { + rejectUnauthorized: false, + }, + requestTimeout: 120000, + auth, + }); + + return { + ...client, + async search( + request: TSearchRequest + ) { + const response = await client.search(request as any); + + return { + ...response, + body: response.body as ESSearchResponse, + }; + }, + }; +} diff --git a/x-pack/plugins/apm/scripts/shared/parse_index_url.ts b/x-pack/plugins/apm/scripts/shared/parse_index_url.ts new file mode 100644 index 0000000000000..190f7fda396bd --- /dev/null +++ b/x-pack/plugins/apm/scripts/shared/parse_index_url.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { parse, format } from 'url'; + +export function parseIndexUrl(url: string): { node: string; index: string } { + const parsed = parse(url); + const { pathname, ...rest } = parsed; + + return { + node: format(rest), + index: pathname!.replace('/', ''), + }; +} diff --git a/x-pack/plugins/apm/scripts/tsconfig.json b/x-pack/plugins/apm/scripts/tsconfig.json index 64602bc6b2769..f1643608496ad 100644 --- a/x-pack/plugins/apm/scripts/tsconfig.json +++ b/x-pack/plugins/apm/scripts/tsconfig.json @@ -1,7 +1,8 @@ { "extends": "../../../../tsconfig.base.json", "include": [ - "./**/*" + "./**/*", + "../observability" ], "exclude": [], "compilerOptions": { 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 e7eb7b8de65e3..93af51b572aa5 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 @@ -81,6 +81,11 @@ export function registerTransactionDurationAnomalyAlertType({ anomalyDetectors, alertParams.environment ); + + if (mlJobIds.length === 0) { + return {}; + } + const anomalySearchParams = { body: { size: 0, diff --git a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts index 75b0471424e79..5b78d97d5b681 100644 --- a/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/get_bucket_size/index.ts @@ -7,22 +7,23 @@ import moment from 'moment'; // @ts-expect-error import { calculateAuto } from './calculate_auto'; -// @ts-expect-error -import { unitToSeconds } from './unit_to_seconds'; -export function getBucketSize(start: number, end: number, interval: string) { +export function getBucketSize( + start: number, + end: number, + numBuckets: number = 100 +) { const duration = moment.duration(end - start, 'ms'); - const bucketSize = Math.max(calculateAuto.near(100, duration).asSeconds(), 1); + const bucketSize = Math.max( + calculateAuto.near(numBuckets, duration).asSeconds(), + 1 + ); const intervalString = `${bucketSize}s`; - const matches = interval && interval.match(/^([\d]+)([shmdwMy]|ms)$/); - const minBucketSize = matches - ? Number(matches[1]) * unitToSeconds(matches[2]) - : 0; - if (bucketSize < minBucketSize) { + if (bucketSize < 0) { return { - bucketSize: minBucketSize, - intervalString: interval, + bucketSize: 0, + intervalString: 'auto', }; } diff --git a/x-pack/plugins/apm/server/lib/helpers/metrics.ts b/x-pack/plugins/apm/server/lib/helpers/metrics.ts index 9f5b5cdf47552..ea018868f9517 100644 --- a/x-pack/plugins/apm/server/lib/helpers/metrics.ts +++ b/x-pack/plugins/apm/server/lib/helpers/metrics.ts @@ -11,7 +11,7 @@ export function getMetricsDateHistogramParams( end: number, metricsInterval: number ) { - const { bucketSize } = getBucketSize(start, end, 'auto'); + const { bucketSize } = getBucketSize(start, end); return { field: '@timestamp', diff --git a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts index 6b69e57389dff..eba75433a5148 100644 --- a/x-pack/plugins/apm/server/lib/helpers/setup_request.ts +++ b/x-pack/plugins/apm/server/lib/helpers/setup_request.ts @@ -5,6 +5,7 @@ */ import moment from 'moment'; +import { isActivePlatinumLicense } from '../../../common/service_map'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/common'; import { KibanaRequest } from '../../../../../../src/core/server'; import { APMConfig } from '../..'; @@ -98,11 +99,14 @@ export async function setupRequest( context, request, }), - ml: getMlSetup( - context.plugins.ml, - context.core.savedObjects.client, - request - ), + ml: + context.plugins.ml && isActivePlatinumLicense(context.licensing.license) + ? getMlSetup( + context.plugins.ml, + context.core.savedObjects.client, + request + ) + : undefined, config, }; @@ -115,14 +119,10 @@ export async function setupRequest( } function getMlSetup( - ml: APMRequestHandlerContext['plugins']['ml'], + ml: Required['ml'], savedObjectsClient: APMRequestHandlerContext['core']['savedObjects']['client'], request: KibanaRequest ) { - if (!ml) { - return; - } - return { mlSystem: ml.mlSystemProvider(request), anomalyDetectors: ml.anomalyDetectorsProvider(request), diff --git a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts index 551384da2cca7..d7e64bdcacd12 100644 --- a/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts +++ b/x-pack/plugins/apm/server/lib/metrics/by_agent/java/gc/fetch_and_transform_gc_metrics.ts @@ -44,7 +44,7 @@ export async function fetchAndTransformGcMetrics({ }) { const { start, end, apmEventClient, config } = setup; - const { bucketSize } = getBucketSize(start, end, 'auto'); + const { bucketSize } = getBucketSize(start, end); const projection = getMetricsProjection({ setup, @@ -74,7 +74,7 @@ export async function fetchAndTransformGcMetrics({ field: `${LABEL_NAME}`, }, aggs: { - over_time: { + timeseries: { date_histogram: getMetricsDateHistogramParams( start, end, @@ -123,7 +123,7 @@ export async function fetchAndTransformGcMetrics({ const series = aggregations.per_pool.buckets.map((poolBucket, i) => { const label = poolBucket.key as string; - const timeseriesData = poolBucket.over_time; + const timeseriesData = poolBucket.timeseries; const data = timeseriesData.buckets.map((bucket) => { // derivative/value will be undefined for the first hit and if the `max` value is null diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts index ec274d20b6005..ed8ae923e6e6c 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_anomalies.ts @@ -3,7 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { Logger } from 'kibana/server'; import Boom from 'boom'; import { Setup, SetupTimeRange } from '../helpers/setup_request'; import { PromiseReturnType } from '../../../typings/common'; @@ -27,11 +26,9 @@ export type ServiceAnomaliesResponse = PromiseReturnType< export async function getServiceAnomalies({ setup, - logger, environment, }: { setup: Setup & SetupTimeRange; - logger: Logger; environment?: string; }) { const { ml, start, end } = setup; @@ -41,11 +38,20 @@ export async function getServiceAnomalies({ } const mlCapabilities = await ml.mlSystem.mlCapabilities(); + if (!mlCapabilities.mlFeatureEnabledInSpace) { throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); } const mlJobIds = await getMLJobIds(ml.anomalyDetectors, environment); + + if (!mlJobIds.length) { + return { + mlJobIds: [], + serviceAnomalies: {}, + }; + } + const params = { body: { size: 0, @@ -120,7 +126,9 @@ interface ServiceAnomaliesAggResponse { function transformResponseToServiceAnomalies( response: ServiceAnomaliesAggResponse ): Record { - const serviceAnomaliesMap = response.aggregations.services.buckets.reduce( + const serviceAnomaliesMap = ( + response.aggregations?.services.buckets ?? [] + ).reduce( (statsByServiceName, { key: serviceName, top_score: topScoreAgg }) => { return { ...statsByServiceName, @@ -153,7 +161,7 @@ export async function getMLJobIds( (job) => job.custom_settings?.job_tags?.environment === environment ); if (!matchingMLJob) { - throw new Error(`ML job Not Found for environment "${environment}".`); + return []; } return [matchingMLJob.job_id]; } diff --git a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts index d1c99d778c8f0..1e26b6f3f58f9 100644 --- a/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts +++ b/x-pack/plugins/apm/server/lib/service_map/get_service_map_service_node_info.test.ts @@ -58,6 +58,9 @@ describe('getServiceMapServiceNodeInfo', () => { indices: {}, start: 1593460053026000, end: 1593497863217000, + config: { + 'xpack.apm.metricsInterval': 30, + }, } as unknown) as Setup & SetupTimeRange; const environment = 'test environment'; const serviceName = 'test service name'; 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 ca86c1d93fa6e..c5e072e073992 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 @@ -105,6 +105,24 @@ Array [ "field": "transaction.duration.us", }, }, + "timeseries": Object { + "aggs": Object { + "average": Object { + "avg": Object { + "field": "transaction.duration.us", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "43200s", + "min_doc_count": 0, + }, + }, }, "terms": Object { "field": "service.name", @@ -194,6 +212,19 @@ Array [ "body": Object { "aggs": Object { "services": Object { + "aggs": Object { + "timeseries": Object { + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "43200s", + "min_doc_count": 0, + }, + }, + }, "terms": Object { "field": "service.name", "size": 500, @@ -226,12 +257,37 @@ Array [ Object { "apm": Object { "events": Array [ - "error", + "transaction", ], }, "body": Object { "aggs": Object { "services": Object { + "aggs": Object { + "outcomes": Object { + "terms": Object { + "field": "event.outcome", + }, + }, + "timeseries": Object { + "aggs": Object { + "outcomes": Object { + "terms": Object { + "field": "event.outcome", + }, + }, + }, + "date_histogram": Object { + "extended_bounds": Object { + "max": 1528977600000, + "min": 1528113600000, + }, + "field": "@timestamp", + "fixed_interval": "43200s", + "min_doc_count": 0, + }, + }, + }, "terms": Object { "field": "service.name", "size": 500, @@ -255,6 +311,14 @@ Array [ "my.custom.ui.filter": "foo-bar", }, }, + Object { + "terms": Object { + "event.outcome": Array [ + "failure", + "success", + ], + }, + }, ], }, }, diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts index d888b43b63fac..50a968467fb4b 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items.ts @@ -15,15 +15,22 @@ import { getTransactionDurationAverages, getAgentNames, getTransactionRates, - getErrorRates, + getTransactionErrorRates, getEnvironments, + getHealthStatuses, } from './get_services_items_stats'; export type ServiceListAPIResponse = PromiseReturnType; export type ServicesItemsSetup = Setup & SetupTimeRange & SetupUIFilters; export type ServicesItemsProjection = ReturnType; -export async function getServicesItems(setup: ServicesItemsSetup) { +export async function getServicesItems({ + setup, + mlAnomaliesEnvironment, +}: { + setup: ServicesItemsSetup; + mlAnomaliesEnvironment?: string; +}) { const params = { projection: getServicesProjection({ setup }), setup, @@ -33,22 +40,25 @@ export async function getServicesItems(setup: ServicesItemsSetup) { transactionDurationAverages, agentNames, transactionRates, - errorRates, + transactionErrorRates, environments, + healthStatuses, ] = await Promise.all([ getTransactionDurationAverages(params), getAgentNames(params), getTransactionRates(params), - getErrorRates(params), + getTransactionErrorRates(params), getEnvironments(params), + getHealthStatuses(params, mlAnomaliesEnvironment), ]); const allMetrics = [ ...transactionDurationAverages, ...agentNames, ...transactionRates, - ...errorRates, + ...transactionErrorRates, ...environments, + ...healthStatuses, ]; return joinByKey(allMetrics, 'serviceName'); diff --git a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts index ddce3b667a603..ab6b61ca21746 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/get_services_items_stats.ts @@ -4,10 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EventOutcome } from '../../../../common/event_outcome'; +import { getSeverity } from '../../../../common/anomaly_detection'; +import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent'; import { TRANSACTION_DURATION, AGENT_NAME, SERVICE_ENVIRONMENT, + EVENT_OUTCOME, } from '../../../../common/elasticsearch_fieldnames'; import { mergeProjection } from '../../../projections/util/merge_projection'; import { ProcessorEvent } from '../../../../common/processor_event'; @@ -15,6 +19,21 @@ import { ServicesItemsSetup, ServicesItemsProjection, } from './get_services_items'; +import { getBucketSize } from '../../helpers/get_bucket_size'; +import { + getMLJobIds, + getServiceAnomalies, +} from '../../service_map/get_service_anomalies'; +import { AggregationResultOf } from '../../../../typings/elasticsearch/aggregations'; + +function getDateHistogramOpts(start: number, end: number) { + return { + field: '@timestamp', + fixed_interval: getBucketSize(start, end, 20).intervalString, + min_doc_count: 0, + extended_bounds: { min: start, max: end }, + }; +} const MAX_NUMBER_OF_SERVICES = 500; @@ -30,7 +49,7 @@ export const getTransactionDurationAverages = async ({ setup, projection, }: AggregationParams) => { - const { apmEventClient } = setup; + const { apmEventClient, start, end } = setup; const response = await apmEventClient.search( mergeProjection(projection, { @@ -51,6 +70,16 @@ export const getTransactionDurationAverages = async ({ field: TRANSACTION_DURATION, }, }, + timeseries: { + date_histogram: getDateHistogramOpts(start, end), + aggs: { + average: { + avg: { + field: TRANSACTION_DURATION, + }, + }, + }, + }, }, }, }, @@ -64,9 +93,15 @@ export const getTransactionDurationAverages = async ({ return []; } - return aggregations.services.buckets.map((bucket) => ({ - serviceName: bucket.key as string, - avgResponseTime: bucket.average.value, + return aggregations.services.buckets.map((serviceBucket) => ({ + serviceName: serviceBucket.key as string, + avgResponseTime: { + value: serviceBucket.average.value, + timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.average.value, + })), + }, })); }; @@ -112,9 +147,10 @@ export const getAgentNames = async ({ return []; } - return aggregations.services.buckets.map((bucket) => ({ - serviceName: bucket.key as string, - agentName: bucket.agent_name.hits.hits[0]?._source.agent.name, + return aggregations.services.buckets.map((serviceBucket) => ({ + serviceName: serviceBucket.key as string, + agentName: serviceBucket.agent_name.hits.hits[0]?._source.agent + .name as AgentName, })); }; @@ -122,7 +158,7 @@ export const getTransactionRates = async ({ setup, projection, }: AggregationParams) => { - const { apmEventClient } = setup; + const { apmEventClient, start, end } = setup; const response = await apmEventClient.search( mergeProjection(projection, { apm: { @@ -136,6 +172,11 @@ export const getTransactionRates = async ({ ...projection.body.aggs.services.terms, size: MAX_NUMBER_OF_SERVICES, }, + aggs: { + timeseries: { + date_histogram: getDateHistogramOpts(start, end), + }, + }, }, }, }, @@ -150,33 +191,67 @@ export const getTransactionRates = async ({ const deltaAsMinutes = getDeltaAsMinutes(setup); - return aggregations.services.buckets.map((bucket) => { - const transactionsPerMinute = bucket.doc_count / deltaAsMinutes; + return aggregations.services.buckets.map((serviceBucket) => { + const transactionsPerMinute = serviceBucket.doc_count / deltaAsMinutes; return { - serviceName: bucket.key as string, - transactionsPerMinute, + serviceName: serviceBucket.key as string, + transactionsPerMinute: { + value: transactionsPerMinute, + timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => ({ + x: dateBucket.key, + y: dateBucket.doc_count / deltaAsMinutes, + })), + }, }; }); }; -export const getErrorRates = async ({ +export const getTransactionErrorRates = async ({ setup, projection, }: AggregationParams) => { - const { apmEventClient } = setup; + const { apmEventClient, start, end } = setup; + + const outcomes = { + terms: { + field: EVENT_OUTCOME, + }, + }; + const response = await apmEventClient.search( mergeProjection(projection, { apm: { - events: [ProcessorEvent.error], + events: [ProcessorEvent.transaction], }, body: { size: 0, + query: { + bool: { + filter: [ + ...projection.body.query.bool.filter, + { + terms: { + [EVENT_OUTCOME]: [EventOutcome.failure, EventOutcome.success], + }, + }, + ], + }, + }, aggs: { services: { terms: { ...projection.body.aggs.services.terms, size: MAX_NUMBER_OF_SERVICES, }, + aggs: { + outcomes, + timeseries: { + date_histogram: getDateHistogramOpts(start, end), + aggs: { + outcomes, + }, + }, + }, }, }, }, @@ -189,13 +264,36 @@ export const getErrorRates = async ({ return []; } - const deltaAsMinutes = getDeltaAsMinutes(setup); + function calculateTransactionErrorPercentage( + outcomeResponse: AggregationResultOf + ) { + const successfulTransactions = + outcomeResponse.buckets.find( + (bucket) => bucket.key === EventOutcome.success + )?.doc_count ?? 0; + const failedTransactions = + outcomeResponse.buckets.find( + (bucket) => bucket.key === EventOutcome.failure + )?.doc_count ?? 0; - return aggregations.services.buckets.map((bucket) => { - const errorsPerMinute = bucket.doc_count / deltaAsMinutes; + return failedTransactions / (successfulTransactions + failedTransactions); + } + + return aggregations.services.buckets.map((serviceBucket) => { + const transactionErrorRate = calculateTransactionErrorPercentage( + serviceBucket.outcomes + ); return { - serviceName: bucket.key as string, - errorsPerMinute, + serviceName: serviceBucket.key as string, + transactionErrorRate: { + value: transactionErrorRate, + timeseries: serviceBucket.timeseries.buckets.map((dateBucket) => { + return { + x: dateBucket.key, + y: calculateTransactionErrorPercentage(dateBucket.outcomes), + }; + }), + }, }; }); }; @@ -241,8 +339,43 @@ export const getEnvironments = async ({ return []; } - return aggregations.services.buckets.map((bucket) => ({ - serviceName: bucket.key as string, - environments: bucket.environments.buckets.map((env) => env.key as string), + return aggregations.services.buckets.map((serviceBucket) => ({ + serviceName: serviceBucket.key as string, + environments: serviceBucket.environments.buckets.map( + (envBucket) => envBucket.key as string + ), })); }; + +export const getHealthStatuses = async ( + { setup }: AggregationParams, + mlAnomaliesEnvironment?: string +) => { + if (!setup.ml) { + return []; + } + + const jobIds = await getMLJobIds( + setup.ml.anomalyDetectors, + mlAnomaliesEnvironment + ); + if (!jobIds.length) { + return []; + } + + const anomalies = await getServiceAnomalies({ + setup, + environment: mlAnomaliesEnvironment, + }); + + return Object.keys(anomalies.serviceAnomalies).map((serviceName) => { + const stats = anomalies.serviceAnomalies[serviceName]; + + const severity = getSeverity(stats.anomalyScore); + + return { + serviceName, + severity, + }; + }); +}; diff --git a/x-pack/plugins/apm/server/lib/services/get_services/index.ts b/x-pack/plugins/apm/server/lib/services/get_services/index.ts index 5a909ebd6ec54..28b4c64a4af47 100644 --- a/x-pack/plugins/apm/server/lib/services/get_services/index.ts +++ b/x-pack/plugins/apm/server/lib/services/get_services/index.ts @@ -17,11 +17,15 @@ import { getServicesItems } from './get_services_items'; export type ServiceListAPIResponse = PromiseReturnType; -export async function getServices( - setup: Setup & SetupTimeRange & SetupUIFilters -) { +export async function getServices({ + setup, + mlAnomaliesEnvironment, +}: { + setup: Setup & SetupTimeRange & SetupUIFilters; + mlAnomaliesEnvironment?: string; +}) { const [items, hasLegacyData] = await Promise.all([ - getServicesItems(setup), + getServicesItems({ setup, mlAnomaliesEnvironment }), getLegacyDataStatus(setup), ]); 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 99c58a17d396a..9b0dd7a03ca5b 100644 --- a/x-pack/plugins/apm/server/lib/services/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/services/queries.test.ts @@ -38,7 +38,7 @@ describe('services queries', () => { }); it('fetches the service items', async () => { - mock = await inspectSearchParams((setup) => getServicesItems(setup)); + mock = await inspectSearchParams((setup) => getServicesItems({ setup })); const allParams = mock.spy.mock.calls.map((call) => call[0]); diff --git a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts index 2f3b2a602048c..926b2025f4253 100644 --- a/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts +++ b/x-pack/plugins/apm/server/lib/settings/apm_indices/get_apm_indices.ts @@ -5,7 +5,7 @@ */ import { merge } from 'lodash'; -import { Server } from 'hapi'; + import { SavedObjectsClient } from 'src/core/server'; import { PromiseReturnType } from '../../../../../observability/typings/common'; import { @@ -32,10 +32,6 @@ export interface ApmIndicesConfig { export type ApmIndicesName = keyof ApmIndicesConfig; -export type ScopedSavedObjectsClient = ReturnType< - Server['savedObjects']['getScopedSavedObjectsClient'] ->; - async function getApmIndicesSavedObject( savedObjectsClient: ISavedObjectsClient ) { 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 f7b7f72168160..1e08b04416e17 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 @@ -62,7 +62,7 @@ export async function getErrorRate({ total_transactions: { date_histogram: { field: '@timestamp', - fixed_interval: getBucketSize(start, end, 'auto').intervalString, + fixed_interval: getBucketSize(start, end).intervalString, min_doc_count: 0, extended_bounds: { min: start, max: end }, }, diff --git a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts index f68082dfaa1e1..51118278fb824 100644 --- a/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/avg_duration_by_browser/fetcher.ts @@ -24,7 +24,7 @@ export type ESResponse = PromiseReturnType; export function fetcher(options: Options) { const { end, apmEventClient, start, uiFiltersES } = options.setup; const { serviceName, transactionName } = options; - const { intervalString } = getBucketSize(start, end, 'auto'); + const { intervalString } = getBucketSize(start, end); const transactionNameFilter = transactionName ? [{ term: { [TRANSACTION_NAME]: transactionName } }] diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts index 596c3137ec19f..d8865f0049d35 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_anomaly_data/index.ts @@ -64,16 +64,10 @@ export async function getAnomalySeries({ return; } - let mlJobIds: string[] = []; - try { - mlJobIds = await getMLJobIds( - setup.ml.anomalyDetectors, - uiFilters.environment - ); - } catch (error) { - logger.error(error); - return; - } + const mlJobIds = await getMLJobIds( + setup.ml.anomalyDetectors, + uiFilters.environment + ); // don't fetch anomalies if there are isn't exaclty 1 ML job match for the given environment if (mlJobIds.length !== 1) { @@ -87,7 +81,7 @@ export async function getAnomalySeries({ } const { start, end } = setup; - const { intervalString, bucketSize } = getBucketSize(start, end, 'auto'); + const { intervalString, bucketSize } = getBucketSize(start, end); const esResponse = await anomalySeriesFetcher({ serviceName, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts index 1498c22e327d6..f39529b59caa6 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/fetcher.ts @@ -35,7 +35,7 @@ export function timeseriesFetcher({ setup: Setup & SetupTimeRange & SetupUIFilters; }) { const { start, end, uiFiltersES, apmEventClient } = setup; - const { intervalString } = getBucketSize(start, end, 'auto'); + const { intervalString } = getBucketSize(start, end); const filter: ESFilter[] = [ { term: { [SERVICE_NAME]: serviceName } }, diff --git a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts index 8a0fe1a57736f..ea06bd57bfff2 100644 --- a/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts +++ b/x-pack/plugins/apm/server/lib/transactions/charts/get_timeseries_data/index.ts @@ -20,7 +20,7 @@ export async function getApmTimeseriesData(options: { setup: Setup & SetupTimeRange & SetupUIFilters; }) { const { start, end } = options.setup; - const { bucketSize } = getBucketSize(start, end, 'auto'); + const { bucketSize } = getBucketSize(start, end); const durationAsMinutes = (end - start) / 1000 / 60; const timeseriesResponse = await timeseriesFetcher(options); diff --git a/x-pack/plugins/apm/server/plugin.ts b/x-pack/plugins/apm/server/plugin.ts index f7e3977ae7d31..f25e37927f094 100644 --- a/x-pack/plugins/apm/server/plugin.ts +++ b/x-pack/plugins/apm/server/plugin.ts @@ -127,7 +127,7 @@ export class APMPlugin implements Plugin { }; }); - plugins.features.registerFeature(APM_FEATURE); + plugins.features.registerKibanaFeature(APM_FEATURE); plugins.licensing.featureUsage.register( APM_SERVICE_MAPS_FEATURE_NAME, APM_SERVICE_MAPS_LICENSE_TYPE diff --git a/x-pack/plugins/apm/server/routes/service_map.ts b/x-pack/plugins/apm/server/routes/service_map.ts index 971e247d98986..8533d54ed6277 100644 --- a/x-pack/plugins/apm/server/routes/service_map.ts +++ b/x-pack/plugins/apm/server/routes/service_map.ts @@ -8,7 +8,7 @@ import Boom from 'boom'; import * as t from 'io-ts'; import { invalidLicenseMessage, - isValidPlatinumLicense, + isActivePlatinumLicense, } from '../../common/service_map'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceMap } from '../lib/service_map/get_service_map'; @@ -33,7 +33,7 @@ export const serviceMapRoute = createRoute(() => ({ if (!context.config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); } - if (!isValidPlatinumLicense(context.licensing.license)) { + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(invalidLicenseMessage); } context.licensing.featureUsage.notifyUsage(APM_SERVICE_MAPS_FEATURE_NAME); @@ -59,7 +59,7 @@ export const serviceMapServiceNodeRoute = createRoute(() => ({ if (!context.config['xpack.apm.serviceMapEnabled']) { throw Boom.notFound(); } - if (!isValidPlatinumLicense(context.licensing.license)) { + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(invalidLicenseMessage); } const logger = context.logger; diff --git a/x-pack/plugins/apm/server/routes/services.ts b/x-pack/plugins/apm/server/routes/services.ts index 74ab717b8de59..cc7f25867df2c 100644 --- a/x-pack/plugins/apm/server/routes/services.ts +++ b/x-pack/plugins/apm/server/routes/services.ts @@ -16,6 +16,7 @@ import { createRoute } from './create_route'; import { uiFiltersRt, rangeRt } from './default_api_types'; import { getServiceAnnotations } from '../lib/services/annotations'; import { dateAsStringRt } from '../../common/runtime_types/date_as_string_rt'; +import { getParsedUiFilters } from '../lib/helpers/convert_ui_filters/get_parsed_ui_filters'; export const servicesRoute = createRoute(() => ({ path: '/api/apm/services', @@ -23,8 +24,17 @@ export const servicesRoute = createRoute(() => ({ query: t.intersection([uiFiltersRt, rangeRt]), }, handler: async ({ context, request }) => { + const { environment } = getParsedUiFilters({ + uiFilters: context.params.query.uiFilters, + logger: context.logger, + }); + const setup = await setupRequest(context, request); - const services = await getServices(setup); + + const services = await getServices({ + setup, + mlAnomaliesEnvironment: environment, + }); return services; }, diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index ac25f22751f2f..290e81bd29973 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -6,6 +6,7 @@ import * as t from 'io-ts'; import Boom from 'boom'; +import { isActivePlatinumLicense } from '../../../common/service_map'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { createRoute } from '../create_route'; import { getAnomalyDetectionJobs } from '../../lib/anomaly_detection/get_anomaly_detection_jobs'; @@ -24,8 +25,7 @@ export const anomalyDetectionJobsRoute = createRoute(() => ({ handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - const license = context.licensing.license; - if (!license.isActive || !license.hasAtLeast('platinum')) { + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } @@ -56,8 +56,7 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({ const { environments } = context.params.body; const setup = await setupRequest(context, request); - const license = context.licensing.license; - if (!license.isActive || !license.hasAtLeast('platinum')) { + if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index 97013273c9bcf..78c820fbf4ecd 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -13,7 +13,6 @@ import { } from 'src/core/server'; import { PickByValue, Optional } from 'utility-types'; import { Observable } from 'rxjs'; -import { Server } from 'hapi'; import { ObservabilityPluginSetup } from '../../../observability/server'; import { SecurityPluginSetup } from '../../../security/server'; import { MlPluginSetup } from '../../../ml/server'; @@ -57,12 +56,6 @@ export interface Route< }) => Promise; } -export type APMLegacyServer = Pick & { - plugins: { - elasticsearch: Server['plugins']['elasticsearch']; - }; -}; - export type APMRequestHandlerContext< TDecodedParams extends { [key in keyof Params]: any } = {} > = RequestHandlerContext & { diff --git a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts index 7a7592b248960..bbd2c9eb86249 100644 --- a/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts +++ b/x-pack/plugins/apm/typings/elasticsearch/aggregations.ts @@ -346,6 +346,12 @@ export type ValidAggregationKeysOf< T extends Record > = keyof (UnionToIntersection extends never ? T : UnionToIntersection); +export type AggregationResultOf< + TAggregationOptionsMap extends AggregationOptionsMap, + TDocument +> = AggregationResponsePart[AggregationType & + ValidAggregationKeysOf]; + export type AggregationResponseMap< TAggregationInputMap extends AggregationInputMap | undefined, TDocument diff --git a/x-pack/plugins/beats_management/kibana.json b/x-pack/plugins/beats_management/kibana.json index 3fd1ab6fd8701..c1070eedf07a6 100644 --- a/x-pack/plugins/beats_management/kibana.json +++ b/x-pack/plugins/beats_management/kibana.json @@ -7,7 +7,8 @@ "requiredPlugins": [ "data", "licensing", - "management" + "management", + "features" ], "optionalPlugins": [ "security" diff --git a/x-pack/plugins/beats_management/server/plugin.ts b/x-pack/plugins/beats_management/server/plugin.ts index 92c2278148bc1..fde0a2efecdda 100644 --- a/x-pack/plugins/beats_management/server/plugin.ts +++ b/x-pack/plugins/beats_management/server/plugin.ts @@ -11,6 +11,7 @@ import { Plugin, PluginInitializerContext, } from '../../../../src/core/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { SecurityPluginSetup } from '../../security/server'; import { LicensingPluginStart } from '../../licensing/server'; import { BeatsManagementConfigType } from '../common'; @@ -22,6 +23,7 @@ import { beatsIndexTemplate } from './index_templates'; interface SetupDeps { security?: SecurityPluginSetup; + features: FeaturesPluginSetup; } interface StartDeps { @@ -42,7 +44,7 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep private readonly initializerContext: PluginInitializerContext ) {} - public async setup(core: CoreSetup, { security }: SetupDeps) { + public async setup(core: CoreSetup, { features, security }: SetupDeps) { this.securitySetup = security; const router = core.http.createRouter(); @@ -52,6 +54,20 @@ export class BeatsManagementPlugin implements Plugin<{}, {}, SetupDeps, StartDep return this.beatsLibs!; }); + features.registerElasticsearchFeature({ + id: 'beats_management', + management: { + ingest: ['beats_management'], + }, + privileges: [ + { + ui: [], + requiredClusterPrivileges: [], + requiredRoles: ['beats_admin'], + }, + ], + }); + return {}; } diff --git a/x-pack/plugins/canvas/server/plugin.ts b/x-pack/plugins/canvas/server/plugin.ts index c822ed86cb01c..9a41a00883c13 100644 --- a/x-pack/plugins/canvas/server/plugin.ts +++ b/x-pack/plugins/canvas/server/plugin.ts @@ -37,7 +37,7 @@ export class CanvasPlugin implements Plugin { coreSetup.savedObjects.registerType(workpadType); coreSetup.savedObjects.registerType(workpadTemplateType); - plugins.features.registerFeature({ + plugins.features.registerKibanaFeature({ id: 'canvas', name: 'Canvas', order: 400, diff --git a/x-pack/plugins/case/common/constants.ts b/x-pack/plugins/case/common/constants.ts index bd12c258a5388..15a318002390f 100644 --- a/x-pack/plugins/case/common/constants.ts +++ b/x-pack/plugins/case/common/constants.ts @@ -28,5 +28,11 @@ export const CASE_USER_ACTIONS_URL = `${CASE_DETAILS_URL}/user_actions`; export const ACTION_URL = '/api/actions'; export const ACTION_TYPES_URL = '/api/actions/list_action_types'; export const SERVICENOW_ACTION_TYPE_ID = '.servicenow'; +export const JIRA_ACTION_TYPE_ID = '.jira'; +export const RESILIENT_ACTION_TYPE_ID = '.resilient'; -export const SUPPORTED_CONNECTORS = ['.servicenow', '.jira', '.resilient']; +export const SUPPORTED_CONNECTORS = [ + SERVICENOW_ACTION_TYPE_ID, + JIRA_ACTION_TYPE_ID, + RESILIENT_ACTION_TYPE_ID, +]; diff --git a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts index 28e75dd2f8c32..545ccf82c3d78 100644 --- a/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts +++ b/x-pack/plugins/case/server/routes/api/cases/configure/get_connectors.ts @@ -12,6 +12,8 @@ import { CASE_CONFIGURE_CONNECTORS_URL, SUPPORTED_CONNECTORS, SERVICENOW_ACTION_TYPE_ID, + JIRA_ACTION_TYPE_ID, + RESILIENT_ACTION_TYPE_ID, } from '../../../../../common/constants'; /* @@ -36,8 +38,13 @@ export function initCaseConfigureGetActionConnector({ caseService, router }: Rou (action) => SUPPORTED_CONNECTORS.includes(action.actionTypeId) && // Need this filtering temporary to display only Case owned ServiceNow connectors - (action.actionTypeId !== SERVICENOW_ACTION_TYPE_ID || - (action.actionTypeId === SERVICENOW_ACTION_TYPE_ID && action.config!.isCaseOwned)) + (![SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes( + action.actionTypeId + ) || + ([SERVICENOW_ACTION_TYPE_ID, JIRA_ACTION_TYPE_ID, RESILIENT_ACTION_TYPE_ID].includes( + action.actionTypeId + ) && + action.config?.isCaseOwned === true)) ); return response.ok({ body: results }); } catch (error) { diff --git a/x-pack/plugins/cross_cluster_replication/kibana.json b/x-pack/plugins/cross_cluster_replication/kibana.json index 13746bb0e34c3..292820f81adbe 100644 --- a/x-pack/plugins/cross_cluster_replication/kibana.json +++ b/x-pack/plugins/cross_cluster_replication/kibana.json @@ -8,7 +8,8 @@ "licensing", "management", "remoteClusters", - "indexManagement" + "indexManagement", + "features" ], "optionalPlugins": [ "usageCollection" diff --git a/x-pack/plugins/cross_cluster_replication/server/plugin.ts b/x-pack/plugins/cross_cluster_replication/server/plugin.ts index e39b4dfd471a8..d40a53f289873 100644 --- a/x-pack/plugins/cross_cluster_replication/server/plugin.ts +++ b/x-pack/plugins/cross_cluster_replication/server/plugin.ts @@ -87,7 +87,7 @@ export class CrossClusterReplicationServerPlugin implements Plugin { this.ccrEsClient = this.ccrEsClient ?? (await getCustomEsClient(getStartServices)); return { diff --git a/x-pack/plugins/cross_cluster_replication/server/types.ts b/x-pack/plugins/cross_cluster_replication/server/types.ts index c287acf86eb2b..62c96b48c4373 100644 --- a/x-pack/plugins/cross_cluster_replication/server/types.ts +++ b/x-pack/plugins/cross_cluster_replication/server/types.ts @@ -5,6 +5,7 @@ */ import { IRouter } from 'src/core/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { IndexManagementPluginSetup } from '../../index_management/server'; import { RemoteClustersPluginSetup } from '../../remote_clusters/server'; @@ -16,6 +17,7 @@ export interface Dependencies { licensing: LicensingPluginSetup; indexManagement: IndexManagementPluginSetup; remoteClusters: RemoteClustersPluginSetup; + features: FeaturesPluginSetup; } export interface RouteDependencies { diff --git a/x-pack/plugins/dashboard_mode/kibana.json b/x-pack/plugins/dashboard_mode/kibana.json index 4777b9b25be23..81e2073b5c7fd 100644 --- a/x-pack/plugins/dashboard_mode/kibana.json +++ b/x-pack/plugins/dashboard_mode/kibana.json @@ -9,6 +9,7 @@ "optionalPlugins": ["security"], "requiredPlugins": [ "kibanaLegacy", + "urlForwarding", "dashboard" ], "server": true, diff --git a/x-pack/plugins/dashboard_mode/public/plugin.ts b/x-pack/plugins/dashboard_mode/public/plugin.ts index d988de5851cf5..96486bd6da8c8 100644 --- a/x-pack/plugins/dashboard_mode/public/plugin.ts +++ b/x-pack/plugins/dashboard_mode/public/plugin.ts @@ -7,6 +7,7 @@ import { trimStart } from 'lodash'; import { CoreSetup } from 'kibana/public'; import { KibanaLegacyStart } from '../../../../src/plugins/kibana_legacy/public'; +import { UrlForwardingStart } from '../../../../src/plugins/url_forwarding/public'; import { createDashboardEditUrl, DashboardConstants, @@ -22,7 +23,11 @@ function dashboardAppIdPrefix() { return trimStart(createDashboardEditUrl(''), '/'); } -function migratePath(currentHash: string, kibanaLegacy: KibanaLegacyStart) { +function migratePath( + currentHash: string, + kibanaLegacy: KibanaLegacyStart, + urlForwarding: UrlForwardingStart +) { if (currentHash === '' || currentHash === '#' || currentHash === '#/') { return `#${defaultUrl(kibanaLegacy.config.defaultAppId || '')}`; } @@ -30,7 +35,7 @@ function migratePath(currentHash: string, kibanaLegacy: KibanaLegacyStart) { return currentHash; } - const forwards = kibanaLegacy.getForwards(); + const forwards = urlForwarding.getForwards(); if (currentHash.startsWith('#/dashboards')) { const { rewritePath: migrateListingPath } = forwards.find( @@ -46,18 +51,18 @@ function migratePath(currentHash: string, kibanaLegacy: KibanaLegacyStart) { } export const plugin = () => ({ - setup(core: CoreSetup<{ kibanaLegacy: KibanaLegacyStart }>) { + setup(core: CoreSetup<{ kibanaLegacy: KibanaLegacyStart; urlForwarding: UrlForwardingStart }>) { core.application.register({ id: 'dashboard_mode', title: 'Dashboard mode', navLinkStatus: AppNavLinkStatus.hidden, mount: async () => { - const [coreStart, { kibanaLegacy }] = await core.getStartServices(); + const [coreStart, { kibanaLegacy, urlForwarding }] = await core.getStartServices(); kibanaLegacy.dashboardConfig.turnHideWriteControlsOn(); coreStart.chrome.navLinks.showOnly('dashboards'); setTimeout(() => { coreStart.application.navigateToApp('dashboards', { - path: migratePath(window.location.hash, kibanaLegacy), + path: migratePath(window.location.hash, kibanaLegacy, urlForwarding), }); }, 0); return () => {}; diff --git a/x-pack/plugins/data_enhanced/common/index.ts b/x-pack/plugins/data_enhanced/common/index.ts index d6a3c73aaf363..012f1204da46a 100644 --- a/x-pack/plugins/data_enhanced/common/index.ts +++ b/x-pack/plugins/data_enhanced/common/index.ts @@ -5,7 +5,6 @@ */ export { - EnhancedSearchParams, IEnhancedEsSearchRequest, IAsyncSearchRequest, ENHANCED_ES_SEARCH_STRATEGY, diff --git a/x-pack/plugins/data_enhanced/common/search/index.ts b/x-pack/plugins/data_enhanced/common/search/index.ts index 2ae422bd6b7d7..696938a403e89 100644 --- a/x-pack/plugins/data_enhanced/common/search/index.ts +++ b/x-pack/plugins/data_enhanced/common/search/index.ts @@ -5,7 +5,6 @@ */ export { - EnhancedSearchParams, IEnhancedEsSearchRequest, IAsyncSearchRequest, ENHANCED_ES_SEARCH_STRATEGY, diff --git a/x-pack/plugins/data_enhanced/common/search/types.ts b/x-pack/plugins/data_enhanced/common/search/types.ts index 0d3d3a69e1e57..24d459ade4bf9 100644 --- a/x-pack/plugins/data_enhanced/common/search/types.ts +++ b/x-pack/plugins/data_enhanced/common/search/types.ts @@ -4,21 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IEsSearchRequest, ISearchRequestParams } from '../../../../../src/plugins/data/common'; +import { IEsSearchRequest } from '../../../../../src/plugins/data/common'; export const ENHANCED_ES_SEARCH_STRATEGY = 'ese'; -export interface EnhancedSearchParams extends ISearchRequestParams { - ignoreThrottled: boolean; -} - export interface IAsyncSearchRequest extends IEsSearchRequest { /** * The ID received from the response from the initial request */ id?: string; - - params?: EnhancedSearchParams; } export interface IEnhancedEsSearchRequest extends IEsSearchRequest { diff --git a/x-pack/plugins/data_enhanced/kibana.json b/x-pack/plugins/data_enhanced/kibana.json index 637af39339e27..5ded0f8f0dec3 100644 --- a/x-pack/plugins/data_enhanced/kibana.json +++ b/x-pack/plugins/data_enhanced/kibana.json @@ -6,10 +6,11 @@ "xpack", "data_enhanced" ], "requiredPlugins": [ - "data" + "data", + "features" ], - "optionalPlugins": ["kibanaReact", "kibanaUtils", "usageCollection"], + "optionalPlugins": ["kibanaUtils", "usageCollection"], "server": true, "ui": true, - "requiredBundles": ["kibanaReact", "kibanaUtils"] + "requiredBundles": ["kibanaUtils"] } diff --git a/x-pack/plugins/data_enhanced/public/plugin.ts b/x-pack/plugins/data_enhanced/public/plugin.ts index 7f6e3feac0671..ccc93316482c2 100644 --- a/x-pack/plugins/data_enhanced/public/plugin.ts +++ b/x-pack/plugins/data_enhanced/public/plugin.ts @@ -23,6 +23,8 @@ export type DataEnhancedStart = ReturnType; export class DataEnhancedPlugin implements Plugin { + private enhancedSearchInterceptor!: EnhancedSearchInterceptor; + public setup( core: CoreSetup, { data }: DataEnhancedSetupDependencies @@ -32,20 +34,17 @@ export class DataEnhancedPlugin setupKqlQuerySuggestionProvider(core) ); - const enhancedSearchInterceptor = new EnhancedSearchInterceptor( - { - toasts: core.notifications.toasts, - http: core.http, - uiSettings: core.uiSettings, - startServices: core.getStartServices(), - usageCollector: data.search.usageCollector, - }, - core.injectedMetadata.getInjectedVar('esRequestTimeout') as number - ); + this.enhancedSearchInterceptor = new EnhancedSearchInterceptor({ + toasts: core.notifications.toasts, + http: core.http, + uiSettings: core.uiSettings, + startServices: core.getStartServices(), + usageCollector: data.search.usageCollector, + }); data.__enhance({ search: { - searchInterceptor: enhancedSearchInterceptor, + searchInterceptor: this.enhancedSearchInterceptor, }, }); } @@ -53,4 +52,8 @@ export class DataEnhancedPlugin public start(core: CoreStart, plugins: DataEnhancedStartDependencies) { setAutocompleteService(plugins.data.autocomplete); } + + public stop() { + this.enhancedSearchInterceptor.stop(); + } } diff --git a/x-pack/plugins/data_enhanced/public/search/long_query_notification.tsx b/x-pack/plugins/data_enhanced/public/search/long_query_notification.tsx deleted file mode 100644 index 325cf1145fa5f..0000000000000 --- a/x-pack/plugins/data_enhanced/public/search/long_query_notification.tsx +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { EuiButton, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; -import { FormattedMessage } from '@kbn/i18n/react'; -import React from 'react'; -import { toMountPoint } from '../../../../../src/plugins/kibana_react/public'; - -interface Props { - cancel: () => void; - runBeyondTimeout: () => void; -} - -export function getLongQueryNotification(props: Props) { - return toMountPoint( - - ); -} - -export function LongQueryNotification(props: Props) { - return ( -
- - - - - - - - - - - - - -
- ); -} diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts index 1e2c7987b7041..af2fc85602541 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.test.ts @@ -7,7 +7,7 @@ import { coreMock } from '../../../../../src/core/public/mocks'; import { EnhancedSearchInterceptor } from './search_interceptor'; import { CoreSetup, CoreStart } from 'kibana/public'; -import { AbortError } from '../../../../../src/plugins/data/common'; +import { AbortError, UI_SETTINGS } from '../../../../../src/plugins/data/common'; const timeTravel = (msToRun = 0) => { jest.advanceTimersByTime(msToRun); @@ -43,6 +43,15 @@ describe('EnhancedSearchInterceptor', () => { mockCoreSetup = coreMock.createSetup(); mockCoreStart = coreMock.createStart(); + mockCoreSetup.uiSettings.get.mockImplementation((name: string) => { + switch (name) { + case UI_SETTINGS.SEARCH_TIMEOUT: + return 1000; + default: + return; + } + }); + next.mockClear(); error.mockClear(); complete.mockClear(); @@ -51,9 +60,6 @@ describe('EnhancedSearchInterceptor', () => { mockUsageCollector = { trackQueryTimedOut: jest.fn(), trackQueriesCancelled: jest.fn(), - trackLongQueryPopupShown: jest.fn(), - trackLongQueryDialogDismissed: jest.fn(), - trackLongQueryRunBeyondTimeout: jest.fn(), }; const mockPromise = new Promise((resolve) => { @@ -64,16 +70,13 @@ describe('EnhancedSearchInterceptor', () => { ]); }); - searchInterceptor = new EnhancedSearchInterceptor( - { - toasts: mockCoreSetup.notifications.toasts, - startServices: mockPromise as any, - http: mockCoreSetup.http, - uiSettings: mockCoreSetup.uiSettings, - usageCollector: mockUsageCollector, - }, - 1000 - ); + searchInterceptor = new EnhancedSearchInterceptor({ + toasts: mockCoreSetup.notifications.toasts, + startServices: mockPromise as any, + http: mockCoreSetup.http, + uiSettings: mockCoreSetup.uiSettings, + usageCollector: mockUsageCollector, + }); }); describe('search', () => { @@ -384,88 +387,4 @@ describe('EnhancedSearchInterceptor', () => { expect(mockUsageCollector.trackQueriesCancelled).toBeCalledTimes(1); }); }); - - describe('runBeyondTimeout', () => { - const timedResponses = [ - { - time: 250, - value: { - isPartial: true, - isRunning: true, - id: 1, - rawResponse: { - took: 1, - }, - }, - }, - { - time: 2000, - value: { - isPartial: false, - isRunning: false, - id: 1, - rawResponse: { - took: 1, - }, - }, - }, - ]; - - test('times out if runBeyondTimeout is not called', async () => { - mockFetchImplementation(timedResponses); - - const response = searchInterceptor.search({}); - response.subscribe({ next, error }); - - await timeTravel(250); - - expect(next).toHaveBeenCalled(); - expect(next.mock.calls[0][0]).toStrictEqual(timedResponses[0].value); - - await timeTravel(750); - - expect(error).toHaveBeenCalled(); - expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); - }); - - test('times out if runBeyondTimeout is called too late', async () => { - mockFetchImplementation(timedResponses); - - const response = searchInterceptor.search({}); - response.subscribe({ next, error }); - setTimeout(() => searchInterceptor.runBeyondTimeout(), 1100); - - await timeTravel(250); - - expect(next).toHaveBeenCalled(); - expect(next.mock.calls[0][0]).toStrictEqual(timedResponses[0].value); - - await timeTravel(750); - - expect(error).toHaveBeenCalled(); - expect(error.mock.calls[0][0]).toBeInstanceOf(AbortError); - }); - - test('should prevent the request from timing out', async () => { - mockFetchImplementation(timedResponses); - - const response = searchInterceptor.search({}, { pollInterval: 0 }); - response.subscribe({ next, error, complete }); - setTimeout(() => searchInterceptor.runBeyondTimeout(), 500); - - await timeTravel(250); - - expect(next).toHaveBeenCalled(); - expect(next.mock.calls[0][0]).toStrictEqual(timedResponses[0].value); - - await timeTravel(250); // Run beyond timeout - await timeTravel(1750); // Final response - - expect(next).toHaveBeenCalledTimes(2); - expect(next.mock.calls[0][0]).toStrictEqual(timedResponses[0].value); - expect(next.mock.calls[1][0]).toStrictEqual(timedResponses[1].value); - expect(error).not.toHaveBeenCalled(); - expect(mockUsageCollector.trackLongQueryRunBeyondTimeout).toBeCalledTimes(1); - }); - }); }); diff --git a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts index 6f7899d1188b4..f7ae9fc6d0f91 100644 --- a/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts +++ b/x-pack/plugins/data_enhanced/public/search/search_interceptor.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { throwError, EMPTY, timer, from } from 'rxjs'; +import { throwError, EMPTY, timer, from, Subscription } from 'rxjs'; import { mergeMap, expand, takeUntil, finalize, tap } from 'rxjs/operators'; -import { getLongQueryNotification } from './long_query_notification'; +import { debounce } from 'lodash'; +import { i18n } from '@kbn/i18n'; import { SearchInterceptor, SearchInterceptorDeps, @@ -17,64 +18,46 @@ import { IAsyncSearchOptions } from '.'; import { IAsyncSearchRequest, ENHANCED_ES_SEARCH_STRATEGY } from '../../common'; export class EnhancedSearchInterceptor extends SearchInterceptor { + private uiSettingsSub: Subscription; + private searchTimeout: number; + /** - * This class should be instantiated with a `requestTimeout` corresponding with how many ms after - * requests are initiated that they should automatically cancel. - * @param deps `SearchInterceptorDeps` - * @param requestTimeout Usually config value `elasticsearch.requestTimeout` + * @internal */ - constructor(deps: SearchInterceptorDeps, requestTimeout?: number) { - super(deps, requestTimeout); + constructor(deps: SearchInterceptorDeps) { + super(deps); + this.searchTimeout = deps.uiSettings.get(UI_SETTINGS.SEARCH_TIMEOUT); + + this.uiSettingsSub = deps.uiSettings + .get$(UI_SETTINGS.SEARCH_TIMEOUT) + .subscribe((timeout: number) => { + this.searchTimeout = timeout; + }); + } + + public stop() { + this.uiSettingsSub.unsubscribe(); } /** * Abort our `AbortController`, which in turn aborts any intercepted searches. */ public cancelPending = () => { - this.hideToast(); this.abortController.abort(); this.abortController = new AbortController(); if (this.deps.usageCollector) this.deps.usageCollector.trackQueriesCancelled(); }; - /** - * Un-schedule timing out all of the searches intercepted. - */ - public runBeyondTimeout = () => { - this.hideToast(); - this.timeoutSubscriptions.unsubscribe(); - if (this.deps.usageCollector) this.deps.usageCollector.trackLongQueryRunBeyondTimeout(); - }; - - protected showToast = () => { - if (this.longRunningToast) return; - this.longRunningToast = this.deps.toasts.addInfo( - { - title: 'Your query is taking a while', - text: getLongQueryNotification({ - cancel: this.cancelPending, - runBeyondTimeout: this.runBeyondTimeout, - }), - }, - { - toastLifeTimeMs: 1000000, - } - ); - if (this.deps.usageCollector) this.deps.usageCollector.trackLongQueryPopupShown(); - }; - public search( request: IAsyncSearchRequest, { pollInterval = 1000, ...options }: IAsyncSearchOptions = {} ) { let { id } = request; - request.params = { - ignoreThrottled: !this.deps.uiSettings.get(UI_SETTINGS.SEARCH_INCLUDE_FROZEN), - ...request.params, - }; - - const { combinedSignal, cleanup } = this.setupTimers(options); + const { combinedSignal, cleanup } = this.setupAbortSignal({ + abortSignal: options.abortSignal, + timeout: this.searchTimeout, + }); const aborted$ = from(toPromise(combinedSignal)); const strategy = options?.strategy || ENHANCED_ES_SEARCH_STRATEGY; @@ -108,7 +91,7 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { // we don't need to send a follow-up request to delete this search. Otherwise, we // send the follow-up request to delete this search, then throw an abort error. if (id !== undefined) { - this.deps.http.delete(`/internal/search/es/${id}`); + this.deps.http.delete(`/internal/search/${strategy}/${id}`); } }, }), @@ -118,4 +101,28 @@ export class EnhancedSearchInterceptor extends SearchInterceptor { }) ); } + + // Right now we are debouncing but we will hook this up with background sessions to show only one + // error notification per session. + protected showTimeoutError = debounce( + (e: Error) => { + const message = this.application.capabilities.advancedSettings?.save + ? i18n.translate('xpack.data.search.timeoutIncreaseSetting', { + defaultMessage: + 'One or more queries timed out. Increase run time with the search.timeout advanced setting.', + }) + : i18n.translate('xpack.data.search.timeoutContactAdmin', { + defaultMessage: + 'One or more queries timed out. Contact your system administrator to increase the run time.', + }); + this.deps.toasts.addError(e, { + title: 'Timed out', + toastMessage: message, + }); + }, + 60000, + { + leading: true, + } + ); } diff --git a/x-pack/plugins/data_enhanced/server/plugin.ts b/x-pack/plugins/data_enhanced/server/plugin.ts index f9b6fd4e9ad64..a1dff00ddfdd3 100644 --- a/x-pack/plugins/data_enhanced/server/plugin.ts +++ b/x-pack/plugins/data_enhanced/server/plugin.ts @@ -18,6 +18,7 @@ import { } from '../../../../src/plugins/data/server'; import { enhancedEsSearchStrategyProvider } from './search'; import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; +import { getUiSettings } from './ui_settings'; import { ENHANCED_ES_SEARCH_STRATEGY } from '../common'; interface SetupDependencies { @@ -35,6 +36,8 @@ export class EnhancedDataServerPlugin implements Plugin, deps: SetupDependencies) { const usage = deps.usageCollection ? usageProvider(core) : undefined; + core.uiSettings.register(getUiSettings()); + deps.data.search.registerSearchStrategy( ENHANCED_ES_SEARCH_STRATEGY, enhancedEsSearchStrategyProvider( diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts index a287f72ca9161..f4f3d894a4576 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.test.ts @@ -5,8 +5,8 @@ */ import { RequestHandlerContext } from '../../../../../src/core/server'; -import { pluginInitializerContextConfigMock } from '../../../../../src/core/server/mocks'; import { enhancedEsSearchStrategyProvider } from './es_search_strategy'; +import { BehaviorSubject } from 'rxjs'; const mockAsyncResponse = { body: { @@ -42,6 +42,11 @@ describe('ES search strategy', () => { }; const mockContext = { core: { + uiSettings: { + client: { + get: jest.fn(), + }, + }, elasticsearch: { client: { asCurrentUser: { @@ -55,7 +60,15 @@ describe('ES search strategy', () => { }, }, }; - const mockConfig$ = pluginInitializerContextConfigMock({}).legacy.globalConfig$; + const mockConfig$ = new BehaviorSubject({ + elasticsearch: { + shardTimeout: { + asMilliseconds: () => { + return 100; + }, + }, + }, + }); beforeEach(() => { mockApiCaller.mockClear(); diff --git a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts index 4ace1c4c5385b..72ea1f096e8fb 100644 --- a/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts +++ b/x-pack/plugins/data_enhanced/server/search/es_search_strategy.ts @@ -5,23 +5,20 @@ */ import { first } from 'rxjs/operators'; -import { mapKeys, snakeCase } from 'lodash'; -import { Observable } from 'rxjs'; import { SearchResponse } from 'elasticsearch'; +import { Observable } from 'rxjs'; +import { TransportRequestPromise } from '@elastic/elasticsearch/lib/Transport'; +import { SharedGlobalConfig, RequestHandlerContext, Logger } from '../../../../../src/core/server'; import { - SharedGlobalConfig, - RequestHandlerContext, - ElasticsearchClient, - Logger, -} from '../../../../../src/core/server'; -import { - getDefaultSearchParams, getTotalLoaded, ISearchStrategy, SearchUsage, + getDefaultSearchParams, + getShardTimeout, + toSnakeCase, + shimHitsTotal, } from '../../../../../src/plugins/data/server'; import { IEnhancedEsSearchRequest } from '../../common'; -import { shimHitsTotal } from './shim_hits_total'; import { ISearchOptions, IEsSearchResponse } from '../../../../../src/plugins/data/common/search'; function isEnhancedEsSearchResponse(response: any): response is IEsSearchResponse { @@ -39,17 +36,13 @@ export const enhancedEsSearchStrategyProvider = ( options?: ISearchOptions ) => { logger.debug(`search ${JSON.stringify(request.params) || request.id}`); - const config = await config$.pipe(first()).toPromise(); - const client = context.core.elasticsearch.client.asCurrentUser; - const defaultParams = getDefaultSearchParams(config); - const params = { ...defaultParams, ...request.params }; const isAsync = request.indexType !== 'rollup'; try { const response = isAsync - ? await asyncSearch(client, { ...request, params }, options) - : await rollupSearch(client, { ...request, params }, options); + ? await asyncSearch(context, request, options) + : await rollupSearch(context, request, options); if ( usage && @@ -75,72 +68,84 @@ export const enhancedEsSearchStrategyProvider = ( }); }; - return { search, cancel }; -}; - -async function asyncSearch( - client: ElasticsearchClient, - request: IEnhancedEsSearchRequest, - options?: ISearchOptions -): Promise { - let esResponse; + async function asyncSearch( + context: RequestHandlerContext, + request: IEnhancedEsSearchRequest, + options?: ISearchOptions + ): Promise { + let promise: TransportRequestPromise; + const esClient = context.core.elasticsearch.client.asCurrentUser; + const uiSettingsClient = await context.core.uiSettings.client; + + const asyncOptions = { + waitForCompletionTimeout: '100ms', // Wait up to 100ms for the response to return + keepAlive: '1m', // Extend the TTL for this search request by one minute + }; + + // If we have an ID, then just poll for that ID, otherwise send the entire request body + if (!request.id) { + const submitOptions = toSnakeCase({ + batchedReduceSize: 64, // Only report partial results every 64 shards; this should be reduced when we actually display partial results + ...(await getDefaultSearchParams(uiSettingsClient)), + ...asyncOptions, + ...request.params, + }); + + promise = esClient.asyncSearch.submit(submitOptions); + } else { + promise = esClient.asyncSearch.get({ + id: request.id, + ...toSnakeCase(asyncOptions), + }); + } - const asyncOptions = { - waitForCompletionTimeout: '100ms', // Wait up to 100ms for the response to return - keepAlive: '1m', // Extend the TTL for this search request by one minute - }; + // Temporary workaround until https://github.com/elastic/elasticsearch-js/issues/1297 + if (options?.abortSignal) options.abortSignal.addEventListener('abort', () => promise.abort()); + const esResponse = await promise; + const { id, response, is_partial: isPartial, is_running: isRunning } = esResponse.body; + return { + id, + isPartial, + isRunning, + rawResponse: shimHitsTotal(response), + ...getTotalLoaded(response._shards), + }; + } - // If we have an ID, then just poll for that ID, otherwise send the entire request body - if (!request.id) { - const submitOptions = toSnakeCase({ - batchedReduceSize: 64, // Only report partial results every 64 shards; this should be reduced when we actually display partial results - trackTotalHits: true, // Get the exact count of hits - ...asyncOptions, - ...request.params, + const rollupSearch = async function ( + context: RequestHandlerContext, + request: IEnhancedEsSearchRequest, + options?: ISearchOptions + ): Promise { + const esClient = context.core.elasticsearch.client.asCurrentUser; + const uiSettingsClient = await context.core.uiSettings.client; + const config = await config$.pipe(first()).toPromise(); + const { body, index, ...params } = request.params!; + const method = 'POST'; + const path = encodeURI(`/${index}/_rollup_search`); + const querystring = toSnakeCase({ + ...getShardTimeout(config), + ...(await getDefaultSearchParams(uiSettingsClient)), + ...params, }); - esResponse = await client.asyncSearch.submit(submitOptions); - } else { - esResponse = await client.asyncSearch.get({ - id: request.id, - ...toSnakeCase(asyncOptions), + const promise = esClient.transport.request({ + method, + path, + body, + querystring, }); - } - const { id, response, is_partial: isPartial, is_running: isRunning } = esResponse.body; - return { - id, - isPartial, - isRunning, - rawResponse: shimHitsTotal(response), - ...getTotalLoaded(response._shards), - }; -} + // Temporary workaround until https://github.com/elastic/elasticsearch-js/issues/1297 + if (options?.abortSignal) options.abortSignal.addEventListener('abort', () => promise.abort()); + const esResponse = await promise; -async function rollupSearch( - client: ElasticsearchClient, - request: IEnhancedEsSearchRequest, - options?: ISearchOptions -): Promise { - const { body, index, ...params } = request.params!; - const method = 'POST'; - const path = encodeURI(`/${index}/_rollup_search`); - const querystring = toSnakeCase(params); - - const esResponse = await client.transport.request({ - method, - path, - body, - querystring, - }); - - const response = esResponse.body as SearchResponse; - return { - rawResponse: shimHitsTotal(response), - ...getTotalLoaded(response._shards), + const response = esResponse.body as SearchResponse; + return { + rawResponse: response, + ...getTotalLoaded(response._shards), + }; }; -} -function toSnakeCase(obj: Record) { - return mapKeys(obj, (value, key) => snakeCase(key)); -} + return { search, cancel }; +}; diff --git a/x-pack/plugins/data_enhanced/server/search/shim_hits_total.ts b/x-pack/plugins/data_enhanced/server/search/shim_hits_total.ts deleted file mode 100644 index 10d45be01563a..0000000000000 --- a/x-pack/plugins/data_enhanced/server/search/shim_hits_total.ts +++ /dev/null @@ -1,18 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { SearchResponse } from 'elasticsearch'; - -/** - * Temporary workaround until https://github.com/elastic/kibana/issues/26356 is addressed. - * Since we are setting `track_total_hits` in the request, `hits.total` will be an object - * containing the `value`. - */ -export function shimHitsTotal(response: SearchResponse) { - const total = (response.hits?.total as any)?.value ?? response.hits?.total; - const hits = { ...response.hits, total }; - return { ...response, hits }; -} diff --git a/x-pack/plugins/data_enhanced/server/ui_settings.ts b/x-pack/plugins/data_enhanced/server/ui_settings.ts new file mode 100644 index 0000000000000..f2842da8b8337 --- /dev/null +++ b/x-pack/plugins/data_enhanced/server/ui_settings.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { UiSettingsParams } from 'kibana/server'; +import { UI_SETTINGS } from '../../../../src/plugins/data/server'; + +export function getUiSettings(): Record> { + return { + [UI_SETTINGS.SEARCH_TIMEOUT]: { + name: i18n.translate('xpack.data.advancedSettings.searchTimeout', { + defaultMessage: 'Search Timeout', + }), + value: 600000, + description: i18n.translate('xpack.data.advancedSettings.searchTimeoutDesc', { + defaultMessage: + 'Change the maximum timeout for a search session or set to 0 to disable the timeout and allow queries to run to completion.', + }), + type: 'number', + category: ['search'], + schema: schema.number(), + }, + }; +} diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts index 6a11663ea6c3d..4906d0342be84 100644 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.test.ts @@ -54,6 +54,7 @@ describe('UrlDrilldown', () => { getGlobalScope: () => ({ kibanaUrl: 'http://localhost:5601/' }), getOpenModal: () => Promise.resolve(coreMock.createStart().overlays.openModal), getSyntaxHelpDocsLink: () => 'http://localhost:5601/docs', + getVariablesHelpDocsLink: () => 'http://localhost:5601/docs', navigateToUrl: mockNavigateToUrl, }); diff --git a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx index d5ab095fdd287..80478e6490b8f 100644 --- a/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx +++ b/x-pack/plugins/embeddable_enhanced/public/drilldowns/url_drilldown/url_drilldown.tsx @@ -31,6 +31,7 @@ interface UrlDrilldownDeps { navigateToUrl: (url: string) => Promise; getOpenModal: () => Promise; getSyntaxHelpDocsLink: () => string; + getVariablesHelpDocsLink: () => string; } export type ActionContext = ChartActionContext; @@ -74,6 +75,7 @@ export class UrlDrilldown implements Drilldown ); }; diff --git a/x-pack/plugins/embeddable_enhanced/public/plugin.ts b/x-pack/plugins/embeddable_enhanced/public/plugin.ts index 37e102b40131d..187db998e06ea 100644 --- a/x-pack/plugins/embeddable_enhanced/public/plugin.ts +++ b/x-pack/plugins/embeddable_enhanced/public/plugin.ts @@ -75,7 +75,10 @@ export class EmbeddableEnhancedPlugin navigateToUrl: (url: string) => core.getStartServices().then(([{ application }]) => application.navigateToUrl(url)), getOpenModal: () => core.getStartServices().then(([{ overlays }]) => overlays.openModal), - getSyntaxHelpDocsLink: () => startServices().core.docLinks.links.dashboard.drilldowns, // TODO: replace with docs https://github.com/elastic/kibana/issues/69414 + getSyntaxHelpDocsLink: () => + startServices().core.docLinks.links.dashboard.urlDrilldownTemplateSyntax, + getVariablesHelpDocsLink: () => + startServices().core.docLinks.links.dashboard.urlDrilldownVariables, }) ); diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts index f8d66b8ecac27..18834f55af0a5 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.test.ts @@ -555,7 +555,18 @@ describe('#bulkUpdate', () => { }); describe('namespace', () => { - const doTest = async (namespace: string, expectNamespaceInDescriptor: boolean) => { + interface TestParams { + optionsNamespace: string | undefined; + objectNamespace: string | undefined; + expectOptionsNamespaceInDescriptor: boolean; + expectObjectNamespaceInDescriptor: boolean; + } + const doTest = async ({ + optionsNamespace, + objectNamespace, + expectOptionsNamespaceInDescriptor, + expectObjectNamespaceInDescriptor, + }: TestParams) => { const docs = [ { id: 'some-id', @@ -566,12 +577,13 @@ describe('#bulkUpdate', () => { attrThree: 'three', }, version: 'some-version', + namespace: objectNamespace, }, ]; - const options = { namespace }; + const options = { namespace: optionsNamespace }; mockBaseClient.bulkUpdate.mockResolvedValue({ - saved_objects: docs.map((doc) => ({ ...doc, references: undefined })), + saved_objects: docs.map(({ namespace, ...doc }) => ({ ...doc, references: undefined })), }); await expect(wrapper.bulkUpdate(docs, options)).resolves.toEqual({ @@ -594,7 +606,11 @@ describe('#bulkUpdate', () => { { type: 'known-type', id: 'some-id', - namespace: expectNamespaceInDescriptor ? namespace : undefined, + namespace: expectObjectNamespaceInDescriptor + ? objectNamespace + : expectOptionsNamespaceInDescriptor + ? optionsNamespace + : undefined, }, { attrOne: 'one', attrSecret: 'secret', attrThree: 'three' }, { user: mockAuthenticatedUser() } @@ -612,7 +628,7 @@ describe('#bulkUpdate', () => { attrThree: 'three', }, version: 'some-version', - + namespace: objectNamespace, references: undefined, }, ], @@ -620,13 +636,46 @@ describe('#bulkUpdate', () => { ); }; - it('uses `namespace` to encrypt attributes if it is specified when type is single-namespace', async () => { - await doTest('some-namespace', true); + it('does not use options `namespace` or object `namespace` to encrypt attributes if neither are specified', async () => { + await doTest({ + optionsNamespace: undefined, + objectNamespace: undefined, + expectOptionsNamespaceInDescriptor: false, + expectObjectNamespaceInDescriptor: false, + }); }); - it('does not use `namespace` to encrypt attributes if it is specified when type is not single-namespace', async () => { - mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false); - await doTest('some-namespace', false); + describe('with a single-namespace type', () => { + it('uses options `namespace` to encrypt attributes if it is specified and object `namespace` is not', async () => { + await doTest({ + optionsNamespace: 'some-namespace', + objectNamespace: undefined, + expectOptionsNamespaceInDescriptor: true, + expectObjectNamespaceInDescriptor: false, + }); + }); + + it('uses object `namespace` to encrypt attributes if it is specified', async () => { + // object namespace supersedes options namespace + await doTest({ + optionsNamespace: 'some-namespace', + objectNamespace: 'another-namespace', + expectOptionsNamespaceInDescriptor: false, + expectObjectNamespaceInDescriptor: true, + }); + }); + }); + + describe('with a non-single-namespace type', () => { + it('does not use object `namespace` or options `namespace` to encrypt attributes if it is specified', async () => { + mockBaseTypeRegistry.isSingleNamespace.mockReturnValue(false); + await doTest({ + optionsNamespace: 'some-namespace', + objectNamespace: 'another-namespace', + expectOptionsNamespaceInDescriptor: false, + expectObjectNamespaceInDescriptor: false, + }); + }); }); }); diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts index a2725cbc6a274..0eeb9943b5be9 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/encrypted_saved_objects_client_wrapper.ts @@ -150,14 +150,14 @@ export class EncryptedSavedObjectsClientWrapper implements SavedObjectsClientCon // sequential processing. const encryptedObjects = await Promise.all( objects.map(async (object) => { - const { type, id, attributes } = object; + const { type, id, attributes, namespace: objectNamespace } = object; if (!this.options.service.isRegistered(type)) { return object; } const namespace = getDescriptorNamespace( this.options.baseTypeRegistry, type, - options?.namespace + objectNamespace ?? options?.namespace ); return { ...object, diff --git a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/get_descriptor_namespace.ts b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/get_descriptor_namespace.ts index b2842df909a1d..7201f13fb930b 100644 --- a/x-pack/plugins/encrypted_saved_objects/server/saved_objects/get_descriptor_namespace.ts +++ b/x-pack/plugins/encrypted_saved_objects/server/saved_objects/get_descriptor_namespace.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ISavedObjectTypeRegistry } from 'kibana/server'; +import { ISavedObjectTypeRegistry, SavedObjectsUtils } from '../../../../../src/core/server'; export const getDescriptorNamespace = ( typeRegistry: ISavedObjectTypeRegistry, @@ -12,5 +12,12 @@ export const getDescriptorNamespace = ( namespace?: string ) => { const descriptorNamespace = typeRegistry.isSingleNamespace(type) ? namespace : undefined; - return descriptorNamespace === 'default' ? undefined : descriptorNamespace; + return normalizeNamespace(descriptorNamespace); }; + +/** + * Ensure that a namespace is always in its namespace ID representation. + * This allows `'default'` to be used interchangeably with `undefined`. + */ +const normalizeNamespace = (namespace?: string) => + namespace === undefined ? namespace : SavedObjectsUtils.namespaceStringToId(namespace); 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 2d31be65dd30e..4533383ebd80e 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,9 +7,20 @@ export const DEFAULT_INITIAL_APP_DATA = { readOnlyMode: false, ilmEnabled: true, + isFederatedAuth: false, configuredLimits: { - maxDocumentByteSize: 102400, - maxEnginesPerMetaEngine: 15, + appSearch: { + engine: { + maxDocumentByteSize: 102400, + maxEnginesPerMetaEngine: 15, + }, + }, + workplaceSearch: { + customApiSource: { + maxDocumentByteSize: 102400, + totalFields: 64, + }, + }, }, appSearch: { accountId: 'some-id-string', @@ -29,17 +40,16 @@ export const DEFAULT_INITIAL_APP_DATA = { }, }, workplaceSearch: { - canCreateInvitations: true, - isFederatedAuth: false, organization: { name: 'ACME Donuts', defaultOrgName: 'My Organization', }, - fpAccount: { + account: { id: 'some-id-string', groups: ['Default', 'Cats'], isAdmin: true, canCreatePersonalSources: true, + canCreateInvitations: true, isCurated: false, viewedOnboardingPage: true, }, diff --git a/x-pack/plugins/enterprise_search/common/constants.ts b/x-pack/plugins/enterprise_search/common/constants.ts index 05d27d7337a6e..c6ca0d532ce07 100644 --- a/x-pack/plugins/enterprise_search/common/constants.ts +++ b/x-pack/plugins/enterprise_search/common/constants.ts @@ -11,7 +11,24 @@ export const ENTERPRISE_SEARCH_PLUGIN = { NAME: i18n.translate('xpack.enterpriseSearch.productName', { defaultMessage: 'Enterprise Search', }), - URL: '/app/enterprise_search', + NAV_TITLE: i18n.translate('xpack.enterpriseSearch.navTitle', { + defaultMessage: 'Overview', + }), + SUBTITLE: i18n.translate('xpack.enterpriseSearch.featureCatalogue.subtitle', { + defaultMessage: 'Search everything', + }), + DESCRIPTIONS: [ + i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription1', { + defaultMessage: 'Build a powerful search experience.', + }), + i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription2', { + defaultMessage: 'Connect your users to relevant data.', + }), + i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription3', { + defaultMessage: 'Unify your team content.', + }), + ], + URL: '/app/enterprise_search/overview', }; export const APP_SEARCH_PLUGIN = { @@ -23,6 +40,10 @@ export const APP_SEARCH_PLUGIN = { defaultMessage: 'Leverage dashboards, analytics, and APIs for advanced application search made simple.', }), + CARD_DESCRIPTION: i18n.translate('xpack.enterpriseSearch.appSearch.productCardDescription', { + defaultMessage: + 'Elastic App Search provides user-friendly tools to design and deploy a powerful search to your websites or web/mobile applications.', + }), URL: '/app/enterprise_search/app_search', SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/app-search/', }; @@ -36,12 +57,22 @@ export const WORKPLACE_SEARCH_PLUGIN = { defaultMessage: 'Search all documents, files, and sources available across your virtual workplace.', }), + CARD_DESCRIPTION: i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.productCardDescription', + { + defaultMessage: + "Unify all your team's content in one place, with instant connectivity to popular productivity and collaboration tools.", + } + ), URL: '/app/enterprise_search/workplace_search', SUPPORT_URL: 'https://discuss.elastic.co/c/enterprise-search/workplace-search/', }; export const LICENSED_SUPPORT_URL = 'https://support.elastic.co'; -export const JSON_HEADER = { 'Content-Type': 'application/json' }; // This needs specific casing or Chrome throws a 415 error +export const JSON_HEADER = { + 'Content-Type': 'application/json', // This needs specific casing or Chrome throws a 415 error + Accept: 'application/json', // Required for Enterprise Search APIs +}; export const ENGINES_PAGE_SIZE = 10; diff --git a/x-pack/plugins/enterprise_search/common/types/app_search.ts b/x-pack/plugins/enterprise_search/common/types/app_search.ts index 5d6ec079e66e0..72259ecd2343d 100644 --- a/x-pack/plugins/enterprise_search/common/types/app_search.ts +++ b/x-pack/plugins/enterprise_search/common/types/app_search.ts @@ -23,3 +23,10 @@ export interface IRole { availableRoleTypes: string[]; }; } + +export interface IConfiguredLimits { + engine: { + maxDocumentByteSize: number; + maxEnginesPerMetaEngine: number; + }; +} diff --git a/x-pack/plugins/enterprise_search/common/types/index.ts b/x-pack/plugins/enterprise_search/common/types/index.ts index 008afb234a376..d5774adc0d516 100644 --- a/x-pack/plugins/enterprise_search/common/types/index.ts +++ b/x-pack/plugins/enterprise_search/common/types/index.ts @@ -4,18 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IAccount as IAppSearchAccount } from './app_search'; -import { IWorkplaceSearchInitialData } from './workplace_search'; +import { + IAccount as IAppSearchAccount, + IConfiguredLimits as IAppSearchConfiguredLimits, +} from './app_search'; +import { + IWorkplaceSearchInitialData, + IConfiguredLimits as IWorkplaceSearchConfiguredLimits, +} from './workplace_search'; export interface IInitialAppData { readOnlyMode?: boolean; ilmEnabled?: boolean; + isFederatedAuth?: boolean; configuredLimits?: IConfiguredLimits; + access?: { + hasAppSearchAccess: boolean; + hasWorkplaceSearchAccess: boolean; + }; appSearch?: IAppSearchAccount; workplaceSearch?: IWorkplaceSearchInitialData; } export interface IConfiguredLimits { - maxDocumentByteSize: number; - maxEnginesPerMetaEngine: number; + appSearch: IAppSearchConfiguredLimits; + workplaceSearch: IWorkplaceSearchConfiguredLimits; } diff --git a/x-pack/plugins/enterprise_search/common/types/workplace_search.ts b/x-pack/plugins/enterprise_search/common/types/workplace_search.ts index bc4e39b0788d9..6c82206706b32 100644 --- a/x-pack/plugins/enterprise_search/common/types/workplace_search.ts +++ b/x-pack/plugins/enterprise_search/common/types/workplace_search.ts @@ -10,6 +10,7 @@ export interface IAccount { isAdmin: boolean; isCurated: boolean; canCreatePersonalSources: boolean; + canCreateInvitations?: boolean; viewedOnboardingPage: boolean; } @@ -19,8 +20,13 @@ export interface IOrganization { } export interface IWorkplaceSearchInitialData { - canCreateInvitations: boolean; - isFederatedAuth: boolean; organization: IOrganization; - fpAccount: IAccount; + account: IAccount; +} + +export interface IConfiguredLimits { + customApiSource: { + maxDocumentByteSize: number; + totalFields: number; + }; } diff --git a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts index 779eb1a043e8c..842dcefd3aef8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/__mocks__/react_router_history.mock.ts @@ -9,7 +9,7 @@ * Jest to accept its use within a jest.mock() */ export const mockHistory = { - createHref: jest.fn(({ pathname }) => `/enterprise_search${pathname}`), + createHref: jest.fn(({ pathname }) => `/app/enterprise_search${pathname}`), push: jest.fn(), location: { pathname: '/current-path', diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/app_search.png b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/app_search.png new file mode 100644 index 0000000000000..6cf0639167e2f Binary files /dev/null and b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/app_search.png differ diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/bg_enterprise_search.png b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/bg_enterprise_search.png new file mode 100644 index 0000000000000..1b5e1e489fd96 Binary files /dev/null and b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/bg_enterprise_search.png differ diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/workplace_search.png b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/workplace_search.png new file mode 100644 index 0000000000000..984662b65cb5d Binary files /dev/null and b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/assets/workplace_search.png differ diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/validators.ts b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/index.ts similarity index 81% rename from x-pack/plugins/security_solution/public/common/lib/connectors/validators.ts rename to x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/index.ts index 2989cf4d98f85..df85a10f7e9de 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/validators.ts +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { isUrlInvalid } from '../../utils/validators'; +export { ProductCard } from './product_card'; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.scss b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.scss new file mode 100644 index 0000000000000..d6b6bd3442590 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.scss @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +.productCard { + margin: $euiSizeS; + + &__imageContainer { + max-height: 115px; + overflow: hidden; + background-color: #0076cc; + + @include euiBreakpoint('s', 'm', 'l', 'xl') { + max-height: none; + } + } + + &__image { + width: 100%; + height: auto; + } + + .euiCard__content { + max-width: 350px; + margin-top: $euiSizeL; + + @include euiBreakpoint('s', 'm', 'l', 'xl') { + margin-top: $euiSizeXL; + } + } + + .euiCard__title { + margin-bottom: $euiSizeM; + font-weight: $euiFontWeightBold; + + @include euiBreakpoint('s', 'm', 'l', 'xl') { + margin-bottom: $euiSizeL; + font-size: $euiSizeL; + } + } + + .euiCard__description { + font-weight: $euiFontWeightMedium; + color: $euiColorMediumShade; + margin-bottom: $euiSize; + } + + .euiCard__footer { + margin-bottom: $euiSizeS; + + @include euiBreakpoint('s', 'm', 'l', 'xl') { + margin-bottom: $euiSizeM; + font-size: $euiSizeL; + } + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx new file mode 100644 index 0000000000000..a76b654ccddd0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.test.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiCard } from '@elastic/eui'; +import { EuiButton } from '../../../shared/react_router_helpers'; +import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../../../common/constants'; + +jest.mock('../../../shared/telemetry', () => ({ + sendTelemetry: jest.fn(), +})); +import { sendTelemetry } from '../../../shared/telemetry'; + +import { ProductCard } from './'; + +describe('ProductCard', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders an App Search card', () => { + const wrapper = shallow(); + const card = wrapper.find(EuiCard).dive().shallow(); + + expect(card.find('h2').text()).toEqual('Elastic App Search'); + expect(card.find('.productCard__image').prop('src')).toEqual('as.jpg'); + + const button = card.find(EuiButton); + expect(button.prop('to')).toEqual('/app/enterprise_search/app_search'); + expect(button.prop('data-test-subj')).toEqual('LaunchAppSearchButton'); + + button.simulate('click'); + expect(sendTelemetry).toHaveBeenCalledWith(expect.objectContaining({ metric: 'app_search' })); + }); + + it('renders a Workplace Search card', () => { + const wrapper = shallow(); + const card = wrapper.find(EuiCard).dive().shallow(); + + expect(card.find('h2').text()).toEqual('Elastic Workplace Search'); + expect(card.find('.productCard__image').prop('src')).toEqual('ws.jpg'); + + const button = card.find(EuiButton); + expect(button.prop('to')).toEqual('/app/enterprise_search/workplace_search'); + expect(button.prop('data-test-subj')).toEqual('LaunchWorkplaceSearchButton'); + + button.simulate('click'); + expect(sendTelemetry).toHaveBeenCalledWith( + expect.objectContaining({ metric: 'workplace_search' }) + ); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx new file mode 100644 index 0000000000000..334ca126cabb9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/components/product_card/product_card.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { useContext } from 'react'; +import upperFirst from 'lodash/upperFirst'; +import snakeCase from 'lodash/snakeCase'; +import { i18n } from '@kbn/i18n'; +import { EuiCard, EuiTextColor } from '@elastic/eui'; + +import { EuiButton } from '../../../shared/react_router_helpers'; +import { sendTelemetry } from '../../../shared/telemetry'; +import { KibanaContext, IKibanaContext } from '../../../index'; + +import './product_card.scss'; + +interface IProductCard { + // Expects product plugin constants (@see common/constants.ts) + product: { + ID: string; + NAME: string; + CARD_DESCRIPTION: string; + URL: string; + }; + image: string; +} + +export const ProductCard: React.FC = ({ product, image }) => { + const { http } = useContext(KibanaContext) as IKibanaContext; + + return ( + + +
+ } + paddingSize="l" + description={{product.CARD_DESCRIPTION}} + footer={ + + sendTelemetry({ + http, + product: 'enterprise_search', + action: 'clicked', + metric: snakeCase(product.ID), + }) + } + data-test-subj={`Launch${upperFirst(product.ID)}Button`} + > + {i18n.translate('xpack.enterpriseSearch.overview.productCard.button', { + defaultMessage: `Launch {productName}`, + values: { productName: product.NAME }, + })} + + } + /> + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.scss b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.scss new file mode 100644 index 0000000000000..d937943352317 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.scss @@ -0,0 +1,54 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +.enterpriseSearchOverview { + padding-top: 78px; + background-image: url('./assets/bg_enterprise_search.png'); + background-repeat: no-repeat; + background-size: 670px; + background-position: center -27px; + + @include euiBreakpoint('m', 'l', 'xl') { + padding-top: 158px; + background-size: 1160px; + background-position: center -48px; + } + + &__header { + text-align: center; + margin: auto; + } + + &__heading { + @include euiBreakpoint('xs', 's') { + font-size: $euiFontSizeXL; + line-height: map-get(map-get($euiTitles, 'm'), 'line-height'); + } + } + + &__subheading { + color: $euiColorMediumShade; + font-size: $euiFontSize; + + @include euiBreakpoint('m', 'l', 'xl') { + font-size: $euiFontSizeL; + margin-bottom: $euiSizeL; + } + } + + // EUI override + .euiTitle + .euiTitle { + margin-top: 0; + + @include euiBreakpoint('m', 'l', 'xl') { + margin-top: $euiSizeS; + } + } + + .enterpriseSearchOverview__card { + flex-basis: 50%; + } +} diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx new file mode 100644 index 0000000000000..cd2a22a45bbb4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { shallow } from 'enzyme'; + +import { EuiPage } from '@elastic/eui'; + +import { EnterpriseSearch } from './'; +import { ProductCard } from './components/product_card'; + +describe('EnterpriseSearch', () => { + it('renders the overview page and product cards', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(EuiPage).hasClass('enterpriseSearchOverview')).toBe(true); + expect(wrapper.find(ProductCard)).toHaveLength(2); + }); + + describe('access checks', () => { + it('does not render the App Search card if the user does not have access to AS', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(ProductCard)).toHaveLength(1); + expect(wrapper.find(ProductCard).prop('product').ID).toEqual('workplaceSearch'); + }); + + it('does not render the Workplace Search card if the user does not have access to WS', () => { + const wrapper = shallow( + + ); + + expect(wrapper.find(ProductCard)).toHaveLength(1); + expect(wrapper.find(ProductCard).prop('product').ID).toEqual('appSearch'); + }); + + it('does not render any cards if the user does not have access', () => { + const wrapper = shallow(); + + expect(wrapper.find(ProductCard)).toHaveLength(0); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx new file mode 100644 index 0000000000000..373f595a6a9ea --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/enterprise_search/index.tsx @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { + EuiPage, + EuiPageBody, + EuiPageHeader, + EuiPageHeaderSection, + EuiPageContentBody, + EuiFlexGroup, + EuiFlexItem, + EuiSpacer, + EuiTitle, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { IInitialAppData } from '../../../common/types'; +import { APP_SEARCH_PLUGIN, WORKPLACE_SEARCH_PLUGIN } from '../../../common/constants'; + +import { SetEnterpriseSearchChrome as SetPageChrome } from '../shared/kibana_chrome'; +import { SendEnterpriseSearchTelemetry as SendTelemetry } from '../shared/telemetry'; + +import { ProductCard } from './components/product_card'; + +import AppSearchImage from './assets/app_search.png'; +import WorkplaceSearchImage from './assets/workplace_search.png'; +import './index.scss'; + +export const EnterpriseSearch: React.FC = ({ access = {} }) => { + const { hasAppSearchAccess, hasWorkplaceSearchAccess } = access; + + return ( + + + + + + + + +

+ {i18n.translate('xpack.enterpriseSearch.overview.heading', { + defaultMessage: 'Welcome to Elastic Enterprise Search', + })} +

+
+ +

+ {i18n.translate('xpack.enterpriseSearch.overview.subheading', { + defaultMessage: 'Select a product to get started', + })} +

+
+
+
+ + + {hasAppSearchAccess && ( + + + + )} + {hasWorkplaceSearchAccess && ( + + + + )} + + + +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts index 9e86b239432a7..3c8b3a7218862 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.test.ts @@ -37,27 +37,37 @@ describe('useBreadcrumbs', () => { expect(breadcrumb).toEqual([ { text: 'Hello', - href: '/enterprise_search/hello', + href: '/app/enterprise_search/hello', onClick: expect.any(Function), }, { text: 'World', - href: '/enterprise_search/world', + href: '/app/enterprise_search/world', onClick: expect.any(Function), }, ]); }); it('prevents default navigation and uses React Router history on click', () => { - const breadcrumb = useBreadcrumbs([{ text: '', path: '/' }])[0] as any; + const breadcrumb = useBreadcrumbs([{ text: '', path: '/test' }])[0] as any; const event = { preventDefault: jest.fn() }; breadcrumb.onClick(event); - expect(mockKibanaContext.navigateToUrl).toHaveBeenCalled(); + expect(mockKibanaContext.navigateToUrl).toHaveBeenCalledWith('/app/enterprise_search/test'); expect(mockHistory.createHref).toHaveBeenCalled(); expect(event.preventDefault).toHaveBeenCalled(); }); + it('does not call createHref if shouldNotCreateHref is passed', () => { + const breadcrumb = useBreadcrumbs([ + { text: '', path: '/test', shouldNotCreateHref: true }, + ])[0] as any; + breadcrumb.onClick({ preventDefault: () => null }); + + expect(mockKibanaContext.navigateToUrl).toHaveBeenCalledWith('/test'); + expect(mockHistory.createHref).not.toHaveBeenCalled(); + }); + it('does not prevent default browser behavior on new tab/window clicks', () => { const breadcrumb = useBreadcrumbs([{ text: '', path: '/' }])[0] as any; @@ -95,15 +105,17 @@ describe('useEnterpriseSearchBreadcrumbs', () => { expect(useEnterpriseSearchBreadcrumbs(breadcrumbs)).toEqual([ { text: 'Enterprise Search', + href: '/app/enterprise_search/overview', + onClick: expect.any(Function), }, { text: 'Page 1', - href: '/enterprise_search/page1', + href: '/app/enterprise_search/page1', onClick: expect.any(Function), }, { text: 'Page 2', - href: '/enterprise_search/page2', + href: '/app/enterprise_search/page2', onClick: expect.any(Function), }, ]); @@ -113,6 +125,8 @@ describe('useEnterpriseSearchBreadcrumbs', () => { expect(useEnterpriseSearchBreadcrumbs()).toEqual([ { text: 'Enterprise Search', + href: '/app/enterprise_search/overview', + onClick: expect.any(Function), }, ]); }); @@ -122,7 +136,7 @@ describe('useAppSearchBreadcrumbs', () => { beforeEach(() => { jest.clearAllMocks(); mockHistory.createHref.mockImplementation( - ({ pathname }: any) => `/enterprise_search/app_search${pathname}` + ({ pathname }: any) => `/app/enterprise_search/app_search${pathname}` ); }); @@ -141,20 +155,22 @@ describe('useAppSearchBreadcrumbs', () => { expect(useAppSearchBreadcrumbs(breadcrumbs)).toEqual([ { text: 'Enterprise Search', + href: '/app/enterprise_search/overview', + onClick: expect.any(Function), }, { text: 'App Search', - href: '/enterprise_search/app_search/', + href: '/app/enterprise_search/app_search/', onClick: expect.any(Function), }, { text: 'Page 1', - href: '/enterprise_search/app_search/page1', + href: '/app/enterprise_search/app_search/page1', onClick: expect.any(Function), }, { text: 'Page 2', - href: '/enterprise_search/app_search/page2', + href: '/app/enterprise_search/app_search/page2', onClick: expect.any(Function), }, ]); @@ -164,10 +180,12 @@ describe('useAppSearchBreadcrumbs', () => { expect(useAppSearchBreadcrumbs()).toEqual([ { text: 'Enterprise Search', + href: '/app/enterprise_search/overview', + onClick: expect.any(Function), }, { text: 'App Search', - href: '/enterprise_search/app_search/', + href: '/app/enterprise_search/app_search/', onClick: expect.any(Function), }, ]); @@ -178,7 +196,7 @@ describe('useWorkplaceSearchBreadcrumbs', () => { beforeEach(() => { jest.clearAllMocks(); mockHistory.createHref.mockImplementation( - ({ pathname }: any) => `/enterprise_search/workplace_search${pathname}` + ({ pathname }: any) => `/app/enterprise_search/workplace_search${pathname}` ); }); @@ -197,20 +215,22 @@ describe('useWorkplaceSearchBreadcrumbs', () => { expect(useWorkplaceSearchBreadcrumbs(breadcrumbs)).toEqual([ { text: 'Enterprise Search', + href: '/app/enterprise_search/overview', + onClick: expect.any(Function), }, { text: 'Workplace Search', - href: '/enterprise_search/workplace_search/', + href: '/app/enterprise_search/workplace_search/', onClick: expect.any(Function), }, { text: 'Page 1', - href: '/enterprise_search/workplace_search/page1', + href: '/app/enterprise_search/workplace_search/page1', onClick: expect.any(Function), }, { text: 'Page 2', - href: '/enterprise_search/workplace_search/page2', + href: '/app/enterprise_search/workplace_search/page2', onClick: expect.any(Function), }, ]); @@ -220,10 +240,12 @@ describe('useWorkplaceSearchBreadcrumbs', () => { expect(useWorkplaceSearchBreadcrumbs()).toEqual([ { text: 'Enterprise Search', + href: '/app/enterprise_search/overview', + onClick: expect.any(Function), }, { text: 'Workplace Search', - href: '/enterprise_search/workplace_search/', + href: '/app/enterprise_search/workplace_search/', onClick: expect.any(Function), }, ]); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts index 6eab936719d01..19714608e73e9 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_breadcrumbs.ts @@ -26,6 +26,9 @@ import { letBrowserHandleEvent } from '../react_router_helpers'; interface IBreadcrumb { text: string; path?: string; + // Used to navigate outside of the React Router basename, + // i.e. if we need to go from App Search to Enterprise Search + shouldNotCreateHref?: boolean; } export type TBreadcrumbs = IBreadcrumb[]; @@ -33,11 +36,11 @@ export const useBreadcrumbs = (breadcrumbs: TBreadcrumbs) => { const history = useHistory(); const { navigateToUrl } = useContext(KibanaContext) as IKibanaContext; - return breadcrumbs.map(({ text, path }) => { + return breadcrumbs.map(({ text, path, shouldNotCreateHref }) => { const breadcrumb = { text } as EuiBreadcrumb; if (path) { - const href = history.createHref({ pathname: path }) as string; + const href = shouldNotCreateHref ? path : (history.createHref({ pathname: path }) as string); breadcrumb.href = href; breadcrumb.onClick = (event) => { @@ -56,7 +59,14 @@ export const useBreadcrumbs = (breadcrumbs: TBreadcrumbs) => { */ export const useEnterpriseSearchBreadcrumbs = (breadcrumbs: TBreadcrumbs = []) => - useBreadcrumbs([{ text: ENTERPRISE_SEARCH_PLUGIN.NAME }, ...breadcrumbs]); + useBreadcrumbs([ + { + text: ENTERPRISE_SEARCH_PLUGIN.NAME, + path: ENTERPRISE_SEARCH_PLUGIN.URL, + shouldNotCreateHref: true, + }, + ...breadcrumbs, + ]); export const useAppSearchBreadcrumbs = (breadcrumbs: TBreadcrumbs = []) => useEnterpriseSearchBreadcrumbs([{ text: APP_SEARCH_PLUGIN.NAME, path: '/' }, ...breadcrumbs]); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts index 706baefc00cc2..de5f72de79192 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/generate_title.ts @@ -20,7 +20,7 @@ export type TTitle = string[]; /** * Given an array of page titles, return a final formatted document title * @param pages - e.g., ['Curations', 'some Engine', 'App Search'] - * @returns - e.g., 'Curations | some Engine | App Search' + * @returns - e.g., 'Curations - some Engine - App Search' */ export const generateTitle = (pages: TTitle) => pages.join(' - '); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/index.ts index 4468d11ba94c9..02013a03c3395 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/index.ts @@ -4,4 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -export { SetAppSearchChrome, SetWorkplaceSearchChrome } from './set_chrome'; +export { + SetEnterpriseSearchChrome, + SetAppSearchChrome, + SetWorkplaceSearchChrome, +} from './set_chrome'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx index bda816c9a5554..61a066bb92216 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.test.tsx @@ -12,18 +12,24 @@ import React from 'react'; import { mockKibanaContext, mountWithKibanaContext } from '../../__mocks__'; jest.mock('./generate_breadcrumbs', () => ({ + useEnterpriseSearchBreadcrumbs: jest.fn(() => (crumbs: any) => crumbs), useAppSearchBreadcrumbs: jest.fn(() => (crumbs: any) => crumbs), useWorkplaceSearchBreadcrumbs: jest.fn(() => (crumbs: any) => crumbs), })); -import { useAppSearchBreadcrumbs, useWorkplaceSearchBreadcrumbs } from './generate_breadcrumbs'; +import { + useEnterpriseSearchBreadcrumbs, + useAppSearchBreadcrumbs, + useWorkplaceSearchBreadcrumbs, +} from './generate_breadcrumbs'; jest.mock('./generate_title', () => ({ + enterpriseSearchTitle: jest.fn((title: any) => title), appSearchTitle: jest.fn((title: any) => title), workplaceSearchTitle: jest.fn((title: any) => title), })); -import { appSearchTitle, workplaceSearchTitle } from './generate_title'; +import { enterpriseSearchTitle, appSearchTitle, workplaceSearchTitle } from './generate_title'; -import { SetAppSearchChrome, SetWorkplaceSearchChrome } from './'; +import { SetEnterpriseSearchChrome, SetAppSearchChrome, SetWorkplaceSearchChrome } from './'; describe('Set Kibana Chrome helpers', () => { beforeEach(() => { @@ -35,6 +41,27 @@ describe('Set Kibana Chrome helpers', () => { expect(mockKibanaContext.setDocTitle).toHaveBeenCalled(); }); + describe('SetEnterpriseSearchChrome', () => { + it('sets breadcrumbs and document title', () => { + mountWithKibanaContext(); + + expect(enterpriseSearchTitle).toHaveBeenCalledWith(['Hello World']); + expect(useEnterpriseSearchBreadcrumbs).toHaveBeenCalledWith([ + { + text: 'Hello World', + path: '/current-path', + }, + ]); + }); + + it('sets empty breadcrumbs and document title when isRoot is true', () => { + mountWithKibanaContext(); + + expect(enterpriseSearchTitle).toHaveBeenCalledWith([]); + expect(useEnterpriseSearchBreadcrumbs).toHaveBeenCalledWith([]); + }); + }); + describe('SetAppSearchChrome', () => { it('sets breadcrumbs and document title', () => { mountWithKibanaContext(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx index 43db93c1583d1..5e8d972e1a135 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/kibana_chrome/set_chrome.tsx @@ -10,11 +10,17 @@ import { EuiBreadcrumb } from '@elastic/eui'; import { KibanaContext, IKibanaContext } from '../../index'; import { + useEnterpriseSearchBreadcrumbs, useAppSearchBreadcrumbs, useWorkplaceSearchBreadcrumbs, TBreadcrumbs, } from './generate_breadcrumbs'; -import { appSearchTitle, workplaceSearchTitle, TTitle } from './generate_title'; +import { + enterpriseSearchTitle, + appSearchTitle, + workplaceSearchTitle, + TTitle, +} from './generate_title'; /** * Helpers for setting Kibana chrome (breadcrumbs, doc titles) on React view mount @@ -33,6 +39,24 @@ interface IRootBreadcrumbsProps { } type TBreadcrumbsProps = IBreadcrumbsProps | IRootBreadcrumbsProps; +export const SetEnterpriseSearchChrome: React.FC = ({ text, isRoot }) => { + const history = useHistory(); + const { setBreadcrumbs, setDocTitle } = useContext(KibanaContext) as IKibanaContext; + + const title = isRoot ? [] : [text]; + const docTitle = enterpriseSearchTitle(title as TTitle | []); + + const crumb = isRoot ? [] : [{ text, path: history.location.pathname }]; + const breadcrumbs = useEnterpriseSearchBreadcrumbs(crumb as TBreadcrumbs | []); + + useEffect(() => { + setBreadcrumbs(breadcrumbs); + setDocTitle(docTitle); + }, []); + + return null; +}; + export const SetAppSearchChrome: React.FC = ({ text, isRoot }) => { const history = useHistory(); const { setBreadcrumbs, setDocTitle } = useContext(KibanaContext) as IKibanaContext; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx index 063118f94cd19..0c7bac99085dd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.test.tsx @@ -45,10 +45,18 @@ describe('EUI & React Router Component Helpers', () => { const link = wrapper.find(EuiLink); expect(link.prop('onClick')).toBeInstanceOf(Function); - expect(link.prop('href')).toEqual('/enterprise_search/foo/bar'); + expect(link.prop('href')).toEqual('/app/enterprise_search/foo/bar'); expect(mockHistory.createHref).toHaveBeenCalled(); }); + it('renders with the correct non-basenamed href when shouldNotCreateHref is passed', () => { + const wrapper = mount(); + const link = wrapper.find(EuiLink); + + expect(link.prop('href')).toEqual('/foo/bar'); + expect(mockHistory.createHref).not.toHaveBeenCalled(); + }); + describe('onClick', () => { it('prevents default navigation and uses React Router history', () => { const wrapper = mount(); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx index 7221a61d0997b..e3b46632ddf9e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/react_router_helpers/eui_link.tsx @@ -21,14 +21,22 @@ import { letBrowserHandleEvent } from './link_events'; interface IEuiReactRouterProps { to: string; onClick?(): void; + // Used to navigate outside of the React Router plugin basename but still within Kibana, + // e.g. if we need to go from Enterprise Search to App Search + shouldNotCreateHref?: boolean; } -export const EuiReactRouterHelper: React.FC = ({ to, onClick, children }) => { +export const EuiReactRouterHelper: React.FC = ({ + to, + onClick, + shouldNotCreateHref, + children, +}) => { const history = useHistory(); const { navigateToUrl } = useContext(KibanaContext) as IKibanaContext; // Generate the correct link href (with basename etc. accounted for) - const href = history.createHref({ pathname: to }); + const href = shouldNotCreateHref ? to : history.createHref({ pathname: to }); const reactRouterLinkClick = (event: React.MouseEvent) => { if (onClick) onClick(); // Run any passed click events (e.g. telemetry) @@ -51,9 +59,10 @@ type TEuiReactRouterButtonProps = EuiButtonProps & IEuiReactRouterProps; export const EuiReactRouterLink: React.FC = ({ to, onClick, + shouldNotCreateHref, ...rest }) => ( - + ); @@ -61,9 +70,10 @@ export const EuiReactRouterLink: React.FC = ({ export const EuiReactRouterButton: React.FC = ({ to, onClick, + shouldNotCreateHref, ...rest }) => ( - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts index eadf7fa805590..a8b9636c3ff3e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/index.ts @@ -5,5 +5,8 @@ */ export { sendTelemetry } from './send_telemetry'; -export { SendAppSearchTelemetry } from './send_telemetry'; -export { SendWorkplaceSearchTelemetry } from './send_telemetry'; +export { + SendEnterpriseSearchTelemetry, + SendAppSearchTelemetry, + SendWorkplaceSearchTelemetry, +} from './send_telemetry'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx index 3c873dbc25e37..8f7cf090e2d57 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.test.tsx @@ -10,7 +10,12 @@ import { httpServiceMock } from 'src/core/public/mocks'; import { JSON_HEADER as headers } from '../../../../common/constants'; import { mountWithKibanaContext } from '../../__mocks__'; -import { sendTelemetry, SendAppSearchTelemetry, SendWorkplaceSearchTelemetry } from './'; +import { + sendTelemetry, + SendEnterpriseSearchTelemetry, + SendAppSearchTelemetry, + SendWorkplaceSearchTelemetry, +} from './'; describe('Shared Telemetry Helpers', () => { const httpMock = httpServiceMock.createSetupContract(); @@ -44,6 +49,17 @@ describe('Shared Telemetry Helpers', () => { }); describe('React component helpers', () => { + it('SendEnterpriseSearchTelemetry component', () => { + mountWithKibanaContext(, { + http: httpMock, + }); + + expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', { + headers, + body: '{"product":"enterprise_search","action":"viewed","metric":"page"}', + }); + }); + it('SendAppSearchTelemetry component', () => { mountWithKibanaContext(, { http: httpMock, @@ -56,13 +72,13 @@ describe('Shared Telemetry Helpers', () => { }); it('SendWorkplaceSearchTelemetry component', () => { - mountWithKibanaContext(, { + mountWithKibanaContext(, { http: httpMock, }); expect(httpMock.put).toHaveBeenCalledWith('/api/enterprise_search/telemetry', { headers, - body: '{"product":"workplace_search","action":"viewed","metric":"page"}', + body: '{"product":"workplace_search","action":"error","metric":"not_found"}', }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx index 715d61b31512c..4df1428221de6 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/shared/telemetry/send_telemetry.tsx @@ -35,9 +35,21 @@ export const sendTelemetry = async ({ http, product, action, metric }: ISendTele /** * React component helpers - useful for on-page-load/views - * TODO: SendEnterpriseSearchTelemetry */ +export const SendEnterpriseSearchTelemetry: React.FC = ({ + action, + metric, +}) => { + const { http } = useContext(KibanaContext) as IKibanaContext; + + useEffect(() => { + sendTelemetry({ http, action, metric, product: 'enterprise_search' }); + }, [action, metric, http]); + + return null; +}; + export const SendAppSearchTelemetry: React.FC = ({ action, metric }) => { const { http } = useContext(KibanaContext) as IKibanaContext; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts index bc31b7df5d971..c52eceb2d2fdd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.test.ts @@ -16,7 +16,28 @@ describe('AppLogic', () => { }); const DEFAULT_VALUES = { + account: {}, hasInitialized: false, + isFederatedAuth: true, + organization: {}, + }; + + const expectedLogicValues = { + account: { + canCreateInvitations: true, + canCreatePersonalSources: true, + groups: ['Default', 'Cats'], + id: 'some-id-string', + isAdmin: true, + isCurated: false, + viewedOnboardingPage: true, + }, + hasInitialized: true, + isFederatedAuth: false, + organization: { + defaultOrgName: 'My Organization', + name: 'ACME Donuts', + }, }; it('has expected default values', () => { @@ -27,9 +48,7 @@ describe('AppLogic', () => { it('sets values based on passed props', () => { AppLogic.actions.initializeAppData(DEFAULT_INITIAL_APP_DATA); - expect(AppLogic.values).toEqual({ - hasInitialized: true, - }); + expect(AppLogic.values).toEqual(expectedLogicValues); }); }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts index 5bf2b41cfc264..f88a00f63f487 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/app_logic.ts @@ -7,18 +7,26 @@ import { kea, MakeLogicType } from 'kea'; import { IInitialAppData } from '../../../common/types'; -import { IWorkplaceSearchInitialData } from '../../../common/types/workplace_search'; +import { + IOrganization, + IWorkplaceSearchInitialData, + IAccount, +} from '../../../common/types/workplace_search'; export interface IAppValues extends IWorkplaceSearchInitialData { hasInitialized: boolean; + isFederatedAuth: boolean; } export interface IAppActions { - initializeAppData(props: IInitialAppData): void; + initializeAppData(props: IInitialAppData): IInitialAppData; } export const AppLogic = kea>({ actions: { - initializeAppData: ({ workplaceSearch }) => workplaceSearch, + initializeAppData: ({ workplaceSearch, isFederatedAuth }) => ({ + workplaceSearch, + isFederatedAuth, + }), }, reducers: { hasInitialized: [ @@ -27,5 +35,23 @@ export const AppLogic = kea>({ initializeAppData: () => true, }, ], + isFederatedAuth: [ + true, + { + initializeAppData: (_, { isFederatedAuth }) => !!isFederatedAuth, + }, + ], + organization: [ + {} as IOrganization, + { + initializeAppData: (_, { workplaceSearch }) => workplaceSearch!.organization, + }, + ], + account: [ + {} as IAccount, + { + initializeAppData: (_, { workplaceSearch }) => workplaceSearch!.account, + }, + ], }, }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts index 9e86993a5289d..9f281a541334e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { setMockValues, mockValues, mockActions } from './overview_logic.mock'; +export { setMockValues, mockOverviewValues, mockActions } from './overview_logic.mock'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts index 9ce3021917a21..569e6543ee869 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/__mocks__/overview_logic.mock.ts @@ -5,19 +5,18 @@ */ import { IOverviewValues } from '../overview_logic'; -import { IAccount, IOrganization } from '../../../types'; -export const mockValues = { +import { DEFAULT_INITIAL_APP_DATA } from '../../../../../../common/__mocks__'; + +const { workplaceSearch: mockAppValues } = DEFAULT_INITIAL_APP_DATA; + +export const mockOverviewValues = { accountsCount: 0, activityFeed: [], canCreateContentSources: false, - canCreateInvitations: false, - fpAccount: {} as IAccount, hasOrgSources: false, hasUsers: false, - isFederatedAuth: true, isOldAccount: false, - organization: {} as IOrganization, pendingInvitationsCount: 0, personalSourcesCount: 0, sourcesCount: 0, @@ -28,6 +27,8 @@ export const mockActions = { initializeOverview: jest.fn(() => ({})), }; +const mockValues = { ...mockOverviewValues, ...mockAppValues, isFederatedAuth: true }; + jest.mock('kea', () => ({ ...(jest.requireActual('kea') as object), useActions: jest.fn(() => ({ ...mockActions })), @@ -37,8 +38,5 @@ jest.mock('kea', () => ({ import { useValues } from 'kea'; export const setMockValues = (values: object) => { - (useValues as jest.Mock).mockImplementationOnce(() => ({ - ...mockValues, - ...values, - })); + (useValues as jest.Mock).mockImplementation(() => ({ ...mockValues, ...values })); }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx index acbc66259c2a1..0f3eee074caef 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.test.tsx @@ -25,6 +25,7 @@ const account = { canCreatePersonalSources: true, groups: [], isCurated: false, + canCreateInvitations: true, }; describe('OnboardingSteps', () => { @@ -60,9 +61,8 @@ describe('OnboardingSteps', () => { describe('Users & Invitations', () => { it('renders 0 users when not on federated auth', () => { setMockValues({ - canCreateInvitations: true, isFederatedAuth: false, - fpAccount: account, + account, accountsCount: 0, hasUsers: false, }); @@ -78,7 +78,7 @@ describe('OnboardingSteps', () => { it('renders completed users state', () => { setMockValues({ isFederatedAuth: false, - fpAccount: account, + account, accountsCount: 1, hasUsers: true, }); @@ -90,7 +90,13 @@ describe('OnboardingSteps', () => { }); it('disables link when the user cannot create invitations', () => { - setMockValues({ isFederatedAuth: false, canCreateInvitations: false }); + setMockValues({ + isFederatedAuth: false, + account: { + ...account, + canCreateInvitations: false, + }, + }); const wrapper = shallow(); expect(wrapper.find(OnboardingCard).last().prop('actionPath')).toBe(undefined); }); @@ -98,6 +104,12 @@ describe('OnboardingSteps', () => { describe('Org Name', () => { it('renders button to change name', () => { + setMockValues({ + organization: { + name: 'foo', + defaultOrgName: 'foo', + }, + }); const wrapper = shallow(); const button = wrapper diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx index 5598123f1c286..0baadfc912ad5 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/onboarding_steps.tsx @@ -28,6 +28,7 @@ import { ORG_SOURCES_PATH, USERS_PATH, ORG_SETTINGS_PATH } from '../../routes'; import { ContentSection } from '../../components/shared/content_section'; +import { AppLogic } from '../../app_logic'; import { OverviewLogic } from './overview_logic'; import { OnboardingCard } from './onboarding_card'; @@ -58,16 +59,18 @@ const ONBOARDING_USERS_CARD_DESCRIPTION = i18n.translate( ); export const OnboardingSteps: React.FC = () => { + const { + isFederatedAuth, + organization: { name, defaultOrgName }, + account: { isCurated, canCreateInvitations }, + } = useValues(AppLogic); + const { hasUsers, hasOrgSources, canCreateContentSources, - canCreateInvitations, accountsCount, sourcesCount, - fpAccount: { isCurated }, - organization: { name, defaultOrgName }, - isFederatedAuth, } = useValues(OverviewLogic); const accountsPath = diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx index 4dc762e29deba..6614ac58b0744 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/organization_stats.tsx @@ -14,18 +14,17 @@ import { i18n } from '@kbn/i18n'; import { ContentSection } from '../../components/shared/content_section'; import { ORG_SOURCES_PATH, USERS_PATH } from '../../routes'; +import { AppLogic } from '../../app_logic'; import { OverviewLogic } from './overview_logic'; import { StatisticCard } from './statistic_card'; export const OrganizationStats: React.FC = () => { - const { - sourcesCount, - pendingInvitationsCount, - accountsCount, - personalSourcesCount, - isFederatedAuth, - } = useValues(OverviewLogic); + const { isFederatedAuth } = useValues(AppLogic); + + const { sourcesCount, pendingInvitationsCount, accountsCount, personalSourcesCount } = useValues( + OverviewLogic + ); return ( { - const { initializeOverview } = useActions(OverviewLogic); - const { - dataLoading, - hasUsers, - hasOrgSources, - isOldAccount, organization: { name: orgName, defaultOrgName }, - } = useValues(OverviewLogic); + } = useValues(AppLogic); + + const { initializeOverview } = useActions(OverviewLogic); + const { dataLoading, hasUsers, hasOrgSources, isOldAccount } = useValues(OverviewLogic); useEffect(() => { initializeOverview(); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts index 6989635064ca9..1ec770e9defce 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.test.ts @@ -9,7 +9,7 @@ import { resetContext } from 'kea'; jest.mock('../../../shared/http', () => ({ HttpLogic: { values: { http: { get: jest.fn() } } } })); import { HttpLogic } from '../../../shared/http'; -import { mockValues } from './__mocks__'; +import { mockOverviewValues } from './__mocks__'; import { OverviewLogic } from './overview_logic'; describe('OverviewLogic', () => { @@ -20,32 +20,19 @@ describe('OverviewLogic', () => { }); it('has expected default values', () => { - expect(OverviewLogic.values).toEqual(mockValues); + expect(OverviewLogic.values).toEqual(mockOverviewValues); }); describe('setServerData', () => { const feed = [{ foo: 'bar' }] as any; - const account = { - id: '1243', - groups: ['Default'], - isAdmin: true, - isCurated: false, - canCreatePersonalSources: true, - viewedOnboardingPage: false, - }; - const org = { name: 'ACME', defaultOrgName: 'Org' }; const data = { accountsCount: 1, activityFeed: feed, canCreateContentSources: true, - canCreateInvitations: true, - fpAccount: account, hasOrgSources: true, hasUsers: true, - isFederatedAuth: false, isOldAccount: true, - organization: org, pendingInvitationsCount: 1, personalSourcesCount: 1, sourcesCount: 1, @@ -60,10 +47,6 @@ describe('OverviewLogic', () => { }); it('will set server values', () => { - expect(OverviewLogic.values.organization).toEqual(org); - expect(OverviewLogic.values.isFederatedAuth).toEqual(false); - expect(OverviewLogic.values.fpAccount).toEqual(account); - expect(OverviewLogic.values.canCreateInvitations).toEqual(true); expect(OverviewLogic.values.hasUsers).toEqual(true); expect(OverviewLogic.values.hasOrgSources).toEqual(true); expect(OverviewLogic.values.canCreateContentSources).toEqual(true); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts index 2c6846b6db7db..787d5295db1cf 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/overview_logic.ts @@ -7,24 +7,18 @@ import { kea, MakeLogicType } from 'kea'; import { HttpLogic } from '../../../shared/http'; -import { IAccount, IOrganization } from '../../types'; - import { IFeedActivity } from './recent_activity'; export interface IOverviewServerData { hasUsers: boolean; hasOrgSources: boolean; canCreateContentSources: boolean; - canCreateInvitations: boolean; isOldAccount: boolean; sourcesCount: number; pendingInvitationsCount: number; accountsCount: number; personalSourcesCount: number; activityFeed: IFeedActivity[]; - organization: IOrganization; - isFederatedAuth: boolean; - fpAccount: IAccount; } export interface IOverviewActions { @@ -42,30 +36,6 @@ export const OverviewLogic = kea null, }, reducers: { - organization: [ - {} as IOrganization, - { - setServerData: (_, { organization }) => organization, - }, - ], - isFederatedAuth: [ - true, - { - setServerData: (_, { isFederatedAuth }) => isFederatedAuth, - }, - ], - fpAccount: [ - {} as IAccount, - { - setServerData: (_, { fpAccount }) => fpAccount, - }, - ], - canCreateInvitations: [ - false, - { - setServerData: (_, { canCreateInvitations }) => canCreateInvitations, - }, - ], hasUsers: [ false, { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx index 22a82af18527d..31613098f9fcc 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.test.tsx @@ -12,6 +12,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { EuiEmptyPrompt, EuiLink } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; import { RecentActivity, RecentActivityItem } from './recent_activity'; @@ -61,4 +62,19 @@ describe('RecentActivity', () => { expect(wrapper.find('.activity--error__label')).toHaveLength(1); expect(wrapper.find(EuiLink).prop('color')).toEqual('danger'); }); + + it('renders recent activity message for default org name', () => { + setMockValues({ + organization: { + name: 'foo', + defaultOrgName: 'foo', + }, + }); + const wrapper = shallow(); + const emptyPrompt = wrapper.find(EuiEmptyPrompt).dive(); + + expect(emptyPrompt.find(FormattedMessage).prop('defaultMessage')).toEqual( + 'Your organization has no recent activity' + ); + }); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx index 441f45a947a49..0813999c9a078 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/overview/recent_activity.tsx @@ -17,6 +17,7 @@ import { sendTelemetry } from '../../../shared/telemetry'; import { KibanaContext, IKibanaContext } from '../../../index'; import { SOURCE_DETAILS_PATH, getContentSourcePath } from '../../routes'; +import { AppLogic } from '../../app_logic'; import { OverviewLogic } from './overview_logic'; import './recent_activity.scss'; @@ -32,8 +33,9 @@ export interface IFeedActivity { export const RecentActivity: React.FC = () => { const { organization: { name, defaultOrgName }, - activityFeed, - } = useValues(OverviewLogic); + } = useValues(AppLogic); + + const { activityFeed } = useValues(OverviewLogic); return ( { + const [coreStart] = await core.getStartServices(); + const { chrome } = coreStart; + chrome.docTitle.change(ENTERPRISE_SEARCH_PLUGIN.NAME); + + await this.getInitialData(coreStart.http); + + const { renderApp } = await import('./applications'); + const { EnterpriseSearch } = await import('./applications/enterprise_search'); + + return renderApp(EnterpriseSearch, params, coreStart, plugins, this.config, this.data); + }, + }); + core.application.register({ id: APP_SEARCH_PLUGIN.ID, title: APP_SEARCH_PLUGIN.NAME, @@ -94,22 +112,10 @@ export class EnterpriseSearchPlugin implements Plugin { plugins.home.featureCatalogue.registerSolution({ id: ENTERPRISE_SEARCH_PLUGIN.ID, title: ENTERPRISE_SEARCH_PLUGIN.NAME, - subtitle: i18n.translate('xpack.enterpriseSearch.featureCatalogue.subtitle', { - defaultMessage: 'Search everything', - }), + subtitle: ENTERPRISE_SEARCH_PLUGIN.SUBTITLE, icon: 'logoEnterpriseSearch', - descriptions: [ - i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription1', { - defaultMessage: 'Build a powerful search experience.', - }), - i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription2', { - defaultMessage: 'Connect your users to relevant data.', - }), - i18n.translate('xpack.enterpriseSearch.featureCatalogueDescription3', { - defaultMessage: 'Unify your team content.', - }), - ], - path: APP_SEARCH_PLUGIN.URL, // TODO: Change this to enterprise search overview page once available + descriptions: ENTERPRISE_SEARCH_PLUGIN.DESCRIPTIONS, + path: ENTERPRISE_SEARCH_PLUGIN.URL, }); plugins.home.featureCatalogue.register({ diff --git a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.test.ts new file mode 100644 index 0000000000000..c3e2aff6551c9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { mockLogger } from '../../__mocks__'; + +import { registerTelemetryUsageCollector } from './telemetry'; + +describe('Enterprise Search Telemetry Usage Collector', () => { + const makeUsageCollectorStub = jest.fn(); + const registerStub = jest.fn(); + const usageCollectionMock = { + makeUsageCollector: makeUsageCollectorStub, + registerCollector: registerStub, + } as any; + + const savedObjectsRepoStub = { + get: () => ({ + attributes: { + 'ui_viewed.overview': 10, + 'ui_clicked.app_search': 2, + 'ui_clicked.workplace_search': 3, + }, + }), + incrementCounter: jest.fn(), + }; + const savedObjectsMock = { + createInternalRepository: jest.fn(() => savedObjectsRepoStub), + } as any; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('registerTelemetryUsageCollector', () => { + it('should make and register the usage collector', () => { + registerTelemetryUsageCollector(usageCollectionMock, savedObjectsMock, mockLogger); + + expect(registerStub).toHaveBeenCalledTimes(1); + expect(makeUsageCollectorStub).toHaveBeenCalledTimes(1); + expect(makeUsageCollectorStub.mock.calls[0][0].type).toBe('enterprise_search'); + expect(makeUsageCollectorStub.mock.calls[0][0].isReady()).toBe(true); + }); + }); + + describe('fetchTelemetryMetrics', () => { + it('should return existing saved objects data', async () => { + registerTelemetryUsageCollector(usageCollectionMock, savedObjectsMock, mockLogger); + const savedObjectsCounts = await makeUsageCollectorStub.mock.calls[0][0].fetch(); + + expect(savedObjectsCounts).toEqual({ + ui_viewed: { + overview: 10, + }, + ui_clicked: { + app_search: 2, + workplace_search: 3, + }, + }); + }); + + it('should return a default telemetry object if no saved data exists', async () => { + const emptySavedObjectsMock = { + createInternalRepository: () => ({ + get: () => ({ attributes: null }), + }), + } as any; + + registerTelemetryUsageCollector(usageCollectionMock, emptySavedObjectsMock, mockLogger); + const savedObjectsCounts = await makeUsageCollectorStub.mock.calls[0][0].fetch(); + + expect(savedObjectsCounts).toEqual({ + ui_viewed: { + overview: 0, + }, + ui_clicked: { + app_search: 0, + workplace_search: 0, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts new file mode 100644 index 0000000000000..a124a185b9a34 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/collectors/enterprise_search/telemetry.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get } from 'lodash'; +import { SavedObjectsServiceStart, Logger } from 'src/core/server'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; + +import { getSavedObjectAttributesFromRepo } from '../lib/telemetry'; + +interface ITelemetry { + ui_viewed: { + overview: number; + }; + ui_clicked: { + app_search: number; + workplace_search: number; + }; +} + +export const ES_TELEMETRY_NAME = 'enterprise_search_telemetry'; + +/** + * Register the telemetry collector + */ + +export const registerTelemetryUsageCollector = ( + usageCollection: UsageCollectionSetup, + savedObjects: SavedObjectsServiceStart, + log: Logger +) => { + const telemetryUsageCollector = usageCollection.makeUsageCollector({ + type: 'enterprise_search', + fetch: async () => fetchTelemetryMetrics(savedObjects, log), + isReady: () => true, + schema: { + ui_viewed: { + overview: { type: 'long' }, + }, + ui_clicked: { + app_search: { type: 'long' }, + workplace_search: { type: 'long' }, + }, + }, + }); + usageCollection.registerCollector(telemetryUsageCollector); +}; + +/** + * Fetch the aggregated telemetry metrics from our saved objects + */ + +const fetchTelemetryMetrics = async (savedObjects: SavedObjectsServiceStart, log: Logger) => { + const savedObjectsRepository = savedObjects.createInternalRepository(); + const savedObjectAttributes = await getSavedObjectAttributesFromRepo( + ES_TELEMETRY_NAME, + savedObjectsRepository, + log + ); + + const defaultTelemetrySavedObject: ITelemetry = { + ui_viewed: { + overview: 0, + }, + ui_clicked: { + app_search: 0, + workplace_search: 0, + }, + }; + + // If we don't have an existing/saved telemetry object, return the default + if (!savedObjectAttributes) { + return defaultTelemetrySavedObject; + } + + return { + ui_viewed: { + overview: get(savedObjectAttributes, 'ui_viewed.overview', 0), + }, + ui_clicked: { + app_search: get(savedObjectAttributes, 'ui_clicked.app_search', 0), + workplace_search: get(savedObjectAttributes, 'ui_clicked.workplace_search', 0), + }, + } as ITelemetry; +}; diff --git a/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts b/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts index aae162c23ccb4..6cf0be9fd1f31 100644 --- a/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts +++ b/x-pack/plugins/enterprise_search/server/collectors/lib/telemetry.test.ts @@ -15,7 +15,7 @@ import { SavedObjectsErrorHelpers } from '../../../../../../src/core/server'; import { getSavedObjectAttributesFromRepo, incrementUICounter } from './telemetry'; -describe('App Search Telemetry Usage Collector', () => { +describe('Telemetry helpers', () => { beforeEach(() => { jest.clearAllMocks(); }); diff --git a/x-pack/plugins/enterprise_search/server/lib/check_access.ts b/x-pack/plugins/enterprise_search/server/lib/check_access.ts index 0239cb6422d03..497747f953289 100644 --- a/x-pack/plugins/enterprise_search/server/lib/check_access.ts +++ b/x-pack/plugins/enterprise_search/server/lib/check_access.ts @@ -51,7 +51,7 @@ export const checkAccess = async ({ try { const { hasAllRequested } = await security.authz .checkPrivilegesWithRequest(request) - .globally(security.authz.actions.ui.get('enterpriseSearch', 'all')); + .globally({ kibana: security.authz.actions.ui.get('enterpriseSearch', 'all') }); return hasAllRequested; } catch (err) { if (err.statusCode === 401 || err.statusCode === 403) { 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 323f79e63bc6f..8e3ae2cfbeb86 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 @@ -38,51 +38,63 @@ describe('callEnterpriseSearchConfigAPI', () => { external_url: 'http://some.vanity.url/', read_only_mode: false, ilm_enabled: true, + is_federated_auth: false, configured_limits: { - max_document_byte_size: 102400, - max_engines_per_meta_engine: 15, + app_search: { + engine: { + document_size_in_bytes: 102400, + source_engines_per_meta_engine: 15, + }, + }, + workplace_search: { + custom_api_source: { + document_size_in_bytes: 102400, + total_fields: 64, + }, + }, + }, + }, + current_user: { + name: 'someuser', + access: { + app_search: true, + workplace_search: false, }, app_search: { - account_id: 'some-id-string', - onboarding_complete: true, + account: { + id: 'some-id-string', + onboarding_complete: true, + }, + role: { + id: 'account_id:somestring|user_oid:somestring', + role_type: 'owner', + ability: { + access_all_engines: true, + destroy: ['session'], + manage: ['account_credentials', 'account_engines'], // etc + edit: ['LocoMoco::Account'], // etc + view: ['Engine'], // etc + credential_types: ['admin', 'private', 'search'], + available_role_types: ['owner', 'admin'], + }, + }, }, workplace_search: { - can_create_invitations: true, - is_federated_auth: false, organization: { name: 'ACME Donuts', default_org_name: 'My Organization', }, - fp_account: { + account: { id: 'some-id-string', groups: ['Default', 'Cats'], is_admin: true, can_create_personal_sources: true, + can_create_invitations: true, is_curated: false, viewed_onboarding_page: true, }, }, }, - current_user: { - name: 'someuser', - access: { - app_search: true, - workplace_search: false, - }, - app_search_role: { - id: 'account_id:somestring|user_oid:somestring', - role_type: 'owner', - ability: { - access_all_engines: true, - destroy: ['session'], - manage: ['account_credentials', 'account_engines'], // etc - edit: ['LocoMoco::Account'], // etc - view: ['Engine'], // etc - credential_types: ['admin', 'private', 'search'], - available_role_types: ['owner', 'admin'], - }, - }, - }, }; beforeEach(() => { @@ -91,7 +103,7 @@ describe('callEnterpriseSearchConfigAPI', () => { it('calls the config API endpoint', async () => { fetchMock.mockImplementationOnce((url: string) => { - expect(url).toEqual('http://localhost:3002/api/ent/v1/internal/client_config'); + expect(url).toEqual('http://localhost:3002/api/ent/v2/internal/client_config'); return Promise.resolve(new Response(JSON.stringify(mockResponse))); }); @@ -116,9 +128,20 @@ describe('callEnterpriseSearchConfigAPI', () => { publicUrl: undefined, readOnlyMode: false, ilmEnabled: false, + isFederatedAuth: false, configuredLimits: { - maxDocumentByteSize: undefined, - maxEnginesPerMetaEngine: undefined, + appSearch: { + engine: { + maxDocumentByteSize: undefined, + maxEnginesPerMetaEngine: undefined, + }, + }, + workplaceSearch: { + customApiSource: { + maxDocumentByteSize: undefined, + totalFields: undefined, + }, + }, }, appSearch: { accountId: undefined, @@ -138,17 +161,16 @@ describe('callEnterpriseSearchConfigAPI', () => { }, }, workplaceSearch: { - canCreateInvitations: false, - isFederatedAuth: false, organization: { name: undefined, defaultOrgName: undefined, }, - fpAccount: { + account: { id: undefined, groups: [], isAdmin: false, canCreatePersonalSources: false, + canCreateInvitations: false, isCurated: false, viewedOnboardingPage: false, }, 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 c9cbec15169d9..10a75e59cb249 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 @@ -29,7 +29,7 @@ interface IReturn extends IInitialAppData { * useful various settings (e.g. product access, external URL) * needed by the Kibana plugin at the setup stage */ -const ENDPOINT = '/api/ent/v1/internal/client_config'; +const ENDPOINT = '/api/ent/v2/internal/client_config'; export const callEnterpriseSearchConfigAPI = async ({ config, @@ -67,44 +67,60 @@ export const callEnterpriseSearchConfigAPI = async ({ publicUrl: stripTrailingSlash(data?.settings?.external_url), readOnlyMode: !!data?.settings?.read_only_mode, ilmEnabled: !!data?.settings?.ilm_enabled, + isFederatedAuth: !!data?.settings?.is_federated_auth, // i.e., not standard auth configuredLimits: { - maxDocumentByteSize: data?.settings?.configured_limits?.max_document_byte_size, - maxEnginesPerMetaEngine: data?.settings?.configured_limits?.max_engines_per_meta_engine, + appSearch: { + engine: { + maxDocumentByteSize: + data?.settings?.configured_limits?.app_search?.engine?.document_size_in_bytes, + maxEnginesPerMetaEngine: + data?.settings?.configured_limits?.app_search?.engine?.source_engines_per_meta_engine, + }, + }, + workplaceSearch: { + customApiSource: { + maxDocumentByteSize: + data?.settings?.configured_limits?.workplace_search?.custom_api_source + ?.document_size_in_bytes, + totalFields: + data?.settings?.configured_limits?.workplace_search?.custom_api_source?.total_fields, + }, + }, }, appSearch: { - accountId: data?.settings?.app_search?.account_id, - onBoardingComplete: !!data?.settings?.app_search?.onboarding_complete, + accountId: data?.current_user?.app_search?.account?.id, + onBoardingComplete: !!data?.current_user?.app_search?.account?.onboarding_complete, role: { - id: data?.current_user?.app_search_role?.id, - roleType: data?.current_user?.app_search_role?.role_type, + id: data?.current_user?.app_search?.role?.id, + roleType: data?.current_user?.app_search?.role?.role_type, ability: { - accessAllEngines: !!data?.current_user?.app_search_role?.ability?.access_all_engines, - destroy: data?.current_user?.app_search_role?.ability?.destroy || [], - manage: data?.current_user?.app_search_role?.ability?.manage || [], - edit: data?.current_user?.app_search_role?.ability?.edit || [], - view: data?.current_user?.app_search_role?.ability?.view || [], - credentialTypes: data?.current_user?.app_search_role?.ability?.credential_types || [], + accessAllEngines: !!data?.current_user?.app_search?.role?.ability?.access_all_engines, + destroy: data?.current_user?.app_search?.role?.ability?.destroy || [], + manage: data?.current_user?.app_search?.role?.ability?.manage || [], + edit: data?.current_user?.app_search?.role?.ability?.edit || [], + view: data?.current_user?.app_search?.role?.ability?.view || [], + credentialTypes: data?.current_user?.app_search?.role?.ability?.credential_types || [], availableRoleTypes: - data?.current_user?.app_search_role?.ability?.available_role_types || [], + data?.current_user?.app_search?.role?.ability?.available_role_types || [], }, }, }, workplaceSearch: { - canCreateInvitations: !!data?.settings?.workplace_search?.can_create_invitations, - isFederatedAuth: !!data?.settings?.workplace_search?.is_federated_auth, organization: { - name: data?.settings?.workplace_search?.organization?.name, - defaultOrgName: data?.settings?.workplace_search?.organization?.default_org_name, + name: data?.current_user?.workplace_search?.organization?.name, + defaultOrgName: data?.current_user?.workplace_search?.organization?.default_org_name, }, - fpAccount: { - id: data?.settings?.workplace_search?.fp_account.id, - groups: data?.settings?.workplace_search?.fp_account.groups || [], - isAdmin: !!data?.settings?.workplace_search?.fp_account?.is_admin, - canCreatePersonalSources: !!data?.settings?.workplace_search?.fp_account + account: { + id: data?.current_user?.workplace_search?.account?.id, + groups: data?.current_user?.workplace_search?.account?.groups || [], + isAdmin: !!data?.current_user?.workplace_search?.account?.is_admin, + canCreatePersonalSources: !!data?.current_user?.workplace_search?.account ?.can_create_personal_sources, - isCurated: !!data?.settings?.workplace_search?.fp_account.is_curated, - viewedOnboardingPage: !!data?.settings?.workplace_search?.fp_account - .viewed_onboarding_page, + canCreateInvitations: !!data?.current_user?.workplace_search?.account + ?.can_create_invitations, + isCurated: !!data?.current_user?.workplace_search?.account?.is_curated, + viewedOnboardingPage: !!data?.current_user?.workplace_search?.account + ?.viewed_onboarding_page, }, }, }; diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts index 3f3f182433144..34f83ef3a3fd2 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.test.ts @@ -5,6 +5,7 @@ */ import { mockConfig, mockLogger } from '../__mocks__'; +import { JSON_HEADER } from '../../common/constants'; import { EnterpriseSearchRequestHandler } from './enterprise_search_request_handler'; @@ -150,18 +151,26 @@ describe('EnterpriseSearchRequestHandler', () => { ); }); - it('returns an error when user authentication to Enterprise Search fails', async () => { - EnterpriseSearchAPI.mockReturn({}, { url: 'http://localhost:3002/login' }); - const requestHandler = enterpriseSearchRequestHandler.createRequest({ - path: '/api/unauthenticated', + describe('user authentication errors', () => { + afterEach(async () => { + const requestHandler = enterpriseSearchRequestHandler.createRequest({ + path: '/api/unauthenticated', + }); + await makeAPICall(requestHandler); + + EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/unauthenticated'); + expect(responseMock.customError).toHaveBeenCalledWith({ + body: 'Error connecting to Enterprise Search: Cannot authenticate Enterprise Search user', + statusCode: 502, + }); }); - await makeAPICall(requestHandler); - EnterpriseSearchAPI.shouldHaveBeenCalledWith('http://localhost:3002/api/unauthenticated'); + it('errors when redirected to /login', async () => { + EnterpriseSearchAPI.mockReturn({}, { url: 'http://localhost:3002/login' }); + }); - expect(responseMock.customError).toHaveBeenCalledWith({ - body: 'Error connecting to Enterprise Search: Cannot authenticate Enterprise Search user', - statusCode: 502, + it('errors when redirected to /ent/select', async () => { + EnterpriseSearchAPI.mockReturn({}, { url: 'http://localhost:3002/ent/select' }); }); }); }); @@ -185,7 +194,7 @@ const makeAPICall = (handler: Function, params = {}) => { const EnterpriseSearchAPI = { shouldHaveBeenCalledWith(expectedUrl: string, expectedParams = {}) { expect(fetchMock).toHaveBeenCalledWith(expectedUrl, { - headers: { Authorization: 'Basic 123' }, + headers: { Authorization: 'Basic 123', ...JSON_HEADER }, method: 'GET', body: undefined, ...expectedParams, diff --git a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts index 8f31bd9063d4a..18f10c590847c 100644 --- a/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts +++ b/x-pack/plugins/enterprise_search/server/lib/enterprise_search_request_handler.ts @@ -14,6 +14,7 @@ import { Logger, } from 'src/core/server'; import { ConfigType } from '../index'; +import { JSON_HEADER } from '../../common/constants'; interface IConstructorDependencies { config: ConfigType; @@ -25,7 +26,7 @@ interface IRequestParams { hasValidData?: (body?: ResponseBody) => boolean; } export interface IEnterpriseSearchRequestHandler { - createRequest(requestParams?: object): RequestHandler, unknown>; + createRequest(requestParams?: object): RequestHandler; } /** @@ -52,12 +53,12 @@ export class EnterpriseSearchRequestHandler { }: IRequestParams) { return async ( _context: RequestHandlerContext, - request: KibanaRequest, unknown>, + request: KibanaRequest, response: KibanaResponseFactory ) => { try { // Set up API URL - const queryParams = { ...request.query, ...params }; + const queryParams = { ...(request.query as object), ...params }; const queryString = !this.isEmptyObj(queryParams) ? `?${querystring.stringify(queryParams)}` : ''; @@ -65,7 +66,7 @@ export class EnterpriseSearchRequestHandler { // Set up API options const { method } = request.route; - const headers = { Authorization: request.headers.authorization as string }; + const headers = { Authorization: request.headers.authorization as string, ...JSON_HEADER }; const body = !this.isEmptyObj(request.body as object) ? JSON.stringify(request.body) : undefined; @@ -73,7 +74,7 @@ export class EnterpriseSearchRequestHandler { // Call the Enterprise Search API and pass back response to the front-end const apiResponse = await fetch(url, { method, headers, body }); - if (apiResponse.url.endsWith('/login')) { + if (apiResponse.url.endsWith('/login') || apiResponse.url.endsWith('/ent/select')) { throw new Error('Cannot authenticate Enterprise Search user'); } diff --git a/x-pack/plugins/enterprise_search/server/plugin.ts b/x-pack/plugins/enterprise_search/server/plugin.ts index 617210a544262..3d28a05a4b7b4 100644 --- a/x-pack/plugins/enterprise_search/server/plugin.ts +++ b/x-pack/plugins/enterprise_search/server/plugin.ts @@ -31,8 +31,10 @@ import { IEnterpriseSearchRequestHandler, } from './lib/enterprise_search_request_handler'; -import { registerConfigDataRoute } from './routes/enterprise_search/config_data'; +import { enterpriseSearchTelemetryType } from './saved_objects/enterprise_search/telemetry'; +import { registerTelemetryUsageCollector as registerESTelemetryUsageCollector } from './collectors/enterprise_search/telemetry'; import { registerTelemetryRoute } from './routes/enterprise_search/telemetry'; +import { registerConfigDataRoute } from './routes/enterprise_search/config_data'; import { appSearchTelemetryType } from './saved_objects/app_search/telemetry'; import { registerTelemetryUsageCollector as registerASTelemetryUsageCollector } from './collectors/app_search/telemetry'; @@ -76,13 +78,17 @@ export class EnterpriseSearchPlugin implements Plugin { /** * Register space/feature control */ - features.registerFeature({ + features.registerKibanaFeature({ id: ENTERPRISE_SEARCH_PLUGIN.ID, name: ENTERPRISE_SEARCH_PLUGIN.NAME, order: 0, icon: 'logoEnterpriseSearch', - navLinkId: APP_SEARCH_PLUGIN.ID, // TODO - remove this once functional tests no longer rely on navLinkId - app: ['kibana', APP_SEARCH_PLUGIN.ID, WORKPLACE_SEARCH_PLUGIN.ID], + app: [ + 'kibana', + ENTERPRISE_SEARCH_PLUGIN.ID, + APP_SEARCH_PLUGIN.ID, + WORKPLACE_SEARCH_PLUGIN.ID, + ], catalogue: [ENTERPRISE_SEARCH_PLUGIN.ID, APP_SEARCH_PLUGIN.ID, WORKPLACE_SEARCH_PLUGIN.ID], privileges: null, }); @@ -94,14 +100,16 @@ export class EnterpriseSearchPlugin implements Plugin { const dependencies = { config, security, request, log }; const { hasAppSearchAccess, hasWorkplaceSearchAccess } = await checkAccess(dependencies); + const showEnterpriseSearchOverview = hasAppSearchAccess || hasWorkplaceSearchAccess; return { navLinks: { + enterpriseSearch: showEnterpriseSearchOverview, appSearch: hasAppSearchAccess, workplaceSearch: hasWorkplaceSearchAccess, }, catalogue: { - enterpriseSearch: hasAppSearchAccess || hasWorkplaceSearchAccess, + enterpriseSearch: showEnterpriseSearchOverview, appSearch: hasAppSearchAccess, workplaceSearch: hasWorkplaceSearchAccess, }, @@ -123,6 +131,7 @@ export class EnterpriseSearchPlugin implements Plugin { /** * Bootstrap the routes, saved objects, and collector for telemetry */ + savedObjects.registerType(enterpriseSearchTelemetryType); savedObjects.registerType(appSearchTelemetryType); savedObjects.registerType(workplaceSearchTelemetryType); let savedObjectsStarted: SavedObjectsServiceStart; @@ -131,6 +140,7 @@ export class EnterpriseSearchPlugin implements Plugin { savedObjectsStarted = coreStart.savedObjects; if (usageCollection) { + registerESTelemetryUsageCollector(usageCollection, savedObjectsStarted, this.logger); registerASTelemetryUsageCollector(usageCollection, savedObjectsStarted, this.logger); registerWSTelemetryUsageCollector(usageCollection, savedObjectsStarted, this.logger); } diff --git a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts index 7ed1d7b17753c..bfc07c8b64ef5 100644 --- a/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts +++ b/x-pack/plugins/enterprise_search/server/routes/enterprise_search/telemetry.ts @@ -9,12 +9,13 @@ import { schema } from '@kbn/config-schema'; import { IRouteDependencies } from '../../plugin'; import { incrementUICounter } from '../../collectors/lib/telemetry'; +import { ES_TELEMETRY_NAME } from '../../collectors/enterprise_search/telemetry'; import { AS_TELEMETRY_NAME } from '../../collectors/app_search/telemetry'; import { WS_TELEMETRY_NAME } from '../../collectors/workplace_search/telemetry'; const productToTelemetryMap = { + enterprise_search: ES_TELEMETRY_NAME, app_search: AS_TELEMETRY_NAME, workplace_search: WS_TELEMETRY_NAME, - enterprise_search: 'TODO', }; export function registerTelemetryRoute({ diff --git a/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/telemetry.ts b/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/telemetry.ts new file mode 100644 index 0000000000000..54044e67939da --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/saved_objects/enterprise_search/telemetry.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +/* istanbul ignore file */ + +import { SavedObjectsType } from 'src/core/server'; +import { ES_TELEMETRY_NAME } from '../../collectors/enterprise_search/telemetry'; + +export const enterpriseSearchTelemetryType: SavedObjectsType = { + name: ES_TELEMETRY_NAME, + hidden: false, + namespaceType: 'agnostic', + mappings: { + dynamic: false, + properties: {}, + }, +}; diff --git a/x-pack/plugins/features/common/elasticsearch_feature.ts b/x-pack/plugins/features/common/elasticsearch_feature.ts new file mode 100644 index 0000000000000..4566afc77bd8b --- /dev/null +++ b/x-pack/plugins/features/common/elasticsearch_feature.ts @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { RecursiveReadonly } from '@kbn/utility-types'; +import { FeatureElasticsearchPrivileges } from './feature_elasticsearch_privileges'; + +/** + * Interface for registering an Elasticsearch feature. + * Feature registration allows plugins to hide their applications based + * on configured cluster or index privileges. + */ +export interface ElasticsearchFeatureConfig { + /** + * Unique identifier for this feature. + * This identifier is also used when generating UI Capabilities. + * + * @see UICapabilities + */ + id: string; + + /** + * Management sections associated with this feature. + * + * @example + * ```ts + * // Enables access to the "Advanced Settings" management page within the Kibana section + * management: { + * kibana: ['settings'] + * } + * ``` + */ + management?: { + [sectionId: string]: string[]; + }; + + /** + * If this feature includes a catalogue entry, you can specify them here to control visibility based on the current space. + * + */ + catalogue?: string[]; + + /** + * Feature privilege definition. Specify one or more privileges which grant access to this feature. + * Users must satisfy all privileges in at least one of the defined sets of privileges in order to be granted access. + * + * @example + * ```ts + * [{ + * requiredClusterPrivileges: ['monitor'], + * requiredIndexPrivileges: { + * ['metricbeat-*']: ['read', 'view_index_metadata'] + * } + * }] + * ``` + * @see FeatureElasticsearchPrivileges + */ + privileges: FeatureElasticsearchPrivileges[]; +} + +export class ElasticsearchFeature { + constructor(protected readonly config: RecursiveReadonly) {} + + public get id() { + return this.config.id; + } + + public get catalogue() { + return this.config.catalogue; + } + + public get management() { + return this.config.management; + } + + public get privileges() { + return this.config.privileges; + } + + public toRaw() { + return { ...this.config } as ElasticsearchFeatureConfig; + } +} diff --git a/x-pack/plugins/features/common/feature_elasticsearch_privileges.ts b/x-pack/plugins/features/common/feature_elasticsearch_privileges.ts new file mode 100644 index 0000000000000..1100b2cc648c9 --- /dev/null +++ b/x-pack/plugins/features/common/feature_elasticsearch_privileges.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/** + * Elasticsearch Feature privilege definition + */ +export interface FeatureElasticsearchPrivileges { + /** + * A set of Elasticsearch cluster privileges which are required for this feature to be enabled. + * See https://www.elastic.co/guide/en/elasticsearch/reference/current/security-privileges.html + * + */ + requiredClusterPrivileges: string[]; + + /** + * A set of Elasticsearch index privileges which are required for this feature to be enabled, keyed on index name or pattern. + * See https://www.elastic.co/guide/en/elasticsearch/reference/current/security-privileges.html#privileges-list-indices + * + * @example + * + * Requiring `read` access to `logstash-*` and `all` access to `foo-*` + * ```ts + * feature.registerElasticsearchPrivilege({ + * privileges: [{ + * requiredIndexPrivileges: { + * ['logstash-*']: ['read'], + * ['foo-*]: ['all'] + * } + * }] + * }) + * ``` + * + */ + requiredIndexPrivileges?: { + [indexName: string]: string[]; + }; + + /** + * A set of Elasticsearch roles which are required for this feature to be enabled. + * + * @deprecated do not rely on hard-coded role names. + * + * This is relied on by the reporting feature, and should be removed once reporting + * migrates to using the Kibana Privilege model: https://github.com/elastic/kibana/issues/19914 + */ + requiredRoles?: string[]; + + /** + * A list of UI Capabilities that should be granted to users with this privilege. + * These capabilities will automatically be namespaces within your feature id. + * + * @example + * ```ts + * { + * ui: ['show', 'save'] + * } + * + * This translates in the UI to the following (assuming a feature id of "foo"): + * import { uiCapabilities } from 'ui/capabilities'; + * + * const canShowApp = uiCapabilities.foo.show; + * const canSave = uiCapabilities.foo.save; + * ``` + * Note: Since these are automatically namespaced, you are free to use generic names like "show" and "save". + * + * @see UICapabilities + */ + ui: string[]; +} diff --git a/x-pack/plugins/features/common/index.ts b/x-pack/plugins/features/common/index.ts index e359efbda20d2..a08de2f118712 100644 --- a/x-pack/plugins/features/common/index.ts +++ b/x-pack/plugins/features/common/index.ts @@ -4,8 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ +export { FeatureElasticsearchPrivileges } from './feature_elasticsearch_privileges'; export { FeatureKibanaPrivileges } from './feature_kibana_privileges'; -export { Feature, FeatureConfig } from './feature'; +export { ElasticsearchFeature, ElasticsearchFeatureConfig } from './elasticsearch_feature'; +export { KibanaFeature, KibanaFeatureConfig } from './kibana_feature'; export { SubFeature, SubFeatureConfig, diff --git a/x-pack/plugins/features/common/feature.ts b/x-pack/plugins/features/common/kibana_feature.ts similarity index 92% rename from x-pack/plugins/features/common/feature.ts rename to x-pack/plugins/features/common/kibana_feature.ts index 1b700fb1a6ad0..a600ada554afd 100644 --- a/x-pack/plugins/features/common/feature.ts +++ b/x-pack/plugins/features/common/kibana_feature.ts @@ -6,7 +6,7 @@ import { RecursiveReadonly } from '@kbn/utility-types'; import { FeatureKibanaPrivileges } from './feature_kibana_privileges'; -import { SubFeatureConfig, SubFeature } from './sub_feature'; +import { SubFeatureConfig, SubFeature as KibanaSubFeature } from './sub_feature'; import { ReservedKibanaPrivilege } from './reserved_kibana_privilege'; /** @@ -14,7 +14,7 @@ import { ReservedKibanaPrivilege } from './reserved_kibana_privilege'; * Feature registration allows plugins to hide their applications with spaces, * and secure access when configured for security. */ -export interface FeatureConfig { +export interface KibanaFeatureConfig { /** * Unique identifier for this feature. * This identifier is also used when generating UI Capabilities. @@ -137,12 +137,12 @@ export interface FeatureConfig { }; } -export class Feature { - public readonly subFeatures: SubFeature[]; +export class KibanaFeature { + public readonly subFeatures: KibanaSubFeature[]; - constructor(protected readonly config: RecursiveReadonly) { + constructor(protected readonly config: RecursiveReadonly) { this.subFeatures = (config.subFeatures ?? []).map( - (subFeatureConfig) => new SubFeature(subFeatureConfig) + (subFeatureConfig) => new KibanaSubFeature(subFeatureConfig) ); } @@ -199,6 +199,6 @@ export class Feature { } public toRaw() { - return { ...this.config } as FeatureConfig; + return { ...this.config } as KibanaFeatureConfig; } } diff --git a/x-pack/plugins/features/public/features_api_client.ts b/x-pack/plugins/features/public/features_api_client.ts index 50cc54a197f56..cacc623aa853f 100644 --- a/x-pack/plugins/features/public/features_api_client.ts +++ b/x-pack/plugins/features/public/features_api_client.ts @@ -5,13 +5,13 @@ */ import { HttpSetup } from 'src/core/public'; -import { FeatureConfig, Feature } from '.'; +import { KibanaFeatureConfig, KibanaFeature } from '.'; export class FeaturesAPIClient { constructor(private readonly http: HttpSetup) {} public async getFeatures() { - const features = await this.http.get('/api/features'); - return features.map((config) => new Feature(config)); + const features = await this.http.get('/api/features'); + return features.map((config) => new KibanaFeature(config)); } } diff --git a/x-pack/plugins/features/public/index.ts b/x-pack/plugins/features/public/index.ts index f19c7f947d97f..7d86312e466ee 100644 --- a/x-pack/plugins/features/public/index.ts +++ b/x-pack/plugins/features/public/index.ts @@ -8,8 +8,8 @@ import { PluginInitializer } from 'src/core/public'; import { FeaturesPlugin, FeaturesPluginSetup, FeaturesPluginStart } from './plugin'; export { - Feature, - FeatureConfig, + KibanaFeature, + KibanaFeatureConfig, FeatureKibanaPrivileges, SubFeatureConfig, SubFeaturePrivilegeConfig, diff --git a/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap index e033b241f9e25..fdeb53dd2fa12 100644 --- a/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/feature_registry.test.ts.snap @@ -1,27 +1,27 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`FeatureRegistry prevents features from being registered with a catalogue entry of "" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" is not allowed to be empty]]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" is not allowed to be empty]]"`; -exports[`FeatureRegistry prevents features from being registered with a catalogue entry of "contains space" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "contains space" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]"`; -exports[`FeatureRegistry prevents features from being registered with a catalogue entry of "contains_invalid()_chars" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with a catalogue entry of "contains_invalid()_chars" 1`] = `"child \\"catalogue\\" fails because [\\"catalogue\\" at position 0 fails because [\\"0\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]"`; -exports[`FeatureRegistry prevents features from being registered with a management id of "" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" is not allowed to be empty]]]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" is not allowed to be empty]]]"`; -exports[`FeatureRegistry prevents features from being registered with a management id of "contains space" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "contains space" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]]"`; -exports[`FeatureRegistry prevents features from being registered with a management id of "contains_invalid()_chars" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with a management id of "contains_invalid()_chars" 1`] = `"child \\"management\\" fails because [child \\"kibana\\" fails because [\\"kibana\\" at position 0 fails because [\\"0\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]]]"`; -exports[`FeatureRegistry prevents features from being registered with a navLinkId of "" 1`] = `"child \\"navLinkId\\" fails because [\\"navLinkId\\" is not allowed to be empty]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with a navLinkId of "" 1`] = `"child \\"navLinkId\\" fails because [\\"navLinkId\\" is not allowed to be empty]"`; -exports[`FeatureRegistry prevents features from being registered with a navLinkId of "contains space" 1`] = `"child \\"navLinkId\\" fails because [\\"navLinkId\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with a navLinkId of "contains space" 1`] = `"child \\"navLinkId\\" fails because [\\"navLinkId\\" with value \\"contains space\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]"`; -exports[`FeatureRegistry prevents features from being registered with a navLinkId of "contains_invalid()_chars" 1`] = `"child \\"navLinkId\\" fails because [\\"navLinkId\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with a navLinkId of "contains_invalid()_chars" 1`] = `"child \\"navLinkId\\" fails because [\\"navLinkId\\" with value \\"contains_invalid()_chars\\" fails to match the required pattern: /^[a-zA-Z0-9:_-]+$/]"`; -exports[`FeatureRegistry prevents features from being registered with an ID of "catalogue" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "catalogue" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; -exports[`FeatureRegistry prevents features from being registered with an ID of "doesn't match valid regex" 1`] = `"child \\"id\\" fails because [\\"id\\" with value \\"doesn't match valid regex\\" fails to match the required pattern: /^[a-zA-Z0-9_-]+$/]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "doesn't match valid regex" 1`] = `"child \\"id\\" fails because [\\"id\\" with value \\"doesn't match valid regex\\" fails to match the required pattern: /^[a-zA-Z0-9_-]+$/]"`; -exports[`FeatureRegistry prevents features from being registered with an ID of "management" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "management" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; -exports[`FeatureRegistry prevents features from being registered with an ID of "navLinks" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; +exports[`FeatureRegistry Kibana Features prevents features from being registered with an ID of "navLinks" 1`] = `"child \\"id\\" fails because [\\"id\\" contains an invalid value]"`; diff --git a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap index e4014cf49778c..63a59d59d6d07 100644 --- a/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap +++ b/x-pack/plugins/features/server/__snapshots__/oss_features.test.ts.snap @@ -111,6 +111,7 @@ Array [ "visualization", "timelion-sheet", "canvas-workpad", + "lens", "map", "dashboard", "query", diff --git a/x-pack/plugins/features/server/feature_registry.test.ts b/x-pack/plugins/features/server/feature_registry.test.ts index f123068e41758..24aae3a69ee5d 100644 --- a/x-pack/plugins/features/server/feature_registry.test.ts +++ b/x-pack/plugins/features/server/feature_registry.test.ts @@ -5,1192 +5,1389 @@ */ import { FeatureRegistry } from './feature_registry'; -import { FeatureConfig } from '../common/feature'; +import { ElasticsearchFeatureConfig, KibanaFeatureConfig } from '../common'; describe('FeatureRegistry', () => { - it('allows a minimal feature to be registered', () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: null, - }; + describe('Kibana Features', () => { + it('allows a minimal feature to be registered', () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + }; - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); - const result = featureRegistry.getAll(); - expect(result).toHaveLength(1); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + const result = featureRegistry.getAllKibanaFeatures(); + expect(result).toHaveLength(1); - // Should be the equal, but not the same instance (i.e., a defensive copy) - expect(result[0].toRaw()).not.toBe(feature); - expect(result[0].toRaw()).toEqual(feature); - }); + // Should be the equal, but not the same instance (i.e., a defensive copy) + expect(result[0].toRaw()).not.toBe(feature); + expect(result[0].toRaw()).toEqual(feature); + }); - it('allows a complex feature to be registered', () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - excludeFromBasePrivileges: true, - icon: 'addDataApp', - navLinkId: 'someNavLink', - app: ['app1'], - validLicenses: ['standard', 'basic', 'gold', 'platinum'], - catalogue: ['foo'], - management: { - foo: ['bar'], - }, - privileges: { - all: { - catalogue: ['foo'], - management: { - foo: ['bar'], - }, - app: ['app1'], - savedObject: { - all: ['space', 'etc', 'telemetry'], - read: ['canvas', 'config', 'url'], - }, - api: ['someApiEndpointTag', 'anotherEndpointTag'], - ui: ['allowsFoo', 'showBar', 'showBaz'], + it('allows a complex feature to be registered', () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + excludeFromBasePrivileges: true, + icon: 'addDataApp', + navLinkId: 'someNavLink', + app: ['app1'], + validLicenses: ['standard', 'basic', 'gold', 'platinum'], + catalogue: ['foo'], + management: { + foo: ['bar'], }, - read: { - savedObject: { - all: [], - read: ['config', 'url'], + privileges: { + all: { + catalogue: ['foo'], + management: { + foo: ['bar'], + }, + app: ['app1'], + savedObject: { + all: ['space', 'etc', 'telemetry'], + read: ['canvas', 'config', 'url'], + }, + api: ['someApiEndpointTag', 'anotherEndpointTag'], + ui: ['allowsFoo', 'showBar', 'showBaz'], + }, + read: { + savedObject: { + all: [], + read: ['config', 'url'], + }, + ui: [], }, - ui: [], }, - }, - subFeatures: [ - { - name: 'sub-feature-1', - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - id: 'foo', - name: 'foo', - includeIn: 'read', - savedObject: { - all: [], - read: [], + subFeatures: [ + { + name: 'sub-feature-1', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'foo', + name: 'foo', + includeIn: 'read', + savedObject: { + all: [], + read: [], + }, + ui: [], }, - ui: [], - }, - ], - }, - { - groupType: 'mutually_exclusive', - privileges: [ - { - id: 'bar', - name: 'bar', - includeIn: 'all', - savedObject: { - all: [], - read: [], + ], + }, + { + groupType: 'mutually_exclusive', + privileges: [ + { + id: 'bar', + name: 'bar', + includeIn: 'all', + savedObject: { + all: [], + read: [], + }, + ui: [], }, - ui: [], - }, - { - id: 'baz', - name: 'baz', - includeIn: 'none', - savedObject: { - all: [], - read: [], + { + id: 'baz', + name: 'baz', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], }, - ui: [], + ], + }, + ], + }, + ], + privilegesTooltip: 'some fancy tooltip', + reserved: { + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['foo'], + management: { + foo: ['bar'], }, - ], + app: ['app1'], + savedObject: { + all: ['space', 'etc', 'telemetry'], + read: ['canvas', 'config', 'url'], + }, + api: ['someApiEndpointTag', 'anotherEndpointTag'], + ui: ['allowsFoo', 'showBar', 'showBaz'], + }, }, ], + description: 'some completely adequate description', }, - ], - privilegesTooltip: 'some fancy tooltip', - reserved: { - privileges: [ - { - id: 'reserved', - privilege: { - catalogue: ['foo'], - management: { - foo: ['bar'], - }, - app: ['app1'], - savedObject: { - all: ['space', 'etc', 'telemetry'], - read: ['canvas', 'config', 'url'], - }, - api: ['someApiEndpointTag', 'anotherEndpointTag'], - ui: ['allowsFoo', 'showBar', 'showBaz'], - }, - }, - ], - description: 'some completely adequate description', - }, - }; + }; - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); - const result = featureRegistry.getAll(); - expect(result).toHaveLength(1); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + const result = featureRegistry.getAllKibanaFeatures(); + expect(result).toHaveLength(1); - // Should be the equal, but not the same instance (i.e., a defensive copy) - expect(result[0].toRaw()).not.toBe(feature); - expect(result[0].toRaw()).toEqual(feature); - }); + // Should be the equal, but not the same instance (i.e., a defensive copy) + expect(result[0].toRaw()).not.toBe(feature); + expect(result[0].toRaw()).toEqual(feature); + }); - it(`requires a value for privileges`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - } as any; + it(`requires a value for privileges`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + } as any; - const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"child \\"privileges\\" fails because [\\"privileges\\" is required]"` - ); - }); + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"privileges\\" fails because [\\"privileges\\" is required]"` + ); + }); - it(`does not allow sub-features to be registered when no primary privileges are not registered`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: null, - subFeatures: [ - { - name: 'my sub feature', - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - { - id: 'my-sub-priv', - name: 'my sub priv', - includeIn: 'none', - savedObject: { - all: [], - read: [], + it(`does not allow sub-features to be registered when no primary privileges are not registered`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + subFeatures: [ + { + name: 'my sub feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'my-sub-priv', + name: 'my sub priv', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], }, - ui: [], - }, - ], - }, - ], - }, - ], - }; + ], + }, + ], + }, + ], + }; - const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"child \\"subFeatures\\" fails because [\\"subFeatures\\" must contain less than or equal to 0 items]"` - ); - }); + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"subFeatures\\" fails because [\\"subFeatures\\" must contain less than or equal to 0 items]"` + ); + }); - it(`automatically grants 'all' access to telemetry saved objects for the 'all' privilege`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: { - all: { - ui: [], - savedObject: { - all: [], - read: [], + it(`automatically grants 'all' access to telemetry saved objects for the 'all' privilege`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: { + all: { + ui: [], + savedObject: { + all: [], + read: [], + }, }, - }, - read: { - ui: [], - savedObject: { - all: [], - read: [], + read: { + ui: [], + savedObject: { + all: [], + read: [], + }, }, }, - }, - }; + }; - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); - const result = featureRegistry.getAll(); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + const result = featureRegistry.getAllKibanaFeatures(); - expect(result[0].privileges).toHaveProperty('all'); - expect(result[0].privileges).toHaveProperty('read'); + expect(result[0].privileges).toHaveProperty('all'); + expect(result[0].privileges).toHaveProperty('read'); - const allPrivilege = result[0].privileges?.all; - expect(allPrivilege?.savedObject.all).toEqual(['telemetry']); - }); + const allPrivilege = result[0].privileges?.all; + expect(allPrivilege?.savedObject.all).toEqual(['telemetry']); + }); - it(`automatically grants 'read' access to config and url saved objects for both privileges`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: { - all: { - ui: [], - savedObject: { - all: [], - read: [], + it(`automatically grants 'read' access to config and url saved objects for both privileges`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: { + all: { + ui: [], + savedObject: { + all: [], + read: [], + }, }, - }, - read: { - ui: [], - savedObject: { - all: [], - read: [], + read: { + ui: [], + savedObject: { + all: [], + read: [], + }, }, }, - }, - }; + }; - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); - const result = featureRegistry.getAll(); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + const result = featureRegistry.getAllKibanaFeatures(); - expect(result[0].privileges).toHaveProperty('all'); - expect(result[0].privileges).toHaveProperty('read'); + expect(result[0].privileges).toHaveProperty('all'); + expect(result[0].privileges).toHaveProperty('read'); - const allPrivilege = result[0].privileges?.all; - const readPrivilege = result[0].privileges?.read; - expect(allPrivilege?.savedObject.read).toEqual(['config', 'url']); - expect(readPrivilege?.savedObject.read).toEqual(['config', 'url']); - }); + const allPrivilege = result[0].privileges?.all; + const readPrivilege = result[0].privileges?.read; + expect(allPrivilege?.savedObject.read).toEqual(['config', 'url']); + expect(readPrivilege?.savedObject.read).toEqual(['config', 'url']); + }); - it(`automatically grants 'all' access to telemetry and 'read' to [config, url] saved objects for the reserved privilege`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: null, - reserved: { - description: 'foo', - privileges: [ - { - id: 'reserved', - privilege: { - ui: [], - savedObject: { - all: [], - read: [], + it(`automatically grants 'all' access to telemetry and 'read' to [config, url] saved objects for the reserved privilege`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + reserved: { + description: 'foo', + privileges: [ + { + id: 'reserved', + privilege: { + ui: [], + savedObject: { + all: [], + read: [], + }, }, }, - }, - ], - }, - }; + ], + }, + }; - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); - const result = featureRegistry.getAll(); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + const result = featureRegistry.getAllKibanaFeatures(); - const reservedPrivilege = result[0]!.reserved!.privileges[0].privilege; - expect(reservedPrivilege.savedObject.all).toEqual(['telemetry']); - expect(reservedPrivilege.savedObject.read).toEqual(['config', 'url']); - }); + const reservedPrivilege = result[0]!.reserved!.privileges[0].privilege; + expect(reservedPrivilege.savedObject.all).toEqual(['telemetry']); + expect(reservedPrivilege.savedObject.read).toEqual(['config', 'url']); + }); - it(`does not duplicate the automatic grants if specified on the incoming feature`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: { - all: { - ui: [], - savedObject: { - all: ['telemetry'], - read: ['config', 'url'], + it(`does not duplicate the automatic grants if specified on the incoming feature`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: { + all: { + ui: [], + savedObject: { + all: ['telemetry'], + read: ['config', 'url'], + }, }, - }, - read: { - ui: [], - savedObject: { - all: [], - read: ['config', 'url'], + read: { + ui: [], + savedObject: { + all: [], + read: ['config', 'url'], + }, }, }, - }, - }; - - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); - const result = featureRegistry.getAll(); + }; - expect(result[0].privileges).toHaveProperty('all'); - expect(result[0].privileges).toHaveProperty('read'); - - const allPrivilege = result[0].privileges!.all; - const readPrivilege = result[0].privileges!.read; - expect(allPrivilege?.savedObject.all).toEqual(['telemetry']); - expect(allPrivilege?.savedObject.read).toEqual(['config', 'url']); - expect(readPrivilege?.savedObject.read).toEqual(['config', 'url']); - }); - - it(`does not allow duplicate features to be registered`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: null, - }; + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + const result = featureRegistry.getAllKibanaFeatures(); - const duplicateFeature: FeatureConfig = { - id: 'test-feature', - name: 'Duplicate Test Feature', - app: [], - privileges: null, - }; + expect(result[0].privileges).toHaveProperty('all'); + expect(result[0].privileges).toHaveProperty('read'); - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); + const allPrivilege = result[0].privileges!.all; + const readPrivilege = result[0].privileges!.read; + expect(allPrivilege?.savedObject.all).toEqual(['telemetry']); + expect(allPrivilege?.savedObject.read).toEqual(['config', 'url']); + expect(readPrivilege?.savedObject.read).toEqual(['config', 'url']); + }); - expect(() => featureRegistry.register(duplicateFeature)).toThrowErrorMatchingInlineSnapshot( - `"Feature with id test-feature is already registered."` - ); - }); + it(`does not allow duplicate features to be registered`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + }; + + const duplicateFeature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Duplicate Test Feature', + app: [], + privileges: null, + }; - ['contains space', 'contains_invalid()_chars', ''].forEach((prohibitedChars) => { - it(`prevents features from being registered with a navLinkId of "${prohibitedChars}"`, () => { const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + expect(() => - featureRegistry.register({ - id: 'foo', - name: 'some feature', - navLinkId: prohibitedChars, - app: [], - privileges: null, - }) - ).toThrowErrorMatchingSnapshot(); + featureRegistry.registerKibanaFeature(duplicateFeature) + ).toThrowErrorMatchingInlineSnapshot(`"Feature with id test-feature is already registered."`); }); - it(`prevents features from being registered with a management id of "${prohibitedChars}"`, () => { - const featureRegistry = new FeatureRegistry(); - expect(() => - featureRegistry.register({ - id: 'foo', - name: 'some feature', - management: { - kibana: [prohibitedChars], - }, - app: [], - privileges: null, - }) - ).toThrowErrorMatchingSnapshot(); + ['contains space', 'contains_invalid()_chars', ''].forEach((prohibitedChars) => { + it(`prevents features from being registered with a navLinkId of "${prohibitedChars}"`, () => { + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature({ + id: 'foo', + name: 'some feature', + navLinkId: prohibitedChars, + app: [], + privileges: null, + }) + ).toThrowErrorMatchingSnapshot(); + }); + + it(`prevents features from being registered with a management id of "${prohibitedChars}"`, () => { + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature({ + id: 'foo', + name: 'some feature', + management: { + kibana: [prohibitedChars], + }, + app: [], + privileges: null, + }) + ).toThrowErrorMatchingSnapshot(); + }); + + it(`prevents features from being registered with a catalogue entry of "${prohibitedChars}"`, () => { + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature({ + id: 'foo', + name: 'some feature', + catalogue: [prohibitedChars], + app: [], + privileges: null, + }) + ).toThrowErrorMatchingSnapshot(); + }); }); - it(`prevents features from being registered with a catalogue entry of "${prohibitedChars}"`, () => { - const featureRegistry = new FeatureRegistry(); - expect(() => - featureRegistry.register({ - id: 'foo', - name: 'some feature', - catalogue: [prohibitedChars], - app: [], - privileges: null, - }) - ).toThrowErrorMatchingSnapshot(); + ['catalogue', 'management', 'navLinks', `doesn't match valid regex`].forEach((prohibitedId) => { + it(`prevents features from being registered with an ID of "${prohibitedId}"`, () => { + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature({ + id: prohibitedId, + name: 'some feature', + app: [], + privileges: null, + }) + ).toThrowErrorMatchingSnapshot(); + }); }); - }); - ['catalogue', 'management', 'navLinks', `doesn't match valid regex`].forEach((prohibitedId) => { - it(`prevents features from being registered with an ID of "${prohibitedId}"`, () => { + it('prevents features from being registered with invalid privilege names', () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: ['app1', 'app2'], + privileges: { + foo: { + name: 'Foo', + app: ['app1', 'app2'], + savedObject: { + all: ['config', 'space', 'etc'], + read: ['canvas'], + }, + api: ['someApiEndpointTag', 'anotherEndpointTag'], + ui: ['allowsFoo', 'showBar', 'showBaz'], + }, + } as any, + }; + const featureRegistry = new FeatureRegistry(); expect(() => - featureRegistry.register({ - id: prohibitedId, - name: 'some feature', - app: [], - privileges: null, - }) - ).toThrowErrorMatchingSnapshot(); + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"privileges\\" fails because [\\"foo\\" is not allowed]"` + ); }); - }); - it('prevents features from being registered with invalid privilege names', () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: ['app1', 'app2'], - privileges: { - foo: { - name: 'Foo', - app: ['app1', 'app2'], - savedObject: { - all: ['config', 'space', 'etc'], - read: ['canvas'], + it(`prevents privileges from specifying app entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: ['bar'], + privileges: { + all: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo', 'bar', 'baz'], + }, + read: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo', 'bar', 'baz'], }, - api: ['someApiEndpointTag', 'anotherEndpointTag'], - ui: ['allowsFoo', 'showBar', 'showBaz'], }, - } as any, - }; + }; - const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"child \\"privileges\\" fails because [\\"foo\\" is not allowed]"` - ); - }); + const featureRegistry = new FeatureRegistry(); - it(`prevents privileges from specifying app entries that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: ['bar'], - privileges: { - all: { - savedObject: { - all: [], - read: [], + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown app entries: foo, baz"` + ); + }); + + it(`prevents features from specifying app entries that don't exist at the privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: ['foo', 'bar', 'baz'], + privileges: { + all: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['bar'], }, - ui: [], - app: ['foo', 'bar', 'baz'], - }, - read: { - savedObject: { - all: [], - read: [], + read: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - ui: [], - app: ['foo', 'bar', 'baz'], }, - }, - }; + subFeatures: [ + { + name: 'my sub feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cool-sub-feature-privilege', + name: 'cool privilege', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo'], + }, + ], + }, + ], + }, + ], + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.all has unknown app entries: foo, baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies app entries which are not granted to any privileges: baz"` + ); + }); - it(`prevents features from specifying app entries that don't exist at the privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: ['foo', 'bar', 'baz'], - privileges: { - all: { - savedObject: { - all: [], - read: [], - }, - ui: [], - app: ['bar'], - }, - read: { - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, - }, - subFeatures: [ - { - name: 'my sub feature', - privilegeGroups: [ + it(`prevents reserved privileges from specifying app entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: ['bar'], + privileges: null, + reserved: { + description: 'something', + privileges: [ { - groupType: 'independent', - privileges: [ - { - id: 'cool-sub-feature-privilege', - name: 'cool privilege', - includeIn: 'none', - savedObject: { - all: [], - read: [], - }, - ui: [], - app: ['foo'], + id: 'reserved', + privilege: { + savedObject: { + all: [], + read: [], }, - ], + ui: [], + app: ['foo', 'bar', 'baz'], + }, }, ], }, - ], - }; + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies app entries which are not granted to any privileges: baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown app entries: foo, baz"` + ); + }); - it(`prevents reserved privileges from specifying app entries that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: ['bar'], - privileges: null, - reserved: { - description: 'something', - privileges: [ - { - id: 'reserved', - privilege: { - savedObject: { - all: [], - read: [], + it(`prevents features from specifying app entries that don't exist at the reserved privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: ['foo', 'bar', 'baz'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: ['foo', 'bar'], }, - ui: [], - app: ['foo', 'bar', 'baz'], }, - }, - ], - }, - }; + ], + }, + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.reserved has unknown app entries: foo, baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies app entries which are not granted to any privileges: baz"` + ); + }); - it(`prevents features from specifying app entries that don't exist at the reserved privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: ['foo', 'bar', 'baz'], - privileges: null, - reserved: { - description: 'something', - privileges: [ - { - id: 'reserved', - privilege: { - savedObject: { - all: [], - read: [], - }, - ui: [], - app: ['foo', 'bar'], + it(`prevents privileges from specifying catalogue entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + privileges: { + all: { + catalogue: ['foo', 'bar', 'baz'], + savedObject: { + all: [], + read: [], }, + ui: [], + app: [], }, - ], - }, - }; + read: { + catalogue: ['foo', 'bar', 'baz'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies app entries which are not granted to any privileges: baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown catalogue entries: foo, baz"` + ); + }); - it(`prevents privileges from specifying catalogue entries that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['bar'], - privileges: { - all: { - catalogue: ['foo', 'bar', 'baz'], - savedObject: { - all: [], - read: [], + it(`prevents features from specifying catalogue entries that don't exist at the privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['foo', 'bar', 'baz'], + privileges: { + all: { + catalogue: ['foo'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - ui: [], - app: [], - }, - read: { - catalogue: ['foo', 'bar', 'baz'], - savedObject: { - all: [], - read: [], + read: { + catalogue: ['foo'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - ui: [], - app: [], }, - }, - }; + subFeatures: [ + { + name: 'my sub feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cool-sub-feature-privilege', + name: 'cool privilege', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], + catalogue: ['bar'], + }, + ], + }, + ], + }, + ], + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.all has unknown catalogue entries: foo, baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies catalogue entries which are not granted to any privileges: baz"` + ); + }); - it(`prevents features from specifying catalogue entries that don't exist at the privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['foo', 'bar', 'baz'], - privileges: { - all: { - catalogue: ['foo'], - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, - read: { - catalogue: ['foo'], - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, - }, - subFeatures: [ - { - name: 'my sub feature', - privilegeGroups: [ + it(`prevents reserved privileges from specifying catalogue entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + privileges: null, + reserved: { + description: 'something', + privileges: [ { - groupType: 'independent', - privileges: [ - { - id: 'cool-sub-feature-privilege', - name: 'cool privilege', - includeIn: 'none', - savedObject: { - all: [], - read: [], - }, - ui: [], - catalogue: ['bar'], + id: 'reserved', + privilege: { + catalogue: ['foo', 'bar', 'baz'], + savedObject: { + all: [], + read: [], }, - ], + ui: [], + app: [], + }, }, ], }, - ], - }; + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies catalogue entries which are not granted to any privileges: baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown catalogue entries: foo, baz"` + ); + }); - it(`prevents reserved privileges from specifying catalogue entries that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['bar'], - privileges: null, - reserved: { - description: 'something', - privileges: [ - { - id: 'reserved', - privilege: { - catalogue: ['foo', 'bar', 'baz'], - savedObject: { - all: [], - read: [], + it(`prevents features from specifying catalogue entries that don't exist at the reserved privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['foo', 'bar', 'baz'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['foo', 'bar'], + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - ui: [], - app: [], }, - }, - ], - }, - }; + ], + }, + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.reserved has unknown catalogue entries: foo, baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies catalogue entries which are not granted to any privileges: baz"` + ); + }); - it(`prevents features from specifying catalogue entries that don't exist at the reserved privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['foo', 'bar', 'baz'], - privileges: null, - reserved: { - description: 'something', - privileges: [ - { - id: 'reserved', - privilege: { - catalogue: ['foo', 'bar'], - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], + it(`prevents privileges from specifying alerting entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['bar'], + privileges: { + all: { + alerting: { + all: ['foo', 'bar'], + read: ['baz'], + }, + savedObject: { + all: [], + read: [], }, + ui: [], + app: [], }, - ], - }, - }; + read: { + alerting: { read: ['foo', 'bar', 'baz'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies catalogue entries which are not granted to any privileges: baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown alerting entries: foo, baz"` + ); + }); - it(`prevents privileges from specifying alerting entries that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - alerting: ['bar'], - privileges: { - all: { - alerting: { - all: ['foo', 'bar'], - read: ['baz'], + it(`prevents features from specifying alerting entries that don't exist at the privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['foo', 'bar', 'baz'], + privileges: { + all: { + alerting: { all: ['foo'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - savedObject: { - all: [], - read: [], + read: { + alerting: { all: ['foo'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - ui: [], - app: [], }, - read: { - alerting: { read: ['foo', 'bar', 'baz'] }, - savedObject: { - all: [], - read: [], + subFeatures: [ + { + name: 'my sub feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cool-sub-feature-privilege', + name: 'cool privilege', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], + alerting: { all: ['bar'] }, + }, + ], + }, + ], }, - ui: [], - app: [], - }, - }, - }; + ], + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.all has unknown alerting entries: foo, baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` + ); + }); - it(`prevents features from specifying alerting entries that don't exist at the privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - alerting: ['foo', 'bar', 'baz'], - privileges: { - all: { - alerting: { all: ['foo'] }, - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, - read: { - alerting: { all: ['foo'] }, - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, - }, - subFeatures: [ - { - name: 'my sub feature', - privilegeGroups: [ + it(`prevents reserved privileges from specifying alerting entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['bar'], + privileges: null, + reserved: { + description: 'something', + privileges: [ { - groupType: 'independent', - privileges: [ - { - id: 'cool-sub-feature-privilege', - name: 'cool privilege', - includeIn: 'none', - savedObject: { - all: [], - read: [], - }, - ui: [], - alerting: { all: ['bar'] }, + id: 'reserved', + privilege: { + alerting: { all: ['foo', 'bar', 'baz'] }, + savedObject: { + all: [], + read: [], }, - ], + ui: [], + app: [], + }, }, ], }, - ], - }; + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown alerting entries: foo, baz"` + ); + }); - it(`prevents reserved privileges from specifying alerting entries that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - alerting: ['bar'], - privileges: null, - reserved: { - description: 'something', - privileges: [ - { - id: 'reserved', - privilege: { - alerting: { all: ['foo', 'bar', 'baz'] }, - savedObject: { - all: [], - read: [], + it(`prevents features from specifying alerting entries that don't exist at the reserved privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + alerting: ['foo', 'bar', 'baz'], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + alerting: { all: ['foo', 'bar'] }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - ui: [], - app: [], }, - }, - ], - }, - }; + ], + }, + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.reserved has unknown alerting entries: foo, baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` + ); + }); - it(`prevents features from specifying alerting entries that don't exist at the reserved privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - alerting: ['foo', 'bar', 'baz'], - privileges: null, - reserved: { - description: 'something', - privileges: [ - { - id: 'reserved', - privilege: { - alerting: { all: ['foo', 'bar'] }, - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], + it(`prevents privileges from specifying management sections that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + management: { + kibana: ['hey'], + }, + privileges: { + all: { + catalogue: ['bar'], + management: { + elasticsearch: ['hey'], + }, + savedObject: { + all: [], + read: [], }, + ui: [], + app: [], }, - ], - }, - }; + read: { + catalogue: ['bar'], + management: { + elasticsearch: ['hey'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies alerting entries which are not granted to any privileges: baz"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.all has unknown management section: elasticsearch"` + ); + }); - it(`prevents privileges from specifying management sections that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['bar'], - management: { - kibana: ['hey'], - }, - privileges: { - all: { - catalogue: ['bar'], - management: { - elasticsearch: ['hey'], + it(`prevents features from specifying management sections that don't exist at the privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + management: { + kibana: ['hey'], + elasticsearch: ['hey', 'there'], + }, + privileges: { + all: { + catalogue: ['bar'], + management: { + elasticsearch: ['hey'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - savedObject: { - all: [], - read: [], + read: { + catalogue: ['bar'], + management: { + elasticsearch: ['hey'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], }, - ui: [], - app: [], }, - read: { - catalogue: ['bar'], - management: { - elasticsearch: ['hey'], - }, - savedObject: { - all: [], - read: [], + subFeatures: [ + { + name: 'my sub feature', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + { + id: 'cool-sub-feature-privilege', + name: 'cool privilege', + includeIn: 'none', + savedObject: { + all: [], + read: [], + }, + ui: [], + management: { + kibana: ['hey'], + elasticsearch: ['hey'], + }, + }, + ], + }, + ], }, - ui: [], - app: [], + ], + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies management entries which are not granted to any privileges: elasticsearch.there"` + ); + }); + + it(`prevents reserved privileges from specifying management entries that don't exist at the root level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + management: { + kibana: ['hey'], }, - }, - }; + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['bar'], + management: { + kibana: ['hey-there'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.all has unknown management section: elasticsearch"` - ); - }); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature privilege test-feature.reserved has unknown management entries for section kibana: hey-there"` + ); + }); - it(`prevents features from specifying management sections that don't exist at the privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['bar'], - management: { - kibana: ['hey'], - elasticsearch: ['hey', 'there'], - }, - privileges: { - all: { - catalogue: ['bar'], - management: { - elasticsearch: ['hey'], - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], + it(`prevents features from specifying management entries that don't exist at the reserved privilege level`, () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + catalogue: ['bar'], + management: { + kibana: ['hey', 'hey-there'], }, - read: { - catalogue: ['bar'], - management: { - elasticsearch: ['hey'], - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], + privileges: null, + reserved: { + description: 'something', + privileges: [ + { + id: 'reserved', + privilege: { + catalogue: ['bar'], + management: { + kibana: ['hey-there'], + }, + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], }, - }, - subFeatures: [ - { - name: 'my sub feature', - privilegeGroups: [ + }; + + const featureRegistry = new FeatureRegistry(); + + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature specifies management entries which are not granted to any privileges: kibana.hey"` + ); + }); + + it('allows multiple reserved feature privileges to be registered', () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + reserved: { + description: 'my reserved privileges', + privileges: [ { - groupType: 'independent', - privileges: [ - { - id: 'cool-sub-feature-privilege', - name: 'cool privilege', - includeIn: 'none', - savedObject: { - all: [], - read: [], - }, - ui: [], - management: { - kibana: ['hey'], - elasticsearch: ['hey'], - }, + id: 'a_reserved_1', + privilege: { + savedObject: { + all: [], + read: [], }, - ], + ui: [], + app: [], + }, + }, + { + id: 'a_reserved_2', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, }, ], }, - ], - }; + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature); + const result = featureRegistry.getAllKibanaFeatures(); + expect(result).toHaveLength(1); + expect(result[0].reserved?.privileges).toHaveLength(2); + }); + + it('does not allow reserved privilege ids to start with "reserved_"', () => { + const feature: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + reserved: { + description: 'my reserved privileges', + privileges: [ + { + id: 'reserved_1', + privilege: { + savedObject: { + all: [], + read: [], + }, + ui: [], + app: [], + }, + }, + ], + }, + }; - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies management entries which are not granted to any privileges: elasticsearch.there"` - ); + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerKibanaFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"reserved\\" fails because [child \\"privileges\\" fails because [\\"privileges\\" at position 0 fails because [child \\"id\\" fails because [\\"id\\" with value \\"reserved_1\\" fails to match the required pattern: /^(?!reserved_)[a-zA-Z0-9_-]+$/]]]]"` + ); + }); + + it('cannot register feature after getAll has been called', () => { + const feature1: KibanaFeatureConfig = { + id: 'test-feature', + name: 'Test Feature', + app: [], + privileges: null, + }; + const feature2: KibanaFeatureConfig = { + id: 'test-feature-2', + name: 'Test Feature 2', + app: [], + privileges: null, + }; + + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerKibanaFeature(feature1); + featureRegistry.getAllKibanaFeatures(); + expect(() => { + featureRegistry.registerKibanaFeature(feature2); + }).toThrowErrorMatchingInlineSnapshot( + `"Features are locked, can't register new features. Attempt to register test-feature-2 failed."` + ); + }); }); - it(`prevents reserved privileges from specifying management entries that don't exist at the root level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['bar'], - management: { - kibana: ['hey'], - }, - privileges: null, - reserved: { - description: 'something', + describe('Elasticsearch Features', () => { + it('allows a minimal feature to be registered', () => { + const feature: ElasticsearchFeatureConfig = { + id: 'test-feature', privileges: [ { - id: 'reserved', - privilege: { - catalogue: ['bar'], - management: { - kibana: ['hey-there'], - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, + requiredClusterPrivileges: ['all'], + ui: [], }, ], - }, - }; + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerElasticsearchFeature(feature); + const result = featureRegistry.getAllElasticsearchFeatures(); + expect(result).toHaveLength(1); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature privilege test-feature.reserved has unknown management entries for section kibana: hey-there"` - ); - }); + // Should be the equal, but not the same instance (i.e., a defensive copy) + expect(result[0].toRaw()).not.toBe(feature); + expect(result[0].toRaw()).toEqual(feature); + }); - it(`prevents features from specifying management entries that don't exist at the reserved privilege level`, () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - catalogue: ['bar'], - management: { - kibana: ['hey', 'hey-there'], - }, - privileges: null, - reserved: { - description: 'something', + it('allows a complex feature to ge registered', () => { + const feature: ElasticsearchFeatureConfig = { + id: 'test-feature', + management: { + kibana: ['foo'], + data: ['bar'], + }, + catalogue: ['foo', 'bar'], privileges: [ { - id: 'reserved', - privilege: { - catalogue: ['bar'], - management: { - kibana: ['hey-there'], - }, - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], + requiredClusterPrivileges: ['monitor', 'manage'], + requiredIndexPrivileges: { + foo: ['read'], + bar: ['all'], + baz: ['view_index_metadata'], }, + ui: ['ui_a'], + }, + { + requiredClusterPrivileges: [], + requiredRoles: ['some_role'], + ui: ['ui_b'], }, ], - }, - }; + }; - const featureRegistry = new FeatureRegistry(); + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerElasticsearchFeature(feature); + const result = featureRegistry.getAllElasticsearchFeatures(); + expect(result).toHaveLength(1); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"Feature test-feature specifies management entries which are not granted to any privileges: kibana.hey"` - ); - }); + // Should be the equal, but not the same instance (i.e., a defensive copy) + expect(result[0].toRaw()).not.toBe(feature); + expect(result[0].toRaw()).toEqual(feature); + }); - it('allows multiple reserved feature privileges to be registered', () => { - const feature: FeatureConfig = { - id: 'test-feature', - name: 'Test Feature', - app: [], - privileges: null, - reserved: { - description: 'my reserved privileges', + it('requires a value for privileges', () => { + const feature: ElasticsearchFeatureConfig = { + id: 'test-feature', + } as any; + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerElasticsearchFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"child \\"privileges\\" fails because [\\"privileges\\" is required]"` + ); + }); + + it('requires privileges to declare some form of required es privileges', () => { + const feature: ElasticsearchFeatureConfig = { + id: 'test-feature', privileges: [ { - id: 'a_reserved_1', - privilege: { - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, + ui: [], }, + ], + } as any; + const featureRegistry = new FeatureRegistry(); + expect(() => + featureRegistry.registerElasticsearchFeature(feature) + ).toThrowErrorMatchingInlineSnapshot( + `"Feature test-feature has a privilege definition at index 0 without any privileges defined."` + ); + }); + + it('does not allow duplicate privilege ids', () => { + const feature: ElasticsearchFeatureConfig = { + id: 'test-feature', + privileges: [ { - id: 'a_reserved_2', - privilege: { - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, + requiredClusterPrivileges: ['all'], + ui: [], }, ], - }, - }; - - const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature); - const result = featureRegistry.getAll(); - expect(result).toHaveLength(1); - expect(result[0].reserved?.privileges).toHaveLength(2); + }; + const featureRegistry = new FeatureRegistry(); + featureRegistry.registerElasticsearchFeature(feature); + expect(() => + featureRegistry.registerElasticsearchFeature(feature) + ).toThrowErrorMatchingInlineSnapshot(`"Feature with id test-feature is already registered."`); + }); }); - it('does not allow reserved privilege ids to start with "reserved_"', () => { - const feature: FeatureConfig = { + it('does not allow a Kibana feature to share an id with an Elasticsearch feature', () => { + const kibanaFeature: KibanaFeatureConfig = { id: 'test-feature', name: 'Test Feature', app: [], privileges: null, - reserved: { - description: 'my reserved privileges', - privileges: [ - { - id: 'reserved_1', - privilege: { - savedObject: { - all: [], - read: [], - }, - ui: [], - app: [], - }, - }, - ], - }, + }; + + const elasticsearchFeature: ElasticsearchFeatureConfig = { + id: 'test-feature', + privileges: [ + { + requiredClusterPrivileges: ['all'], + ui: [], + }, + ], }; const featureRegistry = new FeatureRegistry(); - expect(() => featureRegistry.register(feature)).toThrowErrorMatchingInlineSnapshot( - `"child \\"reserved\\" fails because [child \\"privileges\\" fails because [\\"privileges\\" at position 0 fails because [child \\"id\\" fails because [\\"id\\" with value \\"reserved_1\\" fails to match the required pattern: /^(?!reserved_)[a-zA-Z0-9_-]+$/]]]]"` - ); + featureRegistry.registerElasticsearchFeature(elasticsearchFeature); + expect(() => + featureRegistry.registerKibanaFeature(kibanaFeature) + ).toThrowErrorMatchingInlineSnapshot(`"Feature with id test-feature is already registered."`); }); - it('cannot register feature after getAll has been called', () => { - const feature1: FeatureConfig = { + it('does not allow an Elasticsearch feature to share an id with a Kibana feature', () => { + const kibanaFeature: KibanaFeatureConfig = { id: 'test-feature', name: 'Test Feature', app: [], privileges: null, }; - const feature2: FeatureConfig = { - id: 'test-feature-2', - name: 'Test Feature 2', - app: [], - privileges: null, + + const elasticsearchFeature: ElasticsearchFeatureConfig = { + id: 'test-feature', + privileges: [ + { + requiredClusterPrivileges: ['all'], + ui: [], + }, + ], }; const featureRegistry = new FeatureRegistry(); - featureRegistry.register(feature1); - featureRegistry.getAll(); - expect(() => { - featureRegistry.register(feature2); - }).toThrowErrorMatchingInlineSnapshot( - `"Features are locked, can't register new features. Attempt to register test-feature-2 failed."` - ); + featureRegistry.registerKibanaFeature(kibanaFeature); + expect(() => + featureRegistry.registerElasticsearchFeature(elasticsearchFeature) + ).toThrowErrorMatchingInlineSnapshot(`"Feature with id test-feature is already registered."`); }); }); diff --git a/x-pack/plugins/features/server/feature_registry.ts b/x-pack/plugins/features/server/feature_registry.ts index 12aafd226f754..d357bdb782797 100644 --- a/x-pack/plugins/features/server/feature_registry.ts +++ b/x-pack/plugins/features/server/feature_registry.ts @@ -5,38 +5,72 @@ */ import { cloneDeep, uniq } from 'lodash'; -import { FeatureConfig, Feature, FeatureKibanaPrivileges } from '../common'; -import { validateFeature } from './feature_schema'; +import { + KibanaFeatureConfig, + KibanaFeature, + FeatureKibanaPrivileges, + ElasticsearchFeatureConfig, + ElasticsearchFeature, +} from '../common'; +import { validateKibanaFeature, validateElasticsearchFeature } from './feature_schema'; export class FeatureRegistry { private locked = false; - private features: Record = {}; + private kibanaFeatures: Record = {}; + private esFeatures: Record = {}; - public register(feature: FeatureConfig) { + public registerKibanaFeature(feature: KibanaFeatureConfig) { if (this.locked) { throw new Error( `Features are locked, can't register new features. Attempt to register ${feature.id} failed.` ); } - validateFeature(feature); + validateKibanaFeature(feature); - if (feature.id in this.features) { + if (feature.id in this.kibanaFeatures || feature.id in this.esFeatures) { throw new Error(`Feature with id ${feature.id} is already registered.`); } const featureCopy = cloneDeep(feature); - this.features[feature.id] = applyAutomaticPrivilegeGrants(featureCopy); + this.kibanaFeatures[feature.id] = applyAutomaticPrivilegeGrants(featureCopy); } - public getAll(): Feature[] { + public registerElasticsearchFeature(feature: ElasticsearchFeatureConfig) { + if (this.locked) { + throw new Error( + `Features are locked, can't register new features. Attempt to register ${feature.id} failed.` + ); + } + + if (feature.id in this.kibanaFeatures || feature.id in this.esFeatures) { + throw new Error(`Feature with id ${feature.id} is already registered.`); + } + + validateElasticsearchFeature(feature); + + const featureCopy = cloneDeep(feature); + + this.esFeatures[feature.id] = featureCopy; + } + + public getAllKibanaFeatures(): KibanaFeature[] { + this.locked = true; + return Object.values(this.kibanaFeatures).map( + (featureConfig) => new KibanaFeature(featureConfig) + ); + } + + public getAllElasticsearchFeatures(): ElasticsearchFeature[] { this.locked = true; - return Object.values(this.features).map((featureConfig) => new Feature(featureConfig)); + return Object.values(this.esFeatures).map( + (featureConfig) => new ElasticsearchFeature(featureConfig) + ); } } -function applyAutomaticPrivilegeGrants(feature: FeatureConfig): FeatureConfig { +function applyAutomaticPrivilegeGrants(feature: KibanaFeatureConfig): KibanaFeatureConfig { const allPrivilege = feature.privileges?.all; const readPrivilege = feature.privileges?.read; const reservedPrivileges = (feature.reserved?.privileges ?? []).map((rp) => rp.privilege); diff --git a/x-pack/plugins/features/server/feature_schema.ts b/x-pack/plugins/features/server/feature_schema.ts index 95298603d706a..06a3eb158d99d 100644 --- a/x-pack/plugins/features/server/feature_schema.ts +++ b/x-pack/plugins/features/server/feature_schema.ts @@ -8,8 +8,8 @@ import Joi from 'joi'; import { difference } from 'lodash'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; -import { FeatureConfig } from '../common/feature'; -import { FeatureKibanaPrivileges } from '.'; +import { KibanaFeatureConfig } from '../common'; +import { FeatureKibanaPrivileges, ElasticsearchFeatureConfig } from '.'; // Each feature gets its own property on the UICapabilities object, // but that object has a few built-in properties which should not be overwritten. @@ -28,7 +28,7 @@ const managementSchema = Joi.object().pattern( const catalogueSchema = Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)); const alertingSchema = Joi.array().items(Joi.string()); -const privilegeSchema = Joi.object({ +const kibanaPrivilegeSchema = Joi.object({ excludeFromBasePrivileges: Joi.boolean(), management: managementSchema, catalogue: catalogueSchema, @@ -45,7 +45,7 @@ const privilegeSchema = Joi.object({ ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(), }); -const subFeaturePrivilegeSchema = Joi.object({ +const kibanaSubFeaturePrivilegeSchema = Joi.object({ id: Joi.string().regex(subFeaturePrivilegePartRegex).required(), name: Joi.string().required(), includeIn: Joi.string().allow('all', 'read', 'none').required(), @@ -64,17 +64,17 @@ const subFeaturePrivilegeSchema = Joi.object({ ui: Joi.array().items(Joi.string().regex(uiCapabilitiesRegex)).required(), }); -const subFeatureSchema = Joi.object({ +const kibanaSubFeatureSchema = Joi.object({ name: Joi.string().required(), privilegeGroups: Joi.array().items( Joi.object({ groupType: Joi.string().valid('mutually_exclusive', 'independent').required(), - privileges: Joi.array().items(subFeaturePrivilegeSchema).min(1), + privileges: Joi.array().items(kibanaSubFeaturePrivilegeSchema).min(1), }) ), }); -const schema = Joi.object({ +const kibanaFeatureSchema = Joi.object({ id: Joi.string() .regex(featurePrivilegePartRegex) .invalid(...prohibitedFeatureIds) @@ -93,15 +93,15 @@ const schema = Joi.object({ catalogue: catalogueSchema, alerting: alertingSchema, privileges: Joi.object({ - all: privilegeSchema, - read: privilegeSchema, + all: kibanaPrivilegeSchema, + read: kibanaPrivilegeSchema, }) .allow(null) .required(), subFeatures: Joi.when('privileges', { is: null, - then: Joi.array().items(subFeatureSchema).max(0), - otherwise: Joi.array().items(subFeatureSchema), + then: Joi.array().items(kibanaSubFeatureSchema).max(0), + otherwise: Joi.array().items(kibanaSubFeatureSchema), }), privilegesTooltip: Joi.string(), reserved: Joi.object({ @@ -110,15 +110,32 @@ const schema = Joi.object({ .items( Joi.object({ id: Joi.string().regex(reservedFeaturePrrivilegePartRegex).required(), - privilege: privilegeSchema.required(), + privilege: kibanaPrivilegeSchema.required(), }) ) .required(), }), }); -export function validateFeature(feature: FeatureConfig) { - const validateResult = Joi.validate(feature, schema); +const elasticsearchPrivilegeSchema = Joi.object({ + ui: Joi.array().items(Joi.string()).required(), + requiredClusterPrivileges: Joi.array().items(Joi.string()), + requiredIndexPrivileges: Joi.object().pattern(Joi.string(), Joi.array().items(Joi.string())), + requiredRoles: Joi.array().items(Joi.string()), +}); + +const elasticsearchFeatureSchema = Joi.object({ + id: Joi.string() + .regex(featurePrivilegePartRegex) + .invalid(...prohibitedFeatureIds) + .required(), + management: managementSchema, + catalogue: catalogueSchema, + privileges: Joi.array().items(elasticsearchPrivilegeSchema).required(), +}); + +export function validateKibanaFeature(feature: KibanaFeatureConfig) { + const validateResult = Joi.validate(feature, kibanaFeatureSchema); if (validateResult.error) { throw validateResult.error; } @@ -303,3 +320,29 @@ export function validateFeature(feature: FeatureConfig) { ); } } + +export function validateElasticsearchFeature(feature: ElasticsearchFeatureConfig) { + const validateResult = Joi.validate(feature, elasticsearchFeatureSchema); + if (validateResult.error) { + throw validateResult.error; + } + // the following validation can't be enforced by the Joi schema without a very convoluted and verbose definition + const { privileges } = feature; + privileges.forEach((privilege, index) => { + const { + requiredClusterPrivileges = [], + requiredIndexPrivileges = [], + requiredRoles = [], + } = privilege; + + if ( + requiredClusterPrivileges.length === 0 && + requiredIndexPrivileges.length === 0 && + requiredRoles.length === 0 + ) { + throw new Error( + `Feature ${feature.id} has a privilege definition at index ${index} without any privileges defined.` + ); + } + }); +} diff --git a/x-pack/plugins/features/server/index.ts b/x-pack/plugins/features/server/index.ts index 48a350ae8f8fd..28c0fee041594 100644 --- a/x-pack/plugins/features/server/index.ts +++ b/x-pack/plugins/features/server/index.ts @@ -13,7 +13,14 @@ import { Plugin } from './plugin'; // run-time contracts. export { uiCapabilitiesRegex } from './feature_schema'; -export { Feature, FeatureConfig, FeatureKibanaPrivileges } from '../common'; +export { + KibanaFeature, + KibanaFeatureConfig, + FeatureKibanaPrivileges, + ElasticsearchFeature, + ElasticsearchFeatureConfig, + FeatureElasticsearchPrivileges, +} from '../common'; export { PluginSetupContract, PluginStartContract } from './plugin'; export const plugin = (initializerContext: PluginInitializerContext) => diff --git a/x-pack/plugins/features/server/mocks.ts b/x-pack/plugins/features/server/mocks.ts index d9437169a7453..91c297c50e462 100644 --- a/x-pack/plugins/features/server/mocks.ts +++ b/x-pack/plugins/features/server/mocks.ts @@ -8,15 +8,18 @@ import { PluginSetupContract, PluginStartContract } from './plugin'; const createSetup = (): jest.Mocked => { return { - getFeatures: jest.fn(), + getKibanaFeatures: jest.fn(), + getElasticsearchFeatures: jest.fn(), getFeaturesUICapabilities: jest.fn(), - registerFeature: jest.fn(), + registerKibanaFeature: jest.fn(), + registerElasticsearchFeature: jest.fn(), }; }; const createStart = (): jest.Mocked => { return { - getFeatures: jest.fn(), + getKibanaFeatures: jest.fn(), + getElasticsearchFeatures: jest.fn(), }; }; diff --git a/x-pack/plugins/features/server/oss_features.test.ts b/x-pack/plugins/features/server/oss_features.test.ts index c38f2afc88389..961656aba8bfd 100644 --- a/x-pack/plugins/features/server/oss_features.test.ts +++ b/x-pack/plugins/features/server/oss_features.test.ts @@ -6,7 +6,7 @@ import { buildOSSFeatures } from './oss_features'; import { featurePrivilegeIterator } from '../../security/server/authorization'; -import { Feature } from '.'; +import { KibanaFeature } from '.'; describe('buildOSSFeatures', () => { it('returns features including timelion', () => { @@ -48,7 +48,7 @@ Array [ features.forEach((featureConfig) => { it(`returns the ${featureConfig.id} feature augmented with appropriate sub feature privileges`, () => { const privileges = []; - for (const featurePrivilege of featurePrivilegeIterator(new Feature(featureConfig), { + for (const featurePrivilege of featurePrivilegeIterator(new KibanaFeature(featureConfig), { augmentWithSubFeaturePrivileges: true, })) { privileges.push(featurePrivilege); diff --git a/x-pack/plugins/features/server/oss_features.ts b/x-pack/plugins/features/server/oss_features.ts index e37c7491de5dc..3ff6b1b7bf44f 100644 --- a/x-pack/plugins/features/server/oss_features.ts +++ b/x-pack/plugins/features/server/oss_features.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { i18n } from '@kbn/i18n'; -import { FeatureConfig } from '../common/feature'; +import { KibanaFeatureConfig } from '../common'; export interface BuildOSSFeaturesParams { savedObjectTypes: string[]; @@ -172,6 +172,7 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS 'visualization', 'timelion-sheet', 'canvas-workpad', + 'lens', 'map', 'dashboard', 'query', @@ -367,10 +368,10 @@ export const buildOSSFeatures = ({ savedObjectTypes, includeTimelion }: BuildOSS }, }, ...(includeTimelion ? [timelionFeature] : []), - ] as FeatureConfig[]; + ] as KibanaFeatureConfig[]; }; -const timelionFeature: FeatureConfig = { +const timelionFeature: KibanaFeatureConfig = { id: 'timelion', name: 'Timelion', order: 350, diff --git a/x-pack/plugins/features/server/plugin.test.ts b/x-pack/plugins/features/server/plugin.test.ts index 00d578f5ca866..ee11e0e2bbe2e 100644 --- a/x-pack/plugins/features/server/plugin.test.ts +++ b/x-pack/plugins/features/server/plugin.test.ts @@ -28,19 +28,19 @@ describe('Features Plugin', () => { coreStart.savedObjects.getTypeRegistry.mockReturnValue(typeRegistry); }); - it('returns OSS + registered features', async () => { + it('returns OSS + registered kibana features', async () => { const plugin = new Plugin(initContext); - const { registerFeature } = await plugin.setup(coreSetup, {}); - registerFeature({ + const { registerKibanaFeature } = await plugin.setup(coreSetup, {}); + registerKibanaFeature({ id: 'baz', name: 'baz', app: [], privileges: null, }); - const { getFeatures } = await plugin.start(coreStart); + const { getKibanaFeatures } = plugin.start(coreStart); - expect(getFeatures().map((f) => f.id)).toMatchInlineSnapshot(` + expect(getKibanaFeatures().map((f) => f.id)).toMatchInlineSnapshot(` Array [ "baz", "discover", @@ -54,9 +54,9 @@ describe('Features Plugin', () => { `); }); - it('returns OSS + registered features with timelion when available', async () => { + it('returns OSS + registered kibana features with timelion when available', async () => { const plugin = new Plugin(initContext); - const { registerFeature } = await plugin.setup(coreSetup, { + const { registerKibanaFeature: registerFeature } = await plugin.setup(coreSetup, { visTypeTimelion: { uiEnabled: true }, }); registerFeature({ @@ -66,9 +66,9 @@ describe('Features Plugin', () => { privileges: null, }); - const { getFeatures } = await plugin.start(coreStart); + const { getKibanaFeatures } = plugin.start(coreStart); - expect(getFeatures().map((f) => f.id)).toMatchInlineSnapshot(` + expect(getKibanaFeatures().map((f) => f.id)).toMatchInlineSnapshot(` Array [ "baz", "discover", @@ -83,19 +83,41 @@ describe('Features Plugin', () => { `); }); - it('registers not hidden saved objects types', async () => { + it('registers kibana features with not hidden saved objects types', async () => { const plugin = new Plugin(initContext); await plugin.setup(coreSetup, {}); - const { getFeatures } = await plugin.start(coreStart); + const { getKibanaFeatures } = plugin.start(coreStart); const soTypes = - getFeatures().find((f) => f.id === 'savedObjectsManagement')?.privileges?.all.savedObject - .all || []; + getKibanaFeatures().find((f) => f.id === 'savedObjectsManagement')?.privileges?.all + .savedObject.all || []; expect(soTypes.includes('foo')).toBe(true); expect(soTypes.includes('bar')).toBe(false); }); + it('returns registered elasticsearch features', async () => { + const plugin = new Plugin(initContext); + const { registerElasticsearchFeature } = await plugin.setup(coreSetup, {}); + registerElasticsearchFeature({ + id: 'baz', + privileges: [ + { + requiredClusterPrivileges: ['all'], + ui: ['baz-ui'], + }, + ], + }); + + const { getElasticsearchFeatures } = plugin.start(coreStart); + + expect(getElasticsearchFeatures().map((f) => f.id)).toMatchInlineSnapshot(` + Array [ + "baz", + ] + `); + }); + it('registers a capabilities provider', async () => { const plugin = new Plugin(initContext); await plugin.setup(coreSetup, {}); diff --git a/x-pack/plugins/features/server/plugin.ts b/x-pack/plugins/features/server/plugin.ts index 61b66d95ca44f..8a799887bba09 100644 --- a/x-pack/plugins/features/server/plugin.ts +++ b/x-pack/plugins/features/server/plugin.ts @@ -15,27 +15,40 @@ import { Capabilities as UICapabilities } from '../../../../src/core/server'; import { deepFreeze } from '../../../../src/core/server'; import { PluginSetupContract as TimelionSetupContract } from '../../../../src/plugins/vis_type_timelion/server'; import { FeatureRegistry } from './feature_registry'; -import { Feature, FeatureConfig } from '../common/feature'; import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features'; import { buildOSSFeatures } from './oss_features'; import { defineRoutes } from './routes'; +import { + ElasticsearchFeatureConfig, + ElasticsearchFeature, + KibanaFeature, + KibanaFeatureConfig, +} from '../common'; /** * Describes public Features plugin contract returned at the `setup` stage. */ export interface PluginSetupContract { - registerFeature(feature: FeatureConfig): void; + registerKibanaFeature(feature: KibanaFeatureConfig): void; + registerElasticsearchFeature(feature: ElasticsearchFeatureConfig): void; + /* + * Calling this function during setup will crash Kibana. + * Use start contract instead. + * @deprecated + * */ + getKibanaFeatures(): KibanaFeature[]; /* * Calling this function during setup will crash Kibana. * Use start contract instead. * @deprecated * */ - getFeatures(): Feature[]; + getElasticsearchFeatures(): ElasticsearchFeature[]; getFeaturesUICapabilities(): UICapabilities; } export interface PluginStartContract { - getFeatures(): Feature[]; + getElasticsearchFeatures(): ElasticsearchFeature[]; + getKibanaFeatures(): KibanaFeature[]; } /** @@ -62,13 +75,22 @@ export class Plugin { }); const getFeaturesUICapabilities = () => - uiCapabilitiesForFeatures(this.featureRegistry.getAll()); + uiCapabilitiesForFeatures( + this.featureRegistry.getAllKibanaFeatures(), + this.featureRegistry.getAllElasticsearchFeatures() + ); core.capabilities.registerProvider(getFeaturesUICapabilities); return deepFreeze({ - registerFeature: this.featureRegistry.register.bind(this.featureRegistry), - getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry), + registerKibanaFeature: this.featureRegistry.registerKibanaFeature.bind(this.featureRegistry), + registerElasticsearchFeature: this.featureRegistry.registerElasticsearchFeature.bind( + this.featureRegistry + ), + getKibanaFeatures: this.featureRegistry.getAllKibanaFeatures.bind(this.featureRegistry), + getElasticsearchFeatures: this.featureRegistry.getAllElasticsearchFeatures.bind( + this.featureRegistry + ), getFeaturesUICapabilities, }); } @@ -77,7 +99,10 @@ export class Plugin { this.registerOssFeatures(core.savedObjects); return deepFreeze({ - getFeatures: this.featureRegistry.getAll.bind(this.featureRegistry), + getElasticsearchFeatures: this.featureRegistry.getAllElasticsearchFeatures.bind( + this.featureRegistry + ), + getKibanaFeatures: this.featureRegistry.getAllKibanaFeatures.bind(this.featureRegistry), }); } @@ -98,7 +123,7 @@ export class Plugin { }); for (const feature of features) { - this.featureRegistry.register(feature); + this.featureRegistry.registerKibanaFeature(feature); } } } diff --git a/x-pack/plugins/features/server/routes/index.test.ts b/x-pack/plugins/features/server/routes/index.test.ts index 3d1efc8a479b2..30aa6d07f6b5a 100644 --- a/x-pack/plugins/features/server/routes/index.test.ts +++ b/x-pack/plugins/features/server/routes/index.test.ts @@ -11,7 +11,7 @@ import { httpServerMock, httpServiceMock, coreMock } from '../../../../../src/co import { LicenseType } from '../../../licensing/server/'; import { licensingMock } from '../../../licensing/server/mocks'; import { RequestHandler } from '../../../../../src/core/server'; -import { FeatureConfig } from '../../common'; +import { KibanaFeatureConfig } from '../../common'; function createContextMock(licenseType: LicenseType = 'gold') { return { @@ -24,14 +24,14 @@ describe('GET /api/features', () => { let routeHandler: RequestHandler; beforeEach(() => { const featureRegistry = new FeatureRegistry(); - featureRegistry.register({ + featureRegistry.registerKibanaFeature({ id: 'feature_1', name: 'Feature 1', app: [], privileges: null, }); - featureRegistry.register({ + featureRegistry.registerKibanaFeature({ id: 'feature_2', name: 'Feature 2', order: 2, @@ -39,7 +39,7 @@ describe('GET /api/features', () => { privileges: null, }); - featureRegistry.register({ + featureRegistry.registerKibanaFeature({ id: 'feature_3', name: 'Feature 2', order: 1, @@ -47,7 +47,7 @@ describe('GET /api/features', () => { privileges: null, }); - featureRegistry.register({ + featureRegistry.registerKibanaFeature({ id: 'licensed_feature', name: 'Licensed Feature', app: ['bar-app'], @@ -70,7 +70,7 @@ describe('GET /api/features', () => { expect(mockResponse.ok).toHaveBeenCalledTimes(1); const [call] = mockResponse.ok.mock.calls; - const body = call[0]!.body as FeatureConfig[]; + const body = call[0]!.body as KibanaFeatureConfig[]; const features = body.map((feature) => ({ id: feature.id, order: feature.order })); expect(features).toEqual([ @@ -99,7 +99,7 @@ describe('GET /api/features', () => { expect(mockResponse.ok).toHaveBeenCalledTimes(1); const [call] = mockResponse.ok.mock.calls; - const body = call[0]!.body as FeatureConfig[]; + const body = call[0]!.body as KibanaFeatureConfig[]; const features = body.map((feature) => ({ id: feature.id, order: feature.order })); @@ -129,7 +129,7 @@ describe('GET /api/features', () => { expect(mockResponse.ok).toHaveBeenCalledTimes(1); const [call] = mockResponse.ok.mock.calls; - const body = call[0]!.body as FeatureConfig[]; + const body = call[0]!.body as KibanaFeatureConfig[]; const features = body.map((feature) => ({ id: feature.id, order: feature.order })); @@ -159,7 +159,7 @@ describe('GET /api/features', () => { expect(mockResponse.ok).toHaveBeenCalledTimes(1); const [call] = mockResponse.ok.mock.calls; - const body = call[0]!.body as FeatureConfig[]; + const body = call[0]!.body as KibanaFeatureConfig[]; const features = body.map((feature) => ({ id: feature.id, order: feature.order })); diff --git a/x-pack/plugins/features/server/routes/index.ts b/x-pack/plugins/features/server/routes/index.ts index 147d34d124fca..b5a4203d7a768 100644 --- a/x-pack/plugins/features/server/routes/index.ts +++ b/x-pack/plugins/features/server/routes/index.ts @@ -26,7 +26,7 @@ export function defineRoutes({ router, featureRegistry }: RouteDefinitionParams) }, }, (context, request, response) => { - const allFeatures = featureRegistry.getAll(); + const allFeatures = featureRegistry.getAllKibanaFeatures(); return response.ok({ body: allFeatures diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts index 35dcc4cf42b37..7532bc0573b08 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.test.ts @@ -5,10 +5,10 @@ */ import { uiCapabilitiesForFeatures } from './ui_capabilities_for_features'; -import { Feature } from '.'; -import { SubFeaturePrivilegeGroupConfig } from '../common'; +import { KibanaFeature } from '.'; +import { SubFeaturePrivilegeGroupConfig, ElasticsearchFeature } from '../common'; -function createFeaturePrivilege(capabilities: string[] = []) { +function createKibanaFeaturePrivilege(capabilities: string[] = []) { return { savedObject: { all: [], @@ -19,7 +19,7 @@ function createFeaturePrivilege(capabilities: string[] = []) { }; } -function createSubFeaturePrivilege(privilegeId: string, capabilities: string[] = []) { +function createKibanaSubFeaturePrivilege(privilegeId: string, capabilities: string[] = []) { return { id: privilegeId, name: `sub-feature privilege ${privilegeId}`, @@ -35,44 +35,101 @@ function createSubFeaturePrivilege(privilegeId: string, capabilities: string[] = describe('populateUICapabilities', () => { it('handles no original uiCapabilities and no registered features gracefully', () => { - expect(uiCapabilitiesForFeatures([])).toEqual({}); + expect(uiCapabilitiesForFeatures([], [])).toEqual({}); }); - it('handles features with no registered capabilities', () => { + it('handles kibana features with no registered capabilities', () => { expect( - uiCapabilitiesForFeatures([ - new Feature({ - id: 'newFeature', - name: 'my new feature', - app: ['bar-app'], - privileges: { - all: createFeaturePrivilege(), - read: createFeaturePrivilege(), - }, - }), - ]) + uiCapabilitiesForFeatures( + [ + new KibanaFeature({ + id: 'newFeature', + name: 'my new feature', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(), + read: createKibanaFeaturePrivilege(), + }, + }), + ], + [] + ) + ).toEqual({ + catalogue: {}, + management: {}, + newFeature: {}, + }); + }); + + it('handles elasticsearch features with no registered capabilities', () => { + expect( + uiCapabilitiesForFeatures( + [], + [ + new ElasticsearchFeature({ + id: 'newFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: [], + }, + ], + }), + ] + ) ).toEqual({ catalogue: {}, + management: {}, newFeature: {}, }); }); - it('augments the original uiCapabilities with registered feature capabilities', () => { + it('augments the original uiCapabilities with registered kibana feature capabilities', () => { + expect( + uiCapabilitiesForFeatures( + [ + new KibanaFeature({ + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(), + }, + }), + ], + [] + ) + ).toEqual({ + catalogue: {}, + management: {}, + newFeature: { + capability1: true, + capability2: true, + }, + }); + }); + + it('augments the original uiCapabilities with registered elasticsearch feature capabilities', () => { expect( - uiCapabilitiesForFeatures([ - new Feature({ - id: 'newFeature', - name: 'my new feature', - navLinkId: 'newFeatureNavLink', - app: ['bar-app'], - privileges: { - all: createFeaturePrivilege(['capability1', 'capability2']), - read: createFeaturePrivilege(), - }, - }), - ]) + uiCapabilitiesForFeatures( + [], + [ + new ElasticsearchFeature({ + id: 'newFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: ['capability1', 'capability2'], + }, + ], + }), + ] + ) ).toEqual({ catalogue: {}, + management: {}, newFeature: { capability1: true, capability2: true, @@ -80,26 +137,66 @@ describe('populateUICapabilities', () => { }); }); - it('combines catalogue entries from multiple features', () => { + it('combines catalogue entries from multiple kibana features', () => { expect( - uiCapabilitiesForFeatures([ - new Feature({ - id: 'newFeature', - name: 'my new feature', - navLinkId: 'newFeatureNavLink', - app: ['bar-app'], - catalogue: ['anotherFooEntry', 'anotherBarEntry'], - privileges: { - all: createFeaturePrivilege(['capability1', 'capability2']), - read: createFeaturePrivilege(['capability3', 'capability4']), - }, - }), - ]) + uiCapabilitiesForFeatures( + [ + new KibanaFeature({ + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + catalogue: ['anotherFooEntry', 'anotherBarEntry'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(['capability3', 'capability4']), + }, + }), + ], + [] + ) ).toEqual({ catalogue: { anotherFooEntry: true, anotherBarEntry: true, }, + management: {}, + newFeature: { + capability1: true, + capability2: true, + capability3: true, + capability4: true, + }, + }); + }); + + it('combines catalogue entries from multiple elasticsearch privileges', () => { + expect( + uiCapabilitiesForFeatures( + [], + [ + new ElasticsearchFeature({ + id: 'newFeature', + catalogue: ['anotherFooEntry', 'anotherBarEntry'], + privileges: [ + { + requiredClusterPrivileges: [], + ui: ['capability1', 'capability2'], + }, + { + requiredClusterPrivileges: [], + ui: ['capability3', 'capability4'], + }, + ], + }), + ] + ) + ).toEqual({ + catalogue: { + anotherFooEntry: true, + anotherBarEntry: true, + }, + management: {}, newFeature: { capability1: true, capability2: true, @@ -111,20 +208,24 @@ describe('populateUICapabilities', () => { it(`merges capabilities from all feature privileges`, () => { expect( - uiCapabilitiesForFeatures([ - new Feature({ - id: 'newFeature', - name: 'my new feature', - navLinkId: 'newFeatureNavLink', - app: ['bar-app'], - privileges: { - all: createFeaturePrivilege(['capability1', 'capability2']), - read: createFeaturePrivilege(['capability3', 'capability4', 'capability5']), - }, - }), - ]) + uiCapabilitiesForFeatures( + [ + new KibanaFeature({ + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(['capability3', 'capability4', 'capability5']), + }, + }), + ], + [] + ) ).toEqual({ catalogue: {}, + management: {}, newFeature: { capability1: true, capability2: true, @@ -137,30 +238,38 @@ describe('populateUICapabilities', () => { it(`supports capabilities from reserved privileges`, () => { expect( - uiCapabilitiesForFeatures([ - new Feature({ - id: 'newFeature', - name: 'my new feature', - navLinkId: 'newFeatureNavLink', - app: ['bar-app'], - privileges: null, - reserved: { - description: '', - privileges: [ - { - id: 'rp_1', - privilege: createFeaturePrivilege(['capability1', 'capability2']), - }, - { - id: 'rp_2', - privilege: createFeaturePrivilege(['capability3', 'capability4', 'capability5']), - }, - ], - }, - }), - ]) + uiCapabilitiesForFeatures( + [ + new KibanaFeature({ + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: null, + reserved: { + description: '', + privileges: [ + { + id: 'rp_1', + privilege: createKibanaFeaturePrivilege(['capability1', 'capability2']), + }, + { + id: 'rp_2', + privilege: createKibanaFeaturePrivilege([ + 'capability3', + 'capability4', + 'capability5', + ]), + }, + ], + }, + }), + ], + [] + ) ).toEqual({ catalogue: {}, + management: {}, newFeature: { capability1: true, capability2: true, @@ -173,53 +282,60 @@ describe('populateUICapabilities', () => { it(`supports merging features with sub privileges`, () => { expect( - uiCapabilitiesForFeatures([ - new Feature({ - id: 'newFeature', - name: 'my new feature', - navLinkId: 'newFeatureNavLink', - app: ['bar-app'], - privileges: { - all: createFeaturePrivilege(['capability1', 'capability2']), - read: createFeaturePrivilege(['capability3', 'capability4']), - }, - subFeatures: [ - { - name: 'sub-feature-1', - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - createSubFeaturePrivilege('privilege-1', ['capability5']), - createSubFeaturePrivilege('privilege-2', ['capability6']), - ], - } as SubFeaturePrivilegeGroupConfig, - { - groupType: 'mutually_exclusive', - privileges: [ - createSubFeaturePrivilege('privilege-3', ['capability7']), - createSubFeaturePrivilege('privilege-4', ['capability8']), - ], - } as SubFeaturePrivilegeGroupConfig, - ], + uiCapabilitiesForFeatures( + [ + new KibanaFeature({ + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(['capability3', 'capability4']), }, - { - name: 'sub-feature-2', - privilegeGroups: [ - { - name: 'Group Name', - groupType: 'independent', - privileges: [ - createSubFeaturePrivilege('privilege-5', ['capability9', 'capability10']), - ], - } as SubFeaturePrivilegeGroupConfig, - ], - }, - ], - }), - ]) + subFeatures: [ + { + name: 'sub-feature-1', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + createKibanaSubFeaturePrivilege('privilege-1', ['capability5']), + createKibanaSubFeaturePrivilege('privilege-2', ['capability6']), + ], + } as SubFeaturePrivilegeGroupConfig, + { + groupType: 'mutually_exclusive', + privileges: [ + createKibanaSubFeaturePrivilege('privilege-3', ['capability7']), + createKibanaSubFeaturePrivilege('privilege-4', ['capability8']), + ], + } as SubFeaturePrivilegeGroupConfig, + ], + }, + { + name: 'sub-feature-2', + privilegeGroups: [ + { + name: 'Group Name', + groupType: 'independent', + privileges: [ + createKibanaSubFeaturePrivilege('privilege-5', [ + 'capability9', + 'capability10', + ]), + ], + } as SubFeaturePrivilegeGroupConfig, + ], + }, + ], + }), + ], + [] + ) ).toEqual({ catalogue: {}, + management: {}, newFeature: { capability1: true, capability2: true, @@ -235,53 +351,132 @@ describe('populateUICapabilities', () => { }); }); - it('supports merging multiple features with multiple privileges each', () => { + it('supports merging multiple kibana features with multiple privileges each', () => { expect( - uiCapabilitiesForFeatures([ - new Feature({ - id: 'newFeature', - name: 'my new feature', - navLinkId: 'newFeatureNavLink', - app: ['bar-app'], - privileges: { - all: createFeaturePrivilege(['capability1', 'capability2']), - read: createFeaturePrivilege(['capability3', 'capability4']), - }, - }), - new Feature({ - id: 'anotherNewFeature', - name: 'another new feature', - app: ['bar-app'], - privileges: { - all: createFeaturePrivilege(['capability1', 'capability2']), - read: createFeaturePrivilege(['capability3', 'capability4']), - }, - }), - new Feature({ - id: 'yetAnotherNewFeature', - name: 'yet another new feature', - navLinkId: 'yetAnotherNavLink', - app: ['bar-app'], - privileges: { - all: createFeaturePrivilege(['capability1', 'capability2']), - read: createFeaturePrivilege(['something1', 'something2', 'something3']), - }, - subFeatures: [ - { - name: 'sub-feature-1', - privilegeGroups: [ - { - groupType: 'independent', - privileges: [ - createSubFeaturePrivilege('privilege-1', ['capability3']), - createSubFeaturePrivilege('privilege-2', ['capability4']), - ], - } as SubFeaturePrivilegeGroupConfig, - ], + uiCapabilitiesForFeatures( + [ + new KibanaFeature({ + id: 'newFeature', + name: 'my new feature', + navLinkId: 'newFeatureNavLink', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(['capability3', 'capability4']), + }, + }), + new KibanaFeature({ + id: 'anotherNewFeature', + name: 'another new feature', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(['capability3', 'capability4']), + }, + }), + new KibanaFeature({ + id: 'yetAnotherNewFeature', + name: 'yet another new feature', + navLinkId: 'yetAnotherNavLink', + app: ['bar-app'], + privileges: { + all: createKibanaFeaturePrivilege(['capability1', 'capability2']), + read: createKibanaFeaturePrivilege(['something1', 'something2', 'something3']), }, - ], - }), - ]) + subFeatures: [ + { + name: 'sub-feature-1', + privilegeGroups: [ + { + groupType: 'independent', + privileges: [ + createKibanaSubFeaturePrivilege('privilege-1', ['capability3']), + createKibanaSubFeaturePrivilege('privilege-2', ['capability4']), + ], + } as SubFeaturePrivilegeGroupConfig, + ], + }, + ], + }), + ], + [] + ) + ).toEqual({ + anotherNewFeature: { + capability1: true, + capability2: true, + capability3: true, + capability4: true, + }, + catalogue: {}, + management: {}, + newFeature: { + capability1: true, + capability2: true, + capability3: true, + capability4: true, + }, + yetAnotherNewFeature: { + capability1: true, + capability2: true, + capability3: true, + capability4: true, + something1: true, + something2: true, + something3: true, + }, + }); + }); + + it('supports merging multiple elasticsearch features with multiple privileges each', () => { + expect( + uiCapabilitiesForFeatures( + [], + [ + new ElasticsearchFeature({ + id: 'newFeature', + + privileges: [ + { + requiredClusterPrivileges: [], + ui: ['capability1', 'capability2'], + }, + { + requiredClusterPrivileges: [], + ui: ['capability3', 'capability4'], + }, + ], + }), + new ElasticsearchFeature({ + id: 'anotherNewFeature', + + privileges: [ + { + requiredClusterPrivileges: [], + ui: ['capability1', 'capability2'], + }, + { + requiredClusterPrivileges: [], + ui: ['capability3', 'capability4'], + }, + ], + }), + new ElasticsearchFeature({ + id: 'yetAnotherNewFeature', + + privileges: [ + { + requiredClusterPrivileges: [], + ui: ['capability1', 'capability2', 'capability3', 'capability4'], + }, + { + requiredClusterPrivileges: [], + ui: ['something1', 'something2', 'something3'], + }, + ], + }), + ] + ) ).toEqual({ anotherNewFeature: { capability1: true, @@ -290,6 +485,7 @@ describe('populateUICapabilities', () => { capability4: true, }, catalogue: {}, + management: {}, newFeature: { capability1: true, capability2: true, diff --git a/x-pack/plugins/features/server/ui_capabilities_for_features.ts b/x-pack/plugins/features/server/ui_capabilities_for_features.ts index 2570d4540b6a6..d582dbfdab50c 100644 --- a/x-pack/plugins/features/server/ui_capabilities_for_features.ts +++ b/x-pack/plugins/features/server/ui_capabilities_for_features.ts @@ -5,22 +5,35 @@ */ import _ from 'lodash'; +import { RecursiveReadonly } from '@kbn/utility-types'; import { Capabilities as UICapabilities } from '../../../../src/core/server'; -import { Feature } from '../common/feature'; +import { ElasticsearchFeature, KibanaFeature } from '../common'; const ELIGIBLE_FLAT_MERGE_KEYS = ['catalogue'] as const; +const ELIGIBLE_DEEP_MERGE_KEYS = ['management'] as const; interface FeatureCapabilities { [featureId: string]: Record; } -export function uiCapabilitiesForFeatures(features: Feature[]): UICapabilities { - const featureCapabilities: FeatureCapabilities[] = features.map(getCapabilitiesFromFeature); +export function uiCapabilitiesForFeatures( + kibanaFeatures: KibanaFeature[], + elasticsearchFeatures: ElasticsearchFeature[] +): UICapabilities { + const kibanaFeatureCapabilities = kibanaFeatures.map(getCapabilitiesFromFeature); + const elasticsearchFeatureCapabilities = elasticsearchFeatures.map(getCapabilitiesFromFeature); - return buildCapabilities(...featureCapabilities); + return buildCapabilities(...kibanaFeatureCapabilities, ...elasticsearchFeatureCapabilities); } -function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { +function getCapabilitiesFromFeature( + feature: + | Pick< + KibanaFeature, + 'id' | 'catalogue' | 'management' | 'privileges' | 'subFeatures' | 'reserved' + > + | Pick +): FeatureCapabilities { const UIFeatureCapabilities: FeatureCapabilities = { catalogue: {}, [feature.id]: {}, @@ -39,14 +52,34 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { }; } - const featurePrivileges = Object.values(feature.privileges ?? {}); - if (feature.subFeatures) { - featurePrivileges.push( - ...feature.subFeatures.map((sf) => sf.privilegeGroups.map((pg) => pg.privileges)).flat(2) - ); + if (feature.management) { + const sectionEntries = Object.entries(feature.management); + UIFeatureCapabilities.management = sectionEntries.reduce((acc, [sectionId, sectionItems]) => { + return { + ...acc, + [sectionId]: sectionItems.reduce((acc2, item) => { + return { + ...acc2, + [item]: true, + }; + }, {}), + }; + }, {}); } - if (feature.reserved?.privileges) { - featurePrivileges.push(...feature.reserved.privileges.map((rp) => rp.privilege)); + + const featurePrivileges = Object.values(feature.privileges ?? {}) as Writable< + Array<{ ui: RecursiveReadonly }> + >; + + if (isKibanaFeature(feature)) { + if (feature.subFeatures) { + featurePrivileges.push( + ...feature.subFeatures.map((sf) => sf.privilegeGroups.map((pg) => pg.privileges)).flat(2) + ); + } + if (feature.reserved?.privileges) { + featurePrivileges.push(...feature.reserved.privileges.map((rp) => rp.privilege)); + } } featurePrivileges.forEach((privilege) => { @@ -65,6 +98,20 @@ function getCapabilitiesFromFeature(feature: Feature): FeatureCapabilities { return UIFeatureCapabilities; } +function isKibanaFeature( + feature: Partial | Partial +): feature is KibanaFeature { + // Elasticsearch features define privileges as an array, + // whereas Kibana features define privileges as an object, + // or they define reserved privileges, or they don't define either. + // Elasticsearch features are required to defined privileges. + return ( + (feature as any).reserved != null || + (feature.privileges && !Array.isArray(feature.privileges)) || + feature.privileges === null + ); +} + function buildCapabilities(...allFeatureCapabilities: FeatureCapabilities[]): UICapabilities { return allFeatureCapabilities.reduce((acc, capabilities) => { const mergableCapabilities = _.omit(capabilities, ...ELIGIBLE_FLAT_MERGE_KEYS); @@ -81,6 +128,14 @@ function buildCapabilities(...allFeatureCapabilities: FeatureCapabilities[]): UI }; }); + ELIGIBLE_DEEP_MERGE_KEYS.forEach((key) => { + mergedFeatureCapabilities[key] = _.merge( + {}, + mergedFeatureCapabilities[key], + capabilities[key] + ); + }); + return mergedFeatureCapabilities; }, {} as UICapabilities); } diff --git a/x-pack/plugins/graph/server/plugin.ts b/x-pack/plugins/graph/server/plugin.ts index b2b825fa4683b..d69c592655fb5 100644 --- a/x-pack/plugins/graph/server/plugin.ts +++ b/x-pack/plugins/graph/server/plugin.ts @@ -41,7 +41,7 @@ export class GraphPlugin implements Plugin { } if (features) { - features.registerFeature({ + features.registerKibanaFeature({ id: 'graph', name: i18n.translate('xpack.graph.featureRegistry.graphFeatureName', { defaultMessage: 'Graph', diff --git a/x-pack/plugins/index_lifecycle_management/kibana.json b/x-pack/plugins/index_lifecycle_management/kibana.json index f899287642786..479d651fc6698 100644 --- a/x-pack/plugins/index_lifecycle_management/kibana.json +++ b/x-pack/plugins/index_lifecycle_management/kibana.json @@ -5,7 +5,8 @@ "ui": true, "requiredPlugins": [ "licensing", - "management" + "management", + "features" ], "optionalPlugins": [ "usageCollection", diff --git a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx index 6f80afccbff5e..6a22d8716514c 100644 --- a/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx +++ b/x-pack/plugins/index_lifecycle_management/public/application/sections/edit_policy/components/node_allocation.tsx @@ -52,7 +52,7 @@ export const NodeAllocation = ({ phaseData, isShowingErrors, }: React.PropsWithChildren>) => { - const { isLoading, data: nodes, error, sendRequest } = useLoadNodes(); + const { isLoading, data: nodes, error, resendRequest } = useLoadNodes(); const [selectedNodeAttrsForDetails, setSelectedNodeAttrsForDetails] = useState( null @@ -84,7 +84,7 @@ export const NodeAllocation = ({

{message} ({statusCode})

- + = ({ close, selectedNodeAttrs }) => { - const { data, isLoading, error, sendRequest } = useLoadNodeDetails(selectedNodeAttrs); + const { data, isLoading, error, resendRequest } = useLoadNodeDetails(selectedNodeAttrs); let content; if (isLoading) { content = ; @@ -47,7 +47,7 @@ export const NodeAttrsDetails: React.FunctionComponent = ({ close, select

{message} ({statusCode})

- + = ({ onChange, getUrlForApp, }) => { - const { error, isLoading, data, sendRequest } = useLoadSnapshotPolicies(); + const { error, isLoading, data, resendRequest } = useLoadSnapshotPolicies(); const policies = data.map((name: string) => ({ label: name, @@ -75,7 +75,7 @@ export const SnapshotPolicies: React.FunctionComponent = ({ { - const { error, isLoading, data: policies, sendRequest } = useLoadPoliciesList(false); + const { error, isLoading, data: policies, resendRequest } = useLoadPoliciesList(false); if (isLoading) { return ( } actions={ - + = navigateToApp, history, }) => { - const { data: policies, isLoading, error, sendRequest } = useLoadPoliciesList(true); + const { data: policies, isLoading, error, resendRequest } = useLoadPoliciesList(true); if (isLoading) { return ( @@ -53,7 +53,7 @@ export const PolicyTable: React.FunctionComponent =

} actions={ - + = policies={policies || []} history={history} navigateToApp={navigateToApp} - updatePolicies={sendRequest} + updatePolicies={resendRequest} /> ); }; diff --git a/x-pack/plugins/index_lifecycle_management/server/plugin.ts b/x-pack/plugins/index_lifecycle_management/server/plugin.ts index 76d8539eb4a07..3075f9c89eb8d 100644 --- a/x-pack/plugins/index_lifecycle_management/server/plugin.ts +++ b/x-pack/plugins/index_lifecycle_management/server/plugin.ts @@ -60,7 +60,10 @@ export class IndexLifecycleManagementServerPlugin implements Plugin { + async setup( + { http }: CoreSetup, + { licensing, indexManagement, features }: Dependencies + ): Promise { const router = http.createRouter(); const config = await this.config$.pipe(first()).toPromise(); @@ -78,6 +81,19 @@ export class IndexLifecycleManagementServerPlugin implements Plugin = ({ } = useGlobalFlyout(); const { api, trackMetric, documentation } = useComponentTemplatesContext(); - const { data, isLoading, error, sendRequest } = api.useLoadComponentTemplates(); + const { data, isLoading, error, resendRequest } = api.useLoadComponentTemplates(); const [componentTemplatesToDelete, setComponentTemplatesToDelete] = useState([]); @@ -170,7 +170,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ = ({ } else if (data && data.length === 0) { content = ; } else if (error) { - content = ; + content = ; } return ( @@ -194,7 +194,7 @@ export const ComponentTemplateList: React.FunctionComponent = ({ callback={(deleteResponse) => { if (deleteResponse?.hasDeletedComponentTemplates) { // refetch the component templates - sendRequest(); + resendRequest(); // go back to list view (if deleted from details flyout) goToComponentTemplateList(); } diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_above_parameter.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_above_parameter.tsx new file mode 100644 index 0000000000000..48a8e42f5065d --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/ignore_above_parameter.tsx @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; + +import { i18n } from '@kbn/i18n'; + +import { documentationService } from '../../../../../services/documentation'; +import { getFieldConfig } from '../../../lib'; +import { UseField, Field } from '../../../shared_imports'; +import { EditFieldFormRow } from '../fields/edit_field'; + +interface Props { + defaultToggleValue: boolean; +} + +export const IgnoreAboveParameter: FunctionComponent = ({ defaultToggleValue }) => ( + + + +); diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts index 805a6b6ece705..a2d5c7c8d5308 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/field_parameters/index.ts @@ -69,6 +69,8 @@ export * from './other_type_name_parameter'; export * from './other_type_json_parameter'; +export * from './ignore_above_parameter'; + export const PARAMETER_SERIALIZERS = [relationsSerializer, dynamicSerializer]; export const PARAMETER_DESERIALIZERS = [relationsDeserializer, dynamicDeserializer]; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx index 7c8ac86f14153..e96426ece27e8 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/flattened_type.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { i18n } from '@kbn/i18n'; -import { documentationService } from '../../../../../../services/documentation'; import { NormalizedField, Field as FieldType } from '../../../../types'; import { UseField, Field } from '../../../../shared_imports'; import { getFieldConfig } from '../../../../lib'; @@ -19,6 +18,7 @@ import { NullValueParameter, SimilarityParameter, SplitQueriesOnWhitespaceParameter, + IgnoreAboveParameter, } from '../../field_parameters'; import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field'; @@ -29,6 +29,7 @@ interface Props { const getDefaultToggleValue = (param: string, field: FieldType) => { switch (param) { case 'boost': + case 'ignore_above': case 'similarity': { return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; } @@ -66,28 +67,9 @@ export const FlattenedType = React.memo(({ field }: Props) => { - {/* ignore_above */} - - - + /> diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts index 20623b9d7e62b..d84d9c6ea40cf 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/index.ts @@ -28,6 +28,8 @@ import { ObjectType } from './object_type'; import { OtherType } from './other_type'; import { NestedType } from './nested_type'; import { JoinType } from './join_type'; +import { RankFeatureType } from './rank_feature_type'; +import { WildcardType } from './wildcard_type'; const typeToParametersFormMap: { [key in DataType]?: ComponentType } = { alias: AliasType, @@ -52,6 +54,8 @@ const typeToParametersFormMap: { [key in DataType]?: ComponentType } = { other: OtherType, nested: NestedType, join: JoinType, + rank_feature: RankFeatureType, + wildcard: WildcardType, }; export const getParametersFormForType = ( diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx index 43377357f1e6f..dc4f4b3ba5ff1 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/keyword_type.tsx @@ -23,6 +23,7 @@ import { SimilarityParameter, CopyToParameter, SplitQueriesOnWhitespaceParameter, + IgnoreAboveParameter, } from '../../field_parameters'; import { BasicParametersSection, EditFieldFormRow, AdvancedParametersSection } from '../edit_field'; @@ -79,25 +80,9 @@ export const KeywordType = ({ field }: Props) => { - {/* ignore_above */} - - - + /> diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/rank_feature_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/rank_feature_type.tsx new file mode 100644 index 0000000000000..136a83c6d17fb --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/rank_feature_type.tsx @@ -0,0 +1,32 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { i18n } from '@kbn/i18n'; + +import { BasicParametersSection, EditFieldFormRow } from '../edit_field'; + +export const RankFeatureType = () => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/wildcard_type.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/wildcard_type.tsx new file mode 100644 index 0000000000000..825b9e17c8d2c --- /dev/null +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/components/document_fields/fields/field_types/wildcard_type.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; + +import { NormalizedField, Field as FieldType, ParameterName } from '../../../../types'; +import { getFieldConfig } from '../../../../lib'; +import { IgnoreAboveParameter } from '../../field_parameters'; +import { AdvancedParametersSection } from '../edit_field'; + +interface Props { + field: NormalizedField; +} + +const getDefaultToggleValue = (param: ParameterName, field: FieldType) => { + return field[param] !== undefined && field[param] !== getFieldConfig(param).defaultValue; +}; + +export const WildcardType = ({ field }: Props) => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx index edfb6903a8585..a8844c7a9b270 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/data_types_definition.tsx @@ -784,6 +784,23 @@ export const TYPE_DEFINITION: { [key in DataType]: DataTypeDefinition } = {

), }, + wildcard: { + label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.wildcardDescription', { + defaultMessage: 'Wildcard', + }), + value: 'wildcard', + documentation: { + main: '/keyword.html#wildcard-field-type', + }, + description: () => ( +

+ +

+ ), + }, other: { label: i18n.translate('xpack.idxMgmt.mappingsEditor.dataType.otherDescription', { defaultMessage: 'Other', @@ -825,6 +842,7 @@ export const MAIN_TYPES: MainType[] = [ 'shape', 'text', 'token_count', + 'wildcard', 'other', ]; diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx index c7529ff272e22..f2148f1f657a6 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/constants/parameters_definition.tsx @@ -692,6 +692,12 @@ export const PARAMETERS_DEFINITION: { [key in ParameterName]: ParameterDefinitio }, schema: t.boolean, }, + positive_score_impact: { + fieldConfig: { + defaultValue: true, + }, + schema: t.boolean, + }, preserve_separators: { fieldConfig: { defaultValue: true, diff --git a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts index a9f6d2ea03bdf..fd0e4ed32bfe8 100644 --- a/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts +++ b/x-pack/plugins/index_management/public/application/components/mappings_editor/types/document_fields.ts @@ -24,7 +24,7 @@ export interface DataTypeDefinition { export interface ParameterDefinition { title?: string; description?: JSX.Element | string; - fieldConfig: FieldConfig; + fieldConfig: FieldConfig; schema?: any; props?: { [key: string]: ParameterDefinition }; documentation?: { @@ -59,6 +59,7 @@ export type MainType = | 'geo_point' | 'geo_shape' | 'token_count' + | 'wildcard' /** * 'other' is a special type that only exists inside of MappingsEditor as a placeholder * for undocumented field types. @@ -124,6 +125,7 @@ export type ParameterName = | 'eager_global_ordinals_join' | 'index_prefixes' | 'index_phrases' + | 'positive_score_impact' | 'norms' | 'norms_keyword' | 'term_vector' diff --git a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx index d37576f18e849..4f2a5c4a27b7a 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/data_stream_list/data_stream_list.tsx @@ -49,7 +49,7 @@ export const DataStreamList: React.FunctionComponent {}; + reload: UseRequestResponse['resendRequest']; history: ScopedHistory; includeStats: boolean; filters?: string; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx index 9203e76fce787..7ec6f1f94a2ab 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/legacy_templates/template_table/template_table.tsx @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiInMemoryTable, EuiButton, EuiLink, EuiBasicTableColumn } from '@elastic/eui'; import { ScopedHistory } from 'kibana/public'; -import { SendRequestResponse, reactRouterNavigate } from '../../../../../../shared_imports'; +import { UseRequestResponse, reactRouterNavigate } from '../../../../../../shared_imports'; import { TemplateListItem } from '../../../../../../../common'; import { UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../../../common/constants'; import { TemplateDeleteModal } from '../../../../../components'; @@ -20,7 +20,7 @@ import { TemplateTypeIndicator } from '../../components'; interface Props { templates: TemplateListItem[]; - reload: () => Promise; + reload: UseRequestResponse['resendRequest']; editTemplate: (name: string, isLegacy?: boolean) => void; cloneTemplate: (name: string, isLegacy?: boolean) => void; history: ScopedHistory; diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx index 5bacffc4c2404..94891297c857e 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_details/template_details_content.tsx @@ -31,7 +31,7 @@ import { UIM_TEMPLATE_DETAIL_PANEL_ALIASES_TAB, UIM_TEMPLATE_DETAIL_PANEL_PREVIEW_TAB, } from '../../../../../../common/constants'; -import { SendRequestResponse } from '../../../../../shared_imports'; +import { UseRequestResponse } from '../../../../../shared_imports'; import { TemplateDeleteModal, SectionLoading, SectionError, Error } from '../../../../components'; import { useLoadIndexTemplate } from '../../../../services/api'; import { decodePathFromReactRouter } from '../../../../services/routing'; @@ -92,7 +92,7 @@ export interface Props { onClose: () => void; editTemplate: (name: string, isLegacy?: boolean) => void; cloneTemplate: (name: string, isLegacy?: boolean) => void; - reload: () => Promise; + reload: UseRequestResponse['resendRequest']; } export const TemplateDetailsContent = ({ diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx index f421bc5d87a54..c711f457123fb 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_list.tsx @@ -59,7 +59,7 @@ export const TemplateList: React.FunctionComponent { const { uiMetricService } = useServices(); - const { error, isLoading, data: allTemplates, sendRequest: reload } = useLoadIndexTemplates(); + const { error, isLoading, data: allTemplates, resendRequest: reload } = useLoadIndexTemplates(); const [filters, setFilters] = useState>({ managed: { diff --git a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx index 3dffdcde160f1..c32fd29cf9f92 100644 --- a/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx +++ b/x-pack/plugins/index_management/public/application/sections/home/template_list/template_table/template_table.tsx @@ -12,7 +12,7 @@ import { ScopedHistory } from 'kibana/public'; import { TemplateListItem } from '../../../../../../common'; import { UIM_TEMPLATE_SHOW_DETAILS_CLICK } from '../../../../../../common/constants'; -import { SendRequestResponse, reactRouterNavigate } from '../../../../../shared_imports'; +import { UseRequestResponse, reactRouterNavigate } from '../../../../../shared_imports'; import { encodePathForReactRouter } from '../../../../services/routing'; import { useServices } from '../../../../app_context'; import { TemplateDeleteModal } from '../../../../components'; @@ -21,7 +21,7 @@ import { TemplateTypeIndicator } from '../components'; interface Props { templates: TemplateListItem[]; - reload: () => Promise; + reload: UseRequestResponse['resendRequest']; editTemplate: (name: string) => void; cloneTemplate: (name: string) => void; history: ScopedHistory; diff --git a/x-pack/plugins/index_management/public/shared_imports.ts b/x-pack/plugins/index_management/public/shared_imports.ts index f7f992a090501..d58545768732e 100644 --- a/x-pack/plugins/index_management/public/shared_imports.ts +++ b/x-pack/plugins/index_management/public/shared_imports.ts @@ -8,6 +8,7 @@ export { SendRequestConfig, SendRequestResponse, UseRequestConfig, + UseRequestResponse, sendRequest, useRequest, Forms, diff --git a/x-pack/plugins/index_management/server/plugin.ts b/x-pack/plugins/index_management/server/plugin.ts index 0cd180a980a84..30aeeb6b45362 100644 --- a/x-pack/plugins/index_management/server/plugin.ts +++ b/x-pack/plugins/index_management/server/plugin.ts @@ -59,7 +59,7 @@ export class IndexMgmtServerPlugin implements Plugin { this.dataManagementESClient = this.dataManagementESClient ?? (await getCustomEsClient(getStartServices)); diff --git a/x-pack/plugins/index_management/server/types.ts b/x-pack/plugins/index_management/server/types.ts index fce0414dee936..7aa91629f0a47 100644 --- a/x-pack/plugins/index_management/server/types.ts +++ b/x-pack/plugins/index_management/server/types.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { LegacyScopedClusterClient, IRouter } from 'src/core/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { SecurityPluginSetup } from '../../security/server'; import { License, IndexDataEnricher } from './services'; @@ -12,6 +13,7 @@ import { isEsError } from './shared_imports'; export interface Dependencies { security: SecurityPluginSetup; licensing: LicensingPluginSetup; + features: FeaturesPluginSetup; } export interface RouteDependencies { diff --git a/x-pack/plugins/infra/common/http_api/index.ts b/x-pack/plugins/infra/common/http_api/index.ts index 818009417fb1c..4c729d11ba8c1 100644 --- a/x-pack/plugins/infra/common/http_api/index.ts +++ b/x-pack/plugins/infra/common/http_api/index.ts @@ -10,3 +10,4 @@ export * from './log_entries'; export * from './metrics_explorer'; export * from './metrics_api'; export * from './log_alerts'; +export * from './snapshot_api'; 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 7436566f039ca..41657fdce2153 100644 --- a/x-pack/plugins/infra/common/http_api/metrics_api.ts +++ b/x-pack/plugins/infra/common/http_api/metrics_api.ts @@ -33,7 +33,6 @@ export const MetricsAPIRequestRT = rt.intersection([ afterKey: rt.union([rt.null, afterKeyObjectRT]), limit: rt.union([rt.number, rt.null, rt.undefined]), filters: rt.array(rt.object), - forceInterval: rt.boolean, dropLastBucket: rt.boolean, alignDataToEnd: rt.boolean, }), @@ -59,7 +58,10 @@ export const MetricsAPIRowRT = rt.intersection([ rt.type({ timestamp: rt.number, }), - rt.record(rt.string, rt.union([rt.string, rt.number, rt.null, rt.undefined])), + rt.record( + rt.string, + rt.union([rt.string, rt.number, rt.null, rt.undefined, rt.array(rt.object)]) + ), ]); export const MetricsAPISeriesRT = rt.intersection([ 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 c5776e0b0ced1..460b2bf9d802e 100644 --- a/x-pack/plugins/infra/common/http_api/metrics_explorer.ts +++ b/x-pack/plugins/infra/common/http_api/metrics_explorer.ts @@ -89,7 +89,10 @@ export const metricsExplorerRowRT = rt.intersection([ rt.type({ timestamp: rt.number, }), - rt.record(rt.string, rt.union([rt.string, rt.number, rt.null, rt.undefined])), + rt.record( + rt.string, + rt.union([rt.string, rt.number, rt.null, rt.undefined, rt.array(rt.object)]) + ), ]); export const metricsExplorerSeriesRT = rt.intersection([ diff --git a/x-pack/plugins/infra/common/http_api/snapshot_api.ts b/x-pack/plugins/infra/common/http_api/snapshot_api.ts index 11cb57238f917..e1b8dfa4770ba 100644 --- a/x-pack/plugins/infra/common/http_api/snapshot_api.ts +++ b/x-pack/plugins/infra/common/http_api/snapshot_api.ts @@ -6,7 +6,7 @@ import * as rt from 'io-ts'; import { SnapshotMetricTypeRT, ItemTypeRT } from '../inventory_models/types'; -import { metricsExplorerSeriesRT } from './metrics_explorer'; +import { MetricsAPISeriesRT } from './metrics_api'; export const SnapshotNodePathRT = rt.intersection([ rt.type({ @@ -22,7 +22,7 @@ const SnapshotNodeMetricOptionalRT = rt.partial({ value: rt.union([rt.number, rt.null]), avg: rt.union([rt.number, rt.null]), max: rt.union([rt.number, rt.null]), - timeseries: metricsExplorerSeriesRT, + timeseries: MetricsAPISeriesRT, }); const SnapshotNodeMetricRequiredRT = rt.type({ @@ -36,6 +36,7 @@ export const SnapshotNodeMetricRT = rt.intersection([ export const SnapshotNodeRT = rt.type({ metrics: rt.array(SnapshotNodeMetricRT), path: rt.array(SnapshotNodePathRT), + name: rt.string, }); export const SnapshotNodeResponseRT = rt.type({ diff --git a/x-pack/plugins/infra/common/inventory_models/types.ts b/x-pack/plugins/infra/common/inventory_models/types.ts index 570220bbc7aa5..851646ef1fa12 100644 --- a/x-pack/plugins/infra/common/inventory_models/types.ts +++ b/x-pack/plugins/infra/common/inventory_models/types.ts @@ -281,6 +281,10 @@ export const ESSumBucketAggRT = rt.type({ }), }); +export const ESTopHitsAggRT = rt.type({ + top_hits: rt.object, +}); + interface SnapshotTermsWithAggregation { terms: { field: string }; aggregations: MetricsUIAggregation; @@ -304,6 +308,7 @@ export const ESAggregationRT = rt.union([ ESSumBucketAggRT, ESTermsWithAggregationRT, ESCaridnalityAggRT, + ESTopHitsAggRT, ]); export const MetricsUIAggregationRT = rt.record(rt.string, ESAggregationRT); diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx index d2c30a4f38ee9..e01ca3ab6e844 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/components/waffle/conditional_tooltip.test.tsx @@ -88,6 +88,7 @@ describe('ConditionalToolTip', () => { mockedUseSnapshot.mockReturnValue({ nodes: [ { + name: 'host-01', path: [{ label: 'host-01', value: 'host-01', ip: '192.168.1.10' }], metrics: [ { name: 'cpu', value: 0.1, avg: 0.4, max: 0.7 }, diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/calculate_bounds_from_nodes.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/calculate_bounds_from_nodes.test.ts index fbb6aa933219a..49f4b56532936 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/calculate_bounds_from_nodes.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/calculate_bounds_from_nodes.test.ts @@ -7,6 +7,7 @@ import { calculateBoundsFromNodes } from './calculate_bounds_from_nodes'; import { SnapshotNode } from '../../../../../common/http_api/snapshot_api'; const nodes: SnapshotNode[] = [ { + name: 'host-01', path: [{ value: 'host-01', label: 'host-01' }], metrics: [ { @@ -18,6 +19,7 @@ const nodes: SnapshotNode[] = [ ], }, { + name: 'host-02', path: [{ value: 'host-02', label: 'host-02' }], metrics: [ { diff --git a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/sort_nodes.test.ts b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/sort_nodes.test.ts index 2a9f8b911c124..f7d9f029f00df 100644 --- a/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/sort_nodes.test.ts +++ b/x-pack/plugins/infra/public/pages/metrics/inventory_view/lib/sort_nodes.test.ts @@ -9,6 +9,7 @@ import { SnapshotNode } from '../../../../../common/http_api/snapshot_api'; const nodes: SnapshotNode[] = [ { + name: 'host-01', path: [{ value: 'host-01', label: 'host-01' }], metrics: [ { @@ -20,6 +21,7 @@ const nodes: SnapshotNode[] = [ ], }, { + name: 'host-02', path: [{ value: 'host-02', label: 'host-02' }], metrics: [ { 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 939498305eb98..c5b667fb20538 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 @@ -8,12 +8,12 @@ import { timeMilliseconds } from 'd3-time'; import * as runtimeTypes from 'io-ts'; -import { compact, first, get, has } from 'lodash'; +import { compact, first } from 'lodash'; import { pipe } from 'fp-ts/lib/pipeable'; import { map, fold } from 'fp-ts/lib/Either'; import { identity, constant } from 'fp-ts/lib/function'; import { RequestHandlerContext } from 'src/core/server'; -import { JsonObject, JsonValue } from '../../../../common/typed_json'; +import { JsonValue } from '../../../../common/typed_json'; import { LogEntriesAdapter, LogEntriesParams, @@ -31,7 +31,7 @@ const TIMESTAMP_FORMAT = 'epoch_millis'; interface LogItemHit { _index: string; _id: string; - _source: JsonObject; + fields: { [key: string]: [value: unknown] }; sort: [number, number]; } @@ -82,7 +82,8 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { body: { size: typeof size !== 'undefined' ? size : LOG_ENTRIES_PAGE_SIZE, track_total_hits: false, - _source: fields, + _source: false, + fields, query: { bool: { filter: [ @@ -214,6 +215,8 @@ export class InfraKibanaLogEntriesAdapter implements LogEntriesAdapter { values: [id], }, }, + fields: ['*'], + _source: false, }, }; @@ -230,8 +233,8 @@ function mapHitsToLogEntryDocuments(hits: SortedSearchHit[], fields: string[]): return hits.map((hit) => { const logFields = fields.reduce<{ [fieldName: string]: JsonValue }>( (flattenedFields, field) => { - if (has(hit._source, field)) { - flattenedFields[field] = get(hit._source, field); + if (field in hit.fields) { + flattenedFields[field] = hit.fields[field][0]; } return flattenedFields; }, diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts index 2f3593a11f664..d6592719d0723 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/evaluate_condition.ts @@ -16,12 +16,11 @@ import { } from '../../adapters/framework/adapter_types'; import { Comparator, InventoryMetricConditions } from './types'; import { AlertServices } from '../../../../../alerts/server'; -import { InfraSnapshot } from '../../snapshot'; -import { parseFilterQuery } from '../../../utils/serialized_query'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; -import { InfraTimerangeInput } from '../../../../common/http_api/snapshot_api'; -import { InfraSourceConfiguration } from '../../sources'; +import { InfraTimerangeInput, SnapshotRequest } from '../../../../common/http_api/snapshot_api'; +import { InfraSource } from '../../sources'; import { UNGROUPED_FACTORY_KEY } from '../common/utils'; +import { getNodes } from '../../../routes/snapshot/lib/get_nodes'; type ConditionResult = InventoryMetricConditions & { shouldFire: boolean[]; @@ -33,7 +32,7 @@ type ConditionResult = InventoryMetricConditions & { export const evaluateCondition = async ( condition: InventoryMetricConditions, nodeType: InventoryItemType, - sourceConfiguration: InfraSourceConfiguration, + source: InfraSource, callCluster: AlertServices['callCluster'], filterQuery?: string, lookbackSize?: number @@ -55,7 +54,7 @@ export const evaluateCondition = async ( nodeType, metric, timerange, - sourceConfiguration, + source, filterQuery, customMetric ); @@ -94,12 +93,11 @@ const getData = async ( nodeType: InventoryItemType, metric: SnapshotMetricType, timerange: InfraTimerangeInput, - sourceConfiguration: InfraSourceConfiguration, + source: InfraSource, filterQuery?: string, customMetric?: SnapshotCustomMetricInput ) => { - const snapshot = new InfraSnapshot(); - const esClient = ( + const client = ( options: CallWithRequestParams ): Promise> => callCluster('search', options); @@ -107,17 +105,17 @@ const getData = async ( metric === 'custom' ? (customMetric as SnapshotCustomMetricInput) : { type: metric }, ]; - const options = { - filterQuery: parseFilterQuery(filterQuery), + const snapshotRequest: SnapshotRequest = { + filterQuery, nodeType, groupBy: [], - sourceConfiguration, + sourceId: 'default', metrics, timerange, includeTimeseries: Boolean(timerange.lookbackSize), }; try { - const { nodes } = await snapshot.getNodes(esClient, options); + const { nodes } = await getNodes(client, snapshotRequest, source); if (!nodes.length) return { [UNGROUPED_FACTORY_KEY]: null }; // No Data state diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts index bdac9dcd1dee8..99904f15b4606 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/inventory_metric_threshold_executor.ts @@ -50,9 +50,7 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = ); const results = await Promise.all( - criteria.map((c) => - evaluateCondition(c, nodeType, source.configuration, services.callCluster, filterQuery) - ) + criteria.map((c) => evaluateCondition(c, nodeType, source, services.callCluster, filterQuery)) ); const inventoryItems = Object.keys(first(results)!); diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts index 755c395818f5a..2ab015b6b37a2 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/preview_inventory_metric_threshold_alert.ts @@ -26,7 +26,7 @@ interface InventoryMetricThresholdParams { interface PreviewInventoryMetricThresholdAlertParams { callCluster: ILegacyScopedClusterClient['callAsCurrentUser']; params: InventoryMetricThresholdParams; - config: InfraSource['configuration']; + source: InfraSource; lookback: Unit; alertInterval: string; } @@ -34,7 +34,7 @@ interface PreviewInventoryMetricThresholdAlertParams { export const previewInventoryMetricThresholdAlert = async ({ callCluster, params, - config, + source, lookback, alertInterval, }: PreviewInventoryMetricThresholdAlertParams) => { @@ -55,7 +55,7 @@ export const previewInventoryMetricThresholdAlert = async ({ try { const results = await Promise.all( criteria.map((c) => - evaluateCondition(c, nodeType, config, callCluster, filterQuery, lookbackSize) + evaluateCondition(c, nodeType, source, callCluster, filterQuery, lookbackSize) ) ); 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 078ca46d42e60..8696081043ff7 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 @@ -8,8 +8,8 @@ import { networkTraffic } from '../../../../../common/inventory_models/shared/me import { MetricExpressionParams, Aggregators } from '../types'; import { getIntervalInSeconds } from '../../../../utils/get_interval_in_seconds'; import { roundTimestamp } from '../../../../utils/round_timestamp'; -import { getDateHistogramOffset } from '../../../snapshot/query_helpers'; import { createPercentileAggregation } from './create_percentile_aggregation'; +import { calculateDateHistogramOffset } from '../../../metrics/lib/calculate_date_histogram_offset'; const MINIMUM_BUCKETS = 5; @@ -46,7 +46,7 @@ export const getElasticsearchMetricQuery = ( timeUnit ); - const offset = getDateHistogramOffset(from, interval); + const offset = calculateDateHistogramOffset({ from, to, interval, field: timefield }); const aggregations = aggType === Aggregators.COUNT diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts index 099e7c3b5038c..7c8560d72ff97 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/convert_document_source_to_log_item_fields.ts @@ -20,6 +20,11 @@ const serializeValue = (value: any): string => { } return `${value}`; }; +export const convertESFieldsToLogItemFields = (fields: { + [field: string]: [value: unknown]; +}): LogEntriesItemField[] => { + return Object.keys(fields).map((field) => ({ field, value: serializeValue(fields[field][0]) })); +}; export const convertDocumentSourceToLogItemFields = ( source: JsonObject, diff --git a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts index 9b3e31f4da87a..e211f72b4e076 100644 --- a/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts +++ b/x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts @@ -22,7 +22,7 @@ import { SavedSourceConfigurationFieldColumnRuntimeType, } from '../../sources'; import { getBuiltinRules } from './builtin_rules'; -import { convertDocumentSourceToLogItemFields } from './convert_document_source_to_log_item_fields'; +import { convertESFieldsToLogItemFields } from './convert_document_source_to_log_item_fields'; import { CompiledLogMessageFormattingRule, Fields, @@ -264,7 +264,7 @@ export class InfraLogEntriesDomain { tiebreaker: document.sort[1], }, fields: sortBy( - [...defaultFields, ...convertDocumentSourceToLogItemFields(document._source)], + [...defaultFields, ...convertESFieldsToLogItemFields(document.fields)], 'field' ), }; @@ -313,7 +313,7 @@ export class InfraLogEntriesDomain { interface LogItemHit { _index: string; _id: string; - _source: JsonObject; + fields: { [field: string]: [value: unknown] }; sort: [number, number]; } diff --git a/x-pack/plugins/infra/server/lib/infra_types.ts b/x-pack/plugins/infra/server/lib/infra_types.ts index 9896ad6ac1cd1..084ece52302b0 100644 --- a/x-pack/plugins/infra/server/lib/infra_types.ts +++ b/x-pack/plugins/infra/server/lib/infra_types.ts @@ -8,7 +8,6 @@ import { InfraSourceConfiguration } from '../../common/graphql/types'; import { InfraFieldsDomain } from './domains/fields_domain'; import { InfraLogEntriesDomain } from './domains/log_entries_domain'; import { InfraMetricsDomain } from './domains/metrics_domain'; -import { InfraSnapshot } from './snapshot'; import { InfraSources } from './sources'; import { InfraSourceStatus } from './source_status'; import { InfraConfig } from '../plugin'; @@ -30,7 +29,6 @@ export interface InfraDomainLibs { export interface InfraBackendLibs extends InfraDomainLibs { configuration: InfraConfig; framework: KibanaFramework; - snapshot: InfraSnapshot; sources: InfraSources; sourceStatus: InfraSourceStatus; } diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap b/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap index d2d90914eced5..2cbbc623aed38 100644 --- a/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap +++ b/x-pack/plugins/infra/server/lib/metrics/lib/__snapshots__/create_aggregations.test.ts.snap @@ -53,7 +53,6 @@ Object { "groupBy0": Object { "terms": Object { "field": "host.name", - "order": "asc", }, }, }, diff --git a/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts b/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts index 95e6ece215133..90e584368e9ad 100644 --- a/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts +++ b/x-pack/plugins/infra/server/lib/metrics/lib/convert_histogram_buckets_to_timeseries.ts @@ -5,6 +5,7 @@ */ import { get, values, first } from 'lodash'; +import * as rt from 'io-ts'; import { MetricsAPIRequest, MetricsAPISeries, @@ -13,15 +14,20 @@ import { } from '../../../../common/http_api/metrics_api'; import { HistogramBucket, - MetricValueType, BasicMetricValueRT, NormalizedMetricValueRT, PercentilesTypeRT, PercentilesKeyedTypeRT, + TopHitsTypeRT, + MetricValueTypeRT, } from '../types'; + const BASE_COLUMNS = [{ name: 'timestamp', type: 'date' }] as MetricsAPIColumn[]; -const getValue = (valueObject: string | number | MetricValueType) => { +const ValueObjectTypeRT = rt.union([rt.string, rt.number, MetricValueTypeRT]); +type ValueObjectType = rt.TypeOf; + +const getValue = (valueObject: ValueObjectType) => { if (NormalizedMetricValueRT.is(valueObject)) { return valueObject.normalized_value || valueObject.value; } @@ -50,6 +56,10 @@ const getValue = (valueObject: string | number | MetricValueType) => { return valueObject.value; } + if (TopHitsTypeRT.is(valueObject)) { + return valueObject.hits.hits.map((hit) => hit._source); + } + return null; }; @@ -61,8 +71,8 @@ const convertBucketsToRows = ( const ids = options.metrics.map((metric) => metric.id); const metrics = ids.reduce((acc, id) => { const valueObject = get(bucket, [id]); - return { ...acc, [id]: getValue(valueObject) }; - }, {} as Record); + return { ...acc, [id]: ValueObjectTypeRT.is(valueObject) ? getValue(valueObject) : null }; + }, {} as Record); return { timestamp: bucket.key as number, ...metrics }; }); }; 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 991e5febfc634..63fdbb3d2b30f 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 @@ -33,7 +33,7 @@ export const createAggregations = (options: MetricsAPIRequest) => { composite: { size: limit, sources: options.groupBy.map((field, index) => ({ - [`groupBy${index}`]: { terms: { field, order: 'asc' } }, + [`groupBy${index}`]: { terms: { field } }, })), }, aggs: histogramAggregation, diff --git a/x-pack/plugins/infra/server/lib/metrics/types.ts b/x-pack/plugins/infra/server/lib/metrics/types.ts index d1866470e0cf9..8746614b559d6 100644 --- a/x-pack/plugins/infra/server/lib/metrics/types.ts +++ b/x-pack/plugins/infra/server/lib/metrics/types.ts @@ -25,17 +25,51 @@ export const PercentilesKeyedTypeRT = rt.type({ values: rt.array(rt.type({ key: rt.string, value: NumberOrNullRT })), }); +export const TopHitsTypeRT = rt.type({ + hits: rt.type({ + total: rt.type({ + value: rt.number, + relation: rt.string, + }), + hits: rt.array( + rt.intersection([ + rt.type({ + _index: rt.string, + _id: rt.string, + _score: NumberOrNullRT, + _source: rt.object, + }), + rt.partial({ + sort: rt.array(rt.union([rt.string, rt.number])), + max_score: NumberOrNullRT, + }), + ]) + ), + }), +}); + export const MetricValueTypeRT = rt.union([ BasicMetricValueRT, NormalizedMetricValueRT, PercentilesTypeRT, PercentilesKeyedTypeRT, + TopHitsTypeRT, ]); export type MetricValueType = rt.TypeOf; +export const TermsWithMetrics = rt.intersection([ + rt.type({ + buckets: rt.array(rt.record(rt.string, rt.union([rt.number, rt.string, MetricValueTypeRT]))), + }), + rt.partial({ + sum_other_doc_count: rt.number, + doc_count_error_upper_bound: rt.number, + }), +]); + export const HistogramBucketRT = rt.record( rt.string, - rt.union([rt.number, rt.string, MetricValueTypeRT]) + rt.union([rt.number, rt.string, MetricValueTypeRT, TermsWithMetrics]) ); export const HistogramResponseRT = rt.type({ diff --git a/x-pack/plugins/infra/server/lib/snapshot/query_helpers.ts b/x-pack/plugins/infra/server/lib/snapshot/query_helpers.ts deleted file mode 100644 index ca63043ba868e..0000000000000 --- a/x-pack/plugins/infra/server/lib/snapshot/query_helpers.ts +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { findInventoryModel, findInventoryFields } from '../../../common/inventory_models/index'; -import { InfraSnapshotRequestOptions } from './types'; -import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds'; -import { - MetricsUIAggregation, - MetricsUIAggregationRT, - InventoryItemType, -} from '../../../common/inventory_models/types'; -import { - SnapshotMetricInput, - SnapshotCustomMetricInputRT, -} from '../../../common/http_api/snapshot_api'; -import { networkTraffic } from '../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; - -interface GroupBySource { - [id: string]: { - terms: { - field: string | null | undefined; - missing_bucket?: boolean; - }; - }; -} - -export const getFieldByNodeType = (options: InfraSnapshotRequestOptions) => { - const inventoryFields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields); - return inventoryFields.id; -}; - -export const getGroupedNodesSources = (options: InfraSnapshotRequestOptions) => { - const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields); - const sources: GroupBySource[] = options.groupBy.map((gb) => { - return { [`${gb.field}`]: { terms: { field: gb.field } } }; - }); - sources.push({ - id: { - terms: { field: fields.id }, - }, - }); - sources.push({ - name: { terms: { field: fields.name, missing_bucket: true } }, - }); - return sources; -}; - -export const getMetricsSources = (options: InfraSnapshotRequestOptions) => { - const fields = findInventoryFields(options.nodeType, options.sourceConfiguration.fields); - return [{ id: { terms: { field: fields.id } } }]; -}; - -export const metricToAggregation = ( - nodeType: InventoryItemType, - metric: SnapshotMetricInput, - index: number -) => { - const inventoryModel = findInventoryModel(nodeType); - if (SnapshotCustomMetricInputRT.is(metric)) { - if (metric.aggregation === 'rate') { - return networkTraffic(`custom_${index}`, metric.field); - } - return { - [`custom_${index}`]: { - [metric.aggregation]: { - field: metric.field, - }, - }, - }; - } - return inventoryModel.metrics.snapshot?.[metric.type]; -}; - -export const getMetricsAggregations = ( - options: InfraSnapshotRequestOptions -): MetricsUIAggregation => { - const { metrics } = options; - return metrics.reduce((aggs, metric, index) => { - const aggregation = metricToAggregation(options.nodeType, metric, index); - if (!MetricsUIAggregationRT.is(aggregation)) { - throw new Error( - i18n.translate('xpack.infra.snapshot.missingSnapshotMetricError', { - defaultMessage: 'The aggregation for {metric} for {nodeType} is not available.', - values: { - nodeType: options.nodeType, - metric: metric.type, - }, - }) - ); - } - return { ...aggs, ...aggregation }; - }, {}); -}; - -export const getDateHistogramOffset = (from: number, interval: string): string => { - const fromInSeconds = Math.floor(from / 1000); - const bucketSizeInSeconds = getIntervalInSeconds(interval); - - // negative offset to align buckets with full intervals (e.g. minutes) - const offset = (fromInSeconds % bucketSizeInSeconds) - bucketSizeInSeconds; - return `${offset}s`; -}; diff --git a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.test.ts b/x-pack/plugins/infra/server/lib/snapshot/response_helpers.test.ts deleted file mode 100644 index 74840afc157d2..0000000000000 --- a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.test.ts +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - isIPv4, - getIPFromBucket, - InfraSnapshotNodeGroupByBucket, - getMetricValueFromBucket, - InfraSnapshotMetricsBucket, -} from './response_helpers'; - -describe('InfraOps ResponseHelpers', () => { - describe('isIPv4', () => { - it('should return true for IPv4', () => { - expect(isIPv4('192.168.2.4')).toBe(true); - }); - it('should return false for anything else', () => { - expect(isIPv4('0:0:0:0:0:0:0:1')).toBe(false); - }); - }); - - describe('getIPFromBucket', () => { - it('should return IPv4 address', () => { - const bucket: InfraSnapshotNodeGroupByBucket = { - key: { - id: 'example-01', - name: 'example-01', - }, - ip: { - hits: { - total: { value: 1 }, - hits: [ - { - _index: 'metricbeat-2019-01-01', - _type: '_doc', - _id: '29392939', - _score: null, - sort: [], - _source: { - host: { - ip: ['2001:db8:85a3::8a2e:370:7334', '192.168.1.4'], - }, - }, - }, - ], - }, - }, - }; - expect(getIPFromBucket('host', bucket)).toBe('192.168.1.4'); - }); - it('should NOT return ipv6 address', () => { - const bucket: InfraSnapshotNodeGroupByBucket = { - key: { - id: 'example-01', - name: 'example-01', - }, - ip: { - hits: { - total: { value: 1 }, - hits: [ - { - _index: 'metricbeat-2019-01-01', - _type: '_doc', - _id: '29392939', - _score: null, - sort: [], - _source: { - host: { - ip: ['2001:db8:85a3::8a2e:370:7334'], - }, - }, - }, - ], - }, - }, - }; - expect(getIPFromBucket('host', bucket)).toBe(null); - }); - }); - - describe('getMetricValueFromBucket', () => { - it('should return the value of a bucket with data', () => { - expect(getMetricValueFromBucket('custom', testBucket, 1)).toBe(0.5); - }); - it('should return the normalized value of a bucket with data', () => { - expect(getMetricValueFromBucket('cpu', testNormalizedBucket, 1)).toBe(50); - }); - it('should return null for a bucket with no data', () => { - expect(getMetricValueFromBucket('custom', testEmptyBucket, 1)).toBe(null); - }); - }); -}); - -// Hack to get around TypeScript -const buckets = [ - { - key: 'a', - doc_count: 1, - custom_1: { - value: 0.5, - }, - }, - { - key: 'b', - doc_count: 1, - cpu: { - value: 0.5, - normalized_value: 50, - }, - }, - { - key: 'c', - doc_count: 0, - }, -] as InfraSnapshotMetricsBucket[]; -const [testBucket, testNormalizedBucket, testEmptyBucket] = buckets; diff --git a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts b/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts deleted file mode 100644 index 2652e362b7eff..0000000000000 --- a/x-pack/plugins/infra/server/lib/snapshot/response_helpers.ts +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { isNumber, last, max, sum, get } from 'lodash'; -import moment from 'moment'; - -import { MetricsExplorerSeries } from '../../../common/http_api/metrics_explorer'; -import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds'; -import { InfraSnapshotRequestOptions } from './types'; -import { findInventoryModel } from '../../../common/inventory_models'; -import { InventoryItemType, SnapshotMetricType } from '../../../common/inventory_models/types'; -import { SnapshotNodeMetric, SnapshotNodePath } from '../../../common/http_api/snapshot_api'; - -export interface InfraSnapshotNodeMetricsBucket { - key: { id: string }; - histogram: { - buckets: InfraSnapshotMetricsBucket[]; - }; -} - -// Jumping through TypeScript hoops here: -// We need an interface that has the known members 'key' and 'doc_count' and also -// an unknown number of members with unknown names but known format, containing the -// metrics. -// This union type is the only way I found to express this that TypeScript accepts. -export interface InfraSnapshotBucketWithKey { - key: string | number; - doc_count: number; -} - -export interface InfraSnapshotBucketWithValues { - [name: string]: { value: number; normalized_value?: number }; -} - -export type InfraSnapshotMetricsBucket = InfraSnapshotBucketWithKey & InfraSnapshotBucketWithValues; - -interface InfraSnapshotIpHit { - _index: string; - _type: string; - _id: string; - _score: number | null; - _source: { - host: { - ip: string[] | string; - }; - }; - sort: number[]; -} - -export interface InfraSnapshotNodeGroupByBucket { - key: { - id: string; - name: string; - [groupByField: string]: string; - }; - ip: { - hits: { - total: { value: number }; - hits: InfraSnapshotIpHit[]; - }; - }; -} - -export const isIPv4 = (subject: string) => /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(subject); - -export const getIPFromBucket = ( - nodeType: InventoryItemType, - bucket: InfraSnapshotNodeGroupByBucket -): string | null => { - const inventoryModel = findInventoryModel(nodeType); - if (!inventoryModel.fields.ip) { - return null; - } - const ip = get(bucket, `ip.hits.hits[0]._source.${inventoryModel.fields.ip}`, null) as - | string[] - | null; - if (Array.isArray(ip)) { - return ip.find(isIPv4) || null; - } else if (typeof ip === 'string') { - return ip; - } - - return null; -}; - -export const getNodePath = ( - groupBucket: InfraSnapshotNodeGroupByBucket, - options: InfraSnapshotRequestOptions -): SnapshotNodePath[] => { - const node = groupBucket.key; - const path = options.groupBy.map((gb) => { - return { value: node[`${gb.field}`], label: node[`${gb.field}`] } as SnapshotNodePath; - }); - const ip = getIPFromBucket(options.nodeType, groupBucket); - path.push({ value: node.id, label: node.name || node.id, ip }); - return path; -}; - -interface NodeMetricsForLookup { - [nodeId: string]: InfraSnapshotMetricsBucket[]; -} - -export const getNodeMetricsForLookup = ( - metrics: InfraSnapshotNodeMetricsBucket[] -): NodeMetricsForLookup => { - return metrics.reduce((acc: NodeMetricsForLookup, metric) => { - acc[`${metric.key.id}`] = metric.histogram.buckets; - return acc; - }, {}); -}; - -// In the returned object, -// value contains the value from the last bucket spanning a full interval -// max and avg are calculated from all buckets returned for the timerange -export const getNodeMetrics = ( - nodeBuckets: InfraSnapshotMetricsBucket[], - options: InfraSnapshotRequestOptions -): SnapshotNodeMetric[] => { - if (!nodeBuckets) { - return options.metrics.map((metric) => ({ - name: metric.type, - value: null, - max: null, - avg: null, - })); - } - const lastBucket = findLastFullBucket(nodeBuckets, options); - if (!lastBucket) return []; - return options.metrics.map((metric, index) => { - const metricResult: SnapshotNodeMetric = { - name: metric.type, - value: getMetricValueFromBucket(metric.type, lastBucket, index), - max: calculateMax(nodeBuckets, metric.type, index), - avg: calculateAvg(nodeBuckets, metric.type, index), - }; - if (options.includeTimeseries) { - metricResult.timeseries = getTimeseriesData(nodeBuckets, metric.type, index); - } - return metricResult; - }); -}; - -const findLastFullBucket = ( - buckets: InfraSnapshotMetricsBucket[], - options: InfraSnapshotRequestOptions -) => { - const to = moment.utc(options.timerange.to); - const bucketSize = getIntervalInSeconds(options.timerange.interval); - return buckets.reduce((current, item) => { - const itemKey = isNumber(item.key) ? item.key : parseInt(item.key, 10); - const date = moment.utc(itemKey + bucketSize * 1000); - if (!date.isAfter(to) && item.doc_count > 0) { - return item; - } - return current; - }, last(buckets)); -}; - -export const getMetricValueFromBucket = ( - type: SnapshotMetricType, - bucket: InfraSnapshotMetricsBucket, - index: number -) => { - const key = type === 'custom' ? `custom_${index}` : type; - const metric = bucket[key]; - const value = metric && (metric.normalized_value || metric.value); - return isFinite(value) ? value : null; -}; - -function calculateMax( - buckets: InfraSnapshotMetricsBucket[], - type: SnapshotMetricType, - index: number -) { - return max(buckets.map((bucket) => getMetricValueFromBucket(type, bucket, index))) || 0; -} - -function calculateAvg( - buckets: InfraSnapshotMetricsBucket[], - type: SnapshotMetricType, - index: number -) { - return ( - sum(buckets.map((bucket) => getMetricValueFromBucket(type, bucket, index))) / buckets.length || - 0 - ); -} - -function getTimeseriesData( - buckets: InfraSnapshotMetricsBucket[], - type: SnapshotMetricType, - index: number -): MetricsExplorerSeries { - return { - id: type, - columns: [ - { name: 'timestamp', type: 'date' }, - { name: 'metric_0', type: 'number' }, - ], - rows: buckets.map((bucket) => ({ - timestamp: bucket.key as number, - metric_0: getMetricValueFromBucket(type, bucket, index), - })), - }; -} diff --git a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts b/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts deleted file mode 100644 index 33d8e738a717e..0000000000000 --- a/x-pack/plugins/infra/server/lib/snapshot/snapshot.ts +++ /dev/null @@ -1,238 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { InfraDatabaseSearchResponse, CallWithRequestParams } from '../adapters/framework'; - -import { JsonObject } from '../../../common/typed_json'; -import { SNAPSHOT_COMPOSITE_REQUEST_SIZE } from './constants'; -import { - getGroupedNodesSources, - getMetricsAggregations, - getMetricsSources, - getDateHistogramOffset, -} from './query_helpers'; -import { - getNodeMetrics, - getNodeMetricsForLookup, - getNodePath, - InfraSnapshotNodeGroupByBucket, - InfraSnapshotNodeMetricsBucket, -} from './response_helpers'; -import { getAllCompositeData } from '../../utils/get_all_composite_data'; -import { createAfterKeyHandler } from '../../utils/create_afterkey_handler'; -import { findInventoryModel } from '../../../common/inventory_models'; -import { InfraSnapshotRequestOptions } from './types'; -import { createTimeRangeWithInterval } from './create_timerange_with_interval'; -import { SnapshotNode } from '../../../common/http_api/snapshot_api'; - -type NamedSnapshotNode = SnapshotNode & { name: string }; - -export type ESSearchClient = ( - options: CallWithRequestParams -) => Promise>; -export class InfraSnapshot { - public async getNodes( - client: ESSearchClient, - options: InfraSnapshotRequestOptions - ): Promise<{ nodes: NamedSnapshotNode[]; interval: string }> { - // Both requestGroupedNodes and requestNodeMetrics may send several requests to elasticsearch - // in order to page through the results of their respective composite aggregations. - // Both chains of requests are supposed to run in parallel, and their results be merged - // when they have both been completed. - const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(client, options); - const optionsWithTimerange = { ...options, timerange: timeRangeWithIntervalApplied }; - - const groupedNodesPromise = requestGroupedNodes(client, optionsWithTimerange); - const nodeMetricsPromise = requestNodeMetrics(client, optionsWithTimerange); - const [groupedNodeBuckets, nodeMetricBuckets] = await Promise.all([ - groupedNodesPromise, - nodeMetricsPromise, - ]); - return { - nodes: mergeNodeBuckets(groupedNodeBuckets, nodeMetricBuckets, options), - interval: timeRangeWithIntervalApplied.interval, - }; - } -} - -const bucketSelector = ( - response: InfraDatabaseSearchResponse<{}, InfraSnapshotAggregationResponse> -) => (response.aggregations && response.aggregations.nodes.buckets) || []; - -const handleAfterKey = createAfterKeyHandler( - 'body.aggregations.nodes.composite.after', - (input) => input?.aggregations?.nodes?.after_key -); - -const callClusterFactory = (search: ESSearchClient) => (opts: any) => - search<{}, InfraSnapshotAggregationResponse>(opts); - -const requestGroupedNodes = async ( - client: ESSearchClient, - options: InfraSnapshotRequestOptions -): Promise => { - const inventoryModel = findInventoryModel(options.nodeType); - const query = { - allowNoIndices: true, - index: `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`, - ignoreUnavailable: true, - body: { - query: { - bool: { - filter: buildFilters(options), - }, - }, - size: 0, - aggregations: { - nodes: { - composite: { - size: options.overrideCompositeSize || SNAPSHOT_COMPOSITE_REQUEST_SIZE, - sources: getGroupedNodesSources(options), - }, - aggs: { - ip: { - top_hits: { - sort: [{ [options.sourceConfiguration.fields.timestamp]: { order: 'desc' } }], - _source: { - includes: inventoryModel.fields.ip ? [inventoryModel.fields.ip] : [], - }, - size: 1, - }, - }, - }, - }, - }, - }, - }; - return getAllCompositeData( - callClusterFactory(client), - query, - bucketSelector, - handleAfterKey - ); -}; - -const calculateIndexPatterBasedOnMetrics = (options: InfraSnapshotRequestOptions) => { - const { metrics } = options; - if (metrics.every((m) => m.type === 'logRate')) { - return options.sourceConfiguration.logAlias; - } - if (metrics.some((m) => m.type === 'logRate')) { - return `${options.sourceConfiguration.logAlias},${options.sourceConfiguration.metricAlias}`; - } - return options.sourceConfiguration.metricAlias; -}; - -const requestNodeMetrics = async ( - client: ESSearchClient, - options: InfraSnapshotRequestOptions -): Promise => { - const index = calculateIndexPatterBasedOnMetrics(options); - const query = { - allowNoIndices: true, - index, - ignoreUnavailable: true, - body: { - query: { - bool: { - filter: buildFilters(options, false), - }, - }, - size: 0, - aggregations: { - nodes: { - composite: { - size: options.overrideCompositeSize || SNAPSHOT_COMPOSITE_REQUEST_SIZE, - sources: getMetricsSources(options), - }, - aggregations: { - histogram: { - date_histogram: { - field: options.sourceConfiguration.fields.timestamp, - interval: options.timerange.interval || '1m', - offset: getDateHistogramOffset(options.timerange.from, options.timerange.interval), - extended_bounds: { - min: options.timerange.from, - max: options.timerange.to, - }, - }, - aggregations: getMetricsAggregations(options), - }, - }, - }, - }, - }, - }; - return getAllCompositeData( - callClusterFactory(client), - query, - bucketSelector, - handleAfterKey - ); -}; - -// buckets can be InfraSnapshotNodeGroupByBucket[] or InfraSnapshotNodeMetricsBucket[] -// but typing this in a way that makes TypeScript happy is unreadable (if possible at all) -interface InfraSnapshotAggregationResponse { - nodes: { - buckets: any[]; - after_key: { [id: string]: string }; - }; -} - -const mergeNodeBuckets = ( - nodeGroupByBuckets: InfraSnapshotNodeGroupByBucket[], - nodeMetricsBuckets: InfraSnapshotNodeMetricsBucket[], - options: InfraSnapshotRequestOptions -): NamedSnapshotNode[] => { - const nodeMetricsForLookup = getNodeMetricsForLookup(nodeMetricsBuckets); - - return nodeGroupByBuckets.map((node) => { - return { - name: node.key.name || node.key.id, // For type safety; name can be derived from getNodePath but not in a TS-friendly way - path: getNodePath(node, options), - metrics: getNodeMetrics(nodeMetricsForLookup[node.key.id], options), - }; - }); -}; - -const createQueryFilterClauses = (filterQuery: JsonObject | undefined) => - filterQuery ? [filterQuery] : []; - -const buildFilters = (options: InfraSnapshotRequestOptions, withQuery = true) => { - let filters: any = [ - { - range: { - [options.sourceConfiguration.fields.timestamp]: { - gte: options.timerange.from, - lte: options.timerange.to, - format: 'epoch_millis', - }, - }, - }, - ]; - - if (withQuery) { - filters = [...createQueryFilterClauses(options.filterQuery), ...filters]; - } - - if (options.accountId) { - filters.push({ - term: { - 'cloud.account.id': options.accountId, - }, - }); - } - - if (options.region) { - filters.push({ - term: { - 'cloud.region': options.region, - }, - }); - } - - return filters; -}; diff --git a/x-pack/plugins/infra/server/lib/snapshot/types.ts b/x-pack/plugins/infra/server/lib/snapshot/types.ts deleted file mode 100644 index 7e17cb91c6a59..0000000000000 --- a/x-pack/plugins/infra/server/lib/snapshot/types.ts +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { JsonObject } from '../../../common/typed_json'; -import { InfraSourceConfiguration } from '../../../common/graphql/types'; -import { SnapshotRequest } from '../../../common/http_api/snapshot_api'; - -export interface InfraSnapshotRequestOptions - extends Omit { - sourceConfiguration: InfraSourceConfiguration; - filterQuery: JsonObject | undefined; -} diff --git a/x-pack/plugins/infra/server/lib/sources/has_data.ts b/x-pack/plugins/infra/server/lib/sources/has_data.ts index 79b1375059dcb..53297640e541d 100644 --- a/x-pack/plugins/infra/server/lib/sources/has_data.ts +++ b/x-pack/plugins/infra/server/lib/sources/has_data.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESSearchClient } from '../snapshot'; +import { ESSearchClient } from '../metrics/types'; export const hasData = async (index: string, client: ESSearchClient) => { const params = { diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 51f91d7189db7..737f7ed1b6e4f 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -19,7 +19,6 @@ import { InfraElasticsearchSourceStatusAdapter } from './lib/adapters/source_sta import { InfraFieldsDomain } from './lib/domains/fields_domain'; import { InfraLogEntriesDomain } from './lib/domains/log_entries_domain'; import { InfraMetricsDomain } from './lib/domains/metrics_domain'; -import { InfraSnapshot } from './lib/snapshot'; import { InfraSourceStatus } from './lib/source_status'; import { InfraSources } from './lib/sources'; import { InfraServerPluginDeps } from './lib/adapters/framework'; @@ -105,7 +104,6 @@ export class InfraServerPlugin { sources, } ); - const snapshot = new InfraSnapshot(); // register saved object types core.savedObjects.registerType(infraSourceConfigurationSavedObjectType); @@ -129,14 +127,13 @@ export class InfraServerPlugin { this.libs = { configuration: this.config, framework, - snapshot, sources, sourceStatus, ...domainLibs, }; - plugins.features.registerFeature(METRICS_FEATURE); - plugins.features.registerFeature(LOGS_FEATURE); + plugins.features.registerKibanaFeature(METRICS_FEATURE); + plugins.features.registerKibanaFeature(LOGS_FEATURE); plugins.home.sampleData.addAppLinksToSampleDataset('logs', [ { diff --git a/x-pack/plugins/infra/server/routes/alerting/preview.ts b/x-pack/plugins/infra/server/routes/alerting/preview.ts index 5594323d706de..40d09dadfe050 100644 --- a/x-pack/plugins/infra/server/routes/alerting/preview.ts +++ b/x-pack/plugins/infra/server/routes/alerting/preview.ts @@ -82,7 +82,7 @@ export const initAlertPreviewRoute = ({ framework, sources }: InfraBackendLibs) callCluster, params: { criteria, filterQuery, nodeType }, lookback, - config: source.configuration, + source, alertInterval, }); diff --git a/x-pack/plugins/infra/server/routes/log_entries/entries.ts b/x-pack/plugins/infra/server/routes/log_entries/entries.ts index 2cd889d9c5568..c1f63d9c29577 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/entries.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/entries.ts @@ -4,14 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; - -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { schema } from '@kbn/config-schema'; - -import { throwErrors } from '../../../common/runtime_types'; +import { createValidationFunction } from '../../../common/runtime_types'; import { InfraBackendLibs } from '../../lib/infra_types'; import { @@ -22,22 +15,16 @@ import { import { parseFilterQuery } from '../../utils/serialized_query'; import { LogEntriesParams } from '../../lib/domains/log_entries_domain'; -const escapeHatch = schema.object({}, { unknowns: 'allow' }); - export const initLogEntriesRoute = ({ framework, logEntries }: InfraBackendLibs) => { framework.registerRoute( { method: 'post', path: LOG_ENTRIES_PATH, - validate: { body: escapeHatch }, + validate: { body: createValidationFunction(logEntriesRequestRT) }, }, async (requestContext, request, response) => { try { - const payload = pipe( - logEntriesRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - + const payload = request.body; const { startTimestamp: startTimestamp, endTimestamp: endTimestamp, diff --git a/x-pack/plugins/infra/server/routes/log_entries/item.ts b/x-pack/plugins/infra/server/routes/log_entries/item.ts index 85dba8f598a89..67ca481ff4fcb 100644 --- a/x-pack/plugins/infra/server/routes/log_entries/item.ts +++ b/x-pack/plugins/infra/server/routes/log_entries/item.ts @@ -4,14 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import Boom from 'boom'; - -import { pipe } from 'fp-ts/lib/pipeable'; -import { fold } from 'fp-ts/lib/Either'; -import { identity } from 'fp-ts/lib/function'; -import { schema } from '@kbn/config-schema'; - -import { throwErrors } from '../../../common/runtime_types'; +import { createValidationFunction } from '../../../common/runtime_types'; import { InfraBackendLibs } from '../../lib/infra_types'; import { @@ -20,22 +13,16 @@ import { logEntriesItemResponseRT, } from '../../../common/http_api'; -const escapeHatch = schema.object({}, { unknowns: 'allow' }); - export const initLogEntriesItemRoute = ({ framework, sources, logEntries }: InfraBackendLibs) => { framework.registerRoute( { method: 'post', path: LOG_ENTRIES_ITEM_PATH, - validate: { body: escapeHatch }, + validate: { body: createValidationFunction(logEntriesItemRequestRT) }, }, async (requestContext, request, response) => { try { - const payload = pipe( - logEntriesItemRequestRT.decode(request.body), - fold(throwErrors(Boom.badRequest), identity) - ); - + const payload = request.body; const { id, sourceId } = payload; const sourceConfiguration = ( await sources.getSourceConfiguration(requestContext.core.savedObjects.client, sourceId) 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 876bbb4199441..8ab0f4a44c85d 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 @@ -7,9 +7,9 @@ import { uniq } from 'lodash'; import LRU from 'lru-cache'; import { MetricsExplorerRequestBody } from '../../../../common/http_api'; -import { ESSearchClient } from '../../../lib/snapshot'; import { getDatasetForField } from './get_dataset_for_field'; import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; +import { ESSearchClient } from '../../../lib/metrics/types'; const cache = new LRU({ max: 100, 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 94e91d32b14bb..85bb5b106c87c 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 @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESSearchClient } from '../../../lib/snapshot'; +import { ESSearchClient } from '../../../lib/metrics/types'; interface EventDatasetHit { _source: { diff --git a/x-pack/plugins/infra/server/routes/snapshot/index.ts b/x-pack/plugins/infra/server/routes/snapshot/index.ts index 00bc1e74ea871..3f09ae89bc97e 100644 --- a/x-pack/plugins/infra/server/routes/snapshot/index.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/index.ts @@ -10,10 +10,10 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { InfraBackendLibs } from '../../lib/infra_types'; import { UsageCollector } from '../../usage/usage_collector'; -import { parseFilterQuery } from '../../utils/serialized_query'; import { SnapshotRequestRT, SnapshotNodeResponseRT } from '../../../common/http_api/snapshot_api'; import { throwErrors } from '../../../common/runtime_types'; import { createSearchClient } from '../../lib/create_search_client'; +import { getNodes } from './lib/get_nodes'; const escapeHatch = schema.object({}, { unknowns: 'allow' }); @@ -30,43 +30,22 @@ export const initSnapshotRoute = (libs: InfraBackendLibs) => { }, async (requestContext, request, response) => { try { - const { - filterQuery, - nodeType, - groupBy, - sourceId, - metrics, - timerange, - accountId, - region, - includeTimeseries, - overrideCompositeSize, - } = pipe( + const snapshotRequest = pipe( SnapshotRequestRT.decode(request.body), fold(throwErrors(Boom.badRequest), identity) ); + const source = await libs.sources.getSourceConfiguration( requestContext.core.savedObjects.client, - sourceId + snapshotRequest.sourceId ); - UsageCollector.countNode(nodeType); - const options = { - filterQuery: parseFilterQuery(filterQuery), - accountId, - region, - nodeType, - groupBy, - sourceConfiguration: source.configuration, - metrics, - timerange, - includeTimeseries, - overrideCompositeSize, - }; + UsageCollector.countNode(snapshotRequest.nodeType); const client = createSearchClient(requestContext, framework); - const nodesWithInterval = await libs.snapshot.getNodes(client, options); + const snapshotResponse = await getNodes(client, snapshotRequest, source); + return response.ok({ - body: SnapshotNodeResponseRT.encode(nodesWithInterval), + body: SnapshotNodeResponseRT.encode(snapshotResponse), }); } catch (error) { return response.internalError({ 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 new file mode 100644 index 0000000000000..f41d76bbc156f --- /dev/null +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/apply_metadata_to_last_path.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, last, first, isArray } from 'lodash'; +import { findInventoryFields } from '../../../../common/inventory_models'; +import { + SnapshotRequest, + SnapshotNodePath, + SnapshotNode, + MetricsAPISeries, + MetricsAPIRow, +} from '../../../../common/http_api'; +import { META_KEY } from './constants'; +import { InfraSource } from '../../../lib/sources'; + +export const isIPv4 = (subject: string) => /^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(subject); + +type RowWithMetadata = MetricsAPIRow & { + [META_KEY]: object[]; +}; + +export const applyMetadataToLastPath = ( + series: MetricsAPISeries, + node: SnapshotNode, + snapshotRequest: SnapshotRequest, + source: InfraSource +): SnapshotNodePath[] => { + // First we need to find a row with metadata + const rowWithMeta = series.rows.find( + (row) => (row[META_KEY] && isArray(row[META_KEY]) && (row[META_KEY] as object[]).length) || 0 + ) as RowWithMetadata | undefined; + + if (rowWithMeta) { + // We need just the first doc, there should only be one + const firstMetaDoc = first(rowWithMeta[META_KEY]); + // We also need the last path to add the metadata to + const lastPath = last(node.path); + 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 + ); + // Set the label as the name and fallback to the id OR path.value + lastPath.label = get(firstMetaDoc, inventoryFields.name, lastPath.value); + // If the inventory fields contain an ip address, we need to try and set that + // on the path object. IP addersses are typically stored as multiple fields. We will + // use the first IPV4 address we find. + if (inventoryFields.ip) { + const ipAddresses = get(firstMetaDoc, inventoryFields.ip) as string[]; + if (Array.isArray(ipAddresses)) { + lastPath.ip = ipAddresses.find(isIPv4) || null; + } else if (typeof ipAddresses === 'string') { + lastPath.ip = ipAddresses; + } + } + return [...node.path.slice(0, node.path.length - 1), lastPath]; + } + } + return node.path; +}; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts new file mode 100644 index 0000000000000..4218aecfe74a8 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/calculate_index_pattern_based_on_metrics.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotRequest } from '../../../../common/http_api'; +import { InfraSource } from '../../../lib/sources'; + +export const calculateIndexPatterBasedOnMetrics = ( + options: SnapshotRequest, + source: InfraSource +) => { + const { metrics } = options; + if (metrics.every((m) => m.type === 'logRate')) { + return source.configuration.logAlias; + } + if (metrics.some((m) => m.type === 'logRate')) { + return `${source.configuration.logAlias},${source.configuration.metricAlias}`; + } + return source.configuration.metricAlias; +}; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/constants.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/constants.ts new file mode 100644 index 0000000000000..563c720224435 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/constants.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const META_KEY = '__metadata__'; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/copy_missing_metrics.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/copy_missing_metrics.ts new file mode 100644 index 0000000000000..36397862e4153 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/copy_missing_metrics.ts @@ -0,0 +1,45 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { memoize, last, first } from 'lodash'; +import { SnapshotNode, SnapshotNodeResponse } from '../../../../common/http_api'; + +const createMissingMetricFinder = (nodes: SnapshotNode[]) => + memoize((id: string) => { + const nodeWithMetrics = nodes.find((node) => { + const lastPath = last(node.path); + const metric = first(node.metrics); + return lastPath && metric && lastPath.value === id && metric.value !== null; + }); + if (nodeWithMetrics) { + return nodeWithMetrics.metrics; + } + }); + +/** + * This function will look for nodes with missing data and try to find a node to copy the data from. + * This functionality exists to suppor the use case where the user requests a group by on "Service type". + * Since that grouping naturally excludeds every metric (except the metric for the service.type), we still + * want to display the node with a value. A good example is viewing hosts by CPU Usage and grouping by service + * Without this every service but `system` would be null. + */ +export const copyMissingMetrics = (response: SnapshotNodeResponse) => { + const { nodes } = response; + const find = createMissingMetricFinder(nodes); + const newNodes = nodes.map((node) => { + const lastPath = last(node.path); + const metric = first(node.metrics); + const allRowsNull = metric?.timeseries?.rows.every((r) => r.metric_0 == null) ?? true; + if (lastPath && metric && metric.value === null && allRowsNull) { + const newMetrics = find(lastPath.value); + if (newMetrics) { + return { ...node, metrics: newMetrics }; + } + } + return node; + }); + return { ...response, nodes: newNodes }; +}; diff --git a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts similarity index 80% rename from x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts rename to x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts index 719ffdb8fa7c4..827e0901c1c01 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/create_timerange_with_interval.ts +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/create_timerange_with_interval.ts @@ -5,14 +5,16 @@ */ import { uniq } from 'lodash'; -import { InfraSnapshotRequestOptions } from './types'; -import { getMetricsAggregations } from './query_helpers'; -import { calculateMetricInterval } from '../../utils/calculate_metric_interval'; -import { MetricsUIAggregation, ESBasicMetricAggRT } from '../../../common/inventory_models/types'; -import { getDatasetForField } from '../../routes/metrics_explorer/lib/get_dataset_for_field'; -import { InfraTimerangeInput } from '../../../common/http_api/snapshot_api'; -import { ESSearchClient } from '.'; -import { getIntervalInSeconds } from '../../utils/get_interval_in_seconds'; +import { InfraTimerangeInput } from '../../../../common/http_api'; +import { ESSearchClient } from '../../../lib/metrics/types'; +import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; +import { calculateMetricInterval } from '../../../utils/calculate_metric_interval'; +import { getMetricsAggregations, InfraSnapshotRequestOptions } from './get_metrics_aggregations'; +import { + MetricsUIAggregation, + ESBasicMetricAggRT, +} from '../../../../common/inventory_models/types'; +import { getDatasetForField } from '../../metrics_explorer/lib/get_dataset_for_field'; const createInterval = async (client: ESSearchClient, options: InfraSnapshotRequestOptions) => { const { timerange } = options; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts new file mode 100644 index 0000000000000..2421469eb1bdd --- /dev/null +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_metrics_aggregations.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { JsonObject } from '../../../../common/typed_json'; +import { + InventoryItemType, + MetricsUIAggregation, + MetricsUIAggregationRT, +} from '../../../../common/inventory_models/types'; +import { + SnapshotMetricInput, + SnapshotCustomMetricInputRT, + SnapshotRequest, +} from '../../../../common/http_api'; +import { findInventoryModel } from '../../../../common/inventory_models'; +import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; +import { InfraSourceConfiguration } from '../../../lib/sources'; + +export interface InfraSnapshotRequestOptions + extends Omit { + sourceConfiguration: InfraSourceConfiguration; + filterQuery: JsonObject | undefined; +} + +export const metricToAggregation = ( + nodeType: InventoryItemType, + metric: SnapshotMetricInput, + index: number +) => { + const inventoryModel = findInventoryModel(nodeType); + if (SnapshotCustomMetricInputRT.is(metric)) { + if (metric.aggregation === 'rate') { + return networkTraffic(`custom_${index}`, metric.field); + } + return { + [`custom_${index}`]: { + [metric.aggregation]: { + field: metric.field, + }, + }, + }; + } + return inventoryModel.metrics.snapshot?.[metric.type]; +}; + +export const getMetricsAggregations = ( + options: InfraSnapshotRequestOptions +): MetricsUIAggregation => { + const { metrics } = options; + return metrics.reduce((aggs, metric, index) => { + const aggregation = metricToAggregation(options.nodeType, metric, index); + if (!MetricsUIAggregationRT.is(aggregation)) { + throw new Error( + i18n.translate('xpack.infra.snapshot.missingSnapshotMetricError', { + defaultMessage: 'The aggregation for {metric} for {nodeType} is not available.', + values: { + nodeType: options.nodeType, + metric: metric.type, + }, + }) + ); + } + return { ...aggs, ...aggregation }; + }, {}); +}; 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 new file mode 100644 index 0000000000000..9332d5aee1f52 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/get_nodes.ts @@ -0,0 +1,34 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotRequest } from '../../../../common/http_api'; +import { ESSearchClient } from '../../../lib/metrics/types'; +import { InfraSource } from '../../../lib/sources'; +import { transformRequestToMetricsAPIRequest } from './transform_request_to_metrics_api_request'; +import { queryAllData } from './query_all_data'; +import { transformMetricsApiResponseToSnapshotResponse } from './trasform_metrics_ui_response'; +import { copyMissingMetrics } from './copy_missing_metrics'; + +export const getNodes = async ( + client: ESSearchClient, + snapshotRequest: SnapshotRequest, + source: InfraSource +) => { + const metricsApiRequest = await transformRequestToMetricsAPIRequest( + client, + source, + snapshotRequest + ); + const metricsApiResponse = await queryAllData(client, metricsApiRequest); + return copyMissingMetrics( + transformMetricsApiResponseToSnapshotResponse( + metricsApiRequest, + snapshotRequest, + source, + metricsApiResponse + ) + ); +}; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/query_all_data.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/query_all_data.ts new file mode 100644 index 0000000000000..a9d2352cf55b7 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/query_all_data.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MetricsAPIRequest, MetricsAPIResponse } from '../../../../common/http_api'; +import { ESSearchClient } from '../../../lib/metrics/types'; +import { query } from '../../../lib/metrics'; + +const handleResponse = ( + client: ESSearchClient, + options: MetricsAPIRequest, + previousResponse?: MetricsAPIResponse +) => async (resp: MetricsAPIResponse): Promise => { + const combinedResponse = previousResponse + ? { + ...previousResponse, + series: [...previousResponse.series, ...resp.series], + info: resp.info, + } + : resp; + if (resp.info.afterKey) { + return query(client, { ...options, afterKey: resp.info.afterKey }).then( + handleResponse(client, options, combinedResponse) + ); + } + return combinedResponse; +}; + +export const queryAllData = (client: ESSearchClient, options: MetricsAPIRequest) => { + return query(client, options).then(handleResponse(client, options)); +}; 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 new file mode 100644 index 0000000000000..700f4ef39bb66 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_request_to_metrics_api_request.ts @@ -0,0 +1,84 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { findInventoryFields } from '../../../../common/inventory_models'; +import { MetricsAPIRequest, SnapshotRequest } from '../../../../common/http_api'; +import { ESSearchClient } from '../../../lib/metrics/types'; +import { InfraSource } from '../../../lib/sources'; +import { createTimeRangeWithInterval } from './create_timerange_with_interval'; +import { parseFilterQuery } from '../../../utils/serialized_query'; +import { transformSnapshotMetricsToMetricsAPIMetrics } from './transform_snapshot_metrics_to_metrics_api_metrics'; +import { calculateIndexPatterBasedOnMetrics } from './calculate_index_pattern_based_on_metrics'; +import { META_KEY } from './constants'; + +export const transformRequestToMetricsAPIRequest = async ( + client: ESSearchClient, + source: InfraSource, + snapshotRequest: SnapshotRequest +): Promise => { + const timeRangeWithIntervalApplied = await createTimeRangeWithInterval(client, { + ...snapshotRequest, + filterQuery: parseFilterQuery(snapshotRequest.filterQuery), + sourceConfiguration: source.configuration, + }); + + const metricsApiRequest: MetricsAPIRequest = { + indexPattern: calculateIndexPatterBasedOnMetrics(snapshotRequest, source), + timerange: { + field: source.configuration.fields.timestamp, + from: timeRangeWithIntervalApplied.from, + to: timeRangeWithIntervalApplied.to, + interval: timeRangeWithIntervalApplied.interval, + }, + metrics: transformSnapshotMetricsToMetricsAPIMetrics(snapshotRequest), + limit: snapshotRequest.overrideCompositeSize ? snapshotRequest.overrideCompositeSize : 10, + alignDataToEnd: true, + }; + + const filters = []; + const parsedFilters = parseFilterQuery(snapshotRequest.filterQuery); + if (parsedFilters) { + filters.push(parsedFilters); + } + + if (snapshotRequest.accountId) { + filters.push({ term: { 'cloud.account.id': snapshotRequest.accountId } }); + } + + if (snapshotRequest.region) { + filters.push({ term: { 'cloud.region': snapshotRequest.region } }); + } + + const inventoryFields = findInventoryFields( + snapshotRequest.nodeType, + source.configuration.fields + ); + const groupBy = snapshotRequest.groupBy.map((g) => g.field).filter(Boolean) as string[]; + metricsApiRequest.groupBy = [...groupBy, inventoryFields.id]; + + const metaAggregation = { + id: META_KEY, + aggregations: { + [META_KEY]: { + top_hits: { + size: 1, + _source: [inventoryFields.name], + sort: [{ [source.configuration.fields.timestamp]: 'desc' }], + }, + }, + }, + }; + if (inventoryFields.ip) { + metaAggregation.aggregations[META_KEY].top_hits._source.push(inventoryFields.ip); + } + metricsApiRequest.metrics.push(metaAggregation); + + if (filters.length) { + metricsApiRequest.filters = filters; + } + + return metricsApiRequest; +}; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/transform_snapshot_metrics_to_metrics_api_metrics.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_snapshot_metrics_to_metrics_api_metrics.ts new file mode 100644 index 0000000000000..6f7c88eda5d7a --- /dev/null +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/transform_snapshot_metrics_to_metrics_api_metrics.ts @@ -0,0 +1,38 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { networkTraffic } from '../../../../common/inventory_models/shared/metrics/snapshot/network_traffic'; +import { findInventoryModel } from '../../../../common/inventory_models'; +import { + MetricsAPIMetric, + SnapshotRequest, + SnapshotCustomMetricInputRT, +} from '../../../../common/http_api'; + +export const transformSnapshotMetricsToMetricsAPIMetrics = ( + snapshotRequest: SnapshotRequest +): MetricsAPIMetric[] => { + return snapshotRequest.metrics.map((metric, index) => { + const inventoryModel = findInventoryModel(snapshotRequest.nodeType); + if (SnapshotCustomMetricInputRT.is(metric)) { + const customId = `custom_${index}`; + if (metric.aggregation === 'rate') { + return { id: customId, aggregations: networkTraffic(customId, metric.field) }; + } + return { + id: customId, + aggregations: { + [customId]: { + [metric.aggregation]: { + field: metric.field, + }, + }, + }, + }; + } + return { id: metric.type, aggregations: inventoryModel.metrics.snapshot?.[metric.type] }; + }); +}; diff --git a/x-pack/plugins/infra/server/routes/snapshot/lib/trasform_metrics_ui_response.ts b/x-pack/plugins/infra/server/routes/snapshot/lib/trasform_metrics_ui_response.ts new file mode 100644 index 0000000000000..309598d71c361 --- /dev/null +++ b/x-pack/plugins/infra/server/routes/snapshot/lib/trasform_metrics_ui_response.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, max, sum, last, isNumber } from 'lodash'; +import { SnapshotMetricType } from '../../../../common/inventory_models/types'; +import { + MetricsAPIResponse, + SnapshotNodeResponse, + MetricsAPIRequest, + MetricsExplorerColumnType, + MetricsAPIRow, + SnapshotRequest, + SnapshotNodePath, + SnapshotNodeMetric, +} from '../../../../common/http_api'; +import { META_KEY } from './constants'; +import { InfraSource } from '../../../lib/sources'; +import { applyMetadataToLastPath } from './apply_metadata_to_last_path'; + +const getMetricValue = (row: MetricsAPIRow) => { + if (!isNumber(row.metric_0)) return null; + const value = row.metric_0; + return isFinite(value) ? value : null; +}; + +const calculateMax = (rows: MetricsAPIRow[]) => { + return max(rows.map(getMetricValue)) || 0; +}; + +const calculateAvg = (rows: MetricsAPIRow[]): number => { + return sum(rows.map(getMetricValue)) / rows.length || 0; +}; + +const getLastValue = (rows: MetricsAPIRow[]) => { + const row = last(rows); + if (!row) return null; + return getMetricValue(row); +}; + +export const transformMetricsApiResponseToSnapshotResponse = ( + options: MetricsAPIRequest, + snapshotRequest: SnapshotRequest, + source: InfraSource, + metricsApiResponse: MetricsAPIResponse +): SnapshotNodeResponse => { + const nodes = metricsApiResponse.series.map((series) => { + const node = { + metrics: options.metrics + .filter((m) => m.id !== META_KEY) + .map((metric) => { + const name = metric.id as SnapshotMetricType; + const timeseries = { + id: name, + columns: [ + { name: 'timestamp', type: 'date' as MetricsExplorerColumnType }, + { name: 'metric_0', type: 'number' as MetricsExplorerColumnType }, + ], + rows: series.rows.map((row) => { + return { timestamp: row.timestamp, metric_0: get(row, metric.id, null) }; + }), + }; + const maxValue = calculateMax(timeseries.rows); + const avg = calculateAvg(timeseries.rows); + const value = getLastValue(timeseries.rows); + const nodeMetric: SnapshotNodeMetric = { name, max: maxValue, value, avg }; + if (snapshotRequest.includeTimeseries) { + nodeMetric.timeseries = timeseries; + } + return nodeMetric; + }), + path: + series.keys?.map((key) => { + return { value: key, label: key } as SnapshotNodePath; + }) ?? [], + name: '', + }; + + const path = applyMetadataToLastPath(series, node, snapshotRequest, source); + const lastPath = last(path); + const name = (lastPath && lastPath.label) || 'N/A'; + return { ...node, path, name }; + }); + return { nodes, interval: `${metricsApiResponse.info.interval}s` }; +}; 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 a3d674b324ae8..6d16e045d26d5 100644 --- a/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts +++ b/x-pack/plugins/infra/server/utils/calculate_metric_interval.ts @@ -8,7 +8,7 @@ import { findInventoryModel } from '../../common/inventory_models'; // import { KibanaFramework } from '../lib/adapters/framework/kibana_framework_adapter'; import { InventoryItemType } from '../../common/inventory_models/types'; -import { ESSearchClient } from '../lib/snapshot'; +import { ESSearchClient } from '../lib/metrics/types'; interface Options { indexPattern: string; diff --git a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json index d75a914e080d7..b7856e6d57402 100644 --- a/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json +++ b/x-pack/plugins/ingest_manager/common/openapi/spec_oas3.json @@ -1425,11 +1425,13 @@ }, "icons": [ { - "src": "/package/coredns-1.0.1/img/icon.png", + "path": "/package/coredns-1.0.1/img/icon.png", + "src": "/img/icon.png", "size": "1800x1800" }, { - "src": "/package/coredns-1.0.1/img/icon.svg", + "path": "/package/coredns-1.0.1/img/icon.svg", + "src": "/img/icon.svg", "size": "255x144", "type": "image/svg+xml" } @@ -1704,7 +1706,8 @@ }, "icons": [ { - "src": "/package/endpoint/0.3.0/img/logo-endpoint-64-color.svg", + "path": "/package/endpoint/0.3.0/img/logo-endpoint-64-color.svg", + "src": "/img/logo-endpoint-64-color.svg", "size": "16x16", "type": "image/svg+xml" } @@ -2001,7 +2004,8 @@ "download": "/epr/aws/aws-0.0.3.tar.gz", "icons": [ { - "src": "/package/aws/0.0.3/img/logo_aws.svg", + "path": "/package/aws/0.0.3/img/logo_aws.svg", + "src": "/img/logo_aws.svg", "title": "logo aws", "size": "32x32", "type": "image/svg+xml" @@ -2019,7 +2023,8 @@ "download": "/epr/endpoint/endpoint-0.1.0.tar.gz", "icons": [ { - "src": "/package/endpoint/0.1.0/img/logo-endpoint-64-color.svg", + "path": "/package/endpoint/0.1.0/img/logo-endpoint-64-color.svg", + "src": "/img/logo-endpoint-64-color.svg", "size": "16x16", "type": "image/svg+xml" } @@ -2087,7 +2092,8 @@ "download": "/epr/log/log-0.9.0.tar.gz", "icons": [ { - "src": "/package/log/0.9.0/img/icon.svg", + "path": "/package/log/0.9.0/img/icon.svg", + "src": "/img/icon.svg", "type": "image/svg+xml" } ], @@ -2103,7 +2109,8 @@ "download": "/epr/longdocs/longdocs-1.0.4.tar.gz", "icons": [ { - "src": "/package/longdocs/1.0.4/img/icon.svg", + "path": "/package/longdocs/1.0.4/img/icon.svg", + "src": "/img/icon.svg", "type": "image/svg+xml" } ], @@ -2119,7 +2126,8 @@ "download": "/epr/metricsonly/metricsonly-2.0.1.tar.gz", "icons": [ { - "src": "/package/metricsonly/2.0.1/img/icon.svg", + "path": "/package/metricsonly/2.0.1/img/icon.svg", + "src": "/img/icon.svg", "type": "image/svg+xml" } ], @@ -2135,7 +2143,8 @@ "download": "/epr/multiversion/multiversion-1.1.0.tar.gz", "icons": [ { - "src": "/package/multiversion/1.1.0/img/icon.svg", + "path": "/package/multiversion/1.1.0/img/icon.svg", + "src": "/img/icon.svg", "type": "image/svg+xml" } ], @@ -2151,7 +2160,8 @@ "download": "/epr/mysql/mysql-0.1.0.tar.gz", "icons": [ { - "src": "/package/mysql/0.1.0/img/logo_mysql.svg", + "path": "/package/mysql/0.1.0/img/logo_mysql.svg", + "src": "/img/logo_mysql.svg", "title": "logo mysql", "size": "32x32", "type": "image/svg+xml" @@ -2169,7 +2179,8 @@ "download": "/epr/nginx/nginx-0.1.0.tar.gz", "icons": [ { - "src": "/package/nginx/0.1.0/img/logo_nginx.svg", + "path": "/package/nginx/0.1.0/img/logo_nginx.svg", + "src": "/img/logo_nginx.svg", "title": "logo nginx", "size": "32x32", "type": "image/svg+xml" @@ -2187,7 +2198,8 @@ "download": "/epr/redis/redis-0.1.0.tar.gz", "icons": [ { - "src": "/package/redis/0.1.0/img/logo_redis.svg", + "path": "/package/redis/0.1.0/img/logo_redis.svg", + "src": "/img/logo_redis.svg", "title": "logo redis", "size": "32x32", "type": "image/svg+xml" @@ -2205,7 +2217,8 @@ "download": "/epr/reference/reference-1.0.0.tar.gz", "icons": [ { - "src": "/package/reference/1.0.0/img/icon.svg", + "path": "/package/reference/1.0.0/img/icon.svg", + "src": "/img/icon.svg", "size": "32x32", "type": "image/svg+xml" } @@ -2222,7 +2235,8 @@ "download": "/epr/system/system-0.1.0.tar.gz", "icons": [ { - "src": "/package/system/0.1.0/img/system.svg", + "path": "/package/system/0.1.0/img/system.svg", + "src": "/img/system.svg", "title": "system", "size": "1000x1000", "type": "image/svg+xml" @@ -3913,11 +3927,20 @@ "src": { "type": "string" }, + "path": { + "type": "string" + }, "title": { "type": "string" + }, + "size": { + "type": "string" + }, + "type": { + "type": "string" } }, - "required": ["src"] + "required": ["src", "path"] } }, "icons": { diff --git a/x-pack/plugins/ingest_manager/common/types/models/agent.ts b/x-pack/plugins/ingest_manager/common/types/models/agent.ts index 2b8a306577e7d..a204373fe2e56 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/agent.ts @@ -21,7 +21,8 @@ export type AgentStatus = | 'unenrolling' | 'degraded'; -export type AgentActionType = 'CONFIG_CHANGE' | 'DATA_DUMP' | 'RESUME' | 'PAUSE' | 'UNENROLL'; +export type AgentActionType = 'CONFIG_CHANGE' | 'UNENROLL'; + export interface NewAgentAction { type: AgentActionType; data?: any; @@ -29,20 +30,44 @@ export interface NewAgentAction { } export interface AgentAction extends NewAgentAction { + type: AgentActionType; + data?: any; + sent_at?: string; id: string; agent_id: string; created_at: string; + ack_data?: any; +} + +export interface AgentPolicyAction extends NewAgentAction { + id: string; + type: AgentActionType; + data?: any; + policy_id: string; + policy_revision: number; + created_at: string; + ack_data?: any; } -export interface AgentActionSOAttributes { +interface CommonAgentActionSOAttributes { type: AgentActionType; sent_at?: string; timestamp?: string; created_at: string; - agent_id: string; data?: string; + ack_data?: string; } +export type AgentActionSOAttributes = CommonAgentActionSOAttributes & { + agent_id: string; +}; +export type AgentPolicyActionSOAttributes = CommonAgentActionSOAttributes & { + policy_id: string; + policy_revision: number; +}; + +export type BaseAgentActionSOAttributes = AgentActionSOAttributes | AgentPolicyActionSOAttributes; + export interface NewAgentEvent { type: 'STATE' | 'ERROR' | 'ACTION_RESULT' | 'ACTION'; subtype: // State diff --git a/x-pack/plugins/ingest_manager/common/types/models/epm.ts b/x-pack/plugins/ingest_manager/common/types/models/epm.ts index 140a76ac85e61..8bc5d9f7210b2 100644 --- a/x-pack/plugins/ingest_manager/common/types/models/epm.ts +++ b/x-pack/plugins/ingest_manager/common/types/models/epm.ts @@ -19,6 +19,8 @@ export enum InstallStatus { uninstalling = 'uninstalling', } +export type InstallType = 'reinstall' | 'reupdate' | 'rollback' | 'update' | 'install'; + export type EpmPackageInstallStatus = 'installed' | 'installing'; export type DetailViewPanelName = 'overview' | 'usages' | 'settings'; @@ -38,6 +40,7 @@ export enum ElasticsearchAssetType { ingestPipeline = 'ingest_pipeline', indexTemplate = 'index_template', ilmPolicy = 'ilm_policy', + transform = 'transform', } export enum AgentAssetType { @@ -71,10 +74,8 @@ export interface RegistryPackage { } interface RegistryImage { - // https://github.com/elastic/package-registry/blob/master/util/package.go#L74 - // says src is potentially missing but I couldn't find any examples - // it seems like src should be required. How can you have an image with no reference to the content? src: string; + path: string; title?: string; size?: string; type?: string; diff --git a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts index cf8d3ab1c908a..54cdeade3764e 100644 --- a/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts +++ b/x-pack/plugins/ingest_manager/common/types/rest_spec/agent.ts @@ -7,11 +7,11 @@ import { Agent, AgentAction, + NewAgentAction, NewAgentEvent, AgentEvent, AgentStatus, AgentType, - NewAgentAction, } from '../models'; export interface GetAgentsRequest { diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts index e5a7191372e9c..690ffdf46f704 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/hooks/use_package_icon_type.ts @@ -42,7 +42,7 @@ export const usePackageIconType = ({ const svgIcons = (paramIcons || iconList)?.filter( (iconDef) => iconDef.type === 'image/svg+xml' ); - const localIconSrc = Array.isArray(svgIcons) && svgIcons[0]?.src; + const localIconSrc = Array.isArray(svgIcons) && (svgIcons[0].path || svgIcons[0].src); if (localIconSrc) { CACHED_ICONS.set(pkgKey, toImage(localIconSrc)); setIconType(CACHED_ICONS.get(pkgKey) || ''); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx index 9f48be54f866d..ccf9e45ebc4fa 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/create_package_policy_page/step_select_agent_policy.tsx @@ -83,7 +83,7 @@ export const StepSelectAgentPolicy: React.FunctionComponent<{ data: agentPoliciesData, error: agentPoliciesError, isLoading: isAgentPoliciesLoading, - sendRequest: refreshAgentPolicies, + resendRequest: refreshAgentPolicies, } = useGetAgentPolicies({ page: 1, perPage: 1000, diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/hooks/use_agent_status.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/hooks/use_agent_status.tsx index 71dcd728d5d1b..3483d8dee045a 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/hooks/use_agent_status.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/details_page/hooks/use_agent_status.tsx @@ -25,7 +25,7 @@ export function useGetAgentStatus(policyId?: string, options?: RequestOptions) { isLoading: agentStatusRequest.isLoading, data: agentStatusRequest.data, error: agentStatusRequest.error, - refreshAgentStatus: () => agentStatusRequest.sendRequest, + refreshAgentStatus: () => agentStatusRequest.resendRequest, }; } diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/index.tsx index 361b1c33f1a04..fb963dc67ae1c 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/agent_policy/list_page/index.tsx @@ -108,7 +108,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { ); // Fetch agent policies - const { isLoading, data: agentPolicyData, sendRequest } = useGetAgentPolicies({ + const { isLoading, data: agentPolicyData, resendRequest } = useGetAgentPolicies({ page: pagination.currentPage, perPage: pagination.pageSize, sortField: sorting?.field, @@ -204,7 +204,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { render: (agentPolicy: AgentPolicy) => ( sendRequest()} + onCopySuccess={() => resendRequest()} /> ), }, @@ -218,7 +218,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { } return cols; - }, [getHref, isFleetEnabled, sendRequest]); + }, [getHref, isFleetEnabled, resendRequest]); const createAgentPolicyButton = useMemo( () => ( @@ -270,7 +270,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { { setIsCreateAgentPolicyFlyoutOpen(false); - sendRequest(); + resendRequest(); }} /> ) : null} @@ -289,7 +289,7 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => { /> - sendRequest()}> + resendRequest()}> = () => { const { pagination, pageSizeOptions } = usePagination(); // Fetch data streams - const { isLoading, data: dataStreamsData, sendRequest } = useGetDataStreams(); + const { isLoading, data: dataStreamsData, resendRequest } = useGetDataStreams(); // Some policies retrieved, set up table props const columns = useMemo(() => { @@ -241,7 +241,7 @@ export const DataStreamListPage: React.FunctionComponent<{}> = () => { key="reloadButton" color="primary" iconType="refresh" - onClick={() => sendRequest()} + onClick={() => resendRequest()} > = { dashboard: 'Dashboard', ilm_policy: 'ILM Policy', ingest_pipeline: 'Ingest Pipeline', + transform: 'Transform', 'index-pattern': 'Index Pattern', index_template: 'Index Template', component_template: 'Component Template', diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx index d8388a71556d6..6326e9072be8e 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/epm/screens/detail/screenshots.tsx @@ -75,7 +75,7 @@ export function Screenshots(props: ScreenshotProps) { set image to same width. Will need to update if size changes. */} = ({ ag [key: string]: JSX.Element; }>({}); - const { isLoading, data, sendRequest } = useGetOneAgentEvents(agent.id, { + const { isLoading, data, resendRequest } = useGetOneAgentEvents(agent.id, { page: pagination.currentPage, perPage: pagination.pageSize, kuery: search && search.trim() !== '' ? search.trim() : undefined, }); - const refresh = () => sendRequest(); + const refresh = () => resendRequest(); const total = data ? data.total : 0; const list = data ? data.list : []; diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx index 219b343eba41b..fe0781f4a240b 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_details_page/index.tsx @@ -51,7 +51,7 @@ export const AgentDetailsPage: React.FunctionComponent = () => { isInitialRequest, error, data: agentData, - sendRequest: sendAgentRequest, + resendRequest: sendAgentRequest, } = useGetOneAgent(agentId, { pollIntervalMs: 5000, }); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx index 9548340df5b30..46f7ffb85b21f 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/agent_list_page/index.tsx @@ -344,7 +344,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { return ( agentsRequest.sendRequest()} + refresh={() => agentsRequest.resendRequest()} onReassignClick={() => setAgentToReassignId(agent.id)} /> ); @@ -394,7 +394,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => { agent={agentToReassign} onClose={() => { setAgentToReassignId(undefined); - agentsRequest.sendRequest(); + agentsRequest.resendRequest(); }} /> diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx index 7db9d72eb50e4..04fef7f4b3f21 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/managed_instructions.tsx @@ -24,7 +24,7 @@ interface Props { agentPolicies?: AgentPolicy[]; } -export const ManagedInstructions: React.FunctionComponent = ({ agentPolicies }) => { +export const ManagedInstructions = React.memo(({ agentPolicies }) => { const { getHref } = useLink(); const core = useCore(); const fleetStatus = useFleetStatus(); @@ -91,4 +91,4 @@ export const ManagedInstructions: React.FunctionComponent = ({ agentPolic )} ); -}; +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx index 9262cc2cb42ac..387ccfc66cbc1 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/components/agent_enrollment_flyout/standalone_instructions.tsx @@ -31,7 +31,7 @@ interface Props { const RUN_INSTRUCTIONS = './elastic-agent run'; -export const StandaloneInstructions: React.FunctionComponent = ({ agentPolicies }) => { +export const StandaloneInstructions = React.memo(({ agentPolicies }) => { const { getHref } = useLink(); const core = useCore(); const { notifications } = core; @@ -189,4 +189,4 @@ export const StandaloneInstructions: React.FunctionComponent = ({ agentPo ); -}; +}); diff --git a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx index b3a4938b22310..d85a6e8b5b833 100644 --- a/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx +++ b/x-pack/plugins/ingest_manager/public/applications/ingest_manager/sections/fleet/enrollment_token_list_page/index.tsx @@ -244,7 +244,10 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { render: (_: any, apiKey: EnrollmentAPIKey) => { return ( apiKey.active && ( - enrollmentAPIKeysRequest.sendRequest()} /> + enrollmentAPIKeysRequest.resendRequest()} + /> ) ); }, @@ -258,7 +261,7 @@ export const EnrollmentTokenListPage: React.FunctionComponent<{}> = () => { agentPolicies={agentPolicies} onClose={() => { setFlyoutOpen(false); - enrollmentAPIKeysRequest.sendRequest(); + enrollmentAPIKeysRequest.resendRequest(); }} /> )} diff --git a/x-pack/plugins/ingest_manager/server/errors.test.ts b/x-pack/plugins/ingest_manager/server/errors/handlers.test.ts similarity index 73% rename from x-pack/plugins/ingest_manager/server/errors.test.ts rename to x-pack/plugins/ingest_manager/server/errors/handlers.test.ts index 70e3a3b4150ad..361386a86d547 100644 --- a/x-pack/plugins/ingest_manager/server/errors.test.ts +++ b/x-pack/plugins/ingest_manager/server/errors/handlers.test.ts @@ -5,16 +5,19 @@ */ import Boom from 'boom'; +import { errors } from 'elasticsearch'; import { httpServerMock } from 'src/core/server/mocks'; -import { createAppContextStartContractMock } from './mocks'; - +import { createAppContextStartContractMock } from '../mocks'; +import { appContextService } from '../services'; import { IngestManagerError, RegistryError, PackageNotFoundError, defaultIngestErrorHandler, -} from './errors'; -import { appContextService } from './services'; +} from './index'; + +const LegacyESErrors = errors as Record; +type ITestEsErrorsFnParams = [errorCode: string, error: any, expectedMessage: string]; describe('defaultIngestErrorHandler', () => { let mockContract: ReturnType; @@ -29,6 +32,55 @@ describe('defaultIngestErrorHandler', () => { appContextService.stop(); }); + async function testEsErrorsFn(...args: ITestEsErrorsFnParams) { + const [, error, expectedMessage] = args; + jest.clearAllMocks(); + const response = httpServerMock.createResponseFactory(); + await defaultIngestErrorHandler({ error, response }); + + // response + expect(response.ok).toHaveBeenCalledTimes(0); + expect(response.customError).toHaveBeenCalledTimes(1); + expect(response.customError).toHaveBeenCalledWith({ + statusCode: error.status, + body: { message: expectedMessage }, + }); + + // logging + expect(mockContract.logger?.error).toHaveBeenCalledTimes(1); + expect(mockContract.logger?.error).toHaveBeenCalledWith(expectedMessage); + } + + describe('use the HTTP error status code provided by LegacyESErrors', () => { + const statusCodes = Object.keys(LegacyESErrors).filter((key) => /^\d+$/.test(key)); + const errorCodes = statusCodes.filter((key) => parseInt(key, 10) >= 400); + const casesWithPathResponse: ITestEsErrorsFnParams[] = errorCodes.map((errorCode) => [ + errorCode, + new LegacyESErrors[errorCode]('the root message', { + path: '/path/to/call', + response: 'response is here', + }), + 'the root message response from /path/to/call: response is here', + ]); + const casesWithOtherMeta: ITestEsErrorsFnParams[] = errorCodes.map((errorCode) => [ + errorCode, + new LegacyESErrors[errorCode]('the root message', { + other: '/path/to/call', + props: 'response is here', + }), + 'the root message', + ]); + const casesWithoutMeta: ITestEsErrorsFnParams[] = errorCodes.map((errorCode) => [ + errorCode, + new LegacyESErrors[errorCode]('some message'), + 'some message', + ]); + + test.each(casesWithPathResponse)('%d - with path & response', testEsErrorsFn); + test.each(casesWithOtherMeta)('%d - with other metadata', testEsErrorsFn); + test.each(casesWithoutMeta)('%d - without metadata', testEsErrorsFn); + }); + describe('IngestManagerError', () => { it('502: RegistryError', async () => { const error = new RegistryError('xyz'); diff --git a/x-pack/plugins/ingest_manager/server/errors.ts b/x-pack/plugins/ingest_manager/server/errors/handlers.ts similarity index 60% rename from x-pack/plugins/ingest_manager/server/errors.ts rename to x-pack/plugins/ingest_manager/server/errors/handlers.ts index 9829a4de23d7b..9f776565cf262 100644 --- a/x-pack/plugins/ingest_manager/server/errors.ts +++ b/x-pack/plugins/ingest_manager/server/errors/handlers.ts @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable max-classes-per-file */ import Boom, { isBoom } from 'boom'; import { RequestHandlerContext, @@ -12,25 +11,39 @@ import { IKibanaResponse, KibanaResponseFactory, } from 'src/core/server'; -import { appContextService } from './services'; +import { errors as LegacyESErrors } from 'elasticsearch'; +import { appContextService } from '../services'; +import { IngestManagerError, RegistryError, PackageNotFoundError } from './index'; type IngestErrorHandler = ( params: IngestErrorHandlerParams ) => IKibanaResponse | Promise; - interface IngestErrorHandlerParams { error: IngestManagerError | Boom | Error; response: KibanaResponseFactory; request?: KibanaRequest; context?: RequestHandlerContext; } +// unsure if this is correct. would prefer to use something "official" +// this type is based on BadRequest values observed while debugging https://github.com/elastic/kibana/issues/75862 -export class IngestManagerError extends Error { - constructor(message?: string) { - super(message); - this.name = this.constructor.name; // for stack traces - } +interface LegacyESClientError { + message: string; + stack: string; + status: number; + displayName: string; + path?: string; + query?: string | undefined; + body?: { + error: object; + status: number; + }; + statusCode?: number; + response?: string; } +export const isLegacyESClientError = (error: any): error is LegacyESClientError => { + return error instanceof LegacyESErrors._Abstract; +}; const getHTTPResponseCode = (error: IngestManagerError): number => { if (error instanceof RegistryError) { @@ -48,6 +61,22 @@ export const defaultIngestErrorHandler: IngestErrorHandler = async ({ response, }: IngestErrorHandlerParams): Promise => { const logger = appContextService.getLogger(); + if (isLegacyESClientError(error)) { + // there was a problem communicating with ES (e.g. via `callCluster`) + // only log the message + const message = + error?.path && error?.response + ? // if possible, return the failing endpoint and its response + `${error.message} response from ${error.path}: ${error.response}` + : error.message; + + logger.error(message); + + return response.customError({ + statusCode: error?.statusCode || error.status, + body: { message }, + }); + } // our "expected" errors if (error instanceof IngestManagerError) { @@ -76,9 +105,3 @@ export const defaultIngestErrorHandler: IngestErrorHandler = async ({ body: { message: error.message }, }); }; - -export class RegistryError extends IngestManagerError {} -export class RegistryConnectionError extends RegistryError {} -export class RegistryResponseError extends RegistryError {} -export class PackageNotFoundError extends IngestManagerError {} -export class PackageOutdatedError extends IngestManagerError {} diff --git a/x-pack/plugins/ingest_manager/server/errors/index.ts b/x-pack/plugins/ingest_manager/server/errors/index.ts new file mode 100644 index 0000000000000..5e36a2ec9a884 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/errors/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* eslint-disable max-classes-per-file */ +export { defaultIngestErrorHandler } from './handlers'; + +export class IngestManagerError extends Error { + constructor(message?: string) { + super(message); + this.name = this.constructor.name; // for stack traces + } +} +export class RegistryError extends IngestManagerError {} +export class RegistryConnectionError extends RegistryError {} +export class RegistryResponseError extends RegistryError {} +export class PackageNotFoundError extends IngestManagerError {} +export class PackageOutdatedError extends IngestManagerError {} diff --git a/x-pack/plugins/ingest_manager/server/plugin.ts b/x-pack/plugins/ingest_manager/server/plugin.ts index 4a7677d69d6e7..b10f3527a0459 100644 --- a/x-pack/plugins/ingest_manager/server/plugin.ts +++ b/x-pack/plugins/ingest_manager/server/plugin.ts @@ -173,7 +173,7 @@ export class IngestManagerPlugin // Register feature // TODO: Flesh out privileges if (deps.features) { - deps.features.registerFeature({ + deps.features.registerKibanaFeature({ id: PLUGIN_ID, name: 'Ingest Manager', icon: 'savedObjectsApp', diff --git a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts index b81d44c40f8eb..12a0956b79155 100644 --- a/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/agent/actions_handlers.ts @@ -10,7 +10,6 @@ import { RequestHandler } from 'kibana/server'; import { TypeOf } from '@kbn/config-schema'; import { PostNewAgentActionRequestSchema } from '../../types/rest_spec'; import { ActionsService } from '../../services/agents'; -import { NewAgentAction } from '../../../common/types/models'; import { PostNewAgentActionResponse } from '../../../common/types/rest_spec'; export const postNewAgentActionHandlerBuilder = function ( @@ -26,7 +25,7 @@ export const postNewAgentActionHandlerBuilder = function ( const agent = await actionsService.getAgent(soClient, request.params.agentId); - const newAgentAction = request.body.action as NewAgentAction; + const newAgentAction = request.body.action; const savedAgentAction = await actionsService.createAgentAction(soClient, { created_at: new Date().toISOString(), diff --git a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts index 6d7252ffec41a..385e256933c12 100644 --- a/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts +++ b/x-pack/plugins/ingest_manager/server/routes/epm/handlers.ts @@ -34,6 +34,7 @@ import { } from '../../services/epm/packages'; import { IngestManagerError, defaultIngestErrorHandler } from '../../errors'; import { splitPkgKey } from '../../services/epm/registry'; +import { getInstallType } from '../../services/epm/packages/install'; export const getCategoriesHandler: RequestHandler< undefined, @@ -138,6 +139,8 @@ export const installPackageHandler: RequestHandler< const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; const { pkgkey } = request.params; const { pkgName, pkgVersion } = splitPkgKey(pkgkey); + const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); + const installType = getInstallType({ pkgVersion, installedPkg }); try { const res = await installPackage({ savedObjectsClient, @@ -156,15 +159,25 @@ export const installPackageHandler: RequestHandler< if (e instanceof IngestManagerError) { return defaultResult; } - // if there is an unknown server error, uninstall any package assets + + // if there is an unknown server error, uninstall any package assets or reinstall the previous version if update try { - const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const isUpdate = installedPkg && installedPkg.attributes.version < pkgVersion ? true : false; - if (!isUpdate) { + if (installType === 'install' || installType === 'reinstall') { + logger.error(`uninstalling ${pkgkey} after error installing`); await removeInstallation({ savedObjectsClient, pkgkey, callCluster }); } + if (installType === 'update') { + // @ts-ignore getInstallType ensures we have installedPkg + const prevVersion = `${pkgName}-${installedPkg.attributes.version}`; + logger.error(`rolling back to ${prevVersion} after error installing ${pkgkey}`); + await installPackage({ + savedObjectsClient, + pkgkey: prevVersion, + callCluster, + }); + } } catch (error) { - logger.error(`could not remove failed installation ${error}`); + logger.error(`failed to uninstall or rollback package after installation error ${error}`); } return defaultResult; } diff --git a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts index aff8e607622d4..e86f7b24e2c78 100644 --- a/x-pack/plugins/ingest_manager/server/saved_objects/index.ts +++ b/x-pack/plugins/ingest_manager/server/saved_objects/index.ts @@ -98,8 +98,11 @@ const savedObjectTypes: { [key: string]: SavedObjectsType } = { mappings: { properties: { agent_id: { type: 'keyword' }, + policy_id: { type: 'keyword' }, + policy_revision: { type: 'integer' }, type: { type: 'keyword' }, data: { type: 'binary' }, + ack_data: { type: 'text' }, sent_at: { type: 'date' }, created_at: { type: 'date' }, }, diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts index a03a3b7f59fba..938cfb4351630 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy.ts @@ -21,7 +21,7 @@ import { ListWithKuery, } from '../types'; import { DeleteAgentPolicyResponse, storedPackagePoliciesToAgentInputs } from '../../common'; -import { listAgents } from './agents'; +import { createAgentPolicyAction, listAgents } from './agents'; import { packagePolicyService } from './package_policy'; import { outputService } from './output'; import { agentPolicyUpdateEventHandler } from './agent_policy_update'; @@ -67,6 +67,10 @@ class AgentPolicyService { updated_by: user ? user.username : 'system', }); + if (options.bumpRevision) { + await this.triggerAgentPolicyUpdatedEvent(soClient, 'updated', id); + } + return (await this.get(soClient, id)) as AgentPolicy; } @@ -383,6 +387,32 @@ class AgentPolicyService { }; } + public async createFleetPolicyChangeAction( + soClient: SavedObjectsClientContract, + agentPolicyId: string + ) { + const policy = await agentPolicyService.getFullAgentPolicy(soClient, agentPolicyId); + if (!policy || !policy.revision) { + return; + } + const packages = policy.inputs.reduce((acc, input) => { + const packageName = input.meta?.package?.name; + if (packageName && acc.indexOf(packageName) < 0) { + acc.push(packageName); + } + return acc; + }, []); + + await createAgentPolicyAction(soClient, { + type: 'CONFIG_CHANGE', + data: { config: policy } as any, + ack_data: { packages }, + created_at: new Date().toISOString(), + policy_id: policy.id, + policy_revision: policy.revision, + }); + } + public async getFullAgentPolicy( soClient: SavedObjectsClientContract, id: string, diff --git a/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts b/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts index 3c743dd957f62..ff20e25e5bf0d 100644 --- a/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts +++ b/x-pack/plugins/ingest_manager/server/services/agent_policy_update.ts @@ -8,6 +8,7 @@ import { SavedObjectsClientContract } from 'src/core/server'; import { generateEnrollmentAPIKey, deleteEnrollmentApiKeyForAgentPolicyId } from './api_keys'; import { unenrollForAgentPolicyId } from './agents'; import { outputService } from './output'; +import { agentPolicyService } from './agent_policy'; export async function agentPolicyUpdateEventHandler( soClient: SavedObjectsClientContract, @@ -15,8 +16,9 @@ export async function agentPolicyUpdateEventHandler( agentPolicyId: string ) { const adminUser = await outputService.getAdminUser(soClient); - // If no admin user fleet is not enabled just skip this hook - if (!adminUser) { + const outputId = await outputService.getDefaultOutputId(soClient); + // If no admin user and no default output fleet is not enabled just skip this hook + if (!adminUser || !outputId) { return; } @@ -24,6 +26,11 @@ export async function agentPolicyUpdateEventHandler( await generateEnrollmentAPIKey(soClient, { agentPolicyId, }); + await agentPolicyService.createFleetPolicyChangeAction(soClient, agentPolicyId); + } + + if (action === 'updated') { + await agentPolicyService.createFleetPolicyChangeAction(soClient, agentPolicyId); } if (action === 'deleted') { diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts index 80fdc305d0ba7..866aa587b8a56 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.test.ts @@ -6,45 +6,19 @@ import Boom from 'boom'; import { SavedObjectsBulkResponse } from 'kibana/server'; import { savedObjectsClientMock } from 'src/core/server/mocks'; -import { encryptedSavedObjectsMock } from '../../../../../plugins/encrypted_saved_objects/server/mocks'; import { Agent, - AgentAction, AgentActionSOAttributes, + BaseAgentActionSOAttributes, AgentEvent, } from '../../../common/types/models'; import { AGENT_TYPE_PERMANENT, AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../../common/constants'; import { acknowledgeAgentActions } from './acks'; -import { appContextService } from '../app_context'; -import { IngestManagerAppContext } from '../../plugin'; describe('test agent acks services', () => { it('should succeed on valid and matched actions', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); - const mockStartEncryptedSOPlugin = encryptedSavedObjectsMock.createStart(); - appContextService.start(({ - encryptedSavedObjectsStart: mockStartEncryptedSOPlugin, - } as unknown) as IngestManagerAppContext); - - const [ - { value: mockStartEncryptedSOClient }, - ] = mockStartEncryptedSOPlugin.getClient.mock.results; - - mockStartEncryptedSOClient.getDecryptedAsInternalUser.mockReturnValue( - Promise.resolve({ - id: 'action1', - references: [], - type: AGENT_ACTION_SAVED_OBJECT_TYPE, - attributes: { - type: 'CONFIG_CHANGE', - agent_id: 'id', - sent_at: '2020-03-14T19:45:02.620Z', - timestamp: '2019-01-04T14:32:03.36764-05:00', - created_at: '2020-03-14T19:45:02.620Z', - }, - }) - ); mockSavedObjectsClient.bulkGet.mockReturnValue( Promise.resolve({ @@ -65,7 +39,7 @@ describe('test agent acks services', () => { } as SavedObjectsBulkResponse) ); - const agentActions = await acknowledgeAgentActions( + await acknowledgeAgentActions( mockSavedObjectsClient, ({ id: 'id', @@ -81,125 +55,32 @@ describe('test agent acks services', () => { } as AgentEvent, ] ); - expect(agentActions).toEqual([ - ({ - type: 'CONFIG_CHANGE', - id: 'action1', - agent_id: 'id', - sent_at: '2020-03-14T19:45:02.620Z', - timestamp: '2019-01-04T14:32:03.36764-05:00', - created_at: '2020-03-14T19:45:02.620Z', - } as unknown) as AgentAction, - ]); }); it('should update config field on the agent if a policy change is acknowledged', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); - const mockStartEncryptedSOPlugin = encryptedSavedObjectsMock.createStart(); - appContextService.start(({ - encryptedSavedObjectsStart: mockStartEncryptedSOPlugin, - } as unknown) as IngestManagerAppContext); - const [ - { value: mockStartEncryptedSOClient }, - ] = mockStartEncryptedSOPlugin.getClient.mock.results; - - mockStartEncryptedSOClient.getDecryptedAsInternalUser.mockReturnValue( - Promise.resolve({ - id: 'action1', - references: [], - type: AGENT_ACTION_SAVED_OBJECT_TYPE, - attributes: { - type: 'CONFIG_CHANGE', - agent_id: 'id', - sent_at: '2020-03-14T19:45:02.620Z', - timestamp: '2019-01-04T14:32:03.36764-05:00', - created_at: '2020-03-14T19:45:02.620Z', - data: JSON.stringify({ - config: { - id: 'policy1', - revision: 4, - settings: { - monitoring: { - enabled: true, - use_output: 'default', - logs: true, - metrics: true, - }, - }, - outputs: { - default: { - type: 'elasticsearch', - hosts: ['http://localhost:9200'], - }, - }, - inputs: [ - { - id: 'f2293360-b57c-11ea-8bd3-7bd51e425399', - name: 'system-1', - type: 'logs', - use_output: 'default', - meta: { - package: { - name: 'system', - version: '0.3.0', - }, - }, - dataset: { - namespace: 'default', - }, - streams: [ - { - id: 'logs-system.syslog', - dataset: { - name: 'system.syslog', - }, - paths: ['/var/log/messages*', '/var/log/syslog*'], - exclude_files: ['.gz$'], - multiline: { - pattern: '^\\s', - match: 'after', - }, - processors: [ - { - add_locale: null, - }, - { - add_fields: { - target: '', - fields: { - 'ecs.version': '1.5.0', - }, - }, - }, - ], - }, - ], - }, - ], - }, - }), - }, - }) - ); + const actionAttributes = { + type: 'CONFIG_CHANGE', + policy_id: 'policy1', + policy_revision: 4, + sent_at: '2020-03-14T19:45:02.620Z', + timestamp: '2019-01-04T14:32:03.36764-05:00', + created_at: '2020-03-14T19:45:02.620Z', + ack_data: JSON.stringify({ packages: ['system'] }), + }; mockSavedObjectsClient.bulkGet.mockReturnValue( Promise.resolve({ saved_objects: [ { - id: 'action1', + id: 'action2', references: [], type: AGENT_ACTION_SAVED_OBJECT_TYPE, - attributes: { - type: 'CONFIG_CHANGE', - agent_id: 'id', - sent_at: '2020-03-14T19:45:02.620Z', - timestamp: '2019-01-04T14:32:03.36764-05:00', - created_at: '2020-03-14T19:45:02.620Z', - }, + attributes: actionAttributes, }, ], - } as SavedObjectsBulkResponse) + } as SavedObjectsBulkResponse) ); await acknowledgeAgentActions( @@ -214,13 +95,13 @@ describe('test agent acks services', () => { type: 'ACTION_RESULT', subtype: 'CONFIG', timestamp: '2019-01-04T14:32:03.36764-05:00', - action_id: 'action1', + action_id: 'action2', agent_id: 'id', } as AgentEvent, ] ); expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(2); + expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(1); expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0][0]).toMatchInlineSnapshot(` Object { "attributes": Object { @@ -237,111 +118,25 @@ describe('test agent acks services', () => { it('should not update config field on the agent if a policy change for an old revision is acknowledged', async () => { const mockSavedObjectsClient = savedObjectsClientMock.create(); - const mockStartEncryptedSOPlugin = encryptedSavedObjectsMock.createStart(); - appContextService.start(({ - encryptedSavedObjectsStart: mockStartEncryptedSOPlugin, - } as unknown) as IngestManagerAppContext); - - const [ - { value: mockStartEncryptedSOClient }, - ] = mockStartEncryptedSOPlugin.getClient.mock.results; - - mockStartEncryptedSOClient.getDecryptedAsInternalUser.mockReturnValue( - Promise.resolve({ - id: 'action1', - references: [], - type: AGENT_ACTION_SAVED_OBJECT_TYPE, - attributes: { - type: 'CONFIG_CHANGE', - agent_id: 'id', - sent_at: '2020-03-14T19:45:02.620Z', - timestamp: '2019-01-04T14:32:03.36764-05:00', - created_at: '2020-03-14T19:45:02.620Z', - data: JSON.stringify({ - config: { - id: 'policy1', - revision: 4, - settings: { - monitoring: { - enabled: true, - use_output: 'default', - logs: true, - metrics: true, - }, - }, - outputs: { - default: { - type: 'elasticsearch', - hosts: ['http://localhost:9200'], - }, - }, - inputs: [ - { - id: 'f2293360-b57c-11ea-8bd3-7bd51e425399', - name: 'system-1', - type: 'logs', - use_output: 'default', - meta: { - package: { - name: 'system', - version: '0.3.0', - }, - }, - dataset: { - namespace: 'default', - }, - streams: [ - { - id: 'logs-system.syslog', - dataset: { - name: 'system.syslog', - }, - paths: ['/var/log/messages*', '/var/log/syslog*'], - exclude_files: ['.gz$'], - multiline: { - pattern: '^\\s', - match: 'after', - }, - processors: [ - { - add_locale: null, - }, - { - add_fields: { - target: '', - fields: { - 'ecs.version': '1.5.0', - }, - }, - }, - ], - }, - ], - }, - ], - }, - }), - }, - }) - ); mockSavedObjectsClient.bulkGet.mockReturnValue( Promise.resolve({ saved_objects: [ { - id: 'action1', + id: 'action3', references: [], type: AGENT_ACTION_SAVED_OBJECT_TYPE, attributes: { type: 'CONFIG_CHANGE', - agent_id: 'id', sent_at: '2020-03-14T19:45:02.620Z', timestamp: '2019-01-04T14:32:03.36764-05:00', created_at: '2020-03-14T19:45:02.620Z', + policy_id: 'policy1', + policy_revision: 99, }, }, ], - } as SavedObjectsBulkResponse) + } as SavedObjectsBulkResponse) ); await acknowledgeAgentActions( @@ -357,13 +152,13 @@ describe('test agent acks services', () => { type: 'ACTION_RESULT', subtype: 'CONFIG', timestamp: '2019-01-04T14:32:03.36764-05:00', - action_id: 'action1', + action_id: 'action3', agent_id: 'id', } as AgentEvent, ] ); expect(mockSavedObjectsClient.bulkUpdate).toBeCalled(); - expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(1); + expect(mockSavedObjectsClient.bulkUpdate.mock.calls[0][0]).toHaveLength(0); }); it('should fail for actions that cannot be found on agent actions list', async () => { @@ -372,7 +167,7 @@ describe('test agent acks services', () => { Promise.resolve({ saved_objects: [ { - id: 'action1', + id: 'action4', error: { message: 'Not found', statusCode: 404, @@ -394,7 +189,7 @@ describe('test agent acks services', () => { type: 'ACTION_RESULT', subtype: 'CONFIG', timestamp: '2019-01-04T14:32:03.36764-05:00', - action_id: 'action2', + action_id: 'action4', agent_id: 'id', } as unknown) as AgentEvent, ] @@ -412,7 +207,7 @@ describe('test agent acks services', () => { Promise.resolve({ saved_objects: [ { - id: 'action1', + id: 'action5', references: [], type: AGENT_ACTION_SAVED_OBJECT_TYPE, attributes: { @@ -439,7 +234,7 @@ describe('test agent acks services', () => { type: 'ACTION', subtype: 'FAILED', timestamp: '2019-01-04T14:32:03.36764-05:00', - action_id: 'action1', + action_id: 'action5', agent_id: 'id', } as unknown) as AgentEvent, ] diff --git a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts index 87572ce405ee7..d29dfcec7ef30 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/acks.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/acks.ts @@ -11,14 +11,15 @@ import { SavedObjectsClientContract, } from 'src/core/server'; import Boom from 'boom'; +import LRU from 'lru-cache'; import { Agent, AgentAction, + AgentPolicyAction, AgentEvent, AgentEventSOAttributes, AgentSOAttributes, AgentActionSOAttributes, - FullAgentPolicy, } from '../../types'; import { AGENT_EVENT_SAVED_OBJECT_TYPE, @@ -30,11 +31,20 @@ import { forceUnenrollAgent } from './unenroll'; const ALLOWED_ACKNOWLEDGEMENT_TYPE: string[] = ['ACTION_RESULT']; +const actionCache = new LRU({ + max: 20, + maxAge: 10 * 60 * 1000, // 10 minutes +}); + export async function acknowledgeAgentActions( soClient: SavedObjectsClientContract, agent: Agent, agentEvents: AgentEvent[] ): Promise { + if (agentEvents.length === 0) { + return []; + } + for (const agentEvent of agentEvents) { if (!isAllowedType(agentEvent.type)) { throw Boom.badRequest(`${agentEvent.type} not allowed for acknowledgment only ACTION_RESULT`); @@ -45,9 +55,9 @@ export async function acknowledgeAgentActions( .map((event) => event.action_id) .filter((actionId) => actionId !== undefined) as string[]; - let actions; + let actions: AgentAction[]; try { - actions = await getAgentActionByIds(soClient, actionIds); + actions = await fetchActionsUsingCache(soClient, actionIds); } catch (error) { if (Boom.isBoom(error) && error.output.statusCode === 404) { throw Boom.badRequest(`One or more actions cannot be found`); @@ -55,65 +65,91 @@ export async function acknowledgeAgentActions( throw error; } + const agentActionsIds: string[] = []; for (const action of actions) { - if (action.agent_id !== agent.id) { + if (action.agent_id) { + agentActionsIds.push(action.id); + } + if (action.agent_id && action.agent_id !== agent.id) { throw Boom.badRequest(`${action.id} not found`); } } - if (actions.length === 0) { - return []; - } - const isAgentUnenrolled = actions.some((action) => action.type === 'UNENROLL'); if (isAgentUnenrolled) { await forceUnenrollAgent(soClient, agent.id); } - const agentPolicy = getLatestAgentPolicyIfUpdated(agent, actions); + const configChangeAction = getLatestConfigChangePolicyActionIfUpdated(agent, actions); await soClient.bulkUpdate([ - ...(agentPolicy ? [buildUpdateAgentPolicy(agent.id, agentPolicy)] : []), - ...buildUpdateAgentActionSentAt(actionIds), + ...(configChangeAction + ? [ + { + type: AGENT_SAVED_OBJECT_TYPE, + id: agent.id, + attributes: { + policy_revision: configChangeAction.policy_revision, + packages: configChangeAction?.ack_data?.packages, + }, + }, + ] + : []), + ...buildUpdateAgentActionSentAt(agentActionsIds), ]); return actions; } -function getLatestAgentPolicyIfUpdated(agent: Agent, actions: AgentAction[]) { - return actions.reduce((acc, action) => { - if (action.type !== 'CONFIG_CHANGE') { - return acc; - } - const data = action.data || {}; +async function fetchActionsUsingCache( + soClient: SavedObjectsClientContract, + actionIds: string[] +): Promise { + const missingActionIds: string[] = []; + const actions = actionIds + .map((actionId) => { + const action = actionCache.get(actionId); + if (!action) { + missingActionIds.push(actionId); + } + return action; + }) + .filter((action): action is AgentAction => action !== undefined); + + if (missingActionIds.length === 0) { + return actions; + } - if (data?.config?.id !== agent.policy_id) { - return acc; - } + const freshActions = await getAgentActionByIds(soClient, actionIds, false); + freshActions.forEach((action) => actionCache.set(action.id, action)); - const currentRevision = (acc && acc.revision) || agent.policy_revision || 0; + return [...freshActions, ...actions]; +} - return data?.config?.revision > currentRevision ? data?.config : acc; - }, null); +function isAgentPolicyAction(action: AgentAction | AgentPolicyAction): action is AgentPolicyAction { + return (action as AgentPolicyAction).policy_id !== undefined; } -function buildUpdateAgentPolicy(agentId: string, agentPolicy: FullAgentPolicy) { - const packages = agentPolicy.inputs.reduce((acc, input) => { - const packageName = input.meta?.package?.name; - if (packageName && acc.indexOf(packageName) < 0) { - return [packageName, ...acc]; +function getLatestConfigChangePolicyActionIfUpdated( + agent: Agent, + actions: Array +): AgentPolicyAction | null { + return actions.reduce((acc, action) => { + if ( + !isAgentPolicyAction(action) || + action.type !== 'CONFIG_CHANGE' || + action.policy_id !== agent.policy_id || + (acc?.policy_revision ?? 0) < (agent.policy_revision || 0) + ) { + return acc; } - return acc; - }, []); - return { - type: AGENT_SAVED_OBJECT_TYPE, - id: agentId, - attributes: { - policy_revision: agentPolicy.revision, - packages, - }, - }; + if (action.policy_revision > (acc?.policy_revision ?? 0)) { + return action; + } + + return acc; + }, null); } function buildUpdateAgentActionSentAt( diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts index c739007952389..bcb3fc7fdc7bd 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.test.ts @@ -22,7 +22,13 @@ describe('test agent actions services', () => { }; mockSavedObjectsClient.create.mockReturnValue( Promise.resolve({ - attributes: {}, + attributes: { + agent_id: 'agentid', + type: 'CONFIG_CHANGE', + data: JSON.stringify({ content: 'data' }), + sent_at: '2020-03-14T19:45:02.620Z', + created_at: '2020-03-14T19:45:02.620Z', + }, } as SavedObject) ); await createAgentAction(mockSavedObjectsClient, newAgentAction); diff --git a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts index cd0dd92131230..8519714334986 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/actions.ts @@ -5,9 +5,20 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { Agent, AgentAction, AgentActionSOAttributes } from '../../../common/types/models'; +import { + Agent, + AgentAction, + AgentPolicyAction, + BaseAgentActionSOAttributes, + AgentActionSOAttributes, + AgentPolicyActionSOAttributes, +} from '../../../common/types/models'; import { AGENT_ACTION_SAVED_OBJECT_TYPE } from '../../../common/constants'; -import { savedObjectToAgentAction } from './saved_objects'; +import { + isAgentActionSavedObject, + isPolicyActionSavedObject, + savedObjectToAgentAction, +} from './saved_objects'; import { appContextService } from '../app_context'; import { nodeTypes } from '../../../../../../src/plugins/data/common'; @@ -15,15 +26,45 @@ export async function createAgentAction( soClient: SavedObjectsClientContract, newAgentAction: Omit ): Promise { - const so = await soClient.create(AGENT_ACTION_SAVED_OBJECT_TYPE, { + return createAction(soClient, newAgentAction); +} + +export function createAgentPolicyAction( + soClient: SavedObjectsClientContract, + newAgentAction: Omit +): Promise { + return createAction(soClient, newAgentAction); +} +async function createAction( + soClient: SavedObjectsClientContract, + newAgentAction: Omit +): Promise; +async function createAction( + soClient: SavedObjectsClientContract, + newAgentAction: Omit +): Promise; +async function createAction( + soClient: SavedObjectsClientContract, + newAgentAction: Omit | Omit +): Promise { + const so = await soClient.create(AGENT_ACTION_SAVED_OBJECT_TYPE, { ...newAgentAction, data: newAgentAction.data ? JSON.stringify(newAgentAction.data) : undefined, + ack_data: newAgentAction.ack_data ? JSON.stringify(newAgentAction.ack_data) : undefined, }); - const agentAction = savedObjectToAgentAction(so); - agentAction.data = newAgentAction.data; + if (isAgentActionSavedObject(so)) { + const agentAction = savedObjectToAgentAction(so); + agentAction.data = newAgentAction.data; + + return agentAction; + } else if (isPolicyActionSavedObject(so)) { + const agentAction = savedObjectToAgentAction(so); + agentAction.data = newAgentAction.data; - return agentAction; + return agentAction; + } + throw new Error('Invalid action'); } export async function getAgentActionsForCheckin( @@ -67,7 +108,8 @@ export async function getAgentActionsForCheckin( export async function getAgentActionByIds( soClient: SavedObjectsClientContract, - actionIds: string[] + actionIds: string[], + decryptData: boolean = true ) { const actions = ( await soClient.bulkGet( @@ -76,7 +118,11 @@ export async function getAgentActionByIds( type: AGENT_ACTION_SAVED_OBJECT_TYPE, })) ) - ).saved_objects.map(savedObjectToAgentAction); + ).saved_objects.map((action) => savedObjectToAgentAction(action)); + + if (!decryptData) { + return actions; + } return Promise.all( actions.map(async (action) => { @@ -93,6 +139,39 @@ export async function getAgentActionByIds( ); } +export async function getAgentPolicyActionByIds( + soClient: SavedObjectsClientContract, + actionIds: string[], + decryptData: boolean = true +) { + const actions = ( + await soClient.bulkGet( + actionIds.map((actionId) => ({ + id: actionId, + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + })) + ) + ).saved_objects.map((action) => savedObjectToAgentAction(action)); + + if (!decryptData) { + return actions; + } + + return Promise.all( + actions.map(async (action) => { + // Get decrypted actions + return savedObjectToAgentAction( + await appContextService + .getEncryptedSavedObjects() + .getDecryptedAsInternalUser( + AGENT_ACTION_SAVED_OBJECT_TYPE, + action.id + ) + ); + }) + ); +} + export async function getNewActionsSince(soClient: SavedObjectsClientContract, timestamp: string) { const filter = nodeTypes.function.buildNode('and', [ nodeTypes.function.buildNode( @@ -116,7 +195,26 @@ export async function getNewActionsSince(soClient: SavedObjectsClientContract, t filter, }); - return res.saved_objects.map(savedObjectToAgentAction); + return res.saved_objects + .filter(isAgentActionSavedObject) + .map((so) => savedObjectToAgentAction(so)); +} + +export async function getLatestConfigChangeAction( + soClient: SavedObjectsClientContract, + policyId: string +) { + const res = await soClient.find({ + type: AGENT_ACTION_SAVED_OBJECT_TYPE, + search: policyId, + searchFields: ['policy_id'], + sortField: 'created_at', + sortOrder: 'DESC', + }); + + if (res.saved_objects[0]) { + return savedObjectToAgentAction(res.saved_objects[0]); + } } export interface ActionsService { @@ -124,6 +222,6 @@ export interface ActionsService { createAgentAction: ( soClient: SavedObjectsClientContract, - newAgentAction: AgentActionSOAttributes + newAgentAction: Omit ) => Promise; } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts index eddfb0e64b84b..8f586420c3ecb 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/checkin/state_new_actions.ts @@ -5,6 +5,7 @@ */ import { timer, from, Observable, TimeoutError } from 'rxjs'; +import { omit } from 'lodash'; import { shareReplay, distinctUntilKeyChanged, @@ -16,14 +17,7 @@ import { take, } from 'rxjs/operators'; import { SavedObjectsClientContract, KibanaRequest } from 'src/core/server'; -import { - Agent, - AgentAction, - AgentSOAttributes, - AgentPolicy, - FullAgentPolicy, -} from '../../../types'; -import { agentPolicyService } from '../../agent_policy'; +import { Agent, AgentAction, AgentPolicyAction, AgentSOAttributes } from '../../../types'; import * as APIKeysService from '../../api_keys'; import { AGENT_SAVED_OBJECT_TYPE, @@ -31,7 +25,11 @@ import { AGENT_POLICY_ROLLOUT_RATE_LIMIT_INTERVAL_MS, AGENT_POLICY_ROLLOUT_RATE_LIMIT_REQUEST_PER_INTERVAL, } from '../../../constants'; -import { createAgentAction, getNewActionsSince } from '../actions'; +import { + getNewActionsSince, + getLatestConfigChangeAction, + getAgentPolicyActionByIds, +} from '../actions'; import { appContextService } from '../../app_context'; import { toPromiseAbortable, AbortError, createRateLimiter } from './rxjs_utils'; @@ -54,27 +52,27 @@ function getInternalUserSOClient() { return appContextService.getInternalUserSOClient(fakeRequest); } -function createAgentPolicySharedObservable(agentPolicyId: string) { +function createNewActionsSharedObservable(): Observable { const internalSOClient = getInternalUserSOClient(); + return timer(0, AGENT_UPDATE_ACTIONS_INTERVAL_MS).pipe( - switchMap(() => - from(agentPolicyService.get(internalSOClient, agentPolicyId) as Promise) - ), - distinctUntilKeyChanged('revision'), - switchMap((data) => - from(agentPolicyService.getFullAgentPolicy(internalSOClient, agentPolicyId)) - ), + switchMap(() => { + return from(getNewActionsSince(internalSOClient, new Date().toISOString())); + }), shareReplay({ refCount: true, bufferSize: 1 }) ); } -function createNewActionsSharedObservable(): Observable { - return timer(0, AGENT_UPDATE_ACTIONS_INTERVAL_MS).pipe( - switchMap(() => { - const internalSOClient = getInternalUserSOClient(); +function createAgentPolicyActionSharedObservable(agentPolicyId: string) { + const internalSOClient = getInternalUserSOClient(); - return from(getNewActionsSince(internalSOClient, new Date().toISOString())); - }), + return timer(0, AGENT_UPDATE_ACTIONS_INTERVAL_MS).pipe( + switchMap(() => from(getLatestConfigChangeAction(internalSOClient, agentPolicyId))), + filter((data): data is AgentPolicyAction => data !== undefined), + distinctUntilKeyChanged('id'), + switchMap((data) => + from(getAgentPolicyActionByIds(internalSOClient, [data.id]).then((r) => r[0])) + ), shareReplay({ refCount: true, bufferSize: 1 }) ); } @@ -102,47 +100,35 @@ async function getOrCreateAgentDefaultOutputAPIKey( return outputAPIKey.key; } -function shouldCreateAgentPolicyAction(agent: Agent, agentPolicy: FullAgentPolicy | null): boolean { - if (!agentPolicy || !agentPolicy.revision) { - return false; - } - const isAgentPolicyOutdated = - !agent.policy_revision || agent.policy_revision < agentPolicy.revision; - if (!isAgentPolicyOutdated) { - return false; - } - - return true; -} - -async function createAgentActionFromAgentPolicy( +async function createAgentActionFromPolicyAction( soClient: SavedObjectsClientContract, agent: Agent, - policy: FullAgentPolicy | null + policyAction: AgentPolicyAction ) { - // Deep clone !not supporting Date, and undefined value. - const newAgentPolicy = JSON.parse(JSON.stringify(policy)); + const newAgentAction: AgentAction = Object.assign( + omit( + // Faster than clone + JSON.parse(JSON.stringify(policyAction)) as AgentPolicyAction, + 'policy_id', + 'policy_revision' + ), + { + agent_id: agent.id, + } + ); // Mutate the policy to set the api token for this agent - newAgentPolicy.outputs.default.api_key = await getOrCreateAgentDefaultOutputAPIKey( + newAgentAction.data.config.outputs.default.api_key = await getOrCreateAgentDefaultOutputAPIKey( soClient, agent ); - const policyChangeAction = await createAgentAction(soClient, { - agent_id: agent.id, - type: 'CONFIG_CHANGE', - data: { config: newAgentPolicy } as any, - created_at: new Date().toISOString(), - sent_at: undefined, - }); - - return [policyChangeAction]; + return [newAgentAction]; } export function agentCheckinStateNewActionsFactory() { // Shared Observables - const agentPolicies$ = new Map>(); + const agentPolicies$ = new Map>(); const newActions$ = createNewActionsSharedObservable(); // Rx operators const rateLimiter = createRateLimiter( @@ -162,7 +148,7 @@ export function agentCheckinStateNewActionsFactory() { } const agentPolicyId = agent.policy_id; if (!agentPolicies$.has(agentPolicyId)) { - agentPolicies$.set(agentPolicyId, createAgentPolicySharedObservable(agentPolicyId)); + agentPolicies$.set(agentPolicyId, createAgentPolicyActionSharedObservable(agentPolicyId)); } const agentPolicy$ = agentPolicies$.get(agentPolicyId); if (!agentPolicy$) { @@ -174,15 +160,22 @@ export function agentCheckinStateNewActionsFactory() { // Set a timeout 3s before the real timeout to have a chance to respond an empty response before socket timeout Math.max((appContextService.getConfig()?.fleet.pollingRequestTimeout ?? 0) - 3000, 3000) ), - filter((agentPolicy) => shouldCreateAgentPolicyAction(agent, agentPolicy)), + filter( + (action) => + agent.policy_id !== undefined && + action.policy_revision !== undefined && + action.policy_id !== undefined && + action.policy_id === agent.policy_id && + (!agent.policy_revision || action.policy_revision > agent.policy_revision) + ), rateLimiter(), - mergeMap((agentPolicy) => createAgentActionFromAgentPolicy(soClient, agent, agentPolicy)), + mergeMap((policyAction) => createAgentActionFromPolicyAction(soClient, agent, policyAction)), merge(newActions$), mergeMap(async (data) => { if (!data) { return; } - const newActions = data.filter((action) => action.agent_id); + const newActions = data.filter((action) => action.agent_id === agent.id); if (newActions.length === 0) { return; } diff --git a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts index 2ab5cc8139f69..3ae664c086da9 100644 --- a/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts +++ b/x-pack/plugins/ingest_manager/server/services/agents/saved_objects.ts @@ -6,7 +6,15 @@ import Boom from 'boom'; import { SavedObject } from 'src/core/server'; -import { Agent, AgentSOAttributes, AgentAction, AgentActionSOAttributes } from '../../types'; +import { + Agent, + AgentSOAttributes, + AgentAction, + AgentPolicyAction, + AgentActionSOAttributes, + AgentPolicyActionSOAttributes, + BaseAgentActionSOAttributes, +} from '../../types'; export function savedObjectToAgent(so: SavedObject): Agent { if (so.error) { @@ -27,7 +35,13 @@ export function savedObjectToAgent(so: SavedObject): Agent { }; } -export function savedObjectToAgentAction(so: SavedObject): AgentAction { +export function savedObjectToAgentAction(so: SavedObject): AgentAction; +export function savedObjectToAgentAction( + so: SavedObject +): AgentPolicyAction; +export function savedObjectToAgentAction( + so: SavedObject +): AgentAction | AgentPolicyAction { if (so.error) { if (so.error.statusCode === 404) { throw Boom.notFound(so.error.message); @@ -36,9 +50,42 @@ export function savedObjectToAgentAction(so: SavedObject +): so is SavedObject { + return (so.attributes as AgentActionSOAttributes).agent_id !== undefined; +} + +export function isPolicyActionSavedObject( + so: SavedObject +): so is SavedObject { + return (so.attributes as AgentPolicyActionSOAttributes).policy_id !== undefined; +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts index 44e4eddfbbe6a..878c6ea8f2804 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/ingest_pipeline/install.ts @@ -156,7 +156,12 @@ async function installPipeline({ body: pipeline.contentForInstallation, }; if (pipeline.extension === 'yml') { - callClusterParams.headers = { ['Content-Type']: 'application/yaml' }; + callClusterParams.headers = { + // pipeline is YAML + 'Content-Type': 'application/yaml', + // but we want JSON responses (to extract error messages, status code, or other metadata) + Accept: 'application/json', + }; } // This uses the catch-all endpoint 'transport.request' because we have to explicitly diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/common.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/common.ts new file mode 100644 index 0000000000000..46f36dba96747 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/common.ts @@ -0,0 +1,11 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as Registry from '../../registry'; + +export const getAsset = (path: string): Buffer => { + return Registry.getAsset(path); +}; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts new file mode 100644 index 0000000000000..1e58319183c7d --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/install.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; + +import { saveInstalledEsRefs } from '../../packages/install'; +import * as Registry from '../../registry'; +import { + Dataset, + ElasticsearchAssetType, + EsAssetReference, + RegistryPackage, +} from '../../../../../common/types/models'; +import { CallESAsCurrentUser } from '../../../../types'; +import { getInstallation } from '../../packages'; +import { deleteTransforms, deleteTransformRefs } from './remove'; +import { getAsset } from './common'; + +interface TransformInstallation { + installationName: string; + content: string; +} + +interface TransformPathDataset { + path: string; + dataset: Dataset; +} + +export const installTransformForDataset = async ( + registryPackage: RegistryPackage, + paths: string[], + callCluster: CallESAsCurrentUser, + savedObjectsClient: SavedObjectsClientContract +) => { + const installation = await getInstallation({ savedObjectsClient, pkgName: registryPackage.name }); + let previousInstalledTransformEsAssets: EsAssetReference[] = []; + if (installation) { + previousInstalledTransformEsAssets = installation.installed_es.filter( + ({ type, id }) => type === ElasticsearchAssetType.transform + ); + } + + // delete all previous transform + await deleteTransforms( + callCluster, + previousInstalledTransformEsAssets.map((asset) => asset.id) + ); + // install the latest dataset + const datasets = registryPackage.datasets; + if (!datasets?.length) return []; + const installNameSuffix = `${registryPackage.version}`; + + const transformPaths = paths.filter((path) => isTransform(path)); + let installedTransforms: EsAssetReference[] = []; + if (transformPaths.length > 0) { + const transformPathDatasets = datasets.reduce((acc, dataset) => { + transformPaths.forEach((path) => { + if (isDatasetTransform(path, dataset.path)) { + acc.push({ path, dataset }); + } + }); + return acc; + }, []); + + const transformRefs = transformPathDatasets.reduce( + (acc, transformPathDataset) => { + if (transformPathDataset) { + acc.push({ + id: getTransformNameForInstallation(transformPathDataset, installNameSuffix), + type: ElasticsearchAssetType.transform, + }); + } + return acc; + }, + [] + ); + + // get and save transform refs before installing transforms + await saveInstalledEsRefs(savedObjectsClient, registryPackage.name, transformRefs); + + const transforms: TransformInstallation[] = transformPathDatasets.map( + (transformPathDataset: TransformPathDataset) => { + return { + installationName: getTransformNameForInstallation( + transformPathDataset, + installNameSuffix + ), + content: getAsset(transformPathDataset.path).toString('utf-8'), + }; + } + ); + + const installationPromises = transforms.map(async (transform) => { + return installTransform({ callCluster, transform }); + }); + + installedTransforms = await Promise.all(installationPromises).then((results) => results.flat()); + } + + if (previousInstalledTransformEsAssets.length > 0) { + const currentInstallation = await getInstallation({ + savedObjectsClient, + pkgName: registryPackage.name, + }); + + // remove the saved object reference + await deleteTransformRefs( + savedObjectsClient, + currentInstallation?.installed_es || [], + registryPackage.name, + previousInstalledTransformEsAssets.map((asset) => asset.id), + installedTransforms.map((installed) => installed.id) + ); + } + return installedTransforms; +}; + +const isTransform = (path: string) => { + const pathParts = Registry.pathParts(path); + return pathParts.type === ElasticsearchAssetType.transform; +}; + +const isDatasetTransform = (path: string, datasetName: string) => { + const pathParts = Registry.pathParts(path); + return ( + !path.endsWith('/') && + pathParts.type === ElasticsearchAssetType.transform && + pathParts.dataset !== undefined && + datasetName === pathParts.dataset + ); +}; + +async function installTransform({ + callCluster, + transform, +}: { + callCluster: CallESAsCurrentUser; + transform: TransformInstallation; +}): Promise { + // defer validation on put if the source index is not available + await callCluster('transport.request', { + method: 'PUT', + path: `_transform/${transform.installationName}`, + query: 'defer_validation=true', + body: transform.content, + }); + + await callCluster('transport.request', { + method: 'POST', + path: `_transform/${transform.installationName}/_start`, + }); + + return { id: transform.installationName, type: ElasticsearchAssetType.transform }; +} + +const getTransformNameForInstallation = ( + transformDataset: TransformPathDataset, + suffix: string +) => { + const filename = transformDataset?.path.split('/')?.pop()?.split('.')[0]; + return `${transformDataset.dataset.type}-${transformDataset.dataset.name}-${filename}-${suffix}`; +}; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.test.ts new file mode 100644 index 0000000000000..3f85ee9b550b2 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { savedObjectsClientMock } from '../../../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; +import { deleteTransformRefs } from './remove'; +import { EsAssetReference } from '../../../../../common/types/models'; + +describe('test transform install', () => { + let savedObjectsClient: jest.Mocked; + beforeEach(() => { + savedObjectsClient = savedObjectsClientMock.create(); + }); + + test('can delete transform ref and handle duplicate when previous version and current version are the same', async () => { + await deleteTransformRefs( + savedObjectsClient, + [ + { id: 'metrics-endpoint.policy-0.16.0-dev.0', type: 'ingest_pipeline' }, + { id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0', type: 'transform' }, + ] as EsAssetReference[], + 'endpoint', + ['metrics-endpoint.metadata-current-default-0.16.0-dev.0'], + ['metrics-endpoint.metadata-current-default-0.16.0-dev.0'] + ); + expect(savedObjectsClient.update.mock.calls).toEqual([ + [ + 'epm-packages', + 'endpoint', + { + installed_es: [ + { id: 'metrics-endpoint.policy-0.16.0-dev.0', type: 'ingest_pipeline' }, + { id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0', type: 'transform' }, + ], + }, + ], + ]); + }); + + test('can delete transform ref when previous version and current version are not the same', async () => { + await deleteTransformRefs( + savedObjectsClient, + [ + { id: 'metrics-endpoint.policy-0.16.0-dev.0', type: 'ingest_pipeline' }, + { id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0', type: 'transform' }, + ] as EsAssetReference[], + 'endpoint', + ['metrics-endpoint.metadata-current-default-0.15.0-dev.0'], + ['metrics-endpoint.metadata-current-default-0.16.0-dev.0'] + ); + + expect(savedObjectsClient.update.mock.calls).toEqual([ + [ + 'epm-packages', + 'endpoint', + { + installed_es: [ + { id: 'metrics-endpoint.policy-0.16.0-dev.0', type: 'ingest_pipeline' }, + { id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0', type: 'transform' }, + ], + }, + ], + ]); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts new file mode 100644 index 0000000000000..5c9d3e2846200 --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/remove.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SavedObjectsClientContract } from 'kibana/server'; +import { CallESAsCurrentUser, ElasticsearchAssetType, EsAssetReference } from '../../../../types'; +import { PACKAGES_SAVED_OBJECT_TYPE } from '../../../../../common/constants'; + +export const stopTransforms = async (transformIds: string[], callCluster: CallESAsCurrentUser) => { + for (const transformId of transformIds) { + await callCluster('transport.request', { + method: 'POST', + path: `_transform/${transformId}/_stop`, + query: 'force=true', + ignore: [404], + }); + } +}; + +export const deleteTransforms = async ( + callCluster: CallESAsCurrentUser, + transformIds: string[] +) => { + await Promise.all( + transformIds.map(async (transformId) => { + await stopTransforms([transformId], callCluster); + await callCluster('transport.request', { + method: 'DELETE', + query: 'force=true', + path: `_transform/${transformId}`, + ignore: [404], + }); + }) + ); +}; + +export const deleteTransformRefs = async ( + savedObjectsClient: SavedObjectsClientContract, + installedEsAssets: EsAssetReference[], + pkgName: string, + installedEsIdToRemove: string[], + currentInstalledEsTransformIds: string[] +) => { + const seen = new Set(); + const filteredAssets = installedEsAssets.filter(({ type, id }) => { + if (type !== ElasticsearchAssetType.transform) return true; + const add = + (currentInstalledEsTransformIds.includes(id) || !installedEsIdToRemove.includes(id)) && + !seen.has(id); + seen.add(id); + return add; + }); + return savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { + installed_es: filteredAssets, + }); +}; diff --git a/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts new file mode 100644 index 0000000000000..0b66077b8699a --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/elasticsearch/transform/transform.test.ts @@ -0,0 +1,420 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +jest.mock('../../packages/get', () => { + return { getInstallation: jest.fn(), getInstallationObject: jest.fn() }; +}); + +jest.mock('./common', () => { + return { + getAsset: jest.fn(), + }; +}); + +import { installTransformForDataset } from './install'; +import { ILegacyScopedClusterClient, SavedObject, SavedObjectsClientContract } from 'kibana/server'; +import { ElasticsearchAssetType, Installation, RegistryPackage } from '../../../../types'; +import { getInstallation, getInstallationObject } from '../../packages'; +import { getAsset } from './common'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { savedObjectsClientMock } from '../../../../../../../../src/core/server/saved_objects/service/saved_objects_client.mock'; + +describe('test transform install', () => { + let legacyScopedClusterClient: jest.Mocked; + let savedObjectsClient: jest.Mocked; + beforeEach(() => { + legacyScopedClusterClient = { + callAsInternalUser: jest.fn(), + callAsCurrentUser: jest.fn(), + }; + (getInstallation as jest.MockedFunction).mockReset(); + (getInstallationObject as jest.MockedFunction).mockReset(); + savedObjectsClient = savedObjectsClientMock.create(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + test('can install new versions and removes older version', async () => { + const previousInstallation: Installation = ({ + installed_es: [ + { + id: 'metrics-endpoint.policy-0.16.0-dev.0', + type: ElasticsearchAssetType.ingestPipeline, + }, + { + id: 'metrics-endpoint.metadata_current-default-0.15.0-dev.0', + type: ElasticsearchAssetType.transform, + }, + ], + } as unknown) as Installation; + + const currentInstallation: Installation = ({ + installed_es: [ + { + id: 'metrics-endpoint.policy-0.16.0-dev.0', + type: ElasticsearchAssetType.ingestPipeline, + }, + { + id: 'metrics-endpoint.metadata_current-default-0.15.0-dev.0', + type: ElasticsearchAssetType.transform, + }, + { + id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0', + type: ElasticsearchAssetType.transform, + }, + { + id: 'metrics-endpoint.metadata-default-0.16.0-dev.0', + type: ElasticsearchAssetType.transform, + }, + ], + } as unknown) as Installation; + (getAsset as jest.MockedFunction) + .mockReturnValueOnce(Buffer.from('{"content": "data"}', 'utf8')) + .mockReturnValueOnce(Buffer.from('{"content": "data"}', 'utf8')); + + (getInstallation as jest.MockedFunction) + .mockReturnValueOnce(Promise.resolve(previousInstallation)) + .mockReturnValueOnce(Promise.resolve(currentInstallation)); + + (getInstallationObject as jest.MockedFunction< + typeof getInstallationObject + >).mockReturnValueOnce( + Promise.resolve(({ + attributes: { + installed_es: previousInstallation.installed_es, + }, + } as unknown) as SavedObject) + ); + + await installTransformForDataset( + ({ + name: 'endpoint', + version: '0.16.0-dev.0', + datasets: [ + { + type: 'metrics', + name: 'endpoint.metadata', + title: 'Endpoint Metadata', + release: 'experimental', + package: 'endpoint', + ingest_pipeline: 'default', + elasticsearch: { + 'index_template.mappings': { + dynamic: false, + }, + }, + path: 'metadata', + }, + { + type: 'metrics', + name: 'endpoint.metadata_current', + title: 'Endpoint Metadata Current', + release: 'experimental', + package: 'endpoint', + ingest_pipeline: 'default', + elasticsearch: { + 'index_template.mappings': { + dynamic: false, + }, + }, + path: 'metadata_current', + }, + ], + } as unknown) as RegistryPackage, + [ + 'endpoint-0.16.0-dev.0/dataset/policy/elasticsearch/ingest_pipeline/default.json', + 'endpoint-0.16.0-dev.0/dataset/metadata/elasticsearch/transform/default.json', + 'endpoint-0.16.0-dev.0/dataset/metadata_current/elasticsearch/transform/default.json', + ], + legacyScopedClusterClient.callAsCurrentUser, + savedObjectsClient + ); + + expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([ + [ + 'transport.request', + { + method: 'POST', + path: '_transform/metrics-endpoint.metadata_current-default-0.15.0-dev.0/_stop', + query: 'force=true', + ignore: [404], + }, + ], + [ + 'transport.request', + { + method: 'DELETE', + query: 'force=true', + path: '_transform/metrics-endpoint.metadata_current-default-0.15.0-dev.0', + ignore: [404], + }, + ], + [ + 'transport.request', + { + method: 'PUT', + path: '_transform/metrics-endpoint.metadata-default-0.16.0-dev.0', + query: 'defer_validation=true', + body: '{"content": "data"}', + }, + ], + [ + 'transport.request', + { + method: 'PUT', + path: '_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0', + query: 'defer_validation=true', + body: '{"content": "data"}', + }, + ], + [ + 'transport.request', + { + method: 'POST', + path: '_transform/metrics-endpoint.metadata-default-0.16.0-dev.0/_start', + }, + ], + [ + 'transport.request', + { + method: 'POST', + path: '_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0/_start', + }, + ], + ]); + + expect(savedObjectsClient.update.mock.calls).toEqual([ + [ + 'epm-packages', + 'endpoint', + { + installed_es: [ + { + id: 'metrics-endpoint.policy-0.16.0-dev.0', + type: 'ingest_pipeline', + }, + { + id: 'metrics-endpoint.metadata_current-default-0.15.0-dev.0', + type: 'transform', + }, + { + id: 'metrics-endpoint.metadata-default-0.16.0-dev.0', + type: 'transform', + }, + { + id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0', + type: 'transform', + }, + ], + }, + ], + [ + 'epm-packages', + 'endpoint', + { + installed_es: [ + { + id: 'metrics-endpoint.policy-0.16.0-dev.0', + type: 'ingest_pipeline', + }, + { + id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0', + type: 'transform', + }, + { + id: 'metrics-endpoint.metadata-default-0.16.0-dev.0', + type: 'transform', + }, + ], + }, + ], + ]); + }); + + test('can install new version and when no older version', async () => { + const previousInstallation: Installation = ({ + installed_es: [], + } as unknown) as Installation; + + const currentInstallation: Installation = ({ + installed_es: [ + { + id: 'metrics-endpoint.metadata-current-default-0.16.0-dev.0', + type: ElasticsearchAssetType.transform, + }, + ], + } as unknown) as Installation; + (getAsset as jest.MockedFunction).mockReturnValueOnce( + Buffer.from('{"content": "data"}', 'utf8') + ); + (getInstallation as jest.MockedFunction) + .mockReturnValueOnce(Promise.resolve(previousInstallation)) + .mockReturnValueOnce(Promise.resolve(currentInstallation)); + + (getInstallationObject as jest.MockedFunction< + typeof getInstallationObject + >).mockReturnValueOnce( + Promise.resolve(({ attributes: { installed_es: [] } } as unknown) as SavedObject< + Installation + >) + ); + legacyScopedClusterClient.callAsCurrentUser = jest.fn(); + await installTransformForDataset( + ({ + name: 'endpoint', + version: '0.16.0-dev.0', + datasets: [ + { + type: 'metrics', + name: 'endpoint.metadata_current', + title: 'Endpoint Metadata', + release: 'experimental', + package: 'endpoint', + ingest_pipeline: 'default', + elasticsearch: { + 'index_template.mappings': { + dynamic: false, + }, + }, + path: 'metadata_current', + }, + ], + } as unknown) as RegistryPackage, + ['endpoint-0.16.0-dev.0/dataset/metadata_current/elasticsearch/transform/default.json'], + legacyScopedClusterClient.callAsCurrentUser, + savedObjectsClient + ); + + expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([ + [ + 'transport.request', + { + method: 'PUT', + path: '_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0', + query: 'defer_validation=true', + body: '{"content": "data"}', + }, + ], + [ + 'transport.request', + { + method: 'POST', + path: '_transform/metrics-endpoint.metadata_current-default-0.16.0-dev.0/_start', + }, + ], + ]); + expect(savedObjectsClient.update.mock.calls).toEqual([ + [ + 'epm-packages', + 'endpoint', + { + installed_es: [ + { id: 'metrics-endpoint.metadata_current-default-0.16.0-dev.0', type: 'transform' }, + ], + }, + ], + ]); + }); + + test('can removes older version when no new install in package', async () => { + const previousInstallation: Installation = ({ + installed_es: [ + { + id: 'metrics-endpoint.metadata-current-default-0.15.0-dev.0', + type: ElasticsearchAssetType.transform, + }, + ], + } as unknown) as Installation; + + const currentInstallation: Installation = ({ + installed_es: [], + } as unknown) as Installation; + + (getInstallation as jest.MockedFunction) + .mockReturnValueOnce(Promise.resolve(previousInstallation)) + .mockReturnValueOnce(Promise.resolve(currentInstallation)); + + (getInstallationObject as jest.MockedFunction< + typeof getInstallationObject + >).mockReturnValueOnce( + Promise.resolve(({ + attributes: { installed_es: currentInstallation.installed_es }, + } as unknown) as SavedObject) + ); + + await installTransformForDataset( + ({ + name: 'endpoint', + version: '0.16.0-dev.0', + datasets: [ + { + type: 'metrics', + name: 'endpoint.metadata', + title: 'Endpoint Metadata', + release: 'experimental', + package: 'endpoint', + ingest_pipeline: 'default', + elasticsearch: { + 'index_template.mappings': { + dynamic: false, + }, + }, + path: 'metadata', + }, + { + type: 'metrics', + name: 'endpoint.metadata_current', + title: 'Endpoint Metadata Current', + release: 'experimental', + package: 'endpoint', + ingest_pipeline: 'default', + elasticsearch: { + 'index_template.mappings': { + dynamic: false, + }, + }, + path: 'metadata_current', + }, + ], + } as unknown) as RegistryPackage, + [], + legacyScopedClusterClient.callAsCurrentUser, + savedObjectsClient + ); + + expect(legacyScopedClusterClient.callAsCurrentUser.mock.calls).toEqual([ + [ + 'transport.request', + { + ignore: [404], + method: 'POST', + path: '_transform/metrics-endpoint.metadata-current-default-0.15.0-dev.0/_stop', + query: 'force=true', + }, + ], + [ + 'transport.request', + { + ignore: [404], + method: 'DELETE', + path: '_transform/metrics-endpoint.metadata-current-default-0.15.0-dev.0', + query: 'force=true', + }, + ], + ]); + expect(savedObjectsClient.update.mock.calls).toEqual([ + [ + 'epm-packages', + 'endpoint', + { + installed_es: [], + }, + ], + ]); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts new file mode 100644 index 0000000000000..2f60c74d3514f --- /dev/null +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.test.ts @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ElasticsearchAssetType, Installation, KibanaAssetType } from '../../../types'; +import { SavedObject } from 'src/core/server'; +import { getInstallType } from './install'; + +const mockInstallation: SavedObject = { + id: 'test-pkg', + references: [], + type: 'epm-packages', + attributes: { + id: 'test-pkg', + installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + es_index_patterns: { pattern: 'pattern-name' }, + name: 'test packagek', + version: '1.0.0', + install_status: 'installed', + install_version: '1.0.0', + install_started_at: new Date().toISOString(), + }, +}; +const mockInstallationUpdateFail: SavedObject = { + id: 'test-pkg', + references: [], + type: 'epm-packages', + attributes: { + id: 'test-pkg', + installed_kibana: [{ type: KibanaAssetType.dashboard, id: 'dashboard-1' }], + installed_es: [{ type: ElasticsearchAssetType.ingestPipeline, id: 'pipeline' }], + es_index_patterns: { pattern: 'pattern-name' }, + name: 'test packagek', + version: '1.0.0', + install_status: 'installing', + install_version: '1.0.1', + install_started_at: new Date().toISOString(), + }, +}; +describe('install', () => { + describe('getInstallType', () => { + it('should return correct type when installing and no other version is currently installed', () => { + const installTypeInstall = getInstallType({ pkgVersion: '1.0.0', installedPkg: undefined }); + expect(installTypeInstall).toBe('install'); + + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'update').toBe(false); + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'reinstall').toBe(false); + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'reupdate').toBe(false); + // @ts-expect-error can only be 'install' if no installedPkg given + expect(installTypeInstall === 'rollback').toBe(false); + }); + + it('should return correct type when installing the same version', () => { + const installTypeReinstall = getInstallType({ + pkgVersion: '1.0.0', + installedPkg: mockInstallation, + }); + expect(installTypeReinstall).toBe('reinstall'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeReinstall === 'install').toBe(false); + }); + + it('should return correct type when moving from one version to another', () => { + const installTypeUpdate = getInstallType({ + pkgVersion: '1.0.1', + installedPkg: mockInstallation, + }); + expect(installTypeUpdate).toBe('update'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeUpdate === 'install').toBe(false); + }); + + it('should return correct type when update fails and trys again', () => { + const installTypeReupdate = getInstallType({ + pkgVersion: '1.0.1', + installedPkg: mockInstallationUpdateFail, + }); + expect(installTypeReupdate).toBe('reupdate'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeReupdate === 'install').toBe(false); + }); + + it('should return correct type when attempting to rollback from a failed update', () => { + const installTypeRollback = getInstallType({ + pkgVersion: '1.0.0', + installedPkg: mockInstallationUpdateFail, + }); + expect(installTypeRollback).toBe('rollback'); + + // @ts-expect-error cannot be 'install' if given installedPkg + expect(installTypeRollback === 'install').toBe(false); + }); + }); +}); diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts index e49dbe8f0b5d4..54b9c4d3fbb17 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/install.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SavedObjectsClientContract } from 'src/core/server'; +import { SavedObject, SavedObjectsClientContract } from 'src/core/server'; import semver from 'semver'; import { PACKAGES_SAVED_OBJECT_TYPE, MAX_TIME_COMPLETE_INSTALL } from '../../../constants'; import { @@ -16,6 +16,7 @@ import { KibanaAssetReference, EsAssetReference, ElasticsearchAssetType, + InstallType, } from '../../../types'; import { installIndexPatterns } from '../kibana/index_pattern/install'; import * as Registry from '../registry'; @@ -34,6 +35,7 @@ import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; import { deleteKibanaSavedObjectsAssets } from './remove'; import { PackageOutdatedError } from '../../../errors'; import { getPackageSavedObjects } from './get'; +import { installTransformForDataset } from '../elasticsearch/transform/install'; export async function installLatestPackage(options: { savedObjectsClient: SavedObjectsClientContract; @@ -110,11 +112,13 @@ export async function installPackage({ const latestPackage = await Registry.fetchFindLatestPackage(pkgName); // get the currently installed package const installedPkg = await getInstallationObject({ savedObjectsClient, pkgName }); - const reinstall = pkgVersion === installedPkg?.attributes.version; - const reupdate = pkgVersion === installedPkg?.attributes.install_version; - // let the user install if using the force flag or this is a reinstall or reupdate due to intallation interruption - if (semver.lt(pkgVersion, latestPackage.version) && !force && !reinstall && !reupdate) { + const installType = getInstallType({ pkgVersion, installedPkg }); + + // let the user install if using the force flag or needing to reinstall or install a previous version due to failed update + const installOutOfDateVersionOk = + installType === 'reinstall' || installType === 'reupdate' || installType === 'rollback'; + if (semver.lt(pkgVersion, latestPackage.version) && !force && !installOutOfDateVersionOk) { throw new PackageOutdatedError(`${pkgkey} is out-of-date and cannot be installed or updated`); } const paths = await Registry.getArchiveInfo(pkgName, pkgVersion); @@ -188,8 +192,15 @@ export async function installPackage({ // update current backing indices of each data stream await updateCurrentWriteIndices(callCluster, installedTemplates); - // if this is an update, delete the previous version's pipelines - if (installedPkg && !reinstall) { + const installedTransforms = await installTransformForDataset( + registryPackageInfo, + paths, + callCluster, + savedObjectsClient + ); + + // if this is an update or retrying an update, delete the previous version's pipelines + if ((installType === 'update' || installType === 'reupdate') && installedPkg) { await deletePreviousPipelines( callCluster, savedObjectsClient, @@ -197,19 +208,33 @@ export async function installPackage({ installedPkg.attributes.version ); } - + // pipelines from a different version may have installed during a failed update + if (installType === 'rollback' && installedPkg) { + await deletePreviousPipelines( + callCluster, + savedObjectsClient, + pkgName, + installedPkg.attributes.install_version + ); + } const installedTemplateRefs = installedTemplates.map((template) => ({ id: template.templateName, type: ElasticsearchAssetType.indexTemplate, })); await Promise.all([installKibanaAssetsPromise, installIndexPatternPromise]); + // update to newly installed version when all assets are successfully installed if (installedPkg) await updateVersion(savedObjectsClient, pkgName, pkgVersion); await savedObjectsClient.update(PACKAGES_SAVED_OBJECT_TYPE, pkgName, { install_version: pkgVersion, install_status: 'installed', }); - return [...installedKibanaAssetsRefs, ...installedPipelines, ...installedTemplateRefs]; + return [ + ...installedKibanaAssetsRefs, + ...installedPipelines, + ...installedTemplateRefs, + ...installedTransforms, + ]; } const updateVersion = async ( @@ -326,3 +351,38 @@ export async function ensurePackagesCompletedInstall( await Promise.all(installingPromises); return installingPackages; } + +interface NoPkgArgs { + pkgVersion: string; + installedPkg?: undefined; +} + +interface HasPkgArgs { + pkgVersion: string; + installedPkg: SavedObject; +} + +type OnlyInstall = Extract; +type NotInstall = Exclude; + +// overloads +export function getInstallType(args: NoPkgArgs): OnlyInstall; +export function getInstallType(args: HasPkgArgs): NotInstall; +export function getInstallType(args: NoPkgArgs | HasPkgArgs): OnlyInstall | NotInstall; + +// implementation +export function getInstallType(args: NoPkgArgs | HasPkgArgs): OnlyInstall | NotInstall { + const { pkgVersion, installedPkg } = args; + if (!installedPkg) return 'install'; + + const currentPkgVersion = installedPkg.attributes.version; + const lastStartedInstallVersion = installedPkg.attributes.install_version; + + if (pkgVersion === currentPkgVersion && pkgVersion !== lastStartedInstallVersion) + return 'rollback'; + if (pkgVersion === currentPkgVersion) return 'reinstall'; + if (pkgVersion === lastStartedInstallVersion && pkgVersion !== currentPkgVersion) + return 'reupdate'; + if (pkgVersion !== lastStartedInstallVersion && pkgVersion !== currentPkgVersion) return 'update'; + throw new Error('unknown install type'); +} diff --git a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts index 71eee1ee82c90..2434ebf27aa5d 100644 --- a/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/ingest_manager/server/services/epm/packages/remove.ts @@ -6,12 +6,17 @@ import { SavedObjectsClientContract } from 'src/core/server'; import Boom from 'boom'; -import { PACKAGES_SAVED_OBJECT_TYPE, PACKAGE_POLICY_SAVED_OBJECT_TYPE } from '../../../constants'; -import { AssetReference, AssetType, ElasticsearchAssetType } from '../../../types'; -import { CallESAsCurrentUser } from '../../../types'; +import { PACKAGE_POLICY_SAVED_OBJECT_TYPE, PACKAGES_SAVED_OBJECT_TYPE } from '../../../constants'; +import { + AssetReference, + AssetType, + CallESAsCurrentUser, + ElasticsearchAssetType, +} from '../../../types'; import { getInstallation, savedObjectTypes } from './index'; import { deletePipeline } from '../elasticsearch/ingest_pipeline/'; import { installIndexPatterns } from '../kibana/index_pattern/install'; +import { deleteTransforms } from '../elasticsearch/transform/remove'; import { packagePolicyService, appContextService } from '../..'; import { splitPkgKey, deletePackageCache, getArchiveInfo } from '../registry'; @@ -72,6 +77,8 @@ async function deleteAssets( return deletePipeline(callCluster, id); } else if (assetType === ElasticsearchAssetType.indexTemplate) { return deleteTemplate(callCluster, id); + } else if (assetType === ElasticsearchAssetType.transform) { + return deleteTransforms(callCluster, [id]); } }); try { diff --git a/x-pack/plugins/ingest_manager/server/services/setup.ts b/x-pack/plugins/ingest_manager/server/services/setup.ts index ec3a05a4fa390..f02057bae1598 100644 --- a/x-pack/plugins/ingest_manager/server/services/setup.ts +++ b/x-pack/plugins/ingest_manager/server/services/setup.ts @@ -170,6 +170,12 @@ export async function setupFleet( }); }) ); + + await Promise.all( + agentPolicies.map((agentPolicy) => + agentPolicyService.createFleetPolicyChangeAction(soClient, agentPolicy.id) + ) + ); } function generateRandomPassword() { diff --git a/x-pack/plugins/ingest_manager/server/types/index.tsx b/x-pack/plugins/ingest_manager/server/types/index.tsx index e01568cfbb3c9..d00491afef72b 100644 --- a/x-pack/plugins/ingest_manager/server/types/index.tsx +++ b/x-pack/plugins/ingest_manager/server/types/index.tsx @@ -16,7 +16,10 @@ export { AgentEvent, AgentEventSOAttributes, AgentAction, + AgentPolicyAction, + BaseAgentActionSOAttributes, AgentActionSOAttributes, + AgentPolicyActionSOAttributes, PackagePolicy, PackagePolicyInput, PackagePolicyInputStream, @@ -63,6 +66,7 @@ export { IndexTemplateMappings, Settings, SettingsSOAttributes, + InstallType, // Agent Request types PostAgentEnrollRequest, PostAgentCheckinRequest, diff --git a/x-pack/plugins/ingest_manager/server/types/models/agent.ts b/x-pack/plugins/ingest_manager/server/types/models/agent.ts index 5ad98cfd40622..b249705fe6c2f 100644 --- a/x-pack/plugins/ingest_manager/server/types/models/agent.ts +++ b/x-pack/plugins/ingest_manager/server/types/models/agent.ts @@ -62,12 +62,7 @@ export const AgentEventSchema = schema.object({ }); export const NewAgentActionSchema = schema.object({ - type: schema.oneOf([ - schema.literal('CONFIG_CHANGE'), - schema.literal('DATA_DUMP'), - schema.literal('RESUME'), - schema.literal('PAUSE'), - ]), + type: schema.oneOf([schema.literal('CONFIG_CHANGE'), schema.literal('UNENROLL')]), data: schema.maybe(schema.any()), sent_at: schema.maybe(schema.string()), }); diff --git a/x-pack/plugins/ingest_pipelines/kibana.json b/x-pack/plugins/ingest_pipelines/kibana.json index 75e5e9b5d6c51..38d28fbba20b4 100644 --- a/x-pack/plugins/ingest_pipelines/kibana.json +++ b/x-pack/plugins/ingest_pipelines/kibana.json @@ -3,7 +3,7 @@ "version": "8.0.0", "server": true, "ui": true, - "requiredPlugins": ["licensing", "management"], + "requiredPlugins": ["licensing", "management", "features"], "optionalPlugins": ["security", "usageCollection"], "configPath": ["xpack", "ingest_pipelines"], "requiredBundles": ["esUiShared", "kibanaReact"] diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx index b12f324528167..38c652f41e5e1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/__jest__/pipeline_processors_editor.test.tsx @@ -184,5 +184,14 @@ describe('Pipeline Editor', () => { expect(find('processors>0.moveItemButton').props().disabled).toBe(true); expect(find('processors>1.moveItemButton').props().disabled).toBe(true); }); + + it('can move a processor into an empty tree', () => { + const { actions } = testBed; + actions.moveProcessor('processors>0', 'onFailure.dropButtonEmptyTree'); + const [onUpdateResult2] = onUpdate.mock.calls[onUpdate.mock.calls.length - 1]; + const data = onUpdateResult2.getData(); + expect(data.processors).toEqual([testProcessors.processors[1]]); + expect(data.on_failure).toEqual([testProcessors.processors[0]]); + }); }); }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx index 276d684e3dca1..4aabcc1d59d73 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/add_processor_button.tsx @@ -21,6 +21,7 @@ export const AddProcessorButton: FunctionComponent = (props) => { return ( = ({ processor }) if (type?.length) { const formDescriptor = getProcessorDescriptor(type as any); - if (formDescriptor?.FieldsComponent) { - const renderedFields = ( + if (formDescriptor) { + const renderedFields = formDescriptor.FieldsComponent ? ( - ); + ) : null; return ( <> {renderedFields ? ( diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/properties_field.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/properties_field.tsx new file mode 100644 index 0000000000000..404a80161068c --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/common_fields/properties_field.tsx @@ -0,0 +1,51 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiComboBoxOptionOption } from '@elastic/eui'; +import { ComboBoxField, FIELD_TYPES, UseField } from '../../../../../../../shared_imports'; + +import { FieldsConfig, to } from '../shared'; + +const fieldsConfig: FieldsConfig = { + properties: { + type: FIELD_TYPES.COMBO_BOX, + deserializer: to.arrayOfStrings, + serializer: (v: string[]) => (v.length ? v : undefined), + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.commonFields.propertiesFieldLabel', + { + defaultMessage: 'Properties (optional)', + } + ), + }, +}; + +interface Props { + helpText?: React.ReactNode; + propertyOptions?: EuiComboBoxOptionOption[]; +} + +export const PropertiesField: FunctionComponent = ({ helpText, propertyOptions }) => { + return ( + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/drop.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/drop.tsx index 87b6cb76cdcce..7bc299532df9e 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/drop.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/drop.tsx @@ -4,11 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FunctionComponent } from 'react'; - /** * This fields component has no unique fields */ -export const Drop: FunctionComponent = () => { - return null; -}; +export const Drop = undefined; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx index c0624c988061c..937fa4d3c4d86 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/geoip.tsx @@ -9,18 +9,13 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { EuiCode } from '@elastic/eui'; -import { - FIELD_TYPES, - UseField, - Field, - ComboBoxField, - ToggleField, -} from '../../../../../../shared_imports'; +import { FIELD_TYPES, UseField, Field, ToggleField } from '../../../../../../shared_imports'; import { FieldNameField } from './common_fields/field_name_field'; import { IgnoreMissingField } from './common_fields/ignore_missing_field'; import { FieldsConfig, from, to } from './shared'; import { TargetField } from './common_fields/target_field'; +import { PropertiesField } from './common_fields/properties_field'; const fieldsConfig: FieldsConfig = { /* Optional field config */ @@ -42,21 +37,6 @@ const fieldsConfig: FieldsConfig = { ), }, - properties: { - type: FIELD_TYPES.COMBO_BOX, - deserializer: to.arrayOfStrings, - label: i18n.translate('xpack.ingestPipelines.pipelineEditor.geoIPForm.propertiesFieldLabel', { - defaultMessage: 'Properties (optional)', - }), - helpText: i18n.translate( - 'xpack.ingestPipelines.pipelineEditor.geoIPForm.propertiesFieldHelpText', - { - defaultMessage: - 'Properties added to the target field. Valid properties depend on the database file used.', - } - ), - }, - first_only: { type: FIELD_TYPES.TOGGLE, defaultValue: true, @@ -95,10 +75,14 @@ export const GeoIP: FunctionComponent = () => { - diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts index e83560b4a44ce..e211d682ab0f0 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/index.ts @@ -34,5 +34,9 @@ export { SetProcessor } from './set'; export { SetSecurityUser } from './set_security_user'; export { Split } from './split'; export { Sort } from './sort'; +export { Trim } from './trim'; +export { Uppercase } from './uppercase'; +export { UrlDecode } from './url_decode'; +export { UserAgent } from './user_agent'; export { FormFieldsComponent } from './shared'; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx index f51bf19ad180a..4104e8f727ab1 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/kv.tsx @@ -33,9 +33,15 @@ const fieldsConfig: FieldsConfig = { label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitFieldLabel', { defaultMessage: 'Field split', }), - helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.fieldSplitHelpText', { - defaultMessage: 'Regex pattern for splitting key-value pairs.', - }), + helpText: ( + {'" "'}, + }} + /> + ), validations: [ { validator: emptyField( @@ -52,9 +58,15 @@ const fieldsConfig: FieldsConfig = { label: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitFieldLabel', { defaultMessage: 'Value split', }), - helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.valueSplitHelpText', { - defaultMessage: 'Regex pattern for splitting the key from the value within a key-value pair.', - }), + helpText: ( + {'"="'}, + }} + /> + ), validations: [ { validator: emptyField( @@ -75,8 +87,7 @@ const fieldsConfig: FieldsConfig = { defaultMessage: 'Include keys', }), helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.includeKeysHelpText', { - defaultMessage: - 'List of keys to filter and insert into document. Defaults to including all keys.', + defaultMessage: 'List of extracted keys to include in the output. Defaults to all keys.', }), }, @@ -88,7 +99,7 @@ const fieldsConfig: FieldsConfig = { defaultMessage: 'Exclude keys', }), helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.excludeKeysHelpText', { - defaultMessage: 'List of keys to exclude from document.', + defaultMessage: 'List of extracted keys to exclude from the output.', }), }, @@ -99,7 +110,7 @@ const fieldsConfig: FieldsConfig = { defaultMessage: 'Prefix', }), helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.kvForm.prefixHelpText', { - defaultMessage: 'Prefix to be added to extracted keys.', + defaultMessage: 'Prefix to add to extracted keys.', }), }, @@ -136,7 +147,7 @@ const fieldsConfig: FieldsConfig = { helpText: ( {'()'}, angle: <>, @@ -154,7 +165,7 @@ export const Kv: FunctionComponent = () => { <> @@ -166,8 +177,7 @@ export const Kv: FunctionComponent = () => { helpText={i18n.translate( 'xpack.ingestPipelines.pipelineEditor.kvForm.targetFieldHelpText', { - defaultMessage: - 'Field to insert the extracted keys into. Defaults to the root of the document.', + defaultMessage: 'Output field for the extracted fields. Defaults to the document root.', } )} /> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx index 9db313a05007f..0d8170338ea10 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/lowercase.tsx @@ -6,8 +6,6 @@ import React, { FunctionComponent } from 'react'; import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiCode } from '@elastic/eui'; import { FieldNameField } from './common_fields/field_name_field'; import { TargetField } from './common_fields/target_field'; @@ -23,17 +21,7 @@ export const Lowercase: FunctionComponent = () => { )} /> - {'field'}, - }} - /> - } - /> + diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx index c785cf935833d..57843e2411359 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/pipeline.tsx @@ -27,7 +27,7 @@ const fieldsConfig: FieldsConfig = { helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.pipelineForm.pipelineNameFieldHelpText', { - defaultMessage: 'Name of the pipeline to execute.', + defaultMessage: 'Name of the ingest pipeline to run.', } ), validations: [ diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx index 3e90ce2b76f7b..3ba1cdb0c802d 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/remove.tsx @@ -29,7 +29,7 @@ const fieldsConfig: FieldsConfig = { defaultMessage: 'Fields', }), helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.removeForm.fieldNameHelpText', { - defaultMessage: 'Fields to be removed.', + defaultMessage: 'Fields to remove.', }), validations: [ { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx index 8b796d9664586..099e2bd2c80fb 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/rename.tsx @@ -21,7 +21,7 @@ export const Rename: FunctionComponent = () => { @@ -31,7 +31,7 @@ export const Rename: FunctionComponent = () => { })} helpText={i18n.translate( 'xpack.ingestPipelines.pipelineEditor.renameForm.targetFieldHelpText', - { defaultMessage: 'Name of the new field.' } + { defaultMessage: 'New field name. This field cannot already exist.' } )} validations={[ { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx index ae0bbbb490ae9..de28f66766603 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/script.tsx @@ -32,7 +32,7 @@ const fieldsConfig: FieldsConfig = { helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.scriptForm.storedScriptIDFieldHelpText', { - defaultMessage: 'Stored script reference.', + defaultMessage: 'ID of the stored script to run.', } ), validations: [ @@ -55,7 +55,7 @@ const fieldsConfig: FieldsConfig = { helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.scriptForm.sourceFieldHelpText', { - defaultMessage: 'Script to be executed.', + defaultMessage: 'Inline script to run.', } ), validations: [ @@ -98,7 +98,7 @@ const fieldsConfig: FieldsConfig = { helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.scriptForm.paramsFieldHelpText', { - defaultMessage: 'Script parameters.', + defaultMessage: 'Named parameters passed to the script as variables.', } ), validations: [ @@ -128,7 +128,7 @@ export const Script: FormFieldsComponent = ({ initialFieldValues }) => { setShowId((v) => !v)} diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx index c282be35e5071..04ea0c44c3513 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set.tsx @@ -32,13 +32,13 @@ const fieldsConfig: FieldsConfig = { defaultMessage: 'Value', }), helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueFieldHelpText', { - defaultMessage: 'Value to be set for the field', + defaultMessage: 'Value for the field.', }), validations: [ { validator: emptyField( i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.valueRequiredError', { - defaultMessage: 'A value is required', + defaultMessage: 'A value is required.', }) ), }, @@ -53,9 +53,15 @@ const fieldsConfig: FieldsConfig = { label: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldLabel', { defaultMessage: 'Override', }), - helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.setForm.overrideFieldHelpText', { - defaultMessage: 'If disabled, fields containing non-null values will not be updated.', - }), + helpText: ( + {'null'}, + }} + /> + ), }, ignore_empty_value: { type: FIELD_TYPES.TOGGLE, @@ -71,7 +77,8 @@ const fieldsConfig: FieldsConfig = { helpText: ( {'value'}, nullValue: {'null'}, @@ -89,7 +96,7 @@ export const SetProcessor: FunctionComponent = () => { <> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx index 78128b3d54c75..46bfe8c97ebea 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/set_security_user.tsx @@ -44,7 +44,7 @@ const fieldsConfig: FieldsConfig = { helpText: ( [{helpTextValues}], }} @@ -60,7 +60,7 @@ export const SetSecurityUser: FunctionComponent = () => { helpText={i18n.translate( 'xpack.ingestPipelines.pipelineEditor.setSecurityUserForm.fieldNameField', { - defaultMessage: 'Field to store the user information', + defaultMessage: 'Output field.', } )} /> diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx index cdd0ff888accf..c8c0562011fd6 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/sort.tsx @@ -24,7 +24,8 @@ const fieldsConfig: FieldsConfig = { defaultMessage: 'Order', }), helpText: i18n.translate('xpack.ingestPipelines.pipelineEditor.sortForm.orderFieldHelpText', { - defaultMessage: 'Sort order to use', + defaultMessage: + 'Sort order. Arrays containing a mix of strings and numbers are sorted lexicographically.', }), }, }; @@ -35,7 +36,7 @@ export const Sort: FunctionComponent = () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx index b48ce74110b39..fa178aaddd314 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/split.tsx @@ -33,7 +33,7 @@ const fieldsConfig: FieldsConfig = { helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.splitForm.separatorFieldHelpText', { - defaultMessage: 'Regex to match a separator', + defaultMessage: 'Regex pattern used to delimit the field value.', } ), validations: [ @@ -60,7 +60,7 @@ const fieldsConfig: FieldsConfig = { ), helpText: i18n.translate( 'xpack.ingestPipelines.pipelineEditor.splitForm.preserveTrailingFieldHelpText', - { defaultMessage: 'If enabled, preserve any trailing space.' } + { defaultMessage: 'Preserve any trailing whitespace in the split field values.' } ), }, }; @@ -71,7 +71,7 @@ export const Split: FunctionComponent = () => { diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/trim.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/trim.tsx new file mode 100644 index 0000000000000..aca5a3b4121b5 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/trim.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; + +export const Trim: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/uppercase.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/uppercase.tsx new file mode 100644 index 0000000000000..336b68f8c2b7b --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/uppercase.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; + +export const Uppercase: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/url_decode.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/url_decode.tsx new file mode 100644 index 0000000000000..196645a89f707 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/url_decode.tsx @@ -0,0 +1,29 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; + +export const UrlDecode: FunctionComponent = () => { + return ( + <> + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/user_agent.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/user_agent.tsx new file mode 100644 index 0000000000000..8395833c09f28 --- /dev/null +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/manage_processor_form/processors/user_agent.tsx @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FunctionComponent } from 'react'; +import { i18n } from '@kbn/i18n'; + +import { EuiComboBoxOptionOption } from '@elastic/eui'; +import { FIELD_TYPES, UseField, Field } from '../../../../../../shared_imports'; + +import { FieldsConfig } from './shared'; +import { IgnoreMissingField } from './common_fields/ignore_missing_field'; +import { FieldNameField } from './common_fields/field_name_field'; +import { TargetField } from './common_fields/target_field'; +import { PropertiesField } from './common_fields/properties_field'; + +const propertyOptions: EuiComboBoxOptionOption[] = [ + { label: 'name' }, + { label: 'os' }, + { label: 'device' }, + { label: 'original' }, + { label: 'version' }, +]; + +const fieldsConfig: FieldsConfig = { + /* Optional fields config */ + regex_file: { + type: FIELD_TYPES.TEXT, + deserializer: String, + label: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldLabel', + { + defaultMessage: 'Regex file (optional)', + } + ), + helpText: i18n.translate( + 'xpack.ingestPipelines.pipelineEditor.userAgentForm.regexFileFieldHelpText', + { + defaultMessage: + 'A filename containing the regular expressions for parsing the user agent string.', + } + ), + }, +}; + +export const UserAgent: FunctionComponent = () => { + return ( + <> + + + + + + + + + + + ); +}; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss index a54cc994ab730..25e4eb7320bf4 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.scss @@ -51,6 +51,10 @@ } } + &__addProcessorButton { + width: fit-content; + } + &__onFailureHandlerContainer { margin-top: $euiSizeS; margin-bottom: $euiSizeS; diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx index 4458bd66c88de..8b344a137f3a8 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/processors_tree/processors_tree.tsx @@ -7,12 +7,14 @@ import React, { FunctionComponent, memo, useRef, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem, keys } from '@elastic/eui'; import { List, WindowScroller } from 'react-virtualized'; +import { DropSpecialLocations } from '../../constants'; import { ProcessorInternal, ProcessorSelector } from '../../types'; import { selectorToDataTestSubject } from '../../utils'; +import { AddProcessorButton } from '../add_processor_button'; + +import { PrivateTree, DropZoneButton } from './components'; import './processors_tree.scss'; -import { AddProcessorButton } from '../add_processor_button'; -import { PrivateTree } from './components'; export interface ProcessorInfo { id: string; @@ -96,8 +98,25 @@ export const ProcessorsTree: FunctionComponent = memo((props) => { /> - - + + + {!processors.length && ( + { + event.preventDefault(); + onAction({ + type: 'move', + payload: { + destination: baseSelector.concat(DropSpecialLocations.top), + source: movingProcessor!.selector, + }, + }); + }} + /> + )} { onAction({ type: 'addProcessor', payload: { target: baseSelector } }); diff --git a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx index 59ec64944a3c9..95a8d35c119a6 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/components/pipeline_processors_editor/components/shared/map_processor_type_to_form.tsx @@ -41,6 +41,10 @@ import { SetSecurityUser, Split, Sort, + Trim, + Uppercase, + UrlDecode, + UserAgent, FormFieldsComponent, } from '../manage_processor_form/processors'; @@ -107,7 +111,7 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { defaultMessage: 'CSV', }), description: i18n.translate('xpack.ingestPipelines.processors.description.csv', { - defaultMessage: 'Extracts fields values from CSV data.', + defaultMessage: 'Extracts field values from CSV data.', }), }, date: { @@ -306,7 +310,10 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { FieldsComponent: Kv, docLinkPath: '/kv-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.kv', { - defaultMessage: 'KV', + defaultMessage: 'Key-value (KV)', + }), + description: i18n.translate('xpack.ingestPipelines.processors.description.kv', { + defaultMessage: 'Extracts fields from a string containing key-value pairs.', }), }, lowercase: { @@ -315,6 +322,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.lowercase', { defaultMessage: 'Lowercase', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.lowercase', { + defaultMessage: 'Converts a string to lowercase.', + }), }, pipeline: { FieldsComponent: Pipeline, @@ -322,6 +332,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.pipeline', { defaultMessage: 'Pipeline', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.pipeline', { + defaultMessage: 'Runs another ingest node pipeline.', + }), }, remove: { FieldsComponent: Remove, @@ -329,6 +342,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.remove', { defaultMessage: 'Remove', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.remove', { + defaultMessage: 'Removes one or more fields.', + }), }, rename: { FieldsComponent: Rename, @@ -336,6 +352,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.rename', { defaultMessage: 'Rename', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.rename', { + defaultMessage: 'Renames an existing field.', + }), }, script: { FieldsComponent: Script, @@ -343,6 +362,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.script', { defaultMessage: 'Script', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.script', { + defaultMessage: 'Runs a script on incoming documents.', + }), }, set: { FieldsComponent: SetProcessor, @@ -350,6 +372,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.set', { defaultMessage: 'Set', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.set', { + defaultMessage: 'Sets the value of a field.', + }), }, set_security_user: { FieldsComponent: SetSecurityUser, @@ -357,12 +382,9 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.setSecurityUser', { defaultMessage: 'Set security user', }), - }, - split: { - FieldsComponent: Split, - docLinkPath: '/split-processor.html', - label: i18n.translate('xpack.ingestPipelines.processors.label.split', { - defaultMessage: 'Split', + description: i18n.translate('xpack.ingestPipelines.processors.description.setSecurityUser', { + defaultMessage: + 'Adds details about the current user, such user name and email address, to incoming documents. Requires an authenticated user for the indexing request.', }), }, sort: { @@ -371,30 +393,43 @@ export const mapProcessorTypeToDescriptor: MapProcessorTypeToDescriptor = { label: i18n.translate('xpack.ingestPipelines.processors.label.sort', { defaultMessage: 'Sort', }), + description: i18n.translate('xpack.ingestPipelines.processors.description.sort', { + defaultMessage: "Sorts a field's array elements.", + }), + }, + split: { + FieldsComponent: Split, + docLinkPath: '/split-processor.html', + label: i18n.translate('xpack.ingestPipelines.processors.label.split', { + defaultMessage: 'Split', + }), + description: i18n.translate('xpack.ingestPipelines.processors.description.split', { + defaultMessage: 'Splits a field value into an array.', + }), }, trim: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Trim, docLinkPath: '/trim-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.trim', { defaultMessage: 'Trim', }), }, uppercase: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: Uppercase, docLinkPath: '/uppercase-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.uppercase', { defaultMessage: 'Uppercase', }), }, urldecode: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: UrlDecode, docLinkPath: '/urldecode-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.urldecode', { defaultMessage: 'URL decode', }), }, user_agent: { - FieldsComponent: undefined, // TODO: Implement + FieldsComponent: UserAgent, docLinkPath: '/user-agent-processor.html', label: i18n.translate('xpack.ingestPipelines.processors.label.userAgent', { defaultMessage: 'User agent', diff --git a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx index ccb50376dddb7..88148f1bc5746 100644 --- a/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx +++ b/x-pack/plugins/ingest_pipelines/public/application/sections/pipelines_list/main.tsx @@ -51,7 +51,7 @@ export const PipelinesList: React.FunctionComponent = ({ const [pipelinesToDelete, setPipelinesToDelete] = useState([]); - const { data, isLoading, error, sendRequest } = services.api.useLoadPipelines(); + const { data, isLoading, error, resendRequest } = services.api.useLoadPipelines(); // Track component loaded useEffect(() => { @@ -98,7 +98,7 @@ export const PipelinesList: React.FunctionComponent = ({ } else if (data?.length) { content = ( = ({ defaultMessage="Unable to load pipelines. {reloadLink}" values={{ reloadLink: ( - + = ({ callback={(deleteResponse) => { if (deleteResponse?.hasDeletedPipelines) { // reload pipelines list - sendRequest(); + resendRequest(); setSelectedPipeline(undefined); goHome(); } diff --git a/x-pack/plugins/ingest_pipelines/server/plugin.ts b/x-pack/plugins/ingest_pipelines/server/plugin.ts index 7a78bf608b8e1..12668e7c4eadb 100644 --- a/x-pack/plugins/ingest_pipelines/server/plugin.ts +++ b/x-pack/plugins/ingest_pipelines/server/plugin.ts @@ -25,7 +25,7 @@ export class IngestPipelinesPlugin implements Plugin { this.apiRoutes = new ApiRoutes(); } - public setup({ http }: CoreSetup, { licensing, security }: Dependencies) { + public setup({ http }: CoreSetup, { licensing, security, features }: Dependencies) { this.logger.debug('ingest_pipelines: setup'); const router = http.createRouter(); @@ -44,6 +44,19 @@ export class IngestPipelinesPlugin implements Plugin { } ); + features.registerElasticsearchFeature({ + id: 'ingest_pipelines', + management: { + ingest: ['ingest_pipelines'], + }, + privileges: [ + { + ui: [], + requiredClusterPrivileges: ['manage_pipeline', 'cluster:monitor/nodes/info'], + }, + ], + }); + this.apiRoutes.setup({ router, license: this.license, diff --git a/x-pack/plugins/ingest_pipelines/server/types.ts b/x-pack/plugins/ingest_pipelines/server/types.ts index 261317daa26d9..c5d9158caa569 100644 --- a/x-pack/plugins/ingest_pipelines/server/types.ts +++ b/x-pack/plugins/ingest_pipelines/server/types.ts @@ -7,11 +7,13 @@ import { IRouter } from 'src/core/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { SecurityPluginSetup } from '../../security/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { License } from './services'; import { isEsError } from './shared_imports'; export interface Dependencies { security: SecurityPluginSetup; + features: FeaturesPluginSetup; licensing: LicensingPluginSetup; } diff --git a/x-pack/plugins/lens/kibana.json b/x-pack/plugins/lens/kibana.json index b8747fc1f0cde..67d9d5ef64483 100644 --- a/x-pack/plugins/lens/kibana.json +++ b/x-pack/plugins/lens/kibana.json @@ -8,7 +8,7 @@ "data", "expressions", "navigation", - "kibanaLegacy", + "urlForwarding", "visualizations", "dashboard", "charts" diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss index 5b968abd0c061..954fbfadf159b 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_index.scss @@ -1,3 +1,2 @@ @import 'config_panel'; -@import 'dimension_popover'; @import 'layer_panel'; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss index 62bc6d7ed7cc8..ab53ff983ca26 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_layer_panel.scss @@ -43,6 +43,14 @@ min-height: $euiSizeXXL; } +.lnsLayerPanel__anchor { + width: 100%; +} + +.lnsLayerPanel__dndGrab { + padding: $euiSizeS; +} + .lnsLayerPanel__styleEditor { width: $euiSize * 30; padding: $euiSizeS; diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.scss similarity index 51% rename from x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss rename to x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.scss index 691cda9ff0d79..98036c7f31bd9 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/_dimension_popover.scss +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.scss @@ -9,3 +9,10 @@ display: block; word-break: break-word; } + +// todo: remove after closing https://github.com/elastic/eui/issues/3548 +.lnsDimensionPopover__fixTranslateDnd { + // sass-lint:disable-block no-important + transform: none !important; +} + diff --git a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx index 8d31e1bcc2e6a..a90bd8122d18e 100644 --- a/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx +++ b/x-pack/plugins/lens/public/editor_frame_service/editor_frame/config_panel/dimension_popover.tsx @@ -3,6 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ +import './dimension_popover.scss'; import React from 'react'; import { EuiPopover } from '@elastic/eui'; @@ -31,6 +32,7 @@ export function DimensionPopover({ = { + terms: i18n.translate('xpack.lens.indexPattern.groupingOverallTerms', { + defaultMessage: 'Overall top {field}', + values: { field: fieldName }, + }), + filters: i18n.translate('xpack.lens.indexPattern.groupingOverallFilters', { + defaultMessage: 'Top values for each custom query', + }), + date_histogram: i18n.translate('xpack.lens.indexPattern.groupingOverallDateHistogram', { + defaultMessage: 'Top values for each {field}', + values: { field: fieldName }, + }), + }; + + const bottomLevelCopy: Record = { + terms: i18n.translate('xpack.lens.indexPattern.groupingSecondTerms', { + defaultMessage: 'Top values for each {target}', + values: { target: target.fieldName }, + }), + filters: i18n.translate('xpack.lens.indexPattern.groupingSecondFilters', { + defaultMessage: 'Overall top {target}', + values: { target: target.fieldName }, + }), + date_histogram: i18n.translate('xpack.lens.indexPattern.groupingSecondDateHistogram', { + defaultMessage: 'Overall top {target}', + values: { target: target.fieldName }, + }), + }; + return ( <> @@ -73,34 +104,14 @@ export function BucketNestingEditor({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx index 038b51b922286..d5f0110f071f1 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/dimension_panel/popover_editor.tsx @@ -160,6 +160,11 @@ export function PopoverEditor(props: PopoverEditorProps) { compatibleWithCurrentField ? '' : ' incompatible' }`, onClick() { + // todo: when moving from terms agg to filters, we want to create a filter `$field.name : *` + // it probably has to be re-thought when removing the field name. + const isTermsToFilters = + selectedColumn?.operationType === 'terms' && operationType === 'filters'; + if (!selectedColumn || !compatibleWithCurrentField) { const possibleFields = fieldByOperation[operationType] || []; @@ -186,7 +191,7 @@ export function PopoverEditor(props: PopoverEditorProps) { trackUiEvent(`indexpattern_dimension_operation_${operationType}`); return; } - if (incompatibleSelectedOperationType) { + if (incompatibleSelectedOperationType && !isTermsToFilters) { setInvalidOperationType(null); } if (selectedColumn.operationType === operationType) { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx index a0cc5ec352130..cf15c29844053 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/field_item.tsx @@ -117,14 +117,7 @@ export const InnerFieldItem = function InnerFieldItem(props: FieldItemProps) { ); function fetchData() { - if ( - state.isLoading || - (field.type !== 'number' && - field.type !== 'string' && - field.type !== 'date' && - field.type !== 'boolean' && - field.type !== 'ip') - ) { + if (state.isLoading) { return; } diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx index e2ca933504849..3b3750cf7c560 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/indexpattern.tsx @@ -263,6 +263,7 @@ export function getIndexPatternDatasource({ data, savedObjects: core.savedObjects, docLinks: core.docLinks, + http: core.http, }} > ({ - name: field.name, - displayName: field.displayName, - type: field.type, - aggregatable: field.aggregatable, - searchable: field.searchable, - scripted: field.scripted, - esTypes: field.esTypes, - }) + (field): IndexPatternField => { + // Convert the getters on the index pattern service into plain JSON + const base = { + name: field.name, + displayName: field.displayName, + type: field.type, + aggregatable: field.aggregatable, + searchable: field.searchable, + esTypes: field.esTypes, + scripted: field.scripted, + }; + + // Simplifies tests by hiding optional properties instead of undefined + return base.scripted + ? { + ...base, + lang: field.lang, + script: field.script, + } + : base; + } ) .concat(documentField); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts index 31e6240993d36..21ed23321cf57 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts +++ b/x-pack/plugins/lens/public/indexpattern_datasource/mocks.ts @@ -64,6 +64,16 @@ export const createMockedIndexPattern = (): IndexPattern => ({ searchable: true, esTypes: ['keyword'], }, + { + name: 'scripted', + displayName: 'Scripted', + type: 'string', + searchable: true, + aggregatable: true, + scripted: true, + lang: 'painless', + script: '1234', + }, ], }); @@ -95,6 +105,8 @@ export const createMockedRestrictedIndexPattern = () => ({ searchable: true, scripted: true, esTypes: ['keyword'], + lang: 'painless', + script: '1234', }, ], typeMeta: { diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx index ad04891b637d4..b0777c7febd7d 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/cardinality.tsx @@ -59,7 +59,11 @@ export const cardinalityOperation: OperationDefinition ({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx index 4e081da2c6dc9..bb1aef856de78 100644 --- a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/count.tsx @@ -49,7 +49,11 @@ export const countOperation: OperationDefinition = { scale: 'ratio', sourceField: field.name, params: - previousColumn && previousColumn.dataType === 'number' ? previousColumn.params : undefined, + previousColumn?.dataType === 'number' && + previousColumn.params && + 'format' in previousColumn.params + ? previousColumn.params + : undefined, }; }, toEsAggsConfig: (column, columnId) => ({ diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.scss b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.scss new file mode 100644 index 0000000000000..6838812e4b999 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.scss @@ -0,0 +1,3 @@ +.lnsIndexPatternDimensionEditor__filtersEditor { + width: $euiSize * 60; +} diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx new file mode 100644 index 0000000000000..4d4b4018d75a7 --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.test.tsx @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { MouseEventHandler } from 'react'; +import { shallow } from 'enzyme'; +import { act } from 'react-dom/test-utils'; +import { EuiPopover, EuiLink } from '@elastic/eui'; +import { createMockedIndexPattern } from '../../../mocks'; +import { FilterPopover, QueryInput, LabelInput } from './filter_popover'; + +jest.mock('.', () => ({ + isQueryValid: () => true, + defaultLabel: 'label', +})); + +const defaultProps = { + filter: { + input: { query: 'bytes >= 1', language: 'kuery' }, + label: 'More than one', + id: '1', + }, + setFilter: jest.fn(), + indexPattern: createMockedIndexPattern(), + Button: ({ onClick }: { onClick: MouseEventHandler }) => ( + trigger + ), + isOpenByCreation: true, + setIsOpenByCreation: jest.fn(), +}; + +describe('filter popover', () => { + jest.mock('../../../../../../../../src/plugins/data/public', () => ({ + QueryStringInput: () => { + return 'QueryStringInput'; + }, + })); + it('should be open if is open by creation', () => { + const setIsOpenByCreation = jest.fn(); + const instance = shallow( + + ); + expect(instance.find(EuiPopover).prop('isOpen')).toEqual(true); + act(() => { + instance.find(EuiPopover).prop('closePopover')!(); + }); + instance.update(); + expect(setIsOpenByCreation).toHaveBeenCalledWith(false); + }); + it('should call setFilter when modifying QueryInput', () => { + const setFilter = jest.fn(); + const instance = shallow(); + instance.find(QueryInput).prop('onChange')!({ + query: 'modified : query', + language: 'lucene', + }); + expect(setFilter).toHaveBeenCalledWith({ + input: { + language: 'lucene', + query: 'modified : query', + }, + label: 'More than one', + id: '1', + }); + }); + it('should call setFilter when modifying LabelInput', () => { + const setFilter = jest.fn(); + const instance = shallow(); + instance.find(LabelInput).prop('onChange')!('Modified label'); + expect(setFilter).toHaveBeenCalledWith({ + input: { + language: 'kuery', + query: 'bytes >= 1', + }, + label: 'Modified label', + id: '1', + }); + }); +}); diff --git a/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx new file mode 100644 index 0000000000000..cdfa19f53a13a --- /dev/null +++ b/x-pack/plugins/lens/public/indexpattern_datasource/operations/definitions/filters/filter_popover.tsx @@ -0,0 +1,193 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import './filter_popover.scss'; + +import React, { MouseEventHandler, useState } from 'react'; +import { useDebounce } from 'react-use'; +import { EuiPopover, EuiFieldText, EuiSpacer, keys } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { FilterValue, defaultLabel, isQueryValid } from '.'; +import { IndexPattern } from '../../../types'; +import { QueryStringInput, Query } from '../../../../../../../../src/plugins/data/public'; + +export const FilterPopover = ({ + filter, + setFilter, + indexPattern, + Button, + isOpenByCreation, + setIsOpenByCreation, +}: { + filter: FilterValue; + setFilter: Function; + indexPattern: IndexPattern; + Button: React.FunctionComponent<{ onClick: MouseEventHandler }>; + isOpenByCreation: boolean; + setIsOpenByCreation: Function; +}) => { + const [isPopoverOpen, setIsPopoverOpen] = useState(false); + const inputRef = React.useRef(); + + const setPopoverOpen = (isOpen: boolean) => { + setIsPopoverOpen(isOpen); + setIsOpenByCreation(isOpen); + }; + + const setFilterLabel = (label: string) => setFilter({ ...filter, label }); + const setFilterQuery = (input: Query) => setFilter({ ...filter, input }); + + const getPlaceholder = (query: Query['query']) => { + if (query === '') { + return defaultLabel; + } + if (query === 'object') return JSON.stringify(query); + else { + return String(query); + } + }; + + return ( + { + setPopoverOpen(false); + }} + button={ +
@@ -59,8 +60,9 @@ exports[`should render metrics editor 1`] = ` "type": "sum", } } - metricsFilter={[Function]} onChange={[Function]} + onRemove={[Function]} + showRemoveButton={false} />
diff --git a/x-pack/plugins/maps/public/components/_metric_editors.scss b/x-pack/plugins/maps/public/components/metrics_editor/_metric_editors.scss similarity index 100% rename from x-pack/plugins/maps/public/components/_metric_editors.scss rename to x-pack/plugins/maps/public/components/metrics_editor/_metric_editors.scss diff --git a/x-pack/plugins/ml/public/application/components/messagebar/index.ts b/x-pack/plugins/maps/public/components/metrics_editor/index.ts similarity index 80% rename from x-pack/plugins/ml/public/application/components/messagebar/index.ts rename to x-pack/plugins/maps/public/components/metrics_editor/index.ts index 35130d28a890d..3c105c2d798ff 100644 --- a/x-pack/plugins/ml/public/application/components/messagebar/index.ts +++ b/x-pack/plugins/maps/public/components/metrics_editor/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { mlMessageBarService } from './messagebar_service'; +export { MetricsEditor } from './metrics_editor'; diff --git a/x-pack/plugins/maps/public/components/metric_editor.js b/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx similarity index 59% rename from x-pack/plugins/maps/public/components/metric_editor.js rename to x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx index 96b52d84653b2..543d144efdcc7 100644 --- a/x-pack/plugins/maps/public/components/metric_editor.js +++ b/x-pack/plugins/maps/public/components/metrics_editor/metric_editor.tsx @@ -4,18 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; +import React, { ChangeEvent, Fragment } from 'react'; import { i18n } from '@kbn/i18n'; -import { EuiFieldText, EuiFormRow } from '@elastic/eui'; +import { EuiButtonEmpty, EuiComboBoxOptionOption, EuiFieldText, EuiFormRow } from '@elastic/eui'; -import { MetricSelect, METRIC_AGGREGATION_VALUES } from './metric_select'; -import { SingleFieldSelect } from './single_field_select'; -import { AGG_TYPE } from '../../common/constants'; -import { getTermsFields } from '../index_pattern_util'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { MetricSelect } from './metric_select'; +import { SingleFieldSelect } from '../single_field_select'; +import { AggDescriptor } from '../../../common/descriptor_types'; +import { AGG_TYPE } from '../../../common/constants'; +import { getTermsFields } from '../../index_pattern_util'; +import { IFieldType } from '../../../../../../src/plugins/data/public'; -function filterFieldsForAgg(fields, aggType) { +function filterFieldsForAgg(fields: IFieldType[], aggType: AGG_TYPE) { if (!fields) { return []; } @@ -34,8 +36,27 @@ function filterFieldsForAgg(fields, aggType) { }); } -export function MetricEditor({ fields, metricsFilter, metric, onChange, removeButton }) { - const onAggChange = (metricAggregationType) => { +interface Props { + metric: AggDescriptor; + fields: IFieldType[]; + onChange: (metric: AggDescriptor) => void; + onRemove: () => void; + metricsFilter?: (metricOption: EuiComboBoxOptionOption) => boolean; + showRemoveButton: boolean; +} + +export function MetricEditor({ + fields, + metricsFilter, + metric, + onChange, + showRemoveButton, + onRemove, +}: Props) { + const onAggChange = (metricAggregationType?: AGG_TYPE) => { + if (!metricAggregationType) { + return; + } const newMetricProps = { ...metric, type: metricAggregationType, @@ -54,13 +75,16 @@ export function MetricEditor({ fields, metricsFilter, metric, onChange, removeBu onChange(newMetricProps); }; - const onFieldChange = (fieldName) => { + const onFieldChange = (fieldName?: string) => { + if (!fieldName) { + return; + } onChange({ ...metric, field: fieldName, }); }; - const onLabelChange = (e) => { + const onLabelChange = (e: ChangeEvent) => { onChange({ ...metric, label: e.target.value, @@ -80,7 +104,7 @@ export function MetricEditor({ fields, metricsFilter, metric, onChange, removeBu placeholder={i18n.translate('xpack.maps.metricsEditor.selectFieldPlaceholder', { defaultMessage: 'Select field', })} - value={metric.field} + value={metric.field ? metric.field : null} onChange={onFieldChange} fields={filterFieldsForAgg(fields, metric.type)} isClearable={false} @@ -108,6 +132,28 @@ export function MetricEditor({ fields, metricsFilter, metric, onChange, removeBu ); } + let removeButton; + if (showRemoveButton) { + removeButton = ( +
+ + + +
+ ); + } + return ( ); } - -MetricEditor.propTypes = { - metric: PropTypes.shape({ - type: PropTypes.oneOf(METRIC_AGGREGATION_VALUES), - field: PropTypes.string, - label: PropTypes.string, - }), - fields: PropTypes.array, - onChange: PropTypes.func.isRequired, - metricsFilter: PropTypes.func, -}; diff --git a/x-pack/plugins/maps/public/components/metric_select.js b/x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx similarity index 80% rename from x-pack/plugins/maps/public/components/metric_select.js rename to x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx index 2ebfcf99dece6..197c5466fe0fd 100644 --- a/x-pack/plugins/maps/public/components/metric_select.js +++ b/x-pack/plugins/maps/public/components/metrics_editor/metric_select.tsx @@ -5,10 +5,9 @@ */ import React from 'react'; -import PropTypes from 'prop-types'; import { i18n } from '@kbn/i18n'; -import { EuiComboBox } from '@elastic/eui'; -import { AGG_TYPE } from '../../common/constants'; +import { EuiComboBox, EuiComboBoxOptionOption, EuiComboBoxProps } from '@elastic/eui'; +import { AGG_TYPE } from '../../../common/constants'; const AGG_OPTIONS = [ { @@ -55,17 +54,19 @@ const AGG_OPTIONS = [ }, ]; -export const METRIC_AGGREGATION_VALUES = AGG_OPTIONS.map(({ value }) => { - return value; -}); +type Props = Omit, 'onChange'> & { + value: AGG_TYPE; + onChange: (aggType: AGG_TYPE) => void; + metricsFilter?: (metricOption: EuiComboBoxOptionOption) => boolean; +}; -export function MetricSelect({ value, onChange, metricsFilter, ...rest }) { - function onAggChange(selectedOptions) { +export function MetricSelect({ value, onChange, metricsFilter, ...rest }: Props) { + function onAggChange(selectedOptions: Array>) { if (selectedOptions.length === 0) { return; } - const aggType = selectedOptions[0].value; + const aggType = selectedOptions[0].value!; onChange(aggType); } @@ -87,9 +88,3 @@ export function MetricSelect({ value, onChange, metricsFilter, ...rest }) { /> ); } - -MetricSelect.propTypes = { - metricsFilter: PropTypes.func, - value: PropTypes.oneOf(METRIC_AGGREGATION_VALUES), - onChange: PropTypes.func.isRequired, -}; diff --git a/x-pack/plugins/maps/public/components/metrics_editor.test.js b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx similarity index 84% rename from x-pack/plugins/maps/public/components/metrics_editor.test.js rename to x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx index bcbeef29875ee..7ce7fbce2b066 100644 --- a/x-pack/plugins/maps/public/components/metrics_editor.test.js +++ b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { shallow } from 'enzyme'; import { MetricsEditor } from './metrics_editor'; -import { AGG_TYPE } from '../../common/constants'; +import { AGG_TYPE } from '../../../common/constants'; const defaultProps = { metrics: [ @@ -19,15 +19,14 @@ const defaultProps = { fields: [], onChange: () => {}, allowMultipleMetrics: true, - metricsFilter: () => {}, }; -test('should render metrics editor', async () => { +test('should render metrics editor', () => { const component = shallow(); expect(component).toMatchSnapshot(); }); -test('should add default count metric when metrics is empty array', async () => { +test('should add default count metric when metrics is empty array', () => { const component = shallow(); expect(component).toMatchSnapshot(); }); diff --git a/x-pack/plugins/maps/public/components/metrics_editor.js b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx similarity index 54% rename from x-pack/plugins/maps/public/components/metrics_editor.js rename to x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx index 7d4d7bf3ec7ab..dae1f51469281 100644 --- a/x-pack/plugins/maps/public/components/metrics_editor.js +++ b/x-pack/plugins/maps/public/components/metrics_editor/metrics_editor.tsx @@ -5,48 +5,42 @@ */ import React, { Fragment } from 'react'; -import PropTypes from 'prop-types'; -import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; -import { EuiButtonEmpty, EuiSpacer, EuiTextAlign } from '@elastic/eui'; +import { EuiButtonEmpty, EuiComboBoxOptionOption, EuiSpacer, EuiTextAlign } from '@elastic/eui'; import { MetricEditor } from './metric_editor'; -import { DEFAULT_METRIC } from '../classes/sources/es_agg_source'; +import { DEFAULT_METRIC } from '../../classes/sources/es_agg_source'; +import { IFieldType } from '../../../../../../src/plugins/data/public'; +import { AggDescriptor } from '../../../common/descriptor_types'; +import { AGG_TYPE } from '../../../common/constants'; -export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics, metricsFilter }) { +interface Props { + allowMultipleMetrics: boolean; + metrics: AggDescriptor[]; + fields: IFieldType[]; + onChange: (metrics: AggDescriptor[]) => void; + metricsFilter?: (metricOption: EuiComboBoxOptionOption) => boolean; +} + +export function MetricsEditor({ + fields, + metrics = [DEFAULT_METRIC], + onChange, + allowMultipleMetrics = true, + metricsFilter, +}: Props) { function renderMetrics() { // There was a bug in 7.8 that initialized metrics to []. // This check is needed to handle any saved objects created before the bug was patched. const nonEmptyMetrics = metrics.length === 0 ? [DEFAULT_METRIC] : metrics; return nonEmptyMetrics.map((metric, index) => { - const onMetricChange = (metric) => { - onChange([...metrics.slice(0, index), metric, ...metrics.slice(index + 1)]); + const onMetricChange = (updatedMetric: AggDescriptor) => { + onChange([...metrics.slice(0, index), updatedMetric, ...metrics.slice(index + 1)]); }; const onRemove = () => { onChange([...metrics.slice(0, index), ...metrics.slice(index + 1)]); }; - let removeButton; - if (index > 0) { - removeButton = ( -
- - - -
- ); - } return (
0} + onRemove={onRemove} />
); @@ -62,7 +57,7 @@ export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics, } function addMetric() { - onChange([...metrics, {}]); + onChange([...metrics, { type: AGG_TYPE.AVG }]); } function renderAddMetricButton() { @@ -71,7 +66,7 @@ export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics, } return ( - <> + @@ -81,7 +76,7 @@ export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics, /> - + ); } @@ -93,16 +88,3 @@ export function MetricsEditor({ fields, metrics, onChange, allowMultipleMetrics,
); } - -MetricsEditor.propTypes = { - metrics: PropTypes.array, - fields: PropTypes.array, - onChange: PropTypes.func.isRequired, - allowMultipleMetrics: PropTypes.bool, - metricsFilter: PropTypes.func, -}; - -MetricsEditor.defaultProps = { - metrics: [DEFAULT_METRIC], - allowMultipleMetrics: true, -}; diff --git a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js index 3cd8a3c42879a..e0e1556ecde06 100644 --- a/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js +++ b/x-pack/plugins/maps/public/connected_components/layer_panel/join_editor/resources/metrics_expression.test.js @@ -4,12 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -jest.mock('../../../../components/metric_editor', () => ({ - MetricsEditor: () => { - return
mockMetricsEditor
; - }, -})); - import React from 'react'; import { shallow } from 'enzyme'; import { MetricsExpression } from './metrics_expression'; diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts index 5f2a640aa9d0f..03752a1c3e11e 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/index.ts @@ -7,7 +7,7 @@ import { AnyAction } from 'redux'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { IndexPatternsContract } from 'src/plugins/data/public/index_patterns'; -import { ReactElement } from 'react'; +import { AppMountContext, AppMountParameters } from 'kibana/public'; import { IndexPattern } from 'src/plugins/data/public'; import { Embeddable, IContainer } from '../../../../../src/plugins/embeddable/public'; import { LayerDescriptor } from '../../common/descriptor_types'; @@ -40,7 +40,7 @@ interface LazyLoadedMapModules { initialLayers?: LayerDescriptor[] ) => LayerDescriptor[]; mergeInputWithSavedMap: any; - renderApp: (context: unknown, params: unknown) => ReactElement; + renderApp: (context: AppMountContext, params: AppMountParameters) => Promise<() => void>; createSecurityLayerDescriptors: ( indexPatternId: string, indexPatternTitle: string @@ -57,7 +57,6 @@ export async function lazyLoadMapModules(): Promise { loadModulesPromise = new Promise(async (resolve) => { const { - // @ts-expect-error getMapsSavedObjectLoader, getQueryableUniqueIndexPatternIds, MapEmbeddable, @@ -68,7 +67,6 @@ export async function lazyLoadMapModules(): Promise { addLayerWithoutDataSync, getInitialLayers, mergeInputWithSavedMap, - // @ts-expect-error renderApp, createSecurityLayerDescriptors, registerLayerWizard, diff --git a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts index e55160383a8f3..28f5acdc17656 100644 --- a/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts +++ b/x-pack/plugins/maps/public/lazy_load_bundle/lazy/index.ts @@ -7,7 +7,6 @@ // These are map-dependencies of the embeddable. // By lazy-loading these, the Maps-app can register the embeddable when the plugin mounts, without actually pulling all the code. -// @ts-expect-error export * from '../../routing/bootstrap/services/gis_map_saved_object_loader'; export * from '../../embeddable/map_embeddable'; export * from '../../kibana_services'; @@ -16,7 +15,6 @@ export * from '../../actions'; export * from '../../selectors/map_selectors'; export * from '../../routing/bootstrap/get_initial_layers'; export * from '../../embeddable/merge_input_with_saved_map'; -// @ts-expect-error export * from '../../routing/maps_router'; export * from '../../classes/layers/solution_layers/security'; export { registerLayerWizard } from '../../classes/layers/layer_wizard_registry'; diff --git a/x-pack/plugins/maps/public/plugin.ts b/x-pack/plugins/maps/public/plugin.ts index b08135b4e486c..00ee7f376efc6 100644 --- a/x-pack/plugins/maps/public/plugin.ts +++ b/x-pack/plugins/maps/public/plugin.ts @@ -123,7 +123,6 @@ export class MapsPlugin icon: `plugins/${APP_ID}/icon.svg`, euiIconType: APP_ICON, category: DEFAULT_APP_CATEGORIES.kibana, - // @ts-expect-error async mount(context, params) { const { renderApp } = await lazyLoadMapModules(); return renderApp(context, params); diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.d.ts b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.d.ts deleted file mode 100644 index a23e715a08295..0000000000000 --- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { LayerDescriptor } from '../../../common/descriptor_types'; - -export function getInitialLayers( - layerListJSON?: string, - initialLayers?: LayerDescriptor[] -): LayerDescriptor[]; diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.ts similarity index 87% rename from x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.ts index b47f83d5a6664..e828dc88409cb 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_layers.ts @@ -15,15 +15,19 @@ import '../../classes/sources/es_pew_pew_source'; import '../../classes/sources/kibana_regionmap_source'; import '../../classes/sources/es_geo_grid_source'; import '../../classes/sources/xyz_tms_source'; +import { LayerDescriptor } from '../../../common/descriptor_types'; +// @ts-expect-error import { KibanaTilemapSource } from '../../classes/sources/kibana_tilemap_source'; import { TileLayer } from '../../classes/layers/tile_layer/tile_layer'; +// @ts-expect-error import { EMSTMSSource } from '../../classes/sources/ems_tms_source'; +// @ts-expect-error import { VectorTileLayer } from '../../classes/layers/vector_tile_layer/vector_tile_layer'; import { getIsEmsEnabled, getToasts } from '../../kibana_services'; import { INITIAL_LAYERS_KEY } from '../../../common/constants'; import { getKibanaTileMap } from '../../meta'; -export function getInitialLayers(layerListJSON, initialLayers = []) { +export function getInitialLayers(layerListJSON?: string, initialLayers: LayerDescriptor[] = []) { if (layerListJSON) { return JSON.parse(layerListJSON); } @@ -58,9 +62,10 @@ export function getInitialLayersFromUrlParam() { try { let mapInitLayers = mapAppParams.get(INITIAL_LAYERS_KEY); - if (mapInitLayers[mapInitLayers.length - 1] === '#') { - mapInitLayers = mapInitLayers.substr(0, mapInitLayers.length - 1); + if (mapInitLayers![mapInitLayers!.length - 1] === '#') { + mapInitLayers = mapInitLayers!.substr(0, mapInitLayers!.length - 1); } + // @ts-ignore return rison.decode_array(mapInitLayers); } catch (e) { getToasts().addWarning({ diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.ts similarity index 73% rename from x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.js rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.ts index 1f2cf27077623..43293d152dbff 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_query.ts @@ -5,8 +5,15 @@ */ import { getData } from '../../kibana_services'; +import { MapsAppState } from '../state_syncing/app_state_manager'; -export function getInitialQuery({ mapStateJSON, appState = {} }) { +export function getInitialQuery({ + mapStateJSON, + appState = {}, +}: { + mapStateJSON?: string; + appState: MapsAppState; +}) { if (appState.query) { return appState.query; } diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.ts similarity index 81% rename from x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.js rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.ts index d7b3bbf5b4ab2..7d759cb25052f 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_refresh_config.ts @@ -4,10 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ +import { QueryState } from 'src/plugins/data/public'; import { getUiSettings } from '../../kibana_services'; import { UI_SETTINGS } from '../../../../../../src/plugins/data/public'; -export function getInitialRefreshConfig({ mapStateJSON, globalState = {} }) { +export function getInitialRefreshConfig({ + mapStateJSON, + globalState = {}, +}: { + mapStateJSON?: string; + globalState: QueryState; +}) { const uiSettings = getUiSettings(); if (mapStateJSON) { diff --git a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.js b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.ts similarity index 75% rename from x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.js rename to x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.ts index 9c11dabe03923..549cc154fe487 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.js +++ b/x-pack/plugins/maps/public/routing/bootstrap/get_initial_time_filters.ts @@ -4,9 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import { QueryState } from 'src/plugins/data/public'; import { getUiSettings } from '../../kibana_services'; -export function getInitialTimeFilters({ mapStateJSON, globalState }) { +export function getInitialTimeFilters({ + mapStateJSON, + globalState, +}: { + mapStateJSON?: string; + globalState: QueryState; +}) { if (mapStateJSON) { const mapState = JSON.parse(mapStateJSON); if (mapState.timeFilters) { diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js b/x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.ts similarity index 100% rename from x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.js rename to x-pack/plugins/maps/public/routing/bootstrap/services/gis_map_saved_object_loader.ts diff --git a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts index 6f8e7777f671b..511f015b0ff80 100644 --- a/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts +++ b/x-pack/plugins/maps/public/routing/bootstrap/services/saved_gis_map.ts @@ -27,7 +27,6 @@ import { copyPersistentState } from '../../../reducers/util'; // @ts-expect-error import { extractReferences, injectReferences } from '../../../../common/migrations/references'; import { getExistingMapPath, MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; -// @ts-expect-error import { getStore } from '../../store_operations'; import { MapStoreState } from '../../../reducers/store'; import { LayerDescriptor } from '../../../../common/descriptor_types'; diff --git a/x-pack/plugins/maps/public/routing/maps_router.js b/x-pack/plugins/maps/public/routing/maps_router.tsx similarity index 80% rename from x-pack/plugins/maps/public/routing/maps_router.js rename to x-pack/plugins/maps/public/routing/maps_router.tsx index f0f5234e3f989..5291d9c361161 100644 --- a/x-pack/plugins/maps/public/routing/maps_router.js +++ b/x-pack/plugins/maps/public/routing/maps_router.tsx @@ -6,8 +6,10 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; -import { Router, Switch, Route, Redirect } from 'react-router-dom'; +import { Router, Switch, Route, Redirect, RouteComponentProps } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; +import { Provider } from 'react-redux'; +import { AppMountContext, AppMountParameters } from 'kibana/public'; import { getCoreChrome, getCoreI18n, @@ -18,16 +20,19 @@ import { import { createKbnUrlStateStorage, withNotifyOnErrors, + IKbnUrlStateStorage, } from '../../../../../src/plugins/kibana_utils/public'; import { getStore } from './store_operations'; -import { Provider } from 'react-redux'; import { LoadListAndRender } from './routes/list/load_list_and_render'; import { LoadMapAndRender } from './routes/maps_app/load_map_and_render'; -export let goToSpecifiedPath; -export let kbnUrlStateStorage; +export let goToSpecifiedPath: (path: string) => void; +export let kbnUrlStateStorage: IKbnUrlStateStorage; -export async function renderApp(context, { appBasePath, element, history, onAppLeave }) { +export async function renderApp( + context: AppMountContext, + { appBasePath, element, history, onAppLeave }: AppMountParameters +) { goToSpecifiedPath = (path) => history.push(path); kbnUrlStateStorage = createKbnUrlStateStorage({ useHash: false, @@ -42,11 +47,19 @@ export async function renderApp(context, { appBasePath, element, history, onAppL }; } -const App = ({ history, appBasePath, onAppLeave }) => { +interface Props { + history: AppMountParameters['history'] | RouteComponentProps['history']; + appBasePath: AppMountParameters['appBasePath']; + onAppLeave: AppMountParameters['onAppLeave']; +} + +const App: React.FC = ({ history, appBasePath, onAppLeave }) => { const store = getStore(); const I18nContext = getCoreI18n().Context; - const stateTransfer = getEmbeddableService()?.getStateTransfer(history); + const stateTransfer = getEmbeddableService()?.getStateTransfer( + history as AppMountParameters['history'] + ); const { originatingApp } = stateTransfer?.getIncomingEditorState({ keysToRemoveAfterFetch: ['originatingApp'] }) || {}; @@ -66,7 +79,7 @@ const App = ({ history, appBasePath, onAppLeave }) => { return ( - + getMapsSavedObjectLoader().find(search, this.state.listingLimit); + _find = (search: string) => getMapsSavedObjectLoader().find(search, this.state.listingLimit); - _delete = (ids) => getMapsSavedObjectLoader().delete(ids); + _delete = (ids: string[]) => getMapsSavedObjectLoader().delete(ids); debouncedFetch = _.debounce(async (filter) => { const response = await this._find(filter); @@ -135,10 +163,10 @@ export class MapsListView extends React.Component { this.setState({ showDeleteModal: true }); }; - onTableChange = ({ page, sort = {} }) => { + onTableChange = ({ page, sort }: CriteriaWithPagination) => { const { index: pageIndex, size: pageSize } = page; - let { field: sortField, direction: sortDirection } = sort; + let { field: sortField, direction: sortDirection } = sort || {}; // 3rd sorting state that is not captured by sort - native order (no sort) // when switching from desc to asc for the same field - use native order @@ -147,8 +175,8 @@ export class MapsListView extends React.Component { this.state.sortDirection === 'desc' && sortDirection === 'asc' ) { - sortField = null; - sortDirection = null; + sortField = undefined; + sortDirection = undefined; } this.setState({ @@ -165,8 +193,8 @@ export class MapsListView extends React.Component { if (this.state.sortField) { itemsCopy.sort((a, b) => { - const fieldA = _.get(a, this.state.sortField, ''); - const fieldB = _.get(b, this.state.sortField, ''); + const fieldA = _.get(a, this.state.sortField!, ''); + const fieldB = _.get(b, this.state.sortField!, ''); let order = 1; if (this.state.sortDirection === 'desc') { order = -1; @@ -320,7 +348,7 @@ export class MapsListView extends React.Component { } renderTable() { - const tableColumns = [ + const tableColumns: Array> = [ { field: 'title', name: i18n.translate('xpack.maps.mapListing.titleFieldTitle', { @@ -329,7 +357,7 @@ export class MapsListView extends React.Component { sortable: true, render: (field, record) => ( { + onClick={(e: MouseEvent) => { e.preventDefault(); goToSpecifiedPath(`/map/${record.id}`); }} @@ -355,12 +383,12 @@ export class MapsListView extends React.Component { pageSizeOptions: [10, 20, 50], }; - let selection = false; + let selection; if (!this.state.readOnly) { selection = { - onSelectionChange: (selection) => { + onSelectionChange: (s: SelectionItem[]) => { this.setState({ - selectedIds: selection.map((item) => { + selectedIds: s.map((item) => { return item.id; }), }); @@ -368,11 +396,11 @@ export class MapsListView extends React.Component { }; } - const sorting = {}; + const sorting: EuiTableSortingType = {}; if (this.state.sortField) { sorting.sort = { field: this.state.sortField, - direction: this.state.sortDirection, + direction: this.state.sortDirection!, }; } const items = this.state.items.length === 0 ? [] : this.getPageOfItems(); diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx index 1ccf890597edc..149c04b414c18 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/get_breadcrumbs.tsx @@ -6,7 +6,6 @@ import { i18n } from '@kbn/i18n'; import { getNavigateToApp } from '../../../kibana_services'; -// @ts-expect-error import { goToSpecifiedPath } from '../../maps_router'; export const unsavedChangesWarning = i18n.translate( @@ -25,7 +24,7 @@ export function getBreadcrumbs({ title: string; getHasUnsavedChanges: () => boolean; originatingApp?: string; - getAppNameFromId?: (id: string) => string; + getAppNameFromId?: (id: string) => string | undefined; }) { const breadcrumbs = []; if (originatingApp && getAppNameFromId) { diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js b/x-pack/plugins/maps/public/routing/routes/maps_app/index.ts similarity index 62% rename from x-pack/plugins/maps/public/routing/routes/maps_app/index.js rename to x-pack/plugins/maps/public/routing/routes/maps_app/index.ts index 326db7289e60d..812d7fcf30981 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/index.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/index.ts @@ -5,6 +5,9 @@ */ import { connect } from 'react-redux'; +import { ThunkDispatch } from 'redux-thunk'; +import { AnyAction } from 'redux'; +import { Filter, Query, TimeRange } from 'src/plugins/data/public'; import { MapsAppView } from './maps_app_view'; import { getFlyoutDisplay, getIsFullScreen } from '../../../selectors/ui_selectors'; import { @@ -33,8 +36,15 @@ import { import { FLYOUT_STATE } from '../../../reducers/ui'; import { getMapsCapabilities } from '../../../kibana_services'; import { getInspectorAdapters } from '../../../reducers/non_serializable_instances'; +import { MapStoreState } from '../../../reducers/store'; +import { + MapRefreshConfig, + MapCenterAndZoom, + LayerDescriptor, +} from '../../../../common/descriptor_types'; +import { MapSettings } from '../../../reducers/map'; -function mapStateToProps(state = {}) { +function mapStateToProps(state: MapStoreState) { return { isFullScreen: getIsFullScreen(state), isOpenSettingsDisabled: getFlyoutDisplay(state) !== FLYOUT_STATE.NONE, @@ -50,9 +60,19 @@ function mapStateToProps(state = {}) { }; } -function mapDispatchToProps(dispatch) { +function mapDispatchToProps(dispatch: ThunkDispatch) { return { - dispatchSetQuery: ({ forceRefresh, filters, query, timeFilters }) => { + dispatchSetQuery: ({ + forceRefresh, + filters, + query, + timeFilters, + }: { + filters?: Filter[]; + query?: Query; + timeFilters?: TimeRange; + forceRefresh?: boolean; + }) => { dispatch( setQuery({ filters, @@ -62,12 +82,13 @@ function mapDispatchToProps(dispatch) { }) ); }, - setRefreshConfig: (refreshConfig) => dispatch(setRefreshConfig(refreshConfig)), - replaceLayerList: (layerList) => dispatch(replaceLayerList(layerList)), - setGotoWithCenter: (latLonZoom) => dispatch(setGotoWithCenter(latLonZoom)), - setMapSettings: (mapSettings) => dispatch(setMapSettings(mapSettings)), - setIsLayerTOCOpen: (isLayerTOCOpen) => dispatch(setIsLayerTOCOpen(isLayerTOCOpen)), - setOpenTOCDetails: (openTOCDetails) => dispatch(setOpenTOCDetails(openTOCDetails)), + setRefreshConfig: (refreshConfig: MapRefreshConfig) => + dispatch(setRefreshConfig(refreshConfig)), + replaceLayerList: (layerList: LayerDescriptor[]) => dispatch(replaceLayerList(layerList)), + setGotoWithCenter: (latLonZoom: MapCenterAndZoom) => dispatch(setGotoWithCenter(latLonZoom)), + setMapSettings: (mapSettings: MapSettings) => dispatch(setMapSettings(mapSettings)), + setIsLayerTOCOpen: (isLayerTOCOpen: boolean) => dispatch(setIsLayerTOCOpen(isLayerTOCOpen)), + setOpenTOCDetails: (openTOCDetails: string[]) => dispatch(setOpenTOCDetails(openTOCDetails)), clearUi: () => { dispatch(setSelectedLayer(null)); dispatch(updateFlyout(FLYOUT_STATE.NONE)); diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx similarity index 75% rename from x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js rename to x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx index eebbb17582821..7ab138300dc4c 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/load_map_and_render.tsx @@ -5,15 +5,31 @@ */ import React from 'react'; -import { MapsAppView } from '.'; -import { getMapsSavedObjectLoader } from '../../bootstrap/services/gis_map_saved_object_loader'; -import { getCoreChrome, getToasts } from '../../../kibana_services'; import { i18n } from '@kbn/i18n'; import { Redirect } from 'react-router-dom'; +import { AppMountParameters } from 'kibana/public'; +import { EmbeddableStateTransfer } from 'src/plugins/embeddable/public'; +import { getCoreChrome, getToasts } from '../../../kibana_services'; +import { getMapsSavedObjectLoader } from '../../bootstrap/services/gis_map_saved_object_loader'; +import { MapsAppView } from '.'; +import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map'; + +interface Props { + savedMapId?: string; + onAppLeave: AppMountParameters['onAppLeave']; + stateTransfer: EmbeddableStateTransfer; + originatingApp?: string; +} + +interface State { + savedMap?: ISavedGisMap; + failedToLoad: boolean; +} -export const LoadMapAndRender = class extends React.Component { - state = { - savedMap: null, +export const LoadMapAndRender = class extends React.Component { + _isMounted: boolean = false; + state: State = { + savedMap: undefined, failedToLoad: false, }; diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx similarity index 73% rename from x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js rename to x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx index 485b0ed7682fa..b3377547b2dd1 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.js +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/maps_app_view.tsx @@ -7,6 +7,9 @@ import React from 'react'; import 'mapbox-gl/dist/mapbox-gl.css'; import _ from 'lodash'; +import { AppLeaveAction, AppMountParameters } from 'kibana/public'; +import { EmbeddableStateTransfer, Adapters } from 'src/plugins/embeddable/public'; +import { Subscription } from 'rxjs'; import { DEFAULT_IS_LAYER_TOC_OPEN } from '../../../reducers/ui'; import { getData, @@ -23,29 +26,91 @@ import { getGlobalState, updateGlobalState, startGlobalStateSyncing, + MapsGlobalState, } from '../../state_syncing/global_sync'; import { AppStateManager } from '../../state_syncing/app_state_manager'; import { startAppStateSyncing } from '../../state_syncing/app_sync'; -import { esFilters } from '../../../../../../../src/plugins/data/public'; +import { + esFilters, + Filter, + Query, + TimeRange, + IndexPattern, + SavedQuery, + QueryStateChange, + QueryState, +} from '../../../../../../../src/plugins/data/public'; import { MapContainer } from '../../../connected_components/map_container'; import { getIndexPatternsFromIds } from '../../../index_pattern_util'; import { getTopNavConfig } from './top_nav_config'; import { getBreadcrumbs, unsavedChangesWarning } from './get_breadcrumbs'; +import { + LayerDescriptor, + MapRefreshConfig, + MapCenterAndZoom, + MapQuery, +} from '../../../../common/descriptor_types'; +import { MapSettings } from '../../../reducers/map'; +import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map'; + +interface Props { + savedMap: ISavedGisMap; + onAppLeave: AppMountParameters['onAppLeave']; + stateTransfer: EmbeddableStateTransfer; + originatingApp?: string; + layerListConfigOnly: LayerDescriptor[]; + replaceLayerList: (layerList: LayerDescriptor[]) => void; + filters: Filter[]; + isFullScreen: boolean; + isOpenSettingsDisabled: boolean; + enableFullScreen: () => void; + openMapSettings: () => void; + inspectorAdapters: Adapters; + nextIndexPatternIds: string[]; + dispatchSetQuery: ({ + forceRefresh, + filters, + query, + timeFilters, + }: { + filters?: Filter[]; + query?: Query; + timeFilters?: TimeRange; + forceRefresh?: boolean; + }) => void; + timeFilters: TimeRange; + refreshConfig: MapRefreshConfig; + setRefreshConfig: (refreshConfig: MapRefreshConfig) => void; + isSaveDisabled: boolean; + clearUi: () => void; + setGotoWithCenter: (latLonZoom: MapCenterAndZoom) => void; + setMapSettings: (mapSettings: MapSettings) => void; + setIsLayerTOCOpen: (isLayerTOCOpen: boolean) => void; + setOpenTOCDetails: (openTOCDetails: string[]) => void; + query: MapQuery | undefined; +} -export class MapsAppView extends React.Component { - _globalSyncUnsubscribe = null; - _globalSyncChangeMonitorSubscription = null; - _appSyncUnsubscribe = null; +interface State { + initialized: boolean; + initialLayerListConfig?: LayerDescriptor[]; + indexPatterns: IndexPattern[]; + savedQuery?: SavedQuery; + originatingApp?: string; +} + +export class MapsAppView extends React.Component { + _globalSyncUnsubscribe: (() => void) | null = null; + _globalSyncChangeMonitorSubscription: Subscription | null = null; + _appSyncUnsubscribe: (() => void) | null = null; _appStateManager = new AppStateManager(); - _prevIndexPatternIds = null; + _prevIndexPatternIds: string[] | null = null; + _isMounted: boolean = false; - constructor(props) { + constructor(props: Props) { super(props); this.state = { indexPatterns: [], initialized: false, - savedQuery: '', - initialLayerListConfig: null, // tracking originatingApp in state so the connection can be broken by users originatingApp: props.originatingApp, }; @@ -60,10 +125,11 @@ export class MapsAppView extends React.Component { this._updateFromGlobalState ); - const initialSavedQuery = this._appStateManager.getAppState().savedQuery; - if (initialSavedQuery) { - this._updateStateFromSavedQuery(initialSavedQuery); - } + // savedQuery must be fetched from savedQueryId + // const initialSavedQuery = this._appStateManager.getAppState().savedQuery; + // if (initialSavedQuery) { + // this._updateStateFromSavedQuery(initialSavedQuery as SavedQuery); + // } this._initMap(); @@ -72,10 +138,10 @@ export class MapsAppView extends React.Component { this.props.onAppLeave((actions) => { if (this._hasUnsavedChanges()) { if (!window.confirm(unsavedChangesWarning)) { - return; + return {} as AppLeaveAction; } } - return actions.default(); + return actions.default() as AppLeaveAction; }); } @@ -121,7 +187,13 @@ export class MapsAppView extends React.Component { getCoreChrome().setBreadcrumbs(breadcrumbs); }; - _updateFromGlobalState = ({ changes, state: globalState }) => { + _updateFromGlobalState = ({ + changes, + state: globalState, + }: { + changes: QueryStateChange; + state: QueryState; + }) => { if (!this.state.initialized || !changes || !globalState) { return; } @@ -144,7 +216,17 @@ export class MapsAppView extends React.Component { } } - _onQueryChange = ({ filters, query, time, forceRefresh = false }) => { + _onQueryChange = ({ + filters, + query, + time, + forceRefresh = false, + }: { + filters?: Filter[]; + query?: Query; + time?: TimeRange; + forceRefresh?: boolean; + }) => { const { filterManager } = getData().query; if (filters) { @@ -165,7 +247,9 @@ export class MapsAppView extends React.Component { }); // sync globalState - const updatedGlobalState = { filters: filterManager.getGlobalFilters() }; + const updatedGlobalState: MapsGlobalState = { + filters: filterManager.getGlobalFilters(), + }; if (time) { updatedGlobalState.time = time; } @@ -173,7 +257,7 @@ export class MapsAppView extends React.Component { }; _initMapAndLayerSettings() { - const globalState = getGlobalState(); + const globalState: MapsGlobalState = getGlobalState(); const mapStateJSON = this.props.savedMap.mapStateJSON; let savedObjectFilters = []; @@ -219,14 +303,14 @@ export class MapsAppView extends React.Component { }); } - _onFiltersChange = (filters) => { + _onFiltersChange = (filters: Filter[]) => { this._onQueryChange({ filters, }); }; // mapRefreshConfig: MapRefreshConfig - _onRefreshConfigChange(mapRefreshConfig) { + _onRefreshConfigChange(mapRefreshConfig: MapRefreshConfig) { this.props.setRefreshConfig(mapRefreshConfig); updateGlobalState( { @@ -239,9 +323,9 @@ export class MapsAppView extends React.Component { ); } - _updateStateFromSavedQuery = (savedQuery) => { + _updateStateFromSavedQuery = (savedQuery: SavedQuery) => { this.setState({ savedQuery: { ...savedQuery } }); - this._appStateManager.setQueryAndFilters({ savedQuery }); + this._appStateManager.setQueryAndFilters({ savedQueryId: savedQuery.id }); const { filterManager } = getData().query; const savedQueryFilters = savedQuery.attributes.filters || []; @@ -328,7 +412,13 @@ export class MapsAppView extends React.Component { dateRangeTo={this.props.timeFilters.to} isRefreshPaused={this.props.refreshConfig.isPaused} refreshInterval={this.props.refreshConfig.interval} - onRefreshChange={({ isPaused, refreshInterval }) => { + onRefreshChange={({ + isPaused, + refreshInterval, + }: { + isPaused: boolean; + refreshInterval: number; + }) => { this._onRefreshConfigChange({ isPaused, interval: refreshInterval, @@ -337,14 +427,14 @@ export class MapsAppView extends React.Component { showSearchBar={true} showFilterBar={true} showDatePicker={true} - showSaveQuery={getMapsCapabilities().saveQuery} + showSaveQuery={!!getMapsCapabilities().saveQuery} savedQuery={this.state.savedQuery} onSaved={this._updateStateFromSavedQuery} onSavedQueryUpdated={this._updateStateFromSavedQuery} onClearSavedQuery={() => { const { filterManager, queryString } = getData().query; - this.setState({ savedQuery: '' }); - this._appStateManager.setQueryAndFilters({ savedQuery: '' }); + this.setState({ savedQuery: undefined }); + this._appStateManager.setQueryAndFilters({ savedQueryId: '' }); this._onQueryChange({ filters: filterManager.getGlobalFilters(), query: queryString.getDefaultQuery(), @@ -354,7 +444,7 @@ export class MapsAppView extends React.Component { ); } - _addFilter = (newFilters) => { + _addFilter = async (newFilters: Filter[]) => { newFilters.forEach((filter) => { filter.$state = { store: esFilters.FilterStateStore.APP_STATE }; }); diff --git a/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx index 497c87ad533a6..47f41f2b76f3e 100644 --- a/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx +++ b/x-pack/plugins/maps/public/routing/routes/maps_app/top_nav_config.tsx @@ -21,7 +21,6 @@ import { showSaveModal, } from '../../../../../../../src/plugins/saved_objects/public'; import { MAP_SAVED_OBJECT_TYPE } from '../../../../common/constants'; -// @ts-expect-error import { goToSpecifiedPath } from '../../maps_router'; import { ISavedGisMap } from '../../bootstrap/services/saved_gis_map'; import { EmbeddableStateTransfer } from '../../../../../../../src/plugins/embeddable/public'; diff --git a/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js b/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.ts similarity index 58% rename from x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js rename to x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.ts index 4cdba13bd85d2..122b50f823a95 100644 --- a/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.js +++ b/x-pack/plugins/maps/public/routing/state_syncing/app_state_manager.ts @@ -5,20 +5,27 @@ */ import { Subject } from 'rxjs'; +import { Filter, Query } from 'src/plugins/data/public'; + +export interface MapsAppState { + query?: Query | null; + savedQueryId?: string; + filters?: Filter[]; +} export class AppStateManager { - _query = ''; - _savedQuery = ''; - _filters = []; + _query: Query | null = null; + _savedQueryId: string = ''; + _filters: Filter[] = []; _updated$ = new Subject(); - setQueryAndFilters({ query, savedQuery, filters }) { + setQueryAndFilters({ query, savedQueryId, filters }: MapsAppState) { if (query && this._query !== query) { this._query = query; } - if (savedQuery && this._savedQuery !== savedQuery) { - this._savedQuery = savedQuery; + if (savedQueryId && this._savedQueryId !== savedQueryId) { + this._savedQueryId = savedQueryId; } if (filters && this._filters !== filters) { this._filters = filters; @@ -34,10 +41,10 @@ export class AppStateManager { return this._filters; } - getAppState() { + getAppState(): MapsAppState { return { query: this._query, - savedQuery: this._savedQuery, + savedQueryId: this._savedQueryId, filters: this._filters, }; } diff --git a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts similarity index 88% rename from x-pack/plugins/maps/public/routing/state_syncing/app_sync.js rename to x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts index 60e8dc9cd574c..b346822913bec 100644 --- a/x-pack/plugins/maps/public/routing/state_syncing/app_sync.js +++ b/x-pack/plugins/maps/public/routing/state_syncing/app_sync.ts @@ -4,13 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { connectToQueryState, esFilters } from '../../../../../../src/plugins/data/public'; -import { syncState } from '../../../../../../src/plugins/kibana_utils/public'; import { map } from 'rxjs/operators'; +import { connectToQueryState, esFilters } from '../../../../../../src/plugins/data/public'; +import { syncState, BaseStateContainer } from '../../../../../../src/plugins/kibana_utils/public'; import { getData } from '../../kibana_services'; import { kbnUrlStateStorage } from '../maps_router'; +import { AppStateManager } from './app_state_manager'; -export function startAppStateSyncing(appStateManager) { +export function startAppStateSyncing(appStateManager: AppStateManager) { // get appStateContainer // sync app filters with app state container from data.query to state container const { query } = getData(); @@ -19,7 +20,7 @@ export function startAppStateSyncing(appStateManager) { // clear app state filters to prevent application filters from other applications being transfered to maps query.filterManager.setAppFilters([]); - const stateContainer = { + const stateContainer: BaseStateContainer = { get: () => ({ query: appStateManager.getQuery(), filters: appStateManager.getFilters(), @@ -48,6 +49,7 @@ export function startAppStateSyncing(appStateManager) { // merge initial state from app state container and current state in url const initialAppState = { ...stateContainer.get(), + // @ts-ignore ...kbnUrlStateStorage.get('_a'), }; // trigger state update. actually needed in case some data was in url diff --git a/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts index 4e17241752f53..1e779831c5e0c 100644 --- a/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts +++ b/x-pack/plugins/maps/public/routing/state_syncing/global_sync.ts @@ -3,27 +3,30 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - +import { TimeRange, RefreshInterval, Filter } from 'src/plugins/data/public'; import { syncQueryStateWithUrl } from '../../../../../../src/plugins/data/public'; import { getData } from '../../kibana_services'; -// @ts-ignore import { kbnUrlStateStorage } from '../maps_router'; +export interface MapsGlobalState { + time?: TimeRange; + refreshInterval?: RefreshInterval; + filters?: Filter[]; +} + export function startGlobalStateSyncing() { const { stop } = syncQueryStateWithUrl(getData().query, kbnUrlStateStorage); return stop; } -export function getGlobalState() { - return kbnUrlStateStorage.get('_g'); +export function getGlobalState(): MapsGlobalState { + return kbnUrlStateStorage.get('_g') as MapsGlobalState; } -export function updateGlobalState(newState: unknown, flushUrlState = false) { +export function updateGlobalState(newState: MapsGlobalState, flushUrlState = false) { const globalState = getGlobalState(); kbnUrlStateStorage.set('_g', { - // @ts-ignore ...globalState, - // @ts-ignore ...newState, }); if (flushUrlState) { diff --git a/x-pack/plugins/maps/public/routing/store_operations.js b/x-pack/plugins/maps/public/routing/store_operations.ts similarity index 100% rename from x-pack/plugins/maps/public/routing/store_operations.js rename to x-pack/plugins/maps/public/routing/store_operations.ts diff --git a/x-pack/plugins/maps/server/plugin.ts b/x-pack/plugins/maps/server/plugin.ts index 6862e7536b07f..5eb0482905e36 100644 --- a/x-pack/plugins/maps/server/plugin.ts +++ b/x-pack/plugins/maps/server/plugin.ts @@ -163,7 +163,7 @@ export class MapsPlugin implements Plugin { this._initHomeData(home, core.http.basePath.prepend, mapsLegacyConfig); - features.registerFeature({ + features.registerKibanaFeature({ id: APP_ID, name: i18n.translate('xpack.maps.featureRegistry.mapsFeatureName', { defaultMessage: 'Maps', diff --git a/x-pack/plugins/ml/common/constants/data_frame_analytics.ts b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts new file mode 100644 index 0000000000000..830537cbadbc8 --- /dev/null +++ b/x-pack/plugins/ml/common/constants/data_frame_analytics.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const DEFAULT_RESULTS_FIELD = 'ml'; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts b/x-pack/plugins/ml/common/index.ts similarity index 65% rename from x-pack/plugins/security_solution/public/common/lib/connectors/index.ts rename to x-pack/plugins/ml/common/index.ts index f32e1e0df184e..791a7de48f36f 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -4,5 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { getActionType as jiraActionType } from './jira'; -export { getActionType as resilientActionType } from './resilient'; +export { SearchResponse7 } from './types/es_client'; diff --git a/x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json b/x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json index 18a49bb3841b3..6bc0e55b5aadd 100644 --- a/x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json +++ b/x-pack/plugins/ml/common/types/__mocks__/job_config_farequote.json @@ -69,9 +69,7 @@ "datafeed_id": "datafeed-farequote", "job_id": "farequote", "query_delay": "115823ms", - "indices": [ - "farequote" - ], + "indices": ["farequote"], "query": { "bool": { "must": [ @@ -103,7 +101,7 @@ "buckets": { "date_histogram": { "field": "@timestamp", - "interval": 900000, + "fixed_interval": "15m", "offset": 0, "order": { "_key": "asc" diff --git a/x-pack/plugins/ml/common/types/capabilities.ts b/x-pack/plugins/ml/common/types/capabilities.ts index 8f29365fdca1b..42f056b890828 100644 --- a/x-pack/plugins/ml/common/types/capabilities.ts +++ b/x-pack/plugins/ml/common/types/capabilities.ts @@ -102,6 +102,7 @@ export function getPluginPrivileges() { ...privilege, api: userMlCapabilitiesKeys.map((k) => `ml:${k}`), catalogue: [PLUGIN_ID], + management: { insightsAndAlerting: [] }, ui: userMlCapabilitiesKeys, savedObject: { all: [], diff --git a/x-pack/plugins/ml/common/types/data_frame_analytics.ts b/x-pack/plugins/ml/common/types/data_frame_analytics.ts index f0aac75047585..96d6c81a3d309 100644 --- a/x-pack/plugins/ml/common/types/data_frame_analytics.ts +++ b/x-pack/plugins/ml/common/types/data_frame_analytics.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { CustomHttpResponseOptions, ResponseError } from 'kibana/server'; +import Boom from 'boom'; +import { EsErrorBody } from '../util/errors'; export interface DeleteDataFrameAnalyticsWithIndexStatus { success: boolean; - error?: CustomHttpResponseOptions; + error?: EsErrorBody | Boom; } export type IndexName = string; @@ -79,3 +80,9 @@ export interface DataFrameAnalyticsConfig { version: string; allow_lazy_start?: boolean; } + +export enum ANALYSIS_CONFIG_TYPE { + OUTLIER_DETECTION = 'outlier_detection', + REGRESSION = 'regression', + CLASSIFICATION = 'classification', +} diff --git a/x-pack/plugins/ml/common/types/es_client.ts b/x-pack/plugins/ml/common/types/es_client.ts new file mode 100644 index 0000000000000..d9ca9a3b584ab --- /dev/null +++ b/x-pack/plugins/ml/common/types/es_client.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SearchResponse, ShardsResponse } from 'elasticsearch'; + +// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. +interface SearchResponse7Hits { + hits: SearchResponse['hits']['hits']; + max_score: number; + total: { + value: number; + relation: string; + }; +} +export interface SearchResponse7 { + took: number; + timed_out: boolean; + _scroll_id?: string; + _shards: ShardsResponse; + hits: SearchResponse7Hits; + aggregations?: any; +} diff --git a/x-pack/plugins/ml/common/types/feature_importance.ts b/x-pack/plugins/ml/common/types/feature_importance.ts new file mode 100644 index 0000000000000..d2ab9f6c58608 --- /dev/null +++ b/x-pack/plugins/ml/common/types/feature_importance.ts @@ -0,0 +1,23 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface ClassFeatureImportance { + class_name: string | boolean; + importance: number; +} +export interface FeatureImportance { + feature_name: string; + importance?: number; + classes?: ClassFeatureImportance[]; +} + +export interface TopClass { + class_name: string; + class_probability: number; + class_score: number; +} + +export type TopClasses = TopClass[]; diff --git a/x-pack/plugins/ml/common/types/file_datavisualizer.ts b/x-pack/plugins/ml/common/types/file_datavisualizer.ts index c997a4e24f868..a8b775c8d5f60 100644 --- a/x-pack/plugins/ml/common/types/file_datavisualizer.ts +++ b/x-pack/plugins/ml/common/types/file_datavisualizer.ts @@ -84,7 +84,12 @@ export interface Settings { } export interface Mappings { - [key: string]: any; + _meta?: { + created_by: string; + }; + properties: { + [key: string]: any; + }; } export interface IngestPipelineWrapper { diff --git a/x-pack/plugins/ml/common/util/analytics_utils.ts b/x-pack/plugins/ml/common/util/analytics_utils.ts new file mode 100644 index 0000000000000..d725984a47d66 --- /dev/null +++ b/x-pack/plugins/ml/common/util/analytics_utils.ts @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + AnalysisConfig, + ClassificationAnalysis, + OutlierAnalysis, + RegressionAnalysis, + ANALYSIS_CONFIG_TYPE, +} from '../types/data_frame_analytics'; + +export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => { + const keys = Object.keys(arg); + return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION; +}; + +export const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => { + const keys = Object.keys(arg); + return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION; +}; + +export const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysis => { + const keys = Object.keys(arg); + return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; +}; + +export const getDependentVar = ( + analysis: AnalysisConfig +): + | RegressionAnalysis['regression']['dependent_variable'] + | ClassificationAnalysis['classification']['dependent_variable'] => { + let depVar = ''; + + if (isRegressionAnalysis(analysis)) { + depVar = analysis.regression.dependent_variable; + } + + if (isClassificationAnalysis(analysis)) { + depVar = analysis.classification.dependent_variable; + } + return depVar; +}; + +export const getPredictionFieldName = ( + analysis: AnalysisConfig +): + | RegressionAnalysis['regression']['prediction_field_name'] + | ClassificationAnalysis['classification']['prediction_field_name'] => { + // If undefined will be defaulted to dependent_variable when config is created + let predictionFieldName; + if (isRegressionAnalysis(analysis) && analysis.regression.prediction_field_name !== undefined) { + predictionFieldName = analysis.regression.prediction_field_name; + } else if ( + isClassificationAnalysis(analysis) && + analysis.classification.prediction_field_name !== undefined + ) { + predictionFieldName = analysis.classification.prediction_field_name; + } + return predictionFieldName; +}; + +export const getDefaultPredictionFieldName = (analysis: AnalysisConfig) => { + return `${getDependentVar(analysis)}_prediction`; +}; +export const getPredictedFieldName = ( + resultsField: string, + analysis: AnalysisConfig, + forSort?: boolean +) => { + // default is 'ml' + const predictionFieldName = getPredictionFieldName(analysis); + const predictedField = `${resultsField}.${ + predictionFieldName ? predictionFieldName : getDefaultPredictionFieldName(analysis) + }`; + return predictedField; +}; diff --git a/x-pack/plugins/ml/common/util/errors.test.ts b/x-pack/plugins/ml/common/util/errors.test.ts deleted file mode 100644 index 0b99799e3b6ec..0000000000000 --- a/x-pack/plugins/ml/common/util/errors.test.ts +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - BoomResponse, - extractErrorMessage, - MLCustomHttpResponseOptions, - MLResponseError, -} from './errors'; -import { ResponseError } from 'kibana/server'; - -describe('ML - error message utils', () => { - describe('extractErrorMessage', () => { - test('returns just the error message', () => { - const testMsg = 'Saved object [index-pattern/blahblahblah] not found'; - - const bodyWithNestedErrorMsg: MLCustomHttpResponseOptions = { - body: { - message: { - msg: testMsg, - }, - }, - statusCode: 404, - }; - expect(extractErrorMessage(bodyWithNestedErrorMsg)).toBe(testMsg); - - const bodyWithStringMsg: MLCustomHttpResponseOptions = { - body: { - msg: testMsg, - statusCode: 404, - response: `{"error":{"reason":"${testMsg}"}}`, - }, - statusCode: 404, - }; - expect(extractErrorMessage(bodyWithStringMsg)).toBe(testMsg); - - const bodyWithStringMessage: MLCustomHttpResponseOptions = { - body: { - message: testMsg, - }, - statusCode: 404, - }; - expect(extractErrorMessage(bodyWithStringMessage)).toBe(testMsg); - - const bodyWithString: MLCustomHttpResponseOptions = { - body: testMsg, - statusCode: 404, - }; - expect(extractErrorMessage(bodyWithString)).toBe(testMsg); - - const bodyWithError: MLCustomHttpResponseOptions = { - body: new Error(testMsg), - statusCode: 404, - }; - expect(extractErrorMessage(bodyWithError)).toBe(testMsg); - - const bodyWithBoomError: MLCustomHttpResponseOptions = { - statusCode: 404, - body: { - data: [], - isBoom: true, - isServer: false, - output: { - statusCode: 404, - payload: { - statusCode: 404, - error: testMsg, - message: testMsg, - }, - headers: {}, - }, - }, - }; - expect(extractErrorMessage(bodyWithBoomError)).toBe(testMsg); - }); - }); -}); diff --git a/x-pack/plugins/ml/common/util/errors.ts b/x-pack/plugins/ml/common/util/errors.ts deleted file mode 100644 index 6c5fa7bd75daf..0000000000000 --- a/x-pack/plugins/ml/common/util/errors.ts +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ResponseError, ResponseHeaders } from 'kibana/server'; -import { isErrorResponse } from '../types/errors'; - -export function getErrorMessage(error: any) { - if (isErrorResponse(error)) { - return `${error.body.error}: ${error.body.message}`; - } - - if (typeof error === 'object' && typeof error.message === 'string') { - return error.message; - } - - return JSON.stringify(error); -} - -// Adding temporary types until Kibana ResponseError is updated - -export interface BoomResponse { - data: any; - isBoom: boolean; - isServer: boolean; - output: { - statusCode: number; - payload: { - statusCode: number; - error: string; - message: string; - }; - headers: {}; - }; -} -export type MLResponseError = - | { - message: { - msg: string; - }; - } - | { msg: string; statusCode: number; response: string }; - -export interface MLCustomHttpResponseOptions< - T extends ResponseError | MLResponseError | BoomResponse -> { - /** HTTP message to send to the client */ - body?: T; - /** HTTP Headers with additional information about response */ - headers?: ResponseHeaders; - statusCode: number; -} - -export interface MLErrorObject { - message: string; - fullErrorMessage?: string; // For use in a 'See full error' popover. - statusCode?: number; -} - -export const extractErrorProperties = ( - error: - | MLCustomHttpResponseOptions - | string - | undefined -): MLErrorObject => { - // extract properties of the error object from within the response error - // coming from Kibana, Elasticsearch, and our own ML messages - let message = ''; - let fullErrorMessage; - let statusCode; - - if (typeof error === 'string') { - return { - message: error, - }; - } - if (error?.body === undefined) { - return { - message: '', - }; - } - - if (typeof error.body === 'string') { - return { - message: error.body, - }; - } - if ( - typeof error.body === 'object' && - 'output' in error.body && - error.body.output.payload.message - ) { - return { - message: error.body.output.payload.message, - }; - } - - if ( - typeof error.body === 'object' && - 'response' in error.body && - typeof error.body.response === 'string' - ) { - const errorResponse = JSON.parse(error.body.response); - if ('error' in errorResponse && typeof errorResponse === 'object') { - const errorResponseError = errorResponse.error; - if ('reason' in errorResponseError) { - message = errorResponseError.reason; - } - if ('caused_by' in errorResponseError) { - const causedByMessage = JSON.stringify(errorResponseError.caused_by); - // Only add a fullErrorMessage if different to the message. - if (causedByMessage !== message) { - fullErrorMessage = causedByMessage; - } - } - return { - message, - fullErrorMessage, - statusCode: error.statusCode, - }; - } - } - - if (typeof error.body === 'object' && 'msg' in error.body && typeof error.body.msg === 'string') { - return { - message: error.body.msg, - }; - } - - if (typeof error.body === 'object' && 'message' in error.body) { - if ( - 'attributes' in error.body && - typeof error.body.attributes === 'object' && - error.body.attributes.body?.status !== undefined - ) { - statusCode = error.body.attributes.body?.status; - } - - if (typeof error.body.message === 'string') { - return { - message: error.body.message, - statusCode, - }; - } - if (!(error.body.message instanceof Error) && typeof (error.body.message.msg === 'string')) { - return { - message: error.body.message.msg, - statusCode, - }; - } - } - - // If all else fail return an empty message instead of JSON.stringify - return { - message: '', - }; -}; - -export const extractErrorMessage = ( - error: - | MLCustomHttpResponseOptions - | undefined - | string -): string => { - // extract only the error message within the response error coming from Kibana, Elasticsearch, and our own ML messages - const errorObj = extractErrorProperties(error); - return errorObj.message; -}; diff --git a/x-pack/plugins/ml/common/util/errors/errors.test.ts b/x-pack/plugins/ml/common/util/errors/errors.test.ts new file mode 100644 index 0000000000000..ddaf0b04dd12b --- /dev/null +++ b/x-pack/plugins/ml/common/util/errors/errors.test.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import Boom from 'boom'; + +import { extractErrorMessage, MLHttpFetchError, MLResponseError, EsErrorBody } from './index'; + +describe('ML - error message utils', () => { + describe('extractErrorMessage', () => { + test('returns just the error message', () => { + const testMsg = 'Saved object [index-pattern/indexpattern] not found'; + + // bad error, return empty string + const badError = {} as any; + expect(extractErrorMessage(badError)).toBe(''); + + // raw es error + const esErrorMsg: EsErrorBody = { + error: { + root_cause: [ + { + type: 'type', + reason: 'reason', + }, + ], + type: 'type', + reason: testMsg, + }, + status: 404, + }; + expect(extractErrorMessage(esErrorMsg)).toBe(testMsg); + + // error is basic string + const stringMessage = testMsg; + expect(extractErrorMessage(stringMessage)).toBe(testMsg); + + // kibana error without attributes + const bodyWithoutAttributes: MLHttpFetchError = { + name: 'name', + req: {} as Request, + request: {} as Request, + message: 'Something else', + body: { + statusCode: 404, + error: 'error', + message: testMsg, + }, + }; + expect(extractErrorMessage(bodyWithoutAttributes)).toBe(testMsg); + + // kibana error with attributes + const bodyWithAttributes: MLHttpFetchError = { + name: 'name', + req: {} as Request, + request: {} as Request, + message: 'Something else', + body: { + statusCode: 404, + error: 'error', + message: 'Something else', + attributes: { + body: { + status: 404, + error: { + reason: testMsg, + type: 'type', + root_cause: [{ type: 'type', reason: 'reason' }], + }, + }, + }, + }, + }; + expect(extractErrorMessage(bodyWithAttributes)).toBe(testMsg); + + // boom error + const boomError: Boom = { + message: '', + reformat: () => '', + name: '', + data: [], + isBoom: true, + isServer: false, + output: { + statusCode: 404, + payload: { + statusCode: 404, + error: testMsg, + message: testMsg, + }, + headers: {}, + }, + }; + expect(extractErrorMessage(boomError)).toBe(testMsg); + }); + }); +}); diff --git a/x-pack/plugins/ml/common/util/errors/index.ts b/x-pack/plugins/ml/common/util/errors/index.ts new file mode 100644 index 0000000000000..d6d33b86e6fa9 --- /dev/null +++ b/x-pack/plugins/ml/common/util/errors/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { MLRequestFailure } from './request_error'; +export { extractErrorMessage, extractErrorProperties } from './process_errors'; +export { + ErrorType, + EsErrorBody, + EsErrorRootCause, + MLErrorObject, + MLHttpFetchError, + MLResponseError, + isBoomError, + isErrorString, + isEsErrorBody, + isMLResponseError, +} from './types'; diff --git a/x-pack/plugins/ml/common/util/errors/process_errors.ts b/x-pack/plugins/ml/common/util/errors/process_errors.ts new file mode 100644 index 0000000000000..4addfa414ef56 --- /dev/null +++ b/x-pack/plugins/ml/common/util/errors/process_errors.ts @@ -0,0 +1,83 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + ErrorType, + MLErrorObject, + isBoomError, + isErrorString, + isEsErrorBody, + isMLResponseError, +} from './types'; + +export const extractErrorProperties = (error: ErrorType): MLErrorObject => { + // extract properties of the error object from within the response error + // coming from Kibana, Elasticsearch, and our own ML messages + + // some responses contain raw es errors as part of a bulk response + // e.g. if some jobs fail the action in a bulk request + if (isEsErrorBody(error)) { + return { + message: error.error.reason, + statusCode: error.status, + fullError: error, + }; + } + + if (isErrorString(error)) { + return { + message: error, + }; + } + + if (isBoomError(error)) { + return { + message: error.output.payload.message, + statusCode: error.output.payload.statusCode, + }; + } + + if (error?.body === undefined) { + return { + message: '', + }; + } + + if (typeof error.body === 'string') { + return { + message: error.body, + }; + } + + if (isMLResponseError(error)) { + if ( + typeof error.body.attributes === 'object' && + typeof error.body.attributes.body?.error?.reason === 'string' + ) { + return { + message: error.body.attributes.body.error.reason, + statusCode: error.body.statusCode, + fullError: error.body.attributes.body, + }; + } else { + return { + message: error.body.message, + statusCode: error.body.statusCode, + }; + } + } + + // If all else fail return an empty message instead of JSON.stringify + return { + message: '', + }; +}; + +export const extractErrorMessage = (error: ErrorType): string => { + // extract only the error message within the response error coming from Kibana, Elasticsearch, and our own ML messages + const errorObj = extractErrorProperties(error); + return errorObj.message; +}; diff --git a/x-pack/plugins/ml/common/util/errors/request_error.ts b/x-pack/plugins/ml/common/util/errors/request_error.ts new file mode 100644 index 0000000000000..faa8a8e2dc0d4 --- /dev/null +++ b/x-pack/plugins/ml/common/util/errors/request_error.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MLErrorObject, ErrorType } from './types'; + +export class MLRequestFailure extends Error { + constructor(error: MLErrorObject, resp: ErrorType) { + super(error.message); + Object.setPrototypeOf(this, new.target.prototype); + + if (typeof resp !== 'string' && typeof resp !== 'undefined') { + if ('body' in resp) { + this.stack = JSON.stringify(resp.body, null, 2); + } else { + try { + this.stack = JSON.stringify(resp, null, 2); + } catch (e) { + // fail silently + } + } + } + } +} diff --git a/x-pack/plugins/ml/common/util/errors/types.ts b/x-pack/plugins/ml/common/util/errors/types.ts new file mode 100644 index 0000000000000..3d9fc82be8d85 --- /dev/null +++ b/x-pack/plugins/ml/common/util/errors/types.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpFetchError } from 'kibana/public'; +import Boom from 'boom'; + +export interface EsErrorRootCause { + type: string; + reason: string; +} + +export interface EsErrorBody { + error: { + root_cause?: EsErrorRootCause[]; + caused_by?: EsErrorRootCause; + type: string; + reason: string; + }; + status: number; +} + +export interface MLResponseError { + statusCode: number; + error: string; + message: string; + attributes?: { + body: EsErrorBody; + }; +} + +export interface MLErrorObject { + message: string; + statusCode?: number; + fullError?: EsErrorBody; +} + +export interface MLHttpFetchError extends HttpFetchError { + body: T; +} + +export type ErrorType = MLHttpFetchError | EsErrorBody | Boom | string | undefined; + +export function isEsErrorBody(error: any): error is EsErrorBody { + return error && error.error?.reason !== undefined; +} + +export function isErrorString(error: any): error is string { + return typeof error === 'string'; +} + +export function isMLResponseError(error: any): error is MLResponseError { + return typeof error.body === 'object' && 'message' in error.body; +} + +export function isBoomError(error: any): error is Boom { + return error.isBoom === true; +} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/common.ts b/x-pack/plugins/ml/public/application/components/data_grid/common.ts index 1f0fcb63f019d..f252729cc20cd 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/common.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/common.ts @@ -119,13 +119,14 @@ export const getDataGridSchemasFromFieldTypes = (fieldTypes: FieldTypes, results schema = 'numeric'; } - if ( - field.includes(`${resultsField}.${FEATURE_IMPORTANCE}`) || - field.includes(`${resultsField}.${TOP_CLASSES}`) - ) { + if (field.includes(`${resultsField}.${TOP_CLASSES}`)) { schema = 'json'; } + if (field.includes(`${resultsField}.${FEATURE_IMPORTANCE}`)) { + schema = 'featureImportance'; + } + return { id: field, schema, isSortable }; }); }; @@ -250,10 +251,6 @@ export const useRenderCellValue = ( return cellValue ? 'true' : 'false'; } - if (typeof cellValue === 'object' && cellValue !== null) { - return JSON.stringify(cellValue); - } - return cellValue; }; }, [indexPattern?.fields, pagination.pageIndex, pagination.pageSize, tableItems]); diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx index d4be2eab13d26..22815fe593d57 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.tsx @@ -5,8 +5,7 @@ */ import { isEqual } from 'lodash'; -import React, { memo, useEffect, FC } from 'react'; - +import React, { memo, useEffect, FC, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { @@ -24,13 +23,16 @@ import { } from '@elastic/eui'; import { CoreSetup } from 'src/core/public'; - import { DEFAULT_SAMPLER_SHARD_SIZE } from '../../../../common/constants/field_histograms'; -import { INDEX_STATUS } from '../../data_frame_analytics/common'; +import { ANALYSIS_CONFIG_TYPE, INDEX_STATUS } from '../../data_frame_analytics/common'; import { euiDataGridStyle, euiDataGridToolbarSettings } from './common'; import { UseIndexDataReturnType } from './types'; +import { DecisionPathPopover } from './feature_importance/decision_path_popover'; +import { TopClasses } from '../../../../common/types/feature_importance'; +import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants/data_frame_analytics'; + // TODO Fix row hovering + bar highlighting // import { hoveredRow$ } from './column_chart'; @@ -41,6 +43,9 @@ export const DataGridTitle: FC<{ title: string }> = ({ title }) => ( ); interface PropsWithoutHeader extends UseIndexDataReturnType { + baseline?: number; + analysisType?: ANALYSIS_CONFIG_TYPE; + resultsField?: string; dataTestSubj: string; toastNotifications: CoreSetup['notifications']['toasts']; } @@ -60,6 +65,7 @@ type Props = PropsWithHeader | PropsWithoutHeader; export const DataGrid: FC = memo( (props) => { const { + baseline, chartsVisible, chartsButtonVisible, columnsWithCharts, @@ -80,8 +86,10 @@ export const DataGrid: FC = memo( toastNotifications, toggleChartVisibility, visibleColumns, + predictionFieldName, + resultsField, + analysisType, } = props; - // TODO Fix row hovering + bar highlighting // const getRowProps = (item: any) => { // return { @@ -90,6 +98,45 @@ export const DataGrid: FC = memo( // }; // }; + const popOverContent = useMemo(() => { + return analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION || + analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION + ? { + featureImportance: ({ children }: { cellContentsElement: any; children: any }) => { + const rowIndex = children?.props?.visibleRowIndex; + const row = data[rowIndex]; + if (!row) return
; + // if resultsField for some reason is not available then use ml + const mlResultsField = resultsField ?? DEFAULT_RESULTS_FIELD; + const parsedFIArray = row[mlResultsField].feature_importance; + let predictedValue: string | number | undefined; + let topClasses: TopClasses = []; + if ( + predictionFieldName !== undefined && + row && + row[mlResultsField][predictionFieldName] !== undefined + ) { + predictedValue = row[mlResultsField][predictionFieldName]; + topClasses = row[mlResultsField].top_classes; + } + + return ( + + ); + }, + } + : undefined; + }, [baseline, data]); + useEffect(() => { if (invalidSortingColumnns.length > 0) { invalidSortingColumnns.forEach((columnId) => { @@ -225,6 +272,7 @@ export const DataGrid: FC = memo( } : {}), }} + popoverContents={popOverContent} pagination={{ ...pagination, pageSizeOptions: [5, 10, 25], diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx new file mode 100644 index 0000000000000..b546ac1db57dd --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_chart.tsx @@ -0,0 +1,166 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + AnnotationDomainTypes, + Axis, + AxisStyle, + Chart, + LineAnnotation, + LineAnnotationStyle, + LineAnnotationDatum, + LineSeries, + PartialTheme, + Position, + RecursivePartial, + ScaleType, + Settings, +} from '@elastic/charts'; +import { EuiIcon } from '@elastic/eui'; + +import React, { useCallback, useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import euiVars from '@elastic/eui/dist/eui_theme_light.json'; +import { DecisionPathPlotData } from './use_classification_path_data'; + +const { euiColorFullShade, euiColorMediumShade } = euiVars; +const axisColor = euiColorMediumShade; + +const baselineStyle: LineAnnotationStyle = { + line: { + strokeWidth: 1, + stroke: euiColorFullShade, + opacity: 0.75, + }, + details: { + fontFamily: 'Arial', + fontSize: 10, + fontStyle: 'bold', + fill: euiColorMediumShade, + padding: 0, + }, +}; + +const axes: RecursivePartial = { + axisLine: { + stroke: axisColor, + }, + tickLabel: { + fontSize: 10, + fill: axisColor, + }, + tickLine: { + stroke: axisColor, + }, + gridLine: { + horizontal: { + dash: [1, 2], + }, + vertical: { + strokeWidth: 0, + }, + }, +}; +const theme: PartialTheme = { + axes, +}; + +interface DecisionPathChartProps { + decisionPathData: DecisionPathPlotData; + predictionFieldName?: string; + baseline?: number; + minDomain: number | undefined; + maxDomain: number | undefined; +} + +const DECISION_PATH_MARGIN = 125; +const DECISION_PATH_ROW_HEIGHT = 10; +const NUM_PRECISION = 3; +const AnnotationBaselineMarker = ; + +export const DecisionPathChart = ({ + decisionPathData, + predictionFieldName, + minDomain, + maxDomain, + baseline, +}: DecisionPathChartProps) => { + // adjust the height so it's compact for items with more features + const baselineData: LineAnnotationDatum[] = useMemo( + () => [ + { + dataValue: baseline, + header: baseline ? baseline.toPrecision(NUM_PRECISION) : '', + details: i18n.translate( + 'xpack.ml.dataframe.analytics.explorationResults.decisionPathBaselineText', + { + defaultMessage: + 'baseline (average of predictions for all data points in the training data set)', + } + ), + }, + ], + [baseline] + ); + // guarantee up to num_precision significant digits + // without having it in scientific notation + const tickFormatter = useCallback((d) => Number(d.toPrecision(NUM_PRECISION)).toString(), []); + + return ( + + + {baseline && ( + + )} + + + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx new file mode 100644 index 0000000000000..bd001fa81a582 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_classification.tsx @@ -0,0 +1,105 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useMemo, useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { EuiHealth, EuiSpacer, EuiSuperSelect, EuiTitle } from '@elastic/eui'; +import d3 from 'd3'; +import { + isDecisionPathData, + useDecisionPathData, + getStringBasedClassName, +} from './use_classification_path_data'; +import { FeatureImportance, TopClasses } from '../../../../../common/types/feature_importance'; +import { DecisionPathChart } from './decision_path_chart'; +import { MissingDecisionPathCallout } from './missing_decision_path_callout'; + +interface ClassificationDecisionPathProps { + predictedValue: string | boolean; + predictionFieldName?: string; + featureImportance: FeatureImportance[]; + topClasses: TopClasses; +} + +export const ClassificationDecisionPath: FC = ({ + featureImportance, + predictedValue, + topClasses, + predictionFieldName, +}) => { + const [currentClass, setCurrentClass] = useState( + getStringBasedClassName(topClasses[0].class_name) + ); + const { decisionPathData } = useDecisionPathData({ + featureImportance, + predictedValue: currentClass, + }); + const options = useMemo(() => { + const predictionValueStr = getStringBasedClassName(predictedValue); + + return Array.isArray(topClasses) + ? topClasses.map((c) => { + const className = getStringBasedClassName(c.class_name); + return { + value: className, + inputDisplay: + className === predictionValueStr ? ( + + {className} + + ) : ( + className + ), + }; + }) + : undefined; + }, [topClasses, predictedValue]); + + const domain = useMemo(() => { + let maxDomain; + let minDomain; + // if decisionPathData has calculated cumulative path + if (decisionPathData && isDecisionPathData(decisionPathData)) { + const [min, max] = d3.extent(decisionPathData, (d: [string, number, number]) => d[2]); + const buffer = Math.abs(max - min) * 0.1; + maxDomain = max + buffer; + minDomain = min - buffer; + } + return { maxDomain, minDomain }; + }, [decisionPathData]); + + if (!decisionPathData) return ; + + return ( + <> + + + + {i18n.translate( + 'xpack.ml.dataframe.analytics.explorationResults.classificationDecisionPathClassNameTitle', + { + defaultMessage: 'Class name', + } + )} + + + {options !== undefined && ( + + )} + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx new file mode 100644 index 0000000000000..343324b27f9b5 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_json_viewer.tsx @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC } from 'react'; +import { EuiCodeBlock } from '@elastic/eui'; +import { FeatureImportance } from '../../../../../common/types/feature_importance'; + +interface DecisionPathJSONViewerProps { + featureImportance: FeatureImportance[]; +} +export const DecisionPathJSONViewer: FC = ({ featureImportance }) => { + return {JSON.stringify(featureImportance)}; +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx new file mode 100644 index 0000000000000..263337f93e9a8 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_popover.tsx @@ -0,0 +1,134 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useState } from 'react'; +import { EuiLink, EuiTab, EuiTabs, EuiText } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { RegressionDecisionPath } from './decision_path_regression'; +import { DecisionPathJSONViewer } from './decision_path_json_viewer'; +import { FeatureImportance, TopClasses } from '../../../../../common/types/feature_importance'; +import { ANALYSIS_CONFIG_TYPE } from '../../../data_frame_analytics/common'; +import { ClassificationDecisionPath } from './decision_path_classification'; +import { useMlKibana } from '../../../contexts/kibana'; + +interface DecisionPathPopoverProps { + featureImportance: FeatureImportance[]; + analysisType: ANALYSIS_CONFIG_TYPE; + predictionFieldName?: string; + baseline?: number; + predictedValue?: number | string | undefined; + topClasses?: TopClasses; +} + +enum DECISION_PATH_TABS { + CHART = 'decision_path_chart', + JSON = 'decision_path_json', +} + +export interface ExtendedFeatureImportance extends FeatureImportance { + absImportance?: number; +} + +export const DecisionPathPopover: FC = ({ + baseline, + featureImportance, + predictedValue, + topClasses, + analysisType, + predictionFieldName, +}) => { + const [selectedTabId, setSelectedTabId] = useState(DECISION_PATH_TABS.CHART); + const { + services: { docLinks }, + } = useMlKibana(); + const { ELASTIC_WEBSITE_URL, DOC_LINK_VERSION } = docLinks; + + if (featureImportance.length < 2) { + return ; + } + + const tabs = [ + { + id: DECISION_PATH_TABS.CHART, + name: ( + + ), + }, + { + id: DECISION_PATH_TABS.JSON, + name: ( + + ), + }, + ]; + + return ( + <> +
+ + {tabs.map((tab) => ( + setSelectedTabId(tab.id)} + key={tab.id} + > + {tab.name} + + ))} + +
+ {selectedTabId === DECISION_PATH_TABS.CHART && ( + <> + + + + + ), + }} + /> + + {analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && ( + + )} + {analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION && ( + + )} + + )} + {selectedTabId === DECISION_PATH_TABS.JSON && ( + + )} + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.tsx new file mode 100644 index 0000000000000..345269a944f02 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/decision_path_regression.tsx @@ -0,0 +1,79 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { FC, useMemo } from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; +import d3 from 'd3'; +import { FeatureImportance, TopClasses } from '../../../../../common/types/feature_importance'; +import { useDecisionPathData, isDecisionPathData } from './use_classification_path_data'; +import { DecisionPathChart } from './decision_path_chart'; +import { MissingDecisionPathCallout } from './missing_decision_path_callout'; + +interface RegressionDecisionPathProps { + predictionFieldName?: string; + baseline?: number; + predictedValue?: number | undefined; + featureImportance: FeatureImportance[]; + topClasses?: TopClasses; +} + +export const RegressionDecisionPath: FC = ({ + baseline, + featureImportance, + predictedValue, + predictionFieldName, +}) => { + const { decisionPathData } = useDecisionPathData({ + baseline, + featureImportance, + predictedValue, + }); + const domain = useMemo(() => { + let maxDomain; + let minDomain; + // if decisionPathData has calculated cumulative path + if (decisionPathData && isDecisionPathData(decisionPathData)) { + const [min, max] = d3.extent(decisionPathData, (d: [string, number, number]) => d[2]); + maxDomain = max; + minDomain = min; + const buffer = Math.abs(maxDomain - minDomain) * 0.1; + maxDomain = + (typeof baseline === 'number' ? Math.max(maxDomain, baseline) : maxDomain) + buffer; + minDomain = + (typeof baseline === 'number' ? Math.min(minDomain, baseline) : minDomain) - buffer; + } + return { maxDomain, minDomain }; + }, [decisionPathData, baseline]); + + if (!decisionPathData) return ; + + return ( + <> + {baseline === undefined && ( + + } + color="warning" + iconType="alert" + /> + )} + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/missing_decision_path_callout.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/missing_decision_path_callout.tsx new file mode 100644 index 0000000000000..66eb2047b1314 --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/missing_decision_path_callout.tsx @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n/react'; + +export const MissingDecisionPathCallout = () => { + return ( + + + + ); +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx new file mode 100644 index 0000000000000..90216c4a58ffc --- /dev/null +++ b/x-pack/plugins/ml/public/application/components/data_grid/feature_importance/use_classification_path_data.tsx @@ -0,0 +1,173 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FeatureImportance, TopClasses } from '../../../../../common/types/feature_importance'; +import { ExtendedFeatureImportance } from './decision_path_popover'; + +export type DecisionPathPlotData = Array<[string, number, number]>; + +interface UseDecisionPathDataParams { + featureImportance: FeatureImportance[]; + baseline?: number; + predictedValue?: string | number | undefined; + topClasses?: TopClasses; +} + +interface RegressionDecisionPathProps { + baseline?: number; + predictedValue?: number | undefined; + featureImportance: FeatureImportance[]; + topClasses?: TopClasses; +} +const FEATURE_NAME = 'feature_name'; +const FEATURE_IMPORTANCE = 'importance'; + +export const isDecisionPathData = (decisionPathData: any): boolean => { + return ( + Array.isArray(decisionPathData) && + decisionPathData.length > 0 && + decisionPathData[0].length === 3 + ); +}; + +// cast to 'True' | 'False' | value to match Eui display +export const getStringBasedClassName = (v: string | boolean | undefined | number): string => { + if (v === undefined) { + return ''; + } + if (typeof v === 'boolean') { + return v ? 'True' : 'False'; + } + if (typeof v === 'number') { + return v.toString(); + } + return v; +}; + +export const useDecisionPathData = ({ + baseline, + featureImportance, + predictedValue, +}: UseDecisionPathDataParams): { decisionPathData: DecisionPathPlotData | undefined } => { + const decisionPathData = useMemo(() => { + return baseline + ? buildRegressionDecisionPathData({ + baseline, + featureImportance, + predictedValue: predictedValue as number | undefined, + }) + : buildClassificationDecisionPathData({ + featureImportance, + currentClass: predictedValue as string | undefined, + }); + }, [baseline, featureImportance, predictedValue]); + + return { decisionPathData }; +}; + +export const buildDecisionPathData = (featureImportance: ExtendedFeatureImportance[]) => { + const finalResult: DecisionPathPlotData = featureImportance + // sort so absolute importance so it goes from bottom (baseline) to top + .sort( + (a: ExtendedFeatureImportance, b: ExtendedFeatureImportance) => + b.absImportance! - a.absImportance! + ) + .map((d) => [d[FEATURE_NAME] as string, d[FEATURE_IMPORTANCE] as number, NaN]); + + // start at the baseline and end at predicted value + // for regression, cumulativeSum should add up to baseline + let cumulativeSum = 0; + for (let i = featureImportance.length - 1; i >= 0; i--) { + cumulativeSum += finalResult[i][1]; + finalResult[i][2] = cumulativeSum; + } + return finalResult; +}; +export const buildRegressionDecisionPathData = ({ + baseline, + featureImportance, + predictedValue, +}: RegressionDecisionPathProps): DecisionPathPlotData | undefined => { + let mappedFeatureImportance: ExtendedFeatureImportance[] = featureImportance; + mappedFeatureImportance = mappedFeatureImportance.map((d) => ({ + ...d, + absImportance: Math.abs(d[FEATURE_IMPORTANCE] as number), + })); + + if (baseline && predictedValue !== undefined && Number.isFinite(predictedValue)) { + // get the adjusted importance needed for when # of fields included in c++ analysis != max allowed + // if num fields included = num features allowed exactly, adjustedImportance should be 0 + const adjustedImportance = + predictedValue - + mappedFeatureImportance.reduce( + (accumulator, currentValue) => accumulator + currentValue.importance!, + 0 + ) - + baseline; + + mappedFeatureImportance.push({ + [FEATURE_NAME]: i18n.translate( + 'xpack.ml.dataframe.analytics.decisionPathFeatureBaselineTitle', + { + defaultMessage: 'baseline', + } + ), + [FEATURE_IMPORTANCE]: baseline, + absImportance: -1, + }); + + // if the difference is small enough then no need to plot the residual feature importance + if (Math.abs(adjustedImportance) > 1e-5) { + mappedFeatureImportance.push({ + [FEATURE_NAME]: i18n.translate( + 'xpack.ml.dataframe.analytics.decisionPathFeatureOtherTitle', + { + defaultMessage: 'other', + } + ), + [FEATURE_IMPORTANCE]: adjustedImportance, + absImportance: 0, // arbitrary importance so this will be of higher importance than baseline + }); + } + } + const filteredFeatureImportance = mappedFeatureImportance.filter( + (f) => f !== undefined + ) as ExtendedFeatureImportance[]; + + return buildDecisionPathData(filteredFeatureImportance); +}; + +export const buildClassificationDecisionPathData = ({ + featureImportance, + currentClass, +}: { + featureImportance: FeatureImportance[]; + currentClass: string | undefined; +}): DecisionPathPlotData | undefined => { + if (currentClass === undefined) return []; + const mappedFeatureImportance: Array< + ExtendedFeatureImportance | undefined + > = featureImportance.map((feature) => { + const classFeatureImportance = Array.isArray(feature.classes) + ? feature.classes.find((c) => getStringBasedClassName(c.class_name) === currentClass) + : feature; + if (classFeatureImportance && typeof classFeatureImportance[FEATURE_IMPORTANCE] === 'number') { + return { + [FEATURE_NAME]: feature[FEATURE_NAME], + [FEATURE_IMPORTANCE]: classFeatureImportance[FEATURE_IMPORTANCE], + absImportance: Math.abs(classFeatureImportance[FEATURE_IMPORTANCE] as number), + }; + } + return undefined; + }); + const filteredFeatureImportance = mappedFeatureImportance.filter( + (f) => f !== undefined + ) as ExtendedFeatureImportance[]; + + return buildDecisionPathData(filteredFeatureImportance); +}; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/index.ts b/x-pack/plugins/ml/public/application/components/data_grid/index.ts index 4bbd3595e5a7e..633d70687dd27 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/index.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/index.ts @@ -19,7 +19,6 @@ export { DataGridItem, EsSorting, RenderCellValue, - SearchResponse7, UseDataGridReturnType, UseIndexDataReturnType, } from './types'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/types.ts b/x-pack/plugins/ml/public/application/components/data_grid/types.ts index 756f74c8f9302..22fff0f6e0b93 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/types.ts +++ b/x-pack/plugins/ml/public/application/components/data_grid/types.ts @@ -5,7 +5,6 @@ */ import { Dispatch, SetStateAction } from 'react'; -import { SearchResponse } from 'elasticsearch'; import { EuiDataGridPaginationProps, EuiDataGridSorting, EuiDataGridColumn } from '@elastic/eui'; @@ -43,16 +42,6 @@ export type EsSorting = Dictionary<{ order: 'asc' | 'desc'; }>; -// The types specified in `@types/elasticsearch` are out of date and still have `total: number`. -export interface SearchResponse7 extends SearchResponse { - hits: SearchResponse['hits'] & { - total: { - value: number; - relation: string; - }; - }; -} - export interface UseIndexDataReturnType extends Pick< UseDataGridReturnType, @@ -74,6 +63,9 @@ export interface UseIndexDataReturnType | 'tableItems' | 'toggleChartVisibility' | 'visibleColumns' + | 'baseline' + | 'predictionFieldName' + | 'resultsField' > { renderCellValue: RenderCellValue; } @@ -105,4 +97,7 @@ export interface UseDataGridReturnType { tableItems: DataGridItem[]; toggleChartVisibility: () => void; visibleColumns: ColumnId[]; + baseline?: number; + predictionFieldName?: string; + resultsField?: string; } diff --git a/x-pack/plugins/ml/public/application/components/messagebar/messagebar_service.js b/x-pack/plugins/ml/public/application/components/messagebar/messagebar_service.js deleted file mode 100644 index 1dc7140bd2687..0000000000000 --- a/x-pack/plugins/ml/public/application/components/messagebar/messagebar_service.js +++ /dev/null @@ -1,33 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getToastNotifications } from '../../util/dependency_cache'; -import { MLRequestFailure } from '../../util/ml_error'; -import { i18n } from '@kbn/i18n'; - -function errorNotify(text, resp) { - let err = null; - if (typeof text === 'object' && text.response !== undefined) { - resp = text.response; - } else if (typeof text === 'object' && text.message !== undefined) { - err = new Error(text.message); - } else { - err = new Error(text); - } - - const toastNotifications = getToastNotifications(); - toastNotifications.addError(new MLRequestFailure(err, resp), { - title: i18n.translate('xpack.ml.messagebarService.errorTitle', { - defaultMessage: 'An error has occurred', - }), - }); -} - -export const mlMessageBarService = { - notify: { - error: errorNotify, - }, -}; diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js index eed57aaf1b491..5b0224c8e9f0a 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/rule_editor_flyout.js @@ -10,6 +10,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, @@ -51,8 +53,7 @@ import { getPartitioningFieldNames } from '../../../../common/util/job_utils'; import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { mlJobService } from '../../services/job_service'; import { ml } from '../../services/ml_api_service'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { extractErrorMessage } from '../../../../common/util/errors'; class RuleEditorFlyoutUI extends Component { static propTypes = { @@ -431,8 +432,8 @@ class RuleEditorFlyoutUI extends Component { values: { jobId }, } ); - if (error.message) { - errorMessage += ` : ${error.message}`; + if (error.error) { + errorMessage += ` : ${extractErrorMessage(error.error)}`; } toasts.addDanger(errorMessage); }); diff --git a/x-pack/plugins/ml/public/application/components/rule_editor/utils.js b/x-pack/plugins/ml/public/application/components/rule_editor/utils.js index a697eed00fd66..6a8e20647cdbe 100644 --- a/x-pack/plugins/ml/public/application/components/rule_editor/utils.js +++ b/x-pack/plugins/ml/public/application/components/rule_editor/utils.js @@ -146,22 +146,17 @@ export function updateJobRules(job, detectorIndex, rules) { } return new Promise((resolve, reject) => { - mlJobService - .updateJob(jobId, jobData) - .then((resp) => { - if (resp.success) { - // Refresh the job data in the job service before resolving. - mlJobService - .refreshJob(jobId) - .then(() => { - resolve({ success: true }); - }) - .catch((refreshResp) => { - reject(refreshResp); - }); - } else { - reject(resp); - } + ml.updateJob({ jobId: jobId, job: jobData }) + .then(() => { + // Refresh the job data in the job service before resolving. + mlJobService + .refreshJob(jobId) + .then(() => { + resolve({ success: true }); + }) + .catch((refreshResp) => { + reject(refreshResp); + }); }) .catch((resp) => { reject(resp); diff --git a/x-pack/plugins/ml/public/application/components/validate_job/__snapshots__/validate_job_view.test.js.snap b/x-pack/plugins/ml/public/application/components/validate_job/__snapshots__/validate_job_view.test.js.snap index cbbf84b9e51ec..f6a161cc01999 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/__snapshots__/validate_job_view.test.js.snap +++ b/x-pack/plugins/ml/public/application/components/validate_job/__snapshots__/validate_job_view.test.js.snap @@ -8,7 +8,7 @@ exports[`ValidateJob renders button and modal with a message 1`] = ` iconSide="right" iconType="questionInCircle" isDisabled={false} - isLoading={false} + isLoading={true} onClick={[Function]} size="s" > @@ -18,62 +18,6 @@ exports[`ValidateJob renders button and modal with a message 1`] = ` values={Object {}} /> - - } - > - - - - - - - - , - } - } - /> - -
`; @@ -108,7 +52,7 @@ exports[`ValidateJob renders the button and modal with a success message 1`] = ` iconSide="right" iconType="questionInCircle" isDisabled={false} - isLoading={false} + isLoading={true} onClick={[Function]} size="s" > @@ -118,52 +62,6 @@ exports[`ValidateJob renders the button and modal with a success message 1`] = ` values={Object {}} /> - - } - > - - - - - - - -
, - } - } - /> - -
`; diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.d.ts b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.d.ts index 43e0a5f3eac78..35e4e189b4326 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.d.ts +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.d.ts @@ -8,7 +8,7 @@ import { FC } from 'react'; declare const ValidateJob: FC<{ getJobConfig: any; getDuration: any; - mlJobService: any; + ml: any; embedded?: boolean; setIsValid?: (valid: boolean) => void; idFilterList?: string[]; diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js index dde6925631d3e..0c079bc11cffc 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.js @@ -32,6 +32,8 @@ import { getDocLinks } from '../../util/dependency_cache'; import { VALIDATION_STATUS } from '../../../../common/constants/validation'; import { getMostSevereMessageStatus } from '../../../../common/util/validation_utils'; +import { toastNotificationServiceProvider } from '../../services/toast_notification_service'; +import { withKibana } from '../../../../../../../src/plugins/kibana_react/public'; const defaultIconType = 'questionInCircle'; const getDefaultState = () => ({ @@ -182,7 +184,7 @@ Modal.propType = { title: PropTypes.string, }; -export class ValidateJob extends Component { +export class ValidateJobUI extends Component { constructor(props) { super(props); this.state = getDefaultState(); @@ -209,25 +211,40 @@ export class ValidateJob extends Component { if (typeof job === 'object') { let shouldShowLoadingIndicator = true; - this.props.mlJobService.validateJob({ duration, fields, job }).then((data) => { - shouldShowLoadingIndicator = false; - this.setState({ - ...this.state, - ui: { - ...this.state.ui, - iconType: statusToEuiIconType(getMostSevereMessageStatus(data.messages)), - isLoading: false, - isModalVisible: true, - }, - data, - title: job.job_id, - }); - if (typeof this.props.setIsValid === 'function') { - this.props.setIsValid( - data.messages.some((m) => m.status === VALIDATION_STATUS.ERROR) === false + this.props.ml + .validateJob({ duration, fields, job }) + .then((messages) => { + shouldShowLoadingIndicator = false; + this.setState({ + ...this.state, + ui: { + ...this.state.ui, + iconType: statusToEuiIconType(getMostSevereMessageStatus(messages)), + isLoading: false, + isModalVisible: true, + }, + data: { + messages, + success: true, + }, + title: job.job_id, + }); + if (typeof this.props.setIsValid === 'function') { + this.props.setIsValid( + messages.some((m) => m.status === VALIDATION_STATUS.ERROR) === false + ); + } + }) + .catch((error) => { + const { toasts } = this.props.kibana.services.notifications; + const toastNotificationService = toastNotificationServiceProvider(toasts); + toastNotificationService.displayErrorToast( + error, + i18n.translate('xpack.ml.jobService.validateJobErrorTitle', { + defaultMessage: 'Job Validation Error', + }) ); - } - }); + }); // wait for 250ms before triggering the loading indicator // to avoid flickering when there's a loading time below @@ -335,15 +352,17 @@ export class ValidateJob extends Component { ); } } -ValidateJob.propTypes = { +ValidateJobUI.propTypes = { fields: PropTypes.object, fill: PropTypes.bool, getDuration: PropTypes.func, getJobConfig: PropTypes.func.isRequired, isCurrentJobConfig: PropTypes.bool, isDisabled: PropTypes.bool, - mlJobService: PropTypes.object.isRequired, + ml: PropTypes.object.isRequired, embedded: PropTypes.bool, setIsValid: PropTypes.func, idFilterList: PropTypes.array, }; + +export const ValidateJob = withKibana(ValidateJobUI); diff --git a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js index cc8a5abb4e9ab..280dbd76d5487 100644 --- a/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js +++ b/x-pack/plugins/ml/public/application/components/validate_job/validate_job_view.test.js @@ -16,6 +16,12 @@ jest.mock('../../util/dependency_cache', () => ({ }), })); +jest.mock('../../../../../../../src/plugins/kibana_react/public', () => ({ + withKibana: (comp) => { + return comp; + }, +})); + const job = { job_id: 'test-id', }; @@ -25,11 +31,16 @@ const getJobConfig = () => job; function prepareTest(messages) { const p = Promise.resolve(messages); - const mlJobService = { - validateJob: () => p, + const ml = { + validateJob: () => Promise.resolve(messages), + }; + const kibana = { + services: { + notifications: { toasts: { addDanger: jest.fn() } }, + }, }; - const component = ; + const component = ; const wrapper = shallowWithIntl(component); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts index 8ad861e616b7a..60681fb6e7bbe 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/analytics.ts @@ -10,23 +10,24 @@ import { distinctUntilChanged, filter } from 'rxjs/operators'; import { cloneDeep } from 'lodash'; import { ml } from '../../services/ml_api_service'; import { Dictionary } from '../../../../common/types/common'; -import { getErrorMessage } from '../../../../common/util/errors'; +import { extractErrorMessage } from '../../../../common/util/errors'; import { SavedSearchQuery } from '../../contexts/ml'; import { AnalysisConfig, ClassificationAnalysis, - OutlierAnalysis, RegressionAnalysis, + ANALYSIS_CONFIG_TYPE, } from '../../../../common/types/data_frame_analytics'; - +import { + isOutlierAnalysis, + isRegressionAnalysis, + isClassificationAnalysis, + getPredictionFieldName, + getDependentVar, + getPredictedFieldName, +} from '../../../../common/util/analytics_utils'; export type IndexPattern = string; -export enum ANALYSIS_CONFIG_TYPE { - OUTLIER_DETECTION = 'outlier_detection', - REGRESSION = 'regression', - CLASSIFICATION = 'classification', -} - export enum ANALYSIS_ADVANCED_FIELDS { ETA = 'eta', FEATURE_BAG_FRACTION = 'feature_bag_fraction', @@ -156,23 +157,6 @@ export const getAnalysisType = (analysis: AnalysisConfig): string => { return 'unknown'; }; -export const getDependentVar = ( - analysis: AnalysisConfig -): - | RegressionAnalysis['regression']['dependent_variable'] - | ClassificationAnalysis['classification']['dependent_variable'] => { - let depVar = ''; - - if (isRegressionAnalysis(analysis)) { - depVar = analysis.regression.dependent_variable; - } - - if (isClassificationAnalysis(analysis)) { - depVar = analysis.classification.dependent_variable; - } - return depVar; -}; - export const getTrainingPercent = ( analysis: AnalysisConfig ): @@ -190,24 +174,6 @@ export const getTrainingPercent = ( return trainingPercent; }; -export const getPredictionFieldName = ( - analysis: AnalysisConfig -): - | RegressionAnalysis['regression']['prediction_field_name'] - | ClassificationAnalysis['classification']['prediction_field_name'] => { - // If undefined will be defaulted to dependent_variable when config is created - let predictionFieldName; - if (isRegressionAnalysis(analysis) && analysis.regression.prediction_field_name !== undefined) { - predictionFieldName = analysis.regression.prediction_field_name; - } else if ( - isClassificationAnalysis(analysis) && - analysis.classification.prediction_field_name !== undefined - ) { - predictionFieldName = analysis.classification.prediction_field_name; - } - return predictionFieldName; -}; - export const getNumTopClasses = ( analysis: AnalysisConfig ): ClassificationAnalysis['classification']['num_top_classes'] => { @@ -238,35 +204,6 @@ export const getNumTopFeatureImportanceValues = ( return numTopFeatureImportanceValues; }; -export const getPredictedFieldName = ( - resultsField: string, - analysis: AnalysisConfig, - forSort?: boolean -) => { - // default is 'ml' - const predictionFieldName = getPredictionFieldName(analysis); - const defaultPredictionField = `${getDependentVar(analysis)}_prediction`; - const predictedField = `${resultsField}.${ - predictionFieldName ? predictionFieldName : defaultPredictionField - }`; - return predictedField; -}; - -export const isOutlierAnalysis = (arg: any): arg is OutlierAnalysis => { - const keys = Object.keys(arg); - return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.OUTLIER_DETECTION; -}; - -export const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => { - const keys = Object.keys(arg); - return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION; -}; - -export const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysis => { - const keys = Object.keys(arg); - return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; -}; - export const isResultsSearchBoolQuery = (arg: any): arg is ResultsSearchBoolQuery => { if (arg === undefined) return false; const keys = Object.keys(arg); @@ -549,7 +486,7 @@ export const loadEvalData = async ({ results.eval = evalResult; return results; } catch (e) { - results.error = getErrorMessage(e); + results.error = extractErrorMessage(e); return results; } }; @@ -607,3 +544,13 @@ export const loadDocsCount = async ({ }; } }; + +export { + isOutlierAnalysis, + isRegressionAnalysis, + isClassificationAnalysis, + getPredictionFieldName, + ANALYSIS_CONFIG_TYPE, + getDependentVar, + getPredictedFieldName, +}; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts index 2f14dfdfdfca3..c2295a92af89c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/constants.ts @@ -3,8 +3,6 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ - -export const DEFAULT_RESULTS_FIELD = 'ml'; export const FEATURE_IMPORTANCE = 'feature_importance'; export const FEATURE_INFLUENCE = 'feature_influence'; export const TOP_CLASSES = 'top_classes'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts index 847aefefbc6c8..f9c9bf26a9d16 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/fields.ts @@ -4,17 +4,16 @@ * you may not use this file except in compliance with the Elastic License. */ +import { getNumTopClasses, getNumTopFeatureImportanceValues } from './analytics'; +import { Field } from '../../../../common/types/fields'; import { - getNumTopClasses, - getNumTopFeatureImportanceValues, getPredictedFieldName, getDependentVar, getPredictionFieldName, isClassificationAnalysis, isOutlierAnalysis, isRegressionAnalysis, -} from './analytics'; -import { Field } from '../../../../common/types/fields'; +} from '../../../../common/util/analytics_utils'; import { ES_FIELD_TYPES, KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; import { newJobCapsService } from '../../services/new_job_capabilities_service'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts index c162cb2754c10..361a79d42214d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/get_index_data.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getErrorMessage } from '../../../../common/util/errors'; +import type { SearchResponse7 } from '../../../../common/types/es_client'; +import { extractErrorMessage } from '../../../../common/util/errors'; -import { EsSorting, SearchResponse7, UseDataGridReturnType } from '../../components/data_grid'; +import { EsSorting, UseDataGridReturnType } from '../../components/data_grid'; import { ml } from '../../services/ml_api_service'; import { isKeywordAndTextType } from '../common/fields'; @@ -62,7 +63,7 @@ export const getIndexData = async ( setTableItems(docs); setStatus(INDEX_STATUS.LOADED); } catch (e) { - setErrorMessage(getErrorMessage(e)); + setErrorMessage(extractErrorMessage(e)); setStatus(INDEX_STATUS.ERROR); } } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts index fde1b26106508..b0e73edff7476 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/common/use_results_view_config.ts @@ -8,7 +8,7 @@ import { useEffect, useState } from 'react'; import { IndexPattern } from '../../../../../../../src/plugins/data/public'; -import { getErrorMessage } from '../../../../common/util/errors'; +import { extractErrorMessage } from '../../../../common/util/errors'; import { getIndexPatternIdFromName } from '../../util/index_utils'; import { ml } from '../../services/ml_api_service'; @@ -83,12 +83,12 @@ export const useResultsViewConfig = (jobId: string) => { setIsLoadingJobConfig(false); } } catch (e) { - setJobCapsServiceErrorMessage(getErrorMessage(e)); + setJobCapsServiceErrorMessage(extractErrorMessage(e)); setIsLoadingJobConfig(false); } } } catch (e) { - setJobConfigErrorMessage(getErrorMessage(e)); + setJobConfigErrorMessage(extractErrorMessage(e)); setIsLoadingJobConfig(false); } })(); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx index 25baff98556a6..dd9ecc963840a 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/components/configuration_step/configuration_step_form.tsx @@ -209,7 +209,6 @@ export const ConfigurationStepForm: FC = ({ let unsupportedFieldsErrorMessage; if ( jobType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION && - errorMessage.includes('status_exception') && (errorMessage.includes('must have at most') || errorMessage.includes('must have at least')) ) { maxDistinctValuesErrorMessage = errorMessage; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts index eab5165a42137..74d45b86c8c4d 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_creation/hooks/use_index_data.ts @@ -22,10 +22,10 @@ import { useDataGrid, useRenderCellValue, EsSorting, - SearchResponse7, UseIndexDataReturnType, } from '../../../../components/data_grid'; -import { getErrorMessage } from '../../../../../../common/util/errors'; +import type { SearchResponse7 } from '../../../../../../common/types/es_client'; +import { extractErrorMessage } from '../../../../../../common/util/errors'; import { INDEX_STATUS } from '../../../common/analytics'; import { ml } from '../../../../services/ml_api_service'; @@ -94,7 +94,7 @@ export const useIndexData = ( setTableItems(docs); setStatus(INDEX_STATUS.LOADED); } catch (e) { - setErrorMessage(getErrorMessage(e)); + setErrorMessage(extractErrorMessage(e)); setStatus(INDEX_STATUS.ERROR); } }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx index ccac9a697210b..2e3a5d89367ce 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/classification_exploration/classification_exploration.tsx @@ -9,7 +9,6 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { ExplorationPageWrapper } from '../exploration_page_wrapper'; - import { EvaluatePanel } from './evaluate_panel'; interface Props { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx index 34ff36c59fa6c..84b44ef0d349f 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_page_wrapper/exploration_page_wrapper.tsx @@ -51,7 +51,6 @@ export const ExplorationPageWrapper: FC = ({ jobId, title, EvaluatePanel /> ); } - return ( <> {isLoadingJobConfig === true && jobConfig === undefined && } diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx index 8395a11bd6fda..eea579ef1d064 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/exploration_results_table.tsx @@ -28,6 +28,8 @@ import { INDEX_STATUS, SEARCH_SIZE, defaultSearchQuery, + getAnalysisType, + ANALYSIS_CONFIG_TYPE, } from '../../../../common'; import { getTaskStateBadge } from '../../../analytics_management/components/analytics_list/use_columns'; import { DATA_FRAME_TASK_STATE } from '../../../analytics_management/components/analytics_list/common'; @@ -36,6 +38,7 @@ import { ExplorationQueryBar } from '../exploration_query_bar'; import { IndexPatternPrompt } from '../index_pattern_prompt'; import { useExplorationResults } from './use_exploration_results'; +import { useMlKibana } from '../../../../../contexts/kibana'; const showingDocs = i18n.translate( 'xpack.ml.dataframe.analytics.explorationResults.documentsShownHelpText', @@ -70,18 +73,27 @@ export const ExplorationResultsTable: FC = React.memo( setEvaluateSearchQuery, title, }) => { + const { + services: { + mlServices: { mlApiServices }, + }, + } = useMlKibana(); const [searchQuery, setSearchQuery] = useState(defaultSearchQuery); useEffect(() => { setEvaluateSearchQuery(searchQuery); }, [JSON.stringify(searchQuery)]); + const analysisType = getAnalysisType(jobConfig.analysis); + const classificationData = useExplorationResults( indexPattern, jobConfig, searchQuery, - getToastNotifications() + getToastNotifications(), + mlApiServices ); + const docFieldsCount = classificationData.columnsWithCharts.length; const { columnsWithCharts, @@ -94,7 +106,6 @@ export const ExplorationResultsTable: FC = React.memo( if (jobConfig === undefined || classificationData === undefined) { return null; } - // if it's a searchBar syntax error leave the table visible so they can try again if (status === INDEX_STATUS.ERROR && !errorMessage.includes('failed to create query')) { return ( @@ -184,6 +195,7 @@ export const ExplorationResultsTable: FC = React.memo( {...classificationData} dataTestSubj="mlExplorationDataGrid" toastNotifications={getToastNotifications()} + analysisType={(analysisType as unknown) as ANALYSIS_CONFIG_TYPE} /> diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts index 8d53214d23d47..a56345017258e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/exploration_results_table/use_exploration_results.ts @@ -4,12 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { useEffect, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { EuiDataGridColumn } from '@elastic/eui'; import { CoreSetup } from 'src/core/public'; +import { i18n } from '@kbn/i18n'; +import { MlApiServices } from '../../../../../services/ml_api_service'; import { IndexPattern } from '../../../../../../../../../../src/plugins/data/public'; import { DataLoader } from '../../../../../datavisualizer/index_based/data_loader'; @@ -23,21 +25,26 @@ import { UseIndexDataReturnType, } from '../../../../../components/data_grid'; import { SavedSearchQuery } from '../../../../../contexts/ml'; - import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; import { - DEFAULT_RESULTS_FIELD, - FEATURE_IMPORTANCE, - TOP_CLASSES, -} from '../../../../common/constants'; + getPredictionFieldName, + getDefaultPredictionFieldName, +} from '../../../../../../../common/util/analytics_utils'; +import { FEATURE_IMPORTANCE, TOP_CLASSES } from '../../../../common/constants'; +import { DEFAULT_RESULTS_FIELD } from '../../../../../../../common/constants/data_frame_analytics'; import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; +import { isRegressionAnalysis } from '../../../../common/analytics'; +import { extractErrorMessage } from '../../../../../../../common/util/errors'; export const useExplorationResults = ( indexPattern: IndexPattern | undefined, jobConfig: DataFrameAnalyticsConfig | undefined, searchQuery: SavedSearchQuery, - toastNotifications: CoreSetup['notifications']['toasts'] + toastNotifications: CoreSetup['notifications']['toasts'], + mlApiServices: MlApiServices ): UseIndexDataReturnType => { + const [baseline, setBaseLine] = useState(); + const needsDestIndexFields = indexPattern !== undefined && indexPattern.title === jobConfig?.source.index[0]; @@ -52,7 +59,6 @@ export const useExplorationResults = ( ) ); } - const dataGrid = useDataGrid( columns, 25, @@ -107,16 +113,60 @@ export const useExplorationResults = ( jobConfig?.dest.index, JSON.stringify([searchQuery, dataGrid.visibleColumns]), ]); + const predictionFieldName = useMemo(() => { + if (jobConfig) { + return ( + getPredictionFieldName(jobConfig.analysis) ?? + getDefaultPredictionFieldName(jobConfig.analysis) + ); + } + return undefined; + }, [jobConfig]); + + const getAnalyticsBaseline = useCallback(async () => { + try { + if ( + jobConfig !== undefined && + jobConfig.analysis !== undefined && + isRegressionAnalysis(jobConfig.analysis) + ) { + const result = await mlApiServices.dataFrameAnalytics.getAnalyticsBaseline(jobConfig.id); + if (result?.baseline) { + setBaseLine(result.baseline); + } + } + } catch (e) { + const error = extractErrorMessage(e); + + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.ml.dataframe.analytics.explorationResults.baselineErrorMessageToast', + { + defaultMessage: 'An error occurred getting feature importance baseline', + } + ), + text: error, + }); + } + }, [mlApiServices, jobConfig]); + + useEffect(() => { + getAnalyticsBaseline(); + }, [jobConfig]); + const resultsField = jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD; const renderCellValue = useRenderCellValue( indexPattern, dataGrid.pagination, dataGrid.tableItems, - jobConfig?.dest.results_field ?? DEFAULT_RESULTS_FIELD + resultsField ); return { ...dataGrid, renderCellValue, + baseline, + predictionFieldName, + resultsField, }; }; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts index 24649ae5f1e71..151e5ea4e6feb 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_exploration/components/outlier_exploration/use_outlier_data.ts @@ -29,7 +29,8 @@ import { SavedSearchQuery } from '../../../../../contexts/ml'; import { getToastNotifications } from '../../../../../util/dependency_cache'; import { getIndexData, getIndexFields, DataFrameAnalyticsConfig } from '../../../../common'; -import { DEFAULT_RESULTS_FIELD, FEATURE_INFLUENCE } from '../../../../common/constants'; +import { FEATURE_INFLUENCE } from '../../../../common/constants'; +import { DEFAULT_RESULTS_FIELD } from '../../../../../../../common/constants/data_frame_analytics'; import { sortExplorationResultsFields, ML__ID_COPY } from '../../../../common/fields'; import { getFeatureCount, getOutlierScoreFieldName } from './common'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx index 60c699ba0d370..ce24892c9de45 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/action_clone/clone_action_name.tsx @@ -12,7 +12,7 @@ import { IIndexPattern } from 'src/plugins/data/common'; import { DeepReadonly } from '../../../../../../../common/types/common'; import { DataFrameAnalyticsConfig, isOutlierAnalysis } from '../../../../common'; import { isClassificationAnalysis, isRegressionAnalysis } from '../../../../common/analytics'; -import { DEFAULT_RESULTS_FIELD } from '../../../../common/constants'; +import { DEFAULT_RESULTS_FIELD } from '../../../../../../../common/constants/data_frame_analytics'; import { useMlKibana, useNavigateToPath } from '../../../../../contexts/kibana'; import { DEFAULT_NUM_TOP_FEATURE_IMPORTANCE_VALUES } from '../../hooks/use_create_analytics_form'; import { State } from '../../hooks/use_create_analytics_form/state'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx index 6d73340cc396a..0c3bff58c25cd 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/analytics_list.tsx @@ -99,13 +99,7 @@ export const DataFrameAnalyticsList: FC = ({ const [isInitialized, setIsInitialized] = useState(false); const [isSourceIndexModalVisible, setIsSourceIndexModalVisible] = useState(false); const [isLoading, setIsLoading] = useState(false); - const [filteredAnalytics, setFilteredAnalytics] = useState<{ - active: boolean; - items: DataFrameAnalyticsListRow[]; - }>({ - active: false, - items: [], - }); + const [filteredAnalytics, setFilteredAnalytics] = useState([]); const [searchQueryText, setSearchQueryText] = useState(''); const [analytics, setAnalytics] = useState([]); const [analyticsStats, setAnalyticsStats] = useState( @@ -129,12 +123,12 @@ export const DataFrameAnalyticsList: FC = ({ blockRefresh ); - const setQueryClauses = (queryClauses: any) => { + const updateFilteredItems = (queryClauses: any) => { if (queryClauses.length) { const filtered = filterAnalytics(analytics, queryClauses); - setFilteredAnalytics({ active: true, items: filtered }); + setFilteredAnalytics(filtered); } else { - setFilteredAnalytics({ active: false, items: [] }); + setFilteredAnalytics(analytics); } }; @@ -146,9 +140,9 @@ export const DataFrameAnalyticsList: FC = ({ if (query && query.ast !== undefined && query.ast.clauses !== undefined) { clauses = query.ast.clauses; } - setQueryClauses(clauses); + updateFilteredItems(clauses); } else { - setQueryClauses([]); + updateFilteredItems([]); } }; @@ -192,9 +186,9 @@ export const DataFrameAnalyticsList: FC = ({ isMlEnabledInSpace ); - const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings( - filteredAnalytics.active ? filteredAnalytics.items : analytics - ); + const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings< + DataFrameAnalyticsListRow + >(DataFrameAnalyticsListColumn.id, filteredAnalytics); // Before the analytics have been loaded for the first time, display the loading indicator only. // Otherwise a user would see 'No data frame analytics found' during the initial loading. diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts index 57eb9f6857053..052068c30b84c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_list/use_table_settings.ts @@ -8,7 +8,6 @@ import { useState } from 'react'; import { Direction, EuiBasicTableProps, EuiTableSortingType } from '@elastic/eui'; import sortBy from 'lodash/sortBy'; import get from 'lodash/get'; -import { DataFrameAnalyticsListColumn, DataFrameAnalyticsListRow } from './common'; const PAGE_SIZE = 10; const PAGE_SIZE_OPTIONS = [10, 25, 50]; @@ -19,37 +18,59 @@ const jobPropertyMap = { Type: 'job_type', }; -interface AnalyticsBasicTableSettings { +// Copying from EUI EuiBasicTable types as type is not correctly picked up for table's onChange +// Can be removed when https://github.com/elastic/eui/issues/4011 is addressed in EUI +export interface Criteria { + page?: { + index: number; + size: number; + }; + sort?: { + field: keyof T; + direction: Direction; + }; +} +export interface CriteriaWithPagination extends Criteria { + page: { + index: number; + size: number; + }; +} + +interface AnalyticsBasicTableSettings { pageIndex: number; pageSize: number; totalItemCount: number; hidePerPageOptions: boolean; - sortField: string; + sortField: keyof T; sortDirection: Direction; } -interface UseTableSettingsReturnValue { - onTableChange: EuiBasicTableProps['onChange']; - pageOfItems: DataFrameAnalyticsListRow[]; - pagination: EuiBasicTableProps['pagination']; +interface UseTableSettingsReturnValue { + onTableChange: EuiBasicTableProps['onChange']; + pageOfItems: T[]; + pagination: EuiBasicTableProps['pagination']; sorting: EuiTableSortingType; } -export function useTableSettings(items: DataFrameAnalyticsListRow[]): UseTableSettingsReturnValue { - const [tableSettings, setTableSettings] = useState({ +export function useTableSettings( + sortByField: keyof TypeOfItem, + items: TypeOfItem[] +): UseTableSettingsReturnValue { + const [tableSettings, setTableSettings] = useState>({ pageIndex: 0, pageSize: PAGE_SIZE, totalItemCount: 0, hidePerPageOptions: false, - sortField: DataFrameAnalyticsListColumn.id, + sortField: sortByField, sortDirection: 'asc', }); const getPageOfItems = ( - list: any[], + list: TypeOfItem[], index: number, size: number, - sortField: string, + sortField: keyof TypeOfItem, sortDirection: Direction ) => { list = sortBy(list, (item) => @@ -72,13 +93,10 @@ export function useTableSettings(items: DataFrameAnalyticsListRow[]): UseTableSe }; }; - const onTableChange = ({ + const onTableChange: EuiBasicTableProps['onChange'] = ({ page = { index: 0, size: PAGE_SIZE }, - sort = { field: DataFrameAnalyticsListColumn.id, direction: 'asc' }, - }: { - page?: { index: number; size: number }; - sort?: { field: string; direction: Direction }; - }) => { + sort = { field: sortByField, direction: 'asc' }, + }: CriteriaWithPagination) => { const { index, size } = page; const { field, direction } = sort; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx index 44a6572a3766c..7a366bb63420c 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/analytics_search_bar.tsx @@ -20,6 +20,68 @@ import { Value, DataFrameAnalyticsListRow, } from '../analytics_list/common'; +import { ModelItem } from '../models_management/models_list'; + +export function filterAnalyticsModels( + items: ModelItem[], + clauses: Array +) { + if (clauses.length === 0) { + return items; + } + + // keep count of the number of matches we make as we're looping over the clauses + // we only want to return items which match all clauses, i.e. each search term is ANDed + const matches: Record = items.reduce((p: Record, c) => { + p[c.model_id] = { + model: c, + count: 0, + }; + return p; + }, {}); + + clauses.forEach((c) => { + // the search term could be negated with a minus, e.g. -bananas + const bool = c.match === 'must'; + let ms = []; + + if (c.type === 'term') { + // filter term based clauses, e.g. bananas + // match on model_id and type + // if the term has been negated, AND the matches + if (bool === true) { + ms = items.filter( + (item) => + stringMatch(item.model_id, c.value) === bool || stringMatch(item.type, c.value) === bool + ); + } else { + ms = items.filter( + (item) => + stringMatch(item.model_id, c.value) === bool && stringMatch(item.type, c.value) === bool + ); + } + } else { + // filter other clauses, i.e. the filters for type + if (Array.isArray(c.value)) { + // type value is an array of string(s) e.g. c.value => ['classification'] + ms = items.filter((item) => { + return item.type !== undefined && (c.value as Value[]).includes(item.type); + }); + } else { + ms = items.filter((item) => item[c.field as keyof typeof item] === c.value); + } + } + + ms.forEach((j) => matches[j.model_id].count++); + }); + + // loop through the matches and return only those items which have match all the clauses + const filtered = Object.values(matches) + .filter((m) => (m && m.count) >= clauses.length) + .map((m) => m.model); + + return filtered; +} export function filterAnalytics( items: DataFrameAnalyticsListRow[], diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts index 3b901f5063eb1..2748764d7f46e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/analytics_search_bar/index.ts @@ -4,4 +4,4 @@ * you may not use this file except in compliance with the Elastic License. */ -export { AnalyticsSearchBar, filterAnalytics } from './analytics_search_bar'; +export { AnalyticsSearchBar, filterAnalytics, filterAnalyticsModels } from './analytics_search_bar'; diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx index 3104ec55c3a6d..338b6444671a6 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/components/models_management/models_list.tsx @@ -4,20 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { FC, useState, useCallback, useMemo } from 'react'; +import React, { FC, useState, useCallback, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { - Direction, + EuiBasicTable, EuiFlexGroup, EuiFlexItem, - EuiInMemoryTable, EuiTitle, EuiButton, - EuiSearchBarProps, + EuiSearchBar, EuiSpacer, EuiButtonIcon, EuiBadge, + SearchFilterConfig, } from '@elastic/eui'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; @@ -42,6 +42,8 @@ import { refreshAnalyticsList$, useRefreshAnalyticsList, } from '../../../../common'; +import { useTableSettings } from '../analytics_list/use_table_settings'; +import { filterAnalyticsModels, AnalyticsSearchBar } from '../analytics_search_bar'; type Stats = Omit; @@ -66,22 +68,41 @@ export const ModelsList: FC = () => { const { toasts } = useNotifications(); const [searchQueryText, setSearchQueryText] = useState(''); - - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(10); - const [sortField, setSortField] = useState(ModelsTableToConfigMapping.id); - const [sortDirection, setSortDirection] = useState('asc'); - + const [filteredModels, setFilteredModels] = useState([]); const [isLoading, setIsLoading] = useState(false); const [items, setItems] = useState([]); const [selectedModels, setSelectedModels] = useState([]); - const [modelsToDelete, setModelsToDelete] = useState([]); - const [itemIdToExpandedRowMap, setItemIdToExpandedRowMap] = useState>( {} ); + const updateFilteredItems = (queryClauses: any) => { + if (queryClauses.length) { + const filtered = filterAnalyticsModels(items, queryClauses); + setFilteredModels(filtered); + } else { + setFilteredModels(items); + } + }; + + const filterList = () => { + if (searchQueryText !== '') { + const query = EuiSearchBar.Query.parse(searchQueryText); + let clauses: any = []; + if (query && query.ast !== undefined && query.ast.clauses !== undefined) { + clauses = query.ast.clauses; + } + updateFilteredItems(clauses); + } else { + updateFilteredItems([]); + } + }; + + useEffect(() => { + filterList(); + }, [searchQueryText, items]); + /** * Fetches inference trained models. */ @@ -355,91 +376,51 @@ export const ModelsList: FC = () => { }, ]; - const pagination = { - initialPageIndex: pageIndex, - initialPageSize: pageSize, - totalItemCount: items.length, - pageSizeOptions: [10, 20, 50], - hidePerPageOptions: false, - }; + const filters: SearchFilterConfig[] = + inferenceTypesOptions && inferenceTypesOptions.length > 0 + ? [ + { + type: 'field_value_selection', + field: 'type', + name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', { + defaultMessage: 'Type', + }), + multiSelect: 'or', + options: inferenceTypesOptions, + }, + ] + : []; - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - const search: EuiSearchBarProps = { - query: searchQueryText, - onChange: (searchChange) => { - if (searchChange.error !== null) { - return false; - } - setSearchQueryText(searchChange.queryText); - return true; - }, - box: { - incremental: true, - }, - ...(inferenceTypesOptions && inferenceTypesOptions.length > 0 - ? { - filters: [ - { - type: 'field_value_selection', - field: 'type', - name: i18n.translate('xpack.ml.dataframe.analyticsList.typeFilter', { - defaultMessage: 'Type', - }), - multiSelect: 'or', - options: inferenceTypesOptions, - }, - ], - } - : {}), - ...(selectedModels.length > 0 - ? { - toolsLeft: ( - - - -
- -
-
-
- - - - - -
- ), - } - : {}), - }; + const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings( + ModelsTableToConfigMapping.id, + filteredModels + ); - const onTableChange: EuiInMemoryTable['onTableChange'] = ({ - page = { index: 0, size: 10 }, - sort = { field: ModelsTableToConfigMapping.id, direction: 'asc' }, - }) => { - const { index, size } = page; - setPageIndex(index); - setPageSize(size); - - const { field, direction } = sort; - setSortField(field); - setSortDirection(direction); - }; + const toolsLeft = ( + + + + +
+ +
+
+
+ + + + + +
+
+ ); const isSelectionAllowed = canDeleteDataFrameAnalytics; @@ -473,21 +454,31 @@ export const ModelsList: FC = () => {
- + {selectedModels.length > 0 && toolsLeft} + + + + + + columns={columns} hasActions={true} isExpandable={true} - itemIdToExpandedRowMap={itemIdToExpandedRowMap} isSelectable={false} - items={items} + items={pageOfItems} itemId={ModelsTableToConfigMapping.id} + itemIdToExpandedRowMap={itemIdToExpandedRowMap} loading={isLoading} - onTableChange={onTableChange} - pagination={pagination} - sorting={sorting} - search={search} + onChange={onTableChange} selection={selection} + pagination={pagination!} + sorting={sorting} + data-test-subj={isLoading ? 'mlModelsTable loading' : 'mlModelsTable loaded'} rowProps={(item) => ({ 'data-test-subj': `mlModelsTableRow row-${item.model_id}`, })} diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx index ac1c710e1d106..f833cf4708cec 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.test.tsx @@ -11,7 +11,6 @@ import { MlContext } from '../../../../../contexts/ml'; import { kibanaContextValueMock } from '../../../../../contexts/ml/__mocks__/kibana_context_value'; import { useCreateAnalyticsForm } from './use_create_analytics_form'; -import { getErrorMessage } from '../../../../../../../common/util/errors'; const getMountedHook = () => mountHook( @@ -21,28 +20,6 @@ const getMountedHook = () => ) ); -describe('getErrorMessage()', () => { - test('verify error message response formats', () => { - const customError1 = { - body: { statusCode: 403, error: 'Forbidden', message: 'the-error-message' }, - }; - const errorMessage1 = getErrorMessage(customError1); - expect(errorMessage1).toBe('Forbidden: the-error-message'); - - const customError2 = new Error('the-error-message'); - const errorMessage2 = getErrorMessage(customError2); - expect(errorMessage2).toBe('the-error-message'); - - const customError3 = { customErrorMessage: 'the-error-message' }; - const errorMessage3 = getErrorMessage(customError3); - expect(errorMessage3).toBe('{"customErrorMessage":"the-error-message"}'); - - const customError4 = { message: 'the-error-message' }; - const errorMessage4 = getErrorMessage(customError4); - expect(errorMessage4).toBe('the-error-message'); - }); -}); - describe('useCreateAnalyticsForm()', () => { test('initialization', () => { const { getLastHookValue } = getMountedHook(); diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts index 9612b9213d120..161dde51df43e 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/hooks/use_create_analytics_form/use_create_analytics_form.ts @@ -8,7 +8,7 @@ import { useReducer } from 'react'; import { i18n } from '@kbn/i18n'; -import { getErrorMessage } from '../../../../../../../common/util/errors'; +import { extractErrorMessage } from '../../../../../../../common/util/errors'; import { DeepReadonly } from '../../../../../../../common/types/common'; import { ml } from '../../../../../services/ml_api_service'; import { useMlContext } from '../../../../../contexts/ml'; @@ -115,7 +115,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { refresh(); } catch (e) { addRequestMessage({ - error: getErrorMessage(e), + error: extractErrorMessage(e), message: i18n.translate( 'xpack.ml.dataframe.analytics.create.errorCreatingDataFrameAnalyticsJob', { @@ -178,7 +178,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { }); } catch (e) { addRequestMessage({ - error: getErrorMessage(e), + error: extractErrorMessage(e), message: i18n.translate( 'xpack.ml.dataframe.analytics.create.createIndexPatternErrorMessage', { @@ -199,7 +199,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { ); } catch (e) { addRequestMessage({ - error: getErrorMessage(e), + error: extractErrorMessage(e), message: i18n.translate( 'xpack.ml.dataframe.analytics.create.errorGettingDataFrameAnalyticsList', { @@ -225,7 +225,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { }); } catch (e) { addRequestMessage({ - error: getErrorMessage(e), + error: extractErrorMessage(e), message: i18n.translate( 'xpack.ml.dataframe.analytics.create.errorGettingIndexPatternTitles', { @@ -260,7 +260,7 @@ export const useCreateAnalyticsForm = (): CreateAnalyticsFormProps => { refresh(); } catch (e) { addRequestMessage({ - error: getErrorMessage(e), + error: extractErrorMessage(e), message: i18n.translate( 'xpack.ml.dataframe.analytics.create.errorStartingDataFrameAnalyticsJob', { diff --git a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts index 9de859742438e..a21be83732613 100644 --- a/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts +++ b/x-pack/plugins/ml/public/application/data_frame_analytics/pages/analytics_management/services/analytics_service/delete_analytics.ts @@ -85,12 +85,11 @@ export const deleteAnalyticsAndDestIndex = async ( ); } if (status.destIndexDeleted?.error) { - const error = extractErrorMessage(status.destIndexDeleted.error); - toastNotificationService.displayDangerToast( + toastNotificationService.displayErrorToast( + status.destIndexDeleted.error, i18n.translate('xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexErrorMessage', { - defaultMessage: - 'An error occurred deleting destination index {destinationIndex}: {error}', - values: { destinationIndex, error }, + defaultMessage: 'An error occurred deleting destination index {destinationIndex}', + values: { destinationIndex }, }) ); } diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts index 34f86ffa18788..b36eccdde2798 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/data_loader/data_loader.ts @@ -80,7 +80,7 @@ export class DataLoader { earliest: number | undefined, latest: number | undefined, fields: FieldRequestConfig[], - interval?: string + interval?: number ): Promise { const stats = await ml.getVisualizerFieldStats({ indexPatternTitle: this._indexPatternTitle, diff --git a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx index 26ed3152058dd..bad1488166e23 100644 --- a/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx +++ b/x-pack/plugins/ml/public/application/datavisualizer/index_based/page.tsx @@ -348,7 +348,7 @@ export const Page: FC = () => { earliest, latest, existMetricFields, - aggInterval.expression + aggInterval.asMilliseconds() ); // Add the metric stats to the existing stats in the corresponding config. diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.test.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.test.js index d705e47a5e906..58adf3d892f66 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.test.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_chart_config_builder.test.js @@ -8,8 +8,6 @@ import mockAnomalyRecord from './__mocks__/mock_anomaly_record.json'; import mockDetectorsByJob from './__mocks__/mock_detectors_by_job.json'; import mockJobConfig from './__mocks__/mock_job_config.json'; -jest.mock('../../util/ml_error', () => class MLRequestFailure {}); - jest.mock('../../services/job_service', () => ({ mlJobService: { getJob() { diff --git a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js index 712b64af2db80..a6fda86f27a7c 100644 --- a/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js +++ b/x-pack/plugins/ml/public/application/explorer/explorer_charts/explorer_charts_container_service.js @@ -111,7 +111,7 @@ export const anomalyDataChange = function ( // Query 1 - load the raw metric data. function getMetricData(config, range) { - const { jobId, detectorIndex, entityFields, interval } = config; + const { jobId, detectorIndex, entityFields, bucketSpanSeconds } = config; const job = mlJobService.getJob(jobId); @@ -122,14 +122,14 @@ export const anomalyDataChange = function ( return mlResultsService .getMetricData( config.datafeedConfig.indices, - config.entityFields, + entityFields, datafeedQuery, config.metricFunction, config.metricFieldName, config.timeField, range.min, range.max, - config.interval + bucketSpanSeconds * 1000 ) .toPromise(); } else { @@ -175,7 +175,14 @@ export const anomalyDataChange = function ( }; return mlResultsService - .getModelPlotOutput(jobId, detectorIndex, criteriaFields, range.min, range.max, interval) + .getModelPlotOutput( + jobId, + detectorIndex, + criteriaFields, + range.min, + range.max, + bucketSpanSeconds * 1000 + ) .toPromise() .then((resp) => { // Return data in format required by the explorer charts. @@ -218,7 +225,7 @@ export const anomalyDataChange = function ( [config.jobId], range.min, range.max, - config.interval, + config.bucketSpanSeconds * 1000, 1, MAX_SCHEDULED_EVENTS ) @@ -252,7 +259,7 @@ export const anomalyDataChange = function ( config.timeField, range.min, range.max, - config.interval + config.bucketSpanSeconds * 1000 ); } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js index 9d0082ffcb568..bd781d32a6b06 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_job_flyout.js @@ -7,6 +7,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; import { cloneDeep, isEqual, pick } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, EuiButtonEmpty, @@ -28,8 +30,6 @@ import { loadFullJob } from '../utils'; import { validateModelMemoryLimit, validateGroupNames, isValidCustomUrls } from '../validate_job'; import { toastNotificationServiceProvider } from '../../../../services/toast_notification_service'; import { withKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; import { collapseLiteralStrings } from '../../../../../../shared_imports'; import { DATAFEED_STATE } from '../../../../../../common/constants/states'; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js index 5030c48a4e367..adcc576c5e356 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/edit_job_flyout/edit_utils.js @@ -6,9 +6,9 @@ import { difference } from 'lodash'; import { getNewJobLimits } from '../../../../services/ml_server_info'; -import { mlJobService } from '../../../../services/job_service'; import { processCreatedBy } from '../../../../../../common/util/job_utils'; import { getSavedObjectsClient } from '../../../../util/dependency_cache'; +import { ml } from '../../../../services/ml_api_service'; export function saveJob(job, newJobData, finish) { return new Promise((resolve, reject) => { @@ -41,14 +41,9 @@ export function saveJob(job, newJobData, finish) { // if anything has changed, post the changes if (Object.keys(jobData).length) { - mlJobService - .updateJob(job.job_id, jobData) - .then((resp) => { - if (resp.success) { - saveDatafeedWrapper(); - } else { - reject(resp); - } + ml.updateJob({ jobId: job.job_id, job: jobData }) + .then(() => { + saveDatafeedWrapper(); }) .catch((error) => { reject(error); @@ -59,17 +54,17 @@ export function saveJob(job, newJobData, finish) { }); } -function saveDatafeed(datafeedData, job) { +function saveDatafeed(datafeedConfig, job) { return new Promise((resolve, reject) => { - if (Object.keys(datafeedData).length) { + if (Object.keys(datafeedConfig).length) { const datafeedId = job.datafeed_config.datafeed_id; - mlJobService.updateDatafeed(datafeedId, datafeedData).then((resp) => { - if (resp.success) { + ml.updateDatafeed({ datafeedId, datafeedConfig }) + .then(() => { resolve(); - } else { - reject(resp); - } - }); + }) + .catch((error) => { + reject(error); + }); } else { resolve(); } diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js index f73dde69a3d4c..a379f49a83159 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/multi_job_actions/group_selector/group_selector.js @@ -6,6 +6,8 @@ import PropTypes from 'prop-types'; import React, { Component } from 'react'; +import { i18n } from '@kbn/i18n'; +import { FormattedMessage } from '@kbn/i18n/react'; import { EuiButton, @@ -25,9 +27,7 @@ import { ml } from '../../../../../services/ml_api_service'; import { checkPermission } from '../../../../../capabilities/check_capabilities'; import { GroupList } from './group_list'; import { NewGroupInput } from './new_group_input'; -import { mlMessageBarService } from '../../../../../components/messagebar'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; +import { getToastNotificationService } from '../../../../../services/toast_notification_service'; function createSelectedGroups(jobs, groups) { const jobIds = jobs.map((j) => j.id); @@ -160,7 +160,7 @@ export class GroupSelector extends Component { // check success of each job update if (resp.hasOwnProperty(jobId)) { if (resp[jobId].success === false) { - mlMessageBarService.notify.error(resp[jobId].error); + getToastNotificationService().displayErrorToast(resp[jobId].error); success = false; } } @@ -175,7 +175,7 @@ export class GroupSelector extends Component { } }) .catch((error) => { - mlMessageBarService.notify.error(error); + getToastNotificationService().displayErrorToast(error); console.error(error); }); }; diff --git a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js index 913727bda67df..21824aac18cdd 100644 --- a/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js +++ b/x-pack/plugins/ml/public/application/jobs/jobs_list/components/utils.js @@ -5,17 +5,19 @@ */ import { each } from 'lodash'; -import { mlMessageBarService } from '../../../components/messagebar'; +import { i18n } from '@kbn/i18n'; import rison from 'rison-node'; import { mlJobService } from '../../../services/job_service'; -import { toastNotificationServiceProvider } from '../../../services/toast_notification_service'; -import { ml } from '../../../services/ml_api_service'; +import { + getToastNotificationService, + toastNotificationServiceProvider, +} from '../../../services/toast_notification_service'; import { getToastNotifications } from '../../../util/dependency_cache'; +import { ml } from '../../../services/ml_api_service'; import { stringMatch } from '../../../util/string_utils'; import { JOB_STATE, DATAFEED_STATE } from '../../../../../common/constants/states'; import { parseInterval } from '../../../../../common/util/parse_interval'; -import { i18n } from '@kbn/i18n'; import { mlCalendarService } from '../../../services/calendar_service'; export function loadFullJob(jobId) { @@ -60,7 +62,6 @@ export function forceStartDatafeeds(jobs, start, end, finish = () => {}) { finish(); }) .catch((error) => { - mlMessageBarService.notify.error(error); const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.jobsList.startJobErrorMessage', { @@ -81,7 +82,6 @@ export function stopDatafeeds(jobs, finish = () => {}) { finish(); }) .catch((error) => { - mlMessageBarService.notify.error(error); const toastNotifications = getToastNotifications(); toastNotifications.addDanger( i18n.translate('xpack.ml.jobsList.stopJobErrorMessage', { @@ -219,9 +219,8 @@ export async function cloneJob(jobId) { window.location.href = '#/jobs/new_job'; } catch (error) { - mlMessageBarService.notify.error(error); - const toastNotifications = getToastNotifications(); - toastNotifications.addDanger( + getToastNotificationService().displayErrorToast( + error, i18n.translate('xpack.ml.jobsList.cloneJobErrorMessage', { defaultMessage: 'Could not clone {jobId}. Job could not be found', values: { jobId }, @@ -239,13 +238,11 @@ export function closeJobs(jobs, finish = () => {}) { finish(); }) .catch((error) => { - mlMessageBarService.notify.error(error); - const toastNotifications = getToastNotifications(); - toastNotifications.addDanger( + getToastNotificationService().displayErrorToast( + error, i18n.translate('xpack.ml.jobsList.closeJobErrorMessage', { defaultMessage: 'Jobs failed to close', - }), - error + }) ); finish(); }); @@ -260,13 +257,11 @@ export function deleteJobs(jobs, finish = () => {}) { finish(); }) .catch((error) => { - mlMessageBarService.notify.error(error); - const toastNotifications = getToastNotifications(); - toastNotifications.addDanger( + getToastNotificationService().displayErrorToast( + error, i18n.translate('xpack.ml.jobsList.deleteJobErrorMessage', { defaultMessage: 'Jobs failed to delete', - }), - error + }) ); finish(); }); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts index 0011c88d2b524..6671aaa83abe0 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/job_creator/util/model_memory_estimator.ts @@ -23,7 +23,7 @@ import { useEffect, useMemo } from 'react'; import { DEFAULT_MODEL_MEMORY_LIMIT } from '../../../../../../../common/constants/new_job'; import { ml } from '../../../../../services/ml_api_service'; import { JobValidator, VALIDATION_DELAY_MS } from '../../job_validator/job_validator'; -import { ErrorResponse } from '../../../../../../../common/types/errors'; +import { MLHttpFetchError, MLResponseError } from '../../../../../../../common/util/errors'; import { useMlKibana } from '../../../../../contexts/kibana'; import { JobCreator } from '../job_creator'; @@ -36,10 +36,10 @@ export const modelMemoryEstimatorProvider = ( jobValidator: JobValidator ) => { const modelMemoryCheck$ = new Subject(); - const error$ = new Subject(); + const error$ = new Subject>(); return { - get error$(): Observable { + get error$(): Observable> { return error$.asObservable(); }, get updates$(): Observable { @@ -64,7 +64,7 @@ export const modelMemoryEstimatorProvider = ( catchError((error) => { // eslint-disable-next-line no-console console.error('Model memory limit could not be calculated', error.body); - error$.next(error.body); + error$.next(error); // fallback to the default in case estimation failed return of(DEFAULT_MODEL_MEMORY_LIMIT); }) @@ -120,7 +120,8 @@ export const useModelMemoryEstimator = ( title: i18n.translate('xpack.ml.newJob.wizard.estimateModelMemoryError', { defaultMessage: 'Model memory limit could not be calculated', }), - text: error.message, + text: + error.body.attributes?.body.error.caused_by?.reason || error.body.message || undefined, }); }) ); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts index 2b250b9622286..af31df863ab76 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/results_loader.ts @@ -161,7 +161,7 @@ export class ResultsLoader { [], this._lastModelTimeStamp, this._jobCreator.end, - `${this._chartInterval.getInterval().asMilliseconds()}ms`, + this._chartInterval.getInterval().asMilliseconds(), agg.mlModelPlotAgg ) .toPromise(); @@ -211,7 +211,7 @@ export class ResultsLoader { [this._jobCreator.jobId], this._jobCreator.start, this._jobCreator.end, - `${this._chartInterval.getInterval().asMilliseconds()}ms`, + this._chartInterval.getInterval().asMilliseconds(), 1 ); @@ -233,7 +233,7 @@ export class ResultsLoader { this._jobCreator.jobId, this._jobCreator.start, this._jobCreator.end, - `${this._chartInterval.getInterval().asMilliseconds()}ms`, + this._chartInterval.getInterval().asMilliseconds(), this._detectorSplitFieldFilters ); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts index 51c396518c851..02a6f47bed6c9 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/common/results_loader/searches.ts @@ -32,7 +32,7 @@ export function getScoresByRecord( jobId: string, earliestMs: number, latestMs: number, - interval: string, + intervalMs: number, firstSplitField: SplitFieldWithValue | null ): Promise { return new Promise((resolve, reject) => { @@ -104,7 +104,7 @@ export function getScoresByRecord( byTime: { date_histogram: { field: 'timestamp', - interval, + fixed_interval: `${intervalMs}ms`, min_doc_count: 1, extended_bounds: { min: earliestMs, diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts index 0ec3b609b604f..a87ba4c29baa9 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/bucket_span_estimator/estimate_bucket_span.ts @@ -15,7 +15,7 @@ import { } from '../../../../../common/job_creator'; import { ml, BucketSpanEstimatorData } from '../../../../../../../services/ml_api_service'; import { useMlContext } from '../../../../../../../contexts/ml'; -import { mlMessageBarService } from '../../../../../../../components/messagebar'; +import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; export enum ESTIMATE_STATUS { NOT_RUNNING, @@ -68,7 +68,7 @@ export function useEstimateBucketSpan() { const { name, error, message } = await ml.estimateBucketSpan(data); setStatus(ESTIMATE_STATUS.NOT_RUNNING); if (error === true) { - mlMessageBarService.notify.error(message); + getToastNotificationService().displayErrorToast(message); } else { jobCreator.bucketSpan = name; jobCreatorUpdate(); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx index cbbddb5bbc5b8..da2e5cc0e63d9 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/categorization_view/metric_selection.tsx @@ -6,7 +6,7 @@ import React, { FC, useContext, useEffect, useState } from 'react'; import { EuiHorizontalRule } from '@elastic/eui'; -import { mlMessageBarService } from '../../../../../../../components/messagebar'; +import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; import { JobCreatorContext } from '../../../job_creator_context'; import { CategorizationJobCreator } from '../../../../../common/job_creator'; @@ -94,7 +94,7 @@ export const CategorizationDetectors: FC = ({ setIsValid }) => { setFieldExamples(null); setValidationChecks([]); setOverallValidStatus(CATEGORY_EXAMPLES_VALIDATION_STATUS.INVALID); - mlMessageBarService.notify.error(error); + getToastNotificationService().displayErrorToast(error); } } else { setFieldExamples(null); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx index 684cb5b4e0dda..762d18a5367f1 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection.tsx @@ -15,7 +15,7 @@ import { AggFieldPair } from '../../../../../../../../../common/types/fields'; import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; import { MetricSelector } from './metric_selector'; import { ChartGrid } from './chart_grid'; -import { mlMessageBarService } from '../../../../../../../components/messagebar'; +import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; interface Props { setIsValid: (na: boolean) => void; @@ -109,7 +109,7 @@ export const MultiMetricDetectors: FC = ({ setIsValid }) => { .loadFieldExampleValues(splitField) .then(setFieldValues) .catch((error) => { - mlMessageBarService.notify.error(error); + getToastNotificationService().displayErrorToast(error); }); } else { setFieldValues([]); @@ -138,7 +138,7 @@ export const MultiMetricDetectors: FC = ({ setIsValid }) => { ); setLineChartsData(resp); } catch (error) { - mlMessageBarService.notify.error(error); + getToastNotificationService().displayErrorToast(error); setLineChartsData([]); } setLoadingData(false); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx index f39a316440e74..cc0fbf2fc0a04 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/multi_metric_view/metric_selection_summary.tsx @@ -12,7 +12,7 @@ import { Results, ModelItem, Anomaly } from '../../../../../common/results_loade import { LineChartData } from '../../../../../common/chart_loader'; import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; import { ChartGrid } from './chart_grid'; -import { mlMessageBarService } from '../../../../../../../components/messagebar'; +import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; export const MultiMetricDetectorsSummary: FC = () => { const { jobCreator: jc, chartLoader, resultsLoader, chartInterval } = useContext( @@ -43,7 +43,7 @@ export const MultiMetricDetectorsSummary: FC = () => { const tempFieldValues = await chartLoader.loadFieldExampleValues(jobCreator.splitField); setFieldValues(tempFieldValues); } catch (error) { - mlMessageBarService.notify.error(error); + getToastNotificationService().displayErrorToast(error); } } })(); @@ -75,7 +75,7 @@ export const MultiMetricDetectorsSummary: FC = () => { ); setLineChartsData(resp); } catch (error) { - mlMessageBarService.notify.error(error); + getToastNotificationService().displayErrorToast(error); setLineChartsData({}); } setLoadingData(false); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx index e5f5ba48900d9..46f91550f6e32 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection.tsx @@ -17,7 +17,7 @@ import { getChartSettings, defaultChartSettings } from '../../../charts/common/s import { MetricSelector } from './metric_selector'; import { SplitFieldSelector } from '../split_field'; import { ChartGrid } from './chart_grid'; -import { mlMessageBarService } from '../../../../../../../components/messagebar'; +import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; interface Props { setIsValid: (na: boolean) => void; @@ -159,7 +159,7 @@ export const PopulationDetectors: FC = ({ setIsValid }) => { setLineChartsData(resp); } catch (error) { - mlMessageBarService.notify.error(error); + getToastNotificationService().displayErrorToast(error); setLineChartsData([]); } setLoadingData(false); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx index 06f7092e8ac06..c32cc6ecc445a 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/population_view/metric_selection_summary.tsx @@ -15,7 +15,7 @@ import { LineChartData } from '../../../../../common/chart_loader'; import { Field, AggFieldPair } from '../../../../../../../../../common/types/fields'; import { getChartSettings, defaultChartSettings } from '../../../charts/common/settings'; import { ChartGrid } from './chart_grid'; -import { mlMessageBarService } from '../../../../../../../components/messagebar'; +import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; type DetectorFieldValues = Record; @@ -81,7 +81,7 @@ export const PopulationDetectorsSummary: FC = () => { setLineChartsData(resp); } catch (error) { - mlMessageBarService.notify.error(error); + getToastNotificationService().displayErrorToast(error); setLineChartsData({}); } setLoadingData(false); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx index f04b63f47789e..5844e59225ab5 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection.tsx @@ -13,7 +13,7 @@ import { newJobCapsService } from '../../../../../../../services/new_job_capabil import { AggFieldPair } from '../../../../../../../../../common/types/fields'; import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; import { getChartSettings } from '../../../charts/common/settings'; -import { mlMessageBarService } from '../../../../../../../components/messagebar'; +import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; interface Props { setIsValid: (na: boolean) => void; @@ -93,7 +93,7 @@ export const SingleMetricDetectors: FC = ({ setIsValid }) => { setLineChartData(resp); } } catch (error) { - mlMessageBarService.notify.error(error); + getToastNotificationService().displayErrorToast(error); setLineChartData({}); } setLoadingData(false); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx index 85fb5890307ba..ae019ee1bbf84 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/pick_fields_step/components/single_metric_view/metric_selection_summary.tsx @@ -11,7 +11,7 @@ import { Results, ModelItem, Anomaly } from '../../../../../common/results_loade import { LineChartData } from '../../../../../common/chart_loader'; import { AnomalyChart, CHART_TYPE } from '../../../charts/anomaly_chart'; import { getChartSettings } from '../../../charts/common/settings'; -import { mlMessageBarService } from '../../../../../../../components/messagebar'; +import { getToastNotificationService } from '../../../../../../../services/toast_notification_service'; const DTR_IDX = 0; @@ -63,7 +63,7 @@ export const SingleMetricDetectorsSummary: FC = () => { setLineChartData(resp); } } catch (error) { - mlMessageBarService.notify.error(error); + getToastNotificationService().displayErrorToast(error); setLineChartData({}); } setLoadingData(false); diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx index 2e7cc9c413a25..82a023cd1779b 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/components/post_save_options/post_save_options.tsx @@ -10,7 +10,7 @@ import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { JobRunner } from '../../../../../common/job_runner'; import { useMlKibana } from '../../../../../../../contexts/kibana'; -import { getErrorMessage } from '../../../../../../../../../common/util/errors'; +import { extractErrorMessage } from '../../../../../../../../../common/util/errors'; // @ts-ignore import { CreateWatchFlyout } from '../../../../../../jobs_list/components/create_watch_flyout/index'; @@ -70,7 +70,7 @@ export const PostSaveOptions: FC = ({ jobRunner }) => { defaultMessage: `Error starting job`, } ), - text: getErrorMessage(error), + text: extractErrorMessage(error), }); } } diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx index 24d7fb9fc2a40..3000ce8449138 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/summary_step/summary.tsx @@ -22,13 +22,13 @@ import { JobCreatorContext } from '../job_creator_context'; import { JobRunner } from '../../../common/job_runner'; import { mlJobService } from '../../../../../services/job_service'; import { JsonEditorFlyout, EDITOR_MODE } from '../common/json_editor_flyout'; -import { getErrorMessage } from '../../../../../../../common/util/errors'; import { isSingleMetricJobCreator, isAdvancedJobCreator } from '../../../common/job_creator'; import { JobDetails } from './components/job_details'; import { DatafeedDetails } from './components/datafeed_details'; import { DetectorChart } from './components/detector_chart'; import { JobProgress } from './components/job_progress'; import { PostSaveOptions } from './components/post_save_options'; +import { toastNotificationServiceProvider } from '../../../../../services/toast_notification_service'; import { convertToAdvancedJob, resetJob, @@ -72,15 +72,7 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => const jr = await jobCreator.createAndStartJob(); setJobRunner(jr); } catch (error) { - // catch and display all job creation errors - const { toasts } = notifications; - toasts.addDanger({ - title: i18n.translate('xpack.ml.newJob.wizard.summaryStep.createJobError', { - defaultMessage: `Job creation error`, - }), - text: getErrorMessage(error), - }); - setCreatingJob(false); + handleJobCreationError(error); } } @@ -91,18 +83,21 @@ export const SummaryStep: FC = ({ setCurrentStep, isCurrentStep }) => await jobCreator.createDatafeed(); advancedStartDatafeed(jobCreator, navigateToPath); } catch (error) { - // catch and display all job creation errors - const { toasts } = notifications; - toasts.addDanger({ - title: i18n.translate('xpack.ml.newJob.wizard.summaryStep.createJobError', { - defaultMessage: `Job creation error`, - }), - text: getErrorMessage(error), - }); - setCreatingJob(false); + handleJobCreationError(error); } } + function handleJobCreationError(error: any) { + const { displayErrorToast } = toastNotificationServiceProvider(notifications.toasts); + displayErrorToast( + error, + i18n.translate('xpack.ml.newJob.wizard.summaryStep.createJobError', { + defaultMessage: `Job creation error`, + }) + ); + setCreatingJob(false); + } + function viewResults() { const url = mlJobService.createResultsUrl( [jobCreator.jobId], diff --git a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx index 19b89ffec02ac..3bde32f40eeb5 100644 --- a/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx +++ b/x-pack/plugins/ml/public/application/jobs/new_job/pages/components/validation_step/validation.tsx @@ -8,7 +8,7 @@ import React, { Fragment, FC, useContext, useState, useEffect } from 'react'; import { WizardNav } from '../wizard_nav'; import { WIZARD_STEPS, StepProps } from '../step_types'; import { JobCreatorContext } from '../job_creator_context'; -import { mlJobService } from '../../../../../services/job_service'; +import { ml } from '../../../../../services/ml_api_service'; import { ValidateJob } from '../../../../../components/validate_job'; import { JOB_TYPE } from '../../../../../../../common/constants/new_job'; @@ -66,7 +66,7 @@ export const ValidationStep: FC = ({ setCurrentStep, isCurrentStep }) ) { if (management !== undefined) { - management.sections.section.insightsAndAlerting.registerApp({ + return management.sections.section.insightsAndAlerting.registerApp({ id: 'jobsListLink', title: i18n.translate('xpack.ml.management.jobsListTitle', { defaultMessage: 'Machine Learning Jobs', diff --git a/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts index 2bdb758be874c..3ee50a4759006 100644 --- a/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts +++ b/x-pack/plugins/ml/public/application/services/anomaly_timeline_service.ts @@ -180,7 +180,7 @@ export class AnomalyTimelineService { // Pass the interval in seconds as the swim lane relies on a fixed number of seconds between buckets // which wouldn't be the case if e.g. '1M' was used. - const interval = `${swimlaneBucketInterval.asSeconds()}s`; + const intervalMs = swimlaneBucketInterval.asMilliseconds(); let response; if (viewBySwimlaneFieldName === VIEW_BY_JOB_LABEL) { @@ -190,7 +190,7 @@ export class AnomalyTimelineService { jobIds, searchBounds.min.valueOf(), searchBounds.max.valueOf(), - interval, + intervalMs, perPage, fromPage ); @@ -201,7 +201,7 @@ export class AnomalyTimelineService { fieldValues, searchBounds.min.valueOf(), searchBounds.max.valueOf(), - interval, + intervalMs, swimlaneLimit, perPage, fromPage, @@ -269,7 +269,7 @@ export class AnomalyTimelineService { selectedJobIds, earliestMs, latestMs, - this.getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth).asSeconds() + 's', + this.getSwimlaneBucketInterval(selectedJobs, swimlaneContainerWidth).asMilliseconds(), swimlaneLimit ); return Object.keys(resp.results); diff --git a/x-pack/plugins/ml/public/application/services/forecast_service.d.ts b/x-pack/plugins/ml/public/application/services/forecast_service.d.ts index 9eff86c753da9..e30790c57966b 100644 --- a/x-pack/plugins/ml/public/application/services/forecast_service.d.ts +++ b/x-pack/plugins/ml/public/application/services/forecast_service.d.ts @@ -25,7 +25,7 @@ export const mlForecastService: { entityFields: any[], earliestMs: number, latestMs: number, - interval: string, + intervalMs: number, aggType: any ) => Observable; diff --git a/x-pack/plugins/ml/public/application/services/forecast_service.js b/x-pack/plugins/ml/public/application/services/forecast_service.js index 57e50387a03ab..c13e265b4655c 100644 --- a/x-pack/plugins/ml/public/application/services/forecast_service.js +++ b/x-pack/plugins/ml/public/application/services/forecast_service.js @@ -153,7 +153,7 @@ function getForecastData( entityFields, earliestMs, latestMs, - interval, + intervalMs, aggType ) { // Extract the partition, by, over fields on which to filter. @@ -257,7 +257,7 @@ function getForecastData( times: { date_histogram: { field: 'timestamp', - interval: interval, + fixed_interval: `${intervalMs}ms`, min_doc_count: 1, }, aggs: { diff --git a/x-pack/plugins/ml/public/application/services/job_service.js b/x-pack/plugins/ml/public/application/services/job_service.js index 640f63617b7d4..dfa1b5f4e68cd 100644 --- a/x-pack/plugins/ml/public/application/services/job_service.js +++ b/x-pack/plugins/ml/public/application/services/job_service.js @@ -14,15 +14,13 @@ import { i18n } from '@kbn/i18n'; import { ml } from './ml_api_service'; -import { mlMessageBarService } from '../components/messagebar'; -import { getToastNotifications } from '../util/dependency_cache'; +import { getToastNotificationService } from '../services/toast_notification_service'; import { isWebUrl } from '../util/url_utils'; import { ML_DATA_PREVIEW_COUNT } from '../../../common/util/job_utils'; import { TIME_FORMAT } from '../../../common/constants/time_format'; import { parseInterval } from '../../../common/util/parse_interval'; -import { toastNotificationServiceProvider } from '../services/toast_notification_service'; import { validateTimeRange } from '../util/date_utils'; -const msgs = mlMessageBarService; + let jobs = []; let datafeedIds = {}; @@ -119,7 +117,6 @@ class JobService { return new Promise((resolve, reject) => { jobs = []; datafeedIds = {}; - ml.getJobs() .then((resp) => { jobs = resp.jobs; @@ -162,7 +159,6 @@ class JobService { } processBasicJobInfo(this, jobs); this.jobs = jobs; - createJobStats(this.jobs, this.jobStats); resolve({ jobs: this.jobs }); }); }) @@ -176,12 +172,7 @@ class JobService { function error(err) { console.log('jobService error getting list of jobs:', err); - msgs.notify.error( - i18n.translate('xpack.ml.jobService.jobsListCouldNotBeRetrievedErrorMessage', { - defaultMessage: 'Jobs list could not be retrieved', - }) - ); - msgs.notify.error('', err); + getToastNotificationService().displayErrorToast(err); reject({ jobs, err }); } }); @@ -248,7 +239,6 @@ class JobService { } } this.jobs = jobs; - createJobStats(this.jobs, this.jobStats); resolve({ jobs: this.jobs }); }); }) @@ -263,12 +253,7 @@ class JobService { function error(err) { console.log('JobService error getting list of jobs:', err); - msgs.notify.error( - i18n.translate('xpack.ml.jobService.jobsListCouldNotBeRetrievedErrorMessage', { - defaultMessage: 'Jobs list could not be retrieved', - }) - ); - msgs.notify.error('', err); + getToastNotificationService().displayErrorToast(err); reject({ jobs, err }); } }); @@ -280,9 +265,6 @@ class JobService { ml.getDatafeeds(sId) .then((resp) => { - // console.log('loadDatafeeds query response:', resp); - - // make deep copy of datafeeds const datafeeds = resp.datafeeds; // load datafeeds stats @@ -309,12 +291,7 @@ class JobService { function error(err) { console.log('loadDatafeeds error getting list of datafeeds:', err); - msgs.notify.error( - i18n.translate('xpack.ml.jobService.datafeedsListCouldNotBeRetrievedErrorMessage', { - defaultMessage: 'datafeeds list could not be retrieved', - }) - ); - msgs.notify.error('', err); + getToastNotificationService().displayErrorToast(err); reject({ jobs, err }); } }); @@ -415,62 +392,6 @@ class JobService { return tempJob; } - updateJob(jobId, job) { - // return the promise chain - return ml - .updateJob({ jobId, job }) - .then(() => { - return { success: true }; - }) - .catch((err) => { - // TODO - all the functions in here should just return the error and not - // display the toast, as currently both the component and this service display - // errors, so we end up with duplicate toasts. - const toastNotifications = getToastNotifications(); - const toastNotificationService = toastNotificationServiceProvider(toastNotifications); - toastNotificationService.displayErrorToast( - err, - i18n.translate('xpack.ml.jobService.updateJobErrorTitle', { - defaultMessage: 'Could not update job: {jobId}', - values: { jobId }, - }) - ); - - console.error('update job', err); - return { success: false, message: err }; - }); - } - - validateJob(obj) { - // return the promise chain - return ml - .validateJob(obj) - .then((messages) => { - return { success: true, messages }; - }) - .catch((err) => { - const toastNotifications = getToastNotifications(); - const toastNotificationService = toastNotificationServiceProvider(toastNotifications); - toastNotificationService.displayErrorToast( - err, - i18n.translate('xpack.ml.jobService.validateJobErrorTitle', { - defaultMessage: 'Job Validation Error', - }) - ); - - console.log('validate job', err); - return { - success: false, - messages: [ - { - status: 'error', - text: err.message, - }, - ], - }; - }); - } - // find a job based on the id getJob(jobId) { const job = find(jobs, (j) => { @@ -638,25 +559,6 @@ class JobService { }); } - updateDatafeed(datafeedId, datafeedConfig) { - return ml - .updateDatafeed({ datafeedId, datafeedConfig }) - .then((resp) => { - console.log('update datafeed', resp); - return { success: true }; - }) - .catch((err) => { - msgs.notify.error( - i18n.translate('xpack.ml.jobService.couldNotUpdateDatafeedErrorMessage', { - defaultMessage: 'Could not update datafeed: {datafeedId}', - values: { datafeedId }, - }) - ); - console.log('update datafeed', err); - return { success: false, message: err.message }; - }); - } - // start the datafeed for a given job // refresh the job state on start success startDatafeed(datafeedId, jobId, start, end) { @@ -677,49 +579,6 @@ class JobService { }) .catch((err) => { console.log('jobService error starting datafeed:', err); - msgs.notify.error( - i18n.translate('xpack.ml.jobService.couldNotStartDatafeedErrorMessage', { - defaultMessage: 'Could not start datafeed for {jobId}', - values: { jobId }, - }), - err - ); - reject(err); - }); - }); - } - - // stop the datafeed for a given job - // refresh the job state on stop success - stopDatafeed(datafeedId, jobId) { - return new Promise((resolve, reject) => { - ml.stopDatafeed({ - datafeedId, - }) - .then((resp) => { - resolve(resp); - }) - .catch((err) => { - console.log('jobService error stopping datafeed:', err); - const couldNotStopDatafeedErrorMessage = i18n.translate( - 'xpack.ml.jobService.couldNotStopDatafeedErrorMessage', - { - defaultMessage: 'Could not stop datafeed for {jobId}', - values: { jobId }, - } - ); - - if (err.statusCode === 500) { - msgs.notify.error(couldNotStopDatafeedErrorMessage); - msgs.notify.error( - i18n.translate('xpack.ml.jobService.requestMayHaveTimedOutErrorMessage', { - defaultMessage: - 'Request may have timed out and may still be running in the background.', - }) - ); - } else { - msgs.notify.error(couldNotStopDatafeedErrorMessage, err); - } reject(err); }); }); @@ -887,51 +746,6 @@ function processBasicJobInfo(localJobService, jobsList) { return processedJobsList; } -// Loop through the jobs list and create basic stats -// stats are displayed along the top of the Jobs Management page -function createJobStats(jobsList, jobStats) { - jobStats.activeNodes.value = 0; - jobStats.total.value = 0; - jobStats.open.value = 0; - jobStats.closed.value = 0; - jobStats.failed.value = 0; - jobStats.activeDatafeeds.value = 0; - - // object to keep track of nodes being used by jobs - const mlNodes = {}; - let failedJobs = 0; - - each(jobsList, (job) => { - if (job.state === 'opened') { - jobStats.open.value++; - } else if (job.state === 'closed') { - jobStats.closed.value++; - } else if (job.state === 'failed') { - failedJobs++; - } - - if (job.datafeed_config && job.datafeed_config.state === 'started') { - jobStats.activeDatafeeds.value++; - } - - if (job.node && job.node.name) { - mlNodes[job.node.name] = {}; - } - }); - - jobStats.total.value = jobsList.length; - - // // Only show failed jobs if it is non-zero - if (failedJobs) { - jobStats.failed.value = failedJobs; - jobStats.failed.show = true; - } else { - jobStats.failed.show = false; - } - - jobStats.activeNodes.value = Object.keys(mlNodes).length; -} - function createResultsUrlForJobs(jobsList, resultsPage, userTimeRange) { let from = undefined; let to = undefined; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts index 7de39d91047ef..434200d0383f5 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/data_frame_analytics.ts @@ -135,4 +135,10 @@ export const dataFrameAnalytics = { method: 'GET', }); }, + getAnalyticsBaseline(analyticsId: string) { + return http({ + path: `${basePath()}/data_frame/analytics/${analyticsId}/baseline`, + method: 'POST', + }); + }, }; diff --git a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts index 184039729f9ef..0deda455df771 100644 --- a/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts +++ b/x-pack/plugins/ml/public/application/services/ml_api_service/index.ts @@ -62,7 +62,7 @@ export interface BucketSpanEstimatorResponse { name: string; ms: number; error?: boolean; - message?: { msg: string } | string; + message?: string; } export interface GetTimeFieldRangeResponse { @@ -485,7 +485,7 @@ export function mlApiServicesProvider(httpService: HttpService) { earliest?: number; latest?: number; samplerShardSize?: number; - interval?: string; + interval?: number; fields?: FieldRequestConfig[]; maxExamples?: number; }) { diff --git a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts index 898ca8894cbda..22f878a337f51 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/result_service_rx.ts @@ -70,7 +70,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { timeFieldName: string, earliestMs: number, latestMs: number, - interval: string + intervalMs: number ): Observable { // Build the criteria to use in the bool filter part of the request. // Add criteria for the time range, entity fields, @@ -136,7 +136,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { byTime: { date_histogram: { field: timeFieldName, - interval, + fixed_interval: `${intervalMs}ms`, min_doc_count: 0, }, }, @@ -202,7 +202,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { criteriaFields: any[], earliestMs: number, latestMs: number, - interval: string, + intervalMs: number, aggType?: { min: any; max: any } ): Observable { const obj: ModelPlotOutput = { @@ -291,7 +291,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { times: { date_histogram: { field: 'timestamp', - interval, + fixed_interval: `${intervalMs}ms`, min_doc_count: 0, }, aggs: { @@ -446,7 +446,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { jobIds: string[] | undefined, earliestMs: number, latestMs: number, - interval: string, + intervalMs: number, maxJobs: number, maxEvents: number ): Observable { @@ -518,7 +518,7 @@ export function resultsServiceRxProvider(mlApiServices: MlApiServices) { times: { date_histogram: { field: 'timestamp', - interval, + fixed_interval: `${intervalMs}ms`, min_doc_count: 1, }, aggs: { diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts index b26528b76037b..aae0cb51aa81d 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.d.ts @@ -13,7 +13,7 @@ export function resultsServiceProvider( jobIds: string[], earliestMs: number, latestMs: number, - interval: string | number, + intervalMs: number, perPage?: number, fromPage?: number ): Promise; @@ -41,7 +41,7 @@ export function resultsServiceProvider( influencerFieldValues: string[], earliestMs: number, latestMs: number, - interval: string, + intervalMs: number, maxResults: number, perPage: number, fromPage: number, @@ -57,8 +57,25 @@ export function resultsServiceProvider( timeFieldName: string, earliestMs: number, latestMs: number, - interval: string | number + intervalMs: number + ): Promise; + getEventDistributionData( + index: string, + splitField: string, + filterField: string, + query: any, + metricFunction: string, // ES aggregation name + metricFieldName: string, + timeFieldName: string, + earliestMs: number, + latestMs: number, + intervalMs: number + ): Promise; + getRecordMaxScoreByTime( + jobId: string, + criteriaFields: any[], + earliestMs: number, + latestMs: number, + intervalMs: number ): Promise; - getEventDistributionData(): Promise; - getRecordMaxScoreByTime(): Promise; }; diff --git a/x-pack/plugins/ml/public/application/services/results_service/results_service.js b/x-pack/plugins/ml/public/application/services/results_service/results_service.js index ef00c9025763e..fd48845494dfd 100644 --- a/x-pack/plugins/ml/public/application/services/results_service/results_service.js +++ b/x-pack/plugins/ml/public/application/services/results_service/results_service.js @@ -28,7 +28,7 @@ export function resultsServiceProvider(mlApiServices) { // Pass an empty array or ['*'] to search over all job IDs. // Returned response contains a results property, with a key for job // which has results for the specified time range. - getScoresByBucket(jobIds, earliestMs, latestMs, interval, perPage = 10, fromPage = 1) { + getScoresByBucket(jobIds, earliestMs, latestMs, intervalMs, perPage = 10, fromPage = 1) { return new Promise((resolve, reject) => { const obj = { success: true, @@ -116,7 +116,7 @@ export function resultsServiceProvider(mlApiServices) { byTime: { date_histogram: { field: 'timestamp', - interval: interval, + fixed_interval: `${intervalMs}ms`, min_doc_count: 1, extended_bounds: { min: earliestMs, @@ -492,7 +492,7 @@ export function resultsServiceProvider(mlApiServices) { influencerFieldValues, earliestMs, latestMs, - interval, + intervalMs, maxResults = ANOMALY_SWIM_LANE_HARD_LIMIT, perPage = SWIM_LANE_DEFAULT_PAGE_SIZE, fromPage = 1, @@ -615,7 +615,7 @@ export function resultsServiceProvider(mlApiServices) { byTime: { date_histogram: { field: 'timestamp', - interval, + fixed_interval: `${intervalMs}ms`, min_doc_count: 1, }, aggs: { @@ -1033,7 +1033,7 @@ export function resultsServiceProvider(mlApiServices) { // Extra query object can be supplied, or pass null if no additional query. // Returned response contains a results property, which is an object // of document counts against time (epoch millis). - getEventRateData(index, query, timeFieldName, earliestMs, latestMs, interval) { + getEventRateData(index, query, timeFieldName, earliestMs, latestMs, intervalMs) { return new Promise((resolve, reject) => { const obj = { success: true, results: {} }; @@ -1074,7 +1074,7 @@ export function resultsServiceProvider(mlApiServices) { eventRate: { date_histogram: { field: timeFieldName, - interval: interval, + fixed_interval: `${intervalMs}ms`, min_doc_count: 0, extended_bounds: { min: earliestMs, @@ -1118,7 +1118,7 @@ export function resultsServiceProvider(mlApiServices) { timeFieldName, earliestMs, latestMs, - interval + intervalMs ) { return new Promise((resolve, reject) => { if (splitField === undefined) { @@ -1187,7 +1187,7 @@ export function resultsServiceProvider(mlApiServices) { byTime: { date_histogram: { field: timeFieldName, - interval: interval, + fixed_interval: `${intervalMs}ms`, min_doc_count: AGGREGATION_MIN_DOC_COUNT, }, aggs: { @@ -1277,7 +1277,7 @@ export function resultsServiceProvider(mlApiServices) { // criteria, time range, and aggregation interval. // criteriaFields parameter must be an array, with each object in the array having 'fieldName' // 'fieldValue' properties. - getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, interval) { + getRecordMaxScoreByTime(jobId, criteriaFields, earliestMs, latestMs, intervalMs) { return new Promise((resolve, reject) => { const obj = { success: true, @@ -1331,7 +1331,7 @@ export function resultsServiceProvider(mlApiServices) { times: { date_histogram: { field: 'timestamp', - interval: interval, + fixed_interval: `${intervalMs}ms`, min_doc_count: 1, }, aggs: { diff --git a/x-pack/plugins/ml/public/application/services/toast_notification_service.ts b/x-pack/plugins/ml/public/application/services/toast_notification_service.ts deleted file mode 100644 index 94381ae3f1e51..0000000000000 --- a/x-pack/plugins/ml/public/application/services/toast_notification_service.ts +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ToastInput, ToastOptions, ToastsStart } from 'kibana/public'; -import { ResponseError } from 'kibana/server'; -import { useMemo } from 'react'; -import { useNotifications } from '../contexts/kibana'; -import { - BoomResponse, - extractErrorProperties, - MLCustomHttpResponseOptions, - MLErrorObject, - MLResponseError, -} from '../../../common/util/errors'; - -export type ToastNotificationService = ReturnType; - -export function toastNotificationServiceProvider(toastNotifications: ToastsStart) { - return { - displayDangerToast(toastOrTitle: ToastInput, options?: ToastOptions) { - toastNotifications.addDanger(toastOrTitle, options); - }, - - displaySuccessToast(toastOrTitle: ToastInput, options?: ToastOptions) { - toastNotifications.addSuccess(toastOrTitle, options); - }, - - displayErrorToast(error: any, toastTitle: string) { - const errorObj = this.parseErrorMessage(error); - if (errorObj.fullErrorMessage !== undefined) { - // Provide access to the full error message via the 'See full error' button. - toastNotifications.addError(new Error(errorObj.fullErrorMessage), { - title: toastTitle, - toastMessage: errorObj.message, - }); - } else { - toastNotifications.addDanger( - { - title: toastTitle, - text: errorObj.message, - }, - { toastLifeTimeMs: 30000 } - ); - } - }, - - parseErrorMessage( - error: - | MLCustomHttpResponseOptions - | undefined - | string - | MLResponseError - ): MLErrorObject { - if ( - typeof error === 'object' && - 'response' in error && - typeof error.response === 'string' && - error.statusCode !== undefined - ) { - // MLResponseError which has been received back as part of a 'successful' response - // where the error was passed in a separate property in the response. - const wrapMlResponseError = { - body: error, - statusCode: error.statusCode, - }; - return extractErrorProperties(wrapMlResponseError); - } - - return extractErrorProperties( - error as - | MLCustomHttpResponseOptions - | undefined - | string - ); - }, - }; -} - -/** - * Hook to use {@link ToastNotificationService} in React components. - */ -export function useToastNotificationService(): ToastNotificationService { - const { toasts } = useNotifications(); - return useMemo(() => toastNotificationServiceProvider(toasts), []); -} diff --git a/x-pack/plugins/ml/public/application/services/toast_notification_service/index.ts b/x-pack/plugins/ml/public/application/services/toast_notification_service/index.ts new file mode 100644 index 0000000000000..1259f3b47d8e0 --- /dev/null +++ b/x-pack/plugins/ml/public/application/services/toast_notification_service/index.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { + ToastNotificationService, + toastNotificationServiceProvider, + useToastNotificationService, + getToastNotificationService, +} from './toast_notification_service'; diff --git a/x-pack/plugins/ml/public/application/services/toast_notification_service/toast_notification_service.ts b/x-pack/plugins/ml/public/application/services/toast_notification_service/toast_notification_service.ts new file mode 100644 index 0000000000000..61e0480313ebe --- /dev/null +++ b/x-pack/plugins/ml/public/application/services/toast_notification_service/toast_notification_service.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { ToastInput, ToastOptions, ToastsStart } from 'kibana/public'; +import { useMemo } from 'react'; +import { getToastNotifications } from '../../util/dependency_cache'; +import { useNotifications } from '../../contexts/kibana'; +import { + ErrorType, + extractErrorProperties, + MLRequestFailure, +} from '../../../../common/util/errors'; + +export type ToastNotificationService = ReturnType; + +export function toastNotificationServiceProvider(toastNotifications: ToastsStart) { + function displayDangerToast(toastOrTitle: ToastInput, options?: ToastOptions) { + toastNotifications.addDanger(toastOrTitle, options); + } + + function displayWarningToast(toastOrTitle: ToastInput, options?: ToastOptions) { + toastNotifications.addWarning(toastOrTitle, options); + } + + function displaySuccessToast(toastOrTitle: ToastInput, options?: ToastOptions) { + toastNotifications.addSuccess(toastOrTitle, options); + } + + function displayErrorToast(error: ErrorType, title?: string) { + const errorObj = extractErrorProperties(error); + toastNotifications.addError(new MLRequestFailure(errorObj, error), { + title: + title ?? + i18n.translate('xpack.ml.toastNotificationService.errorTitle', { + defaultMessage: 'An error has occurred', + }), + }); + } + + return { displayDangerToast, displayWarningToast, displaySuccessToast, displayErrorToast }; +} + +export function getToastNotificationService() { + const toastNotifications = getToastNotifications(); + return toastNotificationServiceProvider(toastNotifications); +} + +/** + * Hook to use {@link ToastNotificationService} in React components. + */ +export function useToastNotificationService(): ToastNotificationService { + const { toasts } = useNotifications(); + return useMemo(() => toastNotificationServiceProvider(toasts), []); +} diff --git a/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js b/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js index 50777485903d2..e0c7a4db6e898 100644 --- a/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js +++ b/x-pack/plugins/ml/public/application/settings/calendars/list/delete_calendars.js @@ -7,7 +7,7 @@ import { getToastNotifications } from '../../../util/dependency_cache'; import { ml } from '../../../services/ml_api_service'; import { i18n } from '@kbn/i18n'; -import { getErrorMessage } from '../../../../../common/util/errors'; +import { extractErrorMessage } from '../../../../../common/util/errors'; export async function deleteCalendars(calendarsToDelete, callback) { if (calendarsToDelete === undefined || calendarsToDelete.length === 0) { @@ -47,7 +47,7 @@ export async function deleteCalendars(calendarsToDelete, callback) { }, } ), - text: getErrorMessage(error), + text: extractErrorMessage(error), }); } } diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts index d1e959b33e5dc..5149fecb0ec26 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseries_search_service.ts @@ -29,7 +29,7 @@ function getMetricData( entityFields: EntityField[], earliestMs: number, latestMs: number, - interval: string + intervalMs: number ): Observable { if ( isModelPlotChartableForDetector(job, detectorIndex) && @@ -76,7 +76,7 @@ function getMetricData( criteriaFields, earliestMs, latestMs, - interval + intervalMs ); } else { const obj: ModelPlotOutput = { @@ -96,7 +96,7 @@ function getMetricData( chartConfig.timeField, earliestMs, latestMs, - interval + intervalMs ) .pipe( map((resp) => { diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js index 0e99d64cf202f..7d173e161a1cb 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer.js @@ -629,7 +629,7 @@ export class TimeSeriesExplorer extends React.Component { nonBlankEntities, searchBounds.min.valueOf(), searchBounds.max.valueOf(), - stateUpdate.contextAggregationInterval.expression + stateUpdate.contextAggregationInterval.asMilliseconds() ) .toPromise() .then((resp) => { @@ -652,7 +652,7 @@ export class TimeSeriesExplorer extends React.Component { this.getCriteriaFields(detectorIndex, entityControls), searchBounds.min.valueOf(), searchBounds.max.valueOf(), - stateUpdate.contextAggregationInterval.expression + stateUpdate.contextAggregationInterval.asMilliseconds() ) .then((resp) => { const fullRangeRecordScoreData = processRecordScoreResults(resp.results); @@ -703,7 +703,7 @@ export class TimeSeriesExplorer extends React.Component { nonBlankEntities, searchBounds.min.valueOf(), searchBounds.max.valueOf(), - stateUpdate.contextAggregationInterval.expression, + stateUpdate.contextAggregationInterval.asMilliseconds(), aggType ) .toPromise() diff --git a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts index 23d1e3f7cc904..ce0d7b0abc3e0 100644 --- a/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts +++ b/x-pack/plugins/ml/public/application/timeseriesexplorer/timeseriesexplorer_utils/get_focus_data.ts @@ -61,7 +61,7 @@ export function getFocusData( nonBlankEntities, searchBounds.min.valueOf(), searchBounds.max.valueOf(), - focusAggregationInterval.expression + focusAggregationInterval.asMilliseconds() ), // Query 2 - load all the records across selected time range for the chart anomaly markers. mlResultsService.getRecordsForCriteria( @@ -77,7 +77,7 @@ export function getFocusData( [selectedJob.job_id], searchBounds.min.valueOf(), searchBounds.max.valueOf(), - focusAggregationInterval.expression, + focusAggregationInterval.asMilliseconds(), 1, MAX_SCHEDULED_EVENTS ), @@ -123,7 +123,7 @@ export function getFocusData( nonBlankEntities, searchBounds.min.valueOf(), searchBounds.max.valueOf(), - focusAggregationInterval.expression, + focusAggregationInterval.asMilliseconds(), aggType ); })() diff --git a/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts b/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts index 2912aad6819cf..6a5583ecbb8ac 100644 --- a/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts +++ b/x-pack/plugins/ml/public/application/util/custom_url_utils.test.ts @@ -61,8 +61,13 @@ describe('ML - custom URL utils', () => { influencer_field_name: 'airline', influencer_field_values: ['<>:;[}")'], }, + { + influencer_field_name: 'odd:field,name', + influencer_field_values: [">:&12<'"], + }, ], airline: ['<>:;[}")'], + 'odd:field,name': [">:&12<'"], }; const TEST_RECORD_MULTIPLE_INFLUENCER_VALUES: CustomUrlAnomalyRecordDoc = { @@ -98,7 +103,7 @@ describe('ML - custom URL utils', () => { url_name: 'Raw data', time_range: 'auto', url_value: - "discover#/?_g=(time:(from:'$earliest$',mode:absolute,to:'$latest$'))&_a=(index:bf6e5860-9404-11e8-8d4c-593f69c47267,query:(language:kuery,query:'airline:\"$airline$\"'))", + "discover#/?_g=(time:(from:'$earliest$',mode:absolute,to:'$latest$'))&_a=(index:bf6e5860-9404-11e8-8d4c-593f69c47267,query:(language:kuery,query:'airline:\"$airline$\" and odd:field,name : $odd:field,name$'))", }; const TEST_DASHBOARD_LUCENE_URL: KibanaUrlConfig = { @@ -263,9 +268,55 @@ describe('ML - custom URL utils', () => { ); }); - test('returns expected URL for a Kibana Discover type URL when record field contains special characters', () => { + test.skip('returns expected URL for a Kibana Discover type URL when record field contains special characters', () => { expect(getUrlForRecord(TEST_DISCOVER_URL, TEST_RECORD_SPECIAL_CHARS)).toBe( - "discover#/?_g=(time:(from:'2017-02-09T15:10:00.000Z',mode:absolute,to:'2017-02-09T17:15:00.000Z'))&_a=(index:bf6e5860-9404-11e8-8d4c-593f69c47267,query:(language:kuery,query:'airline:\"%3C%3E%3A%3B%5B%7D%5C%22)\"'))" + "discover#/?_g=(time:(from:'2017-02-09T15:10:00.000Z',mode:absolute,to:'2017-02-09T17:15:00.000Z'))&_a=(index:bf6e5860-9404-11e8-8d4c-593f69c47267,query:(language:kuery,query:'airline:\"%3C%3E%3A%3B%5B%7D%5C%22)\" and odd:field,name:>:&12<''))" + ); + }); + + test('correctly encodes special characters inside of a query string', () => { + const testUrl = { + url_name: 'Show dashboard', + time_range: 'auto', + url_value: `dashboards#/view/351de820-f2bb-11ea-ab06-cb93221707e9?_a=(filters:!(),query:(language:kuery,query:'at@name:"$at@name$" and singlequote!'name:"$singlequote!'name$"'))&_g=(filters:!(),time:(from:'$earliest$',mode:absolute,to:'$latest$'))`, + }; + + const testRecord = { + job_id: 'spec-char', + result_type: 'record', + probability: 0.0028099428534745633, + multi_bucket_impact: 5, + record_score: 49.00785814424704, + initial_record_score: 49.00785814424704, + bucket_span: 900, + detector_index: 0, + is_interim: false, + timestamp: 1549593000000, + partition_field_name: 'at@name', + partition_field_value: "contains a ' quote", + function: 'mean', + function_description: 'mean', + typical: [1993.2657340111837], + actual: [1808.3334418402778], + field_name: 'metric%$£&!{(]field', + influencers: [ + { + influencer_field_name: "singlequote'name", + influencer_field_values: ["contains a ' quote"], + }, + { + influencer_field_name: 'at@name', + influencer_field_values: ["contains a ' quote"], + }, + ], + "singlequote'name": ["contains a ' quote"], + 'at@name': ["contains a ' quote"], + earliest: '2019-02-08T00:00:00.000Z', + latest: '2019-02-08T23:59:59.999Z', + }; + + expect(getUrlForRecord(testUrl, testRecord)).toBe( + `dashboards#/view/351de820-f2bb-11ea-ab06-cb93221707e9?_a=(filters:!(),query:(language:kuery,query:'at@name:"contains%20a%20!'%20quote" AND singlequote!'name:"contains%20a%20!'%20quote"'))&_g=(filters:!(),time:(from:'2019-02-08T00:00:00.000Z',mode:absolute,to:'2019-02-08T23:59:59.999Z'))` ); }); @@ -405,6 +456,58 @@ describe('ML - custom URL utils', () => { ); }); + test('return expected url for Security app', () => { + const urlConfig = { + url_name: 'Hosts Details by process name', + url_value: + "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))", + }; + + const testRecords = { + job_id: 'rare_process_by_host_linux_ecs', + result_type: 'record', + probability: 0.018122957282324745, + multi_bucket_impact: 0, + record_score: 20.513469583273547, + initial_record_score: 20.513469583273547, + bucket_span: 900, + detector_index: 0, + is_interim: false, + timestamp: 1549043100000, + by_field_name: 'process.name', + by_field_value: 'seq', + partition_field_name: 'host.name', + partition_field_value: 'showcase', + function: 'rare', + function_description: 'rare', + typical: [0.018122957282324745], + actual: [1], + influencers: [ + { + influencer_field_name: 'user.name', + influencer_field_values: ['sophie'], + }, + { + influencer_field_name: 'process.name', + influencer_field_values: ['seq'], + }, + { + influencer_field_name: 'host.name', + influencer_field_values: ['showcase'], + }, + ], + 'process.name': ['seq'], + 'user.name': ['sophie'], + 'host.name': ['showcase'], + earliest: '2019-02-01T16:00:00.000Z', + latest: '2019-02-01T18:59:59.999Z', + }; + + expect(getUrlForRecord(urlConfig, testRecords)).toBe( + "security/hosts/ml-hosts/showcase?_g=()&query=(language:kuery,query:'process.name:\"seq\"')&timerange=(global:(linkTo:!(timeline),timerange:(from:'2019-02-01T16:00:00.000Z',kind:absolute,to:'2019-02-01T18:59:59.999Z')),timeline:(linkTo:!(global),timerange:(from:'2019-02-01T16%3A00%3A00.000Z',kind:absolute,to:'2019-02-01T18%3A59%3A59.999Z')))" + ); + }); + test('removes an empty path component with a trailing slash', () => { const urlConfig = { url_name: 'APM', diff --git a/x-pack/plugins/ml/public/application/util/custom_url_utils.ts b/x-pack/plugins/ml/public/application/util/custom_url_utils.ts index 8263def2034aa..18ba1e4ee337b 100644 --- a/x-pack/plugins/ml/public/application/util/custom_url_utils.ts +++ b/x-pack/plugins/ml/public/application/util/custom_url_utils.ts @@ -8,6 +8,7 @@ import { get, flow } from 'lodash'; import moment from 'moment'; +import rison, { RisonObject, RisonValue } from 'rison-node'; import { parseInterval } from '../../../common/util/parse_interval'; import { escapeForElasticsearchQuery, replaceStringTokens } from './string_utils'; @@ -131,13 +132,70 @@ function escapeForKQL(value: string | number): string { type GetResultTokenValue = (v: string) => string; +export const isRisonObject = (value: RisonValue): value is RisonObject => { + return value !== null && typeof value === 'object'; +}; + +const getQueryStringResultProvider = ( + record: CustomUrlAnomalyRecordDoc, + getResultTokenValue: GetResultTokenValue +) => (resultPrefix: string, queryString: string, resultPostfix: string): string => { + const URL_LENGTH_LIMIT = 2000; + + let availableCharactersLeft = URL_LENGTH_LIMIT - resultPrefix.length - resultPostfix.length; + + // URL template might contain encoded characters + const queryFields = queryString + // Split query string by AND operator. + .split(/\sand\s/i) + // Get property name from `influencerField:$influencerField$` string. + .map((v) => String(v.split(/:(.+)?\$/)[0]).trim()); + + const queryParts: string[] = []; + const joinOperator = ' AND '; + + fieldsLoop: for (let i = 0; i < queryFields.length; i++) { + const field = queryFields[i]; + // Use lodash get to allow nested JSON fields to be retrieved. + let tokenValues: string[] | string | null = get(record, field) || null; + if (tokenValues === null) { + continue; + } + tokenValues = Array.isArray(tokenValues) ? tokenValues : [tokenValues]; + + // Create a pair `influencerField:value`. + // In cases where there are multiple influencer field values for an anomaly + // combine values with OR operator e.g. `(influencerField:value or influencerField:another_value)`. + let result = ''; + for (let j = 0; j < tokenValues.length; j++) { + const part = `${j > 0 ? ' OR ' : ''}${field}:"${getResultTokenValue(tokenValues[j])}"`; + + // Build up a URL string which is not longer than the allowed length and isn't corrupted by invalid query. + if (availableCharactersLeft < part.length) { + if (result.length > 0) { + queryParts.push(j > 0 ? `(${result})` : result); + } + break fieldsLoop; + } + + result += part; + + availableCharactersLeft -= result.length; + } + + if (result.length > 0) { + queryParts.push(tokenValues.length > 1 ? `(${result})` : result); + } + } + return queryParts.join(joinOperator); +}; + /** * Builds a Kibana dashboard or Discover URL from the supplied config, with any * dollar delimited tokens substituted from the supplied anomaly record. */ function buildKibanaUrl(urlConfig: UrlConfig, record: CustomUrlAnomalyRecordDoc) { const urlValue = urlConfig.url_value; - const URL_LENGTH_LIMIT = 2000; const isLuceneQueryLanguage = urlValue.includes('language:lucene'); @@ -145,11 +203,7 @@ function buildKibanaUrl(urlConfig: UrlConfig, record: CustomUrlAnomalyRecordDoc) ? escapeForElasticsearchQuery : escapeForKQL; - const commonEscapeCallback = flow( - // Kibana URLs used rison encoding, so escape with ! any ! or ' characters - (v: string): string => v.replace(/[!']/g, '!$&'), - encodeURIComponent - ); + const commonEscapeCallback = flow(encodeURIComponent); const replaceSingleTokenValues = (str: string) => { const getResultTokenValue: GetResultTokenValue = flow( @@ -172,65 +226,34 @@ function buildKibanaUrl(urlConfig: UrlConfig, record: CustomUrlAnomalyRecordDoc) return flow( (str: string) => str.replace('$earliest$', record.earliest).replace('$latest$', record.latest), // Process query string content of the URL + decodeURIComponent, (str: string) => { const getResultTokenValue: GetResultTokenValue = flow( queryLanguageEscapeCallback, commonEscapeCallback ); + + const getQueryStringResult = getQueryStringResultProvider(record, getResultTokenValue); + + const match = str.match(/(.+)(\(.*\blanguage:(?:lucene|kuery)\b.*?\))(.+)/); + + if (match !== null && match[2] !== undefined) { + const [, prefix, queryDef, postfix] = match; + + const q = rison.decode(queryDef); + + if (isRisonObject(q) && q.hasOwnProperty('query')) { + const [resultPrefix, resultPostfix] = [prefix, postfix].map(replaceSingleTokenValues); + const resultQuery = getQueryStringResult(resultPrefix, q.query as string, resultPostfix); + return `${resultPrefix}${rison.encode({ ...q, query: resultQuery })}${resultPostfix}`; + } + } + return str.replace( - /(.+query:'|.+&kuery=)([^']*)(['&].+)/, + /(.+&kuery=)(.*?)[^!](&.+)/, (fullMatch, prefix: string, queryString: string, postfix: string) => { const [resultPrefix, resultPostfix] = [prefix, postfix].map(replaceSingleTokenValues); - - let availableCharactersLeft = - URL_LENGTH_LIMIT - resultPrefix.length - resultPostfix.length; - const queryFields = queryString - // Split query string by AND operator. - .split(/\sand\s/i) - // Get property name from `influencerField:$influencerField$` string. - .map((v) => v.split(':')[0]); - - const queryParts: string[] = []; - const joinOperator = ' AND '; - - fieldsLoop: for (let i = 0; i < queryFields.length; i++) { - const field = queryFields[i]; - // Use lodash get to allow nested JSON fields to be retrieved. - let tokenValues: string[] | string | null = get(record, field) || null; - if (tokenValues === null) { - continue; - } - tokenValues = Array.isArray(tokenValues) ? tokenValues : [tokenValues]; - - // Create a pair `influencerField:value`. - // In cases where there are multiple influencer field values for an anomaly - // combine values with OR operator e.g. `(influencerField:value or influencerField:another_value)`. - let result = ''; - for (let j = 0; j < tokenValues.length; j++) { - const part = `${j > 0 ? ' OR ' : ''}${field}:"${getResultTokenValue( - tokenValues[j] - )}"`; - - // Build up a URL string which is not longer than the allowed length and isn't corrupted by invalid query. - if (availableCharactersLeft < part.length) { - if (result.length > 0) { - queryParts.push(j > 0 ? `(${result})` : result); - } - break fieldsLoop; - } - - result += part; - - availableCharactersLeft -= result.length; - } - - if (result.length > 0) { - queryParts.push(tokenValues.length > 1 ? `(${result})` : result); - } - } - - const resultQuery = queryParts.join(joinOperator); - + const resultQuery = getQueryStringResult(resultPrefix, queryString, resultPostfix); return `${resultPrefix}${resultQuery}${resultPostfix}`; } ); diff --git a/x-pack/plugins/ml/public/application/util/ml_error.ts b/x-pack/plugins/ml/public/application/util/ml_error.ts deleted file mode 100644 index 2a0280404c189..0000000000000 --- a/x-pack/plugins/ml/public/application/util/ml_error.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { KbnError } from '../../../../../../src/plugins/kibana_utils/public'; - -export class MLRequestFailure extends KbnError { - origError: any; - resp: any; - // takes an Error object and and optional response object - // if error is falsy (null) the response object will be used - // notify will show the full expandable stack trace of the response if a response object is used and no error is passed in. - constructor(error: any, resp: any) { - error = error || {}; - super(error.message || JSON.stringify(resp)); - - this.origError = error; - this.resp = typeof resp === 'string' ? JSON.parse(resp) : resp; - } -} diff --git a/x-pack/plugins/ml/public/plugin.ts b/x-pack/plugins/ml/public/plugin.ts index 3e8ab99e341ad..fc0d21e9353cf 100644 --- a/x-pack/plugins/ml/public/plugin.ts +++ b/x-pack/plugins/ml/public/plugin.ts @@ -101,6 +101,8 @@ export class MlPlugin implements Plugin { }, }); + const managementApp = registerManagementSection(pluginsSetup.management, core); + const licensing = pluginsSetup.licensing.license$.pipe(take(1)); licensing.subscribe(async (license) => { const [coreStart] = await core.getStartServices(); @@ -110,26 +112,35 @@ export class MlPlugin implements Plugin { registerFeature(pluginsSetup.home); } + const { capabilities } = coreStart.application; + // register ML for the index pattern management no data screen. pluginsSetup.indexPatternManagement.environment.update({ ml: () => - coreStart.application.capabilities.ml.canFindFileStructure - ? MlCardState.ENABLED - : MlCardState.HIDDEN, + capabilities.ml.canFindFileStructure ? MlCardState.ENABLED : MlCardState.HIDDEN, }); + const canManageMLJobs = capabilities.management?.insightsAndAlerting?.jobsListLink ?? false; + // register various ML plugin features which require a full license if (isFullLicense(license)) { - registerManagementSection(pluginsSetup.management, core); + if (canManageMLJobs && managementApp) { + managementApp.enable(); + } registerEmbeddables(pluginsSetup.embeddable, core); registerMlUiActions(pluginsSetup.uiActions, core); registerUrlGenerator(pluginsSetup.share, core); + } else if (managementApp) { + managementApp.disable(); } } else { // if ml is disabled in elasticsearch, disable ML in kibana this.appUpdater.next(() => ({ status: AppStatus.inaccessible, })); + if (managementApp) { + managementApp.disable(); + } } }); diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js index fd0cab7c0625d..981ffe9618d9f 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/polled_data_checker.js @@ -56,7 +56,7 @@ export function polledDataCheckerFactory({ asCurrentUser }) { date_histogram: { min_doc_count: 1, field: this.timeField, - interval: `${intervalMs}ms`, + fixed_interval: `${intervalMs}ms`, }, }, }, diff --git a/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js index 750f0cfc0b4a8..5dd0a5ff563d6 100644 --- a/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js +++ b/x-pack/plugins/ml/server/models/bucket_span_estimator/single_series_checker.js @@ -166,7 +166,7 @@ export function singleSeriesCheckerFactory({ asCurrentUser }) { non_empty_buckets: { date_histogram: { field: this.timeField, - interval: `${intervalMs}ms`, + fixed_interval: `${intervalMs}ms`, }, }, }, diff --git a/x-pack/plugins/ml/server/models/data_frame_analytics/feature_importance.ts b/x-pack/plugins/ml/server/models/data_frame_analytics/feature_importance.ts new file mode 100644 index 0000000000000..94f54a5654873 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_frame_analytics/feature_importance.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IScopedClusterClient } from 'kibana/server'; +import { + getDefaultPredictionFieldName, + getPredictionFieldName, + isRegressionAnalysis, +} from '../../../common/util/analytics_utils'; +import { DEFAULT_RESULTS_FIELD } from '../../../common/constants/data_frame_analytics'; +// Obtains data for the data frame analytics feature importance functionalities +// such as baseline, decision paths, or importance summary. +export function analyticsFeatureImportanceProvider({ + asInternalUser, + asCurrentUser, +}: IScopedClusterClient) { + async function getRegressionAnalyticsBaseline(analyticsId: string): Promise { + const { body } = await asInternalUser.ml.getDataFrameAnalytics({ + id: analyticsId, + }); + const jobConfig = body.data_frame_analytics[0]; + if (!isRegressionAnalysis) return undefined; + const destinationIndex = jobConfig.dest.index; + const predictionFieldName = getPredictionFieldName(jobConfig.analysis); + const mlResultsField = jobConfig.dest?.results_field ?? DEFAULT_RESULTS_FIELD; + const predictedField = `${mlResultsField}.${ + predictionFieldName ? predictionFieldName : getDefaultPredictionFieldName(jobConfig.analysis) + }`; + const isTrainingField = `${mlResultsField}.is_training`; + + const params = { + index: destinationIndex, + size: 0, + body: { + query: { + bool: { + filter: [ + { + term: { + [isTrainingField]: true, + }, + }, + ], + }, + }, + aggs: { + featureImportanceBaseline: { + avg: { + field: predictedField, + }, + }, + }, + }, + }; + let baseline; + const { body: aggregationResult } = await asCurrentUser.search(params); + if (aggregationResult) { + baseline = aggregationResult.aggregations.featureImportanceBaseline.value; + } + return baseline; + } + + return { + getRegressionAnalyticsBaseline, + }; +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json index a9865183320d5..084aa08455405 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/sample_data_weblogs/ml/datafeed_low_request_rate.json @@ -10,7 +10,7 @@ "buckets": { "date_histogram": { "field": "timestamp", - "interval": 3600000 + "fixed_interval": "1h" }, "aggregations": { "timestamp": { diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json index 1e7fcdd4320f8..36d1df6db4c99 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/manifest.json @@ -40,6 +40,46 @@ { "id": "linux_anomalous_user_name_ecs", "file": "linux_anomalous_user_name_ecs.json" + }, + { + "id": "linux_rare_metadata_process", + "file": "linux_rare_metadata_process.json" + }, + { + "id": "linux_rare_metadata_user", + "file": "linux_rare_metadata_user.json" + }, + { + "id": "linux_rare_user_compiler", + "file": "linux_rare_user_compiler.json" + }, + { + "id": "linux_rare_kernel_module_arguments", + "file": "linux_rare_kernel_module_arguments.json" + }, + { + "id": "linux_rare_sudo_user", + "file": "linux_rare_sudo_user.json" + }, + { + "id": "linux_system_user_discovery", + "file": "linux_system_user_discovery.json" + }, + { + "id": "linux_system_information_discovery", + "file": "linux_system_information_discovery.json" + }, + { + "id": "linux_system_process_discovery", + "file": "linux_system_process_discovery.json" + }, + { + "id": "linux_network_connection_discovery", + "file": "linux_network_connection_discovery.json" + }, + { + "id": "linux_network_configuration_discovery", + "file": "linux_network_configuration_discovery.json" } ], "datafeeds": [ @@ -77,6 +117,56 @@ "id": "datafeed-linux_anomalous_user_name_ecs", "file": "datafeed_linux_anomalous_user_name_ecs.json", "job_id": "linux_anomalous_user_name_ecs" + }, + { + "id": "datafeed-linux_rare_metadata_process", + "file": "datafeed_linux_rare_metadata_process.json", + "job_id": "linux_rare_metadata_process" + }, + { + "id": "datafeed-linux_rare_metadata_user", + "file": "datafeed_linux_rare_metadata_user.json", + "job_id": "linux_rare_metadata_user" + }, + { + "id": "datafeed-linux_rare_user_compiler", + "file": "datafeed_linux_rare_user_compiler.json", + "job_id": "linux_rare_user_compiler" + }, + { + "id": "datafeed-linux_rare_kernel_module_arguments", + "file": "datafeed_linux_rare_kernel_module_arguments.json", + "job_id": "linux_rare_kernel_module_arguments" + }, + { + "id": "datafeed-linux_rare_sudo_user", + "file": "datafeed_linux_rare_sudo_user.json", + "job_id": "linux_rare_sudo_user" + }, + { + "id": "datafeed-linux_system_information_discovery", + "file": "datafeed_linux_system_information_discovery.json", + "job_id": "linux_system_information_discovery" + }, + { + "id": "datafeed-linux_system_process_discovery", + "file": "datafeed_linux_system_process_discovery.json", + "job_id": "linux_system_process_discovery" + }, + { + "id": "datafeed-linux_system_user_discovery", + "file": "datafeed_linux_system_user_discovery.json", + "job_id": "linux_system_user_discovery" + }, + { + "id": "datafeed-linux_network_configuration_discovery", + "file": "datafeed_linux_network_configuration_discovery.json", + "job_id": "linux_network_configuration_discovery" + }, + { + "id": "datafeed-linux_network_connection_discovery", + "file": "datafeed_linux_network_connection_discovery.json", + "job_id": "linux_network_connection_discovery" } ] } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_network_configuration_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_network_configuration_discovery.json new file mode 100644 index 0000000000000..d4a130770c920 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_network_configuration_discovery.json @@ -0,0 +1,26 @@ +{ + "job_id": "JOB_ID", + "indices": [ + "INDEX_PATTERN_NAME" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "must": [ + { + "bool": { + "should": [ + {"term": {"process.name": "arp"}}, + {"term": {"process.name": "echo"}}, + {"term": {"process.name": "ethtool"}}, + {"term": {"process.name": "ifconfig"}}, + {"term": {"process.name": "ip"}}, + {"term": {"process.name": "iptables"}}, + {"term": {"process.name": "ufw"}} + ] + } + } + ] + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_network_connection_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_network_connection_discovery.json new file mode 100644 index 0000000000000..0ae80df4bd47d --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_network_connection_discovery.json @@ -0,0 +1,23 @@ +{ + "job_id": "JOB_ID", + "indices": [ + "INDEX_PATTERN_NAME" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "must": [ + { + "bool": { + "should": [ + {"term": {"process.name": "netstat"}}, + {"term": {"process.name": "ss"}}, + {"term": {"process.name": "route"}}, + {"term": {"process.name": "showmount"}} + ] + } + } + ] + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_rare_kernel_module_arguments.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_rare_kernel_module_arguments.json new file mode 100644 index 0000000000000..99bb690c8d73d --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_rare_kernel_module_arguments.json @@ -0,0 +1,22 @@ +{ + "job_id": "JOB_ID", + "indices": [ + "INDEX_PATTERN_NAME" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "filter": [{"exists": {"field": "process.title"}}], + "must": [ + {"bool": { + "should": [ + {"term": {"process.name": "insmod"}}, + {"term": {"process.name": "kmod"}}, + {"term": {"process.name": "modprobe"}}, + {"term": {"process.name": "rmod"}} + ] + }} + ] + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_rare_metadata_process.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_rare_metadata_process.json new file mode 100644 index 0000000000000..dc0f6c4e81b33 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_rare_metadata_process.json @@ -0,0 +1,12 @@ +{ + "job_id": "JOB_ID", + "indices": [ + "INDEX_PATTERN_NAME" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "filter": [{"term": {"destination.ip": "169.254.169.254"}}] + } + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_rare_metadata_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_rare_metadata_user.json new file mode 100644 index 0000000000000..dc0f6c4e81b33 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_rare_metadata_user.json @@ -0,0 +1,12 @@ +{ + "job_id": "JOB_ID", + "indices": [ + "INDEX_PATTERN_NAME" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "filter": [{"term": {"destination.ip": "169.254.169.254"}}] + } + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_rare_sudo_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_rare_sudo_user.json new file mode 100644 index 0000000000000..544675f3d48dc --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_rare_sudo_user.json @@ -0,0 +1,15 @@ +{ + "job_id": "JOB_ID", + "indices": [ + "INDEX_PATTERN_NAME" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "filter": [ + {"term": {"event.action": "executed"}}, + {"term": {"process.name": "sudo"}} + ] + } + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_rare_user_compiler.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_rare_user_compiler.json new file mode 100644 index 0000000000000..027b124010001 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_rare_user_compiler.json @@ -0,0 +1,22 @@ +{ + "job_id": "JOB_ID", + "indices": [ + "INDEX_PATTERN_NAME" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "filter": [{"term": {"event.action": "executed"}}], + "must": [ + {"bool": { + "should": [ + {"term": {"process.name": "compile"}}, + {"term": {"process.name": "gcc"}}, + {"term": {"process.name": "make"}}, + {"term": {"process.name": "yasm"}} + ] + }} + ] + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_system_information_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_system_information_discovery.json new file mode 100644 index 0000000000000..6e7ce26763f79 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_system_information_discovery.json @@ -0,0 +1,31 @@ +{ + "job_id": "JOB_ID", + "indices": [ + "INDEX_PATTERN_NAME" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "must": [ + { + "bool": { + "should": [ + {"term": {"process.name": "cat"}}, + {"term": {"process.name": "grep"}}, + {"term": {"process.name": "head"}}, + {"term": {"process.name": "hostname"}}, + {"term": {"process.name": "less"}}, + {"term": {"process.name": "ls"}}, + {"term": {"process.name": "lsmod"}}, + {"term": {"process.name": "more"}}, + {"term": {"process.name": "strings"}}, + {"term": {"process.name": "tail"}}, + {"term": {"process.name": "uptime"}}, + {"term": {"process.name": "uname"}} + ] + } + } + ] + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_system_process_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_system_process_discovery.json new file mode 100644 index 0000000000000..dbd8f54ff9712 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_system_process_discovery.json @@ -0,0 +1,21 @@ +{ + "job_id": "JOB_ID", + "indices": [ + "INDEX_PATTERN_NAME" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "must": [ + { + "bool": { + "should": [ + {"term": {"process.name": "ps"}}, + {"term": {"process.name": "top"}} + ] + } + } + ] + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_system_user_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_system_user_discovery.json new file mode 100644 index 0000000000000..24230094a47d2 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/datafeed_linux_system_user_discovery.json @@ -0,0 +1,23 @@ +{ + "job_id": "JOB_ID", + "indices": [ + "INDEX_PATTERN_NAME" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "must": [ + { + "bool": { + "should": [ + {"term": {"process.name": "users"}}, + {"term": {"process.name": "w"}}, + {"term": {"process.name": "who"}}, + {"term": {"process.name": "whoami"}} + ] + } + } + ] + } + } +} diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_network_configuration_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_network_configuration_discovery.json new file mode 100644 index 0000000000000..6d687764085e0 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_network_configuration_discovery.json @@ -0,0 +1,53 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Auditbeat - Looks for commands related to system network configuration discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used by a threat actor to engage in system network configuration discovery in order to increase their understanding of connected networks and hosts. This information may be used to shape follow-up behaviors such as lateral movement or additional discovery.", + "groups": [ + "security", + "auditbeat", + "process" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"user.name\"", + "function": "rare", + "by_field_name": "user.name" + } + ], + "influencers": [ + "process.name", + "host.name", + "process.args", + "user.name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "64mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-auditbeat", + "custom_urls": [ + { + "url_name": "Host Details by process name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Host Details by user name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by process name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by user name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + } + ] + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_network_connection_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_network_connection_discovery.json new file mode 100644 index 0000000000000..b41439548dd59 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_network_connection_discovery.json @@ -0,0 +1,53 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Auditbeat - Looks for commands related to system network connection discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used by a threat actor to engage in system network connection discovery in order to increase their understanding of connected services and systems. This information may be used to shape follow-up behaviors such as lateral movement or additional discovery.", + "groups": [ + "security", + "auditbeat", + "process" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"user.name\"", + "function": "rare", + "by_field_name": "user.name" + } + ], + "influencers": [ + "process.name", + "host.name", + "process.args", + "user.name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "64mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-auditbeat", + "custom_urls": [ + { + "url_name": "Host Details by process name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Host Details by user name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by process name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by user name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + } + ] + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_kernel_module_arguments.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_kernel_module_arguments.json new file mode 100644 index 0000000000000..1b79e83054251 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_kernel_module_arguments.json @@ -0,0 +1,45 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Auditbeat - Looks for unusual kernel modules which are often used for stealth.", + "groups": [ + "security", + "auditbeat", + "process" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"process.title\"", + "function": "rare", + "by_field_name": "process.title" + } + ], + "influencers": [ + "process.title", + "process.working_directory", + "host.name", + "user.name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "32mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-auditbeat", + "custom_urls": [ + { + "url_name": "Host Details by user name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by user name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + } + ] + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_metadata_process.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_metadata_process.json new file mode 100644 index 0000000000000..7295f11e600d7 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_metadata_process.json @@ -0,0 +1,52 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Auditbeat - Looks for anomalous access to the metadata service by an unusual process. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", + "groups": [ + "security", + "auditbeat", + "process" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"process.name\"", + "function": "rare", + "by_field_name": "process.name" + } + ], + "influencers": [ + "host.name", + "user.name", + "process.name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "32mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-auditbeat", + "custom_urls": [ + { + "url_name": "Host Details by process name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Host Details by user name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by process name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by user name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + } + ] + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_metadata_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_metadata_user.json new file mode 100644 index 0000000000000..049d10920de00 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_metadata_user.json @@ -0,0 +1,43 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Auditbeat - Looks for anomalous access to the metadata service by an unusual user. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", + "groups": [ + "security", + "auditbeat", + "process" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"user.name\"", + "function": "rare", + "by_field_name": "user.name" + } + ], + "influencers": [ + "host.name", + "user.name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "32mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-auditbeat", + "custom_urls": [ + { + "url_name": "Host Details by user name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by user name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + } + ] + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_sudo_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_sudo_user.json new file mode 100644 index 0000000000000..654f5c76e5698 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_sudo_user.json @@ -0,0 +1,53 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Auditbeat - Looks for sudo activity from an unusual user context.", + "groups": [ + "security", + "auditbeat", + "process" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"user.name\"", + "function": "rare", + "by_field_name": "user.name" + } + ], + "influencers": [ + "process.name", + "host.name", + "process.args", + "user.name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "32mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-auditbeat", + "custom_urls": [ + { + "url_name": "Host Details by process name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Host Details by user name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by process name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by user name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + } + ] + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json new file mode 100644 index 0000000000000..245b7e0819c7d --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_rare_user_compiler.json @@ -0,0 +1,45 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Auditbeat - Looks for compiler activity by a user context which does not normally run compilers. This can be ad-hoc software changes or unauthorized software deployment. This can also be due to local privliege elevation via locally run exploits or malware activity.", + "groups": [ + "security", + "auditbeat", + "process" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"user.name\"", + "function": "rare", + "by_field_name": "user.name" + } + ], + "influencers": [ + "process.title", + "host.name", + "process.working_directory", + "user.name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "256mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-auditbeat", + "custom_urls": [ + { + "url_name": "Host Details by user name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by user name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + } + ] + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_system_information_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_system_information_discovery.json new file mode 100644 index 0000000000000..3a51223b4899c --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_system_information_discovery.json @@ -0,0 +1,53 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Auditbeat - Looks for commands related to system information discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used to engage in system information discovery in order to gather detailed information about system configuration and software versions. This may be a precursor to selection of a persistence mechanism or a method of privilege elevation.", + "groups": [ + "security", + "auditbeat", + "process" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"user.name\"", + "function": "rare", + "by_field_name": "user.name" + } + ], + "influencers": [ + "process.name", + "host.name", + "process.args", + "user.name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "16mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-auditbeat", + "custom_urls": [ + { + "url_name": "Host Details by process name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Host Details by user name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by process name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by user name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + } + ] + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_system_process_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_system_process_discovery.json new file mode 100644 index 0000000000000..592bb5a717fc0 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_system_process_discovery.json @@ -0,0 +1,53 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Auditbeat - Looks for commands related to system process discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used to engage in system process discovery in order to increase their understanding of software applications running on a target host or network. This may be a precursor to selection of a persistence mechanism or a method of privilege elevation.", + "groups": [ + "security", + "auditbeat", + "process" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"user.name\"", + "function": "rare", + "by_field_name": "user.name" + } + ], + "influencers": [ + "process.name", + "host.name", + "process.args", + "user.name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "16mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-auditbeat", + "custom_urls": [ + { + "url_name": "Host Details by process name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Host Details by user name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by process name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by user name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + } + ] + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_system_user_discovery.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_system_user_discovery.json new file mode 100644 index 0000000000000..33f42c274b337 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_auditbeat/ml/linux_system_user_discovery.json @@ -0,0 +1,53 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Auditbeat - Looks for commands related to system user or owner discovery from an unusual user context. This can be due to uncommon troubleshooting activity or due to a compromised account. A compromised account may be used to engage in system owner or user discovery in order to identify currently active or primary users of a system. This may be a precursor to additional discovery, credential dumping or privilege elevation activity.", + "groups": [ + "security", + "auditbeat", + "process" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"user.name\"", + "function": "rare", + "by_field_name": "user.name" + } + ], + "influencers": [ + "process.name", + "host.name", + "process.args", + "user.name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "16mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-auditbeat", + "custom_urls": [ + { + "url_name": "Host Details by process name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Host Details by user name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by process name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by user name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + } + ] + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json index ffbf5aa7d8bb0..969873ead6d9c 100644 --- a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/manifest.json @@ -48,6 +48,14 @@ { "id": "windows_rare_user_runas_event", "file": "windows_rare_user_runas_event.json" + }, + { + "id": "windows_rare_metadata_process", + "file": "windows_rare_metadata_process.json" + }, + { + "id": "windows_rare_metadata_user", + "file": "windows_rare_metadata_user.json" } ], "datafeeds": [ @@ -95,6 +103,16 @@ "id": "datafeed-windows_rare_user_runas_event", "file": "datafeed_windows_rare_user_runas_event.json", "job_id": "windows_rare_user_runas_event" + }, + { + "id": "datafeed-windows_rare_metadata_process", + "file": "datafeed_windows_rare_metadata_process.json", + "job_id": "windows_rare_metadata_process" + }, + { + "id": "datafeed-windows_rare_metadata_user", + "file": "datafeed_windows_rare_metadata_user.json", + "job_id": "windows_rare_metadata_user" } ] -} +} \ No newline at end of file diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_rare_metadata_process.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_rare_metadata_process.json new file mode 100644 index 0000000000000..dc0f6c4e81b33 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_rare_metadata_process.json @@ -0,0 +1,12 @@ +{ + "job_id": "JOB_ID", + "indices": [ + "INDEX_PATTERN_NAME" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "filter": [{"term": {"destination.ip": "169.254.169.254"}}] + } + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_rare_metadata_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_rare_metadata_user.json new file mode 100644 index 0000000000000..dc0f6c4e81b33 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/datafeed_windows_rare_metadata_user.json @@ -0,0 +1,12 @@ +{ + "job_id": "JOB_ID", + "indices": [ + "INDEX_PATTERN_NAME" + ], + "max_empty_searches": 10, + "query": { + "bool": { + "filter": [{"term": {"destination.ip": "169.254.169.254"}}] + } + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_metadata_process.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_metadata_process.json new file mode 100644 index 0000000000000..85fddbcc53e0f --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_metadata_process.json @@ -0,0 +1,52 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Winlogbeat - Looks for anomalous access to the metadata service by an unusual process. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", + "groups": [ + "security", + "winlogbeat", + "process" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"process.name\"", + "function": "rare", + "by_field_name": "process.name" + } + ], + "influencers": [ + "process.name", + "host.name", + "user.name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "64mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-winlogbeat", + "custom_urls": [ + { + "url_name": "Host Details by process name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Host Details by user name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by process name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'process.name%20:%20%22$process.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by user name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + } + ] + } + } diff --git a/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_metadata_user.json b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_metadata_user.json new file mode 100644 index 0000000000000..767c2d5b30ad2 --- /dev/null +++ b/x-pack/plugins/ml/server/models/data_recognizer/modules/siem_winlogbeat/ml/windows_rare_metadata_user.json @@ -0,0 +1,43 @@ +{ + "job_type": "anomaly_detector", + "description": "Security: Winlogbeat - Looks for anomalous access to the metadata service by an unusual user. The metadata service may be targeted in order to harvest credentials or user data scripts containing secrets.", + "groups": [ + "security", + "winlogbeat", + "process" + ], + "analysis_config": { + "bucket_span": "15m", + "detectors": [ + { + "detector_description": "rare by \"user.name\"", + "function": "rare", + "by_field_name": "user.name" + } + ], + "influencers": [ + "host.name", + "user.name" + ] + }, + "allow_lazy_open": true, + "analysis_limits": { + "model_memory_limit": "32mb" + }, + "data_description": { + "time_field": "@timestamp" + }, + "custom_settings": { + "created_by": "ml-module-siem-winlogbeat", + "custom_urls": [ + { + "url_name": "Host Details by user name", + "url_value": "security/hosts/ml-hosts/$host.name$?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + }, + { + "url_name": "Hosts Overview by user name", + "url_value": "security/hosts/ml-hosts?_g=()&query=(query:'user.name%20:%20%22$user.name$%22',language:kuery)&timerange=(global:(linkTo:!(timeline),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')),timeline:(linkTo:!(global),timerange:(from:'$earliest$',kind:absolute,to:'$latest$')))" + } + ] + } + } diff --git a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts index dbfa4b5656e5f..95c4e79150059 100644 --- a/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts +++ b/x-pack/plugins/ml/server/models/data_visualizer/data_visualizer.ts @@ -468,7 +468,7 @@ export class DataVisualizer { timeFieldName: string, earliestMs: number, latestMs: number, - interval: number, + intervalMs: number, maxExamples: number ): Promise { // Batch up fields by type, getting stats for multiple fields at a time. @@ -526,7 +526,7 @@ export class DataVisualizer { timeFieldName, earliestMs, latestMs, - interval + intervalMs ); batchStats.push(stats); } @@ -710,7 +710,7 @@ export class DataVisualizer { timeFieldName: string, earliestMs: number, latestMs: number, - interval: number + intervalMs: number ): Promise { const index = indexPatternTitle; const size = 0; @@ -718,11 +718,12 @@ export class DataVisualizer { // Don't use the sampler aggregation as this can lead to some potentially // confusing date histogram results depending on the date range of data amongst shards. + const aggs = { eventRate: { date_histogram: { field: timeFieldName, - interval, + fixed_interval: `${intervalMs}ms`, min_doc_count: 1, }, }, @@ -756,7 +757,7 @@ export class DataVisualizer { return { documentCounts: { - interval, + interval: intervalMs, buckets, }, }; diff --git a/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts b/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts index 6108454c08aa7..26dba7c2f00c1 100644 --- a/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts +++ b/x-pack/plugins/ml/server/models/file_data_visualizer/import_data.ts @@ -94,7 +94,7 @@ export function importDataProvider({ asCurrentUser }: IScopedClusterClient) { _meta: { created_by: INDEX_META_DATA_CREATED_BY, }, - properties: mappings, + properties: mappings.properties, }, }; diff --git a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts index c0eb1b72825df..62ef9b3621610 100644 --- a/x-pack/plugins/ml/server/models/job_service/datafeeds.ts +++ b/x-pack/plugins/ml/server/models/job_service/datafeeds.ts @@ -118,6 +118,11 @@ export function datafeedsProvider({ asInternalUser }: IScopedClusterClient) { } catch (error) { if (isRequestTimeout(error)) { return fillResultsWithTimeouts(results, datafeedId, datafeedIds, DATAFEED_STATE.STOPPED); + } else { + results[datafeedId] = { + started: false, + error: error.body, + }; } } } diff --git a/x-pack/plugins/ml/server/models/job_service/jobs.ts b/x-pack/plugins/ml/server/models/job_service/jobs.ts index e047d31ba6eb7..f4378e29ef826 100644 --- a/x-pack/plugins/ml/server/models/job_service/jobs.ts +++ b/x-pack/plugins/ml/server/models/job_service/jobs.ts @@ -407,28 +407,21 @@ export function jobsProvider(client: IScopedClusterClient) { // Job IDs in supplied array may contain wildcard '*' characters // e.g. *_low_request_rate_ecs async function jobsExist(jobIds: string[] = []) { - // Get the list of job IDs. - const { body } = await asInternalUser.ml.getJobs({ - job_id: jobIds.join(), - }); - const results: { [id: string]: boolean } = {}; - if (body.count > 0) { - const allJobIds = body.jobs.map((job) => job.job_id); - - // Check if each of the supplied IDs match existing jobs. - jobIds.forEach((jobId) => { - // Create a Regex for each supplied ID as wildcard * is allowed. - const regexp = new RegExp(`^${jobId.replace(/\*+/g, '.*')}$`); - const exists = allJobIds.some((existsJobId) => regexp.test(existsJobId)); - results[jobId] = exists; - }); - } else { - jobIds.forEach((jobId) => { + for (const jobId of jobIds) { + try { + const { body } = await asInternalUser.ml.getJobs({ + job_id: jobId, + }); + results[jobId] = body.count > 0; + } catch (e) { + // if a non-wildcarded job id is supplied, the get jobs endpoint will 404 + if (e.body?.status !== 404) { + throw e; + } results[jobId] = false; - }); + } } - return results; } diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts index 9eea1ea2a28ae..128b28a223445 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/line_chart.ts @@ -114,7 +114,7 @@ function getSearchJsonFromConfig( times: { date_histogram: { field: timeField, - interval: intervalMs, + fixed_interval: `${intervalMs}ms`, min_doc_count: 0, extended_bounds: { min: start, diff --git a/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts index 567afec809405..71e81158d8885 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts +++ b/x-pack/plugins/ml/server/models/job_service/new_job/population_chart.ts @@ -142,7 +142,7 @@ function getPopulationSearchJsonFromConfig( times: { date_histogram: { field: timeField, - interval: intervalMs, + fixed_interval: `${intervalMs}ms`, min_doc_count: 0, extended_bounds: { min: start, diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json index ca356b2bede22..9e2af76264231 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/kibana_saved_objects.json @@ -20,7 +20,7 @@ "type": "index-pattern", "id": "be14ceb0-66b1-11e9-91c9-ffa52374d341", "attributes": { - "typeMeta": "{\"params\":{\"rollup_index\":\"cloud_roll_index\"},\"aggs\":{\"histogram\":{\"NetworkOut\":{\"agg\":\"histogram\",\"interval\":5},\"CPUUtilization\":{\"agg\":\"histogram\",\"interval\":5},\"NetworkIn\":{\"agg\":\"histogram\",\"interval\":5}},\"avg\":{\"NetworkOut\":{\"agg\":\"avg\"},\"CPUUtilization\":{\"agg\":\"avg\"},\"NetworkIn\":{\"agg\":\"avg\"},\"DiskReadBytes\":{\"agg\":\"avg\"}},\"min\":{\"NetworkOut\":{\"agg\":\"min\"},\"NetworkIn\":{\"agg\":\"min\"}},\"value_count\":{\"NetworkOut\":{\"agg\":\"value_count\"},\"DiskReadBytes\":{\"agg\":\"value_count\"},\"CPUUtilization\":{\"agg\":\"value_count\"},\"NetworkIn\":{\"agg\":\"value_count\"}},\"max\":{\"CPUUtilization\":{\"agg\":\"max\"},\"DiskReadBytes\":{\"agg\":\"max\"}},\"date_histogram\":{\"@timestamp\":{\"agg\":\"date_histogram\",\"delay\":\"1d\",\"interval\":\"5m\",\"time_zone\":\"UTC\"}},\"terms\":{\"instance\":{\"agg\":\"terms\"},\"sourcetype.keyword\":{\"agg\":\"terms\"},\"region\":{\"agg\":\"terms\"}},\"sum\":{\"DiskReadBytes\":{\"agg\":\"sum\"},\"NetworkOut\":{\"agg\":\"sum\"}}}}", + "typeMeta": "{\"params\":{\"rollup_index\":\"cloud_roll_index\"},\"aggs\":{\"histogram\":{\"NetworkOut\":{\"agg\":\"histogram\",\"interval\":5},\"CPUUtilization\":{\"agg\":\"histogram\",\"interval\":5},\"NetworkIn\":{\"agg\":\"histogram\",\"interval\":5}},\"avg\":{\"NetworkOut\":{\"agg\":\"avg\"},\"CPUUtilization\":{\"agg\":\"avg\"},\"NetworkIn\":{\"agg\":\"avg\"},\"DiskReadBytes\":{\"agg\":\"avg\"}},\"min\":{\"NetworkOut\":{\"agg\":\"min\"},\"NetworkIn\":{\"agg\":\"min\"}},\"value_count\":{\"NetworkOut\":{\"agg\":\"value_count\"},\"DiskReadBytes\":{\"agg\":\"value_count\"},\"CPUUtilization\":{\"agg\":\"value_count\"},\"NetworkIn\":{\"agg\":\"value_count\"}},\"max\":{\"CPUUtilization\":{\"agg\":\"max\"},\"DiskReadBytes\":{\"agg\":\"max\"}},\"date_histogram\":{\"@timestamp\":{\"agg\":\"date_histogram\",\"delay\":\"1d\",\"fixed_interval\":\"5m\",\"time_zone\":\"UTC\"}},\"terms\":{\"instance\":{\"agg\":\"terms\"},\"sourcetype.keyword\":{\"agg\":\"terms\"},\"region\":{\"agg\":\"terms\"}},\"sum\":{\"DiskReadBytes\":{\"agg\":\"sum\"},\"NetworkOut\":{\"agg\":\"sum\"}}}}", "title": "cloud_roll_index", "type": "rollup" }, diff --git a/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json index 2b2f8576d6769..b62bce700413a 100644 --- a/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json +++ b/x-pack/plugins/ml/server/models/job_service/new_job_caps/__mocks__/responses/rollup_caps.json @@ -37,7 +37,7 @@ { "agg": "date_histogram", "delay": "1d", - "interval": "5m", + "fixed_interval": "5m", "time_zone": "UTC" } ], @@ -123,7 +123,7 @@ { "agg": "date_histogram", "delay": "1d", - "interval": "5m", + "fixed_interval": "5m", "time_zone": "UTC" } ], @@ -174,7 +174,7 @@ { "agg": "date_histogram", "delay": "1d", - "interval": "5m", + "fixed_interval": "5m", "time_zone": "UTC" } ] diff --git a/x-pack/plugins/ml/server/plugin.ts b/x-pack/plugins/ml/server/plugin.ts index 39672f5b188bc..cf248fcc60896 100644 --- a/x-pack/plugins/ml/server/plugin.ts +++ b/x-pack/plugins/ml/server/plugin.ts @@ -67,7 +67,7 @@ export class MlServerPlugin implements Plugin 0) { - destinationIndex = body.data_frame_analytics[0].dest.index; - } - } catch (e) { - return response.customError(wrapError(e)); + try { + // Check if analyticsId is valid and get destination index + const { body } = await client.asInternalUser.ml.getDataFrameAnalytics({ + id: analyticsId, + }); + if (Array.isArray(body.data_frame_analytics) && body.data_frame_analytics.length > 0) { + destinationIndex = body.data_frame_analytics[0].dest.index; } + } catch (e) { + // exist early if the job doesn't exist + return response.customError(wrapError(e)); + } + if (deleteDestIndex || deleteDestIndexPattern) { // If user checks box to delete the destinationIndex associated with the job if (destinationIndex && deleteDestIndex) { // Verify if user has privilege to delete the destination index @@ -348,8 +350,8 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat index: destinationIndex, }); destIndexDeleted.success = true; - } catch (deleteIndexError) { - destIndexDeleted.error = wrapError(deleteIndexError); + } catch ({ body }) { + destIndexDeleted.error = body; } } else { return response.forbidden(); @@ -365,7 +367,7 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat } destIndexPatternDeleted.success = true; } catch (deleteDestIndexPatternError) { - destIndexPatternDeleted.error = wrapError(deleteDestIndexPatternError); + destIndexPatternDeleted.error = deleteDestIndexPatternError; } } } @@ -377,11 +379,8 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat id: analyticsId, }); analyticsJobDeleted.success = true; - } catch (deleteDFAError) { - analyticsJobDeleted.error = wrapError(deleteDFAError); - if (analyticsJobDeleted.error.statusCode === 404) { - return response.notFound(); - } + } catch ({ body }) { + analyticsJobDeleted.error = body; } const results = { analyticsJobDeleted, @@ -545,4 +544,38 @@ export function dataFrameAnalyticsRoutes({ router, mlLicense }: RouteInitializat } }) ); + + /** + * @apiGroup DataFrameAnalytics + * + * @api {get} /api/ml/data_frame/analytics/baseline Get analytics's feature importance baseline + * @apiName GetDataFrameAnalyticsBaseline + * @apiDescription Returns the baseline for data frame analytics job. + * + * @apiSchema (params) analyticsIdSchema + */ + router.post( + { + path: '/api/ml/data_frame/analytics/{analyticsId}/baseline', + validate: { + params: analyticsIdSchema, + }, + options: { + tags: ['access:ml:canGetDataFrameAnalytics'], + }, + }, + mlLicense.fullLicenseAPIGuard(async ({ client, request, response }) => { + try { + const { analyticsId } = request.params; + const { getRegressionAnalyticsBaseline } = analyticsFeatureImportanceProvider(client); + const baseline = await getRegressionAnalyticsBaseline(analyticsId); + + return response.ok({ + body: { baseline }, + }); + } catch (e) { + return response.customError(wrapError(e)); + } + }) + ); } diff --git a/x-pack/plugins/ml/server/routes/data_visualizer.ts b/x-pack/plugins/ml/server/routes/data_visualizer.ts index a697fe017f192..50d9be1be4230 100644 --- a/x-pack/plugins/ml/server/routes/data_visualizer.ts +++ b/x-pack/plugins/ml/server/routes/data_visualizer.ts @@ -84,7 +84,7 @@ export function dataVisualizerRoutes({ router, mlLicense }: RouteInitialization) /** * @apiGroup DataVisualizer * - * @api {post} /api/ml/data_visualizer/get_field_stats/:indexPatternTitle Get histograms for fields + * @api {post} /api/ml/data_visualizer/get_field_histograms/:indexPatternTitle Get histograms for fields * @apiName GetHistogramsForFields * @apiDescription Returns the histograms on a list fields in the specified index pattern. * diff --git a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts index 24e45514e1efc..57bc5578e92c5 100644 --- a/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts +++ b/x-pack/plugins/ml/server/routes/schemas/data_visualizer_schema.ts @@ -32,8 +32,8 @@ export const dataVisualizerFieldStatsSchema = schema.object({ earliest: schema.maybe(schema.number()), /** Latest timestamp for search, as epoch ms (optional). */ latest: schema.maybe(schema.number()), - /** Aggregation interval to use for obtaining document counts over time (optional). */ - interval: schema.maybe(schema.string()), + /** Aggregation interval, in milliseconds, to use for obtaining document counts over time (optional). */ + interval: schema.maybe(schema.number()), /** Maximum number of examples to return for text type fields. */ maxExamples: schema.number(), }); diff --git a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx index 56cba83813a63..c9f82eb521433 100644 --- a/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/cpu_usage_alert/cpu_usage_alert.tsx @@ -23,6 +23,6 @@ export function createCpuUsageAlertType(): AlertTypeModel { ), validate, defaultActionMessage: '{{context.internalFullMessage}}', - requiresAppContext: false, + requiresAppContext: true, }; } diff --git a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx index 58b37e43085ff..f6223d41ab30e 100644 --- a/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx +++ b/x-pack/plugins/monitoring/public/alerts/legacy_alert/legacy_alert.tsx @@ -33,7 +33,7 @@ export function createLegacyAlertTypes(): AlertTypeModel[] { ), defaultActionMessage: '{{context.internalFullMessage}}', validate: () => ({ errors: {} }), - requiresAppContext: false, + requiresAppContext: true, }; }); } diff --git a/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries.js b/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries.js index deaa4fd152cce..c4faf51dc000c 100644 --- a/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries.js +++ b/x-pack/plugins/monitoring/public/components/chart/monitoring_timeseries.js @@ -11,8 +11,8 @@ import { getColor } from './get_color'; import { TimeseriesVisualization } from './timeseries_visualization'; function formatTicksFor(series) { - const format = get(series, '.metric.format', '0,0.0'); - const units = get(series, '.metric.units', ''); + const format = get(series, 'metric.format', '0,0.0'); + const units = get(series, 'metric.units', ''); return function formatTicks(val) { let formatted = numeral(val).format(format); diff --git a/x-pack/plugins/monitoring/public/views/access_denied/index.js b/x-pack/plugins/monitoring/public/views/access_denied/index.js index 2db34842b9324..9f1303f5be522 100644 --- a/x-pack/plugins/monitoring/public/views/access_denied/index.js +++ b/x-pack/plugins/monitoring/public/views/access_denied/index.js @@ -4,7 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -import { kbnBaseUrl } from '../../../../../../src/plugins/kibana_legacy/common/kbn_base_url'; import { uiRoutes } from '../../angular/helpers/routes'; import template from './index.html'; @@ -35,7 +34,7 @@ uiRoutes.when('/access-denied', { const $interval = $injector.get('$interval'); // The template's "Back to Kibana" button click handler - this.goToKibanaURL = kbnBaseUrl; + this.goToKibanaURL = '/app/home'; // keep trying to load data in the background const accessPoller = $interval(() => tryPrivilege($http), 5 * 1000); // every 5 seconds diff --git a/x-pack/plugins/monitoring/server/config.test.ts b/x-pack/plugins/monitoring/server/config.test.ts index 32b8691bd6049..2efc325a3edec 100644 --- a/x-pack/plugins/monitoring/server/config.test.ts +++ b/x-pack/plugins/monitoring/server/config.test.ts @@ -86,6 +86,9 @@ describe('config schema', () => { "index": "filebeat-*", }, "max_bucket_size": 10000, + "metricbeat": Object { + "index": "metricbeat-*", + }, "min_interval_seconds": 10, "show_license_expiration": true, }, diff --git a/x-pack/plugins/monitoring/server/config.ts b/x-pack/plugins/monitoring/server/config.ts index 789211c43db31..6ae99e3d16d64 100644 --- a/x-pack/plugins/monitoring/server/config.ts +++ b/x-pack/plugins/monitoring/server/config.ts @@ -29,6 +29,9 @@ export const configSchema = schema.object({ logs: schema.object({ index: schema.string({ defaultValue: 'filebeat-*' }), }), + metricbeat: schema.object({ + index: schema.string({ defaultValue: 'metricbeat-*' }), + }), max_bucket_size: schema.number({ defaultValue: 10000 }), elasticsearch: monitoringElasticsearchConfigSchema, container: schema.object({ diff --git a/x-pack/plugins/monitoring/server/lib/ccs_utils.js b/x-pack/plugins/monitoring/server/lib/ccs_utils.js index dab1e87435c86..bef07124fb430 100644 --- a/x-pack/plugins/monitoring/server/lib/ccs_utils.js +++ b/x-pack/plugins/monitoring/server/lib/ccs_utils.js @@ -5,6 +5,21 @@ */ import { isFunction, get } from 'lodash'; +export function appendMetricbeatIndex(config, indexPattern) { + // Leverage this function to also append the dynamic metricbeat index too + let mbIndex = null; + // TODO: NP + // This function is called with both NP config and LP config + if (isFunction(config.get)) { + mbIndex = config.get('monitoring.ui.metricbeat.index'); + } else { + mbIndex = get(config, 'monitoring.ui.metricbeat.index'); + } + + const newIndexPattern = `${indexPattern},${mbIndex}`; + return newIndexPattern; +} + /** * Prefix all comma separated index patterns within the original {@code indexPattern}. * @@ -27,7 +42,7 @@ export function prefixIndexPattern(config, indexPattern, ccs) { } if (!ccsEnabled || !ccs) { - return indexPattern; + return appendMetricbeatIndex(config, indexPattern); } const patterns = indexPattern.split(','); @@ -35,10 +50,10 @@ export function prefixIndexPattern(config, indexPattern, ccs) { // if a wildcard is used, then we also want to search the local indices if (ccs === '*') { - return `${prefixedPattern},${indexPattern}`; + return appendMetricbeatIndex(config, `${prefixedPattern},${indexPattern}`); } - return prefixedPattern; + return appendMetricbeatIndex(config, prefixedPattern); } /** diff --git a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js index 16d42d896ca11..e91679eff2817 100644 --- a/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js +++ b/x-pack/plugins/monitoring/server/lib/cluster/get_clusters_from_request.js @@ -119,67 +119,75 @@ export async function getClustersFromRequest( // add alerts data if (isInCodePath(codePaths, [CODE_PATH_ALERTS])) { const alertsClient = req.getAlertsClient(); - if (alertsClient) { - for (const cluster of clusters) { - const verification = verifyMonitoringLicense(req.server); - if (!verification.enabled) { - // return metadata detailing that alerts is disabled because of the monitoring cluster license - cluster.alerts = { - alertsMeta: { - enabled: verification.enabled, - message: verification.message, // NOTE: this is only defined when the alert feature is disabled - }, - list: {}, - }; - continue; - } - - // check the license type of the production cluster for alerts feature support - const license = cluster.license || {}; - const prodLicenseInfo = checkLicenseForAlerts( - license.type, - license.status === 'active', - 'production' - ); - if (prodLicenseInfo.clusterAlerts.enabled) { - cluster.alerts = { - list: await fetchStatus( - alertsClient, - req.server.plugins.monitoring.info, - undefined, - cluster.cluster_uuid, - start, - end, - [] - ), - alertsMeta: { - enabled: true, - }, - }; - continue; - } + for (const cluster of clusters) { + const verification = verifyMonitoringLicense(req.server); + if (!verification.enabled) { + // return metadata detailing that alerts is disabled because of the monitoring cluster license + cluster.alerts = { + alertsMeta: { + enabled: verification.enabled, + message: verification.message, // NOTE: this is only defined when the alert feature is disabled + }, + list: {}, + }; + continue; + } + if (!alertsClient) { cluster.alerts = { list: {}, alertsMeta: { - enabled: true, - }, - clusterMeta: { enabled: false, - message: i18n.translate( - 'xpack.monitoring.clusterAlerts.unsupportedClusterAlertsDescription', - { - defaultMessage: - 'Cluster [{clusterName}] license type [{licenseType}] does not support Cluster Alerts', - values: { - clusterName: cluster.cluster_name, - licenseType: `${license.type}`, - }, - } - ), }, }; + continue; + } + + // check the license type of the production cluster for alerts feature support + const license = cluster.license || {}; + const prodLicenseInfo = checkLicenseForAlerts( + license.type, + license.status === 'active', + 'production' + ); + if (prodLicenseInfo.clusterAlerts.enabled) { + cluster.alerts = { + list: await fetchStatus( + alertsClient, + req.server.plugins.monitoring.info, + undefined, + cluster.cluster_uuid, + start, + end, + [] + ), + alertsMeta: { + enabled: true, + }, + }; + continue; } + + cluster.alerts = { + list: {}, + alertsMeta: { + enabled: false, + }, + clusterMeta: { + enabled: false, + message: i18n.translate( + 'xpack.monitoring.clusterAlerts.unsupportedClusterAlertsDescription', + { + defaultMessage: + 'Cluster [{clusterName}] license type [{licenseType}] does not support Cluster Alerts', + values: { + clusterName: cluster.cluster_name, + licenseType: `${license.type}`, + }, + } + ), + }, + }; } } } diff --git a/x-pack/plugins/monitoring/server/lib/create_query.js b/x-pack/plugins/monitoring/server/lib/create_query.js index 04e0d7642ec58..1983dc3dcf9af 100644 --- a/x-pack/plugins/monitoring/server/lib/create_query.js +++ b/x-pack/plugins/monitoring/server/lib/create_query.js @@ -57,7 +57,7 @@ export function createQuery(options) { let typeFilter; if (type) { - typeFilter = { term: { type } }; + typeFilter = { bool: { should: [{ term: { type } }, { term: { 'metricset.name': type } }] } }; } let clusterUuidFilter; diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js index 6abb392e58818..84384021a3593 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_node_summary.js @@ -17,15 +17,23 @@ export function handleResponse(clusterState, shardStats, nodeUuid) { return (response) => { let nodeSummary = {}; const nodeStatsHits = get(response, 'hits.hits', []); - const nodes = nodeStatsHits.map((hit) => hit._source.source_node); // using [0] value because query results are sorted desc per timestamp + const nodes = nodeStatsHits.map((hit) => + get(hit, '_source.elasticsearch.node', hit._source.source_node) + ); // using [0] value because query results are sorted desc per timestamp const node = nodes[0] || getDefaultNodeFromId(nodeUuid); - const sourceStats = get(response, 'hits.hits[0]._source.node_stats'); + const sourceStats = + get(response, 'hits.hits[0]._source.elasticsearch.node.stats') || + get(response, 'hits.hits[0]._source.node_stats'); const clusterNode = get(clusterState, ['nodes', nodeUuid]); const stats = { resolver: nodeUuid, - node_ids: nodes.map((node) => node.uuid), + node_ids: nodes.map((node) => node.id || node.uuid), attributes: node.attributes, - transport_address: node.transport_address, + transport_address: get( + response, + 'hits.hits[0]._source.service.address', + node.transport_address + ), name: node.name, type: node.type, }; @@ -45,10 +53,17 @@ export function handleResponse(clusterState, shardStats, nodeUuid) { totalShards: _shardStats.shardCount, indexCount: _shardStats.indexCount, documents: get(sourceStats, 'indices.docs.count'), - dataSize: get(sourceStats, 'indices.store.size_in_bytes'), - freeSpace: get(sourceStats, 'fs.total.available_in_bytes'), - totalSpace: get(sourceStats, 'fs.total.total_in_bytes'), - usedHeap: get(sourceStats, 'jvm.mem.heap_used_percent'), + dataSize: + get(sourceStats, 'indices.store.size_in_bytes') || + get(sourceStats, 'indices.store.size.bytes'), + freeSpace: + get(sourceStats, 'fs.total.available_in_bytes') || + get(sourceStats, 'fs.summary.available.bytes'), + totalSpace: + get(sourceStats, 'fs.total.total_in_bytes') || get(sourceStats, 'fs.summary.total.bytes'), + usedHeap: + get(sourceStats, 'jvm.mem.heap_used_percent') || + get(sourceStats, 'jvm.mem.heap.used.pct'), status: i18n.translate('xpack.monitoring.es.nodes.onlineStatusLabel', { defaultMessage: 'Online', }), diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js index 573f1792e5f8a..68bca96e2911b 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_node_ids.js @@ -19,6 +19,7 @@ export async function getNodeIds(req, indexPattern, { clusterUuid }, size) { filterPath: ['aggregations.composite_data.buckets'], body: { query: createQuery({ + type: 'node_stats', start, end, metric: ElasticsearchMetric.getMetricFields(), diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js index 682da324ee72f..c2794b7e7fa44 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/get_nodes.js @@ -96,6 +96,7 @@ export async function getNodes(req, esIndexPattern, pageOfNodes, clusterStats, n }, filterPath: [ 'hits.hits._source.source_node', + 'hits.hits._source.elasticsearch.node', 'aggregations.nodes.buckets.key', ...LISTING_METRICS_PATHS, ], diff --git a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js index 3c719c2ddfbf8..317c1cddf57ae 100644 --- a/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js +++ b/x-pack/plugins/monitoring/server/lib/elasticsearch/nodes/get_nodes/map_nodes_info.js @@ -17,25 +17,29 @@ export function mapNodesInfo(nodeHits, clusterStats, nodesShardCount) { const clusterState = get(clusterStats, 'cluster_state', { nodes: {} }); return nodeHits.reduce((prev, node) => { - const sourceNode = get(node, '_source.source_node'); + const sourceNode = get(node, '_source.source_node') || get(node, '_source.elasticsearch.node'); const calculatedNodeType = calculateNodeType(sourceNode, get(clusterState, 'master_node')); const { nodeType, nodeTypeLabel, nodeTypeClass } = getNodeTypeClassLabel( sourceNode, calculatedNodeType ); - const isOnline = !isUndefined(get(clusterState, ['nodes', sourceNode.uuid])); + const isOnline = !isUndefined(get(clusterState, ['nodes', sourceNode.uuid || sourceNode.id])); return { ...prev, - [sourceNode.uuid]: { + [sourceNode.uuid || sourceNode.id]: { name: sourceNode.name, transport_address: sourceNode.transport_address, type: nodeType, isOnline, nodeTypeLabel: nodeTypeLabel, nodeTypeClass: nodeTypeClass, - shardCount: get(nodesShardCount, `nodes[${sourceNode.uuid}].shardCount`, 0), + shardCount: get( + nodesShardCount, + `nodes[${sourceNode.uuid || sourceNode.id}].shardCount`, + 0 + ), }, }; }, {}); diff --git a/x-pack/plugins/monitoring/server/plugin.ts b/x-pack/plugins/monitoring/server/plugin.ts index f5cbadb523a81..d874c868ae8e8 100644 --- a/x-pack/plugins/monitoring/server/plugin.ts +++ b/x-pack/plugins/monitoring/server/plugin.ts @@ -239,7 +239,7 @@ export class Plugin { } registerPluginInUI(plugins: PluginsSetup) { - plugins.features.registerFeature({ + plugins.features.registerKibanaFeature({ id: 'monitoring', name: i18n.translate('xpack.monitoring.featureRegistry.monitoringFeatureName', { defaultMessage: 'Stack Monitoring', diff --git a/x-pack/plugins/observability/public/components/app/header/index.tsx b/x-pack/plugins/observability/public/components/app/header/index.tsx index 0e35fbb008bee..e8bd229265e37 100644 --- a/x-pack/plugins/observability/public/components/app/header/index.tsx +++ b/x-pack/plugins/observability/public/components/app/header/index.tsx @@ -5,7 +5,6 @@ */ import { - EuiBetaBadge, EuiButtonEmpty, EuiFlexGroup, EuiFlexItem, @@ -58,12 +57,7 @@ export function Header({

{i18n.translate('xpack.observability.home.title', { defaultMessage: 'Observability', - })}{' '} - + })}

diff --git a/x-pack/plugins/observability/public/components/app/news_feed/index.tsx b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx index 625ae94c90aa2..86466baa45410 100644 --- a/x-pack/plugins/observability/public/components/app/news_feed/index.tsx +++ b/x-pack/plugins/observability/public/components/app/news_feed/index.tsx @@ -70,13 +70,13 @@ function NewsItem({ item }: { item: INewsItem }) { - - + + {i18n.translate('xpack.observability.news.readFullStory', { defaultMessage: 'Read full story', })} - - + + diff --git a/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx b/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx index 13f7159ba6043..b5bfe3eec7d35 100644 --- a/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx +++ b/x-pack/plugins/observability/public/hooks/use_chart_theme.tsx @@ -4,10 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ import { EUI_CHARTS_THEME_DARK, EUI_CHARTS_THEME_LIGHT } from '@elastic/eui/dist/eui_charts_theme'; -import { useContext } from 'react'; -import { ThemeContext } from 'styled-components'; +import { useTheme } from './use_theme'; export function useChartTheme() { - const theme = useContext(ThemeContext); + const theme = useTheme(); return theme.darkMode ? EUI_CHARTS_THEME_DARK.theme : EUI_CHARTS_THEME_LIGHT.theme; } diff --git a/x-pack/plugins/observability/public/hooks/use_theme.tsx b/x-pack/plugins/observability/public/hooks/use_theme.tsx new file mode 100644 index 0000000000000..d0449a4432d93 --- /dev/null +++ b/x-pack/plugins/observability/public/hooks/use_theme.tsx @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { useContext } from 'react'; +import { ThemeContext } from 'styled-components'; +import { EuiTheme } from '../../../../legacy/common/eui_styled_components'; + +export function useTheme() { + const theme: EuiTheme = useContext(ThemeContext); + return theme; +} diff --git a/x-pack/plugins/observability/public/index.ts b/x-pack/plugins/observability/public/index.ts index 03939736b64ae..0aecea59ad013 100644 --- a/x-pack/plugins/observability/public/index.ts +++ b/x-pack/plugins/observability/public/index.ts @@ -26,3 +26,6 @@ export { } from './hooks/use_track_metric'; export * from './typings'; + +export { useChartTheme } from './hooks/use_chart_theme'; +export { useTheme } from './hooks/use_theme'; diff --git a/x-pack/plugins/observability/public/pages/overview/index.tsx b/x-pack/plugins/observability/public/pages/overview/index.tsx index 8870bcbc9fa38..10bbdaaae34a8 100644 --- a/x-pack/plugins/observability/public/pages/overview/index.tsx +++ b/x-pack/plugins/observability/public/pages/overview/index.tsx @@ -200,7 +200,7 @@ export function OverviewPage({ routeParams }: Props) { {!!newsFeed?.items?.length && ( - + )} diff --git a/x-pack/plugins/oss_telemetry/constants.ts b/x-pack/plugins/oss_telemetry/constants.ts deleted file mode 100644 index 1e83bff092f2c..0000000000000 --- a/x-pack/plugins/oss_telemetry/constants.ts +++ /dev/null @@ -1,9 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export const PLUGIN_ID = 'oss_telemetry'; // prefix used for registering properties with services from this plugin -export const VIS_TELEMETRY_TASK = 'vis_telemetry'; // suffix for the _id of our task instance, which must be `get`-able -export const VIS_USAGE_TYPE = 'visualization_types'; // suffix for the properties of data registered with the usage service diff --git a/x-pack/plugins/oss_telemetry/kibana.json b/x-pack/plugins/oss_telemetry/kibana.json deleted file mode 100644 index 0defee0881e0e..0000000000000 --- a/x-pack/plugins/oss_telemetry/kibana.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "id": "ossTelemetry", - "server": true, - "version": "8.0.0", - "kibanaVersion": "kibana", - "requiredPlugins": ["usageCollection", "taskManager"], - "configPath": ["xpack", "oss_telemetry"], - "ui": false -} diff --git a/x-pack/plugins/oss_telemetry/server/index.ts b/x-pack/plugins/oss_telemetry/server/index.ts deleted file mode 100644 index 64527ca6daa7e..0000000000000 --- a/x-pack/plugins/oss_telemetry/server/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { PluginInitializerContext } from 'src/core/server'; -import { OssTelemetryPlugin } from './plugin'; - -export const plugin = (context: PluginInitializerContext) => new OssTelemetryPlugin(context); - -export * from './plugin'; diff --git a/x-pack/plugins/oss_telemetry/server/lib/collectors/index.ts b/x-pack/plugins/oss_telemetry/server/lib/collectors/index.ts deleted file mode 100644 index 845e11b80af0e..0000000000000 --- a/x-pack/plugins/oss_telemetry/server/lib/collectors/index.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { registerVisualizationsCollector } from './visualizations/register_usage_collector'; -import { UsageCollectionSetup } from '../../../../../../src/plugins/usage_collection/server'; -import { TaskManagerStartContract } from '../../../../task_manager/server'; - -export function registerCollectors( - usageCollection: UsageCollectionSetup, - taskManager: Promise -) { - registerVisualizationsCollector(usageCollection, taskManager); -} diff --git a/x-pack/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.test.ts b/x-pack/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.test.ts deleted file mode 100644 index 43114787b40e5..0000000000000 --- a/x-pack/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - getMockTaskFetch, - getMockThrowingTaskFetch, - getMockTaskInstance, -} from '../../../test_utils'; -import { taskManagerMock } from '../../../../../task_manager/server/task_manager.mock'; -import { getUsageCollector } from './get_usage_collector'; - -describe('getVisualizationsCollector#fetch', () => { - test('can return empty stats', async () => { - const { type, fetch } = getUsageCollector( - Promise.resolve(taskManagerMock.start(getMockTaskFetch())) - ); - expect(type).toBe('visualization_types'); - const fetchResult = await fetch(); - expect(fetchResult).toEqual({}); - }); - - test('provides known stats', async () => { - const { type, fetch } = getUsageCollector( - Promise.resolve( - taskManagerMock.start( - getMockTaskFetch([ - getMockTaskInstance({ - state: { - runs: 1, - stats: { comic_books: { total: 16, max: 12, min: 2, avg: 6 } }, - }, - taskType: 'test', - params: {}, - }), - ]) - ) - ) - ); - expect(type).toBe('visualization_types'); - const fetchResult = await fetch(); - expect(fetchResult).toEqual({ comic_books: { avg: 6, max: 12, min: 2, total: 16 } }); - }); - - describe('Error handling', () => { - test('Silently handles Task Manager NotInitialized', async () => { - const { fetch } = getUsageCollector( - Promise.resolve( - taskManagerMock.start( - getMockThrowingTaskFetch( - new Error('NotInitialized taskManager is still waiting for plugins to load') - ) - ) - ) - ); - const result = await fetch(); - expect(result).toBe(undefined); - }); - // In real life, the CollectorSet calls fetch and handles errors - test('defers the errors', async () => { - const { fetch } = getUsageCollector( - Promise.resolve(taskManagerMock.start(getMockThrowingTaskFetch(new Error('BOOM')))) - ); - await expect(fetch()).rejects.toThrowErrorMatchingInlineSnapshot(`"BOOM"`); - }); - }); -}); diff --git a/x-pack/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.ts b/x-pack/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.ts deleted file mode 100644 index 9828dea4c9393..0000000000000 --- a/x-pack/plugins/oss_telemetry/server/lib/collectors/visualizations/get_usage_collector.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { get } from 'lodash'; -import { PLUGIN_ID, VIS_TELEMETRY_TASK, VIS_USAGE_TYPE } from '../../../../constants'; -import { TaskManagerStartContract } from '../../../../../task_manager/server'; - -async function fetch(taskManager: TaskManagerStartContract) { - let docs; - try { - ({ docs } = await taskManager.fetch({ - query: { bool: { filter: { term: { _id: `task:${PLUGIN_ID}-${VIS_TELEMETRY_TASK}` } } } }, - })); - } catch (err) { - const errMessage = err && err.message ? err.message : err.toString(); - /* - The usage service WILL to try to fetch from this collector before the task manager has been initialized, because the task manager has to wait for all plugins to initialize first. It's fine to ignore it as next time around it will be initialized (or it will throw a different type of error) - */ - if (errMessage.includes('NotInitialized')) { - docs = null; - } else { - throw err; - } - } - - return docs; -} - -export function getUsageCollector(taskManager: Promise) { - return { - type: VIS_USAGE_TYPE, - isReady: () => true, - fetch: async () => { - const docs = await fetch(await taskManager); - // get the accumulated state from the recurring task - return get(docs, '[0].state.stats'); - }, - }; -} diff --git a/x-pack/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts b/x-pack/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts deleted file mode 100644 index 667e8b9b875fd..0000000000000 --- a/x-pack/plugins/oss_telemetry/server/lib/collectors/visualizations/register_usage_collector.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; -import { TaskManagerStartContract } from '../../../../../task_manager/server'; -import { getUsageCollector } from './get_usage_collector'; - -export function registerVisualizationsCollector( - collectorSet: UsageCollectionSetup, - taskManager: Promise -): void { - const collector = collectorSet.makeUsageCollector(getUsageCollector(taskManager)); - collectorSet.registerCollector(collector); -} diff --git a/x-pack/plugins/oss_telemetry/server/lib/get_next_midnight.test.ts b/x-pack/plugins/oss_telemetry/server/lib/get_next_midnight.test.ts deleted file mode 100644 index 3bafb84d61157..0000000000000 --- a/x-pack/plugins/oss_telemetry/server/lib/get_next_midnight.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment'; -import { getNextMidnight } from './get_next_midnight'; - -describe('getNextMidnight', () => { - test('Returns the next time and date of midnight as an iso string', () => { - const nextMidnightMoment = moment().add(1, 'days').startOf('day').toDate(); - - expect(getNextMidnight()).toEqual(nextMidnightMoment); - }); -}); diff --git a/x-pack/plugins/oss_telemetry/server/lib/get_next_midnight.ts b/x-pack/plugins/oss_telemetry/server/lib/get_next_midnight.ts deleted file mode 100644 index a5ee8d572343c..0000000000000 --- a/x-pack/plugins/oss_telemetry/server/lib/get_next_midnight.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export function getNextMidnight() { - const nextMidnight = new Date(); - nextMidnight.setHours(0, 0, 0, 0); - nextMidnight.setDate(nextMidnight.getDate() + 1); - return nextMidnight; -} diff --git a/x-pack/plugins/oss_telemetry/server/lib/get_past_days.test.ts b/x-pack/plugins/oss_telemetry/server/lib/get_past_days.test.ts deleted file mode 100644 index 28909779343a5..0000000000000 --- a/x-pack/plugins/oss_telemetry/server/lib/get_past_days.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import moment from 'moment'; -import { getPastDays } from './get_past_days'; - -describe('getPastDays', () => { - test('Returns 2 days that have passed from the current date', () => { - const pastDate = moment().subtract(2, 'days').startOf('day').toString(); - - expect(getPastDays(pastDate)).toEqual(2); - }); - - test('Returns 30 days that have passed from the current date', () => { - const pastDate = moment().subtract(30, 'days').startOf('day').toString(); - - expect(getPastDays(pastDate)).toEqual(30); - }); -}); diff --git a/x-pack/plugins/oss_telemetry/server/lib/get_past_days.ts b/x-pack/plugins/oss_telemetry/server/lib/get_past_days.ts deleted file mode 100644 index 4f25ef147ad43..0000000000000 --- a/x-pack/plugins/oss_telemetry/server/lib/get_past_days.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; - * you may not use this file except in compliance with the Elastic License. - */ -export const getPastDays = (dateString: string): number => { - const date = new Date(dateString); - const today = new Date(); - const diff = Math.abs(date.getTime() - today.getTime()); - return Math.trunc(diff / (1000 * 60 * 60 * 24)); -}; diff --git a/x-pack/plugins/oss_telemetry/server/lib/tasks/index.ts b/x-pack/plugins/oss_telemetry/server/lib/tasks/index.ts deleted file mode 100644 index 415aeb2791d9e..0000000000000 --- a/x-pack/plugins/oss_telemetry/server/lib/tasks/index.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Observable } from 'rxjs'; -import { CoreSetup, Logger } from 'kibana/server'; -import { PLUGIN_ID, VIS_TELEMETRY_TASK } from '../../../constants'; -import { visualizationsTaskRunner } from './visualizations/task_runner'; -import { - TaskInstance, - TaskManagerStartContract, - TaskManagerSetupContract, -} from '../../../../task_manager/server'; - -export function registerTasks({ - taskManager, - logger, - getStartServices, - config, -}: { - taskManager?: TaskManagerSetupContract; - logger: Logger; - getStartServices: CoreSetup['getStartServices']; - config: Observable<{ kibana: { index: string } }>; -}) { - if (!taskManager) { - logger.debug('Task manager is not available'); - return; - } - - const esClientPromise = getStartServices().then( - ([{ elasticsearch }]) => elasticsearch.legacy.client - ); - - taskManager.registerTaskDefinitions({ - [VIS_TELEMETRY_TASK]: { - title: 'X-Pack telemetry calculator for Visualizations', - type: VIS_TELEMETRY_TASK, - createTaskRunner({ taskInstance }: { taskInstance: TaskInstance }) { - return { - run: visualizationsTaskRunner(taskInstance, config, esClientPromise), - cancel: async () => {}, - }; - }, - }, - }); -} - -export async function scheduleTasks({ - taskManager, - logger, -}: { - taskManager?: TaskManagerStartContract; - logger: Logger; -}) { - if (!taskManager) { - logger.debug('Task manager is not available'); - return; - } - - try { - await taskManager.ensureScheduled({ - id: `${PLUGIN_ID}-${VIS_TELEMETRY_TASK}`, - taskType: VIS_TELEMETRY_TASK, - state: { stats: {}, runs: 0 }, - params: {}, - }); - } catch (e) { - logger.debug(`Error scheduling task, received ${e.message}`); - } -} diff --git a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.test.ts b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.test.ts deleted file mode 100644 index c064f39f4bc6a..0000000000000 --- a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.test.ts +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { - getMockCallWithInternal, - getMockConfig, - getMockEs, - getMockTaskInstance, -} from '../../../test_utils'; -import { visualizationsTaskRunner } from './task_runner'; -import { TaskInstance } from '../../../../../task_manager/server'; -import { getNextMidnight } from '../../get_next_midnight'; -import moment from 'moment'; - -describe('visualizationsTaskRunner', () => { - let mockTaskInstance: TaskInstance; - beforeEach(() => { - mockTaskInstance = getMockTaskInstance(); - }); - - describe('Error handling', () => { - test('catches its own errors', async () => { - const mockCallWithInternal = () => Promise.reject(new Error('Things did not go well!')); - - const runner = visualizationsTaskRunner( - mockTaskInstance, - getMockConfig(), - getMockEs(mockCallWithInternal) - ); - const result = await runner(); - expect(result).toMatchObject({ - error: 'Things did not go well!', - state: { - runs: 1, - stats: undefined, - }, - }); - }); - }); - - test('Summarizes visualization response data', async () => { - const runner = visualizationsTaskRunner(mockTaskInstance, getMockConfig(), getMockEs()); - const result = await runner(); - - expect(result).toMatchObject({ - error: undefined, - runAt: getNextMidnight(), - state: { - runs: 1, - stats: { - shell_beads: { - spaces_avg: 1, - spaces_max: 1, - spaces_min: 1, - total: 1, - saved_7_days_total: 1, - saved_30_days_total: 1, - saved_90_days_total: 1, - }, - }, - }, - }); - }); - - test('Summarizes visualization response data per Space', async () => { - const mockCallWithInternal = getMockCallWithInternal([ - // default space - { - _id: 'visualization:coolviz-123', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "cave_painting"}' }, - updated_at: moment().subtract(7, 'days').startOf('day').toString(), - }, - }, - { - _id: 'visualization:coolviz-456', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "printing_press"}' }, - updated_at: moment().subtract(20, 'days').startOf('day').toString(), - }, - }, - { - _id: 'meat:visualization:coolviz-789', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "floppy_disk"}' }, - updated_at: moment().subtract(2, 'months').startOf('day').toString(), - }, - }, - // meat space - { - _id: 'meat:visualization:coolviz-789', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "cave_painting"}' }, - updated_at: moment().subtract(89, 'days').startOf('day').toString(), - }, - }, - { - _id: 'meat:visualization:coolviz-789', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "cuneiform"}' }, - updated_at: moment().subtract(5, 'months').startOf('day').toString(), - }, - }, - { - _id: 'meat:visualization:coolviz-789', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "cuneiform"}' }, - updated_at: moment().subtract(2, 'days').startOf('day').toString(), - }, - }, - { - _id: 'meat:visualization:coolviz-789', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "floppy_disk"}' }, - updated_at: moment().subtract(7, 'days').startOf('day').toString(), - }, - }, - // cyber space - { - _id: 'cyber:visualization:coolviz-789', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "floppy_disk"}' }, - updated_at: moment().subtract(7, 'months').startOf('day').toString(), - }, - }, - { - _id: 'cyber:visualization:coolviz-789', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "floppy_disk"}' }, - updated_at: moment().subtract(3, 'days').startOf('day').toString(), - }, - }, - { - _id: 'cyber:visualization:coolviz-123', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "cave_painting"}' }, - updated_at: moment().subtract(15, 'days').startOf('day').toString(), - }, - }, - ]); - - const expectedStats = { - cave_painting: { - total: 3, - spaces_min: 1, - spaces_max: 1, - spaces_avg: 1, - saved_7_days_total: 1, - saved_30_days_total: 2, - saved_90_days_total: 3, - }, - printing_press: { - total: 1, - spaces_min: 1, - spaces_max: 1, - spaces_avg: 1, - saved_7_days_total: 0, - saved_30_days_total: 1, - saved_90_days_total: 1, - }, - cuneiform: { - total: 2, - spaces_min: 2, - spaces_max: 2, - spaces_avg: 2, - saved_7_days_total: 1, - saved_30_days_total: 1, - saved_90_days_total: 1, - }, - floppy_disk: { - total: 4, - spaces_min: 2, - spaces_max: 2, - spaces_avg: 2, - saved_7_days_total: 2, - saved_30_days_total: 2, - saved_90_days_total: 3, - }, - }; - - const runner = visualizationsTaskRunner( - mockTaskInstance, - getMockConfig(), - getMockEs(mockCallWithInternal) - ); - const result = await runner(); - - expect(result).toMatchObject({ - error: undefined, - state: { - runs: 1, - stats: expectedStats, - }, - }); - - expect(result.state.stats).toMatchObject(expectedStats); - }); -}); diff --git a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts b/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts deleted file mode 100644 index 27913fafe3257..0000000000000 --- a/x-pack/plugins/oss_telemetry/server/lib/tasks/visualizations/task_runner.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Observable } from 'rxjs'; -import _, { countBy, groupBy, mapValues } from 'lodash'; -import { first } from 'rxjs/operators'; - -import { LegacyAPICaller, ILegacyClusterClient } from 'src/core/server'; -import { getNextMidnight } from '../../get_next_midnight'; -import { getPastDays } from '../../get_past_days'; -import { TaskInstance } from '../../../../../task_manager/server'; -import { ESSearchHit } from '../../../../../apm/typings/elasticsearch'; - -interface VisSummary { - type: string; - space: string; - past_days: number; -} - -/* - * Parse the response data into telemetry payload - */ -async function getStats(callCluster: LegacyAPICaller, index: string) { - const searchParams = { - size: 10000, // elasticsearch index.max_result_window default value - index, - ignoreUnavailable: true, - filterPath: [ - 'hits.hits._id', - 'hits.hits._source.visualization', - 'hits.hits._source.updated_at', - ], - body: { - query: { - bool: { filter: { term: { type: 'visualization' } } }, - }, - }, - }; - const esResponse = await callCluster('search', searchParams); - const size = _.get(esResponse, 'hits.hits.length') as number; - if (size < 1) { - return; - } - - // `map` to get the raw types - const visSummaries: VisSummary[] = esResponse.hits.hits.map( - (hit: ESSearchHit<{ visState: string }>) => { - const spacePhrases: string[] = hit._id.split(':'); - const lastUpdated: string = _.get(hit, '_source.updated_at'); - const space = spacePhrases.length === 3 ? spacePhrases[0] : 'default'; // if in a custom space, the format of a saved object ID is space:type:id - const visualization = _.get(hit, '_source.visualization', { visState: '{}' }); - const visState: { type?: string } = JSON.parse(visualization.visState); - return { - type: visState.type || '_na_', - space, - past_days: getPastDays(lastUpdated), - }; - } - ); - - // organize stats per type - const visTypes = groupBy(visSummaries, 'type'); - - // get the final result - return mapValues(visTypes, (curr) => { - const total = curr.length; - const spacesBreakdown = countBy(curr, 'space'); - const spaceCounts: number[] = _.values(spacesBreakdown); - - return { - total, - spaces_min: _.min(spaceCounts), - spaces_max: _.max(spaceCounts), - spaces_avg: total / spaceCounts.length, - saved_7_days_total: curr.filter((c) => c.past_days <= 7).length, - saved_30_days_total: curr.filter((c) => c.past_days <= 30).length, - saved_90_days_total: curr.filter((c) => c.past_days <= 90).length, - }; - }); -} - -export function visualizationsTaskRunner( - taskInstance: TaskInstance, - config: Observable<{ kibana: { index: string } }>, - esClientPromise: Promise -) { - return async () => { - let stats; - let error; - - try { - const index = (await config.pipe(first()).toPromise()).kibana.index; - stats = await getStats((await esClientPromise).callAsInternalUser, index); - } catch (err) { - if (err.constructor === Error) { - error = err.message; - } else { - error = err; - } - } - - return { - runAt: getNextMidnight(), - state: { - runs: taskInstance.state.runs + 1, - stats, - }, - error, - }; - }; -} diff --git a/x-pack/plugins/oss_telemetry/server/plugin.ts b/x-pack/plugins/oss_telemetry/server/plugin.ts deleted file mode 100644 index 6a447da66952a..0000000000000 --- a/x-pack/plugins/oss_telemetry/server/plugin.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Observable } from 'rxjs'; -import { CoreSetup, CoreStart, Logger, Plugin, PluginInitializerContext } from 'kibana/server'; -import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; -import { registerCollectors } from './lib/collectors'; -import { registerTasks, scheduleTasks } from './lib/tasks'; -import { UsageCollectionSetup } from '../../../../src/plugins/usage_collection/server'; - -export interface OssTelemetrySetupDependencies { - usageCollection: UsageCollectionSetup; - taskManager: TaskManagerSetupContract; -} -export interface OssTelemetryStartDependencies { - taskManager: TaskManagerStartContract; -} - -export class OssTelemetryPlugin implements Plugin { - private readonly logger: Logger; - private readonly config: Observable<{ kibana: { index: string } }>; - - constructor(initializerContext: PluginInitializerContext) { - this.logger = initializerContext.logger.get('oss_telemetry'); - this.config = initializerContext.config.legacy.globalConfig$; - } - - public setup( - core: CoreSetup, - deps: OssTelemetrySetupDependencies - ) { - registerTasks({ - taskManager: deps.taskManager, - logger: this.logger, - getStartServices: core.getStartServices, - config: this.config, - }); - registerCollectors( - deps.usageCollection, - core.getStartServices().then(([_, { taskManager }]) => taskManager) - ); - } - - public start(core: CoreStart, deps: OssTelemetryStartDependencies) { - scheduleTasks({ - taskManager: deps.taskManager, - logger: this.logger, - }); - } -} diff --git a/x-pack/plugins/oss_telemetry/server/test_utils/index.ts b/x-pack/plugins/oss_telemetry/server/test_utils/index.ts deleted file mode 100644 index 9201899d5a161..0000000000000 --- a/x-pack/plugins/oss_telemetry/server/test_utils/index.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { LegacyAPICaller } from 'kibana/server'; - -import { of } from 'rxjs'; -import moment from 'moment'; -import { elasticsearchServiceMock } from '../../../../../src/core/server/mocks'; -import { - ConcreteTaskInstance, - TaskStatus, - TaskManagerStartContract, -} from '../../../task_manager/server'; - -export const getMockTaskInstance = ( - overrides: Partial = {} -): ConcreteTaskInstance => ({ - state: { runs: 0, stats: {} }, - taskType: 'test', - params: {}, - id: '', - scheduledAt: new Date(), - attempts: 1, - status: TaskStatus.Idle, - runAt: new Date(), - startedAt: null, - retryAt: null, - ownerId: null, - ...overrides, -}); - -const defaultMockSavedObjects = [ - { - _id: 'visualization:coolviz-123', - _source: { - type: 'visualization', - visualization: { visState: '{"type": "shell_beads"}' }, - updated_at: moment().subtract(7, 'days').startOf('day').toString(), - }, - }, -]; - -const defaultMockTaskDocs = [getMockTaskInstance()]; - -export const getMockEs = async ( - mockCallWithInternal: LegacyAPICaller = getMockCallWithInternal() -) => { - const client = elasticsearchServiceMock.createLegacyClusterClient(); - (client.callAsInternalUser as any) = mockCallWithInternal; - return client; -}; - -export const getMockCallWithInternal = ( - hits: unknown[] = defaultMockSavedObjects -): LegacyAPICaller => { - return ((() => { - return Promise.resolve({ hits: { hits } }); - }) as unknown) as LegacyAPICaller; -}; - -export const getMockTaskFetch = ( - docs: ConcreteTaskInstance[] = defaultMockTaskDocs -): Partial> => { - return { - fetch: jest.fn((fetchOpts) => { - return Promise.resolve({ docs, searchAfter: [] }); - }), - } as Partial>; -}; - -export const getMockThrowingTaskFetch = ( - throws: Error -): Partial> => { - return { - fetch: jest.fn((fetchOpts) => { - throw throws; - }), - } as Partial>; -}; - -export const getMockConfig = () => { - return of({ kibana: { index: '' } }); -}; - -export const getCluster = () => ({ - callWithInternalUser: getMockCallWithInternal(), -}); diff --git a/x-pack/plugins/remote_clusters/kibana.json b/x-pack/plugins/remote_clusters/kibana.json index d90d6ea460573..0334af5a868f2 100644 --- a/x-pack/plugins/remote_clusters/kibana.json +++ b/x-pack/plugins/remote_clusters/kibana.json @@ -8,7 +8,8 @@ "requiredPlugins": [ "licensing", "management", - "indexManagement" + "indexManagement", + "features" ], "optionalPlugins": [ "usageCollection", diff --git a/x-pack/plugins/remote_clusters/server/plugin.ts b/x-pack/plugins/remote_clusters/server/plugin.ts index 9b2d6a0a05385..0bef5d70fe70d 100644 --- a/x-pack/plugins/remote_clusters/server/plugin.ts +++ b/x-pack/plugins/remote_clusters/server/plugin.ts @@ -35,7 +35,7 @@ export class RemoteClustersServerPlugin this.licenseStatus = { valid: false }; } - async setup({ http }: CoreSetup, { licensing, cloud }: Dependencies) { + async setup({ http }: CoreSetup, { features, licensing, cloud }: Dependencies) { const router = http.createRouter(); const config = await this.config$.pipe(first()).toPromise(); @@ -47,6 +47,19 @@ export class RemoteClustersServerPlugin }, }; + features.registerElasticsearchFeature({ + id: 'remote_clusters', + management: { + data: ['remote_clusters'], + }, + privileges: [ + { + requiredClusterPrivileges: ['manage'], + ui: [], + }, + ], + }); + // Register routes registerGetRoute(routeDependencies); registerAddRoute(routeDependencies); diff --git a/x-pack/plugins/remote_clusters/server/types.ts b/x-pack/plugins/remote_clusters/server/types.ts index 23f4ed158c2d4..86862a90da9c1 100644 --- a/x-pack/plugins/remote_clusters/server/types.ts +++ b/x-pack/plugins/remote_clusters/server/types.ts @@ -5,12 +5,14 @@ */ import { IRouter } from 'kibana/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { CloudSetup } from '../../cloud/server'; export interface Dependencies { licensing: LicensingPluginSetup; cloud: CloudSetup; + features: FeaturesPluginSetup; } export interface RouteDependencies { diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index c461c2de4e2ad..e5bca43cef562 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -16,6 +16,7 @@ export const API_BASE_URL_V1 = '/api/reporting/v1'; // export const API_BASE_GENERATE_V1 = `${API_BASE_URL_V1}/generate`; export const API_LIST_URL = '/api/reporting/jobs'; export const API_GENERATE_IMMEDIATE = `${API_BASE_URL_V1}/generate/immediate/csv/saved-object`; +export const API_DIAGNOSE_URL = `${API_BASE_URL}/diagnose`; export const CONTENT_TYPE_CSV = 'text/csv'; export const CSV_REPORTING_ACTION = 'downloadCsvReport'; diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index a5d7f3d20c44c..33141eec46299 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -14,7 +14,8 @@ "licensing", "uiActions", "embeddable", - "share" + "share", + "features" ], "server": true, "ui": true, diff --git a/x-pack/plugins/reporting/public/components/report_diagnostic.tsx b/x-pack/plugins/reporting/public/components/report_diagnostic.tsx new file mode 100644 index 0000000000000..b5b055207ddbb --- /dev/null +++ b/x-pack/plugins/reporting/public/components/report_diagnostic.tsx @@ -0,0 +1,281 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import React, { useState, Fragment } from 'react'; +import { FormattedMessage } from '@kbn/i18n/react'; +import { + EuiButton, + EuiButtonEmpty, + EuiCallOut, + EuiCodeBlock, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutHeader, + EuiSpacer, + EuiSteps, + EuiText, + EuiTitle, +} from '@elastic/eui'; +import { ReportingAPIClient, DiagnoseResponse } from '../lib/reporting_api_client'; + +interface Props { + apiClient: ReportingAPIClient; +} + +type ResultStatus = 'danger' | 'incomplete' | 'complete'; + +enum statuses { + configStatus = 'configStatus', + chromeStatus = 'chromeStatus', + screenshotStatus = 'screenshotStatus', +} + +interface State { + isFlyoutVisible: boolean; + configStatus: ResultStatus; + chromeStatus: ResultStatus; + screenshotStatus: ResultStatus; + help: string[]; + logs: string; + isBusy: boolean; + success: boolean; +} + +const initialState: State = { + [statuses.configStatus]: 'incomplete', + [statuses.chromeStatus]: 'incomplete', + [statuses.screenshotStatus]: 'incomplete', + isFlyoutVisible: false, + help: [], + logs: '', + isBusy: false, + success: true, +}; + +export const ReportDiagnostic = ({ apiClient }: Props) => { + const [state, setStateBase] = useState(initialState); + const setState = (s: Partial) => + setStateBase({ + ...state, + ...s, + }); + const { + configStatus, + isBusy, + screenshotStatus, + chromeStatus, + isFlyoutVisible, + help, + logs, + success, + } = state; + + const closeFlyout = () => setState({ ...initialState, isFlyoutVisible: false }); + const showFlyout = () => setState({ isFlyoutVisible: true }); + const apiWrapper = (apiMethod: () => Promise, statusProp: statuses) => () => { + setState({ isBusy: true, [statusProp]: 'incomplete' }); + apiMethod() + .then((response) => { + setState({ + isBusy: false, + help: response.help, + logs: response.logs, + success: response.success, + [statusProp]: response.success ? 'complete' : 'danger', + }); + }) + .catch((error) => { + setState({ + isBusy: false, + help: [ + i18n.translate('xpack.reporting.listing.diagnosticApiCallFailure', { + defaultMessage: `There was a problem running the diagnostic: {error}`, + values: { error }, + }), + ], + logs: `${error.message}`, + success: false, + [statusProp]: 'danger', + }); + }); + }; + + const steps = [ + { + title: i18n.translate('xpack.reporting.listing.diagnosticConfigTitle', { + defaultMessage: 'Verify Kibana Configuration', + }), + children: ( + + + + + + + + ), + status: !success && configStatus !== 'complete' ? 'danger' : configStatus, + }, + ]; + + if (configStatus === 'complete') { + steps.push({ + title: i18n.translate('xpack.reporting.listing.diagnosticBrowserTitle', { + defaultMessage: 'Check Browser', + }), + children: ( + + + + + + + + ), + status: !success && chromeStatus !== 'complete' ? 'danger' : chromeStatus, + }); + } + + if (chromeStatus === 'complete') { + steps.push({ + title: i18n.translate('xpack.reporting.listing.diagnosticScreenshotTitle', { + defaultMessage: 'Check Screen Capture Capabilities', + }), + children: ( + + + + + + + + ), + status: !success && screenshotStatus !== 'complete' ? 'danger' : screenshotStatus, + }); + } + + if (screenshotStatus === 'complete') { + steps.push({ + title: i18n.translate('xpack.reporting.listing.diagnosticSuccessTitle', { + defaultMessage: 'All set!', + }), + children: ( + + + + ), + status: !success ? 'danger' : screenshotStatus, + }); + } + + if (!success) { + steps.push({ + title: i18n.translate('xpack.reporting.listing.diagnosticFailureTitle', { + defaultMessage: "Whoops! Looks like something isn't working properly.", + }), + children: ( + + {help.length ? ( + + +

{help.join('\n')}

+
+
+ ) : null} + {logs.length ? ( + + + + + {logs} + + ) : null} +
+ ), + status: 'danger', + }); + } + + let flyout; + if (isFlyoutVisible) { + flyout = ( + + + +

+ +

+
+ + + + +
+ + + +
+ ); + } + return ( +
+ {flyout} + + + +
+ ); +}; diff --git a/x-pack/plugins/reporting/public/components/report_listing.tsx b/x-pack/plugins/reporting/public/components/report_listing.tsx index afcae93a8db16..65db13f22788b 100644 --- a/x-pack/plugins/reporting/public/components/report_listing.tsx +++ b/x-pack/plugins/reporting/public/components/report_listing.tsx @@ -6,6 +6,8 @@ import { EuiBasicTable, + EuiFlexItem, + EuiFlexGroup, EuiPageContent, EuiSpacer, EuiText, @@ -31,6 +33,7 @@ import { ReportErrorButton, ReportInfoButton, } from './buttons'; +import { ReportDiagnostic } from './report_diagnostic'; export interface Job { id: string; @@ -134,23 +137,38 @@ class ReportListingUi extends Component { public render() { return ( - - -

- -

-
- -

- -

-
- - {this.renderTable()} -
+
+ + + + +

+ +

+
+ +

+ +

+
+
+
+ + {this.renderTable()} +
+ + + + + + +
); } diff --git a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts index 54bdc99532320..2f813bd811c6c 100644 --- a/x-pack/plugins/reporting/public/lib/reporting_api_client.ts +++ b/x-pack/plugins/reporting/public/lib/reporting_api_client.ts @@ -8,7 +8,12 @@ import { stringify } from 'query-string'; import rison from 'rison-node'; import { HttpSetup } from 'src/core/public'; import { JobId, SourceJob } from '../../common/types'; -import { API_BASE_GENERATE, API_LIST_URL, REPORTING_MANAGEMENT_HOME } from '../../constants'; +import { + API_BASE_URL, + API_BASE_GENERATE, + API_LIST_URL, + REPORTING_MANAGEMENT_HOME, +} from '../../constants'; import { add } from './job_completion_notifications'; export interface JobQueueEntry { @@ -59,6 +64,12 @@ interface JobParams { [paramName: string]: any; } +export interface DiagnoseResponse { + help: string[]; + success: boolean; + logs: string; +} + export class ReportingAPIClient { private http: HttpSetup; @@ -157,4 +168,28 @@ export class ReportingAPIClient { * provides the raw server basePath to allow it to be stripped out from relativeUrls in job params */ public getServerBasePath = () => this.http.basePath.serverBasePath; + + /* + * Diagnostic-related API calls + */ + public verifyConfig = (): Promise => + this.http.post(`${API_BASE_URL}/diagnose/config`, { + asSystemRequest: true, + }); + + /* + * Diagnostic-related API calls + */ + public verifyBrowser = (): Promise => + this.http.post(`${API_BASE_URL}/diagnose/browser`, { + asSystemRequest: true, + }); + + /* + * Diagnostic-related API calls + */ + public verifyScreenCapture = (): Promise => + this.http.post(`${API_BASE_URL}/diagnose/screenshot`, { + asSystemRequest: true, + }); } diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts index 809bfb57dd4fa..88be86d1ecc30 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts @@ -59,28 +59,6 @@ export class HeadlessChromiumDriverFactory { type = BROWSER_TYPE; - test(logger: LevelLogger) { - const chromiumArgs = args({ - userDataDir: this.userDataDir, - viewport: { width: 800, height: 600 }, - disableSandbox: this.browserConfig.disableSandbox, - proxy: this.browserConfig.proxy, - }); - - return puppeteerLaunch({ - userDataDir: this.userDataDir, - executablePath: this.binaryPath, - ignoreHTTPSErrors: true, - args: chromiumArgs, - } as LaunchOptions).catch((error: Error) => { - logger.error( - `The Reporting plugin encountered issues launching Chromium in a self-test. You may have trouble generating reports.` - ); - logger.error(error); - return null; - }); - } - /* * Return an observable to objects which will drive screenshot capture for a page */ diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/start_logs.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/start_logs.ts new file mode 100644 index 0000000000000..8eafbd8e0ddbe --- /dev/null +++ b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/start_logs.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { spawn } from 'child_process'; +import del from 'del'; +import { mkdtempSync } from 'fs'; +import { uniq } from 'lodash'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { createInterface } from 'readline'; +import { fromEvent, timer, merge, of } from 'rxjs'; +import { takeUntil, map, reduce, tap, catchError } from 'rxjs/operators'; +import { ReportingCore } from '../../..'; +import { LevelLogger } from '../../../lib'; +import { getBinaryPath } from '../../install'; +import { args } from './args'; + +const browserLaunchTimeToWait = 5 * 1000; + +// Default args used by pptr +// https://github.com/puppeteer/puppeteer/blob/13ea347/src/node/Launcher.ts#L168 +const defaultArgs = [ + '--disable-background-networking', + '--enable-features=NetworkService,NetworkServiceInProcess', + '--disable-background-timer-throttling', + '--disable-backgrounding-occluded-windows', + '--disable-breakpad', + '--disable-client-side-phishing-detection', + '--disable-component-extensions-with-background-pages', + '--disable-default-apps', + '--disable-dev-shm-usage', + '--disable-extensions', + '--disable-features=TranslateUI', + '--disable-hang-monitor', + '--disable-ipc-flooding-protection', + '--disable-popup-blocking', + '--disable-prompt-on-repost', + '--disable-renderer-backgrounding', + '--disable-sync', + '--force-color-profile=srgb', + '--metrics-recording-only', + '--no-first-run', + '--enable-automation', + '--password-store=basic', + '--use-mock-keychain', + '--remote-debugging-port=0', + '--headless', +]; + +export const browserStartLogs = ( + core: ReportingCore, + logger: LevelLogger, + overrideFlags: string[] = [] +) => { + const config = core.getConfig(); + const proxy = config.get('capture', 'browser', 'chromium', 'proxy'); + const disableSandbox = config.get('capture', 'browser', 'chromium', 'disableSandbox'); + const userDataDir = mkdtempSync(join(tmpdir(), 'chromium-')); + const binaryPath = getBinaryPath(); + const kbnArgs = args({ + userDataDir, + viewport: { width: 800, height: 600 }, + disableSandbox, + proxy, + }); + const finalArgs = uniq([...defaultArgs, ...kbnArgs, ...overrideFlags]); + + // On non-windows platforms, `detached: true` makes child process a + // leader of a new process group, making it possible to kill child + // process tree with `.kill(-pid)` command. @see + // https://nodejs.org/api/child_process.html#child_process_options_detached + const browserProcess = spawn(binaryPath, finalArgs, { + detached: process.platform !== 'win32', + }); + + const rl = createInterface({ input: browserProcess.stderr }); + + const exit$ = fromEvent(browserProcess, 'exit').pipe( + map((code) => { + logger.error(`Browser exited abnormally, received code: ${code}`); + return i18n.translate('xpack.reporting.diagnostic.browserCrashed', { + defaultMessage: `Browser exited abnormally during startup`, + }); + }) + ); + + const error$ = fromEvent(browserProcess, 'error').pipe( + map(() => { + logger.error(`Browser process threw an error on startup`); + return i18n.translate('xpack.reporting.diagnostic.browserErrored', { + defaultMessage: `Browser process threw an error on startup`, + }); + }) + ); + + const browserProcessLogger = logger.clone(['chromium-stderr']); + const log$ = fromEvent(rl, 'line').pipe( + tap((message: unknown) => { + if (typeof message === 'string') { + browserProcessLogger.info(message); + } + }) + ); + + // Collect all events (exit, error and on log-lines), but let chromium keep spitting out + // logs as sometimes it's "bind" successfully for remote connections, but later emit + // a log indicative of an issue (for example, no default font found). + return merge(exit$, error$, log$).pipe( + takeUntil(timer(browserLaunchTimeToWait)), + reduce((acc, curr) => `${acc}${curr}\n`, ''), + tap(() => { + if (browserProcess && browserProcess.pid && !browserProcess.killed) { + browserProcess.kill('SIGKILL'); + logger.info(`Successfully sent 'SIGKILL' to browser process (PID: ${browserProcess.pid})`); + } + browserProcess.removeAllListeners(); + rl.removeAllListeners(); + rl.close(); + del(userDataDir, { force: true }).catch((error) => { + logger.error(`Error deleting user data directory at [${userDataDir}]!`); + logger.error(error); + }); + }), + catchError((error) => { + logger.error(error); + return of(error); + }) + ); +}; diff --git a/x-pack/plugins/reporting/server/browsers/install.ts b/x-pack/plugins/reporting/server/browsers/install.ts index 9eddbe5ef0498..35cc5b6d8b7c2 100644 --- a/x-pack/plugins/reporting/server/browsers/install.ts +++ b/x-pack/plugins/reporting/server/browsers/install.ts @@ -4,24 +4,43 @@ * you may not use this file except in compliance with the Elastic License. */ +import del from 'del'; import os from 'os'; import path from 'path'; -import del from 'del'; - import * as Rx from 'rxjs'; import { LevelLogger } from '../lib'; +import { paths } from './chromium/paths'; import { ensureBrowserDownloaded } from './download'; // @ts-ignore import { md5 } from './download/checksum'; // @ts-ignore import { extract } from './extract'; -import { paths } from './chromium/paths'; interface Package { platforms: string[]; architecture: string; } +/** + * Small helper util to resolve where chromium is installed + */ +export const getBinaryPath = ( + chromiumPath: string = path.resolve(__dirname, '../../chromium'), + platform: string = process.platform, + architecture: string = os.arch() +) => { + const pkg = paths.packages.find((p: Package) => { + return p.platforms.includes(platform) && p.architecture === architecture; + }); + + if (!pkg) { + // TODO: validate this + throw new Error(`Unsupported platform: ${platform}-${architecture}`); + } + + return path.join(chromiumPath, pkg.binaryRelativePath); +}; + /** * "install" a browser by type into installs path by extracting the downloaded * archive. If there is an error extracting the archive an `ExtractError` is thrown @@ -43,7 +62,7 @@ export function installBrowser( throw new Error(`Unsupported platform: ${platform}-${architecture}`); } - const binaryPath = path.join(chromiumPath, pkg.binaryRelativePath); + const binaryPath = getBinaryPath(chromiumPath, platform, architecture); const binaryChecksum = await md5(binaryPath).catch(() => ''); if (binaryChecksum !== pkg.binaryChecksum) { diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index be32b52f19813..3657d323b3edf 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -15,6 +15,7 @@ import { SavedObjectsServiceStart, UiSettingsServiceStart, } from 'src/core/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { SecurityPluginSetup } from '../../security/server'; import { ReportingConfig } from './'; @@ -25,6 +26,7 @@ import { screenshotsObservableFactory, ScreenshotsObservableFn } from './lib/scr import { ReportingStore } from './lib/store'; export interface ReportingInternalSetup { + features: FeaturesPluginSetup; elasticsearch: ElasticsearchServiceSetup; licensing: LicensingPluginSetup; basePath: BasePath['get']; @@ -99,6 +101,26 @@ export class ReportingCore { this.pluginSetup$.next(true); } + /** + * Registers reporting as an Elasticsearch feature for the purpose of toggling visibility based on roles. + */ + public registerFeature() { + const config = this.getConfig(); + const allowedRoles = ['superuser', ...(config.get('roles')?.allow ?? [])]; + this.getPluginSetupDeps().features.registerElasticsearchFeature({ + id: 'reporting', + catalogue: ['reporting'], + management: { + insightsAndAlerting: ['reporting'], + }, + privileges: allowedRoles.map((role) => ({ + requiredClusterPrivileges: [], + requiredRoles: [role], + ui: [], + })), + }); + } + /* * Gives synchronous access to the config */ diff --git a/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts index c3d5b2cc60051..096d0bd428214 100644 --- a/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts +++ b/x-pack/plugins/reporting/server/export_types/png/lib/generate_png.ts @@ -28,7 +28,7 @@ export async function generatePngObservableFactory(reporting: ReportingCore) { if (!layoutParams || !layoutParams.dimensions) { throw new Error(`LayoutParams.Dimensions is undefined.`); } - const layout = new PreserveLayout(layoutParams.dimensions); + const layout = new PreserveLayout(layoutParams.dimensions, layoutParams.selectors); if (apmLayout) apmLayout.end(); const apmScreenshots = apmTrans?.startSpan('screenshots_pipeline', 'setup'); diff --git a/x-pack/plugins/reporting/server/lib/index.ts b/x-pack/plugins/reporting/server/lib/index.ts index f3a09cffbb104..9e5a3ca76126d 100644 --- a/x-pack/plugins/reporting/server/lib/index.ts +++ b/x-pack/plugins/reporting/server/lib/index.ts @@ -13,4 +13,3 @@ export { LevelLogger } from './level_logger'; export { statuses } from './statuses'; export { ReportingStore } from './store'; export { startTrace } from './trace'; -export { runValidations } from './validate'; diff --git a/x-pack/plugins/reporting/server/lib/layouts/index.ts b/x-pack/plugins/reporting/server/lib/layouts/index.ts index d46f088475222..507b7614072ea 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/index.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/index.ts @@ -54,6 +54,7 @@ export interface Size { export interface LayoutParams { id: string; dimensions: Size; + selectors?: LayoutSelectorDictionary; } interface LayoutSelectors { diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts index 9041055ddce2d..e8d182dac0b1d 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts +++ b/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts @@ -25,12 +25,16 @@ export class PreserveLayout extends Layout { private readonly scaledHeight: number; private readonly scaledWidth: number; - constructor(size: Size) { + constructor(size: Size, layoutSelectors?: LayoutSelectorDictionary) { super(LayoutTypes.PRESERVE_LAYOUT); this.height = size.height; this.width = size.width; this.scaledHeight = size.height * ZOOM; this.scaledWidth = size.width * ZOOM; + + if (layoutSelectors) { + this.selectors = layoutSelectors; + } } public getCssOverridesPath() { diff --git a/x-pack/plugins/reporting/server/lib/store/store.test.ts b/x-pack/plugins/reporting/server/lib/store/store.test.ts index e6c4eb7346460..b87466ca289cf 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -7,8 +7,7 @@ import sinon from 'sinon'; import { ElasticsearchServiceSetup } from 'src/core/server'; import { ReportingConfig, ReportingCore } from '../..'; -import { createMockReportingCore } from '../../test_helpers'; -import { createMockLevelLogger } from '../../test_helpers/create_mock_levellogger'; +import { createMockReportingCore, createMockLevelLogger } from '../../test_helpers'; import { Report } from './report'; import { ReportingStore } from './store'; diff --git a/x-pack/plugins/reporting/server/lib/validate/index.ts b/x-pack/plugins/reporting/server/lib/validate/index.ts deleted file mode 100644 index d20df6b7315be..0000000000000 --- a/x-pack/plugins/reporting/server/lib/validate/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import { ElasticsearchServiceSetup } from 'kibana/server'; -import { ReportingConfig } from '../../'; -import { HeadlessChromiumDriverFactory } from '../../browsers/chromium/driver_factory'; -import { LevelLogger } from '../'; -import { validateBrowser } from './validate_browser'; -import { validateMaxContentLength } from './validate_max_content_length'; - -export async function runValidations( - config: ReportingConfig, - elasticsearch: ElasticsearchServiceSetup, - browserFactory: HeadlessChromiumDriverFactory, - parentLogger: LevelLogger -) { - const logger = parentLogger.clone(['validations']); - try { - await Promise.all([ - validateBrowser(browserFactory, logger), - validateMaxContentLength(config, elasticsearch, logger), - ]); - logger.debug( - i18n.translate('xpack.reporting.selfCheck.ok', { - defaultMessage: `Reporting plugin self-check ok!`, - }) - ); - } catch (err) { - logger.error(err); - logger.warning( - i18n.translate('xpack.reporting.selfCheck.warning', { - defaultMessage: `Reporting plugin self-check generated a warning: {err}`, - values: { - err, - }, - }) - ); - } -} diff --git a/x-pack/plugins/reporting/server/lib/validate/validate_browser.ts b/x-pack/plugins/reporting/server/lib/validate/validate_browser.ts deleted file mode 100644 index d29aa522dad90..0000000000000 --- a/x-pack/plugins/reporting/server/lib/validate/validate_browser.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Browser } from 'puppeteer'; -import { BROWSER_TYPE } from '../../../common/constants'; -import { HeadlessChromiumDriverFactory } from '../../browsers/chromium/driver_factory'; -import { LevelLogger } from '../'; - -/* - * Validate the Reporting headless browser can launch, and that it can connect - * to the locally running Kibana instance. - */ -export const validateBrowser = async ( - browserFactory: HeadlessChromiumDriverFactory, - logger: LevelLogger -) => { - if (browserFactory.type === BROWSER_TYPE) { - return browserFactory.test(logger).then((browser: Browser | null) => { - if (browser && browser.close) { - browser.close(); - } else { - throw new Error('Could not close browser client handle!'); - } - }); - } -}; diff --git a/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.test.js b/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.test.js deleted file mode 100644 index f358021560cff..0000000000000 --- a/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.test.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import sinon from 'sinon'; -import { validateMaxContentLength } from './validate_max_content_length'; - -const FIVE_HUNDRED_MEGABYTES = 524288000; -const ONE_HUNDRED_MEGABYTES = 104857600; - -describe('Reporting: Validate Max Content Length', () => { - const elasticsearch = { - legacy: { - client: { - callAsInternalUser: () => ({ - defaults: { - http: { - max_content_length: '100mb', - }, - }, - }), - }, - }, - }; - - const logger = { - warning: sinon.spy(), - }; - - beforeEach(() => { - logger.warning.resetHistory(); - }); - - it('should log warning messages when reporting has a higher max-size than elasticsearch', async () => { - const config = { get: sinon.stub().returns(FIVE_HUNDRED_MEGABYTES) }; - const elasticsearch = { - legacy: { - client: { - callAsInternalUser: () => ({ - defaults: { - http: { - max_content_length: '100mb', - }, - }, - }), - }, - }, - }; - - await validateMaxContentLength(config, elasticsearch, logger); - - sinon.assert.calledWithMatch( - logger.warning, - `xpack.reporting.csv.maxSizeBytes (524288000) is higher` - ); - sinon.assert.calledWithMatch( - logger.warning, - `than ElasticSearch's http.max_content_length (104857600)` - ); - sinon.assert.calledWithMatch( - logger.warning, - 'Please set http.max_content_length in ElasticSearch to match' - ); - sinon.assert.calledWithMatch( - logger.warning, - 'or lower your xpack.reporting.csv.maxSizeBytes in Kibana' - ); - }); - - it('should do nothing when reporting has the same max-size as elasticsearch', async () => { - const config = { get: sinon.stub().returns(ONE_HUNDRED_MEGABYTES) }; - - expect( - async () => await validateMaxContentLength(config, elasticsearch, logger.warning) - ).not.toThrow(); - sinon.assert.notCalled(logger.warning); - }); -}); diff --git a/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.ts b/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.ts deleted file mode 100644 index c38c6e5297854..0000000000000 --- a/x-pack/plugins/reporting/server/lib/validate/validate_max_content_length.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import numeral from '@elastic/numeral'; -import { ElasticsearchServiceSetup } from 'kibana/server'; -import { defaults, get } from 'lodash'; -import { ReportingConfig } from '../../'; -import { LevelLogger } from '../'; - -const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes'; -const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length'; - -export async function validateMaxContentLength( - config: ReportingConfig, - elasticsearch: ElasticsearchServiceSetup, - logger: LevelLogger -) { - const { callAsInternalUser } = elasticsearch.legacy.client; - - const elasticClusterSettingsResponse = await callAsInternalUser('cluster.getSettings', { - includeDefaults: true, - }); - const { persistent, transient, defaults: defaultSettings } = elasticClusterSettingsResponse; - const elasticClusterSettings = defaults({}, persistent, transient, defaultSettings); - - const elasticSearchMaxContent = get(elasticClusterSettings, 'http.max_content_length', '100mb'); - const elasticSearchMaxContentBytes = numeral().unformat(elasticSearchMaxContent.toUpperCase()); - const kibanaMaxContentBytes = config.get('csv', 'maxSizeBytes'); - - if (kibanaMaxContentBytes > elasticSearchMaxContentBytes) { - // TODO this should simply throw an error and let the handler conver it to a warning mesasge. See validateServerHost. - logger.warning( - `xpack.reporting.${KIBANA_MAX_SIZE_BYTES_PATH} (${kibanaMaxContentBytes}) is higher than ElasticSearch's ${ES_MAX_SIZE_BYTES_PATH} (${elasticSearchMaxContentBytes}). ` + - `Please set ${ES_MAX_SIZE_BYTES_PATH} in ElasticSearch to match, or lower your xpack.reporting.${KIBANA_MAX_SIZE_BYTES_PATH} in Kibana to avoid this warning.` - ); - } -} diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts index e0d018869cef1..d323a281c06ff 100644 --- a/x-pack/plugins/reporting/server/plugin.test.ts +++ b/x-pack/plugins/reporting/server/plugin.test.ts @@ -17,6 +17,7 @@ jest.mock('./browsers/install', () => ({ import { coreMock } from 'src/core/server/mocks'; import { ReportingPlugin } from './plugin'; import { createMockConfigSchema } from './test_helpers'; +import { featuresPluginMock } from '../../features/server/mocks'; const sleep = (time: number) => new Promise((r) => setTimeout(r, time)); @@ -35,6 +36,7 @@ describe('Reporting Plugin', () => { coreStart = await coreMock.createStart(); pluginSetup = ({ licensing: {}, + features: featuresPluginMock.createSetup(), usageCollection: { makeUsageCollector: jest.fn(), registerCollector: jest.fn(), diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index 8c0e352aa06c5..adb89abe20280 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -11,7 +11,7 @@ import { PLUGIN_ID, UI_SETTINGS_CUSTOM_PDF_LOGO } from '../common/constants'; import { ReportingCore } from './'; import { initializeBrowserDriverFactory } from './browsers'; import { buildConfig, ReportingConfigType } from './config'; -import { createQueueFactory, LevelLogger, ReportingStore, runValidations } from './lib'; +import { createQueueFactory, LevelLogger, ReportingStore } from './lib'; import { registerRoutes } from './routes'; import { setFieldFormats } from './services'; import { ReportingSetup, ReportingSetupDeps, ReportingStart, ReportingStartDeps } from './types'; @@ -70,13 +70,14 @@ export class ReportingPlugin }); const { elasticsearch, http } = core; - const { licensing, security } = plugins; + const { features, licensing, security } = plugins; const { initializerContext: initContext, reportingCore } = this; const router = http.createRouter(); const basePath = http.basePath.get; reportingCore.pluginSetup({ + features, elasticsearch, licensing, basePath, @@ -91,6 +92,8 @@ export class ReportingPlugin (async () => { const config = await buildConfig(initContext, core, this.logger); reportingCore.setConfig(config); + // Feature registration relies on config, so it cannot be setup before here. + reportingCore.registerFeature(); this.logger.debug('Setup complete'); })().catch((e) => { this.logger.error(`Error in Reporting setup, reporting may not function properly`); @@ -105,7 +108,6 @@ export class ReportingPlugin setFieldFormats(plugins.data.fieldFormats); const { logger, reportingCore } = this; - const { elasticsearch } = reportingCore.getPluginSetupDeps(); // async background start (async () => { @@ -124,9 +126,6 @@ export class ReportingPlugin store, }); - // run self-check validations - runValidations(config, elasticsearch, browserDriverFactory, this.logger); - this.logger.debug('Start complete'); })().catch((e) => { this.logger.error(`Error in Reporting start, reporting may not function properly`); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts new file mode 100644 index 0000000000000..f92fbfc7013cf --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts @@ -0,0 +1,250 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UnwrapPromise } from '@kbn/utility-types'; +import { spawn } from 'child_process'; +import { createInterface } from 'readline'; +import { setupServer } from 'src/core/server/test_utils'; +import supertest from 'supertest'; +import { ReportingCore } from '../..'; +import { createMockLevelLogger, createMockReportingCore } from '../../test_helpers'; +import { registerDiagnoseBrowser } from './browser'; + +jest.mock('child_process'); +jest.mock('readline'); + +type SetupServerReturn = UnwrapPromise>; + +const devtoolMessage = 'DevTools listening on (ws://localhost:4000)'; +const fontNotFoundMessage = 'Could not find the default font'; + +describe('POST /diagnose/browser', () => { + jest.setTimeout(6000); + const reportingSymbol = Symbol('reporting'); + const mockLogger = createMockLevelLogger(); + + let server: SetupServerReturn['server']; + let httpSetup: SetupServerReturn['httpSetup']; + let core: ReportingCore; + const mockedSpawn: any = spawn; + const mockedCreateInterface: any = createInterface; + + const config = { + get: jest.fn().mockImplementation(() => ({})), + kbnConfig: { get: jest.fn() }, + }; + + beforeEach(async () => { + ({ server, httpSetup } = await setupServer(reportingSymbol)); + httpSetup.registerRouteHandlerContext(reportingSymbol, 'reporting', () => ({})); + + const mockSetupDeps = ({ + elasticsearch: { + legacy: { client: { callAsInternalUser: jest.fn() } }, + }, + router: httpSetup.createRouter(''), + } as unknown) as any; + + core = await createMockReportingCore(config, mockSetupDeps); + + mockedSpawn.mockImplementation(() => ({ + removeAllListeners: jest.fn(), + kill: jest.fn(), + pid: 123, + stderr: 'stderr', + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + })); + + mockedCreateInterface.mockImplementation(() => ({ + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + removeAllListeners: jest.fn(), + close: jest.fn(), + })); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('returns a 200 when successful', async () => { + registerDiagnoseBrowser(core, mockLogger); + + await server.start(); + + mockedCreateInterface.mockImplementation(() => ({ + addEventListener: (e: string, cb: any) => setTimeout(() => cb(devtoolMessage), 0), + removeEventListener: jest.fn(), + removeAllListeners: jest.fn(), + close: jest.fn(), + })); + + return supertest(httpSetup.server.listener) + .post('/api/reporting/diagnose/browser') + .expect(200) + .then(({ body }) => { + expect(body.success).toEqual(true); + expect(body.help).toEqual([]); + }); + }); + + it('returns logs when browser crashes + helpful links', async () => { + const logs = `Could not find the default font`; + registerDiagnoseBrowser(core, mockLogger); + + await server.start(); + + mockedCreateInterface.mockImplementation(() => ({ + addEventListener: (e: string, cb: any) => setTimeout(() => cb(logs), 0), + removeEventListener: jest.fn(), + removeAllListeners: jest.fn(), + close: jest.fn(), + })); + + mockedSpawn.mockImplementation(() => ({ + removeAllListeners: jest.fn(), + kill: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + })); + + return supertest(httpSetup.server.listener) + .post('/api/reporting/diagnose/browser') + .expect(200) + .then(({ body }) => { + expect(body).toMatchInlineSnapshot(` + Object { + "help": Array [ + "The browser couldn't locate a default font. Please see https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies to fix this issue.", + ], + "logs": "Could not find the default font + ", + "success": false, + } + `); + }); + }); + + it('logs a message when the browser starts, but then has problems later', async () => { + registerDiagnoseBrowser(core, mockLogger); + + await server.start(); + + mockedCreateInterface.mockImplementation(() => ({ + addEventListener: (e: string, cb: any) => { + setTimeout(() => cb(devtoolMessage), 0); + setTimeout(() => cb(fontNotFoundMessage), 0); + }, + removeEventListener: jest.fn(), + removeAllListeners: jest.fn(), + close: jest.fn(), + })); + + mockedSpawn.mockImplementation(() => ({ + removeAllListeners: jest.fn(), + kill: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + })); + + return supertest(httpSetup.server.listener) + .post('/api/reporting/diagnose/browser') + .expect(200) + .then(({ body }) => { + expect(body).toMatchInlineSnapshot(` + Object { + "help": Array [ + "The browser couldn't locate a default font. Please see https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies to fix this issue.", + ], + "logs": "DevTools listening on (ws://localhost:4000) + Could not find the default font + ", + "success": false, + } + `); + }); + }); + + it('logs a message when the browser starts, but then crashes', async () => { + registerDiagnoseBrowser(core, mockLogger); + + await server.start(); + + mockedCreateInterface.mockImplementation(() => ({ + addEventListener: (e: string, cb: any) => { + setTimeout(() => cb(fontNotFoundMessage), 0); + }, + removeEventListener: jest.fn(), + removeAllListeners: jest.fn(), + close: jest.fn(), + })); + + mockedSpawn.mockImplementation(() => ({ + removeAllListeners: jest.fn(), + kill: jest.fn(), + addEventListener: (e: string, cb: any) => { + if (e === 'exit') { + setTimeout(() => cb(), 5); + } + }, + removeEventListener: jest.fn(), + })); + + return supertest(httpSetup.server.listener) + .post('/api/reporting/diagnose/browser') + .expect(200) + .then(({ body }) => { + expect(body).toMatchInlineSnapshot(` + Object { + "help": Array [ + "The browser couldn't locate a default font. Please see https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies to fix this issue.", + ], + "logs": "Could not find the default font + Browser exited abnormally during startup + ", + "success": false, + } + `); + }); + }); + + it('cleans up process and subscribers', async () => { + registerDiagnoseBrowser(core, mockLogger); + + await server.start(); + const killMock = jest.fn(); + const spawnListenersMock = jest.fn(); + const createInterfaceListenersMock = jest.fn(); + const createInterfaceCloseMock = jest.fn(); + + mockedSpawn.mockImplementation(() => ({ + removeAllListeners: spawnListenersMock, + kill: killMock, + pid: 123, + stderr: 'stderr', + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + })); + + mockedCreateInterface.mockImplementation(() => ({ + addEventListener: (e: string, cb: any) => setTimeout(() => cb(devtoolMessage), 0), + removeEventListener: jest.fn(), + removeAllListeners: createInterfaceListenersMock, + close: createInterfaceCloseMock, + })); + + return supertest(httpSetup.server.listener) + .post('/api/reporting/diagnose/browser') + .expect(200) + .then(() => { + expect(killMock.mock.calls.length).toBe(1); + expect(spawnListenersMock.mock.calls.length).toBe(1); + expect(createInterfaceListenersMock.mock.calls.length).toBe(1); + expect(createInterfaceCloseMock.mock.calls.length).toBe(1); + }); + }); +}); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts new file mode 100644 index 0000000000000..24b85220defb4 --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { ReportingCore } from '../..'; +import { API_DIAGNOSE_URL } from '../../../common/constants'; +import { browserStartLogs } from '../../browsers/chromium/driver_factory/start_logs'; +import { LevelLogger as Logger } from '../../lib'; +import { DiagnosticResponse } from '../../types'; +import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing'; + +const logsToHelpMap = { + 'error while loading shared libraries': i18n.translate( + 'xpack.reporting.diagnostic.browserMissingDependency', + { + defaultMessage: `The browser couldn't start properly due to missing system dependencies. Please see {url}`, + values: { + url: + 'https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies', + }, + } + ), + + 'Could not find the default font': i18n.translate( + 'xpack.reporting.diagnostic.browserMissingFonts', + { + defaultMessage: `The browser couldn't locate a default font. Please see {url} to fix this issue.`, + values: { + url: + 'https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-system-dependencies', + }, + } + ), + + 'No usable sandbox': i18n.translate('xpack.reporting.diagnostic.noUsableSandbox', { + defaultMessage: `Unable to use Chromium sandbox. This can be disabled at your own risk with 'xpack.reporting.capture.browser.chromium.disableSandbox'. Please see {url}`, + values: { + url: + 'https://www.elastic.co/guide/en/kibana/current/reporting-troubleshooting.html#reporting-troubleshooting-sandbox-dependency', + }, + }), +}; + +export const registerDiagnoseBrowser = (reporting: ReportingCore, logger: Logger) => { + const { router } = reporting.getPluginSetupDeps(); + const userHandler = authorizedUserPreRoutingFactory(reporting); + + router.post( + { + path: `${API_DIAGNOSE_URL}/browser`, + validate: {}, + }, + userHandler(async (user, context, req, res) => { + const logs = await browserStartLogs(reporting, logger).toPromise(); + const knownIssues = Object.keys(logsToHelpMap) as Array; + + const boundSuccessfully = logs.includes(`DevTools listening on`); + const help = knownIssues.reduce((helpTexts: string[], knownIssue) => { + const helpText = logsToHelpMap[knownIssue]; + if (logs.includes(knownIssue)) { + helpTexts.push(helpText); + } + return helpTexts; + }, []); + + const response: DiagnosticResponse = { + success: boundSuccessfully && !help.length, + help, + logs, + }; + + return res.ok({ body: response }); + }) + ); +}; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts new file mode 100644 index 0000000000000..624397246656d --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/diagnostic/config.test.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UnwrapPromise } from '@kbn/utility-types'; +import { setupServer } from 'src/core/server/test_utils'; +import supertest from 'supertest'; +import { ReportingCore } from '../..'; +import { createMockReportingCore, createMockLevelLogger } from '../../test_helpers'; +import { registerDiagnoseConfig } from './config'; + +type SetupServerReturn = UnwrapPromise>; + +describe('POST /diagnose/config', () => { + const reportingSymbol = Symbol('reporting'); + let server: SetupServerReturn['server']; + let httpSetup: SetupServerReturn['httpSetup']; + let core: ReportingCore; + let mockSetupDeps: any; + let config: any; + + const mockLogger = createMockLevelLogger(); + + beforeEach(async () => { + ({ server, httpSetup } = await setupServer(reportingSymbol)); + httpSetup.registerRouteHandlerContext(reportingSymbol, 'reporting', () => ({})); + + mockSetupDeps = ({ + elasticsearch: { + legacy: { client: { callAsInternalUser: jest.fn() } }, + }, + router: httpSetup.createRouter(''), + } as unknown) as any; + + config = { + get: jest.fn(), + kbnConfig: { get: jest.fn() }, + }; + + core = await createMockReportingCore(config, mockSetupDeps); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('returns a 200 by default when configured properly', async () => { + mockSetupDeps.elasticsearch.legacy.client.callAsInternalUser.mockImplementation(() => + Promise.resolve({ + defaults: { + http: { + max_content_length: '100mb', + }, + }, + }) + ); + registerDiagnoseConfig(core, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post('/api/reporting/diagnose/config') + .expect(200) + .then(({ body }) => { + expect(body).toMatchInlineSnapshot(` + Object { + "help": Array [], + "logs": "", + "success": true, + } + `); + }); + }); + + it('returns a 200 with help text when not configured properly', async () => { + config.get.mockImplementation(() => 10485760); + mockSetupDeps.elasticsearch.legacy.client.callAsInternalUser.mockImplementation(() => + Promise.resolve({ + defaults: { + http: { + max_content_length: '5mb', + }, + }, + }) + ); + registerDiagnoseConfig(core, mockLogger); + + await server.start(); + + await supertest(httpSetup.server.listener) + .post('/api/reporting/diagnose/config') + .expect(200) + .then(({ body }) => { + expect(body).toMatchInlineSnapshot(` + Object { + "help": Array [ + "xpack.reporting.csv.maxSizeBytes (10485760) is higher than ElasticSearch's http.max_content_length (5242880). Please set http.max_content_length in ElasticSearch to match, or lower your xpack.reporting.csv.maxSizeBytes in Kibana.", + ], + "logs": "xpack.reporting.csv.maxSizeBytes (10485760) is higher than ElasticSearch's http.max_content_length (5242880). Please set http.max_content_length in ElasticSearch to match, or lower your xpack.reporting.csv.maxSizeBytes in Kibana.", + "success": false, + } + `); + }); + }); +}); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/config.ts b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts new file mode 100644 index 0000000000000..198ba63e2614d --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/diagnostic/config.ts @@ -0,0 +1,81 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import numeral from '@elastic/numeral'; +import { defaults, get } from 'lodash'; +import { ReportingCore } from '../..'; +import { API_DIAGNOSE_URL } from '../../../common/constants'; +import { LevelLogger as Logger } from '../../lib'; +import { DiagnosticResponse } from '../../types'; +import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing'; + +const KIBANA_MAX_SIZE_BYTES_PATH = 'csv.maxSizeBytes'; +const ES_MAX_SIZE_BYTES_PATH = 'http.max_content_length'; + +export const registerDiagnoseConfig = (reporting: ReportingCore, logger: Logger) => { + const setupDeps = reporting.getPluginSetupDeps(); + const userHandler = authorizedUserPreRoutingFactory(reporting); + const { router, elasticsearch } = setupDeps; + + router.post( + { + path: `${API_DIAGNOSE_URL}/config`, + validate: {}, + }, + userHandler(async (user, context, req, res) => { + const warnings = []; + const { callAsInternalUser } = elasticsearch.legacy.client; + const config = reporting.getConfig(); + + const elasticClusterSettingsResponse = await callAsInternalUser('cluster.getSettings', { + includeDefaults: true, + }); + const { persistent, transient, defaults: defaultSettings } = elasticClusterSettingsResponse; + const elasticClusterSettings = defaults({}, persistent, transient, defaultSettings); + + const elasticSearchMaxContent = get( + elasticClusterSettings, + 'http.max_content_length', + '100mb' + ); + const elasticSearchMaxContentBytes = numeral().unformat( + elasticSearchMaxContent.toUpperCase() + ); + const kibanaMaxContentBytes = config.get('csv', 'maxSizeBytes'); + + if (kibanaMaxContentBytes > elasticSearchMaxContentBytes) { + const maxContentSizeWarning = i18n.translate( + 'xpack.reporting.diagnostic.configSizeMismatch', + { + defaultMessage: + `xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} ({kibanaMaxContentBytes}) is higher than ElasticSearch's {ES_MAX_SIZE_BYTES_PATH} ({elasticSearchMaxContentBytes}). ` + + `Please set {ES_MAX_SIZE_BYTES_PATH} in ElasticSearch to match, or lower your xpack.reporting.{KIBANA_MAX_SIZE_BYTES_PATH} in Kibana.`, + values: { + kibanaMaxContentBytes, + elasticSearchMaxContentBytes, + KIBANA_MAX_SIZE_BYTES_PATH, + ES_MAX_SIZE_BYTES_PATH, + }, + } + ); + warnings.push(maxContentSizeWarning); + } + + if (warnings.length) { + warnings.forEach((warn) => logger.warn(warn)); + } + + const body: DiagnosticResponse = { + help: warnings, + success: !warnings.length, + logs: warnings.join('\n'), + }; + + return res.ok({ body }); + }) + ); +}; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/index.ts b/x-pack/plugins/reporting/server/routes/diagnostic/index.ts new file mode 100644 index 0000000000000..895dee32614f1 --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/diagnostic/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { registerDiagnoseBrowser } from './browser'; +import { registerDiagnoseConfig } from './config'; +import { registerDiagnoseScreenshot } from './screenshot'; +import { LevelLogger as Logger } from '../../lib'; +import { ReportingCore } from '../../core'; + +export const registerDiagnosticRoutes = (reporting: ReportingCore, logger: Logger) => { + registerDiagnoseBrowser(reporting, logger); + registerDiagnoseConfig(reporting, logger); + registerDiagnoseScreenshot(reporting, logger); +}; diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts new file mode 100644 index 0000000000000..ec4ab0446ae5f --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UnwrapPromise } from '@kbn/utility-types'; +import { setupServer } from 'src/core/server/test_utils'; +import supertest from 'supertest'; +import { ReportingCore } from '../..'; +import { createMockReportingCore, createMockLevelLogger } from '../../test_helpers'; +import { registerDiagnoseScreenshot } from './screenshot'; + +jest.mock('../../export_types/png/lib/generate_png'); + +import { generatePngObservableFactory } from '../../export_types/png/lib/generate_png'; + +type SetupServerReturn = UnwrapPromise>; + +describe('POST /diagnose/screenshot', () => { + const reportingSymbol = Symbol('reporting'); + let server: SetupServerReturn['server']; + let httpSetup: SetupServerReturn['httpSetup']; + let core: ReportingCore; + + const setScreenshotResponse = (resp: object | Error) => { + const generateMock = Promise.resolve(() => ({ + pipe: () => ({ + toPromise: () => (resp instanceof Error ? Promise.reject(resp) : Promise.resolve(resp)), + }), + })); + (generatePngObservableFactory as any).mockResolvedValue(generateMock); + }; + + const config = { + get: jest.fn(), + kbnConfig: { get: jest.fn() }, + }; + const mockLogger = createMockLevelLogger(); + + beforeEach(async () => { + ({ server, httpSetup } = await setupServer(reportingSymbol)); + httpSetup.registerRouteHandlerContext(reportingSymbol, 'reporting', () => ({})); + + const mockSetupDeps = ({ + elasticsearch: { + legacy: { client: { callAsInternalUser: jest.fn() } }, + }, + router: httpSetup.createRouter(''), + } as unknown) as any; + + core = await createMockReportingCore(config, mockSetupDeps); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('returns a 200 by default', async () => { + registerDiagnoseScreenshot(core, mockLogger); + setScreenshotResponse({ warnings: [] }); + await server.start(); + + await supertest(httpSetup.server.listener) + .post('/api/reporting/diagnose/screenshot') + .expect(200) + .then(({ body }) => { + expect(body).toMatchInlineSnapshot(` + Object { + "help": Array [], + "logs": "", + "success": true, + } + `); + }); + }); + + it('returns a 200 when it fails and sets success to false', async () => { + registerDiagnoseScreenshot(core, mockLogger); + setScreenshotResponse({ warnings: [`Timeout waiting for .dank to load`] }); + await server.start(); + + await supertest(httpSetup.server.listener) + .post('/api/reporting/diagnose/screenshot') + .expect(200) + .then(({ body }) => { + expect(body).toMatchInlineSnapshot(` + Object { + "help": Array [], + "logs": Array [ + "Timeout waiting for .dank to load", + ], + "success": false, + } + `); + }); + }); + + it('catches errors and returns a well formed response', async () => { + registerDiagnoseScreenshot(core, mockLogger); + setScreenshotResponse(new Error('Failure to start chromium!')); + await server.start(); + + await supertest(httpSetup.server.listener) + .post('/api/reporting/diagnose/screenshot') + .expect(200) + .then(({ body }) => { + expect(body.help).toContain(`We couldn't screenshot your Kibana install.`); + expect(body.logs).toContain(`Failure to start chromium!`); + }); + }); +}); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts new file mode 100644 index 0000000000000..7e07779b5fd37 --- /dev/null +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; +import { ReportingCore } from '../..'; +import { API_DIAGNOSE_URL } from '../../../common/constants'; +import { omitBlacklistedHeaders } from '../../export_types/common'; +import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url'; +import { generatePngObservableFactory } from '../../export_types/png/lib/generate_png'; +import { LevelLogger as Logger } from '../../lib'; +import { DiagnosticResponse } from '../../types'; +import { authorizedUserPreRoutingFactory } from '../lib/authorized_user_pre_routing'; + +export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Logger) => { + const setupDeps = reporting.getPluginSetupDeps(); + const userHandler = authorizedUserPreRoutingFactory(reporting); + const { router } = setupDeps; + + router.post( + { + path: `${API_DIAGNOSE_URL}/screenshot`, + validate: {}, + }, + userHandler(async (user, context, req, res) => { + const generatePngObservable = await generatePngObservableFactory(reporting); + const config = reporting.getConfig(); + const decryptedHeaders = req.headers as Record; + const [basePath, protocol, hostname, port] = [ + config.kbnConfig.get('server', 'basePath'), + config.get('kibanaServer', 'protocol'), + config.get('kibanaServer', 'hostname'), + config.get('kibanaServer', 'port'), + ] as string[]; + + const getAbsoluteUrl = getAbsoluteUrlFactory({ + defaultBasePath: basePath, + protocol, + hostname, + port, + }); + + const hashUrl = getAbsoluteUrl({ + basePath, + path: '/', + hash: '', + search: '', + }); + + // Hack the layout to make the base/login page work + const layout = { + id: 'png', + dimensions: { + width: 1440, + height: 2024, + }, + selectors: { + screenshot: '.application', + renderComplete: '.application', + itemsCountAttribute: 'data-test-subj="kibanaChrome"', + timefilterDurationAttribute: 'data-test-subj="kibanaChrome"', + }, + }; + + const headers = { + headers: omitBlacklistedHeaders({ + job: null, + decryptedHeaders, + }), + conditions: { + hostname, + port: +port, + basePath, + protocol, + }, + }; + + return generatePngObservable(logger, hashUrl, 'America/Los_Angeles', headers, layout) + .pipe() + .toPromise() + .then((screenshot) => { + if (screenshot.warnings.length) { + return res.ok({ + body: { + success: false, + help: [], + logs: screenshot.warnings, + }, + }); + } + return res.ok({ + body: { + success: true, + help: [], + logs: '', + } as DiagnosticResponse, + }); + }) + .catch((error) => + res.ok({ + body: { + success: false, + help: [ + i18n.translate('xpack.reporting.diagnostic.screenshotFailureMessage', { + defaultMessage: `We couldn't screenshot your Kibana install.`, + }), + ], + logs: error.message, + } as DiagnosticResponse, + }) + ); + }) + ); +}; diff --git a/x-pack/plugins/reporting/server/routes/generation.test.ts b/x-pack/plugins/reporting/server/routes/generation.test.ts index 0db0073149e57..dd905223a81d5 100644 --- a/x-pack/plugins/reporting/server/routes/generation.test.ts +++ b/x-pack/plugins/reporting/server/routes/generation.test.ts @@ -11,8 +11,7 @@ import { setupServer } from 'src/core/server/test_utils'; import supertest from 'supertest'; import { ReportingCore } from '..'; import { ExportTypesRegistry } from '../lib/export_types_registry'; -import { createMockReportingCore } from '../test_helpers'; -import { createMockLevelLogger } from '../test_helpers/create_mock_levellogger'; +import { createMockReportingCore, createMockLevelLogger } from '../test_helpers'; import { registerJobGenerationRoutes } from './generation'; type SetupServerReturn = UnwrapPromise>; diff --git a/x-pack/plugins/reporting/server/routes/index.ts b/x-pack/plugins/reporting/server/routes/index.ts index 005d82086665c..11ad4cc9d4eb8 100644 --- a/x-pack/plugins/reporting/server/routes/index.ts +++ b/x-pack/plugins/reporting/server/routes/index.ts @@ -8,8 +8,10 @@ import { LevelLogger as Logger } from '../lib'; import { registerJobGenerationRoutes } from './generation'; import { registerJobInfoRoutes } from './jobs'; import { ReportingCore } from '../core'; +import { registerDiagnosticRoutes } from './diagnostic'; export function registerRoutes(reporting: ReportingCore, logger: Logger) { registerJobGenerationRoutes(reporting, logger); registerJobInfoRoutes(reporting); + registerDiagnosticRoutes(reporting, logger); } diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts index c508ee6974ca0..559726e0b8a99 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts +++ b/x-pack/plugins/reporting/server/test_helpers/create_mock_reportingplugin.ts @@ -8,9 +8,9 @@ jest.mock('../routes'); jest.mock('../usage'); jest.mock('../browsers'); jest.mock('../lib/create_queue'); -jest.mock('../lib/validate'); import * as Rx from 'rxjs'; +import { featuresPluginMock } from '../../../features/server/mocks'; import { ReportingConfig, ReportingCore } from '../'; import { chromium, @@ -33,6 +33,7 @@ const createMockPluginSetup = ( setupMock?: any ): ReportingInternalSetup => { return { + features: featuresPluginMock.createSetup(), elasticsearch: setupMock.elasticsearch || { legacy: { client: {} } }, basePath: setupMock.basePath || '/all-about-that-basepath', router: setupMock.router, diff --git a/x-pack/plugins/reporting/server/test_helpers/index.ts b/x-pack/plugins/reporting/server/test_helpers/index.ts index b37b447dc05a9..2d5ef9fdd768d 100644 --- a/x-pack/plugins/reporting/server/test_helpers/index.ts +++ b/x-pack/plugins/reporting/server/test_helpers/index.ts @@ -8,3 +8,4 @@ export { createMockServer } from './create_mock_server'; export { createMockReportingCore, createMockConfigSchema } from './create_mock_reportingplugin'; export { createMockBrowserDriverFactory } from './create_mock_browserdriverfactory'; export { createMockLayoutInstance } from './create_mock_layoutinstance'; +export { createMockLevelLogger } from './create_mock_levellogger'; diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index 10519842d9dec..c67a95c2de754 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -9,6 +9,7 @@ import { KibanaRequest, RequestHandlerContext } from 'src/core/server'; import { DataPluginStart } from 'src/plugins/data/server/plugin'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { CancellationToken } from '../../../plugins/reporting/common'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { AuthenticatedUser, SecurityPluginSetup } from '../../security/server'; import { JobStatus } from '../common/types'; @@ -92,6 +93,7 @@ export interface ConditionalHeaders { export interface ReportingSetupDeps { licensing: LicensingPluginSetup; + features: FeaturesPluginSetup; security?: SecurityPluginSetup; usageCollection?: UsageCollectionSetup; } @@ -160,3 +162,9 @@ export interface ExportTypeDefinition< runTaskFnFactory: RunTaskFnFactory; validLicenses: string[]; } + +export interface DiagnosticResponse { + help: string[]; + success: boolean; + logs: string; +} diff --git a/x-pack/plugins/rollup/kibana.json b/x-pack/plugins/rollup/kibana.json index e6915f65599cc..725b563c3674f 100644 --- a/x-pack/plugins/rollup/kibana.json +++ b/x-pack/plugins/rollup/kibana.json @@ -7,7 +7,8 @@ "requiredPlugins": [ "indexPatternManagement", "management", - "licensing" + "licensing", + "features" ], "optionalPlugins": [ "home", diff --git a/x-pack/plugins/rollup/server/plugin.ts b/x-pack/plugins/rollup/server/plugin.ts index 713852b4d7398..8b3a6355f950d 100644 --- a/x-pack/plugins/rollup/server/plugin.ts +++ b/x-pack/plugins/rollup/server/plugin.ts @@ -64,7 +64,7 @@ export class RollupPlugin implements Plugin { public setup( { http, uiSettings, getStartServices }: CoreSetup, - { licensing, indexManagement, visTypeTimeseries, usageCollection }: Dependencies + { features, licensing, indexManagement, visTypeTimeseries, usageCollection }: Dependencies ) { this.license.setup( { @@ -80,6 +80,20 @@ export class RollupPlugin implements Plugin { } ); + features.registerElasticsearchFeature({ + id: 'rollup_jobs', + management: { + data: ['rollup_jobs'], + }, + catalogue: ['rollup_jobs'], + privileges: [ + { + requiredClusterPrivileges: ['manage_rollup'], + ui: [], + }, + ], + }); + http.registerRouteHandlerContext('rollup', async (context, request) => { this.rollupEsClient = this.rollupEsClient ?? (await getCustomEsClient(getStartServices)); return { diff --git a/x-pack/plugins/rollup/server/types.ts b/x-pack/plugins/rollup/server/types.ts index 2a7644de764b2..290d2df050099 100644 --- a/x-pack/plugins/rollup/server/types.ts +++ b/x-pack/plugins/rollup/server/types.ts @@ -9,6 +9,7 @@ import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { VisTypeTimeseriesSetup } from 'src/plugins/vis_type_timeseries/server'; import { IndexManagementPluginSetup } from '../../index_management/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { License } from './services'; import { IndexPatternsFetcher } from './shared_imports'; @@ -22,6 +23,7 @@ export interface Dependencies { visTypeTimeseries?: VisTypeTimeseriesSetup; usageCollection?: UsageCollectionSetup; licensing: LicensingPluginSetup; + features: FeaturesPluginSetup; } export interface RouteDependencies { diff --git a/x-pack/plugins/security/common/licensing/index.mock.ts b/x-pack/plugins/security/common/licensing/index.mock.ts index 06a7057abb87c..87225f479ceed 100644 --- a/x-pack/plugins/security/common/licensing/index.mock.ts +++ b/x-pack/plugins/security/common/licensing/index.mock.ts @@ -9,6 +9,7 @@ import { SecurityLicense } from '.'; export const licenseMock = { create: (): jest.Mocked => ({ + isLicenseAvailable: jest.fn(), isEnabled: jest.fn().mockReturnValue(true), getFeatures: jest.fn(), features$: of(), diff --git a/x-pack/plugins/security/common/licensing/license_service.test.ts b/x-pack/plugins/security/common/licensing/license_service.test.ts index 564b71a2e0fac..94aad8d3ac539 100644 --- a/x-pack/plugins/security/common/licensing/license_service.test.ts +++ b/x-pack/plugins/security/common/licensing/license_service.test.ts @@ -13,6 +13,7 @@ describe('license features', function () { const serviceSetup = new SecurityLicenseService().setup({ license$: of(undefined as any), }); + expect(serviceSetup.license.isLicenseAvailable()).toEqual(false); expect(serviceSetup.license.getFeatures()).toEqual({ showLogin: true, allowLogin: false, @@ -34,6 +35,7 @@ describe('license features', function () { const serviceSetup = new SecurityLicenseService().setup({ license$: of(rawLicenseMock), }); + expect(serviceSetup.license.isLicenseAvailable()).toEqual(false); expect(serviceSetup.license.getFeatures()).toEqual({ showLogin: true, allowLogin: false, @@ -60,6 +62,7 @@ describe('license features', function () { const subscriptionHandler = jest.fn(); const subscription = serviceSetup.license.features$.subscribe(subscriptionHandler); try { + expect(serviceSetup.license.isLicenseAvailable()).toEqual(false); expect(subscriptionHandler).toHaveBeenCalledTimes(1); expect(subscriptionHandler.mock.calls[0]).toMatchInlineSnapshot(` Array [ @@ -80,6 +83,7 @@ describe('license features', function () { `); rawLicense$.next(licenseMock.createLicenseMock()); + expect(serviceSetup.license.isLicenseAvailable()).toEqual(true); expect(subscriptionHandler).toHaveBeenCalledTimes(2); expect(subscriptionHandler.mock.calls[1]).toMatchInlineSnapshot(` Array [ @@ -112,6 +116,7 @@ describe('license features', function () { const serviceSetup = new SecurityLicenseService().setup({ license$: of(mockRawLicense), }); + expect(serviceSetup.license.isLicenseAvailable()).toEqual(true); expect(serviceSetup.license.getFeatures()).toEqual({ showLogin: true, allowLogin: true, @@ -136,6 +141,7 @@ describe('license features', function () { const serviceSetup = new SecurityLicenseService().setup({ license$: of(mockRawLicense), }); + expect(serviceSetup.license.isLicenseAvailable()).toEqual(true); expect(serviceSetup.license.getFeatures()).toEqual({ showLogin: false, allowLogin: false, @@ -159,6 +165,7 @@ describe('license features', function () { const serviceSetup = new SecurityLicenseService().setup({ license$: of(mockRawLicense), }); + expect(serviceSetup.license.isLicenseAvailable()).toEqual(true); expect(serviceSetup.license.getFeatures()).toEqual({ showLogin: true, allowLogin: true, @@ -182,6 +189,7 @@ describe('license features', function () { const serviceSetup = new SecurityLicenseService().setup({ license$: of(mockRawLicense), }); + expect(serviceSetup.license.isLicenseAvailable()).toEqual(true); expect(serviceSetup.license.getFeatures()).toEqual({ showLogin: true, allowLogin: true, @@ -205,6 +213,7 @@ describe('license features', function () { const serviceSetup = new SecurityLicenseService().setup({ license$: of(mockRawLicense), }); + expect(serviceSetup.license.isLicenseAvailable()).toEqual(true); expect(serviceSetup.license.getFeatures()).toEqual({ showLogin: true, allowLogin: true, diff --git a/x-pack/plugins/security/common/licensing/license_service.ts b/x-pack/plugins/security/common/licensing/license_service.ts index 75c7670f28a67..09b6ae95c282c 100644 --- a/x-pack/plugins/security/common/licensing/license_service.ts +++ b/x-pack/plugins/security/common/licensing/license_service.ts @@ -10,6 +10,7 @@ import { ILicense } from '../../../licensing/common/types'; import { SecurityLicenseFeatures } from './license_features'; export interface SecurityLicense { + isLicenseAvailable(): boolean; isEnabled(): boolean; getFeatures(): SecurityLicenseFeatures; features$: Observable; @@ -31,6 +32,8 @@ export class SecurityLicenseService { return { license: Object.freeze({ + isLicenseAvailable: () => rawLicense?.isAvailable ?? false, + isEnabled: () => this.isSecurityEnabledFromRawLicense(rawLicense), getFeatures: () => this.calculateFeaturesFromRawLicense(rawLicense), diff --git a/x-pack/plugins/security/kibana.json b/x-pack/plugins/security/kibana.json index 6a09e9e55a01b..40d7e293eaf66 100644 --- a/x-pack/plugins/security/kibana.json +++ b/x-pack/plugins/security/kibana.json @@ -4,7 +4,7 @@ "kibanaVersion": "kibana", "configPath": ["xpack", "security"], "requiredPlugins": ["data", "features", "licensing", "taskManager"], - "optionalPlugins": ["home", "management"], + "optionalPlugins": ["home", "management", "usageCollection"], "server": true, "ui": true, "requiredBundles": [ diff --git a/x-pack/plugins/security/public/management/management_service.test.ts b/x-pack/plugins/security/public/management/management_service.test.ts index ce93fb7c98f41..cd06693a43bf9 100644 --- a/x-pack/plugins/security/public/management/management_service.test.ts +++ b/x-pack/plugins/security/public/management/management_service.test.ts @@ -78,7 +78,10 @@ describe('ManagementService', () => { }); describe('start()', () => { - function startService(initialFeatures: Partial) { + function startService( + initialFeatures: Partial, + canManageSecurity: boolean = true + ) { const { fatalErrors, getStartServices } = coreMock.createSetup(); const licenseSubject = new BehaviorSubject( @@ -106,10 +109,11 @@ describe('ManagementService', () => { management: managementSetup, }); - const getMockedApp = () => { + const getMockedApp = (id: string) => { // All apps are enabled by default. let enabled = true; return ({ + id, get enabled() { return enabled; }, @@ -123,13 +127,26 @@ describe('ManagementService', () => { }; mockSection.getApp = jest.fn().mockImplementation((id) => mockApps.get(id)); const mockApps = new Map>([ - [usersManagementApp.id, getMockedApp()], - [rolesManagementApp.id, getMockedApp()], - [apiKeysManagementApp.id, getMockedApp()], - [roleMappingsManagementApp.id, getMockedApp()], + [usersManagementApp.id, getMockedApp(usersManagementApp.id)], + [rolesManagementApp.id, getMockedApp(rolesManagementApp.id)], + [apiKeysManagementApp.id, getMockedApp(apiKeysManagementApp.id)], + [roleMappingsManagementApp.id, getMockedApp(roleMappingsManagementApp.id)], ] as Array<[string, jest.Mocked]>); - service.start(); + service.start({ + capabilities: { + management: { + security: { + users: canManageSecurity, + roles: canManageSecurity, + role_mappings: canManageSecurity, + api_keys: canManageSecurity, + }, + }, + navLinks: {}, + catalogue: {}, + }, + }); return { mockApps, @@ -178,6 +195,19 @@ describe('ManagementService', () => { } }); + it('apps are disabled if capabilities are false', () => { + const { mockApps } = startService( + { + showLinks: true, + showRoleMappingsManagement: true, + }, + false + ); + for (const [, mockApp] of mockApps) { + expect(mockApp.enabled).toBe(false); + } + }); + it('role mappings app is disabled if `showRoleMappingsManagement` changes after `start`', () => { const { mockApps, updateFeatures } = startService({ showLinks: true, diff --git a/x-pack/plugins/security/public/management/management_service.ts b/x-pack/plugins/security/public/management/management_service.ts index 199fd917da071..1fc648c12f80d 100644 --- a/x-pack/plugins/security/public/management/management_service.ts +++ b/x-pack/plugins/security/public/management/management_service.ts @@ -5,7 +5,7 @@ */ import { Subscription } from 'rxjs'; -import { StartServicesAccessor, FatalErrorsSetup } from 'src/core/public'; +import { StartServicesAccessor, FatalErrorsSetup, Capabilities } from 'src/core/public'; import { ManagementApp, ManagementSetup, @@ -27,6 +27,10 @@ interface SetupParams { getStartServices: StartServicesAccessor; } +interface StartParams { + capabilities: Capabilities; +} + export class ManagementService { private license!: SecurityLicense; private licenseFeaturesSubscription?: Subscription; @@ -44,7 +48,7 @@ export class ManagementService { this.securitySection.registerApp(roleMappingsManagementApp.create({ getStartServices })); } - start() { + start({ capabilities }: StartParams) { this.licenseFeaturesSubscription = this.license.features$.subscribe(async (features) => { const securitySection = this.securitySection!; @@ -61,6 +65,11 @@ export class ManagementService { // Iterate over all registered apps and update their enable status depending on the available // license features. for (const [app, enableStatus] of securityManagementAppsStatuses) { + if (capabilities.management.security[app.id] !== true) { + app.disable(); + continue; + } + if (app.enabled === enableStatus) { continue; } diff --git a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts index 08561234fd706..2b78355787ff2 100644 --- a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts +++ b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_features.ts @@ -4,17 +4,20 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureConfig } from '../../../../../features/public'; +import { KibanaFeature, KibanaFeatureConfig } from '../../../../../features/public'; export const createFeature = ( - config: Pick & { + config: Pick< + KibanaFeatureConfig, + 'id' | 'name' | 'subFeatures' | 'reserved' | 'privilegesTooltip' + > & { excludeFromBaseAll?: boolean; excludeFromBaseRead?: boolean; - privileges?: FeatureConfig['privileges']; + privileges?: KibanaFeatureConfig['privileges']; } ) => { const { excludeFromBaseAll, excludeFromBaseRead, privileges, ...rest } = config; - return new Feature({ + return new KibanaFeature({ icon: 'discoverApp', navLinkId: 'discover', app: [], diff --git a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts index 6821c163d817d..02a18039cee74 100644 --- a/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts +++ b/x-pack/plugins/security/public/management/roles/__fixtures__/kibana_privileges.ts @@ -7,7 +7,7 @@ import { Actions } from '../../../../server/authorization'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { privilegesFactory } from '../../../../server/authorization/privileges'; -import { Feature } from '../../../../../features/public'; +import { KibanaFeature } from '../../../../../features/public'; import { KibanaPrivileges } from '../model'; import { SecurityLicenseFeatures } from '../../..'; @@ -15,11 +15,11 @@ import { SecurityLicenseFeatures } from '../../..'; import { featuresPluginMock } from '../../../../../features/server/mocks'; export const createRawKibanaPrivileges = ( - features: Feature[], + features: KibanaFeature[], { allowSubFeaturePrivileges = true } = {} ) => { const featuresService = featuresPluginMock.createSetup(); - featuresService.getFeatures.mockReturnValue(features); + featuresService.getKibanaFeatures.mockReturnValue(features); const licensingService = { getFeatures: () => ({ allowSubFeaturePrivileges } as SecurityLicenseFeatures), @@ -33,7 +33,7 @@ export const createRawKibanaPrivileges = ( }; export const createKibanaPrivileges = ( - features: Feature[], + features: KibanaFeature[], { allowSubFeaturePrivileges = true } = {} ) => { return new KibanaPrivileges( diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx index f6fe2f394fd36..bf791b37087bd 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.test.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { act } from '@testing-library/react'; import { mountWithIntl, nextTick } from 'test_utils/enzyme_helpers'; import { Capabilities } from 'src/core/public'; -import { Feature } from '../../../../../features/public'; +import { KibanaFeature } from '../../../../../features/public'; import { Role } from '../../../../common/model'; import { DocumentationLinksService } from '../documentation_links'; import { EditRolePage } from './edit_role_page'; @@ -27,7 +27,7 @@ import { createRawKibanaPrivileges } from '../__fixtures__/kibana_privileges'; const buildFeatures = () => { return [ - new Feature({ + new KibanaFeature({ id: 'feature1', name: 'Feature 1', icon: 'addDataApp', @@ -51,7 +51,7 @@ const buildFeatures = () => { }, }, }), - new Feature({ + new KibanaFeature({ id: 'feature2', name: 'Feature 2', icon: 'addDataApp', @@ -75,7 +75,7 @@ const buildFeatures = () => { }, }, }), - ] as Feature[]; + ] as KibanaFeature[]; }; const buildBuiltinESPrivileges = () => { diff --git a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx index 15888733ec424..01f8969e61f43 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/edit_role_page.tsx @@ -40,7 +40,7 @@ import { } from 'src/core/public'; import { ScopedHistory } from 'kibana/public'; import { FeaturesPluginStart } from '../../../../../features/public'; -import { Feature } from '../../../../../features/common'; +import { KibanaFeature } from '../../../../../features/common'; import { IndexPatternsContract } from '../../../../../../../src/plugins/data/public'; import { Space } from '../../../../../spaces/public'; import { @@ -247,7 +247,7 @@ function useFeatures( getFeatures: FeaturesPluginStart['getFeatures'], fatalErrors: FatalErrorsSetup ) { - const [features, setFeatures] = useState(null); + const [features, setFeatures] = useState(null); useEffect(() => { getFeatures() .catch((err: IHttpFetchError) => { @@ -260,7 +260,7 @@ function useFeatures( // 404 here, and respond in a way that still allows the UI to render itself. const unauthorizedForFeatures = err.response?.status === 404; if (unauthorizedForFeatures) { - return [] as Feature[]; + return [] as KibanaFeature[]; } fatalErrors.add(err); diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap index 1c020685c246d..a2e46af19bf34 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/es/__snapshots__/elasticsearch_privileges.test.tsx.snap @@ -184,6 +184,7 @@ exports[`it renders without crashing 1`] = ` }, "getFeatures": [MockFunction], "isEnabled": [MockFunction], + "isLicenseAvailable": [MockFunction], } } onChange={[MockFunction]} diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx index 2a0922d614f1d..02d692bf9f507 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/feature_table/feature_table.test.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { FeatureTable } from './feature_table'; import { Role } from '../../../../../../../common/model'; import { mountWithIntl } from 'test_utils/enzyme_helpers'; -import { Feature, SubFeatureConfig } from '../../../../../../../../features/public'; +import { KibanaFeature, SubFeatureConfig } from '../../../../../../../../features/public'; import { kibanaFeatures, createFeature } from '../../../../__fixtures__/kibana_features'; import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges'; import { PrivilegeFormCalculator } from '../privilege_form_calculator'; @@ -24,7 +24,7 @@ const createRole = (kibana: Role['kibana'] = []): Role => { }; interface TestConfig { - features: Feature[]; + features: KibanaFeature[]; role: Role; privilegeIndex: number; calculateDisplayedPrivileges: boolean; diff --git a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx index 5530d9964f8cd..bc60613345910 100644 --- a/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx +++ b/x-pack/plugins/security/public/management/roles/edit_role/privileges/kibana/space_aware_privilege_section/privilege_space_table.test.tsx @@ -13,7 +13,7 @@ import { PrivilegeDisplay } from './privilege_display'; import { Role, RoleKibanaPrivilege } from '../../../../../../../common/model'; import { createKibanaPrivileges } from '../../../../__fixtures__/kibana_privileges'; import { PrivilegeFormCalculator } from '../privilege_form_calculator'; -import { Feature } from '../../../../../../../../features/public'; +import { KibanaFeature } from '../../../../../../../../features/public'; import { findTestSubject } from 'test_utils/find_test_subject'; interface TableRow { @@ -24,7 +24,7 @@ interface TableRow { } const features = [ - new Feature({ + new KibanaFeature({ id: 'normal', name: 'normal feature', app: [], @@ -39,7 +39,7 @@ const features = [ }, }, }), - new Feature({ + new KibanaFeature({ id: 'normal_with_sub', name: 'normal feature with sub features', app: [], @@ -92,7 +92,7 @@ const features = [ }, ], }), - new Feature({ + new KibanaFeature({ id: 'bothPrivilegesExcludedFromBase', name: 'bothPrivilegesExcludedFromBase', app: [], @@ -109,7 +109,7 @@ const features = [ }, }, }), - new Feature({ + new KibanaFeature({ id: 'allPrivilegeExcludedFromBase', name: 'allPrivilegeExcludedFromBase', app: [], diff --git a/x-pack/plugins/security/public/management/roles/model/kibana_privileges.ts b/x-pack/plugins/security/public/management/roles/model/kibana_privileges.ts index fd93aaa23194a..4739346b2cb76 100644 --- a/x-pack/plugins/security/public/management/roles/model/kibana_privileges.ts +++ b/x-pack/plugins/security/public/management/roles/model/kibana_privileges.ts @@ -8,7 +8,7 @@ import { RawKibanaPrivileges, RoleKibanaPrivilege } from '../../../../common/mod import { KibanaPrivilege } from './kibana_privilege'; import { PrivilegeCollection } from './privilege_collection'; import { SecuredFeature } from './secured_feature'; -import { Feature } from '../../../../../features/common'; +import { KibanaFeature } from '../../../../../features/common'; import { isGlobalPrivilegeDefinition } from '../edit_role/privilege_utils'; function toBasePrivilege(entry: [string, string[]]): [string, KibanaPrivilege] { @@ -29,7 +29,7 @@ export class KibanaPrivileges { private feature: ReadonlyMap; - constructor(rawKibanaPrivileges: RawKibanaPrivileges, features: Feature[]) { + constructor(rawKibanaPrivileges: RawKibanaPrivileges, features: KibanaFeature[]) { this.global = recordsToBasePrivilegeMap(rawKibanaPrivileges.global); this.spaces = recordsToBasePrivilegeMap(rawKibanaPrivileges.space); this.feature = new Map( diff --git a/x-pack/plugins/security/public/management/roles/model/secured_feature.ts b/x-pack/plugins/security/public/management/roles/model/secured_feature.ts index 284a85583c33c..894e06b6e5856 100644 --- a/x-pack/plugins/security/public/management/roles/model/secured_feature.ts +++ b/x-pack/plugins/security/public/management/roles/model/secured_feature.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureConfig } from '../../../../../features/common'; +import { KibanaFeature, KibanaFeatureConfig } from '../../../../../features/common'; import { PrimaryFeaturePrivilege } from './primary_feature_privilege'; import { SecuredSubFeature } from './secured_sub_feature'; import { SubFeaturePrivilege } from './sub_feature_privilege'; -export class SecuredFeature extends Feature { +export class SecuredFeature extends KibanaFeature { private readonly primaryFeaturePrivileges: PrimaryFeaturePrivilege[]; private readonly minimalPrimaryFeaturePrivileges: PrimaryFeaturePrivilege[]; @@ -18,7 +18,10 @@ export class SecuredFeature extends Feature { private readonly securedSubFeatures: SecuredSubFeature[]; - constructor(config: FeatureConfig, actionMapping: { [privilegeId: string]: string[] } = {}) { + constructor( + config: KibanaFeatureConfig, + actionMapping: { [privilegeId: string]: string[] } = {} + ) { super(config); this.primaryFeaturePrivileges = Object.entries(this.config.privileges || {}).map( ([id, privilege]) => new PrimaryFeaturePrivilege(id, privilege, actionMapping[id]) diff --git a/x-pack/plugins/security/public/plugin.test.tsx b/x-pack/plugins/security/public/plugin.test.tsx index 8cec4fbc2f5a2..fb8034da11731 100644 --- a/x-pack/plugins/security/public/plugin.test.tsx +++ b/x-pack/plugins/security/public/plugin.test.tsx @@ -41,6 +41,7 @@ describe('Security Plugin', () => { __legacyCompat: { logoutUrl: '/some-base-path/logout', tenant: '/some-base-path' }, authc: { getCurrentUser: expect.any(Function), areAPIKeysEnabled: expect.any(Function) }, license: { + isLicenseAvailable: expect.any(Function), isEnabled: expect.any(Function), getFeatures: expect.any(Function), features$: expect.any(Observable), @@ -67,6 +68,7 @@ describe('Security Plugin', () => { expect(setupManagementServiceMock).toHaveBeenCalledWith({ authc: { getCurrentUser: expect.any(Function), areAPIKeysEnabled: expect.any(Function) }, license: { + isLicenseAvailable: expect.any(Function), isEnabled: expect.any(Function), getFeatures: expect.any(Function), features$: expect.any(Observable), @@ -112,7 +114,8 @@ describe('Security Plugin', () => { } ); - plugin.start(coreMock.createStart({ basePath: '/some-base-path' }), { + const coreStart = coreMock.createStart({ basePath: '/some-base-path' }); + plugin.start(coreStart, { data: {} as DataPublicPluginStart, features: {} as FeaturesPluginStart, management: managementStartMock, diff --git a/x-pack/plugins/security/public/plugin.tsx b/x-pack/plugins/security/public/plugin.tsx index e3905dc2acf45..f5770ae2bc35c 100644 --- a/x-pack/plugins/security/public/plugin.tsx +++ b/x-pack/plugins/security/public/plugin.tsx @@ -141,7 +141,7 @@ export class SecurityPlugin this.sessionTimeout.start(); this.navControlService.start({ core }); if (management) { - this.managementService.start(); + this.managementService.start({ capabilities: core.application.capabilities }); } } diff --git a/x-pack/plugins/security/server/authorization/api_authorization.test.ts b/x-pack/plugins/security/server/authorization/api_authorization.test.ts index 75aa27c3c88c6..d4ec9a0e0db51 100644 --- a/x-pack/plugins/security/server/authorization/api_authorization.test.ts +++ b/x-pack/plugins/security/server/authorization/api_authorization.test.ts @@ -94,7 +94,9 @@ describe('initAPIAuthorization', () => { expect(mockResponse.notFound).not.toHaveBeenCalled(); expect(mockPostAuthToolkit.next).toHaveBeenCalledTimes(1); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockAuthz.actions.api.get('foo')]); + expect(mockCheckPrivileges).toHaveBeenCalledWith({ + kibana: [mockAuthz.actions.api.get('foo')], + }); expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); }); @@ -129,7 +131,9 @@ describe('initAPIAuthorization', () => { expect(mockResponse.notFound).toHaveBeenCalledTimes(1); expect(mockPostAuthToolkit.next).not.toHaveBeenCalled(); - expect(mockCheckPrivileges).toHaveBeenCalledWith([mockAuthz.actions.api.get('foo')]); + expect(mockCheckPrivileges).toHaveBeenCalledWith({ + kibana: [mockAuthz.actions.api.get('foo')], + }); expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); }); }); diff --git a/x-pack/plugins/security/server/authorization/api_authorization.ts b/x-pack/plugins/security/server/authorization/api_authorization.ts index 0ffd3ba7ba823..9129330ec947a 100644 --- a/x-pack/plugins/security/server/authorization/api_authorization.ts +++ b/x-pack/plugins/security/server/authorization/api_authorization.ts @@ -29,7 +29,7 @@ export function initAPIAuthorization( const apiActions = actionTags.map((tag) => actions.api.get(tag.substring(tagPrefix.length))); const checkPrivileges = checkPrivilegesDynamicallyWithRequest(request); - const checkPrivilegesResponse = await checkPrivileges(apiActions); + const checkPrivilegesResponse = await checkPrivileges({ kibana: apiActions }); // we've actually authorized the request if (checkPrivilegesResponse.hasAllRequested) { diff --git a/x-pack/plugins/security/server/authorization/app_authorization.test.ts b/x-pack/plugins/security/server/authorization/app_authorization.test.ts index 1dc072ab2e6e9..f40d502a9cd7c 100644 --- a/x-pack/plugins/security/server/authorization/app_authorization.test.ts +++ b/x-pack/plugins/security/server/authorization/app_authorization.test.ts @@ -18,7 +18,7 @@ import { authorizationMock } from './index.mock'; const createFeaturesSetupContractMock = (): FeaturesSetupContract => { const mock = featuresPluginMock.createSetup(); - mock.getFeatures.mockReturnValue([ + mock.getKibanaFeatures.mockReturnValue([ { id: 'foo', name: 'Foo', app: ['foo'], privileges: {} } as any, ]); return mock; @@ -132,7 +132,7 @@ describe('initAppAuthorization', () => { expect(mockResponse.notFound).not.toHaveBeenCalled(); expect(mockPostAuthToolkit.next).toHaveBeenCalledTimes(1); - expect(mockCheckPrivileges).toHaveBeenCalledWith(mockAuthz.actions.app.get('foo')); + expect(mockCheckPrivileges).toHaveBeenCalledWith({ kibana: mockAuthz.actions.app.get('foo') }); expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); }); @@ -172,7 +172,7 @@ describe('initAppAuthorization', () => { expect(mockResponse.notFound).toHaveBeenCalledTimes(1); expect(mockPostAuthToolkit.next).not.toHaveBeenCalled(); - expect(mockCheckPrivileges).toHaveBeenCalledWith(mockAuthz.actions.app.get('foo')); + expect(mockCheckPrivileges).toHaveBeenCalledWith({ kibana: mockAuthz.actions.app.get('foo') }); expect(mockAuthz.mode.useRbacForRequest).toHaveBeenCalledWith(mockRequest); }); }); diff --git a/x-pack/plugins/security/server/authorization/app_authorization.ts b/x-pack/plugins/security/server/authorization/app_authorization.ts index 1036997ca821d..4170fd2cdb38a 100644 --- a/x-pack/plugins/security/server/authorization/app_authorization.ts +++ b/x-pack/plugins/security/server/authorization/app_authorization.ts @@ -19,7 +19,7 @@ class ProtectedApplications { if (this.applications == null) { this.applications = new Set( this.featuresService - .getFeatures() + .getKibanaFeatures() .map((feature) => feature.app) .flat() ); @@ -63,7 +63,7 @@ export function initAppAuthorization( const checkPrivileges = checkPrivilegesDynamicallyWithRequest(request); const appAction = actions.app.get(appId); - const checkPrivilegesResponse = await checkPrivileges(appAction); + const checkPrivilegesResponse = await checkPrivileges({ kibana: appAction }); logger.debug(`authorizing access to "${appId}"`); // we've actually authorized the request diff --git a/x-pack/plugins/security/server/authorization/authorization_service.test.ts b/x-pack/plugins/security/server/authorization/authorization_service.test.ts index 2fdc2d169e972..c00127f7d1229 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.test.ts +++ b/x-pack/plugins/security/server/authorization/authorization_service.test.ts @@ -74,6 +74,7 @@ it(`#setup returns exposed services`, () => { packageVersion: 'some-version', features: mockFeaturesSetup, getSpacesService: mockGetSpacesService, + getCurrentUser: jest.fn(), }); expect(authz.actions.version).toBe('version:some-version'); @@ -133,10 +134,11 @@ describe('#start', () => { getSpacesService: jest .fn() .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }), + getCurrentUser: jest.fn(), }); const featuresStart = featuresPluginMock.createStart(); - featuresStart.getFeatures.mockReturnValue([]); + featuresStart.getKibanaFeatures.mockReturnValue([]); authorizationService.start({ clusterClient: mockClusterClient, @@ -203,10 +205,12 @@ it('#stop unsubscribes from license and ES updates.', async () => { getSpacesService: jest .fn() .mockReturnValue({ getSpaceId: jest.fn(), namespaceToSpaceId: jest.fn() }), + getCurrentUser: jest.fn(), }); const featuresStart = featuresPluginMock.createStart(); - featuresStart.getFeatures.mockReturnValue([]); + featuresStart.getKibanaFeatures.mockReturnValue([]); + authorizationService.start({ clusterClient: mockClusterClient, features: featuresStart, diff --git a/x-pack/plugins/security/server/authorization/authorization_service.ts b/x-pack/plugins/security/server/authorization/authorization_service.ts index 2dead301b298a..fd3a60fb4d900 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.ts +++ b/x-pack/plugins/security/server/authorization/authorization_service.ts @@ -22,7 +22,7 @@ import { import { SpacesService } from '../plugin'; import { Actions } from './actions'; -import { CheckPrivilegesWithRequest, checkPrivilegesWithRequestFactory } from './check_privileges'; +import { checkPrivilegesWithRequestFactory } from './check_privileges'; import { CheckPrivilegesDynamicallyWithRequest, checkPrivilegesDynamicallyWithRequestFactory, @@ -41,7 +41,9 @@ import { validateReservedPrivileges } from './validate_reserved_privileges'; import { registerPrivilegesWithCluster } from './register_privileges_with_cluster'; import { APPLICATION_PREFIX } from '../../common/constants'; import { SecurityLicense } from '../../common/licensing'; +import { CheckPrivilegesWithRequest } from './types'; import { OnlineStatusRetryScheduler } from '../elasticsearch'; +import { AuthenticatedUser } from '..'; export { Actions } from './actions'; export { CheckSavedObjectsPrivileges } from './check_saved_objects_privileges'; @@ -57,6 +59,7 @@ interface AuthorizationServiceSetupParams { features: FeaturesPluginSetup; kibanaIndexName: string; getSpacesService(): SpacesService | undefined; + getCurrentUser(request: KibanaRequest): AuthenticatedUser | null; } interface AuthorizationServiceStartParams { @@ -92,6 +95,7 @@ export class AuthorizationService { features, kibanaIndexName, getSpacesService, + getCurrentUser, }: AuthorizationServiceSetupParams): AuthorizationServiceSetup { this.logger = loggers.get('authorization'); this.applicationName = `${APPLICATION_PREFIX}${kibanaIndexName}`; @@ -132,9 +136,11 @@ export class AuthorizationService { const disableUICapabilities = disableUICapabilitiesFactory( request, - features.getFeatures(), + features.getKibanaFeatures(), + features.getElasticsearchFeatures(), this.logger, - authz + authz, + getCurrentUser(request) ); if (!request.auth.isAuthenticated) { @@ -152,7 +158,7 @@ export class AuthorizationService { } start({ clusterClient, features, online$ }: AuthorizationServiceStartParams) { - const allFeatures = features.getFeatures(); + const allFeatures = features.getKibanaFeatures(); validateFeaturePrivileges(allFeatures); validateReservedPrivileges(allFeatures); diff --git a/x-pack/plugins/security/server/authorization/check_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_privileges.test.ts index b380f45a12d81..4151ff645005d 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.test.ts @@ -33,7 +33,11 @@ const createMockClusterClient = (response: any) => { describe('#atSpace', () => { const checkPrivilegesAtSpaceTest = async (options: { spaceId: string; - privilegeOrPrivileges: string | string[]; + kibanaPrivileges?: string | string[]; + elasticsearchPrivileges?: { + cluster: string[]; + index: Record; + }; esHasPrivilegesResponse: HasPrivilegesResponse; }) => { const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( @@ -50,25 +54,39 @@ describe('#atSpace', () => { let actualResult; let errorThrown = null; try { - actualResult = await checkPrivileges.atSpace(options.spaceId, options.privilegeOrPrivileges); + actualResult = await checkPrivileges.atSpace(options.spaceId, { + kibana: options.kibanaPrivileges, + elasticsearch: options.elasticsearchPrivileges, + }); } catch (err) { errorThrown = err; } + const expectedIndexPrivilegePayload = Object.entries( + options.elasticsearchPrivileges?.index ?? {} + ).map(([names, indexPrivileges]) => ({ + names, + privileges: indexPrivileges, + })); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.hasPrivileges', { body: { + cluster: options.elasticsearchPrivileges?.cluster, + index: expectedIndexPrivilegePayload, applications: [ { application, resources: [`space:${options.spaceId}`], - privileges: uniq([ - mockActions.version, - mockActions.login, - ...(Array.isArray(options.privilegeOrPrivileges) - ? options.privilegeOrPrivileges - : [options.privilegeOrPrivileges]), - ]), + privileges: options.kibanaPrivileges + ? uniq([ + mockActions.version, + mockActions.login, + ...(Array.isArray(options.kibanaPrivileges) + ? options.kibanaPrivileges + : [options.kibanaPrivileges]), + ]) + : [mockActions.version, mockActions.login], }, ], }, @@ -83,7 +101,7 @@ describe('#atSpace', () => { test('successful when checking for login and user has login', async () => { const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: true, username: 'foo-username', @@ -100,13 +118,19 @@ describe('#atSpace', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": true, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "mock-action:login", - "resource": "space_1", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_1", + }, + ], + }, "username": "foo-username", } `); @@ -115,7 +139,7 @@ describe('#atSpace', () => { test(`failure when checking for login and user doesn't have login`, async () => { const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -132,13 +156,19 @@ describe('#atSpace', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": false, - "privilege": "mock-action:login", - "resource": "space_1", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": false, + "privilege": "mock-action:login", + "resource": "space_1", + }, + ], + }, "username": "foo-username", } `); @@ -147,7 +177,7 @@ describe('#atSpace', () => { test(`throws error when checking for login and user has login but doesn't have version`, async () => { const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -169,7 +199,7 @@ describe('#atSpace', () => { test(`successful when checking for two actions and the user has both`, async () => { const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -191,18 +221,24 @@ describe('#atSpace', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": true, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": "space_1", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": "space_1", - }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + }, "username": "foo-username", } `); @@ -211,7 +247,7 @@ describe('#atSpace', () => { test(`failure when checking for two actions and the user has only one`, async () => { const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -233,18 +269,24 @@ describe('#atSpace', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": false, - "privilege": "saved_object:foo-type/get", - "resource": "space_1", - }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": "space_1", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + }, "username": "foo-username", } `); @@ -254,7 +296,7 @@ describe('#atSpace', () => { test(`throws a validation error when an extra privilege is present in the response`, async () => { const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -278,7 +320,7 @@ describe('#atSpace', () => { test(`throws a validation error when privileges are missing in the response`, async () => { const result = await checkPrivilegesAtSpaceTest({ spaceId: 'space_1', - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -297,12 +339,551 @@ describe('#atSpace', () => { ); }); }); + + describe('with both Kibana and Elasticsearch privileges', () => { + it('successful when checking for privileges, and user has all', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has only es privileges', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has only kibana privileges', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }, + cluster: { + foo: false, + bar: false, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": false, + "privilege": "foo", + }, + Object { + "authorized": false, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has none', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + cluster: { + foo: false, + bar: false, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": false, + "privilege": "foo", + }, + Object { + "authorized": false, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + ], + }, + "username": "foo-username", + } + `); + }); + }); + + describe('with Elasticsearch privileges', () => { + it('successful when checking for cluster privileges, and user has both', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('successful when checking for index privileges, and user has both', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: [], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: true, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": true, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('successful when checking for a combination of index and cluster privileges', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: ['manage', 'monitor'], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + manage: true, + monitor: true, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: true, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "manage", + }, + Object { + "authorized": true, + "privilege": "monitor", + }, + ], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": true, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for a combination of index and cluster privileges, and some are missing', async () => { + const result = await checkPrivilegesAtSpaceTest({ + spaceId: 'space_1', + elasticsearchPrivileges: { + cluster: ['manage', 'monitor'], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + manage: true, + monitor: true, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: false, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "manage", + }, + Object { + "authorized": true, + "privilege": "monitor", + }, + ], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": false, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + }); }); describe('#atSpaces', () => { const checkPrivilegesAtSpacesTest = async (options: { spaceIds: string[]; - privilegeOrPrivileges: string | string[]; + kibanaPrivileges?: string | string[]; + elasticsearchPrivileges?: { + cluster: string[]; + index: Record; + }; esHasPrivilegesResponse: HasPrivilegesResponse; }) => { const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( @@ -319,28 +900,39 @@ describe('#atSpaces', () => { let actualResult; let errorThrown = null; try { - actualResult = await checkPrivileges.atSpaces( - options.spaceIds, - options.privilegeOrPrivileges - ); + actualResult = await checkPrivileges.atSpaces(options.spaceIds, { + kibana: options.kibanaPrivileges, + elasticsearch: options.elasticsearchPrivileges, + }); } catch (err) { errorThrown = err; } + const expectedIndexPrivilegePayload = Object.entries( + options.elasticsearchPrivileges?.index ?? {} + ).map(([names, indexPrivileges]) => ({ + names, + privileges: indexPrivileges, + })); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.hasPrivileges', { body: { + cluster: options.elasticsearchPrivileges?.cluster, + index: expectedIndexPrivilegePayload, applications: [ { application, resources: options.spaceIds.map((spaceId) => `space:${spaceId}`), - privileges: uniq([ - mockActions.version, - mockActions.login, - ...(Array.isArray(options.privilegeOrPrivileges) - ? options.privilegeOrPrivileges - : [options.privilegeOrPrivileges]), - ]), + privileges: options.kibanaPrivileges + ? uniq([ + mockActions.version, + mockActions.login, + ...(Array.isArray(options.kibanaPrivileges) + ? options.kibanaPrivileges + : [options.kibanaPrivileges]), + ]) + : [mockActions.version, mockActions.login], }, ], }, @@ -355,7 +947,7 @@ describe('#atSpaces', () => { test('successful when checking for login and user has login at both spaces', async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: true, username: 'foo-username', @@ -376,18 +968,24 @@ describe('#atSpaces', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": true, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "mock-action:login", - "resource": "space_1", - }, - Object { - "authorized": true, - "privilege": "mock-action:login", - "resource": "space_2", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_2", + }, + ], + }, "username": "foo-username", } `); @@ -396,7 +994,7 @@ describe('#atSpaces', () => { test('failure when checking for login and user has login at only one space', async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -417,18 +1015,24 @@ describe('#atSpaces', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "mock-action:login", - "resource": "space_1", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - Object { - "authorized": false, - "privilege": "mock-action:login", - "resource": "space_2", - }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "mock-action:login", + "resource": "space_2", + }, + ], + }, "username": "foo-username", } `); @@ -437,7 +1041,7 @@ describe('#atSpaces', () => { test(`throws error when checking for login and user has login but doesn't have version`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -463,7 +1067,7 @@ describe('#atSpaces', () => { test(`throws error when Elasticsearch returns malformed response`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -492,7 +1096,7 @@ describe('#atSpaces', () => { test(`successful when checking for two actions at two spaces and user has it all`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -520,28 +1124,34 @@ describe('#atSpaces', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": true, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": "space_1", - }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": "space_1", - }, - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": "space_2", - }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": "space_2", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + }, "username": "foo-username", } `); @@ -550,7 +1160,7 @@ describe('#atSpaces', () => { test(`failure when checking for two actions at two spaces and user has one action at one space`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -578,28 +1188,34 @@ describe('#atSpaces', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": "space_1", - }, - Object { - "authorized": false, - "privilege": "saved_object:bar-type/get", - "resource": "space_1", - }, - Object { - "authorized": false, - "privilege": "saved_object:foo-type/get", - "resource": "space_2", - }, - Object { - "authorized": false, - "privilege": "saved_object:bar-type/get", - "resource": "space_2", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + }, "username": "foo-username", } `); @@ -608,7 +1224,7 @@ describe('#atSpaces', () => { test(`failure when checking for two actions at two spaces and user has two actions at one space`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -636,28 +1252,34 @@ describe('#atSpaces', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": "space_1", - }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": "space_1", - }, - Object { - "authorized": false, - "privilege": "saved_object:foo-type/get", - "resource": "space_2", - }, - Object { - "authorized": false, - "privilege": "saved_object:bar-type/get", - "resource": "space_2", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + }, "username": "foo-username", } `); @@ -666,7 +1288,7 @@ describe('#atSpaces', () => { test(`failure when checking for two actions at two spaces and user has two actions at one space & one action at the other`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -694,28 +1316,34 @@ describe('#atSpaces', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": "space_1", - }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": "space_1", - }, - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": "space_2", - }, - Object { - "authorized": false, - "privilege": "saved_object:bar-type/get", - "resource": "space_2", + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + }, "username": "foo-username", } `); @@ -725,7 +1353,7 @@ describe('#atSpaces', () => { test(`throws a validation error when an extra privilege is present in the response`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -755,7 +1383,7 @@ describe('#atSpaces', () => { test(`throws a validation error when privileges are missing in the response`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -783,7 +1411,7 @@ describe('#atSpaces', () => { test(`throws a validation error when an extra space is present in the response`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -816,7 +1444,7 @@ describe('#atSpaces', () => { test(`throws a validation error when an a space is missing in the response`, async () => { const result = await checkPrivilegesAtSpacesTest({ spaceIds: ['space_1', 'space_2'], - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -836,13 +1464,632 @@ describe('#atSpaces', () => { ); }); }); -}); -describe('#globally', () => { - const checkPrivilegesGloballyTest = async (options: { - privilegeOrPrivileges: string | string[]; - esHasPrivilegesResponse: HasPrivilegesResponse; - }) => { + describe('with both Kibana and Elasticsearch privileges', () => { + it('successful when checking for privileges, and user has all', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has only es privileges', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has only kibana privileges', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }, + cluster: { + foo: false, + bar: false, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": false, + "privilege": "foo", + }, + Object { + "authorized": false, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has none', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + cluster: { + foo: false, + bar: false, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": false, + "privilege": "foo", + }, + Object { + "authorized": false, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_1", + }, + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": "space_2", + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": "space_2", + }, + ], + }, + "username": "foo-username", + } + `); + }); + }); + + describe('with Elasticsearch privileges', () => { + it('successful when checking for cluster privileges, and user has both', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('successful when checking for index privileges, and user has both', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: [], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: true, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": true, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('successful when checking for a combination of index and cluster privileges', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: ['manage', 'monitor'], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + manage: true, + monitor: true, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: true, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "manage", + }, + Object { + "authorized": true, + "privilege": "monitor", + }, + ], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": true, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for a combination of index and cluster privileges, and some are missing', async () => { + const result = await checkPrivilegesAtSpacesTest({ + spaceIds: ['space_1', 'space_2'], + elasticsearchPrivileges: { + cluster: ['manage', 'monitor'], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + 'space:space_1': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + 'space:space_2': { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + manage: true, + monitor: true, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: false, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "manage", + }, + Object { + "authorized": true, + "privilege": "monitor", + }, + ], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": false, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + }); +}); + +describe('#globally', () => { + const checkPrivilegesGloballyTest = async (options: { + kibanaPrivileges?: string | string[]; + elasticsearchPrivileges?: { + cluster: string[]; + index: Record; + }; + esHasPrivilegesResponse: HasPrivilegesResponse; + }) => { const { mockClusterClient, mockScopedClusterClient } = createMockClusterClient( options.esHasPrivilegesResponse ); @@ -857,25 +2104,39 @@ describe('#globally', () => { let actualResult; let errorThrown = null; try { - actualResult = await checkPrivileges.globally(options.privilegeOrPrivileges); + actualResult = await checkPrivileges.globally({ + kibana: options.kibanaPrivileges, + elasticsearch: options.elasticsearchPrivileges, + }); } catch (err) { errorThrown = err; } + const expectedIndexPrivilegePayload = Object.entries( + options.elasticsearchPrivileges?.index ?? {} + ).map(([names, indexPrivileges]) => ({ + names, + privileges: indexPrivileges, + })); + expect(mockClusterClient.asScoped).toHaveBeenCalledWith(request); expect(mockScopedClusterClient.callAsCurrentUser).toHaveBeenCalledWith('shield.hasPrivileges', { body: { + cluster: options.elasticsearchPrivileges?.cluster, + index: expectedIndexPrivilegePayload, applications: [ { application, resources: [GLOBAL_RESOURCE], - privileges: uniq([ - mockActions.version, - mockActions.login, - ...(Array.isArray(options.privilegeOrPrivileges) - ? options.privilegeOrPrivileges - : [options.privilegeOrPrivileges]), - ]), + privileges: options.kibanaPrivileges + ? uniq([ + mockActions.version, + mockActions.login, + ...(Array.isArray(options.kibanaPrivileges) + ? options.kibanaPrivileges + : [options.kibanaPrivileges]), + ]) + : [mockActions.version, mockActions.login], }, ], }, @@ -889,7 +2150,7 @@ describe('#globally', () => { test('successful when checking for login and user has login', async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: true, username: 'foo-username', @@ -906,13 +2167,19 @@ describe('#globally', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": true, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "mock-action:login", - "resource": undefined, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "mock-action:login", + "resource": undefined, + }, + ], + }, "username": "foo-username", } `); @@ -920,7 +2187,7 @@ describe('#globally', () => { test(`failure when checking for login and user doesn't have login`, async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -937,13 +2204,19 @@ describe('#globally', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": false, - "privilege": "mock-action:login", - "resource": undefined, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": false, + "privilege": "mock-action:login", + "resource": undefined, + }, + ], + }, "username": "foo-username", } `); @@ -951,7 +2224,7 @@ describe('#globally', () => { test(`throws error when checking for login and user has login but doesn't have version`, async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: mockActions.login, + kibanaPrivileges: mockActions.login, esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -972,7 +2245,7 @@ describe('#globally', () => { test(`throws error when Elasticsearch returns malformed response`, async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -996,7 +2269,7 @@ describe('#globally', () => { test(`successful when checking for two actions and the user has both`, async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -1018,18 +2291,24 @@ describe('#globally', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": true, - "privileges": Array [ - Object { - "authorized": true, - "privilege": "saved_object:foo-type/get", - "resource": undefined, - }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": undefined, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + }, "username": "foo-username", } `); @@ -1037,7 +2316,7 @@ describe('#globally', () => { test(`failure when checking for two actions and the user has only one`, async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: [ + kibanaPrivileges: [ `saved_object:${savedObjectTypes[0]}/get`, `saved_object:${savedObjectTypes[1]}/get`, ], @@ -1059,18 +2338,24 @@ describe('#globally', () => { expect(result).toMatchInlineSnapshot(` Object { "hasAllRequested": false, - "privileges": Array [ - Object { - "authorized": false, - "privilege": "saved_object:foo-type/get", - "resource": undefined, - }, - Object { - "authorized": true, - "privilege": "saved_object:bar-type/get", - "resource": undefined, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object {}, }, - ], + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + }, "username": "foo-username", } `); @@ -1079,7 +2364,7 @@ describe('#globally', () => { describe('with a malformed Elasticsearch response', () => { test(`throws a validation error when an extra privilege is present in the response`, async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -1102,7 +2387,7 @@ describe('#globally', () => { test(`throws a validation error when privileges are missing in the response`, async () => { const result = await checkPrivilegesGloballyTest({ - privilegeOrPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], + kibanaPrivileges: [`saved_object:${savedObjectTypes[0]}/get`], esHasPrivilegesResponse: { has_all_requested: false, username: 'foo-username', @@ -1121,4 +2406,531 @@ describe('#globally', () => { ); }); }); + + describe('with both Kibana and Elasticsearch privileges', () => { + it('successful when checking for privileges, and user has all', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has only es privileges', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has only kibana privileges', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: true, + [`saved_object:${savedObjectTypes[1]}/get`]: true, + }, + }, + }, + cluster: { + foo: false, + bar: false, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": false, + "privilege": "foo", + }, + Object { + "authorized": false, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": true, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": true, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for privileges, and user has none', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + kibanaPrivileges: [ + `saved_object:${savedObjectTypes[0]}/get`, + `saved_object:${savedObjectTypes[1]}/get`, + ], + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + [`saved_object:${savedObjectTypes[0]}/get`]: false, + [`saved_object:${savedObjectTypes[1]}/get`]: false, + }, + }, + }, + cluster: { + foo: false, + bar: false, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": false, + "privilege": "foo", + }, + Object { + "authorized": false, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [ + Object { + "authorized": false, + "privilege": "saved_object:foo-type/get", + "resource": undefined, + }, + Object { + "authorized": false, + "privilege": "saved_object:bar-type/get", + "resource": undefined, + }, + ], + }, + "username": "foo-username", + } + `); + }); + }); + + describe('with Elasticsearch privileges', () => { + it('successful when checking for cluster privileges, and user has both', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: ['foo', 'bar'], + index: {}, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + foo: true, + bar: true, + }, + index: {}, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "foo", + }, + Object { + "authorized": true, + "privilege": "bar", + }, + ], + "index": Object {}, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('successful when checking for index privileges, and user has both', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: [], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: true, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": true, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('successful when checking for a combination of index and cluster privileges', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: ['manage', 'monitor'], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: true, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + manage: true, + monitor: true, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: true, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": true, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "manage", + }, + Object { + "authorized": true, + "privilege": "monitor", + }, + ], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": true, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + + it('failure when checking for a combination of index and cluster privileges, and some are missing', async () => { + const result = await checkPrivilegesGloballyTest({ + elasticsearchPrivileges: { + cluster: ['manage', 'monitor'], + index: { + foo: ['all'], + bar: ['read', 'view_index_metadata'], + }, + }, + esHasPrivilegesResponse: { + has_all_requested: false, + username: 'foo-username', + application: { + [application]: { + [GLOBAL_RESOURCE]: { + [mockActions.login]: true, + [mockActions.version]: true, + }, + }, + }, + cluster: { + manage: true, + monitor: true, + }, + index: { + foo: { + all: true, + }, + bar: { + read: true, + view_index_metadata: false, + }, + }, + }, + }); + expect(result).toMatchInlineSnapshot(` + Object { + "hasAllRequested": false, + "privileges": Object { + "elasticsearch": Object { + "cluster": Array [ + Object { + "authorized": true, + "privilege": "manage", + }, + Object { + "authorized": true, + "privilege": "monitor", + }, + ], + "index": Object { + "bar": Array [ + Object { + "authorized": true, + "privilege": "read", + }, + Object { + "authorized": false, + "privilege": "view_index_metadata", + }, + ], + "foo": Array [ + Object { + "authorized": true, + "privilege": "all", + }, + ], + }, + }, + "kibana": Array [], + }, + "username": "foo-username", + } + `); + }); + }); }); diff --git a/x-pack/plugins/security/server/authorization/check_privileges.ts b/x-pack/plugins/security/server/authorization/check_privileges.ts index 3129777a7881f..27e1802b4e5c2 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges.ts @@ -8,7 +8,13 @@ import { pick, transform, uniq } from 'lodash'; import { ILegacyClusterClient, KibanaRequest } from '../../../../../src/core/server'; import { GLOBAL_RESOURCE } from '../../common/constants'; import { ResourceSerializer } from './resource_serializer'; -import { HasPrivilegesResponse, HasPrivilegesResponseApplication } from './types'; +import { + HasPrivilegesResponse, + HasPrivilegesResponseApplication, + CheckPrivilegesPayload, + CheckPrivileges, + CheckPrivilegesResponse, +} from './types'; import { validateEsPrivilegeResponse } from './validate_es_response'; interface CheckPrivilegesActions { @@ -16,33 +22,6 @@ interface CheckPrivilegesActions { version: string; } -export interface CheckPrivilegesResponse { - hasAllRequested: boolean; - username: string; - privileges: Array<{ - /** - * If this attribute is undefined, this element is a privilege for the global resource. - */ - resource?: string; - privilege: string; - authorized: boolean; - }>; -} - -export type CheckPrivilegesWithRequest = (request: KibanaRequest) => CheckPrivileges; - -export interface CheckPrivileges { - atSpace( - spaceId: string, - privilegeOrPrivileges: string | string[] - ): Promise; - atSpaces( - spaceIds: string[], - privilegeOrPrivileges: string | string[] - ): Promise; - globally(privilegeOrPrivileges: string | string[]): Promise; -} - export function checkPrivilegesWithRequestFactory( actions: CheckPrivilegesActions, clusterClient: ILegacyClusterClient, @@ -59,17 +38,26 @@ export function checkPrivilegesWithRequestFactory( return function checkPrivilegesWithRequest(request: KibanaRequest): CheckPrivileges { const checkPrivilegesAtResources = async ( resources: string[], - privilegeOrPrivileges: string | string[] + privileges: CheckPrivilegesPayload ): Promise => { - const privileges = Array.isArray(privilegeOrPrivileges) - ? privilegeOrPrivileges - : [privilegeOrPrivileges]; - const allApplicationPrivileges = uniq([actions.version, actions.login, ...privileges]); + const kibanaPrivileges = Array.isArray(privileges.kibana) + ? privileges.kibana + : privileges.kibana + ? [privileges.kibana] + : []; + const allApplicationPrivileges = uniq([actions.version, actions.login, ...kibanaPrivileges]); const hasPrivilegesResponse = (await clusterClient .asScoped(request) .callAsCurrentUser('shield.hasPrivileges', { body: { + cluster: privileges.elasticsearch?.cluster, + index: Object.entries(privileges.elasticsearch?.index ?? {}).map( + ([names, indexPrivileges]) => ({ + names, + privileges: indexPrivileges, + }) + ), applications: [ { application: applicationName, resources, privileges: allApplicationPrivileges }, ], @@ -85,6 +73,27 @@ export function checkPrivilegesWithRequestFactory( const applicationPrivilegesResponse = hasPrivilegesResponse.application[applicationName]; + const clusterPrivilegesResponse = hasPrivilegesResponse.cluster ?? {}; + + const clusterPrivileges = Object.entries(clusterPrivilegesResponse).map( + ([privilege, authorized]) => ({ + privilege, + authorized, + }) + ); + + const indexPrivileges = Object.entries(hasPrivilegesResponse.index ?? {}).reduce< + CheckPrivilegesResponse['privileges']['elasticsearch']['index'] + >((acc, [index, indexResponse]) => { + return { + ...acc, + [index]: Object.entries(indexResponse).map(([privilege, authorized]) => ({ + privilege, + authorized, + })), + }; + }, {}); + if (hasIncompatibleVersion(applicationPrivilegesResponse)) { throw new Error( 'Multiple versions of Kibana are running against the same Elasticsearch cluster, unable to authorize user.' @@ -93,7 +102,7 @@ export function checkPrivilegesWithRequestFactory( // we need to filter out the non requested privileges from the response const resourcePrivileges = transform(applicationPrivilegesResponse, (result, value, key) => { - result[key!] = pick(value, privileges); + result[key!] = pick(value, privileges.kibana ?? []); }) as HasPrivilegesResponseApplication; const privilegeArray = Object.entries(resourcePrivileges) .map(([key, val]) => { @@ -111,23 +120,29 @@ export function checkPrivilegesWithRequestFactory( return { hasAllRequested: hasPrivilegesResponse.has_all_requested, username: hasPrivilegesResponse.username, - privileges: privilegeArray, + privileges: { + kibana: privilegeArray, + elasticsearch: { + cluster: clusterPrivileges, + index: indexPrivileges, + }, + }, }; }; return { - async atSpace(spaceId: string, privilegeOrPrivileges: string | string[]) { + async atSpace(spaceId: string, privileges: CheckPrivilegesPayload) { const spaceResource = ResourceSerializer.serializeSpaceResource(spaceId); - return await checkPrivilegesAtResources([spaceResource], privilegeOrPrivileges); + return await checkPrivilegesAtResources([spaceResource], privileges); }, - async atSpaces(spaceIds: string[], privilegeOrPrivileges: string | string[]) { + async atSpaces(spaceIds: string[], privileges: CheckPrivilegesPayload) { const spaceResources = spaceIds.map((spaceId) => ResourceSerializer.serializeSpaceResource(spaceId) ); - return await checkPrivilegesAtResources(spaceResources, privilegeOrPrivileges); + return await checkPrivilegesAtResources(spaceResources, privileges); }, - async globally(privilegeOrPrivileges: string | string[]) { - return await checkPrivilegesAtResources([GLOBAL_RESOURCE], privilegeOrPrivileges); + async globally(privileges: CheckPrivilegesPayload) { + return await checkPrivilegesAtResources([GLOBAL_RESOURCE], privileges); }, }; }; diff --git a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.test.ts b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.test.ts index 2206748597635..093b308f59391 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.test.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.test.ts @@ -24,11 +24,13 @@ test(`checkPrivileges.atSpace when spaces is enabled`, async () => { namespaceToSpaceId: jest.fn(), }) )(request); - const result = await checkPrivilegesDynamically(privilegeOrPrivileges); + const result = await checkPrivilegesDynamically({ kibana: privilegeOrPrivileges }); expect(result).toBe(expectedResult); expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, privilegeOrPrivileges); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, { + kibana: privilegeOrPrivileges, + }); }); test(`checkPrivileges.globally when spaces is disabled`, async () => { @@ -43,9 +45,9 @@ test(`checkPrivileges.globally when spaces is disabled`, async () => { mockCheckPrivilegesWithRequest, () => undefined )(request); - const result = await checkPrivilegesDynamically(privilegeOrPrivileges); + const result = await checkPrivilegesDynamically({ kibana: privilegeOrPrivileges }); expect(result).toBe(expectedResult); expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith(privilegeOrPrivileges); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith({ kibana: privilegeOrPrivileges }); }); diff --git a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts index 6014bad739e77..cd5961e5940ed 100644 --- a/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts +++ b/x-pack/plugins/security/server/authorization/check_privileges_dynamically.ts @@ -6,10 +6,11 @@ import { KibanaRequest } from '../../../../../src/core/server'; import { SpacesService } from '../plugin'; -import { CheckPrivilegesResponse, CheckPrivilegesWithRequest } from './check_privileges'; +import { CheckPrivilegesResponse, CheckPrivilegesWithRequest } from './types'; +import { CheckPrivilegesPayload } from './types'; export type CheckPrivilegesDynamically = ( - privilegeOrPrivileges: string | string[] + privileges: CheckPrivilegesPayload ) => Promise; export type CheckPrivilegesDynamicallyWithRequest = ( @@ -22,11 +23,11 @@ export function checkPrivilegesDynamicallyWithRequestFactory( ): CheckPrivilegesDynamicallyWithRequest { return function checkPrivilegesDynamicallyWithRequest(request: KibanaRequest) { const checkPrivileges = checkPrivilegesWithRequest(request); - return async function checkPrivilegesDynamically(privilegeOrPrivileges: string | string[]) { + return async function checkPrivilegesDynamically(privileges: CheckPrivilegesPayload) { const spacesService = getSpacesService(); return spacesService - ? await checkPrivileges.atSpace(spacesService.getSpaceId(request), privilegeOrPrivileges) - : await checkPrivileges.globally(privilegeOrPrivileges); + ? await checkPrivileges.atSpace(spacesService.getSpaceId(request), privileges) + : await checkPrivileges.globally(privileges); }; }; } diff --git a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts index 5e38045b88c74..f287cc04280ac 100644 --- a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.test.ts @@ -7,7 +7,7 @@ import { checkSavedObjectsPrivilegesWithRequestFactory } from './check_saved_objects_privileges'; import { httpServerMock } from '../../../../../src/core/server/mocks'; -import { CheckPrivileges, CheckPrivilegesWithRequest } from './check_privileges'; +import { CheckPrivileges, CheckPrivilegesWithRequest } from './types'; import { SpacesService } from '../plugin'; let mockCheckPrivileges: jest.Mocked; @@ -31,7 +31,9 @@ beforeEach(() => { mockSpacesService = { getSpaceId: jest.fn(), - namespaceToSpaceId: jest.fn().mockImplementation((namespace: string) => `${namespace}-id`), + namespaceToSpaceId: jest + .fn() + .mockImplementation((namespace: string = 'default') => `${namespace}-id`), }; }); @@ -41,8 +43,6 @@ describe('#checkSavedObjectsPrivileges', () => { const namespace2 = 'qux'; describe('when checking multiple namespaces', () => { - const namespaces = [namespace1, namespace2]; - test(`throws an error when using an empty namespaces array`, async () => { const checkSavedObjectsPrivileges = createFactory(); @@ -58,6 +58,7 @@ describe('#checkSavedObjectsPrivileges', () => { mockCheckPrivileges.atSpaces.mockReturnValue(expectedResult as any); const checkSavedObjectsPrivileges = createFactory(); + const namespaces = [namespace1, namespace2]; const result = await checkSavedObjectsPrivileges(actions, namespaces); expect(result).toBe(expectedResult); @@ -68,7 +69,31 @@ describe('#checkSavedObjectsPrivileges', () => { expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledTimes(1); const spaceIds = mockSpacesService!.namespaceToSpaceId.mock.results.map((x) => x.value); - expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledWith(spaceIds, actions); + expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledWith(spaceIds, { kibana: actions }); + }); + + test(`de-duplicates namespaces`, async () => { + const expectedResult = Symbol(); + mockCheckPrivileges.atSpaces.mockReturnValue(expectedResult as any); + const checkSavedObjectsPrivileges = createFactory(); + + const namespaces = [undefined, 'default', namespace1, namespace1]; + const result = await checkSavedObjectsPrivileges(actions, namespaces); + + expect(result).toBe(expectedResult); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenCalledTimes(4); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenNthCalledWith(1, undefined); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenNthCalledWith(2, 'default'); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenNthCalledWith(3, namespace1); + expect(mockSpacesService!.namespaceToSpaceId).toHaveBeenNthCalledWith(4, namespace1); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledTimes(1); + expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); + expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledTimes(1); + const spaceIds = [ + mockSpacesService!.namespaceToSpaceId(undefined), // deduplicated with 'default' + mockSpacesService!.namespaceToSpaceId(namespace1), // deduplicated with namespace1 + ]; + expect(mockCheckPrivileges.atSpaces).toHaveBeenCalledWith(spaceIds, { kibana: actions }); }); }); @@ -87,7 +112,7 @@ describe('#checkSavedObjectsPrivileges', () => { expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivileges.atSpace).toHaveBeenCalledTimes(1); const spaceId = mockSpacesService!.namespaceToSpaceId.mock.results[0].value; - expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, actions); + expect(mockCheckPrivileges.atSpace).toHaveBeenCalledWith(spaceId, { kibana: actions }); }); test(`uses checkPrivileges.globally when Spaces is disabled`, async () => { @@ -102,7 +127,7 @@ describe('#checkSavedObjectsPrivileges', () => { expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledTimes(1); expect(mockCheckPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivileges.globally).toHaveBeenCalledTimes(1); - expect(mockCheckPrivileges.globally).toHaveBeenCalledWith(actions); + expect(mockCheckPrivileges.globally).toHaveBeenCalledWith({ kibana: actions }); }); }); }); diff --git a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts index 0c2260542bf72..7c0ca7dcaa392 100644 --- a/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts +++ b/x-pack/plugins/security/server/authorization/check_saved_objects_privileges.ts @@ -6,7 +6,7 @@ import { KibanaRequest } from '../../../../../src/core/server'; import { SpacesService } from '../plugin'; -import { CheckPrivilegesWithRequest, CheckPrivilegesResponse } from './check_privileges'; +import { CheckPrivilegesWithRequest, CheckPrivilegesResponse } from './types'; export type CheckSavedObjectsPrivilegesWithRequest = ( request: KibanaRequest @@ -14,9 +14,13 @@ export type CheckSavedObjectsPrivilegesWithRequest = ( export type CheckSavedObjectsPrivileges = ( actions: string | string[], - namespaceOrNamespaces?: string | string[] + namespaceOrNamespaces?: string | Array ) => Promise; +function uniq(arr: T[]): T[] { + return Array.from(new Set(arr)); +} + export const checkSavedObjectsPrivilegesWithRequestFactory = ( checkPrivilegesWithRequest: CheckPrivilegesWithRequest, getSpacesService: () => SpacesService | undefined @@ -26,23 +30,26 @@ export const checkSavedObjectsPrivilegesWithRequestFactory = ( ): CheckSavedObjectsPrivileges { return async function checkSavedObjectsPrivileges( actions: string | string[], - namespaceOrNamespaces?: string | string[] + namespaceOrNamespaces?: string | Array ) { const spacesService = getSpacesService(); if (!spacesService) { // Spaces disabled, authorizing globally - return await checkPrivilegesWithRequest(request).globally(actions); + return await checkPrivilegesWithRequest(request).globally({ kibana: actions }); } else if (Array.isArray(namespaceOrNamespaces)) { // Spaces enabled, authorizing against multiple spaces if (!namespaceOrNamespaces.length) { throw new Error(`Can't check saved object privileges for 0 namespaces`); } - const spaceIds = namespaceOrNamespaces.map((x) => spacesService.namespaceToSpaceId(x)); - return await checkPrivilegesWithRequest(request).atSpaces(spaceIds, actions); + const spaceIds = uniq( + namespaceOrNamespaces.map((x) => spacesService.namespaceToSpaceId(x)) + ); + + return await checkPrivilegesWithRequest(request).atSpaces(spaceIds, { kibana: actions }); } else { // Spaces enabled, authorizing against a single space const spaceId = spacesService.namespaceToSpaceId(namespaceOrNamespaces); - return await checkPrivilegesWithRequest(request).atSpace(spaceId, actions); + return await checkPrivilegesWithRequest(request).atSpace(spaceId, { kibana: actions }); } }; }; diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts index f9405214aac5a..98faae6edab2c 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.test.ts @@ -9,11 +9,17 @@ import { disableUICapabilitiesFactory } from './disable_ui_capabilities'; import { httpServerMock, loggingSystemMock } from '../../../../../src/core/server/mocks'; import { authorizationMock } from './index.mock'; -import { Feature } from '../../../features/server'; +import { KibanaFeature, ElasticsearchFeature } from '../../../features/server'; +import { AuthenticatedUser } from '..'; +import { CheckPrivilegesResponse } from './types'; type MockAuthzOptions = | { rejectCheckPrivileges: any } - | { resolveCheckPrivileges: { privileges: Array<{ privilege: string; authorized: boolean }> } }; + | { + resolveCheckPrivileges: { + privileges: CheckPrivilegesResponse['privileges']; + }; + }; const actions = new Actions('1.0.0-zeta1'); const mockRequest = httpServerMock.createKibanaRequest(); @@ -31,14 +37,34 @@ const createMockAuthz = (options: MockAuthzOptions) => { throw options.rejectCheckPrivileges; } - const expected = options.resolveCheckPrivileges.privileges.map((x) => x.privilege); - expect(checkActions).toEqual(expected); + const expectedKibana = options.resolveCheckPrivileges.privileges.kibana.map( + (x) => x.privilege + ); + const expectedCluster = ( + options.resolveCheckPrivileges.privileges.elasticsearch.cluster ?? [] + ).map((x) => x.privilege); + + expect(checkActions).toEqual({ + kibana: expectedKibana, + elasticsearch: { cluster: expectedCluster, index: {} }, + }); return options.resolveCheckPrivileges; }); }); + mock.checkElasticsearchPrivilegesWithRequest.mockImplementation((request) => { + expect(request).toBe(mockRequest); + return jest.fn().mockImplementation((privileges) => {}); + }); return mock; }; +const createMockUser = (user: Partial = {}) => + ({ + username: 'mock_user', + roles: [], + ...user, + } as AuthenticatedUser); + describe('usingPrivileges', () => { describe('checkPrivileges errors', () => { test(`disables uiCapabilities when a 401 is thrown`, async () => { @@ -50,16 +76,28 @@ describe('usingPrivileges', () => { const { usingPrivileges } = disableUICapabilitiesFactory( mockRequest, [ - new Feature({ + new KibanaFeature({ id: 'fooFeature', - name: 'Foo Feature', + name: 'Foo KibanaFeature', app: ['fooApp', 'foo'], navLinkId: 'foo', privileges: null, }), ], + [ + new ElasticsearchFeature({ + id: 'esFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: [], + }, + ], + }), + ], mockLoggers.get(), - mockAuthz + mockAuthz, + createMockUser() ); const result = await usingPrivileges( @@ -126,16 +164,28 @@ describe('usingPrivileges', () => { const { usingPrivileges } = disableUICapabilitiesFactory( mockRequest, [ - new Feature({ + new KibanaFeature({ id: 'fooFeature', - name: 'Foo Feature', + name: 'Foo KibanaFeature', app: ['foo'], navLinkId: 'foo', privileges: null, }), ], + [ + new ElasticsearchFeature({ + id: 'esFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: [], + }, + ], + }), + ], mockLoggers.get(), - mockAuthz + mockAuthz, + createMockUser() ); const result = await usingPrivileges( @@ -199,8 +249,10 @@ describe('usingPrivileges', () => { const { usingPrivileges } = disableUICapabilitiesFactory( mockRequest, [], + [], mockLoggers.get(), - mockAuthz + mockAuthz, + createMockUser() ); await expect( @@ -234,40 +286,91 @@ describe('usingPrivileges', () => { test(`disables ui capabilities when they don't have privileges`, async () => { const mockAuthz = createMockAuthz({ resolveCheckPrivileges: { - privileges: [ - { privilege: actions.ui.get('navLinks', 'foo'), authorized: true }, - { privilege: actions.ui.get('navLinks', 'bar'), authorized: false }, - { privilege: actions.ui.get('navLinks', 'quz'), authorized: false }, - { privilege: actions.ui.get('management', 'kibana', 'indices'), authorized: true }, - { privilege: actions.ui.get('management', 'kibana', 'settings'), authorized: false }, - { privilege: actions.ui.get('fooFeature', 'foo'), authorized: true }, - { privilege: actions.ui.get('fooFeature', 'bar'), authorized: false }, - { privilege: actions.ui.get('barFeature', 'foo'), authorized: true }, - { privilege: actions.ui.get('barFeature', 'bar'), authorized: false }, - ], + privileges: { + kibana: [ + { privilege: actions.ui.get('navLinks', 'foo'), authorized: true }, + { privilege: actions.ui.get('navLinks', 'bar'), authorized: false }, + { privilege: actions.ui.get('navLinks', 'quz'), authorized: false }, + { privilege: actions.ui.get('management', 'kibana', 'indices'), authorized: true }, + { privilege: actions.ui.get('management', 'kibana', 'settings'), authorized: false }, + { + privilege: actions.ui.get('management', 'kibana', 'esManagement'), + authorized: false, + }, + { privilege: actions.ui.get('fooFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('fooFeature', 'bar'), authorized: false }, + { privilege: actions.ui.get('barFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('barFeature', 'bar'), authorized: false }, + ], + elasticsearch: { + cluster: [ + { privilege: 'manage', authorized: false }, + { privilege: 'monitor', authorized: true }, + { privilege: 'manage_security', authorized: true }, + ], + index: {}, + }, + }, }, }); const { usingPrivileges } = disableUICapabilitiesFactory( mockRequest, [ - new Feature({ + new KibanaFeature({ id: 'fooFeature', - name: 'Foo Feature', + name: 'Foo KibanaFeature', navLinkId: 'foo', app: [], privileges: null, }), - new Feature({ + new KibanaFeature({ id: 'barFeature', - name: 'Bar Feature', + name: 'Bar KibanaFeature', navLinkId: 'bar', app: ['bar'], privileges: null, }), ], + [ + new ElasticsearchFeature({ + id: 'esFeature', + privileges: [ + { + requiredClusterPrivileges: ['manage'], + ui: ['es_manage'], + }, + { + requiredClusterPrivileges: ['monitor'], + ui: ['es_monitor'], + }, + ], + }), + new ElasticsearchFeature({ + id: 'esSecurityFeature', + privileges: [ + { + requiredClusterPrivileges: ['manage_security'], + ui: ['es_manage_sec'], + }, + ], + }), + new ElasticsearchFeature({ + id: 'esManagementFeature', + management: { + kibana: ['esManagement'], + }, + privileges: [ + { + requiredClusterPrivileges: ['manage_security'], + ui: [], + }, + ], + }), + ], loggingSystemMock.create().get(), - mockAuthz + mockAuthz, + createMockUser() ); const result = await usingPrivileges( @@ -281,6 +384,7 @@ describe('usingPrivileges', () => { kibana: { indices: true, settings: false, + esManagement: true, }, }, catalogue: {}, @@ -292,6 +396,14 @@ describe('usingPrivileges', () => { foo: true, bar: true, }, + esFeature: { + es_manage: true, + es_monitor: true, + }, + esSecurityFeature: { + es_manage_sec: true, + }, + esManagementFeature: {}, }) ); @@ -305,6 +417,7 @@ describe('usingPrivileges', () => { kibana: { indices: true, settings: false, + esManagement: true, }, }, catalogue: {}, @@ -316,44 +429,70 @@ describe('usingPrivileges', () => { foo: true, bar: false, }, + esFeature: { + es_manage: false, + es_monitor: true, + }, + esSecurityFeature: { + es_manage_sec: true, + }, + esManagementFeature: {}, }); }); test(`doesn't re-enable disabled uiCapabilities`, async () => { const mockAuthz = createMockAuthz({ resolveCheckPrivileges: { - privileges: [ - { privilege: actions.ui.get('navLinks', 'foo'), authorized: true }, - { privilege: actions.ui.get('navLinks', 'bar'), authorized: true }, - { privilege: actions.ui.get('management', 'kibana', 'indices'), authorized: true }, - { privilege: actions.ui.get('fooFeature', 'foo'), authorized: true }, - { privilege: actions.ui.get('fooFeature', 'bar'), authorized: true }, - { privilege: actions.ui.get('barFeature', 'foo'), authorized: true }, - { privilege: actions.ui.get('barFeature', 'bar'), authorized: true }, - ], + privileges: { + kibana: [ + { privilege: actions.ui.get('navLinks', 'foo'), authorized: true }, + { privilege: actions.ui.get('navLinks', 'bar'), authorized: true }, + { privilege: actions.ui.get('management', 'kibana', 'indices'), authorized: true }, + { privilege: actions.ui.get('fooFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('fooFeature', 'bar'), authorized: true }, + { privilege: actions.ui.get('barFeature', 'foo'), authorized: true }, + { privilege: actions.ui.get('barFeature', 'bar'), authorized: true }, + ], + elasticsearch: { + cluster: [], + index: {}, + }, + }, }, }); const { usingPrivileges } = disableUICapabilitiesFactory( mockRequest, [ - new Feature({ + new KibanaFeature({ id: 'fooFeature', - name: 'Foo Feature', + name: 'Foo KibanaFeature', navLinkId: 'foo', app: [], privileges: null, }), - new Feature({ + new KibanaFeature({ id: 'barFeature', - name: 'Bar Feature', + name: 'Bar KibanaFeature', navLinkId: 'bar', app: [], privileges: null, }), ], + [ + new ElasticsearchFeature({ + id: 'esFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: [], + }, + ], + }), + ], loggingSystemMock.create().get(), - mockAuthz + mockAuthz, + createMockUser() ); const result = await usingPrivileges( @@ -409,16 +548,28 @@ describe('all', () => { const { all } = disableUICapabilitiesFactory( mockRequest, [ - new Feature({ + new KibanaFeature({ id: 'fooFeature', - name: 'Foo Feature', + name: 'Foo KibanaFeature', app: ['foo'], navLinkId: 'foo', privileges: null, }), ], + [ + new ElasticsearchFeature({ + id: 'esFeature', + privileges: [ + { + requiredClusterPrivileges: [], + ui: ['bar'], + }, + ], + }), + ], loggingSystemMock.create().get(), - mockAuthz + mockAuthz, + createMockUser() ); const result = all( @@ -441,6 +592,9 @@ describe('all', () => { foo: true, bar: true, }, + esFeature: { + bar: true, + }, }) ); expect(result).toEqual({ @@ -462,6 +616,9 @@ describe('all', () => { foo: false, bar: false, }, + esFeature: { + bar: false, + }, }); }); }); diff --git a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts index 41d596d570fb9..89cc9065655cd 100644 --- a/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts +++ b/x-pack/plugins/security/server/authorization/disable_ui_capabilities.ts @@ -5,18 +5,26 @@ */ import { flatten, isObject, mapValues } from 'lodash'; +import { RecursiveReadonly, RecursiveReadonlyArray } from '@kbn/utility-types'; import type { Capabilities as UICapabilities } from '../../../../../src/core/types'; import { KibanaRequest, Logger } from '../../../../../src/core/server'; -import { Feature } from '../../../features/server'; +import { + KibanaFeature, + ElasticsearchFeature, + FeatureElasticsearchPrivileges, +} from '../../../features/server'; -import { CheckPrivilegesResponse } from './check_privileges'; +import { CheckPrivilegesResponse } from './types'; import { AuthorizationServiceSetup } from '.'; +import { AuthenticatedUser } from '..'; export function disableUICapabilitiesFactory( request: KibanaRequest, - features: Feature[], + features: KibanaFeature[], + elasticsearchFeatures: ElasticsearchFeature[], logger: Logger, - authz: AuthorizationServiceSetup + authz: AuthorizationServiceSetup, + user: AuthenticatedUser | null ) { // nav links are sourced from the apps property. // The Kibana Platform associates nav links to the app which registers it, in a 1:1 relationship. @@ -25,6 +33,39 @@ export function disableUICapabilitiesFactory( .flatMap((feature) => feature.app) .filter((navLinkId) => navLinkId != null); + const elasticsearchFeatureMap = elasticsearchFeatures.reduce< + Record> + >((acc, esFeature) => { + return { + ...acc, + [esFeature.id]: esFeature.privileges, + }; + }, {}); + + const allRequiredClusterPrivileges = Array.from( + new Set( + Object.values(elasticsearchFeatureMap) + .flat() + .map((p) => p.requiredClusterPrivileges) + .flat() + ) + ); + + const allRequiredIndexPrivileges = Object.values(elasticsearchFeatureMap) + .flat() + .filter((p) => !!p.requiredIndexPrivileges) + .reduce>((acc, p) => { + return { + ...acc, + ...Object.entries(p.requiredIndexPrivileges!).reduce((acc2, [indexName, privileges]) => { + return { + ...acc2, + [indexName]: [...(acc[indexName] ?? []), ...privileges], + }; + }, {}), + }; + }, {}); + const shouldDisableFeatureUICapability = ( featureId: keyof UICapabilities, uiCapability: string @@ -59,6 +100,12 @@ export function disableUICapabilitiesFactory( uiCapability: string, value: boolean | Record ): string[] { + // Capabilities derived from Elasticsearch features should not be + // included here, as the result is used to check authorization against + // Kibana Privileges, rather than Elasticsearch Privileges. + if (elasticsearchFeatureMap.hasOwnProperty(featureId)) { + return []; + } if (typeof value === 'boolean') { return [authz.actions.ui.get(featureId, uiCapability)]; } @@ -85,7 +132,13 @@ export function disableUICapabilitiesFactory( let checkPrivilegesResponse: CheckPrivilegesResponse; try { const checkPrivilegesDynamically = authz.checkPrivilegesDynamicallyWithRequest(request); - checkPrivilegesResponse = await checkPrivilegesDynamically(uiActions); + checkPrivilegesResponse = await checkPrivilegesDynamically({ + kibana: uiActions, + elasticsearch: { + cluster: allRequiredClusterPrivileges, + index: allRequiredIndexPrivileges, + }, + }); } catch (err) { // if we get a 401/403, then we want to disable all uiCapabilities, as this // is generally when the user hasn't authenticated yet and we're displaying the @@ -110,9 +163,65 @@ export function disableUICapabilitiesFactory( } const action = authz.actions.ui.get(featureId, ...uiCapabilityParts); - return checkPrivilegesResponse.privileges.some( - (x) => x.privilege === action && x.authorized === true - ); + + const isElasticsearchFeature = elasticsearchFeatureMap.hasOwnProperty(featureId); + const isCatalogueFeature = featureId === 'catalogue'; + const isManagementFeature = featureId === 'management'; + + if (!isElasticsearchFeature) { + const hasRequiredKibanaPrivileges = checkPrivilegesResponse.privileges.kibana.some( + (x) => x.privilege === action && x.authorized === true + ); + + // Catalogue and management capbility buckets can also be influenced by ES privileges, + // so the early return is not possible for these. + if ((!isCatalogueFeature && !isManagementFeature) || hasRequiredKibanaPrivileges) { + return hasRequiredKibanaPrivileges; + } + } + + return elasticsearchFeatures.some((esFeature) => { + if (isCatalogueFeature) { + const [catalogueEntry] = uiCapabilityParts; + const featureGrantsCatalogueEntry = (esFeature.catalogue ?? []).includes(catalogueEntry); + return ( + featureGrantsCatalogueEntry && + hasAnyRequiredElasticsearchPrivilegesForFeature( + esFeature, + checkPrivilegesResponse, + user + ) + ); + } else if (isManagementFeature) { + const [managementSectionId, managementEntryId] = uiCapabilityParts; + const featureGrantsManagementEntry = + (esFeature.management ?? {}).hasOwnProperty(managementSectionId) && + esFeature.management![managementSectionId].includes(managementEntryId); + + return ( + featureGrantsManagementEntry && + hasAnyRequiredElasticsearchPrivilegesForFeature( + esFeature, + checkPrivilegesResponse, + user + ) + ); + } else if (esFeature.id === featureId) { + if (uiCapabilityParts.length !== 1) { + // The current privilege system does not allow for this to happen. + // This is a safeguard against future changes. + throw new Error( + `Elasticsearch feature ${esFeature.id} expected a single capability, but found ${uiCapabilityParts.length}` + ); + } + return hasRequiredElasticsearchPrivilegesForCapability( + esFeature, + uiCapabilityParts[0], + checkPrivilegesResponse, + user + ); + } + }); }; return mapValues(uiCapabilities, (featureUICapabilities, featureId) => { @@ -151,3 +260,56 @@ export function disableUICapabilitiesFactory( usingPrivileges, }; } + +function hasRequiredElasticsearchPrivilegesForCapability( + esFeature: ElasticsearchFeature, + uiCapability: string, + checkPrivilegesResponse: CheckPrivilegesResponse, + user: AuthenticatedUser | null +) { + return esFeature.privileges.some((privilege) => { + const privilegeGrantsCapability = privilege.ui.includes(uiCapability); + if (!privilegeGrantsCapability) { + return false; + } + + return isGrantedElasticsearchPrivilege(privilege, checkPrivilegesResponse, user); + }); +} + +function hasAnyRequiredElasticsearchPrivilegesForFeature( + esFeature: ElasticsearchFeature, + checkPrivilegesResponse: CheckPrivilegesResponse, + user: AuthenticatedUser | null +) { + return esFeature.privileges.some((privilege) => { + return isGrantedElasticsearchPrivilege(privilege, checkPrivilegesResponse, user); + }); +} + +function isGrantedElasticsearchPrivilege( + privilege: RecursiveReadonly, + checkPrivilegesResponse: CheckPrivilegesResponse, + user: AuthenticatedUser | null +) { + const hasRequiredClusterPrivileges = privilege.requiredClusterPrivileges.every( + (expectedClusterPriv) => + checkPrivilegesResponse.privileges.elasticsearch.cluster.some( + (x) => x.privilege === expectedClusterPriv && x.authorized === true + ) + ); + + const hasRequiredIndexPrivileges = Object.entries(privilege.requiredIndexPrivileges ?? {}).every( + ([indexName, requiredIndexPrivileges]) => { + return checkPrivilegesResponse.privileges.elasticsearch.index[indexName] + .filter((indexResponse) => requiredIndexPrivileges.includes(indexResponse.privilege)) + .every((indexResponse) => indexResponse.authorized); + } + ); + + const hasRequiredRoles = (privilege.requiredRoles ?? []).every( + (requiredRole) => user?.roles.includes(requiredRole) ?? false + ); + + return hasRequiredClusterPrivileges && hasRequiredIndexPrivileges && hasRequiredRoles; +} diff --git a/x-pack/plugins/security/server/authorization/index.mock.ts b/x-pack/plugins/security/server/authorization/index.mock.ts index 62b254d132d9e..6cb78a3001a9b 100644 --- a/x-pack/plugins/security/server/authorization/index.mock.ts +++ b/x-pack/plugins/security/server/authorization/index.mock.ts @@ -13,6 +13,7 @@ export const authorizationMock = { }: { version?: string; applicationName?: string } = {}) => ({ actions: actionsMock.create(version), checkPrivilegesWithRequest: jest.fn(), + checkElasticsearchPrivilegesWithRequest: jest.fn(), checkPrivilegesDynamicallyWithRequest: jest.fn(), checkSavedObjectsPrivilegesWithRequest: jest.fn(), applicationName, diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts index 5e9c1818cad2b..dc261e2eec982 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.test.ts @@ -6,7 +6,7 @@ import { Actions } from '../../actions'; import { FeaturePrivilegeAlertingBuilder } from './alerting'; -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; const version = '1.0.0-zeta1'; @@ -29,7 +29,7 @@ describe(`feature_privilege_builder`, () => { ui: [], }; - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'my-feature', name: 'my-feature', app: [], @@ -60,7 +60,7 @@ describe(`feature_privilege_builder`, () => { ui: [], }; - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'my-feature', name: 'my-feature', app: [], @@ -97,7 +97,7 @@ describe(`feature_privilege_builder`, () => { ui: [], }; - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'my-feature', name: 'my-feature', app: [], @@ -144,7 +144,7 @@ describe(`feature_privilege_builder`, () => { ui: [], }; - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'my-feature', name: 'my-feature', app: [], diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts index eb278a5755204..fa9cadf2aea62 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/alerting.ts @@ -5,7 +5,7 @@ */ import { uniq } from 'lodash'; -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; const readOperations: string[] = ['get', 'getAlertState', 'getAlertInstanceSummary', 'find']; @@ -24,7 +24,10 @@ const writeOperations: string[] = [ const allOperations: string[] = [...readOperations, ...writeOperations]; export class FeaturePrivilegeAlertingBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions( + privilegeDefinition: FeatureKibanaPrivileges, + feature: KibanaFeature + ): string[] { const getAlertingPrivilege = ( operations: string[], privilegedTypes: readonly string[], diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/api.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/api.ts index 6b7d94bb0127e..0e63cdceffc57 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/api.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/api.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeApiBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions(privilegeDefinition: FeatureKibanaPrivileges): string[] { if (privilegeDefinition.api) { return privilegeDefinition.api.map((operation) => this.actions.api.get(operation)); } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/app.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/app.ts index 213aa83f2d26e..bf6b0e60f1045 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/app.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/app.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeAppBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions(privilegeDefinition: FeatureKibanaPrivileges): string[] { const appIds = privilegeDefinition.app; if (!appIds) { diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/catalogue.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/catalogue.ts index f1ea7091b9481..97a3c9c1e336e 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/catalogue.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/catalogue.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeCatalogueBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions(privilegeDefinition: FeatureKibanaPrivileges): string[] { const catalogueEntries = privilegeDefinition.catalogue; if (!catalogueEntries) { diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts index 172ab24eb7e51..0eded66d65b06 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/feature_privilege_builder.ts @@ -4,17 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { Actions } from '../../actions'; export interface FeaturePrivilegeBuilder { - getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[]; + getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: KibanaFeature): string[]; } export abstract class BaseFeaturePrivilegeBuilder implements FeaturePrivilegeBuilder { constructor(protected readonly actions: Actions) {} public abstract getActions( privilegeDefinition: FeatureKibanaPrivileges, - feature: Feature + feature: KibanaFeature ): string[]; } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts index 76b664cbbe2a7..998fbc5cc5e24 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/index.ts @@ -5,7 +5,7 @@ */ import { flatten } from 'lodash'; -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { Actions } from '../../actions'; import { FeaturePrivilegeApiBuilder } from './api'; import { FeaturePrivilegeAppBuilder } from './app'; @@ -31,7 +31,7 @@ export const featurePrivilegeBuilderFactory = (actions: Actions): FeaturePrivile ]; return { - getActions(privilege: FeatureKibanaPrivileges, feature: Feature) { + getActions(privilege: FeatureKibanaPrivileges, feature: KibanaFeature) { return flatten(builders.map((builder) => builder.getActions(privilege, feature))); }, }; diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/management.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/management.ts index be784949dc2fa..67b8cdb7616d4 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/management.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/management.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeManagementBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions(privilegeDefinition: FeatureKibanaPrivileges): string[] { const managementSections = privilegeDefinition.management; if (!managementSections) { diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts index a6e5a01c7dba8..7400675ed17f3 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/navlink.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeNavlinkBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions(privilegeDefinition: FeatureKibanaPrivileges): string[] { return (privilegeDefinition.app ?? []).map((app) => this.actions.ui.get('navLinks', app)); } } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts index 2c325fc8c6cb7..0dd89f2c5f3c1 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/saved_object.ts @@ -5,7 +5,7 @@ */ import { flatten, uniq } from 'lodash'; -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; const readOperations: string[] = ['bulk_get', 'get', 'find']; @@ -13,7 +13,7 @@ const writeOperations: string[] = ['create', 'bulk_create', 'update', 'bulk_upda const allOperations: string[] = [...readOperations, ...writeOperations]; export class FeaturePrivilegeSavedObjectBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions(privilegeDefinition: FeatureKibanaPrivileges): string[] { return uniq([ ...flatten( privilegeDefinition.savedObject.all.map((type) => [ diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/ui.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/ui.ts index 31bc351206e54..dd167a291f11d 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/ui.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_builder/ui.ts @@ -4,11 +4,14 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { BaseFeaturePrivilegeBuilder } from './feature_privilege_builder'; export class FeaturePrivilegeUIBuilder extends BaseFeaturePrivilegeBuilder { - public getActions(privilegeDefinition: FeatureKibanaPrivileges, feature: Feature): string[] { + public getActions( + privilegeDefinition: FeatureKibanaPrivileges, + feature: KibanaFeature + ): string[] { return privilegeDefinition.ui.map((ui) => this.actions.ui.get(feature.id, ui)); } } diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts index bb1f0c33fdee9..033040fd2f14b 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.test.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../../../features/server'; +import { KibanaFeature } from '../../../../../features/server'; import { featurePrivilegeIterator } from './feature_privilege_iterator'; describe('featurePrivilegeIterator', () => { it('handles features with no privileges', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', privileges: null, @@ -26,7 +26,7 @@ describe('featurePrivilegeIterator', () => { }); it('handles features with no sub-features', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', privileges: { @@ -117,7 +117,7 @@ describe('featurePrivilegeIterator', () => { }); it('filters privileges using the provided predicate', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', privileges: { @@ -190,7 +190,7 @@ describe('featurePrivilegeIterator', () => { }); it('ignores sub features when `augmentWithSubFeaturePrivileges` is false', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -313,7 +313,7 @@ describe('featurePrivilegeIterator', () => { }); it('ignores sub features when `includeIn` is none, even if `augmentWithSubFeaturePrivileges` is true', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -436,7 +436,7 @@ describe('featurePrivilegeIterator', () => { }); it('includes sub feature privileges into both all and read when`augmentWithSubFeaturePrivileges` is true and `includeIn: read`', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -563,7 +563,7 @@ describe('featurePrivilegeIterator', () => { }); it('does not duplicate privileges when merging', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -686,7 +686,7 @@ describe('featurePrivilegeIterator', () => { }); it('includes sub feature privileges into both all and read when`augmentWithSubFeaturePrivileges` is true and `includeIn: all`', () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -811,7 +811,7 @@ describe('featurePrivilegeIterator', () => { }); it(`can augment primary feature privileges even if they don't specify their own`, () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -919,7 +919,7 @@ describe('featurePrivilegeIterator', () => { }); it(`can augment primary feature privileges even if the sub-feature privileges don't specify their own`, () => { - const feature = new Feature({ + const feature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts index 17c9464b14756..dba33f7a4f360 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/feature_privilege_iterator.ts @@ -5,7 +5,7 @@ */ import _ from 'lodash'; -import { Feature, FeatureKibanaPrivileges } from '../../../../../features/server'; +import { KibanaFeature, FeatureKibanaPrivileges } from '../../../../../features/server'; import { subFeaturePrivilegeIterator } from './sub_feature_privilege_iterator'; interface IteratorOptions { @@ -14,7 +14,7 @@ interface IteratorOptions { } export function* featurePrivilegeIterator( - feature: Feature, + feature: KibanaFeature, options: IteratorOptions ): IterableIterator<{ privilegeId: string; privilege: FeatureKibanaPrivileges }> { for (const entry of Object.entries(feature.privileges ?? {})) { @@ -35,7 +35,7 @@ export function* featurePrivilegeIterator( function mergeWithSubFeatures( privilegeId: string, privilege: FeatureKibanaPrivileges, - feature: Feature + feature: KibanaFeature ) { const mergedConfig = _.cloneDeep(privilege); for (const subFeaturePrivilege of subFeaturePrivilegeIterator(feature)) { diff --git a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts index b288262be25c6..d54b6d458d913 100644 --- a/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts +++ b/x-pack/plugins/security/server/authorization/privileges/feature_privilege_iterator/sub_feature_privilege_iterator.ts @@ -4,11 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SubFeaturePrivilegeConfig } from '../../../../../features/common'; -import { Feature } from '../../../../../features/server'; +import { KibanaFeature, SubFeaturePrivilegeConfig } from '../../../../../features/common'; export function* subFeaturePrivilegeIterator( - feature: Feature + feature: KibanaFeature ): IterableIterator { for (const subFeature of feature.subFeatures) { for (const group of subFeature.privilegeGroups) { diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts index 89ac73c220756..dd8ac44386dbd 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../../features/server'; +import { KibanaFeature } from '../../../../features/server'; import { Actions } from '../actions'; import { privilegesFactory } from './privileges'; @@ -14,10 +14,10 @@ const actions = new Actions('1.0.0-zeta1'); describe('features', () => { test('actions defined at the feature do not cascade to the privileges', () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo-feature', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', navLinkId: 'kibana:foo', app: ['app-1', 'app-2'], @@ -45,7 +45,7 @@ describe('features', () => { ]; const mockFeaturesService = featuresPluginMock.createSetup(); - mockFeaturesService.getFeatures.mockReturnValue(features); + mockFeaturesService.getKibanaFeatures.mockReturnValue(features); const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), @@ -60,10 +60,10 @@ describe('features', () => { }); test(`actions only specified at the privilege are alright too`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: { @@ -85,13 +85,13 @@ describe('features', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const expectedAllPrivileges = [ actions.login, @@ -159,23 +159,23 @@ describe('features', () => { }); test(`features with no privileges aren't listed`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: null, }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).not.toHaveProperty('features.foo'); @@ -200,10 +200,10 @@ describe('features', () => { ].forEach(({ group, expectManageSpaces, expectGetFeatures, expectEnterpriseSearch }) => { describe(`${group}`, () => { test('actions defined in any feature privilege are included in `all`', () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', navLinkId: 'kibana:foo', app: [], @@ -238,13 +238,13 @@ describe('features', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).toHaveProperty(`${group}.all`, [ @@ -256,6 +256,7 @@ describe('features', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), ] : []), ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), @@ -319,10 +320,10 @@ describe('features', () => { }); test('actions defined in a feature privilege with name `read` are included in `read`', () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', navLinkId: 'kibana:foo', app: [], @@ -357,13 +358,13 @@ describe('features', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).toHaveProperty(`${group}.read`, [ @@ -401,10 +402,10 @@ describe('features', () => { }); test('actions defined in a reserved privilege are not included in `all` or `read`', () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', navLinkId: 'kibana:foo', app: [], @@ -431,13 +432,13 @@ describe('features', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).toHaveProperty(`${group}.all`, [ @@ -449,6 +450,7 @@ describe('features', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), ] : []), ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), @@ -457,10 +459,10 @@ describe('features', () => { }); test('actions defined in a feature with excludeFromBasePrivileges are not included in `all` or `read', () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', excludeFromBasePrivileges: true, icon: 'arrowDown', navLinkId: 'kibana:foo', @@ -496,13 +498,13 @@ describe('features', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).toHaveProperty(`${group}.all`, [ @@ -514,6 +516,7 @@ describe('features', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), ] : []), ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), @@ -522,10 +525,10 @@ describe('features', () => { }); test('actions defined in an individual feature privilege with excludeFromBasePrivileges are not included in `all` or `read`', () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', navLinkId: 'kibana:foo', app: [], @@ -562,13 +565,13 @@ describe('features', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).toHaveProperty(`${group}.all`, [ @@ -580,6 +583,7 @@ describe('features', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), ] : []), ...(expectEnterpriseSearch ? [actions.ui.get('enterpriseSearch', 'all')] : []), @@ -591,10 +595,10 @@ describe('features', () => { describe('reserved', () => { test('actions defined at the feature do not cascade to the privileges', () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', navLinkId: 'kibana:foo', app: ['app-1', 'app-2'], @@ -621,23 +625,23 @@ describe('reserved', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).toHaveProperty('reserved.foo', [actions.version]); }); test(`actions only specified at the privilege are alright too`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: null, @@ -659,13 +663,13 @@ describe('reserved', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).toHaveProperty('reserved.foo', [ @@ -698,10 +702,10 @@ describe('reserved', () => { }); test(`features with no reservedPrivileges aren't listed`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: { @@ -723,13 +727,13 @@ describe('reserved', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual).not.toHaveProperty('reserved.foo'); @@ -739,10 +743,10 @@ describe('reserved', () => { describe('subFeatures', () => { describe(`with includeIn: 'none'`, () => { test(`should not augment the primary feature privileges, base privileges, or minimal feature privileges`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: { @@ -786,13 +790,13 @@ describe('subFeatures', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`, [ @@ -841,6 +845,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), actions.ui.get('foo', 'foo'), ]); @@ -865,10 +870,10 @@ describe('subFeatures', () => { describe(`with includeIn: 'read'`, () => { test(`should augment the primary feature privileges and base privileges, but never the minimal versions`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: { @@ -912,13 +917,13 @@ describe('subFeatures', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`, [ @@ -993,6 +998,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), actions.savedObject.get('all-sub-feature-type', 'bulk_get'), actions.savedObject.get('all-sub-feature-type', 'get'), @@ -1063,10 +1069,10 @@ describe('subFeatures', () => { }); test(`should augment the primary feature privileges, but not base privileges if feature is excluded from them.`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], excludeFromBasePrivileges: true, @@ -1111,13 +1117,13 @@ describe('subFeatures', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`, [ @@ -1192,6 +1198,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), ]); expect(actual).toHaveProperty('global.read', [actions.login, actions.version]); @@ -1203,10 +1210,10 @@ describe('subFeatures', () => { describe(`with includeIn: 'all'`, () => { test(`should augment the primary 'all' feature privileges and base 'all' privileges, but never the minimal versions`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: { @@ -1250,13 +1257,13 @@ describe('subFeatures', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`, [ @@ -1319,6 +1326,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), actions.savedObject.get('all-sub-feature-type', 'bulk_get'), actions.savedObject.get('all-sub-feature-type', 'get'), @@ -1365,10 +1373,10 @@ describe('subFeatures', () => { }); test(`should augment the primary 'all' feature privileges, but not the base privileges if the feature is excluded from them`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], excludeFromBasePrivileges: true, @@ -1413,13 +1421,13 @@ describe('subFeatures', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: true }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual.features).toHaveProperty(`foo.subFeaturePriv1`, [ @@ -1482,6 +1490,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), ]); expect(actual).toHaveProperty('global.read', [actions.login, actions.version]); @@ -1493,10 +1502,10 @@ describe('subFeatures', () => { describe(`when license does not allow sub features`, () => { test(`should augment the primary feature privileges, and should not create minimal or sub-feature privileges`, () => { - const features: Feature[] = [ - new Feature({ + const features: KibanaFeature[] = [ + new KibanaFeature({ id: 'foo', - name: 'Foo Feature', + name: 'Foo KibanaFeature', icon: 'arrowDown', app: [], privileges: { @@ -1540,13 +1549,13 @@ describe('subFeatures', () => { }), ]; - const mockXPackMainPlugin = { - getFeatures: jest.fn().mockReturnValue(features), + const mockFeaturesPlugin = { + getKibanaFeatures: jest.fn().mockReturnValue(features), }; const mockLicenseService = { getFeatures: jest.fn().mockReturnValue({ allowSubFeaturePrivileges: false }), }; - const privileges = privilegesFactory(actions, mockXPackMainPlugin as any, mockLicenseService); + const privileges = privilegesFactory(actions, mockFeaturesPlugin as any, mockLicenseService); const actual = privileges.get(); expect(actual.features).not.toHaveProperty(`foo.subFeaturePriv1`); @@ -1598,6 +1607,7 @@ describe('subFeatures', () => { actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), actions.savedObject.get('all-sub-feature-type', 'bulk_get'), actions.savedObject.get('all-sub-feature-type', 'get'), diff --git a/x-pack/plugins/security/server/authorization/privileges/privileges.ts b/x-pack/plugins/security/server/authorization/privileges/privileges.ts index 5d8ef3f376cac..24b46222e7f35 100644 --- a/x-pack/plugins/security/server/authorization/privileges/privileges.ts +++ b/x-pack/plugins/security/server/authorization/privileges/privileges.ts @@ -6,7 +6,10 @@ import { uniq } from 'lodash'; import { SecurityLicense } from '../../../common/licensing'; -import { Feature, PluginSetupContract as FeaturesPluginSetup } from '../../../../features/server'; +import { + KibanaFeature, + PluginSetupContract as FeaturesPluginSetup, +} from '../../../../features/server'; import { RawKibanaPrivileges } from '../../../common/model'; import { Actions } from '../actions'; import { featurePrivilegeBuilderFactory } from './feature_privilege_builder'; @@ -28,7 +31,7 @@ export function privilegesFactory( return { get() { - const features = featuresService.getFeatures(); + const features = featuresService.getKibanaFeatures(); const { allowSubFeaturePrivileges } = licenseService.getFeatures(); const basePrivilegeFeatures = features.filter( (feature) => !feature.excludeFromBasePrivileges @@ -100,6 +103,7 @@ export function privilegesFactory( actions.space.manage, actions.ui.get('spaces', 'manage'), actions.ui.get('management', 'kibana', 'spaces'), + actions.ui.get('catalogue', 'spaces'), actions.ui.get('enterpriseSearch', 'all'), ...allActions, ], @@ -109,7 +113,7 @@ export function privilegesFactory( all: [actions.login, actions.version, ...allActions], read: [actions.login, actions.version, ...readActions], }, - reserved: features.reduce((acc: Record, feature: Feature) => { + reserved: features.reduce((acc: Record, feature: KibanaFeature) => { if (feature.reserved) { feature.reserved.privileges.forEach((reservedPrivilege) => { acc[reservedPrivilege.id] = [ diff --git a/x-pack/plugins/security/server/authorization/types.ts b/x-pack/plugins/security/server/authorization/types.ts index 75188d1191b1a..bedf46862e4f5 100644 --- a/x-pack/plugins/security/server/authorization/types.ts +++ b/x-pack/plugins/security/server/authorization/types.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { KibanaRequest } from 'src/core/server'; + export interface HasPrivilegesResponseApplication { [resource: string]: { [privilegeName: string]: boolean; @@ -16,4 +18,58 @@ export interface HasPrivilegesResponse { application: { [applicationName: string]: HasPrivilegesResponseApplication; }; + cluster?: { + [privilegeName: string]: boolean; + }; + index?: { + [indexName: string]: { + [privilegeName: string]: boolean; + }; + }; +} + +export interface CheckPrivilegesResponse { + hasAllRequested: boolean; + username: string; + privileges: { + kibana: Array<{ + /** + * If this attribute is undefined, this element is a privilege for the global resource. + */ + resource?: string; + privilege: string; + authorized: boolean; + }>; + elasticsearch: { + cluster: Array<{ + privilege: string; + authorized: boolean; + }>; + index: { + [indexName: string]: Array<{ + privilege: string; + authorized: boolean; + }>; + }; + }; + }; +} + +export type CheckPrivilegesWithRequest = (request: KibanaRequest) => CheckPrivileges; + +export interface CheckPrivileges { + atSpace(spaceId: string, privileges: CheckPrivilegesPayload): Promise; + atSpaces( + spaceIds: string[], + privileges: CheckPrivilegesPayload + ): Promise; + globally(privileges: CheckPrivilegesPayload): Promise; +} + +export interface CheckPrivilegesPayload { + kibana?: string | string[]; + elasticsearch?: { + cluster: string[]; + index: Record; + }; } diff --git a/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts b/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts index cd2c7faa263c9..8e6d72670c8d9 100644 --- a/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/validate_feature_privileges.test.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../features/server'; +import { KibanaFeature } from '../../../features/server'; import { validateFeaturePrivileges } from './validate_feature_privileges'; it('allows features to be defined without privileges', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -19,7 +19,7 @@ it('allows features to be defined without privileges', () => { }); it('allows features with reserved privileges to be defined', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -45,7 +45,7 @@ it('allows features with reserved privileges to be defined', () => { }); it('allows features with sub-features to be defined', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -108,7 +108,7 @@ it('allows features with sub-features to be defined', () => { }); it('does not allow features with sub-features which have id conflicts with the minimal privileges', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -153,12 +153,12 @@ it('does not allow features with sub-features which have id conflicts with the m }); expect(() => validateFeaturePrivileges([feature])).toThrowErrorMatchingInlineSnapshot( - `"Feature 'foo' already has a privilege with ID 'minimal_all'. Sub feature 'sub-feature-1' cannot also specify this."` + `"KibanaFeature 'foo' already has a privilege with ID 'minimal_all'. Sub feature 'sub-feature-1' cannot also specify this."` ); }); it('does not allow features with sub-features which have id conflicts with the primary feature privileges', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -203,12 +203,12 @@ it('does not allow features with sub-features which have id conflicts with the p }); expect(() => validateFeaturePrivileges([feature])).toThrowErrorMatchingInlineSnapshot( - `"Feature 'foo' already has a privilege with ID 'read'. Sub feature 'sub-feature-1' cannot also specify this."` + `"KibanaFeature 'foo' already has a privilege with ID 'read'. Sub feature 'sub-feature-1' cannot also specify this."` ); }); it('does not allow features with sub-features which have id conflicts each other', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -273,6 +273,6 @@ it('does not allow features with sub-features which have id conflicts each other }); expect(() => validateFeaturePrivileges([feature])).toThrowErrorMatchingInlineSnapshot( - `"Feature 'foo' already has a privilege with ID 'some-sub-feature'. Sub feature 'sub-feature-2' cannot also specify this."` + `"KibanaFeature 'foo' already has a privilege with ID 'some-sub-feature'. Sub feature 'sub-feature-2' cannot also specify this."` ); }); diff --git a/x-pack/plugins/security/server/authorization/validate_feature_privileges.ts b/x-pack/plugins/security/server/authorization/validate_feature_privileges.ts index 79e5348b4ac64..eeb9c4cb74314 100644 --- a/x-pack/plugins/security/server/authorization/validate_feature_privileges.ts +++ b/x-pack/plugins/security/server/authorization/validate_feature_privileges.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../features/server'; +import { KibanaFeature } from '../../../features/server'; -export function validateFeaturePrivileges(features: Feature[]) { +export function validateFeaturePrivileges(features: KibanaFeature[]) { for (const feature of features) { const seenPrivilegeIds = new Set(); Object.keys(feature.privileges ?? {}).forEach((privilegeId) => { @@ -20,7 +20,7 @@ export function validateFeaturePrivileges(features: Feature[]) { subFeaturePrivilegeGroup.privileges.forEach((subFeaturePrivilege) => { if (seenPrivilegeIds.has(subFeaturePrivilege.id)) { throw new Error( - `Feature '${feature.id}' already has a privilege with ID '${subFeaturePrivilege.id}'. Sub feature '${subFeature.name}' cannot also specify this.` + `KibanaFeature '${feature.id}' already has a privilege with ID '${subFeaturePrivilege.id}'. Sub feature '${subFeature.name}' cannot also specify this.` ); } seenPrivilegeIds.add(subFeaturePrivilege.id); diff --git a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts index 26af0dadfb288..d91a4d4151316 100644 --- a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts +++ b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.test.ts @@ -4,11 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../features/server'; +import { KibanaFeature } from '../../../features/server'; import { validateReservedPrivileges } from './validate_reserved_privileges'; it('allows features to be defined without privileges', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -19,7 +19,7 @@ it('allows features to be defined without privileges', () => { }); it('allows features with a single reserved privilege to be defined', () => { - const feature: Feature = new Feature({ + const feature: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -45,7 +45,7 @@ it('allows features with a single reserved privilege to be defined', () => { }); it('allows multiple features with reserved privileges to be defined', () => { - const feature1: Feature = new Feature({ + const feature1: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -67,7 +67,7 @@ it('allows multiple features with reserved privileges to be defined', () => { }, }); - const feature2: Feature = new Feature({ + const feature2: KibanaFeature = new KibanaFeature({ id: 'foo2', name: 'foo', app: [], @@ -93,7 +93,7 @@ it('allows multiple features with reserved privileges to be defined', () => { }); it('prevents a feature from specifying the same reserved privilege id', () => { - const feature1: Feature = new Feature({ + const feature1: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -131,7 +131,7 @@ it('prevents a feature from specifying the same reserved privilege id', () => { }); it('prevents features from sharing a reserved privilege id', () => { - const feature1: Feature = new Feature({ + const feature1: KibanaFeature = new KibanaFeature({ id: 'foo', name: 'foo', app: [], @@ -153,7 +153,7 @@ it('prevents features from sharing a reserved privilege id', () => { }, }); - const feature2: Feature = new Feature({ + const feature2: KibanaFeature = new KibanaFeature({ id: 'foo2', name: 'foo', app: [], diff --git a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.ts b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.ts index 0915308fc0f89..23e5c28a4af1b 100644 --- a/x-pack/plugins/security/server/authorization/validate_reserved_privileges.ts +++ b/x-pack/plugins/security/server/authorization/validate_reserved_privileges.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../features/server'; +import { KibanaFeature } from '../../../features/server'; -export function validateReservedPrivileges(features: Feature[]) { +export function validateReservedPrivileges(features: KibanaFeature[]) { const seenPrivilegeIds = new Set(); for (const feature of features) { diff --git a/x-pack/plugins/security/server/config.test.ts b/x-pack/plugins/security/server/config.test.ts index 520081ae30d8d..093a7643fbf64 100644 --- a/x-pack/plugins/security/server/config.test.ts +++ b/x-pack/plugins/security/server/config.test.ts @@ -904,11 +904,13 @@ describe('createConfig()', () => { }, "sortedProviders": Array [ Object { + "hasAccessAgreement": false, "name": "saml", "order": 0, "type": "saml", }, Object { + "hasAccessAgreement": false, "name": "basic", "order": 1, "type": "basic", @@ -982,6 +984,63 @@ describe('createConfig()', () => { ).toBe(true); }); + it('indicates which providers have the access agreement enabled', () => { + expect( + createConfig( + ConfigSchema.validate({ + authc: { + providers: { + basic: { basic1: { order: 3 } }, + saml: { + saml1: { order: 2, realm: 'saml1', accessAgreement: { message: 'foo' } }, + saml2: { order: 1, realm: 'saml2' }, + }, + oidc: { + oidc1: { order: 0, realm: 'oidc1', accessAgreement: { message: 'foo' } }, + oidc2: { order: 4, realm: 'oidc2' }, + }, + }, + }, + }), + loggingSystemMock.create().get(), + { isTLSEnabled: true } + ).authc.sortedProviders + ).toMatchInlineSnapshot(` + Array [ + Object { + "hasAccessAgreement": true, + "name": "oidc1", + "order": 0, + "type": "oidc", + }, + Object { + "hasAccessAgreement": false, + "name": "saml2", + "order": 1, + "type": "saml", + }, + Object { + "hasAccessAgreement": true, + "name": "saml1", + "order": 2, + "type": "saml", + }, + Object { + "hasAccessAgreement": false, + "name": "basic1", + "order": 3, + "type": "basic", + }, + Object { + "hasAccessAgreement": false, + "name": "oidc2", + "order": 4, + "type": "oidc", + }, + ] + `); + }); + it('correctly sorts providers based on the `order`', () => { expect( createConfig( @@ -1000,26 +1059,31 @@ describe('createConfig()', () => { ).toMatchInlineSnapshot(` Array [ Object { + "hasAccessAgreement": false, "name": "oidc1", "order": 0, "type": "oidc", }, Object { + "hasAccessAgreement": false, "name": "saml2", "order": 1, "type": "saml", }, Object { + "hasAccessAgreement": false, "name": "saml1", "order": 2, "type": "saml", }, Object { + "hasAccessAgreement": false, "name": "basic1", "order": 3, "type": "basic", }, Object { + "hasAccessAgreement": false, "name": "oidc2", "order": 4, "type": "oidc", diff --git a/x-pack/plugins/security/server/config.ts b/x-pack/plugins/security/server/config.ts index dcfe4825fb035..9ccbdac5e09f4 100644 --- a/x-pack/plugins/security/server/config.ts +++ b/x-pack/plugins/security/server/config.ts @@ -255,13 +255,19 @@ export function createConfig( type: keyof ProvidersConfigType; name: string; order: number; + hasAccessAgreement: boolean; }> = []; for (const [type, providerGroup] of Object.entries(providers)) { - for (const [name, { enabled, order }] of Object.entries(providerGroup ?? {})) { + for (const [name, { enabled, order, accessAgreement }] of Object.entries(providerGroup ?? {})) { if (!enabled) { delete providerGroup![name]; } else { - sortedProviders.push({ type: type as any, name, order }); + sortedProviders.push({ + type: type as any, + name, + order, + hasAccessAgreement: !!accessAgreement?.message, + }); } } } diff --git a/x-pack/plugins/security/server/features/index.ts b/x-pack/plugins/security/server/features/index.ts new file mode 100644 index 0000000000000..3fe097c2bec12 --- /dev/null +++ b/x-pack/plugins/security/server/features/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { securityFeatures } from './security_features'; diff --git a/x-pack/plugins/security/server/features/security_features.ts b/x-pack/plugins/security/server/features/security_features.ts new file mode 100644 index 0000000000000..d80314c077aa2 --- /dev/null +++ b/x-pack/plugins/security/server/features/security_features.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ElasticsearchFeatureConfig } from '../../../features/server'; + +const userManagementFeature: ElasticsearchFeatureConfig = { + id: 'users', + management: { + security: ['users'], + }, + catalogue: ['security'], + privileges: [ + { + requiredClusterPrivileges: ['manage_security'], + ui: [], + }, + ], +}; + +const rolesManagementFeature: ElasticsearchFeatureConfig = { + id: 'roles', + management: { + security: ['roles'], + }, + catalogue: ['security'], + privileges: [ + { + requiredClusterPrivileges: ['manage_security'], + ui: [], + }, + ], +}; + +const apiKeysManagementFeature: ElasticsearchFeatureConfig = { + id: 'api_keys', + management: { + security: ['api_keys'], + }, + catalogue: ['security'], + privileges: [ + { + requiredClusterPrivileges: ['manage_api_key'], + ui: [], + }, + { + requiredClusterPrivileges: ['manage_own_api_key'], + ui: [], + }, + ], +}; + +const roleMappingsManagementFeature: ElasticsearchFeatureConfig = { + id: 'role_mappings', + management: { + security: ['role_mappings'], + }, + catalogue: ['security'], + privileges: [ + { + requiredClusterPrivileges: ['manage_security'], + ui: [], + }, + ], +}; + +export const securityFeatures = [ + userManagementFeature, + rolesManagementFeature, + apiKeysManagementFeature, + roleMappingsManagementFeature, +]; diff --git a/x-pack/plugins/security/server/plugin.test.ts b/x-pack/plugins/security/server/plugin.test.ts index 8d13f81075714..9088d4f08d0ef 100644 --- a/x-pack/plugins/security/server/plugin.test.ts +++ b/x-pack/plugins/security/server/plugin.test.ts @@ -11,6 +11,7 @@ import { ConfigSchema } from './config'; import { Plugin, PluginSetupDependencies } from './plugin'; import { coreMock, elasticsearchServiceMock } from '../../../../src/core/server/mocks'; +import { featuresPluginMock } from '../../features/server/mocks'; import { taskManagerMock } from '../../task_manager/server/mocks'; describe('Security Plugin', () => { @@ -44,6 +45,7 @@ describe('Security Plugin', () => { mockDependencies = ({ licensing: { license$: of({}), featureUsage: { register: jest.fn() } }, + features: featuresPluginMock.createSetup(), taskManager: taskManagerMock.createSetup(), } as unknown) as PluginSetupDependencies; }); @@ -108,6 +110,7 @@ describe('Security Plugin', () => { }, "getFeatures": [Function], "isEnabled": [Function], + "isLicenseAvailable": [Function], }, "registerSpacesService": [Function], } diff --git a/x-pack/plugins/security/server/plugin.ts b/x-pack/plugins/security/server/plugin.ts index 7d94e03916fa1..dc9139473004b 100644 --- a/x-pack/plugins/security/server/plugin.ts +++ b/x-pack/plugins/security/server/plugin.ts @@ -7,6 +7,7 @@ import { combineLatest } from 'rxjs'; import { first, map } from 'rxjs/operators'; import { TypeOf } from '@kbn/config-schema'; +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { deepFreeze, CoreSetup, @@ -15,6 +16,7 @@ import { PluginInitializerContext, } from '../../../../src/core/server'; import { SpacesPluginSetup } from '../../spaces/server'; +import { PluginSetupContract as FeaturesSetupContract } from '../../features/server'; import { PluginSetupContract as FeaturesPluginSetup, PluginStartContract as FeaturesPluginStart, @@ -30,14 +32,21 @@ import { SecurityLicenseService, SecurityLicense } from '../common/licensing'; import { setupSavedObjects } from './saved_objects'; import { AuditService, SecurityAuditLogger, AuditServiceSetup } from './audit'; import { SecurityFeatureUsageService, SecurityFeatureUsageServiceStart } from './feature_usage'; +import { securityFeatures } from './features'; import { ElasticsearchService } from './elasticsearch'; import { SessionManagementService } from './session_management'; +import { registerSecurityUsageCollector } from './usage_collector'; export type SpacesService = Pick< SpacesPluginSetup['spacesService'], 'getSpaceId' | 'namespaceToSpaceId' >; +export type FeaturesService = Pick< + FeaturesSetupContract, + 'getKibanaFeatures' | 'getElasticsearchFeatures' +>; + /** * Describes public Security plugin contract returned at the `setup` stage. */ @@ -74,6 +83,7 @@ export interface PluginSetupDependencies { features: FeaturesPluginSetup; licensing: LicensingPluginSetup; taskManager: TaskManagerSetupContract; + usageCollection?: UsageCollectionSetup; } export interface PluginStartDependencies { @@ -123,7 +133,7 @@ export class Plugin { public async setup( core: CoreSetup, - { features, licensing, taskManager }: PluginSetupDependencies + { features, licensing, taskManager, usageCollection }: PluginSetupDependencies ) { const [config, legacyConfig] = await combineLatest([ this.initializerContext.config.create>().pipe( @@ -143,6 +153,10 @@ export class Plugin { license$: licensing.license$, }); + securityFeatures.forEach((securityFeature) => + features.registerElasticsearchFeature(securityFeature) + ); + const { clusterClient } = this.elasticsearchService.setup({ elasticsearch: core.elasticsearch, license, @@ -151,6 +165,8 @@ export class Plugin { this.featureUsageService.setup({ featureUsage: licensing.featureUsage }); + registerSecurityUsageCollector({ usageCollection, config, license }); + const audit = this.auditService.setup({ license, config: config.audit }); const auditLogger = new SecurityAuditLogger(audit.getLogger()); @@ -183,6 +199,7 @@ export class Plugin { packageVersion: this.initializerContext.env.packageInfo.version, getSpacesService: this.getSpacesService, features, + getCurrentUser: authc.getCurrentUser, }); setupSavedObjects({ @@ -206,7 +223,7 @@ export class Plugin { getFeatures: () => core .getStartServices() - .then(([, { features: featuresStart }]) => featuresStart.getFeatures()), + .then(([, { features: featuresStart }]) => featuresStart.getKibanaFeatures()), getFeatureUsageService: this.getFeatureUsageService, }); diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts index 8f115f11329d3..6e9b88f30479f 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.test.ts @@ -15,7 +15,7 @@ import { httpServerMock, } from '../../../../../../../src/core/server/mocks'; import { routeDefinitionParamsMock } from '../../index.mock'; -import { Feature } from '../../../../../features/server'; +import { KibanaFeature } from '../../../../../features/server'; import { securityFeatureUsageServiceMock } from '../../../feature_usage/index.mock'; const application = 'kibana-.kibana'; @@ -83,7 +83,7 @@ const putRoleTest = ( ); mockRouteDefinitionParams.getFeatures.mockResolvedValue([ - new Feature({ + new KibanaFeature({ id: 'feature_1', name: 'feature 1', app: [], diff --git a/x-pack/plugins/security/server/routes/authorization/roles/put.ts b/x-pack/plugins/security/server/routes/authorization/roles/put.ts index d83cf92bcaa0d..cdedc9ac8a5eb 100644 --- a/x-pack/plugins/security/server/routes/authorization/roles/put.ts +++ b/x-pack/plugins/security/server/routes/authorization/roles/put.ts @@ -5,7 +5,7 @@ */ import { schema, TypeOf } from '@kbn/config-schema'; -import { Feature } from '../../../../../features/common'; +import { KibanaFeature } from '../../../../../features/common'; import { RouteDefinitionParams } from '../../index'; import { createLicensedRouteHandler } from '../../licensed_route_handler'; import { wrapIntoCustomErrorResponse } from '../../../errors'; @@ -16,7 +16,7 @@ import { } from './model'; const roleGrantsSubFeaturePrivileges = ( - features: Feature[], + features: KibanaFeature[], role: TypeOf> ) => { if (!role.kibana) { @@ -77,7 +77,7 @@ export function definePutRolesRoutes({ rawRoles[name] ? rawRoles[name].applications : [] ); - const [features] = await Promise.all([ + const [features] = await Promise.all([ getFeatures(), clusterClient .asScoped(request) diff --git a/x-pack/plugins/security/server/routes/index.ts b/x-pack/plugins/security/server/routes/index.ts index a3f046ae4f9e6..7880e95240ff0 100644 --- a/x-pack/plugins/security/server/routes/index.ts +++ b/x-pack/plugins/security/server/routes/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../features/server'; +import { KibanaFeature } from '../../../features/server'; import { HttpResources, IBasePath, @@ -42,7 +42,7 @@ export interface RouteDefinitionParams { authz: AuthorizationServiceSetup; session: PublicMethodsOf; license: SecurityLicense; - getFeatures: () => Promise; + getFeatures: () => Promise; getFeatureUsageService: () => SecurityFeatureUsageServiceStart; } diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts index 7f7f969e8b480..7ada34ff5ccac 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.test.ts @@ -117,7 +117,11 @@ const expectSuccess = async (fn: Function, args: Record, action?: s return result; }; -const expectPrivilegeCheck = async (fn: Function, args: Record) => { +const expectPrivilegeCheck = async ( + fn: Function, + args: Record, + namespacesOverride?: Array +) => { clientOpts.checkSavedObjectsPrivilegesAsCurrentUser.mockImplementation( getMockCheckPrivilegesFailure ); @@ -131,7 +135,7 @@ const expectPrivilegeCheck = async (fn: Function, args: Record) => expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledTimes(1); expect(clientOpts.checkSavedObjectsPrivilegesAsCurrentUser).toHaveBeenCalledWith( actions, - args.options?.namespace ?? args.options?.namespaces + namespacesOverride ?? args.options?.namespace ?? args.options?.namespaces ); }; @@ -218,15 +222,17 @@ function getMockCheckPrivilegesSuccess(actions: string | string[], namespaces?: return { hasAllRequested: true, username: USERNAME, - privileges: _namespaces - .map((resource) => - _actions.map((action) => ({ - resource, - privilege: action, - authorized: true, - })) - ) - .flat(), + privileges: { + kibana: _namespaces + .map((resource) => + _actions.map((action) => ({ + resource, + privilege: action, + authorized: true, + })) + ) + .flat(), + }, }; } @@ -242,15 +248,17 @@ function getMockCheckPrivilegesFailure(actions: string | string[], namespaces?: return { hasAllRequested: false, username: USERNAME, - privileges: _namespaces - .map((resource, idxa) => - _actions.map((action, idxb) => ({ - resource, - privilege: action, - authorized: idxa > 0 || idxb > 0, - })) - ) - .flat(), + privileges: { + kibana: _namespaces + .map((resource, idxa) => + _actions.map((action, idxb) => ({ + resource, + privilege: action, + authorized: idxa > 0 || idxb > 0, + })) + ) + .flat(), + }, }; } @@ -483,7 +491,18 @@ describe('#bulkUpdate', () => { test(`checks privileges for user, actions, and namespace`, async () => { const objects = [obj1, obj2]; - await expectPrivilegeCheck(client.bulkUpdate, { objects, options }); + const namespacesOverride = [options.namespace]; // the bulkCreate function checks privileges as an array + await expectPrivilegeCheck(client.bulkUpdate, { objects, options }, namespacesOverride); + }); + + test(`checks privileges for object namespaces if present`, async () => { + const objects = [ + { ...obj1, namespace: 'foo-ns' }, + { ...obj2, namespace: 'bar-ns' }, + ]; + const namespacesOverride = [undefined, 'foo-ns', 'bar-ns']; + // use the default namespace for the options + await expectPrivilegeCheck(client.bulkUpdate, { objects, options: {} }, namespacesOverride); }); test(`filters namespaces that the user doesn't have access to`, async () => { diff --git a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts index 68fe65d204d6d..16e52c69f274f 100644 --- a/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts +++ b/x-pack/plugins/security/server/saved_objects/secure_saved_objects_client_wrapper.ts @@ -19,7 +19,7 @@ import { } from '../../../../../src/core/server'; import { SecurityAuditLogger } from '../audit'; import { Actions, CheckSavedObjectsPrivileges } from '../authorization'; -import { CheckPrivilegesResponse } from '../authorization/check_privileges'; +import { CheckPrivilegesResponse } from '../authorization/types'; import { SpacesService } from '../plugin'; interface SecureSavedObjectsClientWrapperOptions { @@ -199,12 +199,16 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra objects: Array> = [], options: SavedObjectsBaseOptions = {} ) { - await this.ensureAuthorized( - this.getUniqueObjectTypes(objects), - 'bulk_update', - options && options.namespace, - { objects, options } - ); + const objectNamespaces = objects + // The repository treats an `undefined` object namespace is treated as the absence of a namespace, falling back to options.namespace; + // in this case, filter it out here so we don't accidentally check for privileges in the Default space when we shouldn't be doing so. + .filter(({ namespace }) => namespace !== undefined) + .map(({ namespace }) => namespace!); + const namespaces = [options?.namespace, ...objectNamespaces]; + await this.ensureAuthorized(this.getUniqueObjectTypes(objects), 'bulk_update', namespaces, { + objects, + options, + }); const response = await this.baseClient.bulkUpdate(objects, options); return await this.redactSavedObjectsNamespaces(response); @@ -212,7 +216,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra private async checkPrivileges( actions: string | string[], - namespaceOrNamespaces?: string | string[] + namespaceOrNamespaces?: string | Array ) { try { return await this.checkSavedObjectsPrivilegesAsCurrentUser(actions, namespaceOrNamespaces); @@ -224,7 +228,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra private async ensureAuthorized( typeOrTypes: string | string[], action: string, - namespaceOrNamespaces?: string | string[], + namespaceOrNamespaces?: string | Array, args?: Record, auditAction: string = action, requiresAll = true @@ -238,12 +242,12 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra const { hasAllRequested, username, privileges } = result; const spaceIds = uniq( - privileges.map(({ resource }) => resource).filter((x) => x !== undefined) + privileges.kibana.map(({ resource }) => resource).filter((x) => x !== undefined) ).sort() as string[]; const isAuthorized = (requiresAll && hasAllRequested) || - (!requiresAll && privileges.some(({ authorized }) => authorized)); + (!requiresAll && privileges.kibana.some(({ authorized }) => authorized)); if (isAuthorized) { this.auditLogger.savedObjectsAuthorizationSuccess( username, @@ -271,7 +275,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra } private getMissingPrivileges(privileges: CheckPrivilegesResponse['privileges']) { - return privileges + return privileges.kibana .filter(({ authorized }) => !authorized) .map(({ resource, privilege }) => ({ spaceId: resource, privilege })); } @@ -284,7 +288,7 @@ export class SecureSavedObjectsClientWrapper implements SavedObjectsClientContra const action = this.actions.login; const checkPrivilegesResult = await this.checkPrivileges(action, namespaces); // check if the user can log into each namespace - const map = checkPrivilegesResult.privileges.reduce( + const map = checkPrivilegesResult.privileges.kibana.reduce( (acc: Record, { resource, authorized }) => { // there should never be a case where more than one privilege is returned for a given space // if there is, fail-safe (authorized + unauthorized = unauthorized) diff --git a/x-pack/plugins/security/server/usage_collector/index.ts b/x-pack/plugins/security/server/usage_collector/index.ts new file mode 100644 index 0000000000000..dd405ebac4241 --- /dev/null +++ b/x-pack/plugins/security/server/usage_collector/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { registerSecurityUsageCollector } from './security_usage_collector'; diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts new file mode 100644 index 0000000000000..6c3dcddcdb418 --- /dev/null +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.test.ts @@ -0,0 +1,465 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { createConfig, ConfigSchema } from '../config'; +import { loggingSystemMock } from 'src/core/server/mocks'; +import { TypeOf } from '@kbn/config-schema'; +import { usageCollectionPluginMock } from 'src/plugins/usage_collection/server/mocks'; +import { registerSecurityUsageCollector } from './security_usage_collector'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { licenseMock } from '../../common/licensing/index.mock'; +import { SecurityLicenseFeatures } from '../../common/licensing'; + +describe('Security UsageCollector', () => { + const createSecurityConfig = (config: TypeOf) => { + return createConfig(config, loggingSystemMock.createLogger(), { isTLSEnabled: true }); + }; + + const createSecurityLicense = ({ + allowAccessAgreement = true, + allowAuditLogging = true, + allowRbac = true, + isLicenseAvailable, + }: Partial & { isLicenseAvailable: boolean }) => { + const license = licenseMock.create(); + license.isLicenseAvailable.mockReturnValue(isLicenseAvailable); + license.getFeatures.mockReturnValue({ + allowAccessAgreement, + allowAuditLogging, + allowRbac, + } as SecurityLicenseFeatures); + return license; + }; + + const clusterClient = elasticsearchServiceMock.createLegacyClusterClient(); + + describe('initialization', () => { + it('handles an undefined usage collector', () => { + const config = createSecurityConfig(ConfigSchema.validate({})); + const usageCollection = undefined; + const license = createSecurityLicense({ allowRbac: false, isLicenseAvailable: false }); + registerSecurityUsageCollector({ usageCollection, config, license }); + }); + + it('registers itself and waits for the license to become available before reporting itself as ready', async () => { + const config = createSecurityConfig(ConfigSchema.validate({})); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ allowRbac: false, isLicenseAvailable: false }); + + registerSecurityUsageCollector({ usageCollection, config, license }); + + expect(usageCollection.getCollectorByType('security')?.isReady()).toBe(false); + + license.isLicenseAvailable.mockReturnValue(true); + license.getFeatures.mockReturnValue({ allowRbac: true } as SecurityLicenseFeatures); + + expect(usageCollection.getCollectorByType('security')?.isReady()).toBe(true); + }); + }); + + it('reports correctly for a default configuration', async () => { + const config = createSecurityConfig(ConfigSchema.validate({})); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(clusterClient.asScoped().callAsCurrentUser); + + expect(usage).toEqual({ + auditLoggingEnabled: false, + accessAgreementEnabled: false, + authProviderCount: 1, + enabledAuthProviders: ['basic'], + loginSelectorEnabled: false, + httpAuthSchemes: ['apikey'], + }); + }); + + it('reports correctly when security is disabled in Elasticsearch', async () => { + const config = createSecurityConfig(ConfigSchema.validate({})); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ allowRbac: false, isLicenseAvailable: true }); + + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(clusterClient.asScoped().callAsCurrentUser); + + expect(usage).toEqual({ + auditLoggingEnabled: false, + accessAgreementEnabled: false, + authProviderCount: 0, + enabledAuthProviders: [], + loginSelectorEnabled: false, + httpAuthSchemes: [], + }); + }); + + describe('auth providers', () => { + it('does not report disabled auth providers', async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + authc: { + providers: { + basic: { + basic: { + order: 0, + }, + disabledBasic: { + enabled: false, + order: 1, + }, + }, + saml: { + disabledSaml: { + enabled: false, + realm: 'foo', + order: 2, + }, + }, + }, + }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(clusterClient.asScoped().callAsCurrentUser); + + expect(usage).toEqual({ + auditLoggingEnabled: false, + accessAgreementEnabled: false, + authProviderCount: 1, + enabledAuthProviders: ['basic'], + loginSelectorEnabled: false, + httpAuthSchemes: ['apikey'], + }); + }); + + it('reports the types and count of enabled auth providers', async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + authc: { + providers: { + basic: { + basic: { + order: 0, + enabled: false, + }, + }, + saml: { + saml1: { + realm: 'foo', + order: 1, + }, + saml2: { + realm: 'bar', + order: 2, + }, + }, + pki: { + pki1: { + enabled: true, + order: 3, + }, + }, + }, + }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(clusterClient.asScoped().callAsCurrentUser); + + expect(usage).toEqual({ + auditLoggingEnabled: false, + accessAgreementEnabled: false, + authProviderCount: 3, + enabledAuthProviders: ['saml', 'pki'], + loginSelectorEnabled: true, + httpAuthSchemes: ['apikey'], + }); + }); + }); + + describe('access agreement', () => { + it('reports if the access agreement message is configured for any provider', async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + authc: { + providers: { + saml: { + saml1: { + realm: 'foo', + order: 1, + accessAgreement: { + message: 'foo message', + }, + }, + }, + }, + }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(clusterClient.asScoped().callAsCurrentUser); + + expect(usage).toEqual({ + auditLoggingEnabled: false, + accessAgreementEnabled: true, + authProviderCount: 1, + enabledAuthProviders: ['saml'], + loginSelectorEnabled: false, + httpAuthSchemes: ['apikey'], + }); + }); + it('does not report the access agreement if the license does not permit it', async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + authc: { + providers: { + saml: { + saml1: { + realm: 'foo', + order: 1, + accessAgreement: { + message: 'foo message', + }, + }, + }, + }, + }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ + isLicenseAvailable: true, + allowAccessAgreement: false, + }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(clusterClient.asScoped().callAsCurrentUser); + + expect(usage).toEqual({ + auditLoggingEnabled: false, + accessAgreementEnabled: false, + authProviderCount: 1, + enabledAuthProviders: ['saml'], + loginSelectorEnabled: false, + httpAuthSchemes: ['apikey'], + }); + }); + + it('does not report the access agreement for disabled providers', async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + authc: { + providers: { + saml: { + saml1: { + enabled: false, + realm: 'foo', + order: 1, + accessAgreement: { + message: 'foo message', + }, + }, + saml2: { + realm: 'foo', + order: 2, + }, + }, + }, + }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(clusterClient.asScoped().callAsCurrentUser); + + expect(usage).toEqual({ + auditLoggingEnabled: false, + accessAgreementEnabled: false, + authProviderCount: 1, + enabledAuthProviders: ['saml'], + loginSelectorEnabled: false, + httpAuthSchemes: ['apikey'], + }); + }); + }); + + describe('login selector', () => { + it('reports when the login selector is enabled', async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + authc: { + selector: { + enabled: true, + }, + providers: { + saml: { + saml1: { + realm: 'foo', + order: 1, + showInSelector: true, + }, + }, + }, + }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(clusterClient.asScoped().callAsCurrentUser); + + expect(usage).toEqual({ + auditLoggingEnabled: false, + accessAgreementEnabled: false, + authProviderCount: 1, + enabledAuthProviders: ['saml'], + loginSelectorEnabled: true, + httpAuthSchemes: ['apikey'], + }); + }); + }); + + describe('audit logging', () => { + it('reports when audit logging is enabled', async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + audit: { + enabled: true, + }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true, allowAuditLogging: true }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(clusterClient.asScoped().callAsCurrentUser); + + expect(usage).toEqual({ + auditLoggingEnabled: true, + accessAgreementEnabled: false, + authProviderCount: 1, + enabledAuthProviders: ['basic'], + loginSelectorEnabled: false, + httpAuthSchemes: ['apikey'], + }); + }); + + it('does not report audit logging when the license does not permit it', async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + audit: { + enabled: true, + }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true, allowAuditLogging: false }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(clusterClient.asScoped().callAsCurrentUser); + + expect(usage).toEqual({ + auditLoggingEnabled: false, + accessAgreementEnabled: false, + authProviderCount: 1, + enabledAuthProviders: ['basic'], + loginSelectorEnabled: false, + httpAuthSchemes: ['apikey'], + }); + }); + }); + + describe('http auth schemes', () => { + it('reports customized http auth schemes', async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + authc: { + http: { + schemes: ['basic', 'Negotiate'], + }, + }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true, allowAuditLogging: false }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(clusterClient.asScoped().callAsCurrentUser); + + expect(usage).toEqual({ + auditLoggingEnabled: false, + accessAgreementEnabled: false, + authProviderCount: 1, + enabledAuthProviders: ['basic'], + loginSelectorEnabled: false, + httpAuthSchemes: ['basic', 'Negotiate'], + }); + }); + + it('does not report auth schemes that are not "well known"', async () => { + const config = createSecurityConfig( + ConfigSchema.validate({ + authc: { + http: { + schemes: ['basic', 'Negotiate', 'customScheme'], + }, + }, + }) + ); + const usageCollection = usageCollectionPluginMock.createSetupContract(); + const license = createSecurityLicense({ isLicenseAvailable: true, allowAuditLogging: false }); + registerSecurityUsageCollector({ usageCollection, config, license }); + + const usage = await usageCollection + .getCollectorByType('security') + ?.fetch(clusterClient.asScoped().callAsCurrentUser); + + expect(usage).toEqual({ + auditLoggingEnabled: false, + accessAgreementEnabled: false, + authProviderCount: 1, + enabledAuthProviders: ['basic'], + loginSelectorEnabled: false, + httpAuthSchemes: ['basic', 'Negotiate'], + }); + }); + }); +}); diff --git a/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts new file mode 100644 index 0000000000000..11e58f7f95fc2 --- /dev/null +++ b/x-pack/plugins/security/server/usage_collector/security_usage_collector.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; +import { ConfigType } from '../config'; +import { SecurityLicense } from '../../common/licensing'; + +interface Usage { + auditLoggingEnabled: boolean; + loginSelectorEnabled: boolean; + accessAgreementEnabled: boolean; + authProviderCount: number; + enabledAuthProviders: string[]; + httpAuthSchemes: string[]; +} + +interface Deps { + usageCollection?: UsageCollectionSetup; + config: ConfigType; + license: SecurityLicense; +} + +// List of auth schemes collected from https://www.iana.org/assignments/http-authschemes/http-authschemes.xhtml +const WELL_KNOWN_AUTH_SCHEMES = [ + 'basic', + 'bearer', + 'digest', + 'hoba', + 'mutual', + 'negotiate', + 'oauth', + 'scram-sha-1', + 'scram-sha-256', + 'vapid', + 'apikey', // not part of the spec, but used by the Elastic Stack for API Key authentication +]; + +export function registerSecurityUsageCollector({ usageCollection, config, license }: Deps): void { + // usageCollection is an optional dependency, so make sure to return if it is not registered. + if (!usageCollection) { + return; + } + + // create usage collector + const securityCollector = usageCollection.makeUsageCollector({ + type: 'security', + isReady: () => license.isLicenseAvailable(), + schema: { + auditLoggingEnabled: { + type: 'boolean', + }, + loginSelectorEnabled: { + type: 'boolean', + }, + accessAgreementEnabled: { + type: 'boolean', + }, + authProviderCount: { + type: 'number', + }, + enabledAuthProviders: { + type: 'keyword', + }, + httpAuthSchemes: { + type: 'keyword', + }, + }, + fetch: () => { + const { allowRbac, allowAccessAgreement, allowAuditLogging } = license.getFeatures(); + if (!allowRbac) { + return { + auditLoggingEnabled: false, + loginSelectorEnabled: false, + accessAgreementEnabled: false, + authProviderCount: 0, + enabledAuthProviders: [], + httpAuthSchemes: [], + }; + } + + const auditLoggingEnabled = allowAuditLogging && config.audit.enabled; + const loginSelectorEnabled = config.authc.selector.enabled; + const authProviderCount = config.authc.sortedProviders.length; + const enabledAuthProviders = [ + ...new Set( + config.authc.sortedProviders.reduce( + (acc, provider) => [...acc, provider.type], + [] as string[] + ) + ), + ]; + const accessAgreementEnabled = + allowAccessAgreement && + config.authc.sortedProviders.some((provider) => provider.hasAccessAgreement); + + const httpAuthSchemes = config.authc.http.schemes.filter((scheme) => + WELL_KNOWN_AUTH_SCHEMES.includes(scheme.toLowerCase()) + ); + + return { + auditLoggingEnabled, + loginSelectorEnabled, + accessAgreementEnabled, + authProviderCount, + enabledAuthProviders, + httpAuthSchemes, + }; + }, + }); + + // register usage collector + usageCollection.registerCollector(securityCollector); +} diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index 366bf7a1df1f2..a6018837fa4fe 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -7,6 +7,7 @@ export const eventsIndexPattern = 'logs-endpoint.events.*'; export const alertsIndexPattern = 'logs-endpoint.alerts-*'; export const metadataIndexPattern = 'metrics-endpoint.metadata-*'; +export const metadataCurrentIndexPattern = 'metrics-endpoint.metadata_current-*'; export const policyIndexPattern = 'metrics-endpoint.policy-*'; export const telemetryIndexPattern = 'metrics-endpoint.telemetry-*'; export const LIMITED_CONCURRENCY_ENDPOINT_ROUTE_TAG = 'endpoint:limited-concurrency'; diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts index be3a1e82356c8..7e3b3d125fb5d 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.test.ts @@ -13,6 +13,12 @@ import { ECSCategory, ANCESTRY_LIMIT, } from './generate_data'; +import { firstNonNullValue, values } from './models/ecs_safety_helpers'; +import { + entityIDSafeVersion, + parentEntityIDSafeVersion, + timestampSafeVersion, +} from './models/event'; interface Node { events: Event[]; @@ -30,7 +36,7 @@ describe('data generator', () => { const event1 = generator.generateEvent(); const event2 = generator.generateEvent(); - expect(event2.event.sequence).toBe(event1.event.sequence + 1); + expect(event2.event?.sequence).toBe((firstNonNullValue(event1.event?.sequence) ?? 0) + 1); }); it('creates the same documents with same random seed', () => { @@ -76,37 +82,37 @@ describe('data generator', () => { const timestamp = new Date().getTime(); const alert = generator.generateAlert(timestamp); expect(alert['@timestamp']).toEqual(timestamp); - expect(alert.event.action).not.toBeNull(); + expect(alert.event?.action).not.toBeNull(); expect(alert.Endpoint).not.toBeNull(); expect(alert.agent).not.toBeNull(); expect(alert.host).not.toBeNull(); - expect(alert.process.entity_id).not.toBeNull(); + expect(alert.process?.entity_id).not.toBeNull(); }); it('creates process event documents', () => { const timestamp = new Date().getTime(); const processEvent = generator.generateEvent({ timestamp }); expect(processEvent['@timestamp']).toEqual(timestamp); - expect(processEvent.event.category).toEqual(['process']); - expect(processEvent.event.kind).toEqual('event'); - expect(processEvent.event.type).toEqual(['start']); + expect(processEvent.event?.category).toEqual(['process']); + expect(processEvent.event?.kind).toEqual('event'); + expect(processEvent.event?.type).toEqual(['start']); expect(processEvent.agent).not.toBeNull(); expect(processEvent.host).not.toBeNull(); - expect(processEvent.process.entity_id).not.toBeNull(); - expect(processEvent.process.name).not.toBeNull(); + expect(processEvent.process?.entity_id).not.toBeNull(); + expect(processEvent.process?.name).not.toBeNull(); }); it('creates other event documents', () => { const timestamp = new Date().getTime(); const processEvent = generator.generateEvent({ timestamp, eventCategory: 'dns' }); expect(processEvent['@timestamp']).toEqual(timestamp); - expect(processEvent.event.category).toEqual('dns'); - expect(processEvent.event.kind).toEqual('event'); - expect(processEvent.event.type).toEqual(['start']); + expect(processEvent.event?.category).toEqual('dns'); + expect(processEvent.event?.kind).toEqual('event'); + expect(processEvent.event?.type).toEqual(['start']); expect(processEvent.agent).not.toBeNull(); expect(processEvent.host).not.toBeNull(); - expect(processEvent.process.entity_id).not.toBeNull(); - expect(processEvent.process.name).not.toBeNull(); + expect(processEvent.process?.entity_id).not.toBeNull(); + expect(processEvent.process?.name).not.toBeNull(); }); describe('creates events with an empty ancestry array', () => { @@ -128,7 +134,7 @@ describe('data generator', () => { it('creates all events with an empty ancestry array', () => { for (const event of tree.allEvents) { - expect(event.process.Ext!.ancestry!.length).toEqual(0); + expect(event.process?.Ext?.ancestry?.length).toEqual(0); } }); }); @@ -194,22 +200,23 @@ describe('data generator', () => { const inRelated = node.relatedEvents.includes(event); const inRelatedAlerts = node.relatedAlerts.includes(event); - return (inRelated || inRelatedAlerts || inLifecycle) && event.process.entity_id === node.id; + return (inRelated || inRelatedAlerts || inLifecycle) && event.process?.entity_id === node.id; }; const verifyAncestry = (event: Event, genTree: Tree) => { - if (event.process.Ext!.ancestry!.length > 0) { - expect(event.process.parent?.entity_id).toBe(event.process.Ext!.ancestry![0]); + const ancestry = values(event.process?.Ext?.ancestry); + if (ancestry.length > 0) { + expect(event.process?.parent?.entity_id).toBe(ancestry[0]); } - for (let i = 0; i < event.process.Ext!.ancestry!.length; i++) { - const ancestor = event.process.Ext!.ancestry![i]; + for (let i = 0; i < ancestry.length; i++) { + const ancestor = ancestry[i]; const parent = genTree.children.get(ancestor) || genTree.ancestry.get(ancestor); - expect(ancestor).toBe(parent?.lifecycle[0].process.entity_id); + expect(ancestor).toBe(parent?.lifecycle[0].process?.entity_id); // the next ancestor should be the grandparent - if (i + 1 < event.process.Ext!.ancestry!.length) { - const grandparent = event.process.Ext!.ancestry![i + 1]; - expect(grandparent).toBe(parent?.lifecycle[0].process.parent?.entity_id); + if (i + 1 < ancestry.length) { + const grandparent = ancestry[i + 1]; + expect(grandparent).toBe(parent?.lifecycle[0].process?.parent?.entity_id); } } }; @@ -217,13 +224,14 @@ describe('data generator', () => { it('creates related events in ascending order', () => { // the order should not change since it should already be in ascending order const relatedEventsAsc = _.cloneDeep(tree.origin.relatedEvents).sort( - (event1, event2) => event1['@timestamp'] - event2['@timestamp'] + (event1, event2) => + (timestampSafeVersion(event1) ?? 0) - (timestampSafeVersion(event2) ?? 0) ); expect(tree.origin.relatedEvents).toStrictEqual(relatedEventsAsc); }); it('has ancestry array defined', () => { - expect(tree.origin.lifecycle[0].process.Ext!.ancestry!.length).toBe(ANCESTRY_LIMIT); + expect(values(tree.origin.lifecycle[0].process?.Ext?.ancestry).length).toBe(ANCESTRY_LIMIT); for (const event of tree.allEvents) { verifyAncestry(event, tree); } @@ -252,12 +260,9 @@ describe('data generator', () => { const counts: Record = {}; for (const event of node.relatedEvents) { - if (Array.isArray(event.event.category)) { - for (const cat of event.event.category) { - counts[cat] = counts[cat] + 1 || 1; - } - } else { - counts[event.event.category] = counts[event.event.category] + 1 || 1; + const categories = values(event.event?.category); + for (const cat of categories) { + counts[cat] = counts[cat] + 1 || 1; } } expect(counts[ECSCategory.Driver]).toEqual(1); @@ -316,15 +321,18 @@ describe('data generator', () => { expect(tree.allEvents.length).toBeGreaterThan(0); tree.allEvents.forEach((event) => { - const ancestor = tree.ancestry.get(event.process.entity_id); - if (ancestor) { - expect(eventInNode(event, ancestor)).toBeTruthy(); - return; - } + const entityID = entityIDSafeVersion(event); + if (entityID) { + const ancestor = tree.ancestry.get(entityID); + if (ancestor) { + expect(eventInNode(event, ancestor)).toBeTruthy(); + return; + } - const children = tree.children.get(event.process.entity_id); - if (children) { - expect(eventInNode(event, children)).toBeTruthy(); + const children = tree.children.get(entityID); + if (children) { + expect(eventInNode(event, children)).toBeTruthy(); + } } }); }); @@ -351,9 +359,8 @@ describe('data generator', () => { let events: Event[]; const isCategoryProcess = (event: Event) => { - return ( - _.isEqual(event.event.category, ['process']) || _.isEqual(event.event.category, 'process') - ); + const category = values(event.event?.category); + return _.isEqual(category, ['process']); }; beforeEach(() => { @@ -366,12 +373,16 @@ describe('data generator', () => { it('with n-1 process events', () => { for (let i = events.length - 2; i > 0; ) { - const parentEntityIdOfChild = events[i].process.parent?.entity_id; - for (; --i >= -1 && (events[i].event.kind !== 'event' || !isCategoryProcess(events[i])); ) { + const parentEntityIdOfChild = parentEntityIDSafeVersion(events[i]); + for ( + ; + --i >= -1 && (events[i].event?.kind !== 'event' || !isCategoryProcess(events[i])); + + ) { // related event - skip it } expect(i).toBeGreaterThanOrEqual(0); - expect(parentEntityIdOfChild).toEqual(events[i].process.entity_id); + expect(parentEntityIdOfChild).toEqual(entityIDSafeVersion(events[i])); } }); @@ -380,7 +391,7 @@ describe('data generator', () => { for ( ; previousProcessEventIndex >= -1 && - (events[previousProcessEventIndex].event.kind !== 'event' || + (events[previousProcessEventIndex].event?.kind !== 'event' || !isCategoryProcess(events[previousProcessEventIndex])); previousProcessEventIndex-- ) { @@ -388,14 +399,14 @@ describe('data generator', () => { } expect(previousProcessEventIndex).toBeGreaterThanOrEqual(0); // The alert should be last and have the same entity_id as the previous process event - expect(events[events.length - 1].process.entity_id).toEqual( - events[previousProcessEventIndex].process.entity_id + expect(events[events.length - 1].process?.entity_id).toEqual( + events[previousProcessEventIndex].process?.entity_id ); - expect(events[events.length - 1].process.parent?.entity_id).toEqual( - events[previousProcessEventIndex].process.parent?.entity_id + expect(events[events.length - 1].process?.parent?.entity_id).toEqual( + events[previousProcessEventIndex].process?.parent?.entity_id ); - expect(events[events.length - 1].event.kind).toEqual('alert'); - expect(events[events.length - 1].event.category).toEqual('malware'); + expect(events[events.length - 1].event?.kind).toEqual('alert'); + expect(events[events.length - 1].event?.category).toEqual('malware'); }); }); @@ -403,14 +414,17 @@ describe('data generator', () => { // First pass we gather up all the events by entity_id const tree: Record = {}; events.forEach((event) => { - if (event.process.entity_id in tree) { - tree[event.process.entity_id].events.push(event); - } else { - tree[event.process.entity_id] = { - events: [event], - children: [], - parent_entity_id: event.process.parent?.entity_id, - }; + const entityID = entityIDSafeVersion(event); + if (entityID) { + if (entityID in tree) { + tree[entityID].events.push(event); + } else { + tree[entityID] = { + events: [event], + children: [], + parent_entity_id: parentEntityIDSafeVersion(event), + }; + } } }); // Second pass add child references to each node @@ -419,8 +433,14 @@ describe('data generator', () => { tree[value.parent_entity_id].children.push(value); } } + + const entityID = entityIDSafeVersion(events[0]); + if (!entityID) { + throw new Error('entity id was invalid'); + } + // The root node must be first in the array or this fails - return tree[events[0].process.entity_id]; + return tree[entityID]; } function countResolverEvents(rootNode: Node, generations: number): number { diff --git a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts index 0955f196df176..7f31c71fe712b 100644 --- a/x-pack/plugins/security_solution/common/endpoint/generate_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/generate_data.ts @@ -7,7 +7,6 @@ import uuid from 'uuid'; import seedrandom from 'seedrandom'; import { AlertEvent, - EndpointEvent, EndpointStatus, Host, HostMetadata, @@ -15,9 +14,15 @@ import { HostPolicyResponseActionStatus, OSFields, PolicyData, + SafeEndpointEvent, } from './types'; import { factory as policyFactory } from './models/policy_config'; -import { parentEntityId } from './models/event'; +import { + ancestryArray, + entityIDSafeVersion, + parentEntityIDSafeVersion, + timestampSafeVersion, +} from './models/event'; import { GetAgentPoliciesResponseItem, GetPackagesResponse, @@ -28,8 +33,9 @@ import { InstallationStatus, KibanaAssetReference, } from '../../../ingest_manager/common/types/models'; +import { firstNonNullValue } from './models/ecs_safety_helpers'; -export type Event = AlertEvent | EndpointEvent; +export type Event = AlertEvent | SafeEndpointEvent; /** * This value indicates the limit for the size of the ancestry array. The endpoint currently saves up to 20 values * in its messages. To simulate a limit on the array size I'm using 2 here so that we can't rely on there being a large @@ -426,13 +432,13 @@ export class EndpointDocGenerator { * @param ts - Timestamp to put in the event * @param entityID - entityID of the originating process * @param parentEntityID - optional entityID of the parent process, if it exists - * @param ancestryArray - an array of ancestors for the generated alert + * @param ancestry - an array of ancestors for the generated alert */ public generateAlert( ts = new Date().getTime(), entityID = this.randomString(10), parentEntityID?: string, - ancestryArray: string[] = [] + ancestry: string[] = [] ): AlertEvent { return { ...this.commonInfo, @@ -493,7 +499,7 @@ export class EndpointDocGenerator { sha256: 'fake sha256', }, Ext: { - ancestry: ancestryArray, + ancestry, code_signature: [ { trusted: false, @@ -555,7 +561,7 @@ export class EndpointDocGenerator { * Creates an event, customized by the options parameter * @param options - Allows event field values to be specified */ - public generateEvent(options: EventOptions = {}): EndpointEvent { + public generateEvent(options: EventOptions = {}): Event { // this will default to an empty array for the ancestry field if options.ancestry isn't included const ancestry: string[] = options.ancestry?.slice(0, options?.ancestryArrayLimit ?? ANCESTRY_LIMIT) ?? []; @@ -643,7 +649,11 @@ export class EndpointDocGenerator { public generateTree(options: TreeOptions = {}): Tree { const optionsWithDef = getTreeOptionsWithDef(options); const addEventToMap = (nodeMap: Map, event: Event) => { - const nodeId = event.process.entity_id; + const nodeId = entityIDSafeVersion(event); + if (!nodeId) { + return nodeMap; + } + // if a node already exists for the entity_id we'll use that one, otherwise let's create a new empty node // and add the event to the right array. let node = nodeMap.get(nodeId); @@ -652,18 +662,13 @@ export class EndpointDocGenerator { } // place the event in the right array depending on its category - if (event.event.kind === 'event') { - if ( - (Array.isArray(event.event.category) && - event.event.category.length === 1 && - event.event.category[0] === 'process') || - event.event.category === 'process' - ) { + if (firstNonNullValue(event.event?.kind) === 'event') { + if (firstNonNullValue(event.event?.category) === 'process') { node.lifecycle.push(event); } else { node.relatedEvents.push(event); } - } else if (event.event.kind === 'alert') { + } else if (firstNonNullValue(event.event?.kind) === 'alert') { node.relatedAlerts.push(event); } @@ -673,7 +678,7 @@ export class EndpointDocGenerator { const groupNodesByParent = (children: Map) => { const nodesByParent: Map> = new Map(); for (const node of children.values()) { - const parentID = parentEntityId(node.lifecycle[0]); + const parentID = parentEntityIDSafeVersion(node.lifecycle[0]); if (parentID) { let groupedNodes = nodesByParent.get(parentID); @@ -715,9 +720,13 @@ export class EndpointDocGenerator { const ancestryNodes: Map = ancestry.reduce(addEventToMap, new Map()); const alert = ancestry[ancestry.length - 1]; - const origin = ancestryNodes.get(alert.process.entity_id); + const alertEntityID = entityIDSafeVersion(alert); + if (!alertEntityID) { + throw Error("could not find the originating alert's entity id"); + } + const origin = ancestryNodes.get(alertEntityID); if (!origin) { - throw Error(`could not find origin while building tree: ${alert.process.entity_id}`); + throw Error(`could not find origin while building tree: ${alertEntityID}`); } const children = Array.from(this.descendantsTreeGenerator(alert, optionsWithDef)); @@ -799,7 +808,7 @@ export class EndpointDocGenerator { }); events.push(root); let ancestor = root; - let timestamp = root['@timestamp'] + 1000; + let timestamp = (timestampSafeVersion(root) ?? 0) + 1000; const addRelatedAlerts = ( node: Event, @@ -836,8 +845,8 @@ export class EndpointDocGenerator { events.push( this.generateEvent({ timestamp: timestamp + termProcessDuration * 1000, - entityID: root.process.entity_id, - parentEntityID: root.process.parent?.entity_id, + entityID: entityIDSafeVersion(root), + parentEntityID: parentEntityIDSafeVersion(root), eventCategory: ['process'], eventType: ['end'], }) @@ -845,13 +854,20 @@ export class EndpointDocGenerator { } for (let i = 0; i < opts.ancestors; i++) { + const ancestorEntityID = entityIDSafeVersion(ancestor); + const ancestry: string[] = []; + if (ancestorEntityID) { + ancestry.push(ancestorEntityID); + } + + ancestry.push(...(ancestryArray(ancestor) ?? [])); ancestor = this.generateEvent({ timestamp, - parentEntityID: ancestor.process.entity_id, + parentEntityID: entityIDSafeVersion(ancestor), // add the parent to the ancestry array - ancestry: [ancestor.process.entity_id, ...(ancestor.process.Ext?.ancestry ?? [])], + ancestry, ancestryArrayLimit: opts.ancestryArraySize, - parentPid: ancestor.process.pid, + parentPid: firstNonNullValue(ancestor.process?.pid), pid: this.randomN(5000), }); events.push(ancestor); @@ -862,11 +878,11 @@ export class EndpointDocGenerator { events.push( this.generateEvent({ timestamp: timestamp + termProcessDuration * 1000, - entityID: ancestor.process.entity_id, - parentEntityID: ancestor.process.parent?.entity_id, + entityID: entityIDSafeVersion(ancestor), + parentEntityID: parentEntityIDSafeVersion(ancestor), eventCategory: ['process'], eventType: ['end'], - ancestry: ancestor.process.Ext?.ancestry, + ancestry: ancestryArray(ancestor), ancestryArrayLimit: opts.ancestryArraySize, }) ); @@ -890,9 +906,9 @@ export class EndpointDocGenerator { events.push( this.generateAlert( timestamp, - ancestor.process.entity_id, - ancestor.process.parent?.entity_id, - ancestor.process.Ext?.ancestry + entityIDSafeVersion(ancestor), + parentEntityIDSafeVersion(ancestor), + ancestryArray(ancestor) ) ); return events; @@ -922,7 +938,7 @@ export class EndpointDocGenerator { maxChildren, }; const lineage: NodeState[] = [rootState]; - let timestamp = root['@timestamp']; + let timestamp = timestampSafeVersion(root) ?? 0; while (lineage.length > 0) { const currentState = lineage[lineage.length - 1]; // If we get to a state node and it has made all the children, move back up a level @@ -937,13 +953,17 @@ export class EndpointDocGenerator { // Otherwise, add a child and any nodes associated with it currentState.childrenCreated++; timestamp = timestamp + 1000; + const currentStateEntityID = entityIDSafeVersion(currentState.event); + const ancestry: string[] = []; + if (currentStateEntityID) { + ancestry.push(currentStateEntityID); + } + ancestry.push(...(ancestryArray(currentState.event) ?? [])); + const child = this.generateEvent({ timestamp, - parentEntityID: currentState.event.process.entity_id, - ancestry: [ - currentState.event.process.entity_id, - ...(currentState.event.process.Ext?.ancestry ?? []), - ], + parentEntityID: currentStateEntityID, + ancestry, ancestryArrayLimit: opts.ancestryArraySize, }); @@ -962,11 +982,11 @@ export class EndpointDocGenerator { processDuration = this.randomN(1000000); // This lets termination events be up to 1 million seconds after the creation event (~11 days) yield this.generateEvent({ timestamp: timestamp + processDuration * 1000, - entityID: child.process.entity_id, - parentEntityID: child.process.parent?.entity_id, + entityID: entityIDSafeVersion(child), + parentEntityID: parentEntityIDSafeVersion(child), eventCategory: ['process'], eventType: ['end'], - ancestry: child.process.Ext?.ancestry, + ancestry, ancestryArrayLimit: opts.ancestryArraySize, }); } @@ -998,7 +1018,8 @@ export class EndpointDocGenerator { ordered: boolean = false ) { let relatedEventsInfo: RelatedEventInfo[]; - let ts = node['@timestamp'] + 1; + const nodeTimestamp = timestampSafeVersion(node) ?? 0; + let ts = nodeTimestamp + 1; if (typeof relatedEvents === 'number') { relatedEventsInfo = [{ category: RelatedEventCategory.Random, count: relatedEvents }]; } else { @@ -1017,16 +1038,16 @@ export class EndpointDocGenerator { if (ordered) { ts += this.randomN(processDuration) * 1000; } else { - ts = node['@timestamp'] + this.randomN(processDuration) * 1000; + ts = nodeTimestamp + this.randomN(processDuration) * 1000; } yield this.generateEvent({ timestamp: ts, - entityID: node.process.entity_id, - parentEntityID: node.process.parent?.entity_id, + entityID: entityIDSafeVersion(node), + parentEntityID: parentEntityIDSafeVersion(node), eventCategory: eventInfo.category, eventType: eventInfo.creationType, - ancestry: node.process.Ext?.ancestry, + ancestry: ancestryArray(node), }); } } @@ -1044,12 +1065,12 @@ export class EndpointDocGenerator { alertCreationTime: number = 6 * 3600 ) { for (let i = 0; i < relatedAlerts; i++) { - const ts = node['@timestamp'] + this.randomN(alertCreationTime) * 1000; + const ts = (timestampSafeVersion(node) ?? 0) + this.randomN(alertCreationTime) * 1000; yield this.generateAlert( ts, - node.process.entity_id, - node.process.parent?.entity_id, - node.process.Ext?.ancestry + entityIDSafeVersion(node), + parentEntityIDSafeVersion(node), + ancestryArray(node) ); } } @@ -1132,7 +1153,8 @@ export class EndpointDocGenerator { path: '/package/endpoint/0.5.0', icons: [ { - src: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg', + path: '/package/endpoint/0.5.0/img/logo-endpoint-64-color.svg', + src: '/img/logo-endpoint-64-color.svg', size: '16x16', type: 'image/svg+xml', }, diff --git a/x-pack/plugins/security_solution/common/endpoint/index_data.ts b/x-pack/plugins/security_solution/common/endpoint/index_data.ts index 9a61738cd84b4..b8c2fdbe65f1e 100644 --- a/x-pack/plugins/security_solution/common/endpoint/index_data.ts +++ b/x-pack/plugins/security_solution/common/endpoint/index_data.ts @@ -7,6 +7,7 @@ import { Client } from '@elastic/elasticsearch'; import seedrandom from 'seedrandom'; import { EndpointDocGenerator, TreeOptions, Event } from './generate_data'; +import { firstNonNullValue } from './models/ecs_safety_helpers'; export async function indexHostsAndAlerts( client: Client, @@ -86,7 +87,7 @@ async function indexAlerts( // eslint-disable-next-line @typescript-eslint/no-explicit-any (array: Array>, doc) => { let index = eventIndex; - if (doc.event.kind === 'alert') { + if (firstNonNullValue(doc.event?.kind) === 'alert') { index = alertIndex; } array.push({ create: { _index: index } }, doc); diff --git a/x-pack/plugins/security_solution/common/endpoint/models/ecs_safety_helpers.ts b/x-pack/plugins/security_solution/common/endpoint/models/ecs_safety_helpers.ts index 8b419e90a6ee9..5dc75bb707d0e 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/ecs_safety_helpers.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/ecs_safety_helpers.ts @@ -46,12 +46,12 @@ export function values(valueOrCollection: ECSField): T[] { if (Array.isArray(valueOrCollection)) { const nonNullValues: T[] = []; for (const value of valueOrCollection) { - if (value !== null) { + if (value !== null && value !== undefined) { nonNullValues.push(value); } } return nonNullValues; - } else if (valueOrCollection !== null) { + } else if (valueOrCollection !== null && valueOrCollection !== undefined) { // if there is a single non-null value, wrap it in an array and return it. return [valueOrCollection]; } else { diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts index 6e6e0f443015b..2b0aa1601ab37 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.test.ts @@ -5,7 +5,7 @@ */ import { EndpointDocGenerator } from '../generate_data'; import { descriptiveName, isProcessRunning } from './event'; -import { ResolverEvent } from '../types'; +import { ResolverEvent, SafeResolverEvent } from '../types'; describe('Generated documents', () => { let generator: EndpointDocGenerator; @@ -17,20 +17,31 @@ describe('Generated documents', () => { it('returns the right name for a registry event', () => { const extensions = { registry: { key: `HKLM/Windows/Software/abc` } }; const event = generator.generateEvent({ eventCategory: 'registry', extensions }); - expect(descriptiveName(event)).toEqual({ subject: `HKLM/Windows/Software/abc` }); + // casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies + // on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast. + expect(descriptiveName(event as ResolverEvent)).toEqual({ + subject: `HKLM/Windows/Software/abc`, + }); }); it('returns the right name for a network event', () => { const randomIP = `${generator.randomIP()}`; const extensions = { network: { direction: 'outbound', forwarded_ip: randomIP } }; const event = generator.generateEvent({ eventCategory: 'network', extensions }); - expect(descriptiveName(event)).toEqual({ subject: `${randomIP}`, descriptor: 'outbound' }); + // casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies + // on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast. + expect(descriptiveName(event as ResolverEvent)).toEqual({ + subject: `${randomIP}`, + descriptor: 'outbound', + }); }); it('returns the right name for a file event', () => { const extensions = { file: { path: 'C:\\My Documents\\business\\January\\processName' } }; const event = generator.generateEvent({ eventCategory: 'file', extensions }); - expect(descriptiveName(event)).toEqual({ + // casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies + // on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast. + expect(descriptiveName(event as ResolverEvent)).toEqual({ subject: 'C:\\My Documents\\business\\January\\processName', }); }); @@ -38,27 +49,31 @@ describe('Generated documents', () => { it('returns the right name for a dns event', () => { const extensions = { dns: { question: { name: `${generator.randomIP()}` } } }; const event = generator.generateEvent({ eventCategory: 'dns', extensions }); - expect(descriptiveName(event)).toEqual({ subject: extensions.dns.question.name }); + // casting to ResolverEvent here because the `descriptiveName` function is used by the frontend is still relies + // on the unsafe ResolverEvent type. Once it's switched over to the safe version we can remove this cast. + expect(descriptiveName(event as ResolverEvent)).toEqual({ + subject: extensions.dns.question.name, + }); }); }); describe('Process running events', () => { it('is a running event when event.type is a string', () => { - const event: ResolverEvent = generator.generateEvent({ + const event: SafeResolverEvent = generator.generateEvent({ eventType: 'start', }); expect(isProcessRunning(event)).toBeTruthy(); }); it('is a running event when event.type is an array of strings', () => { - const event: ResolverEvent = generator.generateEvent({ + const event: SafeResolverEvent = generator.generateEvent({ eventType: ['start'], }); expect(isProcessRunning(event)).toBeTruthy(); }); it('is a running event when event.type is an array of strings and contains start', () => { - let event: ResolverEvent = generator.generateEvent({ + let event: SafeResolverEvent = generator.generateEvent({ eventType: ['bogus', 'start', 'creation'], }); expect(isProcessRunning(event)).toBeTruthy(); @@ -70,35 +85,35 @@ describe('Generated documents', () => { }); it('is not a running event when event.type is only and end type', () => { - const event: ResolverEvent = generator.generateEvent({ + const event: SafeResolverEvent = generator.generateEvent({ eventType: ['end'], }); expect(isProcessRunning(event)).toBeFalsy(); }); it('is not a running event when event.type is empty', () => { - const event: ResolverEvent = generator.generateEvent({ + const event: SafeResolverEvent = generator.generateEvent({ eventType: [], }); expect(isProcessRunning(event)).toBeFalsy(); }); it('is not a running event when event.type is bogus', () => { - const event: ResolverEvent = generator.generateEvent({ + const event: SafeResolverEvent = generator.generateEvent({ eventType: ['bogus'], }); expect(isProcessRunning(event)).toBeFalsy(); }); it('is a running event when event.type contains info', () => { - const event: ResolverEvent = generator.generateEvent({ + const event: SafeResolverEvent = generator.generateEvent({ eventType: ['info'], }); expect(isProcessRunning(event)).toBeTruthy(); }); it('is a running event when event.type contains change', () => { - const event: ResolverEvent = generator.generateEvent({ + const event: SafeResolverEvent = generator.generateEvent({ eventType: ['bogus', 'change'], }); expect(isProcessRunning(event)).toBeTruthy(); diff --git a/x-pack/plugins/security_solution/common/endpoint/models/event.ts b/x-pack/plugins/security_solution/common/endpoint/models/event.ts index a0e9be58911c6..07208214a641a 100644 --- a/x-pack/plugins/security_solution/common/endpoint/models/event.ts +++ b/x-pack/plugins/security_solution/common/endpoint/models/event.ts @@ -9,7 +9,7 @@ import { SafeResolverEvent, SafeLegacyEndpointEvent, } from '../types'; -import { firstNonNullValue } from './ecs_safety_helpers'; +import { firstNonNullValue, hasValue, values } from './ecs_safety_helpers'; /* * Determine if a `ResolverEvent` is the legacy variety. Can be used to narrow `ResolverEvent` to `LegacyEndpointEvent`. @@ -27,32 +27,24 @@ export function isLegacyEvent(event: ResolverEvent): event is LegacyEndpointEven return (event as LegacyEndpointEvent).endgame !== undefined; } -export function isProcessRunning(event: ResolverEvent): boolean { - if (isLegacyEvent(event)) { - return ( - event.event?.type === 'process_start' || - event.event?.action === 'fork_event' || - event.event?.type === 'already_running' - ); - } - - if (Array.isArray(event.event.type)) { +export function isProcessRunning(event: SafeResolverEvent): boolean { + if (isLegacyEventSafeVersion(event)) { return ( - event.event.type.includes('start') || - event.event.type.includes('change') || - event.event.type.includes('info') + hasValue(event.event?.type, 'process_start') || + hasValue(event.event?.action, 'fork_event') || + hasValue(event.event?.type, 'already_running') ); } return ( - event.event.type === 'start' || event.event.type === 'change' || event.event.type === 'info' + hasValue(event.event?.type, 'start') || + hasValue(event.event?.type, 'change') || + hasValue(event.event?.type, 'info') ); } -export function timestampSafeVersion(event: SafeResolverEvent): string | undefined | number { - return isLegacyEventSafeVersion(event) - ? firstNonNullValue(event.endgame?.timestamp_utc) - : firstNonNullValue(event?.['@timestamp']); +export function timestampSafeVersion(event: SafeResolverEvent): undefined | number { + return firstNonNullValue(event?.['@timestamp']); } /** @@ -75,11 +67,7 @@ export function timestampAsDateSafeVersion(event: SafeResolverEvent): Date | und } export function eventTimestamp(event: ResolverEvent): string | undefined | number { - if (isLegacyEvent(event)) { - return event.endgame.timestamp_utc; - } else { - return event['@timestamp']; - } + return event['@timestamp']; } export function eventName(event: ResolverEvent): string { @@ -105,14 +93,7 @@ export function eventId(event: ResolverEvent): number | undefined | string { return event.event.id; } -export function eventSequence(event: ResolverEvent): number | undefined { - if (isLegacyEvent(event)) { - return firstNonNullValue(event.endgame.serial_event_id); - } - return firstNonNullValue(event.event?.sequence); -} - -export function eventSequenceSafeVersion(event: SafeResolverEvent): number | undefined { +export function eventSequence(event: SafeResolverEvent): number | undefined { if (isLegacyEventSafeVersion(event)) { return firstNonNullValue(event.endgame.serial_event_id); } @@ -156,16 +137,16 @@ export function parentEntityIDSafeVersion(event: SafeResolverEvent): string | un return firstNonNullValue(event.process?.parent?.entity_id); } -export function ancestryArray(event: ResolverEvent): string[] | undefined { - if (isLegacyEvent(event)) { +export function ancestryArray(event: SafeResolverEvent): string[] | undefined { + if (isLegacyEventSafeVersion(event)) { return undefined; } // this is to guard against the endpoint accidentally not sending the ancestry array // otherwise the request will fail when really we should just try using the parent entity id - return event.process.Ext?.ancestry; + return values(event.process?.Ext?.ancestry); } -export function getAncestryAsArray(event: ResolverEvent | undefined): string[] { +export function getAncestryAsArray(event: SafeResolverEvent | undefined): string[] { if (!event) { return []; } @@ -175,7 +156,7 @@ export function getAncestryAsArray(event: ResolverEvent | undefined): string[] { return ancestors; } - const parentID = parentEntityId(event); + const parentID = parentEntityIDSafeVersion(event); if (parentID) { return [parentID]; } diff --git a/x-pack/plugins/security_solution/common/endpoint/types/index.ts b/x-pack/plugins/security_solution/common/endpoint/types/index.ts index 8e507cbc921a2..cc40225ec1a10 100644 --- a/x-pack/plugins/security_solution/common/endpoint/types/index.ts +++ b/x-pack/plugins/security_solution/common/endpoint/types/index.ts @@ -112,6 +112,27 @@ export interface ResolverChildNode extends ResolverLifecycleNode { nextChild?: string | null; } +/** + * Safe version of `ResolverChildNode`. + */ +export interface SafeResolverChildNode extends SafeResolverLifecycleNode { + /** + * nextChild can have 3 different states: + * + * undefined: This indicates that you should not use this node for additional queries. It does not mean that node does + * not have any more direct children. The node could have more direct children but to determine that, use the + * ResolverChildren node's nextChild. + * + * null: Indicates that we have received all the children of the node. There may be more descendants though. + * + * string: Indicates this is a leaf node and it can be used to continue querying for additional descendants + * using this node's entity_id + * + * For more information see the resolver docs on pagination [here](../../server/endpoint/routes/resolver/docs/README.md#L129) + */ + nextChild?: string | null; +} + /** * The response structure for the children route. The structure is an array of nodes where each node * has an array of lifecycle events. @@ -131,6 +152,24 @@ export interface ResolverChildren { nextChild: string | null; } +/** + * Safe version of `ResolverChildren`. + */ +export interface SafeResolverChildren { + childNodes: SafeResolverChildNode[]; + /** + * nextChild can have 2 different states: + * + * null: Indicates that we have received all the descendants that can be retrieved using this node. To retrieve more + * nodes in the tree use a cursor provided in one of the returned children. If no other cursor exists then the tree + * is complete. + * + * string: Indicates this node has more descendants that can be retrieved, pass this cursor in while using this node's + * entity_id for the request. + */ + nextChild: string | null; +} + /** * A flattened tree representing the nodes in a resolver graph. */ @@ -148,6 +187,23 @@ export interface ResolverTree { stats: ResolverNodeStats; } +/** + * Safe version of `ResolverTree`. + */ +export interface SafeResolverTree { + /** + * Origin of the tree. This is in the middle of the tree. Typically this would be the same + * process node that generated an alert. + */ + entityID: string; + children: SafeResolverChildren; + relatedEvents: Omit; + relatedAlerts: Omit; + ancestry: SafeResolverAncestry; + lifecycle: SafeResolverEvent[]; + stats: ResolverNodeStats; +} + /** * The lifecycle events (start, end etc) for a node. */ @@ -160,6 +216,18 @@ export interface ResolverLifecycleNode { stats?: ResolverNodeStats; } +/** + * Safe version of `ResolverLifecycleNode`. + */ +export interface SafeResolverLifecycleNode { + entityID: string; + lifecycle: SafeResolverEvent[]; + /** + * stats are only set when the entire tree is being fetched + */ + stats?: ResolverNodeStats; +} + /** * The response structure when searching for ancestors of a node. */ @@ -175,6 +243,21 @@ export interface ResolverAncestry { nextAncestor: string | null; } +/** + * Safe version of `ResolverAncestry`. + */ +export interface SafeResolverAncestry { + /** + * An array of ancestors with the lifecycle events grouped together + */ + ancestors: SafeResolverLifecycleNode[]; + /** + * A cursor for retrieving additional ancestors for a particular node. `null` indicates that there were no additional + * ancestors when the request returned. More could have been ingested by ES after the fact though. + */ + nextAncestor: string | null; +} + /** * Response structure for the related events route. */ @@ -198,7 +281,7 @@ export interface SafeResolverRelatedEvents { */ export interface ResolverRelatedAlerts { entityID: string; - alerts: ResolverEvent[]; + alerts: SafeResolverEvent[]; nextAlert: string | null; } @@ -251,152 +334,133 @@ export interface Host { /** * A record of hashes for something. Provides hashes in multiple formats. A favorite structure of the Elastic Endpoint. */ -interface Hashes { +type Hashes = Partial<{ /** * A hash in MD5 format. */ - md5: string; + md5: ECSField; /** * A hash in SHA-1 format. */ - sha1: string; + sha1: ECSField; /** * A hash in SHA-256 format. */ - sha256: string; -} + sha256: ECSField; +}>; -interface MalwareClassification { - identifier: string; - score: number; - threshold: number; - version: string; -} +type MalwareClassification = Partial<{ + identifier: ECSField; + score: ECSField; + threshold: ECSField; + version: ECSField; +}>; -interface ThreadFields { - id: number; - Ext: { - service_name: string; - start: number; - start_address: number; - start_address_module: string; - }; -} +type ThreadFields = Partial<{ + id: ECSField; + Ext: Partial<{ + service_name: ECSField; + start: ECSField; + start_address: ECSField; + start_address_module: ECSField; + }>; +}>; -interface DllFields { +type DllFields = Partial<{ hash: Hashes; - path: string; - pe: { - architecture: string; - }; - code_signature: { - subject_name: string; - trusted: boolean; - }; - Ext: { - compile_time: number; + path: ECSField; + pe: Partial<{ + architecture: ECSField; + }>; + code_signature: Partial<{ + subject_name: ECSField; + trusted: ECSField; + }>; + Ext: Partial<{ + compile_time: ECSField; malware_classification: MalwareClassification; - mapped_address: number; - mapped_size: number; - }; -} + mapped_address: ECSField; + mapped_size: ECSField; + }>; +}>; /** * Describes an Alert Event. */ -export interface AlertEvent { - '@timestamp': number; - agent: { - id: string; - version: string; - type: string; - }; - ecs: { - version: string; - }; - event: { - id: string; - action: string; - category: string; - kind: string; - dataset: string; - module: string; - type: string; - sequence: number; - }; - Endpoint: { - policy: { - applied: { - id: string; - status: HostPolicyResponseActionStatus; - name: string; - }; - }; - }; - process: { - command_line?: string; - pid: number; - ppid?: number; - entity_id: string; - parent?: { - pid: number; - entity_id: string; - }; - name: string; - hash: Hashes; - executable: string; - start: number; - thread?: ThreadFields[]; - uptime: number; - Ext?: { - /* - * The array has a special format. The entity_ids towards the beginning of the array are closer ancestors and the - * values towards the end of the array are more distant ancestors (grandparents). Therefore - * ancestry_array[0] == process.parent.entity_id and ancestry_array[1] == process.parent.parent.entity_id - */ - ancestry?: string[]; - code_signature: Array<{ - subject_name: string; - trusted: boolean; +export type AlertEvent = Partial<{ + event: Partial<{ + action: ECSField; + dataset: ECSField; + module: ECSField; + }>; + Endpoint: Partial<{ + policy: Partial<{ + applied: Partial<{ + id: ECSField; + status: ECSField; + name: ECSField; }>; - malware_classification?: MalwareClassification; - token: { - domain: string; - type: string; - user: string; - sid: string; - integrity_level: number; - integrity_level_name: string; - privileges?: Array<{ - description: string; - name: string; - enabled: boolean; - }>; - }; - user: string; - }; - }; - file: { - owner: string; - name: string; - path: string; - accessed: number; - mtime: number; - created: number; - size: number; - hash: Hashes; - Ext: { + }>; + }>; + process: Partial<{ + command_line: ECSField; + ppid: ECSField; + start: ECSField; + // Using ECSField as the outer because the object is expected to be an array + thread: ECSField; + uptime: ECSField; + Ext: Partial<{ + // Using ECSField as the outer because the object is expected to be an array + code_signature: ECSField< + Partial<{ + subject_name: ECSField; + trusted: ECSField; + }> + >; malware_classification: MalwareClassification; - temp_file_path: string; - code_signature: Array<{ - trusted: boolean; - subject_name: string; + token: Partial<{ + domain: ECSField; + type: ECSField; + user: ECSField; + sid: ECSField; + integrity_level: ECSField; + integrity_level_name: ECSField; + // Using ECSField as the outer because the object is expected to be an array + privileges: ECSField< + Partial<{ + description: ECSField; + name: ECSField; + enabled: ECSField; + }> + >; }>; - }; - }; - host: Host; - dll?: DllFields[]; -} + user: ECSField; + }>; + }>; + file: Partial<{ + owner: ECSField; + name: ECSField; + accessed: ECSField; + mtime: ECSField; + created: ECSField; + size: ECSField; + hash: Hashes; + Ext: Partial<{ + malware_classification: MalwareClassification; + temp_file_path: ECSField; + // Using ECSField as the outer because the object is expected to be an array + code_signature: ECSField< + Partial<{ + trusted: ECSField; + subject_name: ECSField; + }> + >; + }>; + }>; + // Using ECSField as the outer because the object is expected to be an array + dll: ECSField; +}> & + SafeEndpointEvent; /** * The status of the Endpoint Agent as reported by the Agent or the @@ -445,6 +509,13 @@ export type HostInfo = Immutable<{ host_status: HostStatus; }>; +export type HostMetadataDetails = Immutable<{ + agent: { + id: string; + }; + HostDetails: HostMetadata; +}>; + export type HostMetadata = Immutable<{ '@timestamp': number; event: { @@ -578,7 +649,7 @@ export type ResolverEvent = EndpointEvent | LegacyEndpointEvent; * All mappings in Elasticsearch support arrays. They can also return null values or be missing. For example, a `keyword` mapping could return `null` or `[null]` or `[]` or `'hi'`, or `['hi', 'there']`. We need to handle these cases in order to avoid throwing an error. * When dealing with an value that comes from ES, wrap the underlying type in `ECSField`. For example, if you have a `keyword` or `text` value coming from ES, cast it to `ECSField`. */ -export type ECSField = T | null | Array; +export type ECSField = T | null | undefined | Array; /** * A more conservative version of `ResolverEvent` that treats fields as optional and use `ECSField` to type all ECS fields. @@ -641,9 +712,7 @@ export type SafeEndpointEvent = Partial<{ subject_name: ECSField; }>; pid: ECSField; - hash: Partial<{ - md5: ECSField; - }>; + hash: Hashes; parent: Partial<{ entity_id: ECSField; name: ECSField; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts index b7d905d22e839..35fcc3b07fd05 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/index.ts @@ -23,6 +23,8 @@ import { } from './hosts'; import { NetworkQueries, + NetworkDetailsStrategyResponse, + NetworkDetailsRequestOptions, NetworkDnsStrategyResponse, NetworkDnsRequestOptions, NetworkTlsStrategyResponse, @@ -35,6 +37,8 @@ import { NetworkTopCountriesRequestOptions, NetworkTopNFlowStrategyResponse, NetworkTopNFlowRequestOptions, + NetworkUsersStrategyResponse, + NetworkUsersRequestOptions, } from './network'; import { MatrixHistogramQuery, @@ -87,6 +91,8 @@ export type StrategyResponseType = T extends HostsQ ? HostFirstLastSeenStrategyResponse : T extends HostsQueries.uncommonProcesses ? HostUncommonProcessesStrategyResponse + : T extends NetworkQueries.details + ? NetworkDetailsStrategyResponse : T extends NetworkQueries.dns ? NetworkDnsStrategyResponse : T extends NetworkQueries.http @@ -99,6 +105,8 @@ export type StrategyResponseType = T extends HostsQ ? NetworkTopCountriesStrategyResponse : T extends NetworkQueries.topNFlow ? NetworkTopNFlowStrategyResponse + : T extends NetworkQueries.users + ? NetworkUsersStrategyResponse : T extends typeof MatrixHistogramQuery ? MatrixHistogramStrategyResponse : never; @@ -115,6 +123,8 @@ export type StrategyRequestType = T extends HostsQu ? HostFirstLastSeenRequestOptions : T extends HostsQueries.uncommonProcesses ? HostUncommonProcessesRequestOptions + : T extends NetworkQueries.details + ? NetworkDetailsRequestOptions : T extends NetworkQueries.dns ? NetworkDnsRequestOptions : T extends NetworkQueries.http @@ -127,6 +137,8 @@ export type StrategyRequestType = T extends HostsQu ? NetworkTopCountriesRequestOptions : T extends NetworkQueries.topNFlow ? NetworkTopNFlowRequestOptions + : T extends NetworkQueries.users + ? NetworkUsersRequestOptions : T extends typeof MatrixHistogramQuery ? MatrixHistogramRequestOptions : never; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts index 66676569b3c9e..19521741c5f66 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/common/index.ts @@ -15,6 +15,13 @@ export enum NetworkTopTablesFields { source_ips = 'source_ips', } +export enum FlowTarget { + client = 'client', + destination = 'destination', + server = 'server', + source = 'source', +} + export enum FlowTargetSourceDest { destination = 'destination', source = 'source', diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/details/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/details/index.ts new file mode 100644 index 0000000000000..920d7cf8c5eaf --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/details/index.ts @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { HostEcs } from '../../../../ecs/host'; +import { GeoEcs } from '../../../../ecs/geo'; +import { Inspect, Maybe, TotalValue, Hit, ShardsResponse } from '../../../common'; +import { RequestBasicOptions } from '../..'; + +export interface NetworkDetailsRequestOptions extends Omit { + ip: string; +} + +export interface NetworkDetailsStrategyResponse extends IEsSearchResponse { + networkDetails: { + client?: Maybe; + destination?: Maybe; + host?: HostEcs; + server?: Maybe; + source?: Maybe; + }; + inspect?: Maybe; +} + +export interface NetworkDetails { + firstSeen?: Maybe; + lastSeen?: Maybe; + autonomousSystem: AutonomousSystem; + geo: GeoEcs; +} + +export interface AutonomousSystem { + number?: Maybe; + organization?: Maybe; +} + +export interface AutonomousSystemOrganization { + name?: Maybe; +} + +interface ResultHit { + doc_count: number; + results: { + hits: { + total: TotalValue | number; + max_score: number | null; + hits: Array<{ + _source: T; + sort?: [number]; + _index?: string; + _type?: string; + _id?: string; + _score?: number | null; + }>; + }; + }; +} + +export interface NetworkHit { + took?: number; + timed_out?: boolean; + _scroll_id?: string; + _shards?: ShardsResponse; + timeout?: number; + hits?: { + total: number; + hits: Hit[]; + }; + doc_count: number; + geo: ResultHit; + autonomousSystem: ResultHit; + firstSeen: { + value: number; + value_as_string: string; + }; + lastSeen: { + value: number; + value_as_string: string; + }; +} + +export type NetworkDetailsHostHit = ResultHit; + +export interface NetworkDetailsHit { + aggregations: { + destination?: NetworkHit; + source?: NetworkHit; + host: ResultHit; + }; + _shards: { + total: number; + successful: number; + skipped: number; + failed: number; + }; + hits: { + total: { + value: number; + relation: string; + }; + max_score: number | null; + hits: []; + }; + took: number; + timeout: number; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http/index.ts index ad58442b16994..cd661cd9b9e9f 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/http/index.ts @@ -8,6 +8,16 @@ import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/comm import { Maybe, CursorType, Inspect, PageInfoPaginated, GenericBuckets } from '../../../common'; import { RequestOptionsPaginated } from '../..'; +export enum NetworkHttpFields { + domains = 'domains', + lastHost = 'lastHost', + lastSourceIp = 'lastSourceIp', + methods = 'methods', + path = 'path', + requestCount = 'requestCount', + statuses = 'statuses', +} + export interface NetworkHttpRequestOptions extends RequestOptionsPaginated { ip?: string; defaultIndex: string[]; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts index d61acbe62ffb0..4e73fe11ef430 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/index.ts @@ -5,18 +5,22 @@ */ export * from './common'; +export * from './details'; export * from './dns'; export * from './http'; export * from './overview'; export * from './tls'; export * from './top_countries'; export * from './top_n_flow'; +export * from './users'; export enum NetworkQueries { + details = 'networkDetails', dns = 'dns', http = 'http', overview = 'overviewNetwork', tls = 'tls', topCountries = 'topCountries', topNFlow = 'topNFlow', + users = 'users', } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts index dffc994fcf4cb..5e1c9459aaac3 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/tls/index.ts @@ -9,7 +9,7 @@ import { CursorType, Inspect, Maybe, PageInfoPaginated } from '../../../common'; import { RequestOptionsPaginated } from '../..'; import { FlowTargetSourceDest } from '../common'; -export interface TlsBuckets { +export interface NetworkTlsBuckets { key: string; timestamp?: { value: number; @@ -29,7 +29,7 @@ export interface TlsBuckets { }; } -export interface TlsNode { +export interface NetworkTlsNode { _id?: Maybe; timestamp?: Maybe; notAfter?: Maybe; @@ -38,23 +38,23 @@ export interface TlsNode { issuers?: Maybe; } -export enum TlsFields { +export enum NetworkTlsFields { _id = '_id', } -export interface TlsEdges { - node: TlsNode; +export interface NetworkTlsEdges { + node: NetworkTlsNode; cursor: CursorType; } -export interface NetworkTlsRequestOptions extends RequestOptionsPaginated { +export interface NetworkTlsRequestOptions extends RequestOptionsPaginated { ip: string; flowTarget: FlowTargetSourceDest; defaultIndex: string[]; } export interface NetworkTlsStrategyResponse extends IEsSearchResponse { - edges: TlsEdges[]; + edges: NetworkTlsEdges[]; totalCount: number; pageInfo: PageInfoPaginated; inspect?: Maybe; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts index a28388a2c6f8f..2c89bbf048e62 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/top_countries/index.ts @@ -14,13 +14,6 @@ import { TopNetworkTablesEcsField, } from '../common'; -export enum FlowTarget { - client = 'client', - destination = 'destination', - server = 'server', - source = 'source', -} - export interface TopCountriesItemSource { country?: Maybe; destination_ips?: Maybe; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/users/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/users/index.ts new file mode 100644 index 0000000000000..196317e7587bf --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/network/users/index.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../src/plugins/data/common'; +import { CursorType, Inspect, Maybe, PageInfoPaginated, SortField } from '../../../common'; +import { FlowTarget } from '../common'; +import { RequestOptionsPaginated } from '../..'; + +export enum NetworkUsersFields { + name = 'name', + count = 'count', +} + +export interface NetworkUsersRequestOptions extends RequestOptionsPaginated { + ip: string; + sort: SortField; + flowTarget: FlowTarget; +} + +export interface NetworkUsersStrategyResponse extends IEsSearchResponse { + edges: NetworkUsersEdges[]; + totalCount: number; + pageInfo: PageInfoPaginated; + inspect?: Maybe; +} + +export interface NetworkUsersEdges { + node: NetworkUsersNode; + cursor: CursorType; +} + +export interface NetworkUsersNode { + _id?: Maybe; + timestamp?: Maybe; + user?: Maybe; +} + +export interface NetworkUsersItem { + name?: Maybe; + id?: Maybe; + groupId?: Maybe; + groupName?: Maybe; + count?: Maybe; +} + +export interface NetworkUsersBucketsItem { + key: string; + doc_count: number; + groupName?: NetworkUsersGroupName; + groupId?: NetworkUsersGroupId; + id?: Id; +} + +export interface NetworkUsersGroupName { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; + buckets: NetworkUsersBucketsItem[]; +} + +export interface NetworkUsersGroupId { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; + buckets: NetworkUsersBucketsItem[]; +} + +interface Id { + doc_count_error_upper_bound: number; + sum_other_doc_count: number; + buckets: NetworkUsersBucketsItem[]; +} diff --git a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts index d55a8faae021d..5b42897b065e3 100644 --- a/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/url_compatibility.spec.ts @@ -18,8 +18,7 @@ const ABSOLUTE_DATE = { startTime: '2019-08-01T20:03:29.186Z', }; -// FLAKY: https://github.com/elastic/kibana/issues/75697 -describe.skip('URL compatibility', () => { +describe('URL compatibility', () => { it('Redirects to Detection alerts from old Detections URL', () => { loginAndWaitForPage(DETECTIONS); diff --git a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts index 79756621ef502..5ec5bb97250db 100644 --- a/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts +++ b/x-pack/plugins/security_solution/cypress/tasks/alerts_detection_rules.ts @@ -80,9 +80,9 @@ export const selectNumberOfRules = (numberOfRules: number) => { }; export const sortByActivatedRules = () => { - cy.get(SORT_RULES_BTN).click({ force: true }); + cy.get(SORT_RULES_BTN).contains('Activated').click({ force: true }); waitForRulesToBeLoaded(); - cy.get(SORT_RULES_BTN).click({ force: true }); + cy.get(SORT_RULES_BTN).contains('Activated').click({ force: true }); waitForRulesToBeLoaded(); }; diff --git a/x-pack/plugins/security_solution/package.json b/x-pack/plugins/security_solution/package.json index 70dbaa0d31681..fd7941fb17cc5 100644 --- a/x-pack/plugins/security_solution/package.json +++ b/x-pack/plugins/security_solution/package.json @@ -9,7 +9,7 @@ "build-graphql-types": "node scripts/generate_types_from_graphql.js", "cypress:open": "cypress open --config-file ./cypress/cypress.json", "cypress:open-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/visual_config.ts", - "cypress:run": "cypress run --browser chrome --headless --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge --reportDir ../../../target/kibana-security-solution/cypress/results > ../../../target/kibana-security-solution/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/ && exit $status;", + "cypress:run": "cypress run --browser chrome --headless --spec ./cypress/integration/**/*.spec.ts --config-file ./cypress/cypress.json --reporter ../../node_modules/cypress-multi-reporters --reporter-options configFile=./cypress/reporter_config.json; status=$?; ../../node_modules/.bin/mochawesome-merge ../../../target/kibana-security-solution/cypress/results/mochawesome*.json > ../../../target/kibana-security-solution/cypress/results/output.json; ../../../node_modules/.bin/marge ../../../target/kibana-security-solution/cypress/results/output.json --reportDir ../../../target/kibana-security-solution/cypress/results; mkdir -p ../../../target/junit && cp ../../../target/kibana-security-solution/cypress/results/*.xml ../../../target/junit/ && exit $status;", "cypress:run-as-ci": "node ../../../scripts/functional_tests --config ../../test/security_solution_cypress/cli_config.ts", "test:generate": "node scripts/endpoint/resolver_generator" }, diff --git a/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts b/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts index 9b9e978ffca4b..2fc761f4dc429 100644 --- a/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts +++ b/x-pack/plugins/security_solution/public/cases/containers/configure/mock.ts @@ -77,7 +77,7 @@ export const connectorsMock: Connector[] = [ name: 'Jira', config: { apiUrl: 'https://instance.atlassian.ne', - casesConfiguration: { + incidentConfiguration: { mapping: [ { source: 'title', diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/__mocks__/index.ts b/x-pack/plugins/security_solution/public/common/components/link_to/__mocks__/index.ts index 6c9620e27fabf..07855c3477106 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/__mocks__/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/link_to/__mocks__/index.ts @@ -9,7 +9,7 @@ import { SecurityPageName } from '../../../../app/types'; export { getDetectionEngineUrl } from '../redirect_to_detection_engine'; export { getAppOverviewUrl } from '../redirect_to_overview'; export { getHostDetailsUrl, getHostsUrl } from '../redirect_to_hosts'; -export { getNetworkUrl, getIPDetailsUrl } from '../redirect_to_network'; +export { getNetworkUrl, getNetworkDetailsUrl } from '../redirect_to_network'; export { getTimelinesUrl, getTimelineTabsUrl } from '../redirect_to_timelines'; export { getCaseDetailsUrl, diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts index c6e58d4206958..403c8d838fa44 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/link_to/index.ts @@ -15,7 +15,7 @@ import { useKibana } from '../../lib/kibana'; export { getDetectionEngineUrl } from './redirect_to_detection_engine'; export { getAppOverviewUrl } from './redirect_to_overview'; export { getHostDetailsUrl, getHostsUrl } from './redirect_to_hosts'; -export { getNetworkUrl, getIPDetailsUrl } from './redirect_to_network'; +export { getNetworkUrl, getNetworkDetailsUrl } from './redirect_to_network'; export { getTimelinesUrl, getTimelineTabsUrl } from './redirect_to_timelines'; export { getCaseDetailsUrl, diff --git a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx index 100c5e46141a2..c042c8e1470b4 100644 --- a/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx +++ b/x-pack/plugins/security_solution/public/common/components/link_to/redirect_to_network.tsx @@ -13,7 +13,7 @@ import { appendSearch } from './helpers'; export const getNetworkUrl = (search?: string) => `${appendSearch(search)}`; -export const getIPDetailsUrl = ( +export const getNetworkDetailsUrl = ( detailName: string, flowTarget?: FlowTarget | FlowTargetSourceDest, search?: string diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx index b6817c4cab1f2..e6d34b5e432ac 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.test.tsx @@ -14,7 +14,7 @@ import { useUiSetting$ } from '../../lib/kibana'; import { GoogleLink, HostDetailsLink, - IPDetailsLink, + NetworkDetailsLink, ReputationLink, WhoIsLink, CertificateFingerprintLink, @@ -61,9 +61,9 @@ describe('Custom Links', () => { }); }); - describe('IPDetailsLink', () => { + describe('NetworkDetailsLink', () => { test('should render valid link to IP Details with ipv4 as the display text', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('EuiLink').prop('href')).toEqual( `/ip/${encodeURIComponent(ipv4)}/source` ); @@ -71,7 +71,7 @@ describe('Custom Links', () => { }); test('should render valid link to IP Details with child text as the display text', () => { - const wrapper = mount({hostName}); + const wrapper = mount({hostName}); expect(wrapper.find('EuiLink').prop('href')).toEqual( `/ip/${encodeURIComponent(ipv4)}/source` ); @@ -79,7 +79,7 @@ describe('Custom Links', () => { }); test('should render valid link to IP Details with ipv6 as the display text', () => { - const wrapper = mount(); + const wrapper = mount(); expect(wrapper.find('EuiLink').prop('href')).toEqual( `/ip/${encodeURIComponent(ipv6Encoded)}/source` ); diff --git a/x-pack/plugins/security_solution/public/common/components/links/index.tsx b/x-pack/plugins/security_solution/public/common/components/links/index.tsx index 943f2d8336ca7..d6cbd31e86ddb 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/links/index.tsx @@ -28,7 +28,7 @@ import { encodeIpv6 } from '../../lib/helpers'; import { getCaseDetailsUrl, getHostDetailsUrl, - getIPDetailsUrl, + getNetworkDetailsUrl, getCreateCaseUrl, useFormatUrl, } from '../link_to'; @@ -114,7 +114,7 @@ export const ExternalLink = React.memo<{ ExternalLink.displayName = 'ExternalLink'; -const IPDetailsLinkComponent: React.FC<{ +const NetworkDetailsLinkComponent: React.FC<{ children?: React.ReactNode; ip: string; flowTarget?: FlowTarget | FlowTargetSourceDest; @@ -125,7 +125,7 @@ const IPDetailsLinkComponent: React.FC<{ (ev) => { ev.preventDefault(); navigateToApp(`${APP_ID}:${SecurityPageName.network}`, { - path: getIPDetailsUrl(encodeURIComponent(encodeIpv6(ip)), flowTarget, search), + path: getNetworkDetailsUrl(encodeURIComponent(encodeIpv6(ip)), flowTarget, search), }); }, [flowTarget, ip, navigateToApp, search] @@ -134,14 +134,14 @@ const IPDetailsLinkComponent: React.FC<{ return ( {children ? children : ip} ); }; -export const IPDetailsLink = React.memo(IPDetailsLinkComponent); +export const NetworkDetailsLink = React.memo(NetworkDetailsLinkComponent); const CaseDetailsLinkComponent: React.FC<{ children?: React.ReactNode; diff --git a/x-pack/plugins/security_solution/public/common/components/links/translations.ts b/x-pack/plugins/security_solution/public/common/components/links/translations.ts index cf7db8bfa8161..014acdf3fd557 100644 --- a/x-pack/plugins/security_solution/public/common/components/links/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/links/translations.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -export * from '../../../network/components/ip_overview/translations'; +export * from '../../../network/components/details/translations'; export const CASE_DETAILS_LINK_ARIA = (detailName: string) => i18n.translate('xpack.securitySolution.case.caseTable.caseDetailsLinkAria', { diff --git a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx index 52b26a20a8f64..3dd408e5aa822 100644 --- a/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx +++ b/x-pack/plugins/security_solution/public/common/components/ml/tables/get_anomalies_network_table_columns.tsx @@ -14,7 +14,7 @@ import { Anomaly, AnomaliesByNetwork } from '../types'; import { getRowItemDraggable } from '../../tables/helpers'; import { EntityDraggable } from '../entity_draggable'; import { createCompoundNetworkKey } from './create_compound_key'; -import { IPDetailsLink } from '../../links'; +import { NetworkDetailsLink } from '../../links'; import * as i18n from './translations'; import { getEntries } from '../get_entries'; @@ -46,7 +46,7 @@ export const getAnomaliesNetworkTableColumns = ( rowItem: ip, attrName: anomaliesByNetwork.type, idPrefix: `anomalies-network-table-ip-${createCompoundNetworkKey(anomaliesByNetwork)}`, - render: (item) => , + render: (item) => , }), }, { diff --git a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts index a10e4cf568dd1..2964572cb7cf3 100644 --- a/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts +++ b/x-pack/plugins/security_solution/public/common/components/navigation/breadcrumbs/index.ts @@ -10,7 +10,7 @@ import { ChromeBreadcrumb } from '../../../../../../../../src/core/public'; import { APP_NAME } from '../../../../../common/constants'; import { StartServices } from '../../../../types'; import { getBreadcrumbs as getHostDetailsBreadcrumbs } from '../../../../hosts/pages/details/utils'; -import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/pages/ip_details'; +import { getBreadcrumbs as getIPDetailsBreadcrumbs } from '../../../../network/pages/details'; import { getBreadcrumbs as getCaseDetailsBreadcrumbs } from '../../../../cases/pages/utils'; import { getBreadcrumbs as getDetectionRulesBreadcrumbs } from '../../../../detections/pages/detection_engine/rules/utils'; import { getBreadcrumbs as getTimelinesBreadcrumbs } from '../../../../timelines/pages'; diff --git a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx index 448fa58443407..f7b69c4fc8ed3 100644 --- a/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx +++ b/x-pack/plugins/security_solution/public/common/components/paginated_table/index.tsx @@ -16,12 +16,12 @@ import { EuiLoadingContent, EuiPagination, EuiPopover, - Direction, } from '@elastic/eui'; import { noop } from 'lodash/fp'; import React, { FC, memo, useState, useEffect, ComponentType } from 'react'; import styled from 'styled-components'; +import { Direction } from '../../../../common/search_strategy'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../common/constants'; import { AuthTableColumns } from '../../../hosts/components/authentications_table'; import { HostsTableColumns } from '../../../hosts/components/hosts_table'; @@ -29,11 +29,11 @@ import { NetworkDnsColumns } from '../../../network/components/network_dns_table import { NetworkHttpColumns } from '../../../network/components/network_http_table/columns'; import { NetworkTopNFlowColumns, - NetworkTopNFlowColumnsIpDetails, + NetworkTopNFlowColumnsNetworkDetails, } from '../../../network/components/network_top_n_flow_table/columns'; import { NetworkTopCountriesColumns, - NetworkTopCountriesColumnsIpDetails, + NetworkTopCountriesColumnsNetworkDetails, } from '../../../network/components/network_top_countries_table/columns'; import { TlsColumns } from '../../../network/components/tls_table/columns'; import { UncommonProcessTableColumns } from '../../../hosts/components/uncommon_process_table'; @@ -78,9 +78,9 @@ declare type BasicTableColumns = | NetworkDnsColumns | NetworkHttpColumns | NetworkTopCountriesColumns - | NetworkTopCountriesColumnsIpDetails + | NetworkTopCountriesColumnsNetworkDetails | NetworkTopNFlowColumns - | NetworkTopNFlowColumnsIpDetails + | NetworkTopNFlowColumnsNetworkDetails | TlsColumns | UncommonProcessTableColumns | UsersColumns; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/components/connector_flyout/index.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/components/connector_flyout/index.tsx deleted file mode 100644 index 30e2c650a70cc..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/components/connector_flyout/index.tsx +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useCallback, useEffect } from 'react'; -import { EuiFieldText, EuiFlexGroup, EuiFlexItem, EuiFormRow, EuiSpacer } from '@elastic/eui'; - -import { isEmpty, get } from 'lodash/fp'; - -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ActionConnectorFieldsProps } from '../../../../../../../triggers_actions_ui/public/types'; -import { FieldMapping } from '../../../../../cases/components/configure_cases/field_mapping'; - -import { CasesConfigurationMapping } from '../../../../../cases/containers/configure/types'; - -import * as i18n from '../../translations'; -import { ActionConnector, ConnectorFlyoutHOCProps } from '../../types'; -import { createDefaultMapping } from '../../utils'; -import { connectorsConfiguration } from '../../config'; - -export const withConnectorFlyout = ({ - ConnectorFormComponent, - connectorActionTypeId, - secretKeys = [], - configKeys = [], -}: ConnectorFlyoutHOCProps) => { - const ConnectorFlyout: React.FC> = ({ - action, - editActionConfig, - editActionSecrets, - errors, - }) => { - /* We do not provide defaults values to the fields (like empty string for apiUrl) intentionally. - * If we do, errors will be shown the first time the flyout is open even though the user did not - * interact with the form. Also, we would like to show errors for empty fields provided by the user. - /*/ - const { apiUrl, casesConfiguration: { mapping = [] } = {} } = action.config; - const configKeysWithDefault = [...configKeys, 'apiUrl']; - - const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; - - /** - * We need to distinguish between the add flyout and the edit flyout. - * useEffect will run only once on component mount. - * This guarantees that the function below will run only once. - * On the first render of the component the apiUrl can be either undefined or filled. - * If it is filled then we are on the edit flyout. Otherwise we are on the add flyout. - */ - - useEffect(() => { - if (!isEmpty(apiUrl)) { - secretKeys.forEach((key: string) => editActionSecrets(key, '')); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - if (isEmpty(mapping)) { - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: createDefaultMapping(connectorsConfiguration[connectorActionTypeId].fields), - }); - } - - const handleOnChangeActionConfig = useCallback( - (key: string, value: string) => editActionConfig(key, value), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const handleOnBlurActionConfig = useCallback( - (key: string) => { - if (configKeysWithDefault.includes(key) && get(key, action.config) == null) { - editActionConfig(key, ''); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [action.config] - ); - - const handleOnChangeSecretConfig = useCallback( - (key: string, value: string) => editActionSecrets(key, value), - // eslint-disable-next-line react-hooks/exhaustive-deps - [] - ); - - const handleOnBlurSecretConfig = useCallback( - (key: string) => { - if (secretKeys.includes(key) && get(key, action.secrets) == null) { - editActionSecrets(key, ''); - } - }, - // eslint-disable-next-line react-hooks/exhaustive-deps - [action.secrets] - ); - - const handleOnChangeMappingConfig = useCallback( - (newMapping: CasesConfigurationMapping[]) => - editActionConfig('casesConfiguration', { - ...action.config.casesConfiguration, - mapping: newMapping, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [action.config] - ); - - return ( - <> - - - - handleOnChangeActionConfig('apiUrl', evt.target.value)} - onBlur={handleOnBlurActionConfig.bind(null, 'apiUrl')} - /> - - - - - - - - - - - - - ); - }; - - return ConnectorFlyout; -}; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts index 833f85712b5fa..3aca186378820 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/config.ts @@ -4,14 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { ServiceNowConnectorConfiguration } from '../../../../../triggers_actions_ui/public/common'; -import { connector as jiraConnectorConfig } from './jira/config'; -import { connector as resilientConnectorConfig } from './resilient/config'; +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { + ServiceNowConnectorConfiguration, + JiraConnectorConfiguration, + ResilientConnectorConfiguration, +} from '../../../../../triggers_actions_ui/public/common'; import { ConnectorConfiguration } from './types'; export const connectorsConfiguration: Record = { '.servicenow': ServiceNowConnectorConfiguration as ConnectorConfiguration, - '.jira': jiraConnectorConfig, - '.resilient': resilientConnectorConfig, + '.jira': JiraConnectorConfiguration as ConnectorConfiguration, + '.resilient': ResilientConnectorConfiguration as ConnectorConfiguration, }; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx deleted file mode 100644 index 0737db3cd08eb..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/flyout.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldPassword, - EuiSpacer, -} from '@elastic/eui'; - -import * as i18n from './translations'; -import { ConnectorFlyoutFormProps } from '../types'; -import { JiraActionConnector } from './types'; -import { withConnectorFlyout } from '../components/connector_flyout'; - -const JiraConnectorForm: React.FC> = ({ - errors, - action, - onChangeSecret, - onBlurSecret, - onChangeConfig, - onBlurConfig, -}) => { - const { projectKey } = action.config; - const { email, apiToken } = action.secrets; - const isProjectKeyInvalid: boolean = errors.projectKey.length > 0 && projectKey != null; - const isEmailInvalid: boolean = errors.email.length > 0 && email != null; - const isApiTokenInvalid: boolean = errors.apiToken.length > 0 && apiToken != null; - - return ( - <> - - - - onChangeConfig('projectKey', evt.target.value)} - onBlur={() => onBlurConfig('projectKey')} - /> - - - - - - - - onChangeSecret('email', evt.target.value)} - onBlur={() => onBlurSecret('email')} - /> - - - - - - - - onChangeSecret('apiToken', evt.target.value)} - onBlur={() => onBlurSecret('apiToken')} - /> - - - - - ); -}; - -export const JiraConnectorFlyout = withConnectorFlyout({ - ConnectorFormComponent: JiraConnectorForm, - secretKeys: ['email', 'apiToken'], - configKeys: ['projectKey'], - connectorActionTypeId: '.jira', -}); - -// eslint-disable-next-line import/no-default-export -export { JiraConnectorFlyout as default }; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/index.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/index.tsx deleted file mode 100644 index cead392010dc7..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { lazy } from 'react'; -import { - ValidationResult, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../triggers_actions_ui/public/types'; - -import { connector } from './config'; -import { createActionType } from '../utils'; -import logo from './logo.svg'; -import { JiraActionConnector } from './types'; -import * as i18n from './translations'; - -interface Errors { - projectKey: string[]; - email: string[]; - apiToken: string[]; -} - -const validateConnector = (action: JiraActionConnector): ValidationResult => { - const errors: Errors = { - projectKey: [], - email: [], - apiToken: [], - }; - - if (!action.config.projectKey) { - errors.projectKey = [...errors.projectKey, i18n.JIRA_PROJECT_KEY_REQUIRED]; - } - - if (!action.secrets.email) { - errors.email = [...errors.email, i18n.JIRA_EMAIL_REQUIRED]; - } - - if (!action.secrets.apiToken) { - errors.apiToken = [...errors.apiToken, i18n.JIRA_API_TOKEN_REQUIRED]; - } - - return { errors }; -}; - -export const getActionType = createActionType({ - id: connector.id, - iconClass: logo, - selectMessage: i18n.JIRA_DESC, - actionTypeTitle: connector.name, - validateConnector, - actionConnectorFields: lazy(() => import('./flyout')), -}); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts deleted file mode 100644 index d7abf77a58d4c..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/translations.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export * from '../translations'; - -export const JIRA_DESC = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.selectMessageText', - { - defaultMessage: 'Push or update Security case data to a new issue in Jira', - } -); - -export const JIRA_TITLE = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.actionTypeTitle', - { - defaultMessage: 'Jira', - } -); - -export const JIRA_PROJECT_KEY_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.projectKey', - { - defaultMessage: 'Project key', - } -); - -export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.requiredProjectKeyTextField', - { - defaultMessage: 'Project key is required', - } -); - -export const JIRA_EMAIL_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.emailTextFieldLabel', - { - defaultMessage: 'Email or Username', - } -); - -export const JIRA_EMAIL_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.requiredEmailTextField', - { - defaultMessage: 'Email or Username is required', - } -); - -export const JIRA_API_TOKEN_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.apiTokenTextFieldLabel', - { - defaultMessage: 'API token or Password', - } -); - -export const JIRA_API_TOKEN_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.jira.requiredApiTokenTextField', - { - defaultMessage: 'API token or Password is required', - } -); - -export const MAPPING_FIELD_SUMMARY = i18n.translate( - 'xpack.securitySolution.case.configureCases.mappingFieldSummary', - { - defaultMessage: 'Summary', - } -); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/types.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/jira/types.ts deleted file mode 100644 index fafb4a0d41fb3..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable no-restricted-imports */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - -import { - JiraPublicConfigurationType, - JiraSecretConfigurationType, -} from '../../../../../../actions/server/builtin_action_types/jira/types'; - -export { JiraFieldsType } from '../../../../../../case/common/api/connectors'; - -export * from '../types'; - -export interface JiraActionConnector { - config: JiraPublicConfigurationType; - secrets: JiraSecretConfigurationType; -} diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/flyout.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/flyout.tsx deleted file mode 100644 index 31bf0a4dfc34b..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/flyout.tsx +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { - EuiFieldText, - EuiFlexGroup, - EuiFlexItem, - EuiFormRow, - EuiFieldPassword, - EuiSpacer, -} from '@elastic/eui'; - -import * as i18n from './translations'; -import { ConnectorFlyoutFormProps } from '../types'; -import { ResilientActionConnector } from './types'; -import { withConnectorFlyout } from '../components/connector_flyout'; - -const resilientConnectorForm: React.FC> = ({ - errors, - action, - onChangeSecret, - onBlurSecret, - onChangeConfig, - onBlurConfig, -}) => { - const { orgId } = action.config; - const { apiKeyId, apiKeySecret } = action.secrets; - const isOrgIdInvalid: boolean = errors.orgId.length > 0 && orgId != null; - const isApiKeyIdInvalid: boolean = errors.apiKeyId.length > 0 && apiKeyId != null; - const isApiKeySecretInvalid: boolean = errors.apiKeySecret.length > 0 && apiKeySecret != null; - - return ( - <> - - - - onChangeConfig('orgId', evt.target.value)} - onBlur={() => onBlurConfig('orgId')} - /> - - - - - - - - onChangeSecret('apiKeyId', evt.target.value)} - onBlur={() => onBlurSecret('apiKeyId')} - /> - - - - - - - - onChangeSecret('apiKeySecret', evt.target.value)} - onBlur={() => onBlurSecret('apiKeySecret')} - /> - - - - - ); -}; - -export const resilientConnectorFlyout = withConnectorFlyout({ - ConnectorFormComponent: resilientConnectorForm, - secretKeys: ['apiKeyId', 'apiKeySecret'], - configKeys: ['orgId'], - connectorActionTypeId: '.resilient', -}); - -// eslint-disable-next-line import/no-default-export -export { resilientConnectorFlyout as default }; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx deleted file mode 100644 index ba4879e87a1f6..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/index.tsx +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { lazy } from 'react'; -import { - ValidationResult, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../../triggers_actions_ui/public/types'; - -import { connector } from './config'; -import { createActionType } from '../utils'; -import logo from './logo.svg'; -import { ResilientActionConnector } from './types'; -import * as i18n from './translations'; - -interface Errors { - orgId: string[]; - apiKeyId: string[]; - apiKeySecret: string[]; -} - -const validateConnector = (action: ResilientActionConnector): ValidationResult => { - const errors: Errors = { - orgId: [], - apiKeyId: [], - apiKeySecret: [], - }; - - if (!action.config.orgId) { - errors.orgId = [...errors.orgId, i18n.RESILIENT_PROJECT_KEY_REQUIRED]; - } - - if (!action.secrets.apiKeyId) { - errors.apiKeyId = [...errors.apiKeyId, i18n.RESILIENT_API_KEY_ID_REQUIRED]; - } - - if (!action.secrets.apiKeySecret) { - errors.apiKeySecret = [...errors.apiKeySecret, i18n.RESILIENT_API_KEY_SECRET_REQUIRED]; - } - - return { errors }; -}; - -export const getActionType = createActionType({ - id: connector.id, - iconClass: logo, - selectMessage: i18n.RESILIENT_DESC, - actionTypeTitle: connector.name, - validateConnector, - actionConnectorFields: lazy(() => import('./flyout')), -}); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts deleted file mode 100644 index 2ff97ad354095..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/translations.ts +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export * from '../translations'; - -export const RESILIENT_DESC = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.selectMessageText', - { - defaultMessage: 'Push or update Security case data to a new issue in Resilient', - } -); - -export const RESILIENT_TITLE = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.actionTypeTitle', - { - defaultMessage: 'IBM Resilient', - } -); - -export const RESILIENT_PROJECT_KEY_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.orgId', - { - defaultMessage: 'Organization ID', - } -); - -export const RESILIENT_PROJECT_KEY_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField', - { - defaultMessage: 'Organization ID is required', - } -); - -export const RESILIENT_API_KEY_ID_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.apiKeyId', - { - defaultMessage: 'API key ID', - } -); - -export const RESILIENT_API_KEY_ID_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.requiredApiKeyIdTextField', - { - defaultMessage: 'API key ID is required', - } -); - -export const RESILIENT_API_KEY_SECRET_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.apiKeySecret', - { - defaultMessage: 'API key secret', - } -); - -export const RESILIENT_API_KEY_SECRET_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.resilient.requiredApiKeySecretTextField', - { - defaultMessage: 'API key secret is required', - } -); - -export const MAPPING_FIELD_NAME = i18n.translate( - 'xpack.securitySolution.case.configureCases.mappingFieldName', - { - defaultMessage: 'Name', - } -); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/types.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/types.ts deleted file mode 100644 index fe6dbb2b3674a..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/types.ts +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -/* eslint-disable no-restricted-imports */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - -import { - ResilientPublicConfigurationType, - ResilientSecretConfigurationType, -} from '../../../../../../actions/server/builtin_action_types/resilient/types'; - -export { ResilientFieldsType } from '../../../../../../case/common/api/connectors'; - -export * from '../types'; - -export interface ResilientActionConnector { - config: ResilientPublicConfigurationType; - secrets: ResilientSecretConfigurationType; -} diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/translations.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/translations.ts deleted file mode 100644 index 6dd1247d40fcb..0000000000000 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/translations.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; - -export const API_URL_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.common.apiUrlTextFieldLabel', - { - defaultMessage: 'URL', - } -); - -export const API_URL_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.common.requiredApiUrlTextField', - { - defaultMessage: 'URL is required', - } -); - -export const API_URL_INVALID = i18n.translate( - 'xpack.securitySolution.case.connectors.common.invalidApiUrlTextField', - { - defaultMessage: 'URL is invalid', - } -); - -export const USERNAME_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.common.usernameTextFieldLabel', - { - defaultMessage: 'Username', - } -); - -export const USERNAME_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.common.requiredUsernameTextField', - { - defaultMessage: 'Username is required', - } -); - -export const PASSWORD_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.common.passwordTextFieldLabel', - { - defaultMessage: 'Password', - } -); - -export const PASSWORD_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.common.requiredPasswordTextField', - { - defaultMessage: 'Password is required', - } -); - -export const API_TOKEN_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.common.apiTokenTextFieldLabel', - { - defaultMessage: 'API token', - } -); - -export const API_TOKEN_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.common.requiredApiTokenTextField', - { - defaultMessage: 'API token is required', - } -); - -export const EMAIL_LABEL = i18n.translate( - 'xpack.securitySolution.case.connectors.common.emailTextFieldLabel', - { - defaultMessage: 'Email', - } -); - -export const EMAIL_REQUIRED = i18n.translate( - 'xpack.securitySolution.case.connectors.common.requiredEmailTextField', - { - defaultMessage: 'Email is required', - } -); - -export const MAPPING_FIELD_DESC = i18n.translate( - 'xpack.securitySolution.case.configureCases.mappingFieldDescription', - { - defaultMessage: 'Description', - } -); - -export const MAPPING_FIELD_COMMENTS = i18n.translate( - 'xpack.securitySolution.case.configureCases.mappingFieldComments', - { - defaultMessage: 'Comments', - } -); diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/types.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/types.ts index 1d688ad9b1d6a..5d83c226bfeca 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/types.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/types.ts @@ -4,12 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable no-restricted-imports */ -/* eslint-disable @kbn/eslint/no-restricted-paths */ - import { ActionType } from '../../../../../triggers_actions_ui/public'; -import { IErrorObject } from '../../../../../triggers_actions_ui/public/types'; -import { ExternalIncidentServiceConfiguration } from '../../../../../actions/server/builtin_action_types/case/types'; import { ActionType as ThirdPartySupportedActions, @@ -29,34 +24,3 @@ export interface ConnectorConfiguration extends ActionType { logo: string; fields: Record; } - -export interface ActionConnector { - config: ExternalIncidentServiceConfiguration; - secrets: {}; -} - -export interface ActionConnectorParams { - message: string; -} - -export interface ActionConnectorValidationErrors { - apiUrl: string[]; -} - -export type Optional = Omit & Partial; - -export interface ConnectorFlyoutFormProps { - errors: IErrorObject; - action: T; - onChangeSecret: (key: string, value: string) => void; - onBlurSecret: (key: string) => void; - onChangeConfig: (key: string, value: string) => void; - onBlurConfig: (key: string) => void; -} - -export interface ConnectorFlyoutHOCProps { - ConnectorFormComponent: React.FC>; - connectorActionTypeId: string; - configKeys?: string[]; - secretKeys?: string[]; -} diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/utils.ts b/x-pack/plugins/security_solution/public/common/lib/connectors/utils.ts index 6e72205c145a2..0a6dd37d9f9e2 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/utils.ts +++ b/x-pack/plugins/security_solution/public/common/lib/connectors/utils.ts @@ -4,63 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - ActionTypeModel, - ValidationResult, - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -} from '../../../../../triggers_actions_ui/public/types'; - -import { - ActionConnector, - ActionConnectorParams, - ActionConnectorValidationErrors, - Optional, - ThirdPartyField, -} from './types'; -import { isUrlInvalid } from './validators'; - -import * as i18n from './translations'; import { CasesConfigurationMapping } from '../../../cases/containers/configure/types'; -export const createActionType = ({ - id, - actionTypeTitle, - selectMessage, - iconClass, - validateConnector, - validateParams = connectorParamsValidator, - actionConnectorFields, - actionParamsFields = null, -}: Optional) => (): ActionTypeModel => { - return { - id, - iconClass, - selectMessage, - actionTypeTitle, - validateConnector: (action: ActionConnector): ValidationResult => { - const errors: ActionConnectorValidationErrors = { - apiUrl: [], - }; - - if (!action.config.apiUrl) { - errors.apiUrl = [...errors.apiUrl, i18n.API_URL_REQUIRED]; - } - - if (isUrlInvalid(action.config.apiUrl)) { - errors.apiUrl = [...errors.apiUrl, i18n.API_URL_INVALID]; - } - - return { errors: { ...errors, ...validateConnector(action).errors } }; - }, - validateParams, - actionConnectorFields, - actionParamsFields, - }; -}; - -const connectorParamsValidator = (actionParams: ActionConnectorParams): ValidationResult => { - return { errors: {} }; -}; +import { ThirdPartyField } from './types'; export const createDefaultMapping = ( fields: Record diff --git a/x-pack/plugins/security_solution/public/common/mock/global_state.ts b/x-pack/plugins/security_solution/public/common/mock/global_state.ts index 2849e8ffabd36..a74c9a6d2009d 100644 --- a/x-pack/plugins/security_solution/public/common/mock/global_state.ts +++ b/x-pack/plugins/security_solution/public/common/mock/global_state.ts @@ -11,9 +11,9 @@ import { HostsFields, NetworkDnsFields, NetworkTopTablesFields, - TlsFields, - UsersFields, -} from '../../graphql/types'; + NetworkTlsFields, + NetworkUsersFields, +} from '../../../common/search_strategy'; import { State } from '../store'; import { defaultHeaders } from './header'; @@ -100,7 +100,7 @@ export const mockGlobalState: State = { [networkModel.NetworkTableType.tls]: { activePage: 0, limit: 10, - sort: { field: TlsFields._id, direction: Direction.desc }, + sort: { field: NetworkTlsFields._id, direction: Direction.desc }, }, [networkModel.NetworkTableType.http]: { activePage: 0, @@ -116,37 +116,37 @@ export const mockGlobalState: State = { details: { flowTarget: FlowTarget.source, queries: { - [networkModel.IpDetailsTableType.topCountriesDestination]: { + [networkModel.NetworkDetailsTableType.topCountriesDestination]: { activePage: 0, limit: 10, sort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, }, - [networkModel.IpDetailsTableType.topCountriesSource]: { + [networkModel.NetworkDetailsTableType.topCountriesSource]: { activePage: 0, limit: 10, sort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, }, - [networkModel.IpDetailsTableType.topNFlowSource]: { + [networkModel.NetworkDetailsTableType.topNFlowSource]: { activePage: 0, limit: 10, sort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, }, - [networkModel.IpDetailsTableType.topNFlowDestination]: { + [networkModel.NetworkDetailsTableType.topNFlowDestination]: { activePage: 0, limit: 10, sort: { field: NetworkTopTablesFields.bytes_out, direction: Direction.desc }, }, - [networkModel.IpDetailsTableType.tls]: { + [networkModel.NetworkDetailsTableType.tls]: { activePage: 0, limit: 10, - sort: { field: TlsFields._id, direction: Direction.desc }, + sort: { field: NetworkTlsFields._id, direction: Direction.desc }, }, - [networkModel.IpDetailsTableType.users]: { + [networkModel.NetworkDetailsTableType.users]: { activePage: 0, limit: 10, - sort: { field: UsersFields.name, direction: Direction.asc }, + sort: { field: NetworkUsersFields.name, direction: Direction.asc }, }, - [networkModel.IpDetailsTableType.http]: { + [networkModel.NetworkDetailsTableType.http]: { activePage: 0, limit: 10, sort: { direction: Direction.desc }, diff --git a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx index c7810af13eb74..5a4eaad72ac32 100644 --- a/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/security_solution/public/common/mock/test_providers.tsx @@ -95,6 +95,7 @@ export const useFormFieldMock = (options?: Partial): FieldHook => { clearErrors: jest.fn(), validate: jest.fn(), reset: jest.fn(), + __isIncludedInOutput: true, __serializeValue: jest.fn(), ...options, }; diff --git a/x-pack/plugins/security_solution/public/common/store/actions.ts b/x-pack/plugins/security_solution/public/common/store/actions.ts index cd8836e38bfef..6b446ab6692d9 100644 --- a/x-pack/plugins/security_solution/public/common/store/actions.ts +++ b/x-pack/plugins/security_solution/public/common/store/actions.ts @@ -7,10 +7,16 @@ import { EndpointAction } from '../../management/pages/endpoint_hosts/store/action'; import { PolicyListAction } from '../../management/pages/policy/store/policy_list'; import { PolicyDetailsAction } from '../../management/pages/policy/store/policy_details'; +import { TrustedAppsPageAction } from '../../management/pages/trusted_apps/store/action'; export { appActions } from './app'; export { dragAndDropActions } from './drag_and_drop'; export { inputsActions } from './inputs'; import { RoutingAction } from './routing'; -export type AppAction = EndpointAction | RoutingAction | PolicyListAction | PolicyDetailsAction; +export type AppAction = + | EndpointAction + | RoutingAction + | PolicyListAction + | PolicyDetailsAction + | TrustedAppsPageAction; diff --git a/x-pack/plugins/security_solution/public/common/store/routing/action.ts b/x-pack/plugins/security_solution/public/common/store/routing/action.ts index ae5e4eb32d476..d0cc38970ca21 100644 --- a/x-pack/plugins/security_solution/public/common/store/routing/action.ts +++ b/x-pack/plugins/security_solution/public/common/store/routing/action.ts @@ -6,7 +6,7 @@ import { AppLocation, Immutable } from '../../../../common/endpoint/types'; -interface UserChangedUrl { +export interface UserChangedUrl { readonly type: 'userChangedUrl'; readonly payload: Immutable; } diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.tsx index 8fd3f648bc812..bfb23ff6af6a0 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/all_rules_tables/index.tsx @@ -20,7 +20,7 @@ import { RulesColumns, RuleStatusRowItemType, } from '../../../pages/detection_engine/rules/all/columns'; -import { Rule, Rules } from '../../../containers/detection_engine/rules/types'; +import { Rule, Rules, RulesSortingFields } from '../../../containers/detection_engine/rules/types'; import { AllRulesTabs } from '../../../pages/detection_engine/rules/all'; // EuiBasicTable give me a hardtime with adding the ref attributes so I went the easy way @@ -30,7 +30,7 @@ const MyEuiBasicTable = styled(EuiBasicTable as any)`` as any; export interface SortingType { sort: { - field: 'enabled'; + field: RulesSortingFields; direction: Direction; }; } @@ -48,12 +48,7 @@ interface AllRulesTablesProps { rules: Rules; rulesColumns: RulesColumns[]; rulesStatuses: RuleStatusRowItemType[]; - sorting: { - sort: { - field: 'enabled'; - direction: Direction; - }; - }; + sorting: SortingType; tableOnChangeCallback: ({ page, sort }: EuiBasicTableOnChange) => void; tableRef?: React.MutableRefObject; selectedTab: AllRulesTabs; diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap index 1ed55774f935f..4d21a983c9707 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/detections/components/rules/rule_actions_overflow/__snapshots__/index.test.tsx.snap @@ -40,7 +40,7 @@ exports[`RuleActionsOverflow snapshots renders correctly against snapshot 1`] = icon="copy" onClick={[Function]} > - Duplicate rule… + Duplicate rule , - Delete rule… + Delete rule , ] } diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts index cd1ded544cfe5..2a15cf7b95ceb 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.test.ts @@ -202,7 +202,7 @@ describe('Detections Rules API', () => { expect(fetchMock).toHaveBeenCalledWith('/api/detection_engine/rules/_find', { method: 'GET', query: { - filter: 'alert.attributes.tags: "hello" AND alert.attributes.tags: "world"', + filter: 'alert.attributes.tags: "hello" OR alert.attributes.tags: "world"', page: 1, per_page: 20, sort_field: 'enabled', @@ -297,7 +297,7 @@ describe('Detections Rules API', () => { method: 'GET', query: { filter: - 'alert.attributes.name: ruleName AND alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND alert.attributes.tags: "hello" AND alert.attributes.tags: "world"', + 'alert.attributes.name: ruleName AND alert.attributes.tags: "__internal_immutable:false" AND alert.attributes.tags: "__internal_immutable:true" AND (alert.attributes.tags: "hello" OR alert.attributes.tags: "world")', page: 1, per_page: 20, sort_field: 'enabled', diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts index e254516d11076..b66154fbb57d2 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/api.ts @@ -107,7 +107,7 @@ export const fetchRules = async ({ }, signal, }: FetchRulesProps): Promise => { - const filters = [ + const filtersWithoutTags = [ ...(filterOptions.filter.length ? [`alert.attributes.name: ${filterOptions.filter}`] : []), ...(filterOptions.showCustomRules ? [`alert.attributes.tags: "__internal_immutable:false"`] @@ -115,15 +115,27 @@ export const fetchRules = async ({ ...(filterOptions.showElasticRules ? [`alert.attributes.tags: "__internal_immutable:true"`] : []), + ].join(' AND '); + + const tags = [ ...(filterOptions.tags?.map((t) => `alert.attributes.tags: "${t.replace(/"/g, '\\"')}"`) ?? []), - ]; + ].join(' OR '); + + const filterString = + filtersWithoutTags !== '' && tags !== '' + ? `${filtersWithoutTags} AND (${tags})` + : filtersWithoutTags + tags; + + const getFieldNameForSortField = (field: string) => { + return field === 'name' ? `${field}.keyword` : field; + }; const query = { page: pagination.page, per_page: pagination.perPage, - sort_field: filterOptions.sortField, + sort_field: getFieldNameForSortField(filterOptions.sortField), sort_order: filterOptions.sortOrder, - ...(filters.length ? { filter: filters.join(' AND ') } : {}), + ...(filterString !== '' ? { filter: filterString } : {}), }; return KibanaServices.get().http.fetch( diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts index e94e57ad82bcf..49579e893029b 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/types.ts @@ -149,9 +149,10 @@ export interface FetchRulesProps { signal: AbortSignal; } +export type RulesSortingFields = 'enabled' | 'updated_at' | 'name' | 'created_at'; export interface FilterOptions { filter: string; - sortField: string; + sortField: RulesSortingFields; sortOrder: SortOrder; showCustomRules?: boolean; showElasticRules?: boolean; diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx index ea36a0cb0b48d..866d3e896a71d 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/columns.tsx @@ -99,7 +99,6 @@ interface GetColumns { reFetchRules: (refreshPrePackagedRule?: boolean) => void; } -// Michael: Are we able to do custom, in-table-header filters, as shown in my wireframes? export const getColumns = ({ dispatch, dispatchToaster, @@ -127,7 +126,8 @@ export const getColumns = ({ ), truncateText: true, - width: '24%', + width: '20%', + sortable: true, }, { field: 'risk_score', @@ -138,14 +138,14 @@ export const getColumns = ({ ), truncateText: true, - width: '14%', + width: '10%', }, { field: 'severity', name: i18n.COLUMN_SEVERITY, render: (value: Rule['severity']) => , truncateText: true, - width: '16%', + width: '12%', }, { field: 'status_date', @@ -160,7 +160,7 @@ export const getColumns = ({ ); }, truncateText: true, - width: '20%', + width: '14%', }, { field: 'status', @@ -174,9 +174,40 @@ export const getColumns = ({ ); }, - width: '16%', + width: '12%', truncateText: true, }, + { + field: 'updated_at', + name: i18n.COLUMN_LAST_UPDATE, + render: (value: Rule['updated_at']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + + + + ); + }, + sortable: true, + truncateText: true, + width: '14%', + }, + { + field: 'version', + name: i18n.COLUMN_VERSION, + render: (value: Rule['version']) => { + return value == null ? ( + getEmptyTagValue() + ) : ( + + {value} + + ); + }, + truncateText: true, + width: '10%', + }, { field: 'tags', name: i18n.COLUMN_TAGS, @@ -190,7 +221,7 @@ export const getColumns = ({ ), truncateText: true, - width: '20%', + width: '14%', }, { align: 'center', diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx index 110691328b13b..306adbd63ee72 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.tsx @@ -24,6 +24,7 @@ import { Rule, PaginationOptions, exportRules, + RulesSortingFields, } from '../../../../containers/detection_engine/rules'; import { HeaderSection } from '../../../../../common/components/header_section'; import { @@ -53,12 +54,12 @@ import { hasMlLicense } from '../../../../../../common/machine_learning/has_ml_l import { SecurityPageName } from '../../../../../app/types'; import { useFormatUrl } from '../../../../../common/components/link_to'; -const SORT_FIELD = 'enabled'; +const INITIAL_SORT_FIELD = 'enabled'; const initialState: State = { exportRuleIds: [], filterOptions: { filter: '', - sortField: SORT_FIELD, + sortField: INITIAL_SORT_FIELD, sortOrder: 'desc', }, loadingRuleIds: [], @@ -164,8 +165,13 @@ export const AllRules = React.memo( }); const sorting = useMemo( - (): SortingType => ({ sort: { field: 'enabled', direction: filterOptions.sortOrder } }), - [filterOptions.sortOrder] + (): SortingType => ({ + sort: { + field: filterOptions.sortField, + direction: filterOptions.sortOrder, + }, + }), + [filterOptions] ); const prePackagedRuleStatus = getPrePackagedRuleStatus( @@ -215,7 +221,7 @@ export const AllRules = React.memo( dispatch({ type: 'updateFilterOptions', filterOptions: { - sortField: SORT_FIELD, // Only enabled is supported for sorting currently + sortField: (sort?.field as RulesSortingFields) ?? INITIAL_SORT_FIELD, // Narrowing EuiBasicTable sorting types sortOrder: sort?.direction ?? 'desc', }, pagination: { page: page.index + 1, perPage: page.size }, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx index 49fe3438664c6..4fe0bc8f835df 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_table_filters/tags_filter_popover.tsx @@ -4,7 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { Dispatch, SetStateAction, useState } from 'react'; +import React, { + ChangeEvent, + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useState, +} from 'react'; import { EuiFilterButton, EuiFilterSelectItem, @@ -13,6 +21,8 @@ import { EuiPanel, EuiPopover, EuiText, + EuiFieldSearch, + EuiPopoverTitle, } from '@elastic/eui'; import styled from 'styled-components'; import * as i18n from '../../translations'; @@ -37,12 +47,39 @@ const ScrollableDiv = styled.div` * @param tags to display for filtering * @param onSelectedTagsChanged change listener to be notified when tag selection changes */ -export const TagsFilterPopoverComponent = ({ +const TagsFilterPopoverComponent = ({ tags, selectedTags, onSelectedTagsChanged, }: TagsFilterPopoverProps) => { + const sortedTags = useMemo(() => { + return tags.sort((a: string, b: string) => a.toLowerCase().localeCompare(b.toLowerCase())); // Case insensitive + }, [tags]); const [isTagPopoverOpen, setIsTagPopoverOpen] = useState(false); + const [searchInput, setSearchInput] = useState(''); + const [filterTags, setFilterTags] = useState(sortedTags); + + const tagsComponent = useMemo(() => { + return filterTags.map((tag, index) => ( + toggleSelectedGroup(tag, selectedTags, onSelectedTagsChanged)} + > + {`${tag}`} + + )); + }, [onSelectedTagsChanged, selectedTags, filterTags]); + + const onSearchInputChange = useCallback((event: ChangeEvent) => { + setSearchInput(event.target.value); + }, []); + + useEffect(() => { + setFilterTags( + sortedTags.filter((tag) => tag.toLowerCase().includes(searchInput.toLowerCase())) + ); + }, [sortedTags, searchInput]); return ( - - {tags.map((tag, index) => ( - toggleSelectedGroup(tag, selectedTags, onSelectedTagsChanged)} - > - {`${tag}`} - - ))} - - {tags.length === 0 && ( + + + + {tagsComponent} + {filterTags.length === 0 && ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts index b20c8de8ed58b..09503fcf1ef0f 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/translations.ts @@ -16,7 +16,7 @@ export const BACK_TO_DETECTIONS = i18n.translate( export const IMPORT_RULE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.importRuleTitle', { - defaultMessage: 'Import rule…', + defaultMessage: 'Import rule', } ); @@ -100,7 +100,7 @@ export const BATCH_ACTION_ACTIVATE_SELECTED_ERROR = (totalRules: number) => 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.activateSelectedErrorTitle', { values: { totalRules }, - defaultMessage: 'Error activating {totalRules, plural, =1 {rule} other {rules}}…', + defaultMessage: 'Error activating {totalRules, plural, =1 {rule} other {rules}}', } ); @@ -116,7 +116,7 @@ export const BATCH_ACTION_DEACTIVATE_SELECTED_ERROR = (totalRules: number) => 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deactivateSelectedErrorTitle', { values: { totalRules }, - defaultMessage: 'Error deactivating {totalRules, plural, =1 {rule} other {rules}}…', + defaultMessage: 'Error deactivating {totalRules, plural, =1 {rule} other {rules}}', } ); @@ -130,14 +130,14 @@ export const BATCH_ACTION_EXPORT_SELECTED = i18n.translate( export const BATCH_ACTION_DUPLICATE_SELECTED = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.duplicateSelectedTitle', { - defaultMessage: 'Duplicate selected…', + defaultMessage: 'Duplicate selected', } ); export const BATCH_ACTION_DELETE_SELECTED = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedTitle', { - defaultMessage: 'Delete selected…', + defaultMessage: 'Delete selected', } ); @@ -153,7 +153,7 @@ export const BATCH_ACTION_DELETE_SELECTED_ERROR = (totalRules: number) => 'xpack.securitySolution.detectionEngine.rules.allRules.batchActions.deleteSelectedErrorTitle', { values: { totalRules }, - defaultMessage: 'Error deleting {totalRules, plural, =1 {rule} other {rules}}…', + defaultMessage: 'Error deleting {totalRules, plural, =1 {rule} other {rules}}', } ); @@ -224,7 +224,7 @@ export const DUPLICATE = i18n.translate( export const DUPLICATE_RULE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleDescription', { - defaultMessage: 'Duplicate rule…', + defaultMessage: 'Duplicate rule', } ); @@ -241,7 +241,7 @@ export const SUCCESSFULLY_DUPLICATED_RULES = (totalRules: number) => export const DUPLICATE_RULE_ERROR = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.actions.duplicateRuleErrorDescription', { - defaultMessage: 'Error duplicating rule…', + defaultMessage: 'Error duplicating rule', } ); @@ -255,7 +255,7 @@ export const EXPORT_RULE = i18n.translate( export const DELETE_RULE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.actions.deleteeRuleDescription', { - defaultMessage: 'Delete rule…', + defaultMessage: 'Delete rule', } ); @@ -287,6 +287,13 @@ export const COLUMN_LAST_COMPLETE_RUN = i18n.translate( } ); +export const COLUMN_LAST_UPDATE = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.columns.lastUpdateTitle', + { + defaultMessage: 'Last updated', + } +); + export const COLUMN_LAST_RESPONSE = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.columns.lastResponseTitle', { @@ -294,6 +301,13 @@ export const COLUMN_LAST_RESPONSE = i18n.translate( } ); +export const COLUMN_VERSION = i18n.translate( + 'xpack.securitySolution.detectionEngine.rules.allRules.columns.versionTitle', + { + defaultMessage: 'Version', + } +); + export const COLUMN_TAGS = i18n.translate( 'xpack.securitySolution.detectionEngine.rules.allRules.columns.tagsTitle', { diff --git a/x-pack/plugins/security_solution/public/helpers.test.ts b/x-pack/plugins/security_solution/public/helpers.test.ts new file mode 100644 index 0000000000000..9244452a23e6d --- /dev/null +++ b/x-pack/plugins/security_solution/public/helpers.test.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { parseRoute } from './helpers'; + +describe('public helpers parseRoute', () => { + it('should properly parse hash route', () => { + const hashSearch = + '?timerange=(global:(linkTo:!(timeline),timerange:(from:%272020-09-06T11:43:55.814Z%27,fromStr:now-24h,kind:relative,to:%272020-09-07T11:43:55.814Z%27,toStr:now)),timeline:(linkTo:!(global),timerange:(from:%272020-09-06T11:43:55.814Z%27,fromStr:now-24h,kind:relative,to:%272020-09-07T11:43:55.814Z%27,toStr:now)))'; + const hashLocation = { + hash: `#/detections/rules/id/78acc090-bbaa-4a86-916b-ea44784324ae/edit${hashSearch}`, + pathname: '/app/siem', + search: '', + }; + + expect(parseRoute(hashLocation)).toEqual({ + pageName: 'detections', + path: `/rules/id/78acc090-bbaa-4a86-916b-ea44784324ae/edit${hashSearch}`, + search: hashSearch, + }); + }); + + it('should properly parse non-hash route', () => { + const nonHashLocation = { + hash: '', + pathname: '/app/security/detections/rules/id/78acc090-bbaa-4a86-916b-ea44784324ae/edit', + search: + '?timerange=(global:(linkTo:!(timeline),timerange:(from:%272020-09-06T11:43:55.814Z%27,fromStr:now-24h,kind:relative,to:%272020-09-07T11:43:55.814Z%27,toStr:now)),timeline:(linkTo:!(global),timerange:(from:%272020-09-06T11:43:55.814Z%27,fromStr:now-24h,kind:relative,to:%272020-09-07T11:43:55.814Z%27,toStr:now)))', + }; + + expect(parseRoute(nonHashLocation)).toEqual({ + pageName: 'detections', + path: `/rules/id/78acc090-bbaa-4a86-916b-ea44784324ae/edit${nonHashLocation.search}`, + search: nonHashLocation.search, + }); + }); + + it('should properly parse non-hash subplugin route', () => { + const nonHashLocation = { + hash: '', + pathname: '/app/security/detections', + search: + '?timerange=(global:(linkTo:!(timeline),timerange:(from:%272020-09-06T11:43:55.814Z%27,fromStr:now-24h,kind:relative,to:%272020-09-07T11:43:55.814Z%27,toStr:now)),timeline:(linkTo:!(global),timerange:(from:%272020-09-06T11:43:55.814Z%27,fromStr:now-24h,kind:relative,to:%272020-09-07T11:43:55.814Z%27,toStr:now)))', + }; + + expect(parseRoute(nonHashLocation)).toEqual({ + pageName: 'detections', + path: `${nonHashLocation.search}`, + search: nonHashLocation.search, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/helpers.ts b/x-pack/plugins/security_solution/public/helpers.ts index 63c3f3ea81d98..92f3d23907559 100644 --- a/x-pack/plugins/security_solution/public/helpers.ts +++ b/x-pack/plugins/security_solution/public/helpers.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { isEmpty } from 'lodash/fp'; + import { CoreStart } from '../../../../src/core/public'; import { APP_ID } from '../common/constants'; import { @@ -13,13 +15,37 @@ import { import { SecurityPageName } from './app/types'; import { InspectResponse } from './types'; +export const parseRoute = (location: Pick) => { + if (!isEmpty(location.hash)) { + const hashPath = location.hash.split('?'); + const search = hashPath.length >= 1 ? `?${hashPath[1]}` : ''; + const pageRoute = hashPath.length > 0 ? hashPath[0].split('/') : []; + const pageName = pageRoute.length >= 1 ? pageRoute[1] : ''; + const path = `/${pageRoute.slice(2).join('/') ?? ''}${search}`; + + return { + pageName, + path, + search, + }; + } + + const search = location.search; + const pageRoute = location.pathname.split('/'); + const pageName = pageRoute[3]; + const subpluginPath = pageRoute.length > 4 ? `/${pageRoute.slice(4).join('/')}` : ''; + const path = `${subpluginPath}${search}`; + + return { + pageName, + path, + search, + }; +}; + export const manageOldSiemRoutes = async (coreStart: CoreStart) => { const { application } = coreStart; - const hashPath = window.location.hash.split('?'); - const search = hashPath.length >= 1 ? hashPath[1] : ''; - const pageRoute = hashPath.length > 0 ? hashPath[0].split('/') : []; - const pageName = pageRoute.length >= 1 ? pageRoute[1] : ''; - const path = `/${pageRoute.slice(2).join('/') ?? ''}?${search}`; + const { pageName, path, search } = parseRoute(window.location); switch (pageName) { case SecurityPageName.overview: @@ -73,7 +99,7 @@ export const manageOldSiemRoutes = async (coreStart: CoreStart) => { default: application.navigateToApp(`${APP_ID}:${SecurityPageName.overview}`, { replace: true, - path: `?${search}`, + path: `${search}`, }); break; } diff --git a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx index 8e2b47769adf3..3d291d9bf7b28 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/authentications_table/index.tsx @@ -20,7 +20,7 @@ import { import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; import { getEmptyTagValue } from '../../../common/components/empty_value'; import { FormattedRelativePreferenceDate } from '../../../common/components/formatted_date'; -import { HostDetailsLink, IPDetailsLink } from '../../../common/components/links'; +import { HostDetailsLink, NetworkDetailsLink } from '../../../common/components/links'; import { Columns, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; @@ -264,7 +264,7 @@ const getAuthenticationColumns = (): AuthTableColumns => [ : null, attrName: 'source.ip', idPrefix: `authentications-table-${node._id}-lastSuccessSource`, - render: (item) => , + render: (item) => , }), }, { @@ -309,7 +309,7 @@ const getAuthenticationColumns = (): AuthTableColumns => [ : null, attrName: 'source.ip', idPrefix: `authentications-table-${node._id}-lastFailureSource`, - render: (item) => , + render: (item) => , }), }, { diff --git a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx index 49b63a5f76a14..d8cd59f119d52 100644 --- a/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/pages/details/index.tsx @@ -9,6 +9,7 @@ import { noop } from 'lodash/fp'; import React, { useEffect, useCallback, useMemo } from 'react'; import { connect, ConnectedProps } from 'react-redux'; +import { HostItem } from '../../../../common/search_strategy'; import { SecurityPageName } from '../../../app/types'; import { UpdateDateRange } from '../../../common/components/charts/common'; import { FiltersGlobal } from '../../../common/components/filters_global'; @@ -137,7 +138,7 @@ const HostDetailsComponent = React.memo( inspect={inspect} refetch={refetch} setQuery={setQuery} - data={hostOverview} + data={hostOverview as HostItem} anomaliesData={anomaliesData} isLoadingAnomaliesData={isLoadingAnomaliesData} loading={loading} diff --git a/x-pack/plugins/security_solution/public/management/common/constants.ts b/x-pack/plugins/security_solution/public/management/common/constants.ts index 06f0f09bcf54d..cd4ce743bb701 100644 --- a/x-pack/plugins/security_solution/public/management/common/constants.ts +++ b/x-pack/plugins/security_solution/public/management/common/constants.ts @@ -24,6 +24,12 @@ export const MANAGEMENT_STORE_POLICY_LIST_NAMESPACE = 'policyList'; export const MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE = 'policyDetails'; /** Namespace within the Management state where endpoint-host state is maintained */ export const MANAGEMENT_STORE_ENDPOINTS_NAMESPACE = 'endpoints'; +/** Namespace within the Management state where trusted apps page state is maintained */ +export const MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE = 'trustedApps'; + +export const MANAGEMENT_PAGE_SIZE_OPTIONS: readonly number[] = [10, 20, 50]; +export const MANAGEMENT_DEFAULT_PAGE = 0; +export const MANAGEMENT_DEFAULT_PAGE_SIZE = 10; // --[ DEFAULTS ]--------------------------------------------------------------------------- /** The default polling interval to start all polling pages */ diff --git a/x-pack/plugins/security_solution/public/management/common/routing.test.ts b/x-pack/plugins/security_solution/public/management/common/routing.test.ts new file mode 100644 index 0000000000000..7a36654dcffc3 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/common/routing.test.ts @@ -0,0 +1,111 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { extractListPaginationParams, getTrustedAppsListPath } from './routing'; +import { MANAGEMENT_DEFAULT_PAGE, MANAGEMENT_DEFAULT_PAGE_SIZE } from './constants'; + +describe('routing', () => { + describe('extractListPaginationParams()', () => { + it('extracts default page index when not provided', () => { + expect(extractListPaginationParams({}).page_index).toBe(MANAGEMENT_DEFAULT_PAGE); + }); + + it('extracts default page index when too small value provided', () => { + expect(extractListPaginationParams({ page_index: '-1' }).page_index).toBe( + MANAGEMENT_DEFAULT_PAGE + ); + }); + + it('extracts default page index when not a number provided', () => { + expect(extractListPaginationParams({ page_index: 'a' }).page_index).toBe( + MANAGEMENT_DEFAULT_PAGE + ); + }); + + it('extracts only last page index when multiple values provided', () => { + expect(extractListPaginationParams({ page_index: ['1', '2'] }).page_index).toBe(2); + }); + + it('extracts proper page index when single valid value provided', () => { + expect(extractListPaginationParams({ page_index: '2' }).page_index).toBe(2); + }); + + it('extracts default page size when not provided', () => { + expect(extractListPaginationParams({}).page_size).toBe(MANAGEMENT_DEFAULT_PAGE_SIZE); + }); + + it('extracts default page size when invalid option provided', () => { + expect(extractListPaginationParams({ page_size: '25' }).page_size).toBe( + MANAGEMENT_DEFAULT_PAGE_SIZE + ); + }); + + it('extracts default page size when not a number provided', () => { + expect(extractListPaginationParams({ page_size: 'a' }).page_size).toBe( + MANAGEMENT_DEFAULT_PAGE_SIZE + ); + }); + + it('extracts only last page size when multiple values provided', () => { + expect(extractListPaginationParams({ page_size: ['10', '20'] }).page_size).toBe(20); + }); + + it('extracts proper page size when single valid value provided', () => { + expect(extractListPaginationParams({ page_size: '20' }).page_size).toBe(20); + }); + }); + + describe('getTrustedAppsListPath()', () => { + it('builds proper path when no parameters provided', () => { + expect(getTrustedAppsListPath()).toEqual('/trusted_apps'); + }); + + it('builds proper path when empty parameters provided', () => { + expect(getTrustedAppsListPath({})).toEqual('/trusted_apps'); + }); + + it('builds proper path when no page index provided', () => { + expect(getTrustedAppsListPath({ page_size: 20 })).toEqual('/trusted_apps?page_size=20'); + }); + + it('builds proper path when no page size provided', () => { + expect(getTrustedAppsListPath({ page_index: 2 })).toEqual('/trusted_apps?page_index=2'); + }); + + it('builds proper path when both page index and size provided', () => { + expect(getTrustedAppsListPath({ page_index: 2, page_size: 20 })).toEqual( + '/trusted_apps?page_index=2&page_size=20' + ); + }); + + it('builds proper path when page index is equal to default', () => { + const path = getTrustedAppsListPath({ + page_index: MANAGEMENT_DEFAULT_PAGE, + page_size: 20, + }); + + expect(path).toEqual('/trusted_apps?page_size=20'); + }); + + it('builds proper path when page size is equal to default', () => { + const path = getTrustedAppsListPath({ + page_index: 2, + page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, + }); + + expect(path).toEqual('/trusted_apps?page_index=2'); + }); + + it('builds proper path when both page index and size are equal to default', () => { + const path = getTrustedAppsListPath({ + page_index: MANAGEMENT_DEFAULT_PAGE, + page_size: MANAGEMENT_DEFAULT_PAGE_SIZE, + }); + + expect(path).toEqual('/trusted_apps'); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/common/routing.ts b/x-pack/plugins/security_solution/public/management/common/routing.ts index c5ced6f3bcf55..62f360df90192 100644 --- a/x-pack/plugins/security_solution/public/management/common/routing.ts +++ b/x-pack/plugins/security_solution/public/management/common/routing.ts @@ -10,6 +10,9 @@ import { generatePath } from 'react-router-dom'; import querystring from 'querystring'; import { + MANAGEMENT_DEFAULT_PAGE, + MANAGEMENT_DEFAULT_PAGE_SIZE, + MANAGEMENT_PAGE_SIZE_OPTIONS, MANAGEMENT_ROUTING_ENDPOINTS_PATH, MANAGEMENT_ROUTING_POLICIES_PATH, MANAGEMENT_ROUTING_POLICY_DETAILS_PATH, @@ -86,8 +89,61 @@ export const getPolicyDetailPath = (policyId: string, search?: string) => { })}${appendSearch(search)}`; }; -export const getTrustedAppsListPath = (search?: string) => { - return `${generatePath(MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, { +interface ListPaginationParams { + page_index: number; + page_size: number; +} + +const isDefaultOrMissing = (value: number | undefined, defaultValue: number) => { + return value === undefined || value === defaultValue; +}; + +const normalizeListPaginationParams = ( + params?: Partial +): Partial => { + if (params) { + return { + ...(!isDefaultOrMissing(params.page_index, MANAGEMENT_DEFAULT_PAGE) + ? { page_index: params.page_index } + : {}), + ...(!isDefaultOrMissing(params.page_size, MANAGEMENT_DEFAULT_PAGE_SIZE) + ? { page_size: params.page_size } + : {}), + }; + } else { + return {}; + } +}; + +const extractFirstParamValue = (query: querystring.ParsedUrlQuery, key: string): string => { + const value = query[key]; + + return Array.isArray(value) ? value[value.length - 1] : value; +}; + +const extractPageIndex = (query: querystring.ParsedUrlQuery): number => { + const pageIndex = Number(extractFirstParamValue(query, 'page_index')); + + return !Number.isFinite(pageIndex) || pageIndex < 0 ? MANAGEMENT_DEFAULT_PAGE : pageIndex; +}; + +const extractPageSize = (query: querystring.ParsedUrlQuery): number => { + const pageSize = Number(extractFirstParamValue(query, 'page_size')); + + return MANAGEMENT_PAGE_SIZE_OPTIONS.includes(pageSize) ? pageSize : MANAGEMENT_DEFAULT_PAGE_SIZE; +}; + +export const extractListPaginationParams = ( + query: querystring.ParsedUrlQuery +): ListPaginationParams => ({ + page_index: extractPageIndex(query), + page_size: extractPageSize(query), +}); + +export const getTrustedAppsListPath = (params?: Partial): string => { + const path = generatePath(MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, { tabName: AdministrationSubTab.trustedApps, - })}${appendSearch(search)}`; + }); + + return `${path}${appendSearch(querystring.stringify(normalizeListPaginationParams(params)))}`; }; diff --git a/x-pack/plugins/security_solution/public/management/index.ts b/x-pack/plugins/security_solution/public/management/index.ts index 902ed085bd369..4bd9ac495ada9 100644 --- a/x-pack/plugins/security_solution/public/management/index.ts +++ b/x-pack/plugins/security_solution/public/management/index.ts @@ -47,9 +47,7 @@ export class Management { * Cast the ImmutableReducer to a regular reducer for compatibility with * the subplugin architecture (which expects plain redux reducers.) */ - reducer: { - management: managementReducer, - } as ManagementPluginReducer, + reducer: { management: managementReducer } as ManagementPluginReducer, middleware: managementMiddlewareFactory(core, plugins), }, }; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/models/index_pattern.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/models/index_pattern.ts new file mode 100644 index 0000000000000..064a591d0f3fa --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/models/index_pattern.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { all } from 'deepmerge'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/common'; +import { Immutable } from '../../../../../common/endpoint/types'; + +export function clone(value: IIndexPattern | Immutable): IIndexPattern { + return all([value]) as IIndexPattern; +} diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts index 5f36af2a2d8ea..84d09adfc295e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/action.ts @@ -13,6 +13,7 @@ import { ServerApiError } from '../../../../common/types'; import { GetPolicyListResponse } from '../../policy/types'; import { GetPackagesResponse } from '../../../../../../ingest_manager/common'; import { EndpointState } from '../types'; +import { IIndexPattern } from '../../../../../../../../src/plugins/data/public'; interface ServerReturnedEndpointList { type: 'serverReturnedEndpointList'; @@ -86,6 +87,15 @@ interface ServerReturnedEndpointExistValue { payload: boolean; } +interface ServerReturnedMetadataPatterns { + type: 'serverReturnedMetadataPatterns'; + payload: IIndexPattern[]; +} + +interface ServerFailedToReturnMetadataPatterns { + type: 'serverFailedToReturnMetadataPatterns'; + payload: ServerApiError; +} interface UserUpdatedEndpointListRefreshOptions { type: 'userUpdatedEndpointListRefreshOptions'; payload: { @@ -112,6 +122,8 @@ export type EndpointAction = | ServerReturnedEndpointExistValue | ServerCancelledPolicyItemsLoading | ServerReturnedEndpointPackageInfo + | ServerReturnedMetadataPatterns + | ServerFailedToReturnMetadataPatterns | AppRequestedEndpointList | ServerReturnedEndpointNonExistingPolicies | UserUpdatedEndpointListRefreshOptions; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/endpoint_pagination.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/endpoint_pagination.test.ts index 0fd970f4bed12..b4e00319485e9 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/endpoint_pagination.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/endpoint_pagination.test.ts @@ -77,6 +77,7 @@ describe('endpoint list pagination: ', () => { expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', { body: JSON.stringify({ paging_properties: [{ page_index: '0' }, { page_size: '10' }], + filters: { kql: '' }, }), }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts index 3a095644b3b41..f28ae9bf55ab2 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/index.test.ts @@ -53,6 +53,8 @@ describe('EndpointList store concerns', () => { endpointPackageInfo: undefined, nonExistingPolicies: {}, endpointsExist: true, + patterns: [], + patternsError: undefined, isAutoRefreshEnabled: true, autoRefreshInterval: DEFAULT_POLL_INTERVAL, }); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts index 15e89f9771382..c4d2886f3e8e5 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.test.ts @@ -72,6 +72,7 @@ describe('endpoint list middleware', () => { expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', { body: JSON.stringify({ paging_properties: [{ page_index: '0' }, { page_size: '10' }], + filters: { kql: '' }, }), }); expect(listData(getState())).toEqual(apiResponse.hosts); @@ -100,6 +101,7 @@ describe('endpoint list middleware', () => { expect(fakeHttpServices.post).toHaveBeenCalledWith('/api/endpoint/metadata', { body: JSON.stringify({ paging_properties: [{ page_index: '0' }, { page_size: '10' }], + filters: { kql: '' }, }), }); expect(listData(getState())).toEqual(apiResponse.hosts); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts index 2650aa4865228..5bf085023c65d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/middleware.ts @@ -15,6 +15,8 @@ import { listData, endpointPackageInfo, nonExistingPolicies, + patterns, + searchBarQuery, } from './selectors'; import { EndpointState } from '../types'; import { @@ -23,8 +25,24 @@ import { sendGetAgentPolicyList, } from '../../policy/store/policy_list/services/ingest'; import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../ingest_manager/common'; +import { metadataCurrentIndexPattern } from '../../../../../common/endpoint/constants'; +import { IIndexPattern, Query } from '../../../../../../../../src/plugins/data/public'; -export const endpointMiddlewareFactory: ImmutableMiddlewareFactory = (coreStart) => { +export const endpointMiddlewareFactory: ImmutableMiddlewareFactory = ( + coreStart, + depsStart +) => { + async function fetchIndexPatterns(): Promise { + const { indexPatterns } = depsStart.data; + const fields = await indexPatterns.getFieldsForWildcard({ + pattern: metadataCurrentIndexPattern, + }); + const indexPattern: IIndexPattern = { + title: metadataCurrentIndexPattern, + fields, + }; + return [indexPattern]; + } // eslint-disable-next-line complexity return ({ getState, dispatch }) => (next) => async (action) => { next(action); @@ -52,10 +70,31 @@ export const endpointMiddlewareFactory: ImmutableMiddlewareFactory('/api/endpoint/metadata', { body: JSON.stringify({ paging_properties: [{ page_index: pageIndex }, { page_size: pageSize }], + filters: { kql: decodedQuery.query }, }), }); endpointResponse.request_page_index = Number(pageIndex); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts index 060321fa40401..d688fa3b76b5a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/reducer.ts @@ -31,6 +31,8 @@ export const initialEndpointListState: Immutable = { endpointPackageInfo: undefined, nonExistingPolicies: {}, endpointsExist: true, + patterns: [], + patternsError: undefined, isAutoRefreshEnabled: true, autoRefreshInterval: DEFAULT_POLL_INTERVAL, }; @@ -70,6 +72,18 @@ export const endpointListReducer: ImmutableReducer = ( ...action.payload, }, }; + } else if (action.type === 'serverReturnedMetadataPatterns') { + // handle error case + return { + ...state, + patterns: action.payload, + patternsError: undefined, + }; + } else if (action.type === 'serverFailedToReturnMetadataPatterns') { + return { + ...state, + patternsError: action.payload, + }; } else if (action.type === 'serverReturnedEndpointDetails') { return { ...state, diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts index 68ba71b7bbc94..8eefcc271794a 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/selectors.ts @@ -8,6 +8,7 @@ import querystring from 'querystring'; import { createSelector } from 'reselect'; import { matchPath } from 'react-router-dom'; +import { decode } from 'rison-node'; import { Immutable, HostPolicyResponseAppliedAction, @@ -15,9 +16,13 @@ import { HostPolicyResponseActionStatus, } from '../../../../../common/endpoint/types'; import { EndpointState, EndpointIndexUIQueryParams } from '../types'; -import { MANAGEMENT_ROUTING_ENDPOINTS_PATH } from '../../../common/constants'; - -const PAGE_SIZES = Object.freeze([10, 20, 50]); +import { extractListPaginationParams } from '../../../common/routing'; +import { + MANAGEMENT_DEFAULT_PAGE, + MANAGEMENT_DEFAULT_PAGE_SIZE, + MANAGEMENT_ROUTING_ENDPOINTS_PATH, +} from '../../../common/constants'; +import { Query } from '../../../../../../../../src/plugins/data/common/query/types'; export const listData = (state: Immutable) => state.hosts; @@ -54,6 +59,13 @@ export const endpointPackageVersion = createSelector( (info) => info?.version ?? undefined ); +/** + * Returns the index patterns for the SearchBar to use for autosuggest + */ +export const patterns = (state: Immutable) => state.patterns; + +export const patternsError = (state: Immutable) => state.patternsError; + /** * Returns the full policy response from the endpoint after a user modifies a policy. */ @@ -129,16 +141,20 @@ export const uiQueryParams: ( ) => Immutable = createSelector( (state: Immutable) => state.location, (location: Immutable['location']) => { - const data: EndpointIndexUIQueryParams = { page_index: '0', page_size: '10' }; + const data: EndpointIndexUIQueryParams = { + page_index: String(MANAGEMENT_DEFAULT_PAGE), + page_size: String(MANAGEMENT_DEFAULT_PAGE_SIZE), + }; + if (location) { // Removes the `?` from the beginning of query string if it exists const query = querystring.parse(location.search.slice(1)); + const paginationParams = extractListPaginationParams(query); const keys: Array = [ 'selected_endpoint', - 'page_size', - 'page_index', 'show', + 'admin_query', ]; for (const key of keys) { @@ -160,17 +176,10 @@ export const uiQueryParams: ( } } - // Check if page size is an expected size, otherwise default to 10 - if (!PAGE_SIZES.includes(Number(data.page_size))) { - data.page_size = '10'; - } - - // Check if page index is a valid positive integer, otherwise default to 0 - const pageIndexAsNumber = Number(data.page_index); - if (!Number.isFinite(pageIndexAsNumber) || pageIndexAsNumber < 0) { - data.page_index = '0'; - } + data.page_size = String(paginationParams.page_size); + data.page_index = String(paginationParams.page_index); } + return data; } ); @@ -214,3 +223,27 @@ export const nonExistingPolicies: ( */ export const endpointsExist: (state: Immutable) => boolean = (state) => state.endpointsExist; + +/** + * Returns query text from query bar + */ +export const searchBarQuery: (state: Immutable) => Query = createSelector( + uiQueryParams, + ({ admin_query: adminQuery }) => { + const decodedQuery: Query = { query: '', language: 'kuery' }; + if (adminQuery) { + const urlDecodedQuery = (decode(adminQuery) as unknown) as Query; + if (urlDecodedQuery && typeof urlDecodedQuery.query === 'string') { + decodedQuery.query = urlDecodedQuery.query; + } + if ( + urlDecodedQuery && + typeof urlDecodedQuery.language === 'string' && + (urlDecodedQuery.language === 'kuery' || urlDecodedQuery.language === 'lucene') + ) { + decodedQuery.language = urlDecodedQuery.language; + } + } + return decodedQuery; + } +); diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts index 5a6a1af7bd7e8..b73e60718d12e 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/types.ts @@ -14,6 +14,7 @@ import { } from '../../../../common/endpoint/types'; import { ServerApiError } from '../../../common/types'; import { GetPackagesResponse } from '../../../../../ingest_manager/common'; +import { IIndexPattern } from '../../../../../../../src/plugins/data/public'; export interface EndpointState { /** list of host **/ @@ -54,6 +55,10 @@ export interface EndpointState { nonExistingPolicies: Record; /** Tracks whether hosts exist and helps control if onboarding should be visible */ endpointsExist: boolean; + /** index patterns for query bar */ + patterns: IIndexPattern[]; + /** api error from retrieving index patters for query bar */ + patternsError?: ServerApiError; /** Is auto-refresh enabled */ isAutoRefreshEnabled: boolean; /** The current auto refresh interval for data in ms */ @@ -72,4 +77,6 @@ export interface EndpointIndexUIQueryParams { page_index?: string; /** show the policy response or host details */ show?: 'policy_response' | 'details'; + /** Query text from search bar*/ + admin_query?: string; } diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx new file mode 100644 index 0000000000000..b6349a45f383d --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/components/search_bar.tsx @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useCallback, useMemo } from 'react'; +import { useHistory } from 'react-router-dom'; +import { encode, RisonValue } from 'rison-node'; +import styled from 'styled-components'; +import { Query, SearchBar, TimeHistory } from '../../../../../../../../../src/plugins/data/public'; +import { Storage } from '../../../../../../../../../src/plugins/kibana_utils/public'; +import { urlFromQueryParams } from '../url_from_query_params'; +import { useEndpointSelector } from '../hooks'; +import * as selectors from '../../store/selectors'; +import { clone } from '../../models/index_pattern'; + +const AdminQueryBar = styled.div` + .globalQueryBar { + padding: 0; + } +`; + +export const AdminSearchBar = memo(() => { + const history = useHistory(); + const queryParams = useEndpointSelector(selectors.uiQueryParams); + const searchBarIndexPatterns = useEndpointSelector(selectors.patterns); + const searchBarQuery = useEndpointSelector(selectors.searchBarQuery); + const clonedIndexPatterns = useMemo( + () => searchBarIndexPatterns.map((pattern) => clone(pattern)), + [searchBarIndexPatterns] + ); + + const onQuerySubmit = useCallback( + (params: { query?: Query }) => { + history.push( + urlFromQueryParams({ + ...queryParams, + admin_query: encode((params.query as unknown) as RisonValue), + }) + ); + }, + [history, queryParams] + ); + + const timeHistory = useMemo(() => new TimeHistory(new Storage(localStorage)), []); + + return ( +
+ {searchBarIndexPatterns && searchBarIndexPatterns.length > 0 && ( + + + + )} +
+ ); +}); + +AdminSearchBar.displayName = 'AdminSearchBar'; diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx index 8d08ac4e59a87..378f3cc4cb316 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/view/index.tsx @@ -16,6 +16,8 @@ import { EuiSelectableProps, EuiSuperDatePicker, EuiSpacer, + EuiFlexGroup, + EuiFlexItem, } from '@elastic/eui'; import { useHistory } from 'react-router-dom'; import { i18n } from '@kbn/i18n'; @@ -33,7 +35,7 @@ import { import { useNavigateByRouterEventHandler } from '../../../../common/hooks/endpoint/use_navigate_by_router_event_handler'; import { CreateStructuredSelector } from '../../../../common/store'; import { Immutable, HostInfo } from '../../../../../common/endpoint/types'; -import { DEFAULT_POLL_INTERVAL } from '../../../common/constants'; +import { DEFAULT_POLL_INTERVAL, MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../common/constants'; import { PolicyEmptyState, HostsEmptyState } from '../../../components/management_empty_state'; import { FormattedDate } from '../../../../common/components/formatted_date'; import { useNavigateToAppEventHandler } from '../../../../common/hooks/endpoint/use_navigate_to_app_event_handler'; @@ -46,6 +48,7 @@ import { getEndpointListPath, getEndpointDetailsPath } from '../../../common/rou import { useFormatUrl } from '../../../../common/components/link_to'; import { EndpointAction } from '../store/action'; import { EndpointPolicyLink } from './components/endpoint_policy_link'; +import { AdminSearchBar } from './components/search_bar'; import { AdministrationListPage } from '../../../components/administration_list_page'; const EndpointListNavLink = memo<{ @@ -89,6 +92,7 @@ export const EndpointList = () => { endpointsExist, autoRefreshInterval, isAutoRefreshEnabled, + patternsError, } = useEndpointSelector(selector); const { formatUrl, search } = useFormatUrl(SecurityPageName.administration); @@ -99,7 +103,7 @@ export const EndpointList = () => { pageIndex, pageSize, totalItemCount, - pageSizeOptions: [10, 20, 50], + pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS], hidePerPageOptions: false, }; }, [pageIndex, pageSize, totalItemCount]); @@ -397,16 +401,16 @@ export const EndpointList = () => { const hasListData = listData && listData.length > 0; const refreshStyle = useMemo(() => { - return { display: hasListData ? 'flex' : 'none', maxWidth: 200 }; - }, [hasListData]); + return { display: endpointsExist ? 'flex' : 'none', maxWidth: 200 }; + }, [endpointsExist]); const refreshIsPaused = useMemo(() => { - return !hasListData ? false : hasSelectedEndpoint ? true : !isAutoRefreshEnabled; - }, [hasListData, hasSelectedEndpoint, isAutoRefreshEnabled]); + return !endpointsExist ? false : hasSelectedEndpoint ? true : !isAutoRefreshEnabled; + }, [endpointsExist, hasSelectedEndpoint, isAutoRefreshEnabled]); const refreshInterval = useMemo(() => { - return !hasListData ? DEFAULT_POLL_INTERVAL : autoRefreshInterval; - }, [hasListData, autoRefreshInterval]); + return !endpointsExist ? DEFAULT_POLL_INTERVAL : autoRefreshInterval; + }, [endpointsExist, autoRefreshInterval]); return ( { } > {hasSelectedEndpoint && } - { - <> -
+ <> + + {endpointsExist && !patternsError && ( + + + + )} + { onRefreshChange={onRefreshChange} isAutoRefreshOnly={true} /> -
- - - } +
+
+ + {hasListData && ( <> diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts new file mode 100644 index 0000000000000..9308c137cfb9c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/service/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { HttpStart } from 'kibana/public'; +import { TRUSTED_APPS_LIST_API } from '../../../../../common/endpoint/constants'; +import { + GetTrustedListAppsResponse, + GetTrustedAppsListRequest, +} from '../../../../../common/endpoint/types/trusted_apps'; + +export interface TrustedAppsService { + getTrustedAppsList(request: GetTrustedAppsListRequest): Promise; +} + +export class TrustedAppsHttpService implements TrustedAppsService { + constructor(private http: HttpStart) {} + + async getTrustedAppsList(request: GetTrustedAppsListRequest) { + return this.http.get(TRUSTED_APPS_LIST_API, { + query: request, + }); + } +} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.test.ts new file mode 100644 index 0000000000000..5e00d833981ed --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.test.ts @@ -0,0 +1,242 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + UninitialisedResourceState, + LoadingResourceState, + LoadedResourceState, + FailedResourceState, + isUninitialisedResourceState, + isLoadingResourceState, + isLoadedResourceState, + isFailedResourceState, + getLastLoadedResourceState, + getCurrentResourceError, + isOutdatedResourceState, +} from './async_resource_state'; + +interface TestData { + property: string; +} + +const data: TestData = { property: 'value' }; + +const uninitialisedResourceState: UninitialisedResourceState = { + type: 'UninitialisedResourceState', +}; + +const loadedResourceState: LoadedResourceState = { + type: 'LoadedResourceState', + data, +}; + +const failedResourceStateInitially: FailedResourceState = { + type: 'FailedResourceState', + error: {}, +}; + +const failedResourceStateSubsequently: FailedResourceState = { + type: 'FailedResourceState', + error: {}, + lastLoadedState: loadedResourceState, +}; + +const loadingResourceStateInitially: LoadingResourceState = { + type: 'LoadingResourceState', + previousState: uninitialisedResourceState, +}; + +const loadingResourceStateAfterSuccess: LoadingResourceState = { + type: 'LoadingResourceState', + previousState: loadedResourceState, +}; + +const loadingResourceStateAfterInitialFailure: LoadingResourceState = { + type: 'LoadingResourceState', + previousState: failedResourceStateInitially, +}; + +const loadingResourceStateAfterSubsequentFailure: LoadingResourceState = { + type: 'LoadingResourceState', + previousState: failedResourceStateSubsequently, +}; + +describe('AsyncResourceState', () => { + describe('guards', () => { + describe('isUninitialisedResourceState()', () => { + it('returns true for UninitialisedResourceState', () => { + expect(isUninitialisedResourceState(uninitialisedResourceState)).toBe(true); + }); + + it('returns false for LoadingResourceState', () => { + expect(isUninitialisedResourceState(loadingResourceStateInitially)).toBe(false); + }); + + it('returns false for LoadedResourceState', () => { + expect(isUninitialisedResourceState(loadedResourceState)).toBe(false); + }); + + it('returns false for FailedResourceState', () => { + expect(isUninitialisedResourceState(failedResourceStateInitially)).toBe(false); + }); + }); + + describe('isLoadingResourceState()', () => { + it('returns false for UninitialisedResourceState', () => { + expect(isLoadingResourceState(uninitialisedResourceState)).toBe(false); + }); + + it('returns true for LoadingResourceState', () => { + expect(isLoadingResourceState(loadingResourceStateInitially)).toBe(true); + }); + + it('returns false for LoadedResourceState', () => { + expect(isLoadingResourceState(loadedResourceState)).toBe(false); + }); + + it('returns false for FailedResourceState', () => { + expect(isLoadingResourceState(failedResourceStateInitially)).toBe(false); + }); + }); + + describe('isLoadedResourceState()', () => { + it('returns false for UninitialisedResourceState', () => { + expect(isLoadedResourceState(uninitialisedResourceState)).toBe(false); + }); + + it('returns false for LoadingResourceState', () => { + expect(isLoadedResourceState(loadingResourceStateInitially)).toBe(false); + }); + + it('returns true for LoadedResourceState', () => { + expect(isLoadedResourceState(loadedResourceState)).toBe(true); + }); + + it('returns false for FailedResourceState', () => { + expect(isLoadedResourceState(failedResourceStateInitially)).toBe(false); + }); + }); + + describe('isFailedResourceState()', () => { + it('returns false for UninitialisedResourceState', () => { + expect(isFailedResourceState(uninitialisedResourceState)).toBe(false); + }); + + it('returns false for LoadingResourceState', () => { + expect(isFailedResourceState(loadingResourceStateInitially)).toBe(false); + }); + + it('returns false for LoadedResourceState', () => { + expect(isFailedResourceState(loadedResourceState)).toBe(false); + }); + + it('returns true for FailedResourceState', () => { + expect(isFailedResourceState(failedResourceStateInitially)).toBe(true); + }); + }); + }); + + describe('functions', () => { + describe('getLastLoadedResourceState()', () => { + it('returns undefined for UninitialisedResourceState', () => { + expect(getLastLoadedResourceState(uninitialisedResourceState)).toBeUndefined(); + }); + + it('returns current state for LoadedResourceState', () => { + expect(getLastLoadedResourceState(loadedResourceState)).toBe(loadedResourceState); + }); + + it('returns undefined for initial FailedResourceState', () => { + expect(getLastLoadedResourceState(failedResourceStateInitially)).toBeUndefined(); + }); + + it('returns last loaded state for subsequent FailedResourceState', () => { + expect(getLastLoadedResourceState(failedResourceStateSubsequently)).toBe( + loadedResourceState + ); + }); + + it('returns undefined for initial LoadingResourceState', () => { + expect(getLastLoadedResourceState(loadingResourceStateInitially)).toBeUndefined(); + }); + + it('returns previous state for LoadingResourceState after success', () => { + expect(getLastLoadedResourceState(loadingResourceStateAfterSuccess)).toBe( + loadedResourceState + ); + }); + + it('returns undefined for LoadingResourceState after initial failure', () => { + expect(getLastLoadedResourceState(loadingResourceStateAfterInitialFailure)).toBeUndefined(); + }); + + it('returns previous state for LoadingResourceState after subsequent failure', () => { + expect(getLastLoadedResourceState(loadingResourceStateAfterSubsequentFailure)).toBe( + loadedResourceState + ); + }); + }); + + describe('getCurrentResourceError()', () => { + it('returns undefined for UninitialisedResourceState', () => { + expect(getCurrentResourceError(uninitialisedResourceState)).toBeUndefined(); + }); + + it('returns undefined for LoadedResourceState', () => { + expect(getCurrentResourceError(loadedResourceState)).toBeUndefined(); + }); + + it('returns error for FailedResourceState', () => { + expect(getCurrentResourceError(failedResourceStateSubsequently)).toStrictEqual({}); + }); + + it('returns undefined for LoadingResourceState', () => { + expect(getCurrentResourceError(loadingResourceStateAfterSubsequentFailure)).toBeUndefined(); + }); + }); + + describe('isOutdatedResourceState()', () => { + const trueFreshnessTest = (testData: TestData) => true; + const falseFreshnessTest = (testData: TestData) => false; + + it('returns true for UninitialisedResourceState', () => { + expect(isOutdatedResourceState(uninitialisedResourceState, falseFreshnessTest)).toBe(true); + }); + + it('returns false for LoadingResourceState', () => { + expect(isOutdatedResourceState(loadingResourceStateAfterSuccess, falseFreshnessTest)).toBe( + false + ); + }); + + it('returns false for LoadedResourceState and fresh data', () => { + expect(isOutdatedResourceState(loadedResourceState, trueFreshnessTest)).toBe(false); + }); + + it('returns true for LoadedResourceState and outdated data', () => { + expect(isOutdatedResourceState(loadedResourceState, falseFreshnessTest)).toBe(true); + }); + + it('returns true for initial FailedResourceState', () => { + expect(isOutdatedResourceState(failedResourceStateInitially, falseFreshnessTest)).toBe( + true + ); + }); + + it('returns false for subsequent FailedResourceState and fresh data', () => { + expect(isOutdatedResourceState(failedResourceStateSubsequently, trueFreshnessTest)).toBe( + false + ); + }); + + it('returns true for subsequent FailedResourceState and outdated data', () => { + expect(isOutdatedResourceState(failedResourceStateSubsequently, falseFreshnessTest)).toBe( + true + ); + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.ts new file mode 100644 index 0000000000000..4639a50a61865 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/async_resource_state.ts @@ -0,0 +1,138 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +/* + * this file contains set of types to represent state of asynchronous resource. + * Resource is defined as a reference to potential data that is loaded/updated + * using asynchronous communication with data source (for example through REST API call). + * Asynchronous update implies that next to just having data: + * - there is moment in time when data is not loaded/initialised and not in process of loading/updating + * - process performing data update can take considerable time which needs to be communicated to user + * - update can fail due to multiple reasons and also needs to be communicated to the user + */ + +import { Immutable } from '../../../../../common/endpoint/types'; +import { ServerApiError } from '../../../../common/types'; + +/** + * Data type to represent uninitialised state of asynchronous resource. + * This state indicates that no actions to load the data has be taken. + */ +export interface UninitialisedResourceState { + type: 'UninitialisedResourceState'; +} + +/** + * Data type to represent loading state of asynchronous resource. Loading state + * should be used to indicate that data is in the process of loading/updating. + * It contains reference to previous stale state that can be used to present + * previous state of resource to the user (like show previous already loaded + * data or show previous failure). + * + * @param Data - type of the data that is referenced by resource state + * @param Error - type of the error that can happen during attempt to update data + */ +export interface LoadingResourceState { + type: 'LoadingResourceState'; + previousState: StaleResourceState; +} + +/** + * Data type to represent loaded state of asynchronous resource. Loaded state + * is characterised with reference to the loaded data. + * + * @param Data - type of the data that is referenced by resource state + */ +export interface LoadedResourceState { + type: 'LoadedResourceState'; + data: Data; +} + +/** + * Data type to represent failed state of asynchronous resource. Failed state + * is characterised with error and can reference last loaded state. Reference + * to last loaded state can be used to present previous successfully loaded data. + * + * @param Data - type of the data that is referenced by resource state + * @param Error - type of the error that can happen during attempt to update data + */ +export interface FailedResourceState { + type: 'FailedResourceState'; + error: Error; + lastLoadedState?: LoadedResourceState; +} + +/** + * Data type to represent stale (not loading) state of asynchronous resource. + * + * @param Data - type of the data that is referenced by resource state + * @param Error - type of the error that can happen during attempt to update data + */ +export type StaleResourceState = + | UninitialisedResourceState + | LoadedResourceState + | FailedResourceState; + +/** + * Data type to represent any state of asynchronous resource. + * + * @param Data - type of the data that is referenced by resource state + * @param Error - type of the error that can happen during attempt to update data + */ +export type AsyncResourceState = + | UninitialisedResourceState + | LoadingResourceState + | LoadedResourceState + | FailedResourceState; + +// Set of guards to narrow the type of AsyncResourceState that make further refactoring easier + +export const isUninitialisedResourceState = ( + state: Immutable> +): state is Immutable => state.type === 'UninitialisedResourceState'; + +export const isLoadingResourceState = ( + state: Immutable> +): state is Immutable> => state.type === 'LoadingResourceState'; + +export const isLoadedResourceState = ( + state: Immutable> +): state is Immutable> => state.type === 'LoadedResourceState'; + +export const isFailedResourceState = ( + state: Immutable> +): state is Immutable> => state.type === 'FailedResourceState'; + +// Set of functions to work with AsyncResourceState + +export const getLastLoadedResourceState = ( + state: Immutable> +): Immutable> | undefined => { + if (isLoadedResourceState(state)) { + return state; + } else if (isLoadingResourceState(state)) { + return getLastLoadedResourceState(state.previousState); + } else if (isFailedResourceState(state)) { + return state.lastLoadedState; + } else { + return undefined; + } +}; + +export const getCurrentResourceError = ( + state: Immutable> +): Immutable | undefined => { + return isFailedResourceState(state) ? state.error : undefined; +}; + +export const isOutdatedResourceState = ( + state: AsyncResourceState, + isFresh: (data: Data) => boolean +): boolean => + isUninitialisedResourceState(state) || + (isLoadedResourceState(state) && !isFresh(state.data)) || + (isFailedResourceState(state) && + (!state.lastLoadedState || !isFresh(state.lastLoadedState.data))); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/index.ts new file mode 100644 index 0000000000000..99bdac57da4be --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './async_resource_state'; +export * from './trusted_apps_list_page_state'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts new file mode 100644 index 0000000000000..23f4cfd576c56 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/state/trusted_apps_list_page_state.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TrustedApp } from '../../../../../common/endpoint/types/trusted_apps'; +import { AsyncResourceState } from '.'; + +export interface PaginationInfo { + index: number; + size: number; +} + +export interface TrustedAppsListData { + items: TrustedApp[]; + totalItemsCount: number; + paginationInfo: PaginationInfo; +} + +export interface TrustedAppsListPageState { + listView: { + currentListResourceState: AsyncResourceState; + currentPaginationInfo: PaginationInfo; + }; + active: boolean; +} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts new file mode 100644 index 0000000000000..2154a0eca462e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/action.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AsyncResourceState, TrustedAppsListData } from '../state'; + +export interface TrustedAppsListResourceStateChanged { + type: 'trustedAppsListResourceStateChanged'; + payload: { + newState: AsyncResourceState; + }; +} + +export type TrustedAppsPageAction = TrustedAppsListResourceStateChanged; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts new file mode 100644 index 0000000000000..c5abaae473486 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.test.ts @@ -0,0 +1,130 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { applyMiddleware, createStore } from 'redux'; + +import { createSpyMiddleware } from '../../../../common/store/test_utils'; + +import { + createFailedListViewWithPagination, + createLoadedListViewWithPagination, + createLoadingListViewWithPagination, + createSampleTrustedApps, + createServerApiError, + createUserChangedUrlAction, +} from '../test_utils'; + +import { TrustedAppsService } from '../service'; +import { PaginationInfo, TrustedAppsListPageState } from '../state'; +import { initialTrustedAppsPageState, trustedAppsPageReducer } from './reducer'; +import { createTrustedAppsPageMiddleware } from './middleware'; + +const createGetTrustedListAppsResponse = (pagination: PaginationInfo, totalItemsCount: number) => ({ + data: createSampleTrustedApps(pagination), + page: pagination.index, + per_page: pagination.size, + total: totalItemsCount, +}); + +const createTrustedAppsServiceMock = (): jest.Mocked => ({ + getTrustedAppsList: jest.fn(), +}); + +const createStoreSetup = (trustedAppsService: TrustedAppsService) => { + const spyMiddleware = createSpyMiddleware(); + + return { + spyMiddleware, + store: createStore( + trustedAppsPageReducer, + applyMiddleware( + createTrustedAppsPageMiddleware(trustedAppsService), + spyMiddleware.actionSpyMiddleware + ) + ), + }; +}; + +describe('middleware', () => { + describe('refreshing list resource state', () => { + it('sets initial state properly', async () => { + expect(createStoreSetup(createTrustedAppsServiceMock()).store.getState()).toStrictEqual( + initialTrustedAppsPageState + ); + }); + + it('refreshes the list when location changes and data gets outdated', async () => { + const pagination = { index: 2, size: 50 }; + const service = createTrustedAppsServiceMock(); + const { store, spyMiddleware } = createStoreSetup(service); + + service.getTrustedAppsList.mockResolvedValue( + createGetTrustedListAppsResponse(pagination, 500) + ); + + store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50')); + + expect(store.getState()).toStrictEqual({ + listView: createLoadingListViewWithPagination(pagination), + active: true, + }); + + await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); + + expect(store.getState()).toStrictEqual({ + listView: createLoadedListViewWithPagination(pagination, pagination, 500), + active: true, + }); + }); + + it('does not refresh the list when location changes and data does not get outdated', async () => { + const pagination = { index: 2, size: 50 }; + const service = createTrustedAppsServiceMock(); + const { store, spyMiddleware } = createStoreSetup(service); + + service.getTrustedAppsList.mockResolvedValue( + createGetTrustedListAppsResponse(pagination, 500) + ); + + store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50')); + + await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); + + store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50')); + + expect(service.getTrustedAppsList).toBeCalledTimes(1); + expect(store.getState()).toStrictEqual({ + listView: createLoadedListViewWithPagination(pagination, pagination, 500), + active: true, + }); + }); + + it('set list resource state to faile when failing to load data', async () => { + const service = createTrustedAppsServiceMock(); + const { store, spyMiddleware } = createStoreSetup(service); + + service.getTrustedAppsList.mockRejectedValue(createServerApiError('Internal Server Error')); + + store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50')); + + await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); + + expect(store.getState()).toStrictEqual({ + listView: createFailedListViewWithPagination( + { index: 2, size: 50 }, + createServerApiError('Internal Server Error') + ), + active: true, + }); + + const infiniteLoopTest = async () => { + await spyMiddleware.waitForAction('trustedAppsListResourceStateChanged'); + }; + + await expect(infiniteLoopTest).rejects.not.toBeNull(); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts new file mode 100644 index 0000000000000..31c301b8dbd2b --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/middleware.ts @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Immutable } from '../../../../../common/endpoint/types'; +import { AppAction } from '../../../../common/store/actions'; +import { + ImmutableMiddleware, + ImmutableMiddlewareAPI, + ImmutableMiddlewareFactory, +} from '../../../../common/store'; + +import { TrustedAppsHttpService, TrustedAppsService } from '../service'; + +import { + AsyncResourceState, + StaleResourceState, + TrustedAppsListData, + TrustedAppsListPageState, +} from '../state'; + +import { TrustedAppsListResourceStateChanged } from './action'; + +import { + getCurrentListResourceState, + getLastLoadedListResourceState, + getListCurrentPageIndex, + getListCurrentPageSize, + needsRefreshOfListData, +} from './selectors'; + +const createTrustedAppsListResourceStateChangedAction = ( + newState: Immutable> +): Immutable => ({ + type: 'trustedAppsListResourceStateChanged', + payload: { newState }, +}); + +const refreshList = async ( + store: ImmutableMiddlewareAPI, + trustedAppsService: TrustedAppsService +) => { + store.dispatch( + createTrustedAppsListResourceStateChangedAction({ + type: 'LoadingResourceState', + // need to think on how to avoid the casting + previousState: getCurrentListResourceState(store.getState()) as Immutable< + StaleResourceState + >, + }) + ); + + try { + const pageIndex = getListCurrentPageIndex(store.getState()); + const pageSize = getListCurrentPageSize(store.getState()); + const response = await trustedAppsService.getTrustedAppsList({ + page: pageIndex + 1, + per_page: pageSize, + }); + + store.dispatch( + createTrustedAppsListResourceStateChangedAction({ + type: 'LoadedResourceState', + data: { + items: response.data, + totalItemsCount: response.total, + paginationInfo: { index: pageIndex, size: pageSize }, + }, + }) + ); + } catch (error) { + store.dispatch( + createTrustedAppsListResourceStateChangedAction({ + type: 'FailedResourceState', + error, + lastLoadedState: getLastLoadedListResourceState(store.getState()), + }) + ); + } +}; + +export const createTrustedAppsPageMiddleware = ( + trustedAppsService: TrustedAppsService +): ImmutableMiddleware => { + return (store) => (next) => async (action) => { + next(action); + + // TODO: need to think if failed state is a good condition to consider need for refresh + if (action.type === 'userChangedUrl' && needsRefreshOfListData(store.getState())) { + await refreshList(store, trustedAppsService); + } + }; +}; + +export const trustedAppsPageMiddlewareFactory: ImmutableMiddlewareFactory = ( + coreStart +) => createTrustedAppsPageMiddleware(new TrustedAppsHttpService(coreStart.http)); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts new file mode 100644 index 0000000000000..34325e0cf1398 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.test.ts @@ -0,0 +1,95 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { initialTrustedAppsPageState, trustedAppsPageReducer } from './reducer'; +import { + createListLoadedResourceState, + createLoadedListViewWithPagination, + createTrustedAppsListResourceStateChangedAction, + createUserChangedUrlAction, +} from '../test_utils'; + +describe('reducer', () => { + describe('UserChangedUrl', () => { + it('makes page state active and extracts pagination parameters', () => { + const result = trustedAppsPageReducer( + initialTrustedAppsPageState, + createUserChangedUrlAction('/trusted_apps', '?page_index=5&page_size=50') + ); + + expect(result).toStrictEqual({ + listView: { + ...initialTrustedAppsPageState.listView, + currentPaginationInfo: { index: 5, size: 50 }, + }, + active: true, + }); + }); + + it('extracts default pagination parameters when none provided', () => { + const result = trustedAppsPageReducer( + { + ...initialTrustedAppsPageState, + listView: { + ...initialTrustedAppsPageState.listView, + currentPaginationInfo: { index: 5, size: 50 }, + }, + }, + createUserChangedUrlAction('/trusted_apps', '?page_index=b&page_size=60') + ); + + expect(result).toStrictEqual({ + ...initialTrustedAppsPageState, + active: true, + }); + }); + + it('extracts default pagination parameters when invalid provided', () => { + const result = trustedAppsPageReducer( + { + ...initialTrustedAppsPageState, + listView: { + ...initialTrustedAppsPageState.listView, + currentPaginationInfo: { index: 5, size: 50 }, + }, + }, + createUserChangedUrlAction('/trusted_apps') + ); + + expect(result).toStrictEqual({ + ...initialTrustedAppsPageState, + active: true, + }); + }); + + it('makes page state inactive and resets list to uninitialised state when navigating away', () => { + const result = trustedAppsPageReducer( + { listView: createLoadedListViewWithPagination(), active: true }, + createUserChangedUrlAction('/endpoints') + ); + + expect(result).toStrictEqual(initialTrustedAppsPageState); + }); + }); + + describe('TrustedAppsListResourceStateChanged', () => { + it('sets the current list resource state', () => { + const listResourceState = createListLoadedResourceState({ index: 3, size: 50 }, 200); + const result = trustedAppsPageReducer( + initialTrustedAppsPageState, + createTrustedAppsListResourceStateChangedAction(listResourceState) + ); + + expect(result).toStrictEqual({ + ...initialTrustedAppsPageState, + listView: { + ...initialTrustedAppsPageState.listView, + currentListResourceState: listResourceState, + }, + }); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts new file mode 100644 index 0000000000000..4fdc6f90ef40c --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/reducer.ts @@ -0,0 +1,96 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// eslint-disable-next-line import/no-nodejs-modules +import { parse } from 'querystring'; +import { matchPath } from 'react-router-dom'; +import { ImmutableReducer } from '../../../../common/store'; +import { AppLocation, Immutable } from '../../../../../common/endpoint/types'; +import { UserChangedUrl } from '../../../../common/store/routing/action'; +import { AppAction } from '../../../../common/store/actions'; +import { extractListPaginationParams } from '../../../common/routing'; +import { + MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, + MANAGEMENT_DEFAULT_PAGE, + MANAGEMENT_DEFAULT_PAGE_SIZE, +} from '../../../common/constants'; + +import { TrustedAppsListResourceStateChanged } from './action'; +import { TrustedAppsListPageState } from '../state'; + +type StateReducer = ImmutableReducer; +type CaseReducer = ( + state: Immutable, + action: Immutable +) => Immutable; + +const isTrustedAppsPageLocation = (location: Immutable) => { + return ( + matchPath(location.pathname ?? '', { + path: MANAGEMENT_ROUTING_TRUSTED_APPS_PATH, + exact: true, + }) !== null + ); +}; + +const trustedAppsListResourceStateChanged: CaseReducer = ( + state, + action +) => { + return { + ...state, + listView: { + ...state.listView, + currentListResourceState: action.payload.newState, + }, + }; +}; + +const userChangedUrl: CaseReducer = (state, action) => { + if (isTrustedAppsPageLocation(action.payload)) { + const paginationParams = extractListPaginationParams(parse(action.payload.search.slice(1))); + + return { + ...state, + listView: { + ...state.listView, + currentPaginationInfo: { + index: paginationParams.page_index, + size: paginationParams.page_size, + }, + }, + active: true, + }; + } else { + return initialTrustedAppsPageState; + } +}; + +export const initialTrustedAppsPageState: TrustedAppsListPageState = { + listView: { + currentListResourceState: { type: 'UninitialisedResourceState' }, + currentPaginationInfo: { + index: MANAGEMENT_DEFAULT_PAGE, + size: MANAGEMENT_DEFAULT_PAGE_SIZE, + }, + }, + active: false, +}; + +export const trustedAppsPageReducer: StateReducer = ( + state = initialTrustedAppsPageState, + action +) => { + switch (action.type) { + case 'trustedAppsListResourceStateChanged': + return trustedAppsListResourceStateChanged(state, action); + + case 'userChangedUrl': + return userChangedUrl(state, action); + } + + return state; +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts new file mode 100644 index 0000000000000..a969e2dee4773 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.test.ts @@ -0,0 +1,179 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + getCurrentListResourceState, + getLastLoadedListResourceState, + getListCurrentPageIndex, + getListCurrentPageSize, + getListErrorMessage, + getListItems, + getListTotalItemsCount, + isListLoading, + needsRefreshOfListData, +} from './selectors'; + +import { + createDefaultListView, + createDefaultPaginationInfo, + createListComplexLoadingResourceState, + createListFailedResourceState, + createListLoadedResourceState, + createLoadedListViewWithPagination, + createSampleTrustedApps, + createUninitialisedResourceState, +} from '../test_utils'; + +describe('selectors', () => { + describe('needsRefreshOfListData()', () => { + it('returns false for outdated resource state and inactive state', () => { + expect(needsRefreshOfListData({ listView: createDefaultListView(), active: false })).toBe( + false + ); + }); + + it('returns true for outdated resource state and active state', () => { + expect(needsRefreshOfListData({ listView: createDefaultListView(), active: true })).toBe( + true + ); + }); + + it('returns true when current loaded page index is outdated', () => { + const listView = createLoadedListViewWithPagination({ index: 1, size: 20 }); + + expect(needsRefreshOfListData({ listView, active: true })).toBe(true); + }); + + it('returns true when current loaded page size is outdated', () => { + const listView = createLoadedListViewWithPagination({ index: 0, size: 50 }); + + expect(needsRefreshOfListData({ listView, active: true })).toBe(true); + }); + + it('returns false when current loaded data is up to date', () => { + const listView = createLoadedListViewWithPagination(); + + expect(needsRefreshOfListData({ listView, active: true })).toBe(false); + }); + }); + + describe('getCurrentListResourceState()', () => { + it('returns current list resource state', () => { + const listView = createDefaultListView(); + + expect(getCurrentListResourceState({ listView, active: false })).toStrictEqual( + createUninitialisedResourceState() + ); + }); + }); + + describe('getLastLoadedListResourceState()', () => { + it('returns last loaded list resource state', () => { + const listView = { + currentListResourceState: createListComplexLoadingResourceState( + createDefaultPaginationInfo(), + 200 + ), + currentPaginationInfo: createDefaultPaginationInfo(), + }; + + expect(getLastLoadedListResourceState({ listView, active: false })).toStrictEqual( + createListLoadedResourceState(createDefaultPaginationInfo(), 200) + ); + }); + }); + + describe('getListItems()', () => { + it('returns empty list when no valid data loaded', () => { + expect(getListItems({ listView: createDefaultListView(), active: false })).toStrictEqual([]); + }); + + it('returns last loaded list items', () => { + const listView = { + currentListResourceState: createListComplexLoadingResourceState( + createDefaultPaginationInfo(), + 200 + ), + currentPaginationInfo: createDefaultPaginationInfo(), + }; + + expect(getListItems({ listView, active: false })).toStrictEqual( + createSampleTrustedApps(createDefaultPaginationInfo()) + ); + }); + }); + + describe('getListTotalItemsCount()', () => { + it('returns 0 when no valid data loaded', () => { + expect(getListTotalItemsCount({ listView: createDefaultListView(), active: false })).toBe(0); + }); + + it('returns last loaded total items count', () => { + const listView = { + currentListResourceState: createListComplexLoadingResourceState( + createDefaultPaginationInfo(), + 200 + ), + currentPaginationInfo: createDefaultPaginationInfo(), + }; + + expect(getListTotalItemsCount({ listView, active: false })).toBe(200); + }); + }); + + describe('getListCurrentPageIndex()', () => { + it('returns page index', () => { + expect(getListCurrentPageIndex({ listView: createDefaultListView(), active: false })).toBe(0); + }); + }); + + describe('getListCurrentPageSize()', () => { + it('returns page index', () => { + expect(getListCurrentPageSize({ listView: createDefaultListView(), active: false })).toBe(20); + }); + }); + + describe('getListErrorMessage()', () => { + it('returns undefined when not in failed state', () => { + const listView = { + currentListResourceState: createListComplexLoadingResourceState( + createDefaultPaginationInfo(), + 200 + ), + currentPaginationInfo: createDefaultPaginationInfo(), + }; + + expect(getListErrorMessage({ listView, active: false })).toBeUndefined(); + }); + + it('returns message when not in failed state', () => { + const listView = { + currentListResourceState: createListFailedResourceState('Internal Server Error'), + currentPaginationInfo: createDefaultPaginationInfo(), + }; + + expect(getListErrorMessage({ listView, active: false })).toBe('Internal Server Error'); + }); + }); + + describe('isListLoading()', () => { + it('returns false when no loading is happening', () => { + expect(isListLoading({ listView: createDefaultListView(), active: false })).toBe(false); + }); + + it('returns true when loading is in progress', () => { + const listView = { + currentListResourceState: createListComplexLoadingResourceState( + createDefaultPaginationInfo(), + 200 + ), + currentPaginationInfo: createDefaultPaginationInfo(), + }; + + expect(isListLoading({ listView, active: false })).toBe(true); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts new file mode 100644 index 0000000000000..6fde779ac1cce --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/store/selectors.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Immutable, TrustedApp } from '../../../../../common/endpoint/types'; + +import { + AsyncResourceState, + getCurrentResourceError, + getLastLoadedResourceState, + isLoadingResourceState, + isOutdatedResourceState, + LoadedResourceState, + PaginationInfo, + TrustedAppsListData, + TrustedAppsListPageState, +} from '../state'; + +const pageInfosEqual = (pageInfo1: PaginationInfo, pageInfo2: PaginationInfo): boolean => + pageInfo1.index === pageInfo2.index && pageInfo1.size === pageInfo2.size; + +export const needsRefreshOfListData = (state: Immutable): boolean => { + const currentPageInfo = state.listView.currentPaginationInfo; + const currentPage = state.listView.currentListResourceState; + + return ( + state.active && + isOutdatedResourceState(currentPage, (data) => + pageInfosEqual(currentPageInfo, data.paginationInfo) + ) + ); +}; + +export const getCurrentListResourceState = ( + state: Immutable +): Immutable> | undefined => { + return state.listView.currentListResourceState; +}; + +export const getLastLoadedListResourceState = ( + state: Immutable +): Immutable> | undefined => { + return getLastLoadedResourceState(state.listView.currentListResourceState); +}; + +export const getListItems = ( + state: Immutable +): Immutable => { + return getLastLoadedResourceState(state.listView.currentListResourceState)?.data.items || []; +}; + +export const getListCurrentPageIndex = (state: Immutable): number => { + return state.listView.currentPaginationInfo.index; +}; + +export const getListCurrentPageSize = (state: Immutable): number => { + return state.listView.currentPaginationInfo.size; +}; + +export const getListTotalItemsCount = (state: Immutable): number => { + return ( + getLastLoadedResourceState(state.listView.currentListResourceState)?.data.totalItemsCount || 0 + ); +}; + +export const getListErrorMessage = ( + state: Immutable +): string | undefined => { + return getCurrentResourceError(state.listView.currentListResourceState)?.message; +}; + +export const isListLoading = (state: Immutable): boolean => { + return isLoadingResourceState(state.listView.currentListResourceState); +}; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts new file mode 100644 index 0000000000000..fab059a422a2a --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/test_utils/index.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { ServerApiError } from '../../../../common/types'; +import { TrustedApp } from '../../../../../common/endpoint/types'; +import { RoutingAction } from '../../../../common/store/routing'; + +import { + AsyncResourceState, + FailedResourceState, + LoadedResourceState, + LoadingResourceState, + PaginationInfo, + StaleResourceState, + TrustedAppsListData, + TrustedAppsListPageState, + UninitialisedResourceState, +} from '../state'; + +import { TrustedAppsListResourceStateChanged } from '../store/action'; + +const OS_LIST: Array = ['windows', 'macos', 'linux']; + +export const createSampleTrustedApps = (paginationInfo: PaginationInfo): TrustedApp[] => { + return [...new Array(paginationInfo.size).keys()].map((i) => ({ + id: String(paginationInfo.index + i), + name: `trusted app ${paginationInfo.index + i}`, + description: `Trusted App ${paginationInfo.index + i}`, + created_at: '1 minute ago', + created_by: 'someone', + os: OS_LIST[i % 3], + entries: [], + })); +}; + +export const createTrustedAppsListData = ( + paginationInfo: PaginationInfo, + totalItemsCount: number +) => ({ + items: createSampleTrustedApps(paginationInfo), + totalItemsCount, + paginationInfo, +}); + +export const createServerApiError = (message: string) => ({ + statusCode: 500, + error: 'Internal Server Error', + message, +}); + +export const createUninitialisedResourceState = (): UninitialisedResourceState => ({ + type: 'UninitialisedResourceState', +}); + +export const createListLoadedResourceState = ( + paginationInfo: PaginationInfo, + totalItemsCount: number +): LoadedResourceState => ({ + type: 'LoadedResourceState', + data: createTrustedAppsListData(paginationInfo, totalItemsCount), +}); + +export const createListFailedResourceState = ( + message: string, + lastLoadedState?: LoadedResourceState +): FailedResourceState => ({ + type: 'FailedResourceState', + error: createServerApiError(message), + lastLoadedState, +}); + +export const createListLoadingResourceState = ( + previousState: StaleResourceState = createUninitialisedResourceState() +): LoadingResourceState => ({ + type: 'LoadingResourceState', + previousState, +}); + +export const createListComplexLoadingResourceState = ( + paginationInfo: PaginationInfo, + totalItemsCount: number +): LoadingResourceState => + createListLoadingResourceState( + createListFailedResourceState( + 'Internal Server Error', + createListLoadedResourceState(paginationInfo, totalItemsCount) + ) + ); + +export const createDefaultPaginationInfo = () => ({ index: 0, size: 20 }); + +export const createDefaultListView = () => ({ + currentListResourceState: createUninitialisedResourceState(), + currentPaginationInfo: createDefaultPaginationInfo(), +}); + +export const createLoadingListViewWithPagination = ( + currentPaginationInfo: PaginationInfo, + previousState: StaleResourceState = createUninitialisedResourceState() +): TrustedAppsListPageState['listView'] => ({ + currentListResourceState: { type: 'LoadingResourceState', previousState }, + currentPaginationInfo, +}); + +export const createLoadedListViewWithPagination = ( + paginationInfo: PaginationInfo = createDefaultPaginationInfo(), + currentPaginationInfo: PaginationInfo = createDefaultPaginationInfo(), + totalItemsCount: number = 200 +): TrustedAppsListPageState['listView'] => ({ + currentListResourceState: createListLoadedResourceState(paginationInfo, totalItemsCount), + currentPaginationInfo, +}); + +export const createFailedListViewWithPagination = ( + currentPaginationInfo: PaginationInfo, + error: ServerApiError, + lastLoadedState?: LoadedResourceState +): TrustedAppsListPageState['listView'] => ({ + currentListResourceState: { type: 'FailedResourceState', error, lastLoadedState }, + currentPaginationInfo, +}); + +export const createUserChangedUrlAction = (path: string, search: string = ''): RoutingAction => { + return { type: 'userChangedUrl', payload: { pathname: path, search, hash: '' } }; +}; + +export const createTrustedAppsListResourceStateChangedAction = ( + newState: AsyncResourceState +): TrustedAppsListResourceStateChanged => ({ + type: 'trustedAppsListResourceStateChanged', + payload: { newState }, +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_list.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_list.test.tsx.snap new file mode 100644 index 0000000000000..e0f846f5950f7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_list.test.tsx.snap @@ -0,0 +1,5530 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TrustedAppsList renders correctly initially 1`] = ` +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + +
+
+
+ + Name + +
+
+
+ + OS + +
+
+
+ + Date Created + +
+
+
+ + Created By + +
+
+
+ + No items found + +
+
+
+
+
+`; + +exports[`TrustedAppsList renders correctly when failed loading data for the first time 1`] = ` +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + +
+
+
+ + Name + +
+
+
+ + OS + +
+
+
+ + Date Created + +
+
+
+ + Created By + +
+
+
+ +
+ + Intenal Server Error + +
+
+
+
+
+`; + +exports[`TrustedAppsList renders correctly when failed loading data for the second time 1`] = ` +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + +
+
+
+ + Name + +
+
+
+ + OS + +
+
+
+ + Date Created + +
+
+
+ + Created By + +
+
+
+ +
+ + Intenal Server Error + +
+
+
+
+
+`; + +exports[`TrustedAppsList renders correctly when loaded data 1`] = ` +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Name + +
+
+
+ + OS + +
+
+
+ + Date Created + +
+
+
+ + Created By + +
+
+
+ Name +
+
+ + trusted app 0 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 1 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 2 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 3 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 4 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 5 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 6 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 7 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 8 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 9 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 10 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 11 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 12 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 13 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 14 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 15 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 16 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 17 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 18 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 19 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+`; + +exports[`TrustedAppsList renders correctly when loading data for the first time 1`] = ` +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + +
+
+
+ + Name + +
+
+
+ + OS + +
+
+
+ + Date Created + +
+
+
+ + Created By + +
+
+
+ + No items found + +
+
+
+
+
+`; + +exports[`TrustedAppsList renders correctly when loading data for the second time 1`] = ` +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Name + +
+
+
+ + OS + +
+
+
+ + Date Created + +
+
+
+ + Created By + +
+
+
+ Name +
+
+ + trusted app 0 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 1 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 2 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 3 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 4 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 5 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 6 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 7 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 8 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 9 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 10 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 11 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 12 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 13 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 14 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 15 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 16 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 17 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 18 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 19 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+`; + +exports[`TrustedAppsList renders correctly when new page and page sie set (not loading yet) 1`] = ` +
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+ + Name + +
+
+
+ + OS + +
+
+
+ + Date Created + +
+
+
+ + Created By + +
+
+
+ Name +
+
+ + trusted app 0 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 1 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 2 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 3 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 4 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 5 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 6 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 7 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 8 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 9 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 10 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 11 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 12 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 13 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 14 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 15 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 16 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 17 + +
+
+
+ OS +
+
+ Linux +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 18 + +
+
+
+ OS +
+
+ Windows +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+ Name +
+
+ + trusted app 19 + +
+
+
+ OS +
+
+ Mac OS +
+
+
+ Date Created +
+
+ 1 minute ago +
+
+
+ Created By +
+
+ + someone + +
+
+
+
+
+
+
+
+
+ +
+
+
+
+ +
+
+
+
+
+`; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap index 6f074f3809036..d6e9aee108cf6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/__snapshots__/trusted_apps_page.test.tsx.snap @@ -17,5 +17,7 @@ exports[`TrustedAppsPage rendering 1`] = ` values={Object {}} /> } -/> +> + + `; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/hooks.ts new file mode 100644 index 0000000000000..62610610981e0 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/hooks.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useSelector } from 'react-redux'; + +import { State } from '../../../../common/store'; + +import { + MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE as TRUSTED_APPS_NS, + MANAGEMENT_STORE_GLOBAL_NAMESPACE as GLOBAL_NS, +} from '../../../common/constants'; + +import { TrustedAppsListPageState } from '../state'; + +export function useTrustedAppsSelector(selector: (state: TrustedAppsListPageState) => R): R { + return useSelector((state: State) => + selector(state[GLOBAL_NS][TRUSTED_APPS_NS] as TrustedAppsListPageState) + ); +} diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx new file mode 100644 index 0000000000000..0362f5c7a9de6 --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.test.tsx @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { combineReducers, createStore } from 'redux'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { Provider } from 'react-redux'; + +import { + MANAGEMENT_STORE_GLOBAL_NAMESPACE, + MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE, +} from '../../../common/constants'; +import { trustedAppsPageReducer } from '../store/reducer'; +import { TrustedAppsList } from './trusted_apps_list'; +import { + createListFailedResourceState, + createListLoadedResourceState, + createListLoadingResourceState, + createTrustedAppsListResourceStateChangedAction, + createUserChangedUrlAction, +} from '../test_utils'; + +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + htmlIdGenerator: () => () => 'mockId', +})); + +const createStoreSetup = () => { + return createStore( + combineReducers({ + [MANAGEMENT_STORE_GLOBAL_NAMESPACE]: combineReducers({ + [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: trustedAppsPageReducer, + }), + }) + ); +}; + +const renderList = (store: ReturnType) => { + const Wrapper: React.FC = ({ children }) => {children}; + + return render(, { wrapper: Wrapper }); +}; + +describe('TrustedAppsList', () => { + it('renders correctly initially', () => { + expect(renderList(createStoreSetup()).container).toMatchSnapshot(); + }); + + it('renders correctly when loading data for the first time', () => { + const store = createStoreSetup(); + + store.dispatch( + createTrustedAppsListResourceStateChangedAction(createListLoadingResourceState()) + ); + + expect(renderList(store).container).toMatchSnapshot(); + }); + + it('renders correctly when failed loading data for the first time', () => { + const store = createStoreSetup(); + + store.dispatch( + createTrustedAppsListResourceStateChangedAction( + createListFailedResourceState('Intenal Server Error') + ) + ); + + expect(renderList(store).container).toMatchSnapshot(); + }); + + it('renders correctly when loaded data', () => { + const store = createStoreSetup(); + + store.dispatch( + createTrustedAppsListResourceStateChangedAction( + createListLoadedResourceState({ index: 0, size: 20 }, 200) + ) + ); + + expect(renderList(store).container).toMatchSnapshot(); + }); + + it('renders correctly when new page and page sie set (not loading yet)', () => { + const store = createStoreSetup(); + + store.dispatch( + createTrustedAppsListResourceStateChangedAction( + createListLoadedResourceState({ index: 0, size: 20 }, 200) + ) + ); + store.dispatch(createUserChangedUrlAction('/trusted_apps', '?page_index=2&page_size=50')); + + expect(renderList(store).container).toMatchSnapshot(); + }); + + it('renders correctly when loading data for the second time', () => { + const store = createStoreSetup(); + + store.dispatch( + createTrustedAppsListResourceStateChangedAction( + createListLoadingResourceState(createListLoadedResourceState({ index: 0, size: 20 }, 200)) + ) + ); + + expect(renderList(store).container).toMatchSnapshot(); + }); + + it('renders correctly when failed loading data for the second time', () => { + const store = createStoreSetup(); + + store.dispatch( + createTrustedAppsListResourceStateChangedAction( + createListFailedResourceState( + 'Intenal Server Error', + createListLoadedResourceState({ index: 0, size: 20 }, 200) + ) + ) + ); + + expect(renderList(store).container).toMatchSnapshot(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx new file mode 100644 index 0000000000000..a9077dd84913e --- /dev/null +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_list.tsx @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { memo, useCallback, useMemo } from 'react'; +import { useHistory } from 'react-router-dom'; +import { EuiBasicTable, EuiBasicTableColumn } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { Immutable } from '../../../../../common/endpoint/types'; +import { TrustedApp } from '../../../../../common/endpoint/types/trusted_apps'; +import { MANAGEMENT_PAGE_SIZE_OPTIONS } from '../../../common/constants'; +import { getTrustedAppsListPath } from '../../../common/routing'; + +import { + getListCurrentPageIndex, + getListCurrentPageSize, + getListErrorMessage, + getListItems, + getListTotalItemsCount, + isListLoading, +} from '../store/selectors'; + +import { useTrustedAppsSelector } from './hooks'; + +import { FormattedDate } from '../../../../common/components/formatted_date'; + +const OS_TITLES: Readonly<{ [K in TrustedApp['os']]: string }> = { + windows: i18n.translate('xpack.securitySolution.trustedapps.os.windows', { + defaultMessage: 'Windows', + }), + macos: i18n.translate('xpack.securitySolution.trustedapps.os.macos', { + defaultMessage: 'Mac OS', + }), + linux: i18n.translate('xpack.securitySolution.trustedapps.os.linux', { + defaultMessage: 'Linux', + }), +}; + +const COLUMN_TITLES: Readonly<{ [K in keyof Omit]: string }> = { + name: i18n.translate('xpack.securitySolution.trustedapps.list.columns.name', { + defaultMessage: 'Name', + }), + os: i18n.translate('xpack.securitySolution.trustedapps.list.columns.os', { + defaultMessage: 'OS', + }), + created_at: i18n.translate('xpack.securitySolution.trustedapps.list.columns.createdAt', { + defaultMessage: 'Date Created', + }), + created_by: i18n.translate('xpack.securitySolution.trustedapps.list.columns.createdBy', { + defaultMessage: 'Created By', + }), +}; + +const getColumnDefinitions = (): Array>> => [ + { + field: 'name', + name: COLUMN_TITLES.name, + }, + { + field: 'os', + name: COLUMN_TITLES.os, + render(value: TrustedApp['os'], record: Immutable) { + return OS_TITLES[value]; + }, + }, + { + field: 'created_at', + name: COLUMN_TITLES.created_at, + render(value: TrustedApp['created_at'], record: Immutable) { + return ( + + ); + }, + }, + { + field: 'created_by', + name: COLUMN_TITLES.created_by, + }, +]; + +export const TrustedAppsList = memo(() => { + const pageIndex = useTrustedAppsSelector(getListCurrentPageIndex); + const pageSize = useTrustedAppsSelector(getListCurrentPageSize); + const totalItemCount = useTrustedAppsSelector(getListTotalItemsCount); + const listItems = useTrustedAppsSelector(getListItems); + const history = useHistory(); + + return ( + [...listItems], [listItems])} + error={useTrustedAppsSelector(getListErrorMessage)} + loading={useTrustedAppsSelector(isListLoading)} + pagination={useMemo( + () => ({ + pageIndex, + pageSize, + totalItemCount, + hidePerPageOptions: false, + pageSizeOptions: [...MANAGEMENT_PAGE_SIZE_OPTIONS], + }), + [pageIndex, pageSize, totalItemCount] + )} + onChange={useCallback( + ({ page }: { page: { index: number; size: number } }) => { + history.push( + getTrustedAppsListPath({ + page_index: page.index, + page_size: page.size, + }) + ); + }, + [history] + )} + /> + ); +}); + +TrustedAppsList.displayName = 'TrustedAppsList'; diff --git a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx index 7045fa49ffad3..c0d3b9cd699de 100644 --- a/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/trusted_apps/view/trusted_apps_page.tsx @@ -3,11 +3,12 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import React from 'react'; +import React, { memo } from 'react'; import { FormattedMessage } from '@kbn/i18n/react'; import { AdministrationListPage } from '../../../components/administration_list_page'; +import { TrustedAppsList } from './trusted_apps_list'; -export function TrustedAppsPage() { +export const TrustedAppsPage = memo(() => { return ( } - /> + > + + ); -} +}); + +TrustedAppsPage.displayName = 'TrustedAppsPage'; diff --git a/x-pack/plugins/security_solution/public/management/store/middleware.ts b/x-pack/plugins/security_solution/public/management/store/middleware.ts index c7a7d2cad0623..77d02262e93b7 100644 --- a/x-pack/plugins/security_solution/public/management/store/middleware.ts +++ b/x-pack/plugins/security_solution/public/management/store/middleware.ts @@ -9,22 +9,22 @@ import { SecuritySubPluginMiddlewareFactory, State, } from '../../common/store'; -import { policyListMiddlewareFactory } from '../pages/policy/store/policy_list'; -import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details'; import { MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, MANAGEMENT_STORE_GLOBAL_NAMESPACE, MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, + MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE, } from '../common/constants'; +import { policyListMiddlewareFactory } from '../pages/policy/store/policy_list'; +import { policyDetailsMiddlewareFactory } from '../pages/policy/store/policy_details'; import { endpointMiddlewareFactory } from '../pages/endpoint_hosts/store/middleware'; +import { trustedAppsPageMiddlewareFactory } from '../pages/trusted_apps/store/middleware'; + +type ManagementSubStateKey = keyof State[typeof MANAGEMENT_STORE_GLOBAL_NAMESPACE]; -const policyListSelector = (state: State) => - state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]; -const policyDetailsSelector = (state: State) => - state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]; -const endpointsSelector = (state: State) => - state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]; +const createSubStateSelector = (namespace: K) => (state: State) => + state[MANAGEMENT_STORE_GLOBAL_NAMESPACE][namespace]; export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = ( coreStart, @@ -32,13 +32,20 @@ export const managementMiddlewareFactory: SecuritySubPluginMiddlewareFactory = ( ) => { return [ substateMiddlewareFactory( - policyListSelector, + createSubStateSelector(MANAGEMENT_STORE_POLICY_LIST_NAMESPACE), policyListMiddlewareFactory(coreStart, depsStart) ), substateMiddlewareFactory( - policyDetailsSelector, + createSubStateSelector(MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE), policyDetailsMiddlewareFactory(coreStart, depsStart) ), - substateMiddlewareFactory(endpointsSelector, endpointMiddlewareFactory(coreStart, depsStart)), + substateMiddlewareFactory( + createSubStateSelector(MANAGEMENT_STORE_ENDPOINTS_NAMESPACE), + endpointMiddlewareFactory(coreStart, depsStart) + ), + substateMiddlewareFactory( + createSubStateSelector(MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE), + trustedAppsPageMiddlewareFactory(coreStart, depsStart) + ), ]; }; diff --git a/x-pack/plugins/security_solution/public/management/store/reducer.ts b/x-pack/plugins/security_solution/public/management/store/reducer.ts index eafd69c875ff1..29eb2d289ae1c 100644 --- a/x-pack/plugins/security_solution/public/management/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/management/store/reducer.ts @@ -17,6 +17,7 @@ import { MANAGEMENT_STORE_ENDPOINTS_NAMESPACE, MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE, MANAGEMENT_STORE_POLICY_LIST_NAMESPACE, + MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE, } from '../common/constants'; import { ImmutableCombineReducers } from '../../common/store'; import { Immutable } from '../../../common/endpoint/types'; @@ -25,6 +26,10 @@ import { endpointListReducer, initialEndpointListState, } from '../pages/endpoint_hosts/store/reducer'; +import { + initialTrustedAppsPageState, + trustedAppsPageReducer, +} from '../pages/trusted_apps/store/reducer'; const immutableCombineReducers: ImmutableCombineReducers = combineReducers; @@ -35,6 +40,7 @@ export const mockManagementState: Immutable = { [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: initialPolicyListState(), [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: initialPolicyDetailsState(), [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: initialEndpointListState, + [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: initialTrustedAppsPageState, }; /** @@ -44,4 +50,5 @@ export const managementReducer = immutableCombineReducers({ [MANAGEMENT_STORE_POLICY_LIST_NAMESPACE]: policyListReducer, [MANAGEMENT_STORE_POLICY_DETAILS_NAMESPACE]: policyDetailsReducer, [MANAGEMENT_STORE_ENDPOINTS_NAMESPACE]: endpointListReducer, + [MANAGEMENT_STORE_TRUSTED_APPS_NAMESPACE]: trustedAppsPageReducer, }); diff --git a/x-pack/plugins/security_solution/public/management/types.ts b/x-pack/plugins/security_solution/public/management/types.ts index 21214241d1981..8b53f4c1d8525 100644 --- a/x-pack/plugins/security_solution/public/management/types.ts +++ b/x-pack/plugins/security_solution/public/management/types.ts @@ -8,6 +8,7 @@ import { CombinedState } from 'redux'; import { SecurityPageName } from '../app/types'; import { PolicyListState, PolicyDetailsState } from './pages/policy/types'; import { EndpointState } from './pages/endpoint_hosts/types'; +import { TrustedAppsListPageState } from './pages/trusted_apps/state/trusted_apps_list_page_state'; /** * The type for the management store global namespace. Used mostly internally to reference @@ -19,6 +20,7 @@ export type ManagementState = CombinedState<{ policyList: PolicyListState; policyDetails: PolicyDetailsState; endpoints: EndpointState; + trustedApps: TrustedAppsListPageState; }>; /** diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/details/__snapshots__/index.test.tsx.snap similarity index 100% rename from x-pack/plugins/security_solution/public/network/components/ip_overview/__snapshots__/index.test.tsx.snap rename to x-pack/plugins/security_solution/public/network/components/details/__snapshots__/index.test.tsx.snap diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx b/x-pack/plugins/security_solution/public/network/components/details/index.test.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/network/components/ip_overview/index.test.tsx rename to x-pack/plugins/security_solution/public/network/components/details/index.test.tsx diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx b/x-pack/plugins/security_solution/public/network/components/details/index.tsx similarity index 88% rename from x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx rename to x-pack/plugins/security_solution/public/network/components/details/index.tsx index d6dfe1769308e..2c6840895683c 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/details/index.tsx @@ -12,7 +12,7 @@ import React from 'react'; import { DEFAULT_DARK_MODE } from '../../../../common/constants'; import { DescriptionList } from '../../../../common/utility_types'; import { useUiSetting$ } from '../../../common/lib/kibana'; -import { FlowTarget, IpOverviewData, Overview } from '../../../graphql/types'; +import { FlowTarget, NetworkDetailsStrategyResponse } from '../../../../common/search_strategy'; import { networkModel } from '../../store'; import { getEmptyTagValue } from '../../../common/components/empty_value'; @@ -34,8 +34,8 @@ import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_ca import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; -interface OwnProps { - data: IpOverviewData; +export interface IpOverviewProps { + data: NetworkDetailsStrategyResponse['networkDetails']; flowTarget: FlowTarget; id: string; ip: string; @@ -48,15 +48,11 @@ interface OwnProps { narrowDateRange: NarrowDateRange; } -export type IpOverviewProps = OwnProps; - -const getDescriptionList = (descriptionList: DescriptionList[], key: number) => { - return ( - - - - ); -}; +const getDescriptionList = (descriptionList: DescriptionList[], key: number) => ( + + + +); export const IpOverview = React.memo( ({ @@ -74,7 +70,7 @@ export const IpOverview = React.memo( const capabilities = useMlCapabilities(); const userPermissions = hasMlUserPermissions(capabilities); const [darkMode] = useUiSetting$(DEFAULT_DARK_MODE); - const typeData: Overview = data[flowTarget]!; + const typeData = data[flowTarget]!; const column: DescriptionList[] = [ { title: i18n.LOCATION, @@ -124,13 +120,14 @@ export const IpOverview = React.memo( [ { title: i18n.HOST_ID, - description: typeData - ? hostIdRenderer({ host: data.host, ipFilter: ip }) - : getEmptyTagValue(), + description: + typeData && data.host + ? hostIdRenderer({ host: data.host, ipFilter: ip }) + : getEmptyTagValue(), }, { title: i18n.HOST_NAME, - description: typeData ? hostNameRenderer(data.host, ip) : getEmptyTagValue(), + description: typeData && data.host ? hostNameRenderer(data.host, ip) : getEmptyTagValue(), }, ], [ diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/mock.ts b/x-pack/plugins/security_solution/public/network/components/details/mock.ts similarity index 85% rename from x-pack/plugins/security_solution/public/network/components/ip_overview/mock.ts rename to x-pack/plugins/security_solution/public/network/components/details/mock.ts index aa86fb177b02a..b1187a9b22d94 100644 --- a/x-pack/plugins/security_solution/public/network/components/ip_overview/mock.ts +++ b/x-pack/plugins/security_solution/public/network/components/details/mock.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { IpOverviewData } from '../../../graphql/types'; +import { NetworkDetailsStrategyResponse } from '../../../../common/search_strategy'; -export const mockData: Readonly> = { +export const mockData: Readonly> = { complete: { source: { firstSeen: '2019-02-07T17:19:41.636Z', @@ -16,7 +19,7 @@ export const mockData: Readonly> = { continent_name: ['North America'], city_name: ['New York'], country_iso_code: ['US'], - country_name: null, + country_name: undefined, location: { lat: [40.7214], lon: [-74.0052], @@ -33,7 +36,7 @@ export const mockData: Readonly> = { continent_name: ['North America'], city_name: ['New York'], country_iso_code: ['US'], - country_name: null, + country_name: undefined, location: { lat: [40.7214], lon: [-74.0052], diff --git a/x-pack/plugins/security_solution/public/network/components/ip_overview/translations.ts b/x-pack/plugins/security_solution/public/network/components/details/translations.ts similarity index 100% rename from x-pack/plugins/security_solution/public/network/components/ip_overview/translations.ts rename to x-pack/plugins/security_solution/public/network/components/details/translations.ts diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx index 7c05e93e6876e..27fe27adc99c2 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.test.tsx @@ -11,7 +11,7 @@ import '../../../../common/mock/match_media'; import { getRenderedFieldValue, PointToolTipContentComponent } from './point_tool_tip_content'; import { TestProviders } from '../../../../common/mock'; import { getEmptyStringTag } from '../../../../common/components/empty_value'; -import { HostDetailsLink, IPDetailsLink } from '../../../../common/components/links'; +import { HostDetailsLink, NetworkDetailsLink } from '../../../../common/components/links'; import { FlowTarget } from '../../../../graphql/types'; import { TooltipProperty, @@ -50,17 +50,17 @@ describe('PointToolTipContent', () => { ); }); - test('it returns IPDetailsLink if field is source.ip', () => { + test('it returns NetworkDetailsLink if field is source.ip', () => { const value = '127.0.0.1'; expect(getRenderedFieldValue('source.ip', value)).toStrictEqual( - + ); }); - test('it returns IPDetailsLink if field is destination.ip', () => { + test('it returns NetworkDetailsLink if field is destination.ip', () => { const value = '127.0.0.1'; expect(getRenderedFieldValue('destination.ip', value)).toStrictEqual( - + ); }); diff --git a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx index f38f3e054a645..57113a1395778 100644 --- a/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx +++ b/x-pack/plugins/security_solution/public/network/components/embeddables/map_tool_tip/point_tool_tip_content.tsx @@ -11,7 +11,7 @@ import { getOrEmptyTagFromValue, } from '../../../../common/components/empty_value'; import { DescriptionListStyled } from '../../../../common/components/page'; -import { HostDetailsLink, IPDetailsLink } from '../../../../common/components/links'; +import { HostDetailsLink, NetworkDetailsLink } from '../../../../common/components/links'; import { DefaultFieldRenderer } from '../../../../timelines/components/field_renderers/field_renderers'; import { FlowTarget } from '../../../../graphql/types'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths @@ -67,7 +67,7 @@ export const getRenderedFieldValue = (field: string, value: string) => { return ; } else if (['source.ip', 'destination.ip'].includes(field)) { const flowTarget = field.split('.')[0] as FlowTarget; - return ; + return ; } return <>{value}; }; diff --git a/x-pack/plugins/security_solution/public/network/components/flow_target_select_connected/index.tsx b/x-pack/plugins/security_solution/public/network/components/flow_target_select_connected/index.tsx index 3ce623cfc97b8..6ddcf9119b215 100644 --- a/x-pack/plugins/security_solution/public/network/components/flow_target_select_connected/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/flow_target_select_connected/index.tsx @@ -11,7 +11,7 @@ import { useHistory, useLocation } from 'react-router-dom'; import styled from 'styled-components'; import { FlowDirection, FlowTarget } from '../../../graphql/types'; -import * as i18nIp from '../ip_overview/translations'; +import * as i18nIp from '../details/translations'; import { FlowTargetSelect } from '../flow_controls/flow_target_select'; import { IpOverviewId } from '../../../timelines/components/field_renderers/field_renderers'; @@ -40,7 +40,7 @@ export const FlowTargetSelectConnectedComponent: React.FC = ({ flowTarget const history = useHistory(); const location = useLocation(); - const updateIpDetailsFlowTarget = useCallback( + const updateNetworkDetailsFlowTarget = useCallback( (newFlowTarget: FlowTarget) => { const newPath = getUpdatedFlowTargetPath(location, flowTarget, newFlowTarget); history.push(newPath); @@ -56,7 +56,7 @@ export const FlowTargetSelectConnectedComponent: React.FC = ({ flowTarget selectedDirection={FlowDirection.uniDirectional} selectedTarget={flowTarget} displayTextOverride={[i18nIp.AS_SOURCE, i18nIp.AS_DESTINATION]} - updateFlowTargetAction={updateIpDetailsFlowTarget} + updateFlowTargetAction={updateNetworkDetailsFlowTarget} /> ); diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap index 875a490d86be3..7adee9531b1f3 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/__snapshots__/index.test.tsx.snap @@ -1,102 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`NetworkHttp Table Component rendering it renders the default NetworkHttp table 1`] = ` - -`; +exports[`NetworkHttp Table Component rendering it renders the default NetworkHttp table 1`] = `null`; diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/columns.tsx b/x-pack/plugins/security_solution/public/network/components/network_http_table/columns.tsx index 0c5856241c5a3..42e52e3a86f43 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_http_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/columns.tsx @@ -8,10 +8,14 @@ import React from 'react'; import numeral from '@elastic/numeral'; -import { NetworkHttpEdges, NetworkHttpFields, NetworkHttpItem } from '../../../graphql/types'; +import { + NetworkHttpEdges, + NetworkHttpFields, + NetworkHttpItem, +} from '../../../../common/search_strategy/security_solution/network'; import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; import { getEmptyTagValue } from '../../../common/components/empty_value'; -import { IPDetailsLink } from '../../../common/components/links'; +import { NetworkDetailsLink } from '../../../common/components/links'; import { Columns } from '../../../common/components/paginated_table'; import * as i18n from './translations'; @@ -98,7 +102,7 @@ export const getNetworkHttpColumns = (tableId: string): NetworkHttpColumns => [ attrName: 'source.ip', idPrefix: escapeDataProviderId(`${tableId}-table-lastSourceIp-${path}`), rowItem: lastSourceIp, - render: () => , + render: () => , }) : getEmptyTagValue(), }, diff --git a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx index ed25373018207..ccb238bcee274 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_http_table/index.tsx @@ -5,17 +5,17 @@ */ import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import { networkActions, networkModel, networkSelectors } from '../../store'; -import { Direction, NetworkHttpEdges, NetworkHttpFields } from '../../../graphql/types'; +import { NetworkHttpEdges, NetworkHttpFields } from '../../../../common/search_strategy'; import { State } from '../../../common/store'; import { Criteria, ItemsPerRow, PaginatedTable } from '../../../common/components/paginated_table'; import { getNetworkHttpColumns } from './columns'; import * as i18n from './translations'; -interface OwnProps { +interface NetworkHttpTableProps { data: NetworkHttpEdges[]; fakeTotalCount: number; id: string; @@ -27,8 +27,6 @@ interface OwnProps { type: networkModel.NetworkType; } -type NetworkHttpTableProps = OwnProps & PropsFromRedux; - const rowItems: ItemsPerRow[] = [ { text: i18n.ROWS_5, @@ -41,60 +39,68 @@ const rowItems: ItemsPerRow[] = [ ]; const NetworkHttpTableComponent: React.FC = ({ - activePage, data, fakeTotalCount, id, isInspect, - limit, loading, loadPage, showMorePagesIndicator, - sort, totalCount, type, - updateNetworkTable, }) => { + const dispatch = useDispatch(); + const getNetworkHttpSelector = networkSelectors.httpSelector(); + const { activePage, limit, sort } = useSelector( + (state: State) => getNetworkHttpSelector(state, type), + shallowEqual + ); const tableType = type === networkModel.NetworkType.page ? networkModel.NetworkTableType.http - : networkModel.IpDetailsTableType.http; + : networkModel.NetworkDetailsTableType.http; const updateLimitPagination = useCallback( (newLimit) => - updateNetworkTable({ - networkType: type, - tableType, - updates: { limit: newLimit }, - }), - [type, updateNetworkTable, tableType] + dispatch( + networkActions.updateNetworkTable({ + networkType: type, + tableType, + updates: { limit: newLimit }, + }) + ), + [dispatch, type, tableType] ); const updateActivePage = useCallback( (newPage) => - updateNetworkTable({ - networkType: type, - tableType, - updates: { activePage: newPage }, - }), - [type, updateNetworkTable, tableType] + dispatch( + networkActions.updateNetworkTable({ + networkType: type, + tableType, + updates: { activePage: newPage }, + }) + ), + [dispatch, type, tableType] ); const onChange = useCallback( (criteria: Criteria) => { if (criteria.sort != null && criteria.sort.direction !== sort.direction) { - updateNetworkTable({ - networkType: type, - tableType, - updates: { - sort: { - direction: criteria.sort.direction as Direction, + dispatch( + networkActions.updateNetworkTable({ + networkType: type, + tableType, + updates: { + sort: { + direction: criteria.sort.direction, + }, }, - }, - }); + }) + ); } }, - [tableType, sort.direction, type, updateNetworkTable] + [sort.direction, dispatch, type, tableType] ); const sorting = { field: `node.${NetworkHttpFields.requestCount}`, direction: sort.direction }; @@ -128,18 +134,4 @@ const NetworkHttpTableComponent: React.FC = ({ NetworkHttpTableComponent.displayName = 'NetworkHttpTableComponent'; -const makeMapStateToProps = () => { - const getNetworkHttpSelector = networkSelectors.httpSelector(); - const mapStateToProps = (state: State, { type }: OwnProps) => getNetworkHttpSelector(state, type); - return mapStateToProps; -}; - -const mapDispatchToProps = { - updateNetworkTable: networkActions.updateNetworkTable, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; - -export const NetworkHttpTable = connector(React.memo(NetworkHttpTableComponent)); +export const NetworkHttpTable = React.memo(NetworkHttpTableComponent); diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/columns.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/columns.tsx index d6661ed48197a..634bf2ba35d76 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/columns.tsx @@ -37,7 +37,7 @@ export type NetworkTopCountriesColumns = [ Columns ]; -export type NetworkTopCountriesColumnsIpDetails = [ +export type NetworkTopCountriesColumnsNetworkDetails = [ Columns, Columns, Columns, @@ -164,7 +164,7 @@ export const getCountriesColumnsCurated = ( flowTarget: FlowTargetSourceDest, type: networkModel.NetworkType, tableId: string -): NetworkTopCountriesColumns | NetworkTopCountriesColumnsIpDetails => { +): NetworkTopCountriesColumns | NetworkTopCountriesColumnsNetworkDetails => { const columns = getNetworkTopCountriesColumns(indexPattern, flowTarget, type, tableId); // Columns to exclude from host details pages diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx index 114bca9f59d9c..2495f9e7c11c8 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_countries_table/index.tsx @@ -88,8 +88,8 @@ const NetworkTopCountriesTableComponent: React.FC } return flowTargeted === FlowTargetSourceDest.source - ? networkModel.IpDetailsTableType.topCountriesSource - : networkModel.IpDetailsTableType.topCountriesDestination; + ? networkModel.NetworkDetailsTableType.topCountriesSource + : networkModel.NetworkDetailsTableType.topCountriesDestination; }, [flowTargeted, type]); const field = diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/columns.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/columns.tsx index 16c26d5077c8d..ae390e8e2ed71 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/columns.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/columns.tsx @@ -22,7 +22,7 @@ import { } from '../../../common/components/drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; import { getEmptyTagValue } from '../../../common/components/empty_value'; -import { IPDetailsLink } from '../../../common/components/links'; +import { NetworkDetailsLink } from '../../../common/components/links'; import { Columns } from '../../../common/components/paginated_table'; import { IS_OPERATOR } from '../../../timelines/components/timeline/data_providers/data_provider'; import { Provider } from '../../../timelines/components/timeline/data_providers/provider'; @@ -43,7 +43,7 @@ export type NetworkTopNFlowColumns = [ Columns ]; -export type NetworkTopNFlowColumnsIpDetails = [ +export type NetworkTopNFlowColumnsNetworkDetails = [ Columns, Columns, Columns, @@ -86,7 +86,7 @@ export const getNetworkTopNFlowColumns = ( ) : ( - + ) } /> @@ -239,7 +239,7 @@ export const getNFlowColumnsCurated = ( flowTarget: FlowTargetSourceDest, type: networkModel.NetworkType, tableId: string -): NetworkTopNFlowColumns | NetworkTopNFlowColumnsIpDetails => { +): NetworkTopNFlowColumns | NetworkTopNFlowColumnsNetworkDetails => { const columns = getNetworkTopNFlowColumns(flowTarget, tableId); // Columns to exclude from host details pages diff --git a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx index 30b70872432f9..757b178431d90 100644 --- a/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/network_top_n_flow_table/index.tsx @@ -82,8 +82,8 @@ const NetworkTopNFlowTableComponent: React.FC = ({ } else { tableType = flowTargeted === FlowTargetSourceDest.source - ? networkModel.IpDetailsTableType.topNFlowSource - : networkModel.IpDetailsTableType.topNFlowDestination; + ? networkModel.NetworkDetailsTableType.topNFlowSource + : networkModel.NetworkDetailsTableType.topNFlowDestination; } const onChange = useCallback( diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/tls_table/__snapshots__/index.test.tsx.snap index 8b7d8efa7ac37..a9c3a1a006268 100644 --- a/x-pack/plugins/security_solution/public/network/components/tls_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/tls_table/__snapshots__/index.test.tsx.snap @@ -1,79 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Tls Table Component Rendering it renders the default Domains table 1`] = ` - -`; +exports[`Tls Table Component Rendering it renders the default Domains table 1`] = `null`; diff --git a/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx index d0e001466518d..3d19eedc06a8e 100644 --- a/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/tls_table/index.tsx @@ -5,11 +5,16 @@ */ import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { networkActions, networkModel, networkSelectors } from '../../store'; -import { TlsEdges, TlsSortField, TlsFields, Direction } from '../../../graphql/types'; +import { + Direction, + NetworkTlsEdges, + NetworkTlsFields, + SortField, +} from '../../../../common/search_strategy'; import { State } from '../../../common/store'; import { Criteria, @@ -20,8 +25,8 @@ import { import { getTlsColumns } from './columns'; import * as i18n from './translations'; -interface OwnProps { - data: TlsEdges[]; +interface TlsTableProps { + data: NetworkTlsEdges[]; fakeTotalCount: number; id: string; isInspect: boolean; @@ -32,8 +37,6 @@ interface OwnProps { type: networkModel.NetworkType; } -type TlsTableProps = OwnProps & PropsFromRedux; - const rowItems: ItemsPerRow[] = [ { text: i18n.ROWS_5, @@ -47,123 +50,115 @@ const rowItems: ItemsPerRow[] = [ export const tlsTableId = 'tls-table'; -const TlsTableComponent = React.memo( - ({ - activePage, - data, - fakeTotalCount, - id, - isInspect, - limit, - loading, - loadPage, - showMorePagesIndicator, - sort, - totalCount, - type, - updateNetworkTable, - }) => { - const tableType: networkModel.TopTlsTableType = - type === networkModel.NetworkType.page - ? networkModel.NetworkTableType.tls - : networkModel.IpDetailsTableType.tls; - - const updateLimitPagination = useCallback( - (newLimit) => - updateNetworkTable({ +const TlsTableComponent: React.FC = ({ + data, + fakeTotalCount, + id, + isInspect, + loading, + loadPage, + showMorePagesIndicator, + totalCount, + type, +}) => { + const dispatch = useDispatch(); + const getTlsSelector = networkSelectors.tlsSelector(); + const { activePage, limit, sort } = useSelector( + (state: State) => getTlsSelector(state, type), + shallowEqual + ); + const tableType: networkModel.TopTlsTableType = + type === networkModel.NetworkType.page + ? networkModel.NetworkTableType.tls + : networkModel.NetworkDetailsTableType.tls; + + const updateLimitPagination = useCallback( + (newLimit) => + dispatch( + networkActions.updateNetworkTable({ networkType: type, tableType, updates: { limit: newLimit }, - }), - [type, updateNetworkTable, tableType] - ); - - const updateActivePage = useCallback( - (newPage) => - updateNetworkTable({ + }) + ), + [dispatch, type, tableType] + ); + + const updateActivePage = useCallback( + (newPage) => + dispatch( + networkActions.updateNetworkTable({ networkType: type, tableType, updates: { activePage: newPage }, - }), - [type, updateNetworkTable, tableType] - ); - - const onChange = useCallback( - (criteria: Criteria) => { - if (criteria.sort != null) { - const splitField = criteria.sort.field.split('.'); - const newTlsSort: TlsSortField = { - field: getSortFromString(splitField[splitField.length - 1]), - direction: criteria.sort.direction as Direction, - }; - if (!deepEqual(newTlsSort, sort)) { - updateNetworkTable({ + }) + ), + [dispatch, type, tableType] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const splitField = criteria.sort.field.split('.'); + const newTlsSort: SortField = { + field: getSortFromString(splitField[splitField.length - 1]), + direction: criteria.sort.direction as Direction, + }; + if (!deepEqual(newTlsSort, sort)) { + dispatch( + networkActions.updateNetworkTable({ networkType: type, tableType, updates: { sort: newTlsSort }, - }); - } + }) + ); } - }, - [sort, type, tableType, updateNetworkTable] - ); - - // eslint-disable-next-line react-hooks/exhaustive-deps - const columns = useMemo(() => getTlsColumns(tlsTableId), [tlsTableId]); - - return ( - - ); - } -); - -TlsTableComponent.displayName = 'TlsTableComponent'; - -const makeMapStateToProps = () => { - const getTlsSelector = networkSelectors.tlsSelector(); - return (state: State, { type }: OwnProps) => getTlsSelector(state, type); + } + }, + [sort, dispatch, type, tableType] + ); + + const columns = useMemo(() => getTlsColumns(tlsTableId), []); + + return ( + + ); }; -const mapDispatchToProps = { - updateNetworkTable: networkActions.updateNetworkTable, -}; - -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; +TlsTableComponent.displayName = 'TlsTableComponent'; -export const TlsTable = connector(TlsTableComponent); +export const TlsTable = React.memo(TlsTableComponent); -const getSortField = (sortField: TlsSortField): SortingBasicTable => ({ +const getSortField = (sortField: SortField): SortingBasicTable => ({ field: `node.${sortField.field}`, direction: sortField.direction, }); -const getSortFromString = (sortField: string): TlsFields => { +const getSortFromString = (sortField: string): NetworkTlsFields => { switch (sortField) { case '_id': - return TlsFields._id; + return NetworkTlsFields._id; default: - return TlsFields._id; + return NetworkTlsFields._id; } }; diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/__snapshots__/index.test.tsx.snap b/x-pack/plugins/security_solution/public/network/components/users_table/__snapshots__/index.test.tsx.snap index 634253d03291f..934a28fbc4dd0 100644 --- a/x-pack/plugins/security_solution/public/network/components/users_table/__snapshots__/index.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/network/components/users_table/__snapshots__/index.test.tsx.snap @@ -1,83 +1,3 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Users Table Component Rendering it renders the default Users table 1`] = ` - -`; +exports[`Users Table Component Rendering it renders the default Users table 1`] = `null`; diff --git a/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx b/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx index 9a971e0087d12..0355d0a30cfa4 100644 --- a/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx +++ b/x-pack/plugins/security_solution/public/network/components/users_table/index.tsx @@ -5,7 +5,7 @@ */ import React, { useCallback, useMemo } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; import deepEqual from 'fast-deep-equal'; import { assertUnreachable } from '../../../../common/utility_types'; @@ -13,11 +13,10 @@ import { networkActions, networkModel, networkSelectors } from '../../store'; import { Direction, FlowTarget, - UsersEdges, - UsersFields, - UsersSortField, -} from '../../../graphql/types'; -import { State } from '../../../common/store'; + NetworkUsersEdges, + NetworkUsersFields, + SortField, +} from '../../../../common/search_strategy'; import { Criteria, ItemsPerRow, @@ -27,10 +26,10 @@ import { import { getUsersColumns } from './columns'; import * as i18n from './translations'; -const tableType = networkModel.IpDetailsTableType.users; +const tableType = networkModel.NetworkDetailsTableType.users; -interface OwnProps { - data: UsersEdges[]; +interface UsersTableProps { + data: NetworkUsersEdges[]; flowTarget: FlowTarget; fakeTotalCount: number; id: string; @@ -42,8 +41,6 @@ interface OwnProps { type: networkModel.NetworkType; } -type UsersTableProps = OwnProps & PropsFromRedux; - const rowItems: ItemsPerRow[] = [ { text: i18n.ROWS_5, @@ -57,122 +54,110 @@ const rowItems: ItemsPerRow[] = [ export const usersTableId = 'users-table'; -const UsersTableComponent = React.memo( - ({ - activePage, - data, - fakeTotalCount, - flowTarget, - id, - isInspect, - limit, - loading, - loadPage, - showMorePagesIndicator, - totalCount, - type, - updateNetworkTable, - sort, - }) => { - const updateLimitPagination = useCallback( - (newLimit) => - updateNetworkTable({ +const UsersTableComponent: React.FC = ({ + data, + fakeTotalCount, + flowTarget, + id, + isInspect, + loading, + loadPage, + showMorePagesIndicator, + totalCount, + type, +}) => { + const dispatch = useDispatch(); + const getUsersSelector = networkSelectors.usersSelector(); + const { activePage, sort, limit } = useSelector(getUsersSelector, shallowEqual); + const updateLimitPagination = useCallback( + (newLimit) => + dispatch( + networkActions.updateNetworkTable({ networkType: type, tableType, updates: { limit: newLimit }, - }), - [type, updateNetworkTable] - ); - - const updateActivePage = useCallback( - (newPage) => - updateNetworkTable({ + }) + ), + [dispatch, type] + ); + + const updateActivePage = useCallback( + (newPage) => + dispatch( + networkActions.updateNetworkTable({ networkType: type, tableType, updates: { activePage: newPage }, - }), - [type, updateNetworkTable] - ); - - const onChange = useCallback( - (criteria: Criteria) => { - if (criteria.sort != null) { - const splitField = criteria.sort.field.split('.'); - const newUsersSort: UsersSortField = { - field: getSortFromString(splitField[splitField.length - 1]), - direction: criteria.sort.direction as Direction, - }; - if (!deepEqual(newUsersSort, sort)) { - updateNetworkTable({ + }) + ), + [dispatch, type] + ); + + const onChange = useCallback( + (criteria: Criteria) => { + if (criteria.sort != null) { + const splitField = criteria.sort.field.split('.'); + const newUsersSort: SortField = { + field: getSortFromString(splitField[splitField.length - 1]), + direction: criteria.sort.direction as Direction, + }; + if (!deepEqual(newUsersSort, sort)) { + dispatch( + networkActions.updateNetworkTable({ networkType: type, tableType, updates: { sort: newUsersSort }, - }); - } + }) + ); } - }, - [sort, type, updateNetworkTable] - ); - - // eslint-disable-next-line react-hooks/exhaustive-deps - const columns = useMemo(() => getUsersColumns(flowTarget, usersTableId), [ - flowTarget, - usersTableId, - ]); - - return ( - - ); - } -); + } + }, + [dispatch, sort, type] + ); -UsersTableComponent.displayName = 'UsersTableComponent'; - -const makeMapStateToProps = () => { - const getUsersSelector = networkSelectors.usersSelector(); - return (state: State) => ({ - ...getUsersSelector(state), - }); -}; - -const mapDispatchToProps = { - updateNetworkTable: networkActions.updateNetworkTable, + // eslint-disable-next-line react-hooks/exhaustive-deps + const columns = useMemo(() => getUsersColumns(flowTarget, usersTableId), [ + flowTarget, + usersTableId, + ]); + + return ( + + ); }; -const connector = connect(makeMapStateToProps, mapDispatchToProps); - -type PropsFromRedux = ConnectedProps; +UsersTableComponent.displayName = 'UsersTableComponent'; -export const UsersTable = connector(UsersTableComponent); +export const UsersTable = React.memo(UsersTableComponent); -const getSortField = (sortField: UsersSortField): SortingBasicTable => { +const getSortField = (sortField: SortField): SortingBasicTable => { switch (sortField.field) { - case UsersFields.name: + case NetworkUsersFields.name: return { field: `node.user.${sortField.field}`, direction: sortField.direction, }; - case UsersFields.count: + case NetworkUsersFields.count: return { field: `node.user.${sortField.field}`, direction: sortField.direction, @@ -181,13 +166,13 @@ const getSortField = (sortField: UsersSortField): SortingBasicTable => { return assertUnreachable(sortField.field); }; -const getSortFromString = (sortField: string): UsersFields => { +const getSortFromString = (sortField: string): NetworkUsersFields => { switch (sortField) { - case UsersFields.name.valueOf(): - return UsersFields.name; - case UsersFields.count.valueOf(): - return UsersFields.count; + case NetworkUsersFields.name.valueOf(): + return NetworkUsersFields.name; + case NetworkUsersFields.count.valueOf(): + return NetworkUsersFields.count; default: - return UsersFields.name; + return NetworkUsersFields.name; } }; diff --git a/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.gql_query.ts b/x-pack/plugins/security_solution/public/network/containers/details/index.gql_query.ts similarity index 100% rename from x-pack/plugins/security_solution/public/network/containers/ip_overview/index.gql_query.ts rename to x-pack/plugins/security_solution/public/network/containers/details/index.gql_query.ts diff --git a/x-pack/plugins/security_solution/public/network/containers/details/index.tsx b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx new file mode 100644 index 0000000000000..597f85ff082e2 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/details/index.tsx @@ -0,0 +1,153 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { noop } from 'lodash/fp'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import deepEqual from 'fast-deep-equal'; + +import { ESTermQuery } from '../../../../common/typed_json'; +import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; +import { inputsModel } from '../../../common/store'; +import { useKibana } from '../../../common/lib/kibana'; +import { createFilter } from '../../../common/containers/helpers'; +import { + DocValueFields, + NetworkQueries, + NetworkDetailsRequestOptions, + NetworkDetailsStrategyResponse, +} from '../../../../common/search_strategy'; +import { AbortError } from '../../../../../../../src/plugins/data/common'; +import * as i18n from './translations'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; + +const ID = 'networkDetailsQuery'; + +export interface NetworkDetailsArgs { + id: string; + inspect: InspectResponse; + networkDetails: NetworkDetailsStrategyResponse['networkDetails']; + refetch: inputsModel.Refetch; + isInspected: boolean; +} + +interface UseNetworkDetails { + id?: string; + docValueFields: DocValueFields[]; + ip: string; + filterQuery?: ESTermQuery | string; + skip: boolean; +} + +export const useNetworkDetails = ({ + docValueFields, + filterQuery, + id = ID, + skip, + ip, +}: UseNetworkDetails): [boolean, NetworkDetailsArgs] => { + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + + const [networkDetailsRequest, setNetworkDetailsRequest] = useState({ + defaultIndex, + docValueFields: docValueFields ?? [], + factoryQueryType: NetworkQueries.details, + filterQuery: createFilter(filterQuery), + ip, + }); + + const [networkDetailsResponse, setNetworkDetailsResponse] = useState({ + networkDetails: {}, + id, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + refetch: refetch.current, + }); + + const networkDetailsSearch = useCallback( + (request: NetworkDetailsRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setNetworkDetailsResponse((prevResponse) => ({ + ...prevResponse, + networkDetails: response.networkDetails, + inspect: getInspectResponse(response, prevResponse.inspect), + refetch: refetch.current, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_NETWORK_DETAILS); + searchSubscription$.unsubscribe(); + } + }, + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_NETWORK_DETAILS, + text: msg.message, + }); + } + }, + }); + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); + + useEffect(() => { + setNetworkDetailsRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + ip, + docValueFields: docValueFields ?? [], + filterQuery: createFilter(filterQuery), + }; + if (!skip && !deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [defaultIndex, filterQuery, skip, ip, docValueFields]); + + useEffect(() => { + networkDetailsSearch(networkDetailsRequest); + }, [networkDetailsRequest, networkDetailsSearch]); + + return [loading, networkDetailsResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/network/containers/details/translations.ts b/x-pack/plugins/security_solution/public/network/containers/details/translations.ts new file mode 100644 index 0000000000000..4aedd685630b7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/details/translations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_NETWORK_DETAILS = i18n.translate( + 'xpack.securitySolution.networkDetails.errorSearchDescription', + { + defaultMessage: `An error has occurred on network details search`, + } +); + +export const FAIL_NETWORK_DETAILS = i18n.translate( + 'xpack.securitySolution.networkDetails.failSearchDescription', + { + defaultMessage: `Failed to run search on network details`, + } +); diff --git a/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.tsx b/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.tsx deleted file mode 100644 index 6c8b54cc79517..0000000000000 --- a/x-pack/plugins/security_solution/public/network/containers/ip_overview/index.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; - -import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { GetIpOverviewQuery, IpOverviewData } from '../../../graphql/types'; -import { inputsModel, inputsSelectors, State } from '../../../common/store'; -import { useUiSetting } from '../../../common/lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; -import { QueryTemplateProps } from '../../../common/containers/query_template'; -import { networkModel } from '../../store'; -import { ipOverviewQuery } from './index.gql_query'; - -const ID = 'ipOverviewQuery'; - -export interface IpOverviewArgs { - id: string; - inspect: inputsModel.InspectQuery; - ipOverviewData: IpOverviewData; - loading: boolean; - refetch: inputsModel.Refetch; -} - -export interface IpOverviewProps extends QueryTemplateProps { - children: (args: IpOverviewArgs) => React.ReactNode; - type: networkModel.NetworkType; - ip: string; -} - -const IpOverviewComponentQuery = React.memo( - ({ id = ID, docValueFields, isInspected, children, filterQuery, skip, sourceId, ip }) => ( - - query={ipOverviewQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={{ - sourceId, - filterQuery: createFilter(filterQuery), - ip, - defaultIndex: useUiSetting(DEFAULT_INDEX_KEY), - docValueFields: docValueFields ?? [], - inspect: isInspected, - }} - > - {({ data, loading, refetch }) => { - const init: IpOverviewData = { host: {} }; - const ipOverviewData: IpOverviewData = getOr(init, 'source.IpOverview', data); - return children({ - id, - inspect: getOr(null, 'source.IpOverview.inspect', data), - ipOverviewData, - loading, - refetch, - }); - }} - - ) -); - -IpOverviewComponentQuery.displayName = 'IpOverviewComponentQuery'; - -const makeMapStateToProps = () => { - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: IpOverviewProps) => { - const { isInspected } = getQuery(state, id); - return { - isInspected, - }; - }; - return mapStateToProps; -}; - -const connector = connect(makeMapStateToProps); - -type PropsFromRedux = ConnectedProps; - -export const IpOverviewQuery = connector(IpOverviewComponentQuery); diff --git a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx index 53d9a303ab849..6f48cba2ebda1 100644 --- a/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/network_dns/index.tsx @@ -175,10 +175,6 @@ export const useNetworkDns = ({ ); useEffect(() => { - if (skip) { - return; - } - setNetworkDnsRequest((prevRequest) => { const myRequest = { ...prevRequest, @@ -193,7 +189,7 @@ export const useNetworkDns = ({ to: endDate, }, }; - if (!deepEqual(prevRequest, myRequest)) { + if (!skip && !deepEqual(prevRequest, myRequest)) { return myRequest; } return prevRequest; diff --git a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx index efbeb3eb00542..608ccdb084709 100644 --- a/x-pack/plugins/security_solution/public/network/containers/users/index.tsx +++ b/x-pack/plugins/security_solution/public/network/containers/users/index.tsx @@ -4,154 +4,195 @@ * you may not use this file except in compliance with the Elastic License. */ -import { getOr } from 'lodash/fp'; -import React from 'react'; -import { Query } from 'react-apollo'; -import { connect, ConnectedProps } from 'react-redux'; -import { compose } from 'redux'; +import { noop } from 'lodash/fp'; +import { useState, useEffect, useCallback, useRef } from 'react'; +import { shallowEqual, useSelector } from 'react-redux'; +import deepEqual from 'fast-deep-equal'; +import { ESTermQuery } from '../../../../common/typed_json'; import { DEFAULT_INDEX_KEY } from '../../../../common/constants'; -import { GetUsersQuery, FlowTarget, PageInfoPaginated, UsersEdges } from '../../../graphql/types'; -import { inputsModel, State, inputsSelectors } from '../../../common/store'; -import { withKibana, WithKibanaProps } from '../../../common/lib/kibana'; -import { createFilter, getDefaultFetchPolicy } from '../../../common/containers/helpers'; +import { inputsModel } from '../../../common/store'; +import { useKibana } from '../../../common/lib/kibana'; +import { createFilter } from '../../../common/containers/helpers'; +import { PageInfoPaginated } from '../../../graphql/types'; import { generateTablePaginationOptions } from '../../../common/components/paginated_table/helpers'; +import { networkSelectors } from '../../store'; import { - QueryTemplatePaginated, - QueryTemplatePaginatedProps, -} from '../../../common/containers/query_template_paginated'; -import { networkModel, networkSelectors } from '../../store'; + FlowTarget, + NetworkQueries, + NetworkUsersRequestOptions, + NetworkUsersStrategyResponse, +} from '../../../../common/search_strategy/security_solution/network'; +import { AbortError } from '../../../../../../../src/plugins/data/common'; +import * as i18n from './translations'; +import { getInspectResponse } from '../../../helpers'; +import { InspectResponse } from '../../../types'; -import { usersQuery } from './index.gql_query'; +const ID = 'networkUsersQuery'; -const ID = 'usersQuery'; - -export interface UsersArgs { +export interface NetworkUsersArgs { id: string; - inspect: inputsModel.InspectQuery; + inspect: InspectResponse; isInspected: boolean; - loading: boolean; loadPage: (newActivePage: number) => void; + networkUsers: NetworkUsersStrategyResponse['edges']; pageInfo: PageInfoPaginated; refetch: inputsModel.Refetch; + stackByField?: string; totalCount: number; - users: UsersEdges[]; } -export interface OwnProps extends QueryTemplatePaginatedProps { - children: (args: UsersArgs) => React.ReactNode; +interface UseNetworkUsers { + id?: string; + filterQuery?: ESTermQuery | string; + endDate: string; + startDate: string; + skip: boolean; flowTarget: FlowTarget; ip: string; - type: networkModel.NetworkType; } -type UsersProps = OwnProps & PropsFromRedux & WithKibanaProps; +export const useNetworkUsers = ({ + endDate, + filterQuery, + flowTarget, + id = ID, + ip, + skip, + startDate, +}: UseNetworkUsers): [boolean, NetworkUsersArgs] => { + const getNetworkUsersSelector = networkSelectors.usersSelector(); + const { activePage, sort, limit } = useSelector(getNetworkUsersSelector, shallowEqual); + const { data, notifications, uiSettings } = useKibana().services; + const refetch = useRef(noop); + const abortCtrl = useRef(new AbortController()); + const defaultIndex = uiSettings.get(DEFAULT_INDEX_KEY); + const [loading, setLoading] = useState(false); + + const [networkUsersRequest, setNetworkUsersRequest] = useState({ + defaultIndex, + factoryQueryType: NetworkQueries.users, + filterQuery: createFilter(filterQuery), + flowTarget, + ip, + pagination: generateTablePaginationOptions(activePage, limit), + sort, + timerange: { + interval: '12h', + from: startDate ? startDate : '', + to: endDate ? endDate : new Date(Date.now()).toISOString(), + }, + }); + + const wrappedLoadMore = useCallback( + (newActivePage: number) => { + setNetworkUsersRequest((prevRequest) => ({ + ...prevRequest, + pagination: generateTablePaginationOptions(newActivePage, limit), + })); + }, + [limit] + ); -class UsersComponentQuery extends QueryTemplatePaginated< - UsersProps, - GetUsersQuery.Query, - GetUsersQuery.Variables -> { - public render() { - const { - activePage, - children, - endDate, - filterQuery, - flowTarget, - id = ID, - ip, - isInspected, - kibana, - limit, - skip, - sourceId, - startDate, - sort, - } = this.props; - const variables: GetUsersQuery.Variables = { - defaultIndex: kibana.services.uiSettings.get(DEFAULT_INDEX_KEY), - filterQuery: createFilter(filterQuery), - flowTarget, - inspect: isInspected, - ip, - pagination: generateTablePaginationOptions(activePage, limit), - sort, - sourceId, - timerange: { - interval: '12h', - from: startDate!, - to: endDate!, - }, - }; - return ( - - query={usersQuery} - fetchPolicy={getDefaultFetchPolicy()} - notifyOnNetworkStatusChange - skip={skip} - variables={variables} - > - {({ data, loading, fetchMore, networkStatus, refetch }) => { - const users = getOr([], `source.Users.edges`, data); - this.setFetchMore(fetchMore); - this.setFetchMoreOptions((newActivePage: number) => ({ - variables: { - pagination: generateTablePaginationOptions(newActivePage, limit), + const [networkUsersResponse, setNetworkUsersResponse] = useState({ + networkUsers: [], + id, + inspect: { + dsl: [], + response: [], + }, + isInspected: false, + loadPage: wrappedLoadMore, + pageInfo: { + activePage: 0, + fakeTotalCount: 0, + showMorePagesIndicator: false, + }, + refetch: refetch.current, + totalCount: -1, + }); + + const networkUsersSearch = useCallback( + (request: NetworkUsersRequestOptions) => { + let didCancel = false; + const asyncSearch = async () => { + abortCtrl.current = new AbortController(); + setLoading(true); + + const searchSubscription$ = data.search + .search(request, { + strategy: 'securitySolutionSearchStrategy', + abortSignal: abortCtrl.current.signal, + }) + .subscribe({ + next: (response) => { + if (!response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + setNetworkUsersResponse((prevResponse) => ({ + ...prevResponse, + networkUsers: response.edges, + inspect: getInspectResponse(response, prevResponse.inspect), + pageInfo: response.pageInfo, + refetch: refetch.current, + totalCount: response.totalCount, + })); + } + searchSubscription$.unsubscribe(); + } else if (response.isPartial && !response.isRunning) { + if (!didCancel) { + setLoading(false); + } + // TODO: Make response error status clearer + notifications.toasts.addWarning(i18n.ERROR_NETWORK_USERS); + searchSubscription$.unsubscribe(); + } }, - updateQuery: (prev, { fetchMoreResult }) => { - if (!fetchMoreResult) { - return prev; + error: (msg) => { + if (!(msg instanceof AbortError)) { + notifications.toasts.addDanger({ + title: i18n.FAIL_NETWORK_USERS, + text: msg.message, + }); } - return { - ...fetchMoreResult, - source: { - ...fetchMoreResult.source, - Users: { - ...fetchMoreResult.source.Users, - edges: [...fetchMoreResult.source.Users.edges], - }, - }, - }; }, - })); - const isLoading = this.isItAValidLoading(loading, variables, networkStatus); - return children({ - id, - inspect: getOr(null, 'source.Users.inspect', data), - isInspected, - loading: isLoading, - loadPage: this.wrappedLoadMore, - pageInfo: getOr({}, 'source.Users.pageInfo', data), - refetch: this.memoizedRefetchQuery(variables, limit, refetch), - totalCount: getOr(-1, 'source.Users.totalCount', data), - users, }); - }} - - ); - } -} - -const makeMapStateToProps = () => { - const getUsersSelector = networkSelectors.usersSelector(); - const getQuery = inputsSelectors.globalQueryByIdSelector(); - const mapStateToProps = (state: State, { id = ID }: OwnProps) => { - const { isInspected } = getQuery(state, id); - return { - ...getUsersSelector(state), - isInspected, - }; - }; + }; + abortCtrl.current.abort(); + asyncSearch(); + refetch.current = asyncSearch; + return () => { + didCancel = true; + abortCtrl.current.abort(); + }; + }, + [data.search, notifications.toasts] + ); - return mapStateToProps; -}; + useEffect(() => { + setNetworkUsersRequest((prevRequest) => { + const myRequest = { + ...prevRequest, + defaultIndex, + filterQuery: createFilter(filterQuery), + pagination: generateTablePaginationOptions(activePage, limit), + sort, + timerange: { + interval: '12h', + from: startDate, + to: endDate, + }, + }; + if (!skip && !deepEqual(prevRequest, myRequest)) { + return myRequest; + } + return prevRequest; + }); + }, [activePage, defaultIndex, endDate, filterQuery, limit, startDate, sort, skip]); -const connector = connect(makeMapStateToProps); + useEffect(() => { + networkUsersSearch(networkUsersRequest); + }, [networkUsersRequest, networkUsersSearch]); -type PropsFromRedux = ConnectedProps; - -export const UsersQuery = compose>( - connector, - withKibana -)(UsersComponentQuery); + return [loading, networkUsersResponse]; +}; diff --git a/x-pack/plugins/security_solution/public/network/containers/users/translations.ts b/x-pack/plugins/security_solution/public/network/containers/users/translations.ts new file mode 100644 index 0000000000000..dac260dec984b --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/containers/users/translations.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const ERROR_NETWORK_USERS = i18n.translate( + 'xpack.securitySolution.networkUsers.errorSearchDescription', + { + defaultMessage: `An error has occurred on network users search`, + } +); + +export const FAIL_NETWORK_USERS = i18n.translate( + 'xpack.securitySolution.networkUsers.failSearchDescription', + { + defaultMessage: `Failed to run search on network users`, + } +); diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx b/x-pack/plugins/security_solution/public/network/pages/details/index.test.tsx similarity index 67% rename from x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx rename to x-pack/plugins/security_solution/public/network/pages/details/index.test.tsx index e2e458bcec2f5..beae3e4f72aaa 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.test.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/index.test.tsx @@ -4,10 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { shallow } from 'enzyme'; import React from 'react'; -import { Router } from 'react-router-dom'; -import { ActionCreator } from 'typescript-fsa'; +import { Router, useParams } from 'react-router-dom'; import '../../../common/mock/match_media'; @@ -23,14 +21,24 @@ import { } from '../../../common/mock'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; import { createStore, State } from '../../../common/store'; -import { InputsModelId } from '../../../common/store/inputs/constants'; -import { IPDetailsComponent, IPDetails } from './index'; +import { NetworkDetails } from './index'; type Action = 'PUSH' | 'POP' | 'REPLACE'; const pop: Action = 'POP'; type GlobalWithFetch = NodeJS.Global & { fetch: jest.Mock }; +jest.mock('react-router-dom', () => { + const original = jest.requireActual('react-router-dom'); + + return { + ...original, + useParams: jest.fn(), + }; +}); +jest.mock('../../containers/details', () => ({ + useNetworkDetails: jest.fn().mockReturnValue([true, { networkDetails: {} }]), +})); jest.mock('../../../common/lib/kibana'); jest.mock('../../../common/containers/source'); jest.mock('../../../common/containers/use_global_time', () => ({ @@ -70,34 +78,7 @@ const getMockHistory = (ip: string) => ({ listen: jest.fn(), }); -const to = '2018-03-23T18:49:23.132Z'; -const from = '2018-03-24T03:33:52.253Z'; -const getMockProps = (ip: string) => ({ - to, - from, - isInitializing: false, - setQuery: jest.fn(), - query: { query: 'coolQueryhuh?', language: 'keury' }, - filters: [], - flowTarget: FlowTarget.source, - history: getMockHistory(ip), - location: { - pathname: `/network/ip/${ip}`, - search: '', - state: '', - hash: '', - }, - detailName: ip, - match: { params: { detailName: ip, search: '' }, isExact: true, path: '', url: '' }, - setAbsoluteRangeDatePicker: (jest.fn() as unknown) as ActionCreator<{ - id: InputsModelId; - from: string; - to: string; - }>, - setIpDetailsTablesActivePageToZero: (jest.fn() as unknown) as ActionCreator, -}); - -describe('Ip Details', () => { +describe('Network Details', () => { const mount = useMountAppended(); beforeAll(() => { (useWithSource as jest.Mock).mockReturnValue({ @@ -139,31 +120,41 @@ describe('Ip Details', () => { }); test('it renders', () => { - const wrapper = shallow(); - expect(wrapper.find('[data-test-subj="ip-details-page"]').exists()).toBe(true); - }); - - test('it matches the snapshot', () => { - const wrapper = shallow(); - expect(wrapper).toMatchSnapshot(); + const ip = '123.456.78.90'; + (useParams as jest.Mock).mockReturnValue({ + detailName: ip, + flowTarget: FlowTarget.source, + }); + const wrapper = mount( + + + + + + ); + expect(wrapper.find('[data-test-subj="network-details-page"]').exists()).toBe(true); }); test('it renders ipv6 headline', async () => { + const ip = 'fe80--24ce-f7ff-fede-a571'; (useWithSource as jest.Mock).mockReturnValue({ indicesExist: true, indexPattern: {}, }); - const ip = 'fe80--24ce-f7ff-fede-a571'; + (useParams as jest.Mock).mockReturnValue({ + detailName: ip, + flowTarget: FlowTarget.source, + }); const wrapper = mount( - + ); expect( wrapper - .find('[data-test-subj="ip-details-headline"] [data-test-subj="header-page-title"]') + .find('[data-test-subj="network-details-headline"] [data-test-subj="header-page-title"]') .text() ).toEqual('fe80::24ce:f7ff:fede:a571'); }); diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx similarity index 69% rename from x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx rename to x-pack/plugins/security_solution/public/network/pages/details/index.tsx index c9ac1fc7a6e9c..085cddf53ff65 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/details/index.tsx @@ -5,39 +5,40 @@ */ import { EuiHorizontalRule, EuiSpacer, EuiFlexItem } from '@elastic/eui'; -import React, { useCallback, useEffect } from 'react'; -import { connect, ConnectedProps } from 'react-redux'; +import React, { useCallback, useEffect, useMemo } from 'react'; +import { useDispatch, useSelector, shallowEqual } from 'react-redux'; +import { useParams } from 'react-router-dom'; +import { FlowTarget } from '../../../../common/search_strategy'; import { useGlobalTime } from '../../../common/containers/use_global_time'; import { FiltersGlobal } from '../../../common/components/filters_global'; import { HeaderPage } from '../../../common/components/header_page'; import { LastEventTime } from '../../../common/components/last_event_time'; -import { AnomalyTableProvider } from '../../../common/components/ml/anomaly/anomaly_table_provider'; +import { useAnomaliesTableData } from '../../../common/components/ml/anomaly/use_anomalies_table_data'; import { networkToCriteria } from '../../../common/components/ml/criteria/network_to_criteria'; import { scoreIntervalToDateTime } from '../../../common/components/ml/score/score_interval_to_datetime'; import { AnomaliesNetworkTable } from '../../../common/components/ml/tables/anomalies_network_table'; import { manageQuery } from '../../../common/components/page/manage_query'; import { FlowTargetSelectConnected } from '../../components/flow_target_select_connected'; -import { IpOverview } from '../../components/ip_overview'; +import { IpOverview } from '../../components/details'; import { SiemSearchBar } from '../../../common/components/search_bar'; import { WrapperPage } from '../../../common/components/wrapper_page'; -import { IpOverviewQuery } from '../../containers/ip_overview'; +import { useNetworkDetails } from '../../containers/details'; import { useWithSource } from '../../../common/containers/source'; import { FlowTargetSourceDest, LastEventIndexKey } from '../../../graphql/types'; import { useKibana } from '../../../common/lib/kibana'; import { decodeIpv6 } from '../../../common/lib/helpers'; import { convertToBuildEsQuery } from '../../../common/lib/keury'; import { ConditionalFlexGroup } from '../../pages/navigation/conditional_flex_group'; -import { State, inputsSelectors } from '../../../common/store'; -import { setAbsoluteRangeDatePicker as dispatchAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; -import { setIpDetailsTablesActivePageToZero as dispatchIpDetailsTablesActivePageToZero } from '../../store/actions'; +import { inputsSelectors } from '../../../common/store'; +import { setAbsoluteRangeDatePicker } from '../../../common/store/inputs/actions'; +import { setNetworkDetailsTablesActivePageToZero } from '../../store/actions'; import { SpyRoute } from '../../../common/utils/route/spy_routes'; import { OverviewEmpty } from '../../../overview/components/overview_empty'; import { NetworkHttpQueryTable } from './network_http_query_table'; import { NetworkTopCountriesQueryTable } from './network_top_countries_query_table'; import { NetworkTopNFlowQueryTable } from './network_top_n_flow_query_table'; import { TlsQueryTable } from './tls_query_table'; -import { IPDetailsComponentProps } from './types'; import { UsersQueryTable } from './users_query_table'; import { AnomaliesQueryTabBody } from '../../../common/containers/anomalies/anomalies_query_tab_body'; import { esQuery } from '../../../../../../../src/plugins/data/public'; @@ -45,36 +46,42 @@ import { networkModel } from '../../store'; import { SecurityPageName } from '../../../app/types'; export { getBreadcrumbs } from './utils'; -const IpOverviewManage = manageQuery(IpOverview); +const NetworkDetailsManage = manageQuery(IpOverview); -export const IPDetailsComponent: React.FC = ({ - detailName, - filters, - flowTarget, - query, - setAbsoluteRangeDatePicker, - setIpDetailsTablesActivePageToZero, -}) => { +const NetworkDetailsComponent: React.FC = () => { + const dispatch = useDispatch(); const { to, from, setQuery, isInitializing } = useGlobalTime(); + const { detailName, flowTarget } = useParams<{ + detailName: string; + flowTarget: FlowTarget; + }>(); + const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); + const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); + + const query = useSelector(getGlobalQuerySelector, shallowEqual); + const filters = useSelector(getGlobalFiltersQuerySelector, shallowEqual); + const type = networkModel.NetworkType.details; const narrowDateRange = useCallback( (score, interval) => { const fromTo = scoreIntervalToDateTime(score, interval); - setAbsoluteRangeDatePicker({ - id: 'global', - from: fromTo.from, - to: fromTo.to, - }); + dispatch( + setAbsoluteRangeDatePicker({ + id: 'global', + from: fromTo.from, + to: fromTo.to, + }) + ); }, - [setAbsoluteRangeDatePicker] + [dispatch] ); const { services: { uiSettings }, } = useKibana(); useEffect(() => { - setIpDetailsTablesActivePageToZero(); - }, [detailName, setIpDetailsTablesActivePageToZero]); + dispatch(setNetworkDetailsTablesActivePageToZero()); + }, [detailName, dispatch]); const { docValueFields, indicesExist, indexPattern } = useWithSource(); const ip = decodeIpv6(detailName); @@ -85,8 +92,27 @@ export const IPDetailsComponent: React.FC ({ field: `${flowTarget}.ip`, value: ip }), [ + flowTarget, + ip, + ]); + return ( -
+
{indicesExist ? ( <> @@ -96,50 +122,30 @@ export const IPDetailsComponent: React.FC } title={ip} > - - {({ id, inspect, ipOverviewData, loading, refetch }) => ( - - {({ isLoadingAnomaliesData, anomaliesData }) => ( - - )} - - )} - + data={networkDetails} + anomaliesData={anomaliesData} + loading={loading} + isLoadingAnomaliesData={isLoadingAnomaliesData} + type={type} + flowTarget={flowTarget} + refetch={refetch} + setQuery={setQuery} + startDate={from} + endDate={to} + narrowDateRange={narrowDateRange} + /> @@ -272,25 +278,7 @@ export const IPDetailsComponent: React.FC ); }; -IPDetailsComponent.displayName = 'IPDetailsComponent'; - -const makeMapStateToProps = () => { - const getGlobalQuerySelector = inputsSelectors.globalQuerySelector(); - const getGlobalFiltersQuerySelector = inputsSelectors.globalFiltersQuerySelector(); - - return (state: State) => ({ - query: getGlobalQuerySelector(state), - filters: getGlobalFiltersQuerySelector(state), - }); -}; - -const mapDispatchToProps = { - setAbsoluteRangeDatePicker: dispatchAbsoluteRangeDatePicker, - setIpDetailsTablesActivePageToZero: dispatchIpDetailsTablesActivePageToZero, -}; - -export const connector = connect(makeMapStateToProps, mapDispatchToProps); -type PropsFromRedux = ConnectedProps; +NetworkDetailsComponent.displayName = 'NetworkDetailsComponent'; -export const IPDetails = connector(React.memo(IPDetailsComponent)); +export const NetworkDetails = React.memo(NetworkDetailsComponent); diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/network_http_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/network/pages/ip_details/network_http_query_table.tsx rename to x-pack/plugins/security_solution/public/network/pages/details/network_http_query_table.tsx diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_countries_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_countries_query_table.tsx rename to x-pack/plugins/security_solution/public/network/pages/details/network_top_countries_query_table.tsx diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_n_flow_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/network_top_n_flow_query_table.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/network/pages/ip_details/network_top_n_flow_query_table.tsx rename to x-pack/plugins/security_solution/public/network/pages/details/network_top_n_flow_query_table.tsx diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/tls_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx similarity index 100% rename from x-pack/plugins/security_solution/public/network/pages/ip_details/tls_query_table.tsx rename to x-pack/plugins/security_solution/public/network/pages/details/tls_query_table.tsx diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts b/x-pack/plugins/security_solution/public/network/pages/details/types.ts similarity index 92% rename from x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts rename to x-pack/plugins/security_solution/public/network/pages/details/types.ts index cab6e8e09b200..960df0d5e36b9 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/types.ts +++ b/x-pack/plugins/security_solution/public/network/pages/details/types.ts @@ -16,11 +16,6 @@ import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; export const type = NetworkType.details; -export interface IPDetailsComponentProps { - detailName: string; - flowTarget: FlowTarget; -} - export interface OwnProps { type: NetworkType; startDate: string; diff --git a/x-pack/plugins/security_solution/public/network/pages/details/users_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/details/users_query_table.tsx new file mode 100644 index 0000000000000..a7a819151579f --- /dev/null +++ b/x-pack/plugins/security_solution/public/network/pages/details/users_query_table.tsx @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React from 'react'; +import { getOr } from 'lodash/fp'; +import { manageQuery } from '../../../common/components/page/manage_query'; +import { useNetworkUsers } from '../../containers/users'; +import { NetworkComponentsQueryProps } from './types'; +import { UsersTable } from '../../components/users_table'; + +const UsersTableManage = manageQuery(UsersTable); + +export const UsersQueryTable = ({ + endDate, + filterQuery, + flowTarget, + ip, + setQuery, + skip, + startDate, + type, +}: NetworkComponentsQueryProps) => { + const [ + loading, + { id, inspect, isInspected, networkUsers, totalCount, pageInfo, loadPage, refetch }, + ] = useNetworkUsers({ + endDate, + filterQuery, + flowTarget, + ip, + skip, + startDate, + }); + + return ( + + ); +}; + +UsersQueryTable.displayName = 'UsersQueryTable'; diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/utils.ts b/x-pack/plugins/security_solution/public/network/pages/details/utils.ts similarity index 94% rename from x-pack/plugins/security_solution/public/network/pages/ip_details/utils.ts rename to x-pack/plugins/security_solution/public/network/pages/details/utils.ts index 9284a808625a5..2b5c68c81a9d3 100644 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/utils.ts +++ b/x-pack/plugins/security_solution/public/network/pages/details/utils.ts @@ -8,7 +8,7 @@ import { get, isEmpty } from 'lodash/fp'; import { ChromeBreadcrumb } from '../../../../../../../src/core/public'; import { decodeIpv6 } from '../../../common/lib/helpers'; -import { getIPDetailsUrl } from '../../../common/components/link_to/redirect_to_network'; +import { getNetworkDetailsUrl } from '../../../common/components/link_to/redirect_to_network'; import { networkModel } from '../../store'; import * as i18n from '../translations'; import { NetworkRouteType } from '../navigation/types'; @@ -46,7 +46,7 @@ export const getBreadcrumbs = ( { text: decodeIpv6(params.detailName), href: getUrlForApp(`${APP_ID}:${SecurityPageName.network}`, { - path: getIPDetailsUrl( + path: getNetworkDetailsUrl( params.detailName, params.flowTarget, !isEmpty(search[0]) ? search[0] : '' diff --git a/x-pack/plugins/security_solution/public/network/pages/index.tsx b/x-pack/plugins/security_solution/public/network/pages/index.tsx index 07abe7bc8c209..9e796b6f01dfa 100644 --- a/x-pack/plugins/security_solution/public/network/pages/index.tsx +++ b/x-pack/plugins/security_solution/public/network/pages/index.tsx @@ -11,7 +11,7 @@ import { useMlCapabilities } from '../../common/components/ml/hooks/use_ml_capab import { hasMlUserPermissions } from '../../../common/machine_learning/has_ml_user_permissions'; import { FlowTarget } from '../../graphql/types'; -import { IPDetails } from './ip_details'; +import { NetworkDetails } from './details'; import { Network } from './network'; import { getNetworkRoutePath } from './navigation'; import { NetworkRouteType } from './navigation/types'; @@ -49,14 +49,9 @@ const NetworkContainerComponent: React.FC = () => { hasMlUserPermissions={userHasMlUserPermissions} /> - } - /> + + + - - - - - -
-`; diff --git a/x-pack/plugins/security_solution/public/network/pages/ip_details/users_query_table.tsx b/x-pack/plugins/security_solution/public/network/pages/ip_details/users_query_table.tsx deleted file mode 100644 index 4071790b4208a..0000000000000 --- a/x-pack/plugins/security_solution/public/network/pages/ip_details/users_query_table.tsx +++ /dev/null @@ -1,56 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { getOr } from 'lodash/fp'; -import { manageQuery } from '../../../common/components/page/manage_query'; -import { UsersQuery } from '../../containers/users'; -import { NetworkComponentsQueryProps } from './types'; -import { UsersTable } from '../../components/users_table'; - -const UsersTableManage = manageQuery(UsersTable); - -export const UsersQueryTable = ({ - endDate, - filterQuery, - flowTarget, - ip, - setQuery, - skip, - startDate, - type, -}: NetworkComponentsQueryProps) => ( - - {({ id, inspect, isInspected, users, totalCount, pageInfo, loading, loadPage, refetch }) => ( - - )} - -); - -UsersQueryTable.displayName = 'UsersQueryTable'; diff --git a/x-pack/plugins/security_solution/public/network/store/actions.ts b/x-pack/plugins/security_solution/public/network/store/actions.ts index 44a0b7ce1ad10..9c0242741304e 100644 --- a/x-pack/plugins/security_solution/public/network/store/actions.ts +++ b/x-pack/plugins/security_solution/public/network/store/actions.ts @@ -11,11 +11,11 @@ const actionCreator = actionCreatorFactory('x-pack/security_solution/local/netwo export const updateNetworkTable = actionCreator<{ networkType: networkModel.NetworkType; - tableType: networkModel.NetworkTableType | networkModel.IpDetailsTableType; + tableType: networkModel.NetworkTableType | networkModel.NetworkDetailsTableType; updates: networkModel.TableUpdates; }>('UPDATE_NETWORK_TABLE'); -export const setIpDetailsTablesActivePageToZero = actionCreator( +export const setNetworkDetailsTablesActivePageToZero = actionCreator( 'SET_IP_DETAILS_TABLES_ACTIVE_PAGE_TO_ZERO' ); diff --git a/x-pack/plugins/security_solution/public/network/store/helpers.test.ts b/x-pack/plugins/security_solution/public/network/store/helpers.test.ts index a3a2a9b7f5393..944939074693b 100644 --- a/x-pack/plugins/security_solution/public/network/store/helpers.test.ts +++ b/x-pack/plugins/security_solution/public/network/store/helpers.test.ts @@ -9,11 +9,11 @@ import { FlowTarget, NetworkDnsFields, NetworkTopTablesFields, - TlsFields, - UsersFields, -} from '../../graphql/types'; + NetworkTlsFields, + NetworkUsersFields, +} from '../../../common/search_strategy'; import { DEFAULT_TABLE_LIMIT } from '../../common/store/constants'; -import { NetworkModel, NetworkTableType, IpDetailsTableType, NetworkType } from './model'; +import { NetworkModel, NetworkTableType, NetworkDetailsTableType, NetworkType } from './model'; import { setNetworkQueriesActivePageToZero } from './helpers'; export const mockNetworkState: NetworkModel = { @@ -64,7 +64,7 @@ export const mockNetworkState: NetworkModel = { activePage: 2, limit: DEFAULT_TABLE_LIMIT, sort: { - field: TlsFields._id, + field: NetworkTlsFields._id, direction: Direction.desc, }, }, @@ -81,7 +81,7 @@ export const mockNetworkState: NetworkModel = { }, details: { queries: { - [IpDetailsTableType.topCountriesSource]: { + [NetworkDetailsTableType.topCountriesSource]: { activePage: 7, limit: DEFAULT_TABLE_LIMIT, sort: { @@ -89,7 +89,7 @@ export const mockNetworkState: NetworkModel = { direction: Direction.desc, }, }, - [IpDetailsTableType.topCountriesDestination]: { + [NetworkDetailsTableType.topCountriesDestination]: { activePage: 3, limit: DEFAULT_TABLE_LIMIT, sort: { @@ -97,7 +97,7 @@ export const mockNetworkState: NetworkModel = { direction: Direction.desc, }, }, - [IpDetailsTableType.topNFlowSource]: { + [NetworkDetailsTableType.topNFlowSource]: { activePage: 7, limit: DEFAULT_TABLE_LIMIT, sort: { @@ -105,7 +105,7 @@ export const mockNetworkState: NetworkModel = { direction: Direction.desc, }, }, - [IpDetailsTableType.topNFlowDestination]: { + [NetworkDetailsTableType.topNFlowDestination]: { activePage: 3, limit: DEFAULT_TABLE_LIMIT, sort: { @@ -113,23 +113,23 @@ export const mockNetworkState: NetworkModel = { direction: Direction.desc, }, }, - [IpDetailsTableType.tls]: { + [NetworkDetailsTableType.tls]: { activePage: 2, limit: DEFAULT_TABLE_LIMIT, sort: { - field: TlsFields._id, + field: NetworkTlsFields._id, direction: Direction.desc, }, }, - [IpDetailsTableType.users]: { + [NetworkDetailsTableType.users]: { activePage: 6, limit: DEFAULT_TABLE_LIMIT, sort: { - field: UsersFields.name, + field: NetworkUsersFields.name, direction: Direction.asc, }, }, - [IpDetailsTableType.http]: { + [NetworkDetailsTableType.http]: { activePage: 0, limit: DEFAULT_TABLE_LIMIT, sort: { direction: Direction.desc }, @@ -199,17 +199,17 @@ describe('Network redux store', () => { test('set activePage to zero for all queries in ip details ', () => { expect(setNetworkQueriesActivePageToZero(mockNetworkState, NetworkType.details)).toEqual({ - [IpDetailsTableType.topNFlowSource]: { + [NetworkDetailsTableType.topNFlowSource]: { activePage: 0, limit: 10, sort: { field: 'bytes_out', direction: 'desc' }, }, - [IpDetailsTableType.topNFlowDestination]: { + [NetworkDetailsTableType.topNFlowDestination]: { activePage: 0, limit: 10, sort: { field: 'bytes_out', direction: 'desc' }, }, - [IpDetailsTableType.topCountriesDestination]: { + [NetworkDetailsTableType.topCountriesDestination]: { activePage: 0, limit: 10, sort: { @@ -217,7 +217,7 @@ describe('Network redux store', () => { field: 'bytes_out', }, }, - [IpDetailsTableType.topCountriesSource]: { + [NetworkDetailsTableType.topCountriesSource]: { activePage: 0, limit: 10, sort: { @@ -225,19 +225,19 @@ describe('Network redux store', () => { field: 'bytes_out', }, }, - [IpDetailsTableType.http]: { + [NetworkDetailsTableType.http]: { activePage: 0, limit: 10, sort: { direction: 'desc', }, }, - [IpDetailsTableType.tls]: { + [NetworkDetailsTableType.tls]: { activePage: 0, limit: 10, sort: { field: '_id', direction: 'desc' }, }, - [IpDetailsTableType.users]: { + [NetworkDetailsTableType.users]: { activePage: 0, limit: 10, sort: { field: 'name', direction: 'asc' }, diff --git a/x-pack/plugins/security_solution/public/network/store/helpers.ts b/x-pack/plugins/security_solution/public/network/store/helpers.ts index 938de1dedf0b7..e93e5ada03591 100644 --- a/x-pack/plugins/security_solution/public/network/store/helpers.ts +++ b/x-pack/plugins/security_solution/public/network/store/helpers.ts @@ -8,9 +8,9 @@ import { NetworkModel, NetworkType, NetworkTableType, - IpDetailsTableType, + NetworkDetailsTableType, NetworkQueries, - IpOverviewQueries, + NetworkDetailsQueries, } from './model'; import { DEFAULT_TABLE_ACTIVE_PAGE } from '../../common/store/constants'; @@ -48,34 +48,34 @@ export const setNetworkPageQueriesActivePageToZero = (state: NetworkModel): Netw export const setNetworkDetailsQueriesActivePageToZero = ( state: NetworkModel -): IpOverviewQueries => ({ +): NetworkDetailsQueries => ({ ...state.details.queries, - [IpDetailsTableType.topCountriesSource]: { - ...state.details.queries[IpDetailsTableType.topCountriesSource], + [NetworkDetailsTableType.topCountriesSource]: { + ...state.details.queries[NetworkDetailsTableType.topCountriesSource], activePage: DEFAULT_TABLE_ACTIVE_PAGE, }, - [IpDetailsTableType.topCountriesDestination]: { - ...state.details.queries[IpDetailsTableType.topCountriesDestination], + [NetworkDetailsTableType.topCountriesDestination]: { + ...state.details.queries[NetworkDetailsTableType.topCountriesDestination], activePage: DEFAULT_TABLE_ACTIVE_PAGE, }, - [IpDetailsTableType.topNFlowSource]: { - ...state.details.queries[IpDetailsTableType.topNFlowSource], + [NetworkDetailsTableType.topNFlowSource]: { + ...state.details.queries[NetworkDetailsTableType.topNFlowSource], activePage: DEFAULT_TABLE_ACTIVE_PAGE, }, - [IpDetailsTableType.topNFlowDestination]: { - ...state.details.queries[IpDetailsTableType.topNFlowDestination], + [NetworkDetailsTableType.topNFlowDestination]: { + ...state.details.queries[NetworkDetailsTableType.topNFlowDestination], activePage: DEFAULT_TABLE_ACTIVE_PAGE, }, - [IpDetailsTableType.tls]: { - ...state.details.queries[IpDetailsTableType.tls], + [NetworkDetailsTableType.tls]: { + ...state.details.queries[NetworkDetailsTableType.tls], activePage: DEFAULT_TABLE_ACTIVE_PAGE, }, - [IpDetailsTableType.users]: { - ...state.details.queries[IpDetailsTableType.users], + [NetworkDetailsTableType.users]: { + ...state.details.queries[NetworkDetailsTableType.users], activePage: DEFAULT_TABLE_ACTIVE_PAGE, }, - [IpDetailsTableType.http]: { - ...state.details.queries[IpDetailsTableType.http], + [NetworkDetailsTableType.http]: { + ...state.details.queries[NetworkDetailsTableType.http], activePage: DEFAULT_TABLE_ACTIVE_PAGE, }, }); @@ -83,7 +83,7 @@ export const setNetworkDetailsQueriesActivePageToZero = ( export const setNetworkQueriesActivePageToZero = ( state: NetworkModel, type: NetworkType -): NetworkQueries | IpOverviewQueries => { +): NetworkQueries | NetworkDetailsQueries => { if (type === NetworkType.page) { return setNetworkPageQueriesActivePageToZero(state); } else if (type === NetworkType.details) { diff --git a/x-pack/plugins/security_solution/public/network/store/model.ts b/x-pack/plugins/security_solution/public/network/store/model.ts index 4ddfb84024970..f2daddbd5c3d5 100644 --- a/x-pack/plugins/security_solution/public/network/store/model.ts +++ b/x-pack/plugins/security_solution/public/network/store/model.ts @@ -5,13 +5,14 @@ */ import { + Direction, FlowTarget, - NetworkDnsSortField, - NetworkHttpSortField, - NetworkTopTablesSortField, - TlsSortField, - UsersSortField, -} from '../../graphql/types'; + NetworkDnsFields, + NetworkTopTablesFields, + NetworkTlsFields, + NetworkUsersFields, + SortField, +} from '../../../common/search_strategy'; export enum NetworkType { page = 'page', @@ -30,22 +31,22 @@ export enum NetworkTableType { } export type TopNTableType = - | IpDetailsTableType.topNFlowDestination - | IpDetailsTableType.topNFlowSource + | NetworkDetailsTableType.topNFlowDestination + | NetworkDetailsTableType.topNFlowSource | NetworkTableType.topNFlowDestination | NetworkTableType.topNFlowSource; export type TopCountriesTableType = - | IpDetailsTableType.topCountriesDestination - | IpDetailsTableType.topCountriesSource + | NetworkDetailsTableType.topCountriesDestination + | NetworkDetailsTableType.topCountriesSource | NetworkTableType.topCountriesDestination | NetworkTableType.topCountriesSource; -export type TopTlsTableType = IpDetailsTableType.tls | NetworkTableType.tls; +export type TopTlsTableType = NetworkDetailsTableType.tls | NetworkTableType.tls; -export type HttpTableType = IpDetailsTableType.http | NetworkTableType.http; +export type HttpTableType = NetworkDetailsTableType.http | NetworkTableType.http; -export enum IpDetailsTableType { +export enum NetworkDetailsTableType { http = 'http', tls = 'tls', topCountriesDestination = 'topCountriesDestination', @@ -55,7 +56,7 @@ export enum IpDetailsTableType { users = 'users', } -export type AllNetworkTables = NetworkTableType | IpDetailsTableType; +export type AllNetworkTables = NetworkTableType | NetworkDetailsTableType; export interface BasicQueryPaginated { activePage: number; @@ -64,24 +65,26 @@ export interface BasicQueryPaginated { // Network Page Models export interface TopNFlowQuery extends BasicQueryPaginated { - sort: NetworkTopTablesSortField; + sort: SortField; } export interface TopCountriesQuery extends BasicQueryPaginated { - sort: NetworkTopTablesSortField; + sort: SortField; } export interface DnsQuery extends BasicQueryPaginated { - sort: NetworkDnsSortField; + sort: SortField; isPtrIncluded: boolean; } export interface TlsQuery extends BasicQueryPaginated { - sort: TlsSortField; + sort: SortField; } export interface HttpQuery extends BasicQueryPaginated { - sort: NetworkHttpSortField; + sort: { + direction: Direction; + }; } export interface TableUpdates { @@ -89,11 +92,11 @@ export interface TableUpdates { limit?: number; isPtrIncluded?: boolean; sort?: - | NetworkDnsSortField - | NetworkHttpSortField - | NetworkTopTablesSortField - | TlsSortField - | UsersSortField; + | SortField + | HttpQuery['sort'] + | SortField + | SortField + | SortField; } export interface NetworkQueries { @@ -111,23 +114,23 @@ export interface NetworkPageModel { queries: NetworkQueries; } -export interface UsersQuery extends BasicQueryPaginated { - sort: UsersSortField; +export interface NetworkUsersQuery extends BasicQueryPaginated { + sort: SortField; } -export interface IpOverviewQueries { - [IpDetailsTableType.http]: HttpQuery; - [IpDetailsTableType.tls]: TlsQuery; - [IpDetailsTableType.topCountriesDestination]: TopCountriesQuery; - [IpDetailsTableType.topCountriesSource]: TopCountriesQuery; - [IpDetailsTableType.topNFlowDestination]: TopNFlowQuery; - [IpDetailsTableType.topNFlowSource]: TopNFlowQuery; - [IpDetailsTableType.users]: UsersQuery; +export interface NetworkDetailsQueries { + [NetworkDetailsTableType.http]: HttpQuery; + [NetworkDetailsTableType.tls]: TlsQuery; + [NetworkDetailsTableType.topCountriesDestination]: TopCountriesQuery; + [NetworkDetailsTableType.topCountriesSource]: TopCountriesQuery; + [NetworkDetailsTableType.topNFlowDestination]: TopNFlowQuery; + [NetworkDetailsTableType.topNFlowSource]: TopNFlowQuery; + [NetworkDetailsTableType.users]: NetworkUsersQuery; } export interface NetworkDetailsModel { flowTarget: FlowTarget; - queries: IpOverviewQueries; + queries: NetworkDetailsQueries; } export interface NetworkModel { diff --git a/x-pack/plugins/security_solution/public/network/store/reducer.ts b/x-pack/plugins/security_solution/public/network/store/reducer.ts index 380cd660f3e3b..e3eb2acc78d6e 100644 --- a/x-pack/plugins/security_solution/public/network/store/reducer.ts +++ b/x-pack/plugins/security_solution/public/network/store/reducer.ts @@ -11,13 +11,13 @@ import { FlowTarget, NetworkDnsFields, NetworkTopTablesFields, - TlsFields, - UsersFields, -} from '../../graphql/types'; + NetworkTlsFields, + NetworkUsersFields, +} from '../../../common/search_strategy'; import { DEFAULT_TABLE_ACTIVE_PAGE, DEFAULT_TABLE_LIMIT } from '../../common/store/constants'; import { - setIpDetailsTablesActivePageToZero, + setNetworkDetailsTablesActivePageToZero, setNetworkTablesActivePageToZero, updateNetworkTable, } from './actions'; @@ -25,7 +25,7 @@ import { setNetworkDetailsQueriesActivePageToZero, setNetworkPageQueriesActivePageToZero, } from './helpers'; -import { IpDetailsTableType, NetworkModel, NetworkTableType } from './model'; +import { NetworkDetailsTableType, NetworkModel, NetworkTableType } from './model'; export type NetworkState = NetworkModel; @@ -68,7 +68,7 @@ export const initialNetworkState: NetworkState = { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, sort: { - field: TlsFields._id, + field: NetworkTlsFields._id, direction: Direction.desc, }, }, @@ -96,14 +96,14 @@ export const initialNetworkState: NetworkState = { }, details: { queries: { - [IpDetailsTableType.http]: { + [NetworkDetailsTableType.http]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, sort: { direction: Direction.desc, }, }, - [IpDetailsTableType.topCountriesSource]: { + [NetworkDetailsTableType.topCountriesSource]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, sort: { @@ -111,7 +111,7 @@ export const initialNetworkState: NetworkState = { direction: Direction.desc, }, }, - [IpDetailsTableType.topCountriesDestination]: { + [NetworkDetailsTableType.topCountriesDestination]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, sort: { @@ -119,7 +119,7 @@ export const initialNetworkState: NetworkState = { direction: Direction.desc, }, }, - [IpDetailsTableType.topNFlowSource]: { + [NetworkDetailsTableType.topNFlowSource]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, sort: { @@ -127,7 +127,7 @@ export const initialNetworkState: NetworkState = { direction: Direction.desc, }, }, - [IpDetailsTableType.topNFlowDestination]: { + [NetworkDetailsTableType.topNFlowDestination]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, sort: { @@ -135,19 +135,19 @@ export const initialNetworkState: NetworkState = { direction: Direction.desc, }, }, - [IpDetailsTableType.tls]: { + [NetworkDetailsTableType.tls]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, sort: { - field: TlsFields._id, + field: NetworkTlsFields._id, direction: Direction.desc, }, }, - [IpDetailsTableType.users]: { + [NetworkDetailsTableType.users]: { activePage: DEFAULT_TABLE_ACTIVE_PAGE, limit: DEFAULT_TABLE_LIMIT, sort: { - field: UsersFields.name, + field: NetworkUsersFields.name, direction: Direction.asc, }, }, @@ -181,7 +181,7 @@ export const networkReducer = reducerWithInitialState(initialNetworkState) queries: setNetworkDetailsQueriesActivePageToZero(state), }, })) - .case(setIpDetailsTablesActivePageToZero, (state) => ({ + .case(setNetworkDetailsTablesActivePageToZero, (state) => ({ ...state, details: { ...state.details, diff --git a/x-pack/plugins/security_solution/public/network/store/selectors.ts b/x-pack/plugins/security_solution/public/network/store/selectors.ts index 0246305092a32..833cb3a69789c 100644 --- a/x-pack/plugins/security_solution/public/network/store/selectors.ts +++ b/x-pack/plugins/security_solution/public/network/store/selectors.ts @@ -11,7 +11,7 @@ import { FlowTargetSourceDest } from '../../../common/search_strategy/security_s import { State } from '../../common/store/types'; import { initialNetworkState } from './reducer'; import { - IpDetailsTableType, + NetworkDetailsTableType, NetworkDetailsModel, NetworkPageModel, NetworkTableType, @@ -36,7 +36,7 @@ const selectTopNFlowByType = ( ) => { const ft = flowTarget === FlowTargetSourceDest.source ? 'topNFlowSource' : 'topNFlowDestination'; const nFlowType = - networkType === NetworkType.page ? NetworkTableType[ft] : IpDetailsTableType[ft]; + networkType === NetworkType.page ? NetworkTableType[ft] : NetworkDetailsTableType[ft]; return ( get([networkType, 'queries', nFlowType], state.network) || get([networkType, 'queries', nFlowType], initialNetworkState) @@ -46,7 +46,8 @@ const selectTopNFlowByType = ( export const topNFlowSelector = () => createSelector(selectTopNFlowByType, (topNFlowQueries) => topNFlowQueries); const selectTlsByType = (state: State, networkType: NetworkType): TlsQuery => { - const tlsType = networkType === NetworkType.page ? NetworkTableType.tls : IpDetailsTableType.tls; + const tlsType = + networkType === NetworkType.page ? NetworkTableType.tls : NetworkDetailsTableType.tls; return ( get([networkType, 'queries', tlsType], state.network) || get([networkType, 'queries', tlsType], initialNetworkState) @@ -63,7 +64,7 @@ const selectTopCountriesByType = ( const ft = flowTarget === FlowTargetSourceDest.source ? 'topCountriesSource' : 'topCountriesDestination'; const nFlowType = - networkType === NetworkType.page ? NetworkTableType[ft] : IpDetailsTableType[ft]; + networkType === NetworkType.page ? NetworkTableType[ft] : NetworkDetailsTableType[ft]; return ( get([networkType, 'queries', nFlowType], state.network) || @@ -76,7 +77,7 @@ export const topCountriesSelector = () => const selectHttpByType = (state: State, networkType: NetworkType): HttpQuery => { const httpType = - networkType === NetworkType.page ? NetworkTableType.http : IpDetailsTableType.http; + networkType === NetworkType.page ? NetworkTableType.http : NetworkDetailsTableType.http; return ( get([networkType, 'queries', httpType], state.network) || get([networkType, 'queries', httpType], initialNetworkState) diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx index 08f3f01bc99f6..7bbd7f553ac46 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/index.tsx @@ -10,6 +10,7 @@ import lightTheme from '@elastic/eui/dist/eui_theme_light.json'; import { getOr } from 'lodash/fp'; import React, { useCallback, useMemo } from 'react'; +import { HostItem } from '../../../../common/search_strategy'; import { DEFAULT_DARK_MODE } from '../../../../common/constants'; import { DescriptionList } from '../../../../common/utility_types'; import { useUiSetting$ } from '../../../common/lib/kibana'; @@ -19,9 +20,8 @@ import { hostIdRenderer, } from '../../../timelines/components/field_renderers/field_renderers'; import { InspectButton, InspectButtonContainer } from '../../../common/components/inspect'; -import { HostItem } from '../../../graphql/types'; import { Loader } from '../../../common/components/loader'; -import { IPDetailsLink } from '../../../common/components/links'; +import { NetworkDetailsLink } from '../../../common/components/links'; import { hasMlUserPermissions } from '../../../../common/machine_learning/has_ml_user_permissions'; import { useMlCapabilities } from '../../../common/components/ml/hooks/use_ml_capabilities'; import { AnomalyScores } from '../../../common/components/ml/score/anomaly_scores'; @@ -154,7 +154,7 @@ export const HostOverview = React.memo( rowItems={getOr([], 'host.ip', data)} attrName={'host.ip'} idPrefix="host-overview" - render={(ip) => (ip != null ? : getEmptyTagValue())} + render={(ip) => (ip != null ? : getEmptyTagValue())} /> ), }, diff --git a/x-pack/plugins/security_solution/public/overview/components/host_overview/mock.ts b/x-pack/plugins/security_solution/public/overview/components/host_overview/mock.ts index c24cb20e9087c..7fe9ac139e129 100644 --- a/x-pack/plugins/security_solution/public/overview/components/host_overview/mock.ts +++ b/x-pack/plugins/security_solution/public/overview/components/host_overview/mock.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { HostsData } from '../../../graphql/types'; +import { HostsStrategyResponse } from '../../../../common/search_strategy'; -export const mockData: { Hosts: HostsData; DateFields: string[] } = { +export const mockData: { Hosts: HostsStrategyResponse; DateFields: string[] } = { Hosts: { totalCount: 1, edges: [ @@ -47,6 +47,7 @@ export const mockData: { Hosts: HostsData; DateFields: string[] } = { fakeTotalCount: 50, showMorePagesIndicator: true, }, + rawResponse: {} as HostsStrategyResponse['rawResponse'], }, DateFields: ['lastBeat'], }; diff --git a/x-pack/plugins/security_solution/public/plugin.tsx b/x-pack/plugins/security_solution/public/plugin.tsx index 1017cbb6a2c61..9b4eb6a25d1e5 100644 --- a/x-pack/plugins/security_solution/public/plugin.tsx +++ b/x-pack/plugins/security_solution/public/plugin.tsx @@ -21,7 +21,6 @@ import { import { Storage } from '../../../../src/plugins/kibana_utils/public'; import { initTelemetry } from './common/lib/telemetry'; import { KibanaServices } from './common/lib/kibana/services'; -import { jiraActionType, resilientActionType } from './common/lib/connectors'; import { PluginSetup, PluginStart, @@ -96,9 +95,6 @@ export class Plugin implements IPlugin { const storage = new Storage(localStorage); const [coreStart, startPlugins] = await core.getStartServices(); diff --git a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap index db8d047c2ce86..fc0d646fd62ca 100644 --- a/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap +++ b/x-pack/plugins/security_solution/public/resolver/models/indexed_process_tree/__snapshots__/isometric_taxi_layout.test.ts.snap @@ -212,6 +212,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:0:1", }, "points": Array [ @@ -227,6 +231,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:0:2", }, "points": Array [ @@ -242,6 +250,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:0:8", }, "points": Array [ @@ -287,6 +299,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:1:3", }, "points": Array [ @@ -302,6 +318,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:1:4", }, "points": Array [ @@ -347,6 +367,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:2:5", }, "points": Array [ @@ -362,6 +386,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:2:6", }, "points": Array [ @@ -377,6 +405,10 @@ Object { }, Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:6:7", }, "points": Array [ @@ -584,6 +616,10 @@ Object { "edgeLineSegments": Array [ Object { "metadata": Object { + "elapsedTime": Object { + "duration": "<1", + "durationType": "millisecond", + }, "uniqueId": "edge:0:1", }, "points": Array [ diff --git a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts index e6e525334e818..1e2de06ea4af5 100644 --- a/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts +++ b/x-pack/plugins/security_solution/public/resolver/store/data/reducer.test.ts @@ -10,8 +10,9 @@ import { dataReducer } from './reducer'; import * as selectors from './selectors'; import { DataState } from '../../types'; import { DataAction } from './action'; -import { ResolverChildNode, ResolverTree } from '../../../../common/endpoint/types'; +import { ResolverChildNode, ResolverEvent, ResolverTree } from '../../../../common/endpoint/types'; import * as eventModel from '../../../../common/endpoint/models/event'; +import { values } from '../../../../common/endpoint/models/ecs_safety_helpers'; import { mockTreeFetcherParameters } from '../../mocks/tree_fetcher_parameters'; /** @@ -40,7 +41,9 @@ describe('Resolver Data Middleware', () => { // Generate a 'tree' using the Resolver generator code. This structure isn't the same as what the API returns. const baseTree = generateBaseTree(); const tree = mockResolverTree({ - events: baseTree.allEvents, + // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with + // a lot of the frontend functions. So casting it back to the unsafe type for now. + events: baseTree.allEvents as ResolverEvent[], cursors: { childrenNextChild: 'aValidChildCursor', ancestryNextAncestor: 'aValidAncestorCursor', @@ -89,7 +92,9 @@ describe('Resolver Data Middleware', () => { type: 'serverReturnedRelatedEventData', payload: { entityID: firstChildNodeInTree.id, - events: firstChildNodeInTree.relatedEvents, + // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with + // a lot of the frontend functions. So casting it back to the unsafe type for now. + events: firstChildNodeInTree.relatedEvents as ResolverEvent[], nextEvent: null, }, }; @@ -162,7 +167,9 @@ describe('Resolver Data Middleware', () => { type: 'serverReturnedRelatedEventData', payload: { entityID: firstChildNodeInTree.id, - events: firstChildNodeInTree.relatedEvents, + // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with + // a lot of the frontend functions. So casting it back to the unsafe type for now. + events: firstChildNodeInTree.relatedEvents as ResolverEvent[], nextEvent: 'aValidNextEventCursor', }, }; @@ -232,7 +239,9 @@ function mockedTree() { const statsResults = compileStatsForChild(firstChildNodeInTree); const tree = mockResolverTree({ - events: baseTree.allEvents, + // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with + // a lot of the frontend functions. So casting it back to the unsafe type for now. + events: baseTree.allEvents as ResolverEvent[], /** * Calculate children from the ResolverTree response using the children of the `Tree` we generated using the Resolver data generator code. * Compile (and attach) stats to the first child node. @@ -243,14 +252,15 @@ function mockedTree() { * related event limits should be shown. */ children: [...baseTree.children.values()].map((node: TreeNode) => { - // Treat each `TreeNode` as a `ResolverChildNode`. - // These types are almost close enough to be used interchangably (for the purposes of this test.) - const childNode: Partial = node; + const childNode: Partial = {}; + // Casting here because the generator returns the SafeResolverEvent type which isn't yet compatible with + // a lot of the frontend functions. So casting it back to the unsafe type for now. + childNode.lifecycle = node.lifecycle as ResolverEvent[]; // `TreeNode` has `id` which is the same as `entityID`. // The `ResolverChildNode` calls the entityID as `entityID`. // Set `entityID` on `childNode` since the code in test relies on it. - childNode.entityID = (childNode as TreeNode).id; + childNode.entityID = node.id; // This should only be true for the first child. if (node.id === firstChildNodeInTree.id) { @@ -315,10 +325,8 @@ function compileStatsForChild( const compiledStats = node.relatedEvents.reduce( (counts: Record, relatedEvent) => { - // `relatedEvent.event.category` is `string | string[]`. - // Wrap it in an array and flatten that array to get a `string[] | [string]` - // which we can loop over. - const categories: string[] = [relatedEvent.event.category].flat(); + // get an array of categories regardless of whether category is a string or string[] + const categories: string[] = values(relatedEvent.event?.category); for (const category of categories) { // Set the first category as 'categoryToOverCount' diff --git a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx index 6962d300f7072..1317c0ee94b60 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/assets.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/assets.tsx @@ -24,7 +24,8 @@ type ResolverColorNames = | 'resolverBackground' | 'resolverEdge' | 'resolverEdgeText' - | 'resolverBreadcrumbBackground'; + | 'resolverBreadcrumbBackground' + | 'pillStroke'; type ColorMap = Record; interface NodeStyleConfig { @@ -438,6 +439,7 @@ export const useResolverTheme = (): { resolverBreadcrumbBackground: theme.euiColorLightestShade, resolverEdgeText: getThemedOption(theme.euiColorDarkShade, theme.euiColorFullShade), triggerBackingFill: `${theme.euiColorDanger}${getThemedOption('0F', '1F')}`, + pillStroke: theme.euiColorLightShade, }; const nodeAssets: NodeStyleMap = { diff --git a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx index 2aacc5f9176c4..5d7112dd1547a 100644 --- a/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx +++ b/x-pack/plugins/security_solution/public/resolver/view/process_event_dot.tsx @@ -38,6 +38,7 @@ const StyledActionsContainer = styled.div` position: absolute; top: ${(props) => `${props.topPct}%`}; width: auto; + pointer-events: all; `; interface StyledDescriptionText { @@ -61,6 +62,11 @@ const StyledDescriptionText = styled.div` width: fit-content; `; +const StyledOuterGroup = styled.g` + fill: none; + pointer-events: visiblePainted; +`; + /** * An artifact that represents a process node and the things associated with it in the Resolver */ @@ -329,6 +335,7 @@ const UnstyledProcessEventDot = React.memo( } role="img" aria-labelledby={labelHTMLID} + fill="none" style={{ display: 'block', width: '100%', @@ -338,9 +345,10 @@ const UnstyledProcessEventDot = React.memo( left: '0', outline: 'transparent', border: 'none', + pointerEvents: 'none', }} > - + - + unknown; @@ -52,73 +43,51 @@ interface ResolverSubmenuOption { export type ResolverSubmenuOptionList = ResolverSubmenuOption[] | string; -const OptionListItem = styled.div` - width: 175px; +const StyledActionButton = styled(EuiButton)` + &.euiButton--small { + height: fit-content; + line-height: 1; + padding: 0.25em; + font-size: 0.85rem; + } `; -const OptionList = React.memo( +/** + * This will be the "host button" that displays the "total number of related events" and opens + * the sumbmenu (with counts by category) when clicked. + */ +const SubButton = React.memo( ({ - subMenuOptions, - isLoading, + hasMenu, + menuIsOpen, + action, + count, + title, + nodeID, }: { - subMenuOptions: ResolverSubmenuOptionList; - isLoading: boolean; + hasMenu: boolean; + menuIsOpen?: boolean; + action: (evt: React.MouseEvent) => void; + count?: number; + title: string; + nodeID: string; }) => { - const [options, setOptions] = useState(() => - typeof subMenuOptions !== 'object' - ? [] - : subMenuOptions.map((option: ResolverSubmenuOption) => { - const dataTestSubj = 'resolver:map:node-submenu-item'; - return option.prefix - ? { - label: option.optionTitle, - prepend: {option.prefix} , - 'data-test-subj': dataTestSubj, - } - : { - label: option.optionTitle, - prepend: , - 'data-test-subj': dataTestSubj, - }; - }) - ); - - const actionsByLabel: Record unknown> = useMemo(() => { - if (typeof subMenuOptions !== 'object') { - return {}; - } - return subMenuOptions.reduce((titleActionRecord, opt) => { - const { optionTitle, action } = opt; - return { ...titleActionRecord, [optionTitle]: action }; - }, {}); - }, [subMenuOptions]); - - const selectableProps = useMemo(() => { - return { - listProps: { showIcons: true, bordered: true }, - onChange: (newOptions: EuiSelectableOption[]) => { - const selectedOption = newOptions.find((opt) => opt.checked === 'on'); - if (selectedOption) { - const { label } = selectedOption; - const actionToTake = actionsByLabel[label]; - if (typeof actionToTake === 'function') { - actionToTake(); - } - } - setOptions(newOptions); - }, - }; - }, [actionsByLabel]); - + const iconType = menuIsOpen === true ? 'arrowUp' : 'arrowDown'; return ( - - {(list) => {list}} - + {count ? : ''} {title} + ); } ); @@ -177,11 +146,6 @@ const NodeSubMenuComponents = React.memo( [menuAction] ); - const closePopover = useCallback(() => setMenuOpen(false), []); - const popoverId = idGenerator('submenu-popover'); - - const isMenuLoading = optionsWithActions === 'waitingForRelatedEventData'; - // The last projection matrix that was used to position the popover const projectionMatrixAtLastRender = useRef(); @@ -204,6 +168,16 @@ const NodeSubMenuComponents = React.memo( projectionMatrixAtLastRender.current = projectionMatrix; }, [projectionMatrixAtLastRender, projectionMatrix]); + const { + colorMap: { pillStroke: pillBorderStroke, resolverBackground: pillFill }, + } = useResolverTheme(); + const listStylesFromTheme = useMemo(() => { + return { + border: `1.5px solid ${pillBorderStroke}`, + backgroundColor: pillFill, + }; + }, [pillBorderStroke, pillFill]); + if (!optionsWithActions) { /** * When called with a `menuAction` @@ -222,44 +196,47 @@ const NodeSubMenuComponents = React.memo(
); } - /** - * When called with a set of `optionsWithActions`: - * Render with a panel of options that appear when the menu host button is clicked - */ - const submenuPopoverButton = ( - - {count ? : ''} {menuTitle} - - ); + if (typeof optionsWithActions === 'string') { + return <>; + } return ( -
- - {menuIsOpen && typeof optionsWithActions === 'object' && ( - - )} - -
+ <> + + {menuIsOpen ? ( +
    + {optionsWithActions + .sort((opta, optb) => { + return opta.optionTitle.localeCompare(optb.optionTitle); + }) + .map((opt) => { + return ( +
  • + +
  • + ); + })} +
+ ) : null} + ); } ); @@ -271,6 +248,48 @@ export const NodeSubMenu = styled(NodeSubMenuComponents)` display: flex; flex-flow: column; + &.options { + font-size: 0.8rem; + display: flex; + flex-flow: row wrap; + background: transparent; + position: absolute; + top: 6.5em; + contain: content; + width: 12em; + z-index: 2; + } + + &.options .item { + margin: 0.25ch 0.35ch 0.35ch 0; + padding: 0.35em 0.5em; + height: fit-content; + width: fit-content; + border-radius: 2px; + line-height: 0.8; + } + + &.options .item button { + appearance: none; + height: fit-content; + width: fit-content; + line-height: 0.8; + outline-style: none; + border-color: transparent; + box-shadow: none; + } + + &.options .item button:focus { + outline-style: none; + border-color: transparent; + box-shadow: none; + text-decoration: underline; + } + + &.options .item button:active { + transform: scale(0.95); + } + & .euiButton { background-color: ${(props) => props.buttonFill}; border-color: ${(props) => props.buttonBorderColor}; @@ -283,16 +302,4 @@ export const NodeSubMenu = styled(NodeSubMenuComponents)` background-color: ${(props) => props.buttonFill}; } } - - & .euiPopover__anchor { - display: flex; - } - - &.is-open .euiButton { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; - } - &.is-open .euiSelectableListItem__prepend { - color: white; - } `; diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx index c3b67e3300459..bf89cc7fa9084 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.test.tsx @@ -7,7 +7,6 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { FlowTarget, GetIpOverviewQuery, HostEcsFields } from '../../../graphql/types'; import { TestProviders } from '../../../common/mock'; import '../../../common/mock/match_media'; import { getEmptyValue } from '../../../common/components/empty_value'; @@ -23,10 +22,10 @@ import { DEFAULT_MORE_MAX_HEIGHT, MoreContainer, } from './field_renderers'; -import { mockData } from '../../../network/components/ip_overview/mock'; +import { mockData } from '../../../network/components/details/mock'; import { useMountAppended } from '../../../common/utils/use_mount_appended'; - -type AutonomousSystem = GetIpOverviewQuery.AutonomousSystem; +import { AutonomousSystem, FlowTarget } from '../../../../common/search_strategy'; +import { HostEcs } from '../../../../common/ecs/host'; describe('Field Renderers', () => { const mount = useMountAppended(); @@ -98,15 +97,15 @@ describe('Field Renderers', () => { }); describe('#hostIdRenderer', () => { - const emptyIdHost: Partial = { + const emptyIdHost: Partial = { name: ['test'], - id: null, + id: undefined, ip: ['10.10.10.10'], }; - const emptyIpHost: Partial = { + const emptyIpHost: Partial = { name: ['test'], id: ['test'], - ip: null, + ip: undefined, }; test('it renders correctly against snapshot', () => { const wrapper = shallow(hostNameRenderer(mockData.complete.host, '10.10.10.10')); @@ -136,18 +135,18 @@ describe('Field Renderers', () => { }); describe('#hostNameRenderer', () => { - const emptyIdHost: Partial = { + const emptyIdHost: Partial = { name: ['test'], - id: null, + id: undefined, ip: ['10.10.10.10'], }; - const emptyIpHost: Partial = { + const emptyIpHost: Partial = { name: ['test'], id: ['test'], - ip: null, + ip: undefined, }; - const emptyNameHost: Partial = { - name: null, + const emptyNameHost: Partial = { + name: undefined, id: ['test'], ip: ['10.10.10.10'], }; diff --git a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx index 80fe7cb33779a..1f76c2840e8b7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/field_renderers/field_renderers.tsx @@ -10,12 +10,12 @@ import { getOr } from 'lodash/fp'; import React, { Fragment, useState } from 'react'; import styled from 'styled-components'; +import { HostEcs } from '../../../../common/ecs/host'; import { AutonomousSystem, FlowTarget, - HostEcsFields, - IpOverviewData, -} from '../../../graphql/types'; + NetworkDetailsStrategyResponse, +} from '../../../../common/search_strategy'; import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; import { DefaultDraggable } from '../../../common/components/draggables'; import { getEmptyTagValue } from '../../../common/components/empty_value'; @@ -27,7 +27,7 @@ import { ReputationLinkSetting, } from '../../../common/components/links'; import { Spacer } from '../../../common/components/page'; -import * as i18n from '../../../network/components/ip_overview/translations'; +import * as i18n from '../../../network/components/details/translations'; const DraggableContainerFlexGroup = styled(EuiFlexGroup)` flex-grow: unset; @@ -38,7 +38,10 @@ export const IpOverviewId = 'ip-overview'; /** The default max-height of the popover used to show "+n More" items (e.g. `+9 More`) */ export const DEFAULT_MORE_MAX_HEIGHT = '200px'; -export const locationRenderer = (fieldNames: string[], data: IpOverviewData): React.ReactElement => +export const locationRenderer = ( + fieldNames: string[], + data: NetworkDetailsStrategyResponse['networkDetails'] +): React.ReactElement => fieldNames.length > 0 && fieldNames.every((fieldName) => getOr(null, fieldName, data)) ? ( {fieldNames.map((fieldName, index) => { @@ -92,7 +95,7 @@ export const autonomousSystemRenderer = ( ); interface HostIdRendererTypes { - host: HostEcsFields; + host: HostEcs; ipFilter?: string; noLink?: boolean; } @@ -124,8 +127,12 @@ export const hostIdRenderer = ({ getEmptyTagValue() ); -export const hostNameRenderer = (host: HostEcsFields, ipFilter?: string): React.ReactElement => - host.name && host.name[0] && host.ip && (!(ipFilter != null) || host.ip.includes(ipFilter)) ? ( +export const hostNameRenderer = (host?: HostEcs, ipFilter?: string): React.ReactElement => + host && + host.name && + host.name[0] && + host.ip && + (!(ipFilter != null) || host.ip.includes(ipFilter)) ? ( JSX.Element; -// TODO: This causes breaks between elements until the ticket below is fixed -// https://github.com/elastic/ingest-dev/issues/474 export const DefaultFieldRendererComponent: React.FC = ({ attrName, displayCount = 1, diff --git a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx index a0678fb4a437a..e4148b5581435 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.tsx @@ -13,7 +13,7 @@ import { } from '../../../common/components/drag_and_drop/draggable_wrapper'; import { escapeDataProviderId } from '../../../common/components/drag_and_drop/helpers'; import { getOrEmptyTagFromValue } from '../../../common/components/empty_value'; -import { IPDetailsLink } from '../../../common/components/links'; +import { NetworkDetailsLink } from '../../../common/components/links'; import { parseQueryValue } from '../../../timelines/components/timeline/body/renderers/parse_query_value'; import { DataProvider, @@ -122,7 +122,7 @@ const AddressLinksComponent: React.FC<{ /> ) : ( - + ) } truncate={truncate} diff --git a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts index 6a8d56ff41a04..0ec0db9f32776 100644 --- a/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts +++ b/x-pack/plugins/security_solution/server/endpoint/endpoint_app_context_services.ts @@ -12,12 +12,10 @@ import { import { AgentService, IngestManagerStartContract } from '../../../ingest_manager/server'; import { getPackagePolicyCreateCallback } from './ingest_integration'; import { ManifestManager } from './services/artifacts'; -import { ExceptionListClient } from '../../../lists/server'; export type EndpointAppContextServiceStartContract = Partial< Pick > & { - exceptionsListService: ExceptionListClient; logger: Logger; manifestManager?: ManifestManager; registerIngestCallback?: IngestManagerStartContract['registerExternalCallback']; @@ -32,11 +30,9 @@ export class EndpointAppContextService { private agentService: AgentService | undefined; private manifestManager: ManifestManager | undefined; private savedObjectsStart: SavedObjectsServiceStart | undefined; - private exceptionsListService: ExceptionListClient | undefined; public start(dependencies: EndpointAppContextServiceStartContract) { this.agentService = dependencies.agentService; - this.exceptionsListService = dependencies.exceptionsListService; this.manifestManager = dependencies.manifestManager; this.savedObjectsStart = dependencies.savedObjectsStart; @@ -54,13 +50,6 @@ export class EndpointAppContextService { return this.agentService; } - public getExceptionsList() { - if (!this.exceptionsListService) { - throw new Error('exceptionsListService not set'); - } - return this.exceptionsListService; - } - public getManifestManager(): ManifestManager | undefined { return this.manifestManager; } diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index 03754c7be7a5d..b5f35a198fa9e 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -21,7 +21,6 @@ import { import { ManifestManager } from './services/artifacts/manifest_manager/manifest_manager'; import { getManifestManagerMock } from './services/artifacts/manifest_manager/manifest_manager.mock'; import { EndpointAppContext } from './types'; -import { listMock } from '../../../lists/server/mocks'; /** * Creates a mocked EndpointAppContext. @@ -59,7 +58,6 @@ export const createMockEndpointAppContextServiceStartContract = (): jest.Mocked< > => { return { agentService: createMockAgentService(), - exceptionsListService: listMock.getExceptionListClient(), logger: loggingSystemMock.create().get('mock_endpoint_app_context'), savedObjectsStart: savedObjectsServiceMock.createStartContract(), manifestManager: getManifestManagerMock(), diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts index 161a31e2ec934..144c536b4e45f 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/index.ts @@ -9,11 +9,12 @@ import { SearchResponse } from 'elasticsearch'; import { schema } from '@kbn/config-schema'; import Boom from 'boom'; -import { metadataIndexPattern } from '../../../../common/endpoint/constants'; +import { metadataCurrentIndexPattern } from '../../../../common/endpoint/constants'; import { getESQueryHostMetadataByID, kibanaRequestToMetadataListESQuery } from './query_builders'; import { HostInfo, HostMetadata, + HostMetadataDetails, HostResultList, HostStatus, } from '../../../../common/endpoint/types'; @@ -23,10 +24,6 @@ import { Agent, AgentStatus } from '../../../../../ingest_manager/common/types/m import { findAllUnenrolledAgentIds } from './support/unenroll'; import { findAgentIDsByStatus } from './support/agent_status'; -interface HitSource { - _source: HostMetadata; -} - interface MetadataRequestContext { agentService: AgentService; logger: Logger; @@ -127,7 +124,7 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp const queryParams = await kibanaRequestToMetadataListESQuery( req, endpointAppContext, - metadataIndexPattern, + metadataCurrentIndexPattern, { unenrolledAgentIds: unenrolledAgentIds.concat(IGNORED_ELASTIC_AGENT_IDS), statusAgentIDs: statusIDs, @@ -137,7 +134,7 @@ export function registerEndpointRoutes(router: IRouter, endpointAppContext: Endp const response = (await context.core.elasticsearch.legacy.client.callAsCurrentUser( 'search', queryParams - )) as SearchResponse; + )) as SearchResponse; return res.ok({ body: await mapToHostResultList(queryParams, response, metadataRequestContext), @@ -193,17 +190,17 @@ export async function getHostData( metadataRequestContext: MetadataRequestContext, id: string ): Promise { - const query = getESQueryHostMetadataByID(id, metadataIndexPattern); + const query = getESQueryHostMetadataByID(id, metadataCurrentIndexPattern); const response = (await metadataRequestContext.requestHandlerContext.core.elasticsearch.legacy.client.callAsCurrentUser( 'search', query - )) as SearchResponse; + )) as SearchResponse; if (response.hits.hits.length === 0) { return undefined; } - const hostMetadata: HostMetadata = response.hits.hits[0]._source; + const hostMetadata: HostMetadata = response.hits.hits[0]._source.HostDetails; const agent = await findAgent(metadataRequestContext, hostMetadata); if (agent && !agent.active) { @@ -241,19 +238,19 @@ async function findAgent( async function mapToHostResultList( // eslint-disable-next-line @typescript-eslint/no-explicit-any queryParams: Record, - searchResponse: SearchResponse, + searchResponse: SearchResponse, metadataRequestContext: MetadataRequestContext ): Promise { - const totalNumberOfHosts = searchResponse?.aggregations?.total?.value || 0; + const totalNumberOfHosts = + ((searchResponse.hits?.total as unknown) as { value: number; relation: string }).value || 0; if (searchResponse.hits.hits.length > 0) { return { request_page_size: queryParams.size, request_page_index: queryParams.from, hosts: await Promise.all( - searchResponse.hits.hits - .map((response) => response.inner_hits.most_recent.hits.hits) - .flatMap((data) => data as HitSource) - .map(async (entry) => enrichHostMetadata(entry._source, metadataRequestContext)) + searchResponse.hits.hits.map(async (entry) => + enrichHostMetadata(entry._source.HostDetails, metadataRequestContext) + ) ), total: totalNumberOfHosts, }; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts index 29624b35d5c9e..f784941f3539a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/metadata.test.ts @@ -23,6 +23,7 @@ import { import { HostInfo, HostMetadata, + HostMetadataDetails, HostResultList, HostStatus, } from '../../../../common/endpoint/types'; @@ -141,7 +142,7 @@ describe('test endpoint route', () => { bool: { must_not: { terms: { - 'elastic.agent.id': [ + 'HostDetails.elastic.agent.id': [ '00000000-0000-0000-0000-000000000000', '11111111-1111-1111-1111-111111111111', ], @@ -197,7 +198,7 @@ describe('test endpoint route', () => { bool: { must_not: { terms: { - 'elastic.agent.id': [ + 'HostDetails.elastic.agent.id': [ '00000000-0000-0000-0000-000000000000', '11111111-1111-1111-1111-111111111111', ], @@ -442,7 +443,7 @@ describe('Filters Schema Test', () => { }); }); -function createSearchResponse(hostMetadata?: HostMetadata): SearchResponse { +function createSearchResponse(hostMetadata?: HostMetadata): SearchResponse { return ({ took: 15, timed_out: false, @@ -454,7 +455,7 @@ function createSearchResponse(hostMetadata?: HostMetadata): SearchResponse; + } as unknown) as SearchResponse; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts index e9eb7093a7631..84da4a0960820 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.test.ts @@ -7,7 +7,7 @@ import { httpServerMock, loggingSystemMock } from '../../../../../../../src/core import { kibanaRequestToMetadataListESQuery, getESQueryHostMetadataByID } from './query_builders'; import { EndpointAppContextService } from '../../endpoint_app_context_services'; import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; -import { metadataIndexPattern } from '../../../../common/endpoint/constants'; +import { metadataCurrentIndexPattern } from '../../../../common/endpoint/constants'; describe('query builder', () => { describe('MetadataListESQuery', () => { @@ -22,31 +22,16 @@ describe('query builder', () => { service: new EndpointAppContextService(), config: () => Promise.resolve(createMockConfig()), }, - metadataIndexPattern + metadataCurrentIndexPattern ); expect(query).toEqual({ body: { query: { match_all: {}, }, - collapse: { - field: 'host.id', - inner_hits: { - name: 'most_recent', - size: 1, - sort: [{ 'event.created': 'desc' }], - }, - }, - aggs: { - total: { - cardinality: { - field: 'host.id', - }, - }, - }, sort: [ { - 'event.created': { + 'HostDetails.event.created': { order: 'desc', }, }, @@ -54,7 +39,7 @@ describe('query builder', () => { }, from: 0, size: 10, - index: metadataIndexPattern, + index: metadataCurrentIndexPattern, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as Record); }); @@ -74,7 +59,7 @@ describe('query builder', () => { service: new EndpointAppContextService(), config: () => Promise.resolve(createMockConfig()), }, - metadataIndexPattern, + metadataCurrentIndexPattern, { unenrolledAgentIds: [unenrolledElasticAgentId], } @@ -86,29 +71,14 @@ describe('query builder', () => { bool: { must_not: { terms: { - 'elastic.agent.id': [unenrolledElasticAgentId], + 'HostDetails.elastic.agent.id': [unenrolledElasticAgentId], }, }, }, }, - collapse: { - field: 'host.id', - inner_hits: { - name: 'most_recent', - size: 1, - sort: [{ 'event.created': 'desc' }], - }, - }, - aggs: { - total: { - cardinality: { - field: 'host.id', - }, - }, - }, sort: [ { - 'event.created': { + 'HostDetails.event.created': { order: 'desc', }, }, @@ -116,7 +86,7 @@ describe('query builder', () => { }, from: 0, size: 10, - index: metadataIndexPattern, + index: metadataCurrentIndexPattern, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as Record); } @@ -127,7 +97,7 @@ describe('query builder', () => { it('test default query params for all endpoints metadata when body filter is provided', async () => { const mockRequest = httpServerMock.createKibanaRequest({ body: { - filters: { kql: 'not host.ip:10.140.73.246' }, + filters: { kql: 'not HostDetails.host.ip:10.140.73.246' }, }, }); const query = await kibanaRequestToMetadataListESQuery( @@ -137,7 +107,7 @@ describe('query builder', () => { service: new EndpointAppContextService(), config: () => Promise.resolve(createMockConfig()), }, - metadataIndexPattern + metadataCurrentIndexPattern ); expect(query).toEqual({ @@ -152,7 +122,7 @@ describe('query builder', () => { should: [ { match: { - 'host.ip': '10.140.73.246', + 'HostDetails.host.ip': '10.140.73.246', }, }, ], @@ -164,24 +134,9 @@ describe('query builder', () => { ], }, }, - collapse: { - field: 'host.id', - inner_hits: { - name: 'most_recent', - size: 1, - sort: [{ 'event.created': 'desc' }], - }, - }, - aggs: { - total: { - cardinality: { - field: 'host.id', - }, - }, - }, sort: [ { - 'event.created': { + 'HostDetails.event.created': { order: 'desc', }, }, @@ -189,7 +144,7 @@ describe('query builder', () => { }, from: 0, size: 10, - index: metadataIndexPattern, + index: metadataCurrentIndexPattern, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as Record); }); @@ -201,7 +156,7 @@ describe('query builder', () => { const unenrolledElasticAgentId = '1fdca33f-799f-49f4-939c-ea4383c77672'; const mockRequest = httpServerMock.createKibanaRequest({ body: { - filters: { kql: 'not host.ip:10.140.73.246' }, + filters: { kql: 'not HostDetails.host.ip:10.140.73.246' }, }, }); const query = await kibanaRequestToMetadataListESQuery( @@ -211,7 +166,7 @@ describe('query builder', () => { service: new EndpointAppContextService(), config: () => Promise.resolve(createMockConfig()), }, - metadataIndexPattern, + metadataCurrentIndexPattern, { unenrolledAgentIds: [unenrolledElasticAgentId], } @@ -226,7 +181,7 @@ describe('query builder', () => { bool: { must_not: { terms: { - 'elastic.agent.id': [unenrolledElasticAgentId], + 'HostDetails.elastic.agent.id': [unenrolledElasticAgentId], }, }, }, @@ -238,7 +193,7 @@ describe('query builder', () => { should: [ { match: { - 'host.ip': '10.140.73.246', + 'HostDetails.host.ip': '10.140.73.246', }, }, ], @@ -250,24 +205,9 @@ describe('query builder', () => { ], }, }, - collapse: { - field: 'host.id', - inner_hits: { - name: 'most_recent', - size: 1, - sort: [{ 'event.created': 'desc' }], - }, - }, - aggs: { - total: { - cardinality: { - field: 'host.id', - }, - }, - }, sort: [ { - 'event.created': { + 'HostDetails.event.created': { order: 'desc', }, }, @@ -275,7 +215,7 @@ describe('query builder', () => { }, from: 0, size: 10, - index: metadataIndexPattern, + index: metadataCurrentIndexPattern, // eslint-disable-next-line @typescript-eslint/no-explicit-any } as Record); } @@ -285,15 +225,15 @@ describe('query builder', () => { describe('MetadataGetQuery', () => { it('searches for the correct ID', () => { const mockID = 'AABBCCDD-0011-2233-AA44-DEADBEEF8899'; - const query = getESQueryHostMetadataByID(mockID, metadataIndexPattern); + const query = getESQueryHostMetadataByID(mockID, metadataCurrentIndexPattern); expect(query).toEqual({ body: { - query: { match: { 'host.id': mockID } }, - sort: [{ 'event.created': { order: 'desc' } }], + query: { match: { 'HostDetails.host.id': mockID } }, + sort: [{ 'HostDetails.event.created': { order: 'desc' } }], size: 1, }, - index: metadataIndexPattern, + index: metadataCurrentIndexPattern, }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts index ba9be96201dbe..9002d328efbe3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/query_builders.ts @@ -28,24 +28,9 @@ export async function kibanaRequestToMetadataListESQuery( queryBuilderOptions?.unenrolledAgentIds!, queryBuilderOptions?.statusAgentIDs! ), - collapse: { - field: 'host.id', - inner_hits: { - name: 'most_recent', - size: 1, - sort: [{ 'event.created': 'desc' }], - }, - }, - aggs: { - total: { - cardinality: { - field: 'host.id', - }, - }, - }, sort: [ { - 'event.created': { + 'HostDetails.event.created': { order: 'desc', }, }, @@ -90,7 +75,7 @@ function buildQueryBody( ? { must_not: { terms: { - 'elastic.agent.id': unerolledAgentIds, + 'HostDetails.elastic.agent.id': unerolledAgentIds, }, }, } @@ -99,7 +84,7 @@ function buildQueryBody( ? { must: { terms: { - 'elastic.agent.id': statusAgentIDs, + 'HostDetails.elastic.agent.id': statusAgentIDs, }, }, } @@ -137,12 +122,12 @@ export function getESQueryHostMetadataByID(hostID: string, index: string) { body: { query: { match: { - 'host.id': hostID, + 'HostDetails.host.id': hostID, }, }, sort: [ { - 'event.created': { + 'HostDetails.event.created': { order: 'desc', }, }, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts index 54c6cf432aa89..8f68cba893108 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/alerts.ts @@ -5,7 +5,7 @@ */ import { SearchResponse } from 'elasticsearch'; import { esKuery } from '../../../../../../../../src/plugins/data/server'; -import { ResolverEvent } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; import { ResolverQuery } from './base'; import { PaginationBuilder } from '../utils/pagination'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; @@ -13,7 +13,7 @@ import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/com /** * Builds a query for retrieving alerts for a node. */ -export class AlertsQuery extends ResolverQuery { +export class AlertsQuery extends ResolverQuery { private readonly kqlQuery: JsonObject[] = []; constructor( private readonly pagination: PaginationBuilder, @@ -68,7 +68,7 @@ export class AlertsQuery extends ResolverQuery { }; } - formatResponse(response: SearchResponse): ResolverEvent[] { + formatResponse(response: SearchResponse): SafeResolverEvent[] { return this.getResults(response); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts index 0d8a42d7a26f3..a2bdf358745c2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/base.ts @@ -6,7 +6,7 @@ import { SearchResponse } from 'elasticsearch'; import { ILegacyScopedClusterClient } from 'kibana/server'; -import { ResolverEvent } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; import { legacyEventIndexPattern } from './legacy_event_index_pattern'; import { MSearchQuery } from './multi_searcher'; @@ -19,7 +19,7 @@ import { MSearchQuery } from './multi_searcher'; * @param R the is the type after transforming ES's response. Making this definable let's us set whether it is a resolver event * or something else. */ -export abstract class ResolverQuery implements MSearchQuery { +export abstract class ResolverQuery implements MSearchQuery { /** * * @param indexPattern the index pattern to use in the query for finding indices with documents in ES. @@ -77,7 +77,7 @@ export abstract class ResolverQuery implements MSearchQuer * @param ids a single more multiple unique node ids (e.g. entity_id or unique_pid) */ async searchAndFormat(client: ILegacyScopedClusterClient, ids: string | string[]): Promise { - const res: SearchResponse = await this.search(client, ids); + const res: SearchResponse = await this.search(client, ids); return this.formatResponse(res); } @@ -113,5 +113,5 @@ export abstract class ResolverQuery implements MSearchQuer * @param response a SearchResponse from ES resulting from executing this query * @returns the translated ES response into a structured object */ - public abstract formatResponse(response: SearchResponse): T; + public abstract formatResponse(response: SearchResponse): T; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts index 6fb38a32f9581..8c7daf9451217 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/children.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { SearchResponse } from 'elasticsearch'; -import { ResolverEvent } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; import { ResolverQuery } from './base'; import { ChildrenPaginationBuilder } from '../utils/children_pagination'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; @@ -12,7 +12,7 @@ import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/com /** * Builds a query for retrieving descendants of a node. */ -export class ChildrenQuery extends ResolverQuery { +export class ChildrenQuery extends ResolverQuery { constructor( private readonly pagination: ChildrenPaginationBuilder, indexPattern: string | string[], @@ -126,7 +126,7 @@ export class ChildrenQuery extends ResolverQuery { }; } - formatResponse(response: SearchResponse): ResolverEvent[] { + formatResponse(response: SearchResponse): SafeResolverEvent[] { return this.getResults(response); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts index 0969a3c360e4a..bd054d548a93a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/events.ts @@ -5,7 +5,7 @@ */ import { SearchResponse } from 'elasticsearch'; import { esKuery } from '../../../../../../../../src/plugins/data/server'; -import { ResolverEvent } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; import { ResolverQuery } from './base'; import { PaginationBuilder } from '../utils/pagination'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; @@ -13,7 +13,7 @@ import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/com /** * Builds a query for retrieving related events for a node. */ -export class EventsQuery extends ResolverQuery { +export class EventsQuery extends ResolverQuery { private readonly kqlQuery: JsonObject[] = []; constructor( @@ -83,7 +83,7 @@ export class EventsQuery extends ResolverQuery { }; } - formatResponse(response: SearchResponse): ResolverEvent[] { + formatResponse(response: SearchResponse): SafeResolverEvent[] { return this.getResults(response); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts index 0b5728958e91f..ecbc5d8344928 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/lifecycle.ts @@ -6,12 +6,12 @@ import { SearchResponse } from 'elasticsearch'; import { ResolverQuery } from './base'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; -import { ResolverEvent } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; /** * Builds a query for retrieving life cycle information about a node (start, stop, etc). */ -export class LifecycleQuery extends ResolverQuery { +export class LifecycleQuery extends ResolverQuery { protected legacyQuery(endpointID: string, uniquePIDs: string[]): JsonObject { return { query: { @@ -59,7 +59,7 @@ export class LifecycleQuery extends ResolverQuery { }; } - formatResponse(response: SearchResponse): ResolverEvent[] { + formatResponse(response: SearchResponse): SafeResolverEvent[] { return this.getResults(response); } } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts index 02dbd92d9252b..76203973a6211 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/multi_searcher.ts @@ -6,7 +6,7 @@ import { ILegacyScopedClusterClient } from 'kibana/server'; import { MSearchResponse, SearchResponse } from 'elasticsearch'; -import { ResolverEvent } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; /** @@ -37,7 +37,7 @@ export interface QueryInfo { /** * a function to handle the response */ - handler: (response: SearchResponse) => void; + handler: (response: SearchResponse) => void; } /** @@ -65,7 +65,7 @@ export class MultiSearcher { for (const info of queries) { searchQuery.push(...info.query.buildMSearch(info.ids)); } - const res: MSearchResponse = await this.client.callAsCurrentUser('msearch', { + const res: MSearchResponse = await this.client.callAsCurrentUser('msearch', { body: searchQuery, }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts index b8fa409e2ca21..50e56258b7448 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/queries/stats.ts @@ -5,7 +5,7 @@ */ import { SearchResponse } from 'elasticsearch'; import { ResolverQuery } from './base'; -import { ResolverEvent, EventStats } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent, EventStats } from '../../../../../common/endpoint/types'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; export interface StatsResult { @@ -185,7 +185,7 @@ export class StatsQuery extends ResolverQuery { }; } - public formatResponse(response: SearchResponse): StatsResult { + public formatResponse(response: SearchResponse): StatsResult { let alerts: Record = {}; if (response.aggregations?.alerts?.ids?.buckets) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts index efffbc10473d4..f34218ddbde9b 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/alerts_query_handler.ts @@ -6,7 +6,7 @@ import { SearchResponse } from 'elasticsearch'; import { ILegacyScopedClusterClient } from 'kibana/server'; -import { ResolverRelatedAlerts, ResolverEvent } from '../../../../../common/endpoint/types'; +import { ResolverRelatedAlerts, SafeResolverEvent } from '../../../../../common/endpoint/types'; import { createRelatedAlerts } from './node'; import { AlertsQuery } from '../queries/alerts'; import { PaginationBuilder } from './pagination'; @@ -45,7 +45,7 @@ export class RelatedAlertsQueryHandler implements SingleQueryHandler) => { + private handleResponse = (response: SearchResponse) => { const results = this.query.formatResponse(response); this.relatedAlerts = createRelatedAlerts( this.entityID, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts index 7dd47658bc4c1..b796913118c99 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/ancestry_query_handler.ts @@ -7,14 +7,14 @@ import { SearchResponse } from 'elasticsearch'; import { ILegacyScopedClusterClient } from 'kibana/server'; import { - parentEntityId, - entityId, + parentEntityIDSafeVersion, + entityIDSafeVersion, getAncestryAsArray, } from '../../../../../common/endpoint/models/event'; import { - ResolverAncestry, - ResolverEvent, - ResolverLifecycleNode, + SafeResolverAncestry, + SafeResolverEvent, + SafeResolverLifecycleNode, } from '../../../../../common/endpoint/types'; import { createAncestry, createLifecycle } from './node'; import { LifecycleQuery } from '../queries/lifecycle'; @@ -24,8 +24,8 @@ import { QueryHandler } from './fetch'; /** * Retrieve the ancestry portion of a resolver tree. */ -export class AncestryQueryHandler implements QueryHandler { - private readonly ancestry: ResolverAncestry = createAncestry(); +export class AncestryQueryHandler implements QueryHandler { + private readonly ancestry: SafeResolverAncestry = createAncestry(); private ancestorsToFind: string[]; private readonly query: LifecycleQuery; @@ -33,7 +33,7 @@ export class AncestryQueryHandler implements QueryHandler { private levels: number, indexPattern: string, legacyEndpointID: string | undefined, - originNode: ResolverLifecycleNode | undefined + originNode: SafeResolverLifecycleNode | undefined ) { this.ancestorsToFind = getAncestryAsArray(originNode?.lifecycle[0]).slice(0, levels); this.query = new LifecycleQuery(indexPattern, legacyEndpointID); @@ -41,21 +41,28 @@ export class AncestryQueryHandler implements QueryHandler { // add the origin node to the response if it exists if (originNode) { this.ancestry.ancestors.push(originNode); - this.ancestry.nextAncestor = parentEntityId(originNode.lifecycle[0]) || null; + this.ancestry.nextAncestor = parentEntityIDSafeVersion(originNode.lifecycle[0]) || null; } } - private toMapOfNodes(results: ResolverEvent[]) { - return results.reduce((nodes: Map, event: ResolverEvent) => { - const nodeId = entityId(event); - let node = nodes.get(nodeId); - if (!node) { - node = createLifecycle(nodeId, []); - } + private toMapOfNodes(results: SafeResolverEvent[]) { + return results.reduce( + (nodes: Map, event: SafeResolverEvent) => { + const nodeId = entityIDSafeVersion(event); + if (!nodeId) { + return nodes; + } + + let node = nodes.get(nodeId); + if (!node) { + node = createLifecycle(nodeId, []); + } - node.lifecycle.push(event); - return nodes.set(nodeId, node); - }, new Map()); + node.lifecycle.push(event); + return nodes.set(nodeId, node); + }, + new Map() + ); } private setNoMore() { @@ -64,7 +71,7 @@ export class AncestryQueryHandler implements QueryHandler { this.levels = 0; } - private handleResponse = (searchResp: SearchResponse) => { + private handleResponse = (searchResp: SearchResponse) => { const results = this.query.formatResponse(searchResp); if (results.length === 0) { this.setNoMore(); @@ -97,7 +104,7 @@ export class AncestryQueryHandler implements QueryHandler { * Hence: [D, E, B, C, A] */ this.ancestry.ancestors.push(...ancestryNodes.values()); - this.ancestry.nextAncestor = parentEntityId(results[0]) || null; + this.ancestry.nextAncestor = parentEntityIDSafeVersion(results[0]) || null; this.levels = this.levels - ancestryNodes.size; // the results come back in ascending order on timestamp so the first entry in the // results should be the further ancestor (most distant grandparent) diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts index 78e4219aad75c..d33e9a2d70af6 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.test.ts @@ -10,7 +10,7 @@ import { TreeNode, } from '../../../../../common/endpoint/generate_data'; import { ChildrenNodesHelper } from './children_helper'; -import { eventId, isProcessRunning } from '../../../../../common/endpoint/models/event'; +import { eventIDSafeVersion, isProcessRunning } from '../../../../../common/endpoint/models/event'; function getStartEvents(events: Event[]): Event[] { const startEvents: Event[] = []; @@ -179,7 +179,9 @@ describe('Children helper', () => { childrenNodes.childNodes.forEach((node) => { node.lifecycle.forEach((event) => { - expect(childrenEvents.find((child) => child.event.id === eventId(event))).toEqual(event); + expect( + childrenEvents.find((child) => eventIDSafeVersion(child) === eventIDSafeVersion(event)) + ).toEqual(event); }); }); }); @@ -191,7 +193,9 @@ describe('Children helper', () => { const childrenNodes = helper.getNodes(); childrenNodes.childNodes.forEach((node) => { node.lifecycle.forEach((event) => { - expect(childrenEvents.find((child) => child.event.id === eventId(event))).toEqual(event); + expect( + childrenEvents.find((child) => eventIDSafeVersion(child) === eventIDSafeVersion(event)) + ).toEqual(event); }); }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts index b82b972b887b5..e9174548898dd 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_helper.ts @@ -5,15 +5,15 @@ */ import { - entityId, - parentEntityId, + parentEntityIDSafeVersion, isProcessRunning, getAncestryAsArray, + entityIDSafeVersion, } from '../../../../../common/endpoint/models/event'; import { - ResolverChildNode, - ResolverEvent, - ResolverChildren, + SafeResolverChildren, + SafeResolverChildNode, + SafeResolverEvent, } from '../../../../../common/endpoint/types'; import { createChild } from './node'; import { ChildrenPaginationBuilder } from './children_pagination'; @@ -22,7 +22,7 @@ import { ChildrenPaginationBuilder } from './children_pagination'; * This class helps construct the children structure when building a resolver tree. */ export class ChildrenNodesHelper { - private readonly entityToNodeCache: Map = new Map(); + private readonly entityToNodeCache: Map = new Map(); constructor(private readonly rootID: string, private readonly limit: number) { this.entityToNodeCache.set(rootID, createChild(rootID)); @@ -31,8 +31,8 @@ export class ChildrenNodesHelper { /** * Constructs a ResolverChildren response based on the children that were previously add. */ - getNodes(): ResolverChildren { - const cacheCopy: Map = new Map(this.entityToNodeCache); + getNodes(): SafeResolverChildren { + const cacheCopy: Map = new Map(this.entityToNodeCache); const rootNode = cacheCopy.get(this.rootID); let rootNextChild = null; @@ -51,7 +51,7 @@ export class ChildrenNodesHelper { * Get the entity_ids of the nodes that are cached. */ getEntityIDs(): string[] { - const cacheCopy: Map = new Map(this.entityToNodeCache); + const cacheCopy: Map = new Map(this.entityToNodeCache); cacheCopy.delete(this.rootID); return Array.from(cacheCopy.keys()); } @@ -69,9 +69,9 @@ export class ChildrenNodesHelper { * * @param lifecycle an array of resolver lifecycle events for different process nodes returned from ES. */ - addLifecycleEvents(lifecycle: ResolverEvent[]) { + addLifecycleEvents(lifecycle: SafeResolverEvent[]) { for (const event of lifecycle) { - const entityID = entityId(event); + const entityID = entityIDSafeVersion(event); if (entityID) { const cachedChild = this.getOrCreateChildNode(entityID); cachedChild.lifecycle.push(event); @@ -86,19 +86,22 @@ export class ChildrenNodesHelper { * @param queriedNodes the entity_ids of the nodes that returned these start events * @param startEvents an array of start events returned by ES */ - addStartEvents(queriedNodes: Set, startEvents: ResolverEvent[]): Set | undefined { + addStartEvents( + queriedNodes: Set, + startEvents: SafeResolverEvent[] + ): Set | undefined { let largestAncestryArray = 0; const nodesToQueryNext: Map> = new Map(); - const nonLeafNodes: Set = new Set(); + const nonLeafNodes: Set = new Set(); - const isDistantGrandchild = (event: ResolverEvent) => { + const isDistantGrandchild = (event: SafeResolverEvent) => { const ancestry = getAncestryAsArray(event); return ancestry.length > 0 && queriedNodes.has(ancestry[ancestry.length - 1]); }; for (const event of startEvents) { - const parentID = parentEntityId(event); - const entityID = entityId(event); + const parentID = parentEntityIDSafeVersion(event); + const entityID = entityIDSafeVersion(event); if (parentID && entityID && isProcessRunning(event)) { // don't actually add the start event to the node, because that'll be done in // a different call @@ -158,7 +161,7 @@ export class ChildrenNodesHelper { return nodesToQueryNext.get(largestAncestryArray); } - private setPaginationForNodes(nodes: Set, startEvents: ResolverEvent[]) { + private setPaginationForNodes(nodes: Set, startEvents: SafeResolverEvent[]) { for (const nodeEntityID of nodes.values()) { const cachedNode = this.entityToNodeCache.get(nodeEntityID); if (cachedNode) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts index ab610dc9776ca..f9f73c2ad75ff 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/children_lifecycle_query_handler.ts @@ -6,7 +6,7 @@ import { SearchResponse } from 'elasticsearch'; import { ILegacyScopedClusterClient } from 'kibana/server'; -import { ResolverEvent, ResolverChildren } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent, SafeResolverChildren } from '../../../../../common/endpoint/types'; import { LifecycleQuery } from '../queries/lifecycle'; import { QueryInfo } from '../queries/multi_searcher'; import { SingleQueryHandler } from './fetch'; @@ -16,8 +16,8 @@ import { createChildren } from './node'; /** * Returns the children of a resolver tree. */ -export class ChildrenLifecycleQueryHandler implements SingleQueryHandler { - private lifecycle: ResolverChildren | undefined; +export class ChildrenLifecycleQueryHandler implements SingleQueryHandler { + private lifecycle: SafeResolverChildren | undefined; private readonly query: LifecycleQuery; constructor( private readonly childrenHelper: ChildrenNodesHelper, @@ -27,7 +27,7 @@ export class ChildrenLifecycleQueryHandler implements SingleQueryHandler) => { + private handleResponse = (response: SearchResponse) => { this.childrenHelper.addLifecycleEvents(this.query.formatResponse(response)); this.lifecycle = this.childrenHelper.getNodes(); }; @@ -50,7 +50,7 @@ export class ChildrenLifecycleQueryHandler implements SingleQueryHandler) => { + private handleResponse = (response: SearchResponse) => { const results = this.query.formatResponse(response); this.nodesToQuery = this.childrenHelper.addStartEvents(this.nodesToQuery, results) ?? new Set(); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts index 8792f917fb4d6..5c4d9a4741ad7 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/events_query_handler.ts @@ -6,7 +6,7 @@ import { SearchResponse } from 'elasticsearch'; import { ILegacyScopedClusterClient } from 'kibana/server'; -import { ResolverRelatedEvents, ResolverEvent } from '../../../../../common/endpoint/types'; +import { SafeResolverRelatedEvents, SafeResolverEvent } from '../../../../../common/endpoint/types'; import { createRelatedEvents } from './node'; import { EventsQuery } from '../queries/events'; import { PaginationBuilder } from './pagination'; @@ -28,8 +28,8 @@ export interface RelatedEventsParams { /** * This retrieves the related events for the origin node of a resolver tree. */ -export class RelatedEventsQueryHandler implements SingleQueryHandler { - private relatedEvents: ResolverRelatedEvents | undefined; +export class RelatedEventsQueryHandler implements SingleQueryHandler { + private relatedEvents: SafeResolverRelatedEvents | undefined; private readonly query: EventsQuery; private readonly limit: number; private readonly entityID: string; @@ -46,7 +46,7 @@ export class RelatedEventsQueryHandler implements SingleQueryHandler) => { + private handleResponse = (response: SearchResponse) => { const results = this.query.formatResponse(response); this.relatedEvents = createRelatedEvents( this.entityID, diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts index 1b88f965909eb..15a9639872f2a 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/fetch.ts @@ -6,11 +6,11 @@ import { ILegacyScopedClusterClient } from 'kibana/server'; import { - ResolverChildren, - ResolverRelatedEvents, - ResolverAncestry, + SafeResolverChildren, + SafeResolverRelatedEvents, + SafeResolverAncestry, ResolverRelatedAlerts, - ResolverLifecycleNode, + SafeResolverLifecycleNode, } from '../../../../../common/endpoint/types'; import { Tree } from './tree'; import { LifecycleQuery } from '../queries/lifecycle'; @@ -190,7 +190,7 @@ export class Fetcher { * * @param limit upper limit of ancestors to retrieve */ - public async ancestors(limit: number): Promise { + public async ancestors(limit: number): Promise { const originNode = await this.getNode(this.id); const ancestryHandler = new AncestryQueryHandler( limit, @@ -207,7 +207,7 @@ export class Fetcher { * @param limit the number of children to retrieve for a single level * @param after a cursor to use as the starting point for retrieving children */ - public async children(limit: number, after?: string): Promise { + public async children(limit: number, after?: string): Promise { const childrenHandler = new ChildrenStartQueryHandler( limit, this.id, @@ -237,7 +237,7 @@ export class Fetcher { limit: number, after?: string, filter?: string - ): Promise { + ): Promise { const eventsHandler = new RelatedEventsQueryHandler({ limit, entityID: this.id, @@ -285,7 +285,7 @@ export class Fetcher { return tree; } - private async getNode(entityID: string): Promise { + private async getNode(entityID: string): Promise { const query = new LifecycleQuery(this.eventsIndexPattern, this.endpointID); const results = await query.searchAndFormat(this.client, entityID); if (results.length === 0) { diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/lifecycle_query_handler.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/lifecycle_query_handler.ts index ab0501e099490..d4dc12d5e8b66 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/lifecycle_query_handler.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/lifecycle_query_handler.ts @@ -6,7 +6,7 @@ import { SearchResponse } from 'elasticsearch'; import { ILegacyScopedClusterClient } from 'kibana/server'; -import { ResolverEvent, ResolverLifecycleNode } from '../../../../../common/endpoint/types'; +import { SafeResolverEvent, SafeResolverLifecycleNode } from '../../../../../common/endpoint/types'; import { LifecycleQuery } from '../queries/lifecycle'; import { QueryInfo } from '../queries/multi_searcher'; import { SingleQueryHandler } from './fetch'; @@ -15,8 +15,8 @@ import { createLifecycle } from './node'; /** * Retrieve the lifecycle events for a node. */ -export class LifecycleQueryHandler implements SingleQueryHandler { - private lifecycle: ResolverLifecycleNode | undefined; +export class LifecycleQueryHandler implements SingleQueryHandler { + private lifecycle: SafeResolverLifecycleNode | undefined; private readonly query: LifecycleQuery; constructor( private readonly entityID: string, @@ -26,7 +26,7 @@ export class LifecycleQueryHandler implements SingleQueryHandler) => { + private handleResponse = (response: SearchResponse) => { const results = this.query.formatResponse(response); if (results.length !== 0) { this.lifecycle = createLifecycle(this.entityID, results); @@ -51,7 +51,7 @@ export class LifecycleQueryHandler implements SingleQueryHandler { const generator = new EndpointDocGenerator(); - const getSearchAfterInfo = (events: EndpointEvent[]) => { + const getSearchAfterInfo = (events: SafeEndpointEvent[]) => { const lastEvent = events[events.length - 1]; - return [lastEvent['@timestamp'], lastEvent.event.id]; + return [timestampSafeVersion(lastEvent), eventIDSafeVersion(lastEvent)]; }; describe('cursor', () => { const root = generator.generateEvent(); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts index 4a6c65e55a6b6..af0311a262f30 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/pagination.ts @@ -4,8 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ResolverEvent } from '../../../../../common/endpoint/types'; -import { eventId } from '../../../../../common/endpoint/models/event'; +import { SafeResolverEvent } from '../../../../../common/endpoint/types'; +import { + eventIDSafeVersion, + timestampSafeVersion, +} from '../../../../../common/endpoint/models/event'; import { JsonObject } from '../../../../../../../../src/plugins/kibana_utils/common'; import { ChildrenPaginationCursor } from './children_pagination'; @@ -116,11 +119,12 @@ export class PaginationBuilder { * * @param results the events that were returned by the ES query */ - static buildCursor(results: ResolverEvent[]): string | null { + static buildCursor(results: SafeResolverEvent[]): string | null { const lastResult = results[results.length - 1]; const cursor = { - timestamp: lastResult['@timestamp'], - eventID: eventId(lastResult) === undefined ? '' : String(eventId(lastResult)), + timestamp: timestampSafeVersion(lastResult) ?? 0, + eventID: + eventIDSafeVersion(lastResult) === undefined ? '' : String(eventIDSafeVersion(lastResult)), }; return urlEncodeCursor(cursor); } @@ -131,7 +135,10 @@ export class PaginationBuilder { * @param requestLimit the request limit for a query. * @param results the events that were returned by the ES query */ - static buildCursorRequestLimit(requestLimit: number, results: ResolverEvent[]): string | null { + static buildCursorRequestLimit( + requestLimit: number, + results: SafeResolverEvent[] + ): string | null { if (requestLimit <= results.length && results.length > 0) { return PaginationBuilder.buildCursor(results); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts index 21db11f3affd3..290af87a61b1d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.test.ts @@ -7,28 +7,28 @@ import { EndpointDocGenerator } from '../../../../../common/endpoint/generate_data'; import { Tree } from './tree'; import { - ResolverAncestry, - ResolverEvent, - ResolverRelatedEvents, + SafeResolverAncestry, + SafeResolverEvent, + SafeResolverRelatedEvents, } from '../../../../../common/endpoint/types'; -import { entityId } from '../../../../../common/endpoint/models/event'; +import { entityIDSafeVersion } from '../../../../../common/endpoint/models/event'; describe('Tree', () => { const generator = new EndpointDocGenerator(); describe('ancestry', () => { // transform the generator's array of events into the format expected by the tree class - const ancestorInfo: ResolverAncestry = { + const ancestorInfo: SafeResolverAncestry = { ancestors: generator .createAlertEventAncestry({ ancestors: 5, percentTerminated: 0, percentWithRelated: 0 }) .filter((event) => { - return event.event.kind === 'event'; + return event.event?.kind === 'event'; }) .map((event) => { return { - entityID: event.process.entity_id, + entityID: entityIDSafeVersion(event) ?? '', // The generator returns Events, but the tree needs a ResolverEvent - lifecycle: [event as ResolverEvent], + lifecycle: [event as SafeResolverEvent], }; }), nextAncestor: 'hello', @@ -39,7 +39,7 @@ describe('Tree', () => { const ids = tree.ids(); ids.forEach((id) => { const foundAncestor = ancestorInfo.ancestors.find( - (ancestor) => entityId(ancestor.lifecycle[0]) === id + (ancestor) => entityIDSafeVersion(ancestor.lifecycle[0]) === id ); expect(foundAncestor).not.toBeUndefined(); }); @@ -50,12 +50,12 @@ describe('Tree', () => { describe('related events', () => { it('adds related events to the tree', () => { const root = generator.generateEvent(); - const events: ResolverRelatedEvents = { - entityID: root.process.entity_id, + const events: SafeResolverRelatedEvents = { + entityID: entityIDSafeVersion(root) ?? '', events: Array.from(generator.relatedEventsGenerator(root)), nextEvent: null, }; - const tree = new Tree(root.process.entity_id, { relatedEvents: events }); + const tree = new Tree(entityIDSafeVersion(root) ?? '', { relatedEvents: events }); const rendered = tree.render(); expect(rendered.relatedEvents.nextEvent).toBeNull(); expect(rendered.relatedEvents.events).toStrictEqual(events.events); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts index 3f941851a4143..dd493d70ffcd3 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/resolver/utils/tree.ts @@ -6,26 +6,26 @@ import _ from 'lodash'; import { - ResolverEvent, + SafeResolverEvent, ResolverNodeStats, - ResolverRelatedEvents, - ResolverAncestry, - ResolverTree, - ResolverChildren, + SafeResolverRelatedEvents, + SafeResolverAncestry, + SafeResolverTree, + SafeResolverChildren, ResolverRelatedAlerts, } from '../../../../../common/endpoint/types'; import { createTree } from './node'; interface Node { entityID: string; - lifecycle: ResolverEvent[]; + lifecycle: SafeResolverEvent[]; stats?: ResolverNodeStats; } export interface Options { - relatedEvents?: ResolverRelatedEvents; - ancestry?: ResolverAncestry; - children?: ResolverChildren; + relatedEvents?: SafeResolverRelatedEvents; + ancestry?: SafeResolverAncestry; + children?: SafeResolverChildren; relatedAlerts?: ResolverRelatedAlerts; } @@ -37,7 +37,7 @@ export interface Options { */ export class Tree { protected cache: Map = new Map(); - protected tree: ResolverTree; + protected tree: SafeResolverTree; constructor(protected readonly id: string, options: Options = {}) { const tree = createTree(this.id); @@ -55,7 +55,7 @@ export class Tree { * * @returns the origin ResolverNode */ - public render(): ResolverTree { + public render(): SafeResolverTree { return this.tree; } @@ -73,7 +73,7 @@ export class Tree { * * @param relatedEventsInfo is the related events and pagination information to add to the tree. */ - private addRelatedEvents(relatedEventsInfo: ResolverRelatedEvents | undefined) { + private addRelatedEvents(relatedEventsInfo: SafeResolverRelatedEvents | undefined) { if (!relatedEventsInfo) { return; } @@ -101,7 +101,7 @@ export class Tree { * * @param ancestorInfo is the ancestors and pagination information to add to the tree. */ - private addAncestors(ancestorInfo: ResolverAncestry | undefined) { + private addAncestors(ancestorInfo: SafeResolverAncestry | undefined) { if (!ancestorInfo) { return; } @@ -132,7 +132,7 @@ export class Tree { } } - private addChildren(children: ResolverChildren | undefined) { + private addChildren(children: SafeResolverChildren | undefined) { if (!children) { return; } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts index a3e6f54f3eee8..ec4d1efb81b11 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { RequestHandler } from 'kibana/server'; +import { RequestHandler, RequestHandlerContext } from 'kibana/server'; import { GetTrustedAppsListRequest, GetTrustedListAppsResponse, @@ -14,6 +14,17 @@ import { EndpointAppContext } from '../../types'; import { exceptionItemToTrustedAppItem, newTrustedAppItemToExceptionItem } from './utils'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; import { DeleteTrustedAppsRequestParams } from './types'; +import { ExceptionListClient } from '../../../../../lists/server'; + +const exceptionListClientFromContext = (context: RequestHandlerContext): ExceptionListClient => { + const exceptionLists = context.lists?.getExceptionListClient(); + + if (!exceptionLists) { + throw new Error('Exception List client not found'); + } + + return exceptionLists; +}; export const getTrustedAppsDeleteRouteHandler = ( endpointAppContext: EndpointAppContext @@ -21,9 +32,8 @@ export const getTrustedAppsDeleteRouteHandler = ( const logger = endpointAppContext.logFactory.get('trusted_apps'); return async (context, req, res) => { - const exceptionsListService = endpointAppContext.service.getExceptionsList(); - try { + const exceptionsListService = exceptionListClientFromContext(context); const { id } = req.params; const response = await exceptionsListService.deleteExceptionListItem({ id, @@ -49,10 +59,10 @@ export const getTrustedAppsListRouteHandler = ( const logger = endpointAppContext.logFactory.get('trusted_apps'); return async (context, req, res) => { - const exceptionsListService = endpointAppContext.service.getExceptionsList(); const { page, per_page: perPage } = req.query; try { + const exceptionsListService = exceptionListClientFromContext(context); // Ensure list is created if it does not exist await exceptionsListService.createTrustedAppsList(); const results = await exceptionsListService.findExceptionListItem({ @@ -83,11 +93,11 @@ export const getTrustedAppsCreateRouteHandler = ( ): RequestHandler => { const logger = endpointAppContext.logFactory.get('trusted_apps'); - return async (constext, req, res) => { - const exceptionsListService = endpointAppContext.service.getExceptionsList(); + return async (context, req, res) => { const newTrustedApp = req.body; try { + const exceptionsListService = exceptionListClientFromContext(context); // Ensure list is created if it does not exist await exceptionsListService.createTrustedAppsList(); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts index 2325036ef40ae..eeee2d99bf26d 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/trusted_apps.test.ts @@ -24,15 +24,23 @@ import { import { xpackMocks } from '../../../../../../mocks'; import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '../../../../../lists/common/constants'; import { EndpointAppContext } from '../../types'; -import { ExceptionListClient } from '../../../../../lists/server'; -import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; +import { ExceptionListClient, ListClient } from '../../../../../lists/server'; +import { listMock } from '../../../../../lists/server/mocks'; import { ExceptionListItemSchema } from '../../../../../lists/common/schemas/response'; import { DeleteTrustedAppsRequestParams } from './types'; +import { getExceptionListItemSchemaMock } from '../../../../../lists/common/schemas/response/exception_list_item_schema.mock'; + +type RequestHandlerContextWithLists = ReturnType & { + lists?: { + getListClient: () => jest.Mocked; + getExceptionListClient: () => jest.Mocked; + }; +}; describe('when invoking endpoint trusted apps route handlers', () => { let routerMock: jest.Mocked; let endpointAppContextService: EndpointAppContextService; - let context: ReturnType; + let context: RequestHandlerContextWithLists; let response: ReturnType; let exceptionsListClient: jest.Mocked; let endpointAppContext: EndpointAppContext; @@ -41,7 +49,7 @@ describe('when invoking endpoint trusted apps route handlers', () => { routerMock = httpServiceMock.createRouter(); endpointAppContextService = new EndpointAppContextService(); const startContract = createMockEndpointAppContextServiceStartContract(); - exceptionsListClient = startContract.exceptionsListService as jest.Mocked; + exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked; endpointAppContextService.start(startContract); endpointAppContext = { ...createMockEndpointAppContext(), @@ -50,7 +58,13 @@ describe('when invoking endpoint trusted apps route handlers', () => { registerTrustedAppsRoutes(routerMock, endpointAppContext); // For use in individual API calls - context = xpackMocks.createRequestHandlerContext(); + context = { + ...xpackMocks.createRequestHandlerContext(), + lists: { + getListClient: jest.fn(), + getExceptionListClient: jest.fn().mockReturnValue(exceptionsListClient), + }, + }; response = httpServerMock.createResponseFactory(); }); @@ -74,6 +88,12 @@ describe('when invoking endpoint trusted apps route handlers', () => { )!; }); + it('should use ExceptionListClient from route handler context', async () => { + const request = createListRequest(); + await routeHandler(context, request, response); + expect(context.lists?.getExceptionListClient).toHaveBeenCalled(); + }); + it('should create the Trusted Apps List first', async () => { const request = createListRequest(); await routeHandler(context, request, response); @@ -155,6 +175,12 @@ describe('when invoking endpoint trusted apps route handlers', () => { }); }); + it('should use ExceptionListClient from route handler context', async () => { + const request = createPostRequest(); + await routeHandler(context, request, response); + expect(context.lists?.getExceptionListClient).toHaveBeenCalled(); + }); + it('should create trusted app list first', async () => { const request = createPostRequest(); await routeHandler(context, request, response); @@ -238,6 +264,11 @@ describe('when invoking endpoint trusted apps route handlers', () => { }); }); + it('should use ExceptionListClient from route handler context', async () => { + await routeHandler(context, request, response); + expect(context.lists?.getExceptionListClient).toHaveBeenCalled(); + }); + it('should return 200 on successful delete', async () => { await routeHandler(context, request, response); expect(exceptionsListClient.deleteExceptionListItem).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json index 7d80a319e9e52..cfce019910071 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/signals_mapping.json @@ -22,11 +22,33 @@ } } }, + "parents": { + "properties": { + "rule": { + "type": "keyword" + }, + "index": { + "type": "keyword" + }, + "id": { + "type": "keyword" + }, + "type": { + "type": "keyword" + }, + "depth": { + "type": "long" + } + } + }, "ancestors": { "properties": { "rule": { "type": "keyword" }, + "index": { + "type": "keyword" + }, "id": { "type": "keyword" }, @@ -299,6 +321,9 @@ }, "threshold_count": { "type": "float" + }, + "depth": { + "type": "integer" } } } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/README.md index 2310ba979da20..7cf7d11e4c1f8 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/README.md +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/README.md @@ -22,7 +22,7 @@ which will write a single signal document into the signals index by searching fo signal_on_signal_depth_1.json ``` -which has this key part of its query: `"query": "signal.parent.depth: 1 and _id: *"` which will only create signals +which has this key part of its query: `"query": "signal.depth: 1 and _id: *"` which will only create signals from all signals that point directly to an event (signal -> event). Then a second rule called @@ -34,7 +34,7 @@ signal_on_signal_depth_2.json which will only create signals from all signals that point directly to another signal (signal -> signal) with this query ```json -"query": "signal.parent.depth: 2 and _id: *" +"query": "signal.depth: 2 and _id: *" ``` ## Setup @@ -90,38 +90,43 @@ And then you can query against that: GET .siem-signals-default/_search ``` -Check your parent section of the signal and you will see something like this: +Check your `signal` section of the signal and you will see something like this: ```json -"parent" : { - "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", - "id" : "o8G7vm8BvLT8jmu5B1-M", - "type" : "event", - "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 -}, -"ancestors" : [ +"parents" : [ { - "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 } -] +], +"ancestors" : [ + { + "id" : "o8G7vm8BvLT8jmu5B1-M", + "type" : "event", + "index" : "filebeat-8.0.0-2019.12.18-000001", + "depth" : 0 + }, +], +"depth": 1, +"rule": { + "id": "74e0dd0c-4609-416f-b65e-90f8b2564612" +} ``` -The parent and ancestors structure is defined as: +The parents structure is defined as: ``` -rule -> The id of the rule. You can view the rule by ./get_rule_by_rule_id.sh ded57b36-9c4e-4ee4-805d-be4e92033e41 +rule -> The id of the rule, if the parent was generated by a rule. You can view the rule by ./get_rule_by_rule_id.sh ded57b36-9c4e-4ee4-805d-be4e92033e41 id -> The original _id of the document type -> The type of the document, it will be either event or signal index -> The original location of the index -depth -> The depth of this signal. It will be at least 1 to indicate it is a signal generated from a event. Otherwise 2 or more to indicate a signal on signal and what depth we are at -ancestors -> An array tracking all of the parents of this particular signal. As depth increases this will too. +depth -> The depth of the parent event/signal. It will be 0 if the parent is an event, or 1+ if the parent is another signal. ``` +The ancestors structure has the same fields as parents, but is an array of all ancestors (parents, grandparents, etc) of the signal. + This is indicating that you have a single parent of an event from the signal (signal -> event) and this document has a single ancestor of that event. Each 30 seconds that goes it will use de-duplication technique to ensure that this signal is not re-inserted. If after each 30 seconds you DO SEE multiple signals then the bug is a de-duplication bug and a critical bug. If you ever see a duplicate rule in the @@ -138,55 +143,64 @@ running in the system which are generating signals on top of signals. After 30 s documents in the signals index. The first signal is our original (signal -> event) document with a rule id: ```json -"parent" : { - "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", - "id" : "o8G7vm8BvLT8jmu5B1-M", - "type" : "event", - "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 -}, +"parents" : [ + { + "id" : "o8G7vm8BvLT8jmu5B1-M", + "type" : "event", + "index" : "filebeat-8.0.0-2019.12.18-000001", + "depth" : 0 + } +], "ancestors" : [ { - "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 } -] +], +"depth": 1, +"rule": { + "id": "74e0dd0c-4609-416f-b65e-90f8b2564612" +} ``` and the second document is a signal on top of a signal like so: ```json -"parent" : { - "rule" : "1d3b3735-66ef-4e53-b7f5-4340026cc40c", - "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4", - "type" : "signal", - "index" : ".siem-signals-default-000001", - "depth" : 2 -}, -"ancestors" : [ +"parents" : [ { "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", + "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4", + "type" : "signal", + "index" : ".siem-signals-default-000001", + "depth" : 1 + } +] +"ancestors" : [ + { "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 }, { - "rule" : "1d3b3735-66ef-4e53-b7f5-4340026cc40c", + "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 2 + "depth" : 1 } -] +], +"depth": 2, +"rule": { + "id": "1d3b3735-66ef-4e53-b7f5-4340026cc40c" +} ``` Notice that the depth indicates it is at level 2 and its parent is that of a signal. Also notice that the ancestors is an array of size 2 indicating that this signal terminates at an event. Each and every signal ancestors array should terminate at an event and should ONLY contain 1 -event and NEVER 2 or more events. After 30+ seconds you should NOT see any new documents being created and you should be stable +event and NEVER 2 or more events for KQL query based rules. EQL query based rules that use sequences may have multiple parents at the same level. After 30+ seconds you should NOT see any new documents being created and you should be stable at 2. Otherwise we have AND/OR a de-duplication issue, signal on signal issue. Now, post this same rule a second time as a second instance which is going to run against these two documents. @@ -212,79 +226,93 @@ The expected behavior is that eventually you will get 3 total documents but not The original event rule 74e0dd0c-4609-416f-b65e-90f8b2564612 (event -> signal) ```json -"parent" : { - "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", - "id" : "o8G7vm8BvLT8jmu5B1-M", - "type" : "event", - "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 -}, +"parents" : [ + { + "id" : "o8G7vm8BvLT8jmu5B1-M", + "type" : "event", + "index" : "filebeat-8.0.0-2019.12.18-000001", + "depth" : 0 + } +], "ancestors" : [ { - "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 } -] +], +"depth": 1, +"rule": { + "id": "74e0dd0c-4609-416f-b65e-90f8b2564612" +} ``` The first signal to signal rule 1d3b3735-66ef-4e53-b7f5-4340026cc40c (signal -> event) ```json -"parent" : { - "rule" : "1d3b3735-66ef-4e53-b7f5-4340026cc40c", - "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4", - "type" : "signal", - "index" : ".siem-signals-default-000001", - "depth" : 2 -}, -"ancestors" : [ +"parents" : [ { "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", + "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4", + "type" : "signal", + "index" : ".siem-signals-default-000001", + "depth" : 1 + } +] +"ancestors" : [ + { "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 }, { - "rule" : "1d3b3735-66ef-4e53-b7f5-4340026cc40c", + "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 2 + "depth" : 1 } -] +], +"depth": 2, +"rule": { + "id": "1d3b3735-66ef-4e53-b7f5-4340026cc40c" +} ``` Then our second signal to signal rule c93ddb57-e7e9-4973-9886-72ddefb4d22e (signal -> event) which finds the same thing as the first signal to signal ```json -"parent" : { - "rule" : "c93ddb57-e7e9-4973-9886-72ddefb4d22e", - "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4", - "type" : "signal", - "index" : ".siem-signals-default-000001", - "depth" : 2 -}, -"ancestors" : [ +"parents" : [ { "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", + "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4", + "type" : "signal", + "index" : ".siem-signals-default-000001", + "depth" : 1 + } +], +"ancestors" : [ + { "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 }, { - "rule" : "c93ddb57-e7e9-4973-9886-72ddefb4d22e", + "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 2 + "depth" : 1 } -] +], +"depth": 2, +"rule": { + "id": "c93ddb57-e7e9-4973-9886-72ddefb4d22e" +} ``` We should be able to post this depth level as many times as we want and get only 1 new document each time. If we decide though to @@ -298,69 +326,79 @@ The expectation is that a document for each of the previous depth 1 documents wo depth 1 rules running then the signals at depth 2 will produce two new ones and those two will look like so: ```json -"parent" : { - "rule" : "a1f7b520-5bfd-451d-af59-428f60753fee", - "id" : "365236ce5e77770508152403b4e16613f407ae4b1a135a450dcfec427f2a3231", - "type" : "signal", - "index" : ".siem-signals-default-000001", - "depth" : 3 -}, +"parents" : [ + { + "rule" : "1d3b3735-66ef-4e53-b7f5-4340026cc40c", + "id" : "365236ce5e77770508152403b4e16613f407ae4b1a135a450dcfec427f2a3231", + "type" : "signal", + "index" : ".siem-signals-default-000001", + "depth" : 2 + } +], "ancestors" : [ { - "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 }, { - "rule" : "1d3b3735-66ef-4e53-b7f5-4340026cc40c", + "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 2 + "depth" : 1 }, { - "rule" : "a1f7b520-5bfd-451d-af59-428f60753fee", + "rule" : "1d3b3735-66ef-4e53-b7f5-4340026cc40c", "id" : "365236ce5e77770508152403b4e16613f407ae4b1a135a450dcfec427f2a3231", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 3 + "depth" : 2 } -] +], +"depth": 3, +"rule": { + "id": "a1f7b520-5bfd-451d-af59-428f60753fee" +} ``` ```json -"parent" : { - "rule" : "a1f7b520-5bfd-451d-af59-428f60753fee", - "id" : "e8b1f1adb40fd642fa524dea89ef94232e67b05e99fb0b2683f1e47e90b759fb", - "type" : "signal", - "index" : ".siem-signals-default-000001", - "depth" : 3 -}, +"parents" : [ + { + "rule" : "c93ddb57-e7e9-4973-9886-72ddefb4d22e", + "id" : "e8b1f1adb40fd642fa524dea89ef94232e67b05e99fb0b2683f1e47e90b759fb", + "type" : "signal", + "index" : ".siem-signals-default-000001", + "depth" : 2 + } +], "ancestors" : [ { - "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 }, { - "rule" : "c93ddb57-e7e9-4973-9886-72ddefb4d22e", + "rule" : "74e0dd0c-4609-416f-b65e-90f8b2564612", "id" : "4cc69c1cbecdd2ace4075fd1d8a5c28e7d46e4bf31aecc8d2da39252c50c96b4", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 2 + "depth" : 1 }, { - "rule" : "a1f7b520-5bfd-451d-af59-428f60753fee", + "rule" : "c93ddb57-e7e9-4973-9886-72ddefb4d22e", "id" : "e8b1f1adb40fd642fa524dea89ef94232e67b05e99fb0b2683f1e47e90b759fb", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 3 + "depth" : 2 } -] +], +"depth": 3, +"rule": { + "id": "a1f7b520-5bfd-451d-af59-428f60753fee" +} ``` The total number of documents should be 5 at this point. If you were to post this same rule a second time to get a second instance diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/query_single_id.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/query_single_id.json index dc05c656d7cf1..305aa34992623 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/query_single_id.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/query_single_id.json @@ -7,6 +7,6 @@ "from": "now-1d", "interval": "30s", "to": "now", - "query": "_id: o8G7vm8BvLT8jmu5B1-M", + "query": "event.id: 08cde4aa-d249-4e6b-8300-06f3d56c7fe7", "enabled": true } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_1.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_1.json index fb13413a02791..c9132ddb0a590 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_1.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_1.json @@ -7,7 +7,7 @@ "from": "now-1d", "interval": "30s", "to": "now", - "query": "signal.parent.depth: 1 and _id: *", + "query": "signal.depth: 1 and _id: *", "enabled": true, - "index": ".siem-signals-default" + "index": [".siem-signals-default"] } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_2.json b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_2.json index c1b7594653ec7..d1a2749792686 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_2.json +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/depth_test/signal_on_signal_depth_2.json @@ -7,7 +7,7 @@ "from": "now-1d", "interval": "30s", "to": "now", - "query": "signal.parent.depth: 2 and _id: *", + "query": "signal.depth: 2 and _id: *", "enabled": true, - "index": ".siem-signals-default" + "index": [".siem-signals-default"] } diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/README.md b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/README.md index b1a83f5317776..01b21bf762e44 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/README.md +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/rules/test_cases/signals_on_signals/halting_test/README.md @@ -69,38 +69,43 @@ And then you can query against that: GET .siem-signals-default/_search ``` -Check your parent section of the signal and you will see something like this: +Check your `signal` section of the signal and you will see something like this: ```json -"parent" : { - "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", - "id" : "o8G7vm8BvLT8jmu5B1-M", - "type" : "event", - "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 -}, -"ancestors" : [ +"parents" : [ { - "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 } -] +], +"ancestors" : [ + { + "id" : "o8G7vm8BvLT8jmu5B1-M", + "type" : "event", + "index" : "filebeat-8.0.0-2019.12.18-000001", + "depth" : 0 + }, +], +"depth": 1, +"rule": { + "id": "ded57b36-9c4e-4ee4-805d-be4e92033e41" +} ``` -The parent and ancestors structure is defined as: +The parents structure is defined as: ``` -rule -> The id of the rule. You can view the rule by ./get_rule_by_rule_id.sh ded57b36-9c4e-4ee4-805d-be4e92033e41 +rule -> The id of the rule, if the parent was generated by a rule. You can view the rule by ./get_rule_by_rule_id.sh ded57b36-9c4e-4ee4-805d-be4e92033e41 id -> The original _id of the document type -> The type of the document, it will be either event or signal index -> The original location of the index -depth -> The depth of this signal. It will be at least 1 to indicate it is a signal generated from a event. Otherwise 2 or more to indicate a signal on signal and what depth we are at -ancestors -> An array tracking all of the parents of this particular signal. As depth increases this will too. +depth -> The depth of the parent event/signal. It will be 0 if the parent is an event, or 1+ if the parent is another signal. ``` +The ancestors structure has the same fields as parents, but is an array of all ancestors (parents, grandparents, etc) of the signal. + This is indicating that you have a single parent of an event from the signal (signal -> event) and this document has a single ancestor of that event. Each 30 seconds that goes it will use de-duplication technique to ensure that this signal is not re-inserted. If after each 30 seconds you DO SEE multiple signals then the bug is a de-duplication bug and a critical bug. If you ever see a duplicate rule in the @@ -119,22 +124,26 @@ documents in the signals index. The first signal is our original (signal -> even (signal -> event) ```json -"parent" : { - "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", - "id" : "o8G7vm8BvLT8jmu5B1-M", - "type" : "event", - "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 -}, -"ancestors" : [ +"parents" : [ { - "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 } -] +], +"ancestors" : [ + { + "id" : "o8G7vm8BvLT8jmu5B1-M", + "type" : "event", + "index" : "filebeat-8.0.0-2019.12.18-000001", + "depth" : 0 + }, +], +"depth": 1, +"rule": { + "id": "ded57b36-9c4e-4ee4-805d-be4e92033e41" +} ``` and the second document is a signal on top of a signal like so: @@ -143,28 +152,31 @@ and the second document is a signal on top of a signal like so: ```json "parent" : { - "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904", + "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 2 + "depth" : 1 }, "ancestors" : [ { - "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 }, { - "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904", + "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 2 + "depth" : 1 } -] +], +"depth": 2, +"rule": { + "id": "161fa5b8-0b96-4985-b066-0d99b2bcb904" +} ``` Notice that the depth indicates it is at level 2 and its parent is that of a signal. Also notice that the ancestors is an array of size 2 @@ -195,50 +207,57 @@ The expected behavior is that eventually you will get 5 total documents but not The original event rule ded57b36-9c4e-4ee4-805d-be4e92033e41 (event -> signal) ```json -"parent" : { - "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", - "id" : "o8G7vm8BvLT8jmu5B1-M", - "type" : "event", - "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 -}, -"ancestors" : [ +"parents" : [ { - "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 } -] +], +"ancestors" : [ + { + "id" : "o8G7vm8BvLT8jmu5B1-M", + "type" : "event", + "index" : "filebeat-8.0.0-2019.12.18-000001", + "depth" : 0 + }, +], +"depth": 1, +"rule": { + "id": "ded57b36-9c4e-4ee4-805d-be4e92033e41" +} ``` The first signal to signal rule 161fa5b8-0b96-4985-b066-0d99b2bcb904 (signal -> event) ```json "parent" : { - "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904", + "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 2 + "depth" : 1 }, "ancestors" : [ { - "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 }, { - "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904", + "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 2 + "depth" : 1 } -] +], +"depth": 2, +"rule": { + "id": "161fa5b8-0b96-4985-b066-0d99b2bcb904" +} ``` Then our second signal to signal rule f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406 (signal -> event) which finds the same thing as the first @@ -246,28 +265,31 @@ signal to signal ```json "parent" : { - "rule" : "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406", + "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 2 + "depth" : 1 }, "ancestors" : [ { - "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 }, { - "rule" : "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406", + "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 2 + "depth" : 1 } -] +], +"depth": 2, +"rule": { + "id": "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406" +} ``` But then f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406 also finds the first signal to signal rule from 161fa5b8-0b96-4985-b066-0d99b2bcb904 @@ -275,35 +297,38 @@ and writes that document out with a depth of 3. (signal -> signal -> event) ```json "parent" : { - "rule" : "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406", + "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904", "id" : "c627e5e2576f1b10952c6c57249947e89b6153b763a59fb9e391d0b56be8e7fe", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 3 + "depth" : 2 }, "ancestors" : [ { - "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 }, { - "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904", + "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 2 + "depth" : 1 }, { - "rule" : "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406", + "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904", "id" : "c627e5e2576f1b10952c6c57249947e89b6153b763a59fb9e391d0b56be8e7fe", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 3 + "depth" : 2 } -] +], +"depth": 3, +"rule": { + "id": "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406" +} ``` Since it wrote that document, the first signal to signal 161fa5b8-0b96-4985-b066-0d99b2bcb904 writes out it found this newly created signal @@ -311,35 +336,38 @@ Since it wrote that document, the first signal to signal 161fa5b8-0b96-4985-b066 ```json "parent" : { - "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904", + "rule" : "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406", "id" : "efbe514e8d806a5ef3da7658cfa73961e25befefc84f622e963b45dcac798868", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 3 + "depth" : 2 }, "ancestors" : [ { - "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "o8G7vm8BvLT8jmu5B1-M", "type" : "event", "index" : "filebeat-8.0.0-2019.12.18-000001", - "depth" : 1 + "depth" : 0 }, { - "rule" : "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406", + "rule" : "ded57b36-9c4e-4ee4-805d-be4e92033e41", "id" : "9d8710925adbf1a9c469621805407e74334dd08ca2c2ea414840fe971a571938", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 2 + "depth" : 1 }, { - "rule" : "161fa5b8-0b96-4985-b066-0d99b2bcb904", + "rule" : "f2b70c4a-4d8f-4db5-9ed7-d3ab0630e406", "id" : "efbe514e8d806a5ef3da7658cfa73961e25befefc84f622e963b45dcac798868", "type" : "signal", "index" : ".siem-signals-default-000001", - "depth" : 3 + "depth" : 2 } -] +], +"depth": 3, +"rule": { + "id": "161fa5b8-0b96-4985-b066-0d99b2bcb904" +} ``` You will be "halted" at this point as the signal ancestry and de-duplication ensures that we do not report twice on signals and that we do not diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts index 95ec753c21fd8..9d3eb29be08dd 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/__mocks__/es_results.ts @@ -149,21 +149,23 @@ export const sampleDocWithAncestors = (): SignalSearchResponse => { delete sampleDoc._source.source; sampleDoc._source.signal = { parent: { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, ancestors: [ { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, ], + rule: { + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + }, + depth: 1, }; return { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts index ee83c826371bc..967dc5331e46b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.test.ts @@ -48,19 +48,25 @@ describe('buildBulkBody', () => { }, signal: { parent: { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: sampleIdGuid, type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, + parents: [ + { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 0, + }, + ], ancestors: [ { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: sampleIdGuid, type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, ], original_time: '2020-04-20T21:27:45+0000', @@ -102,6 +108,7 @@ describe('buildBulkBody', () => { updated_at: fakeSignalSourceHit.signal.rule?.updated_at, exceptions_list: getListArrayMock(), }, + depth: 1, }, }; expect(fakeSignalSourceHit).toEqual(expected); @@ -151,19 +158,25 @@ describe('buildBulkBody', () => { module: 'system', }, parent: { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: sampleIdGuid, type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, + parents: [ + { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 0, + }, + ], ancestors: [ { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: sampleIdGuid, type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, ], original_time: '2020-04-20T21:27:45+0000', @@ -205,6 +218,7 @@ describe('buildBulkBody', () => { threat: [], exceptions_list: getListArrayMock(), }, + depth: 1, }, }; expect(fakeSignalSourceHit).toEqual(expected); @@ -252,19 +266,25 @@ describe('buildBulkBody', () => { module: 'system', }, parent: { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: sampleIdGuid, type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, + parents: [ + { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 0, + }, + ], ancestors: [ { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: sampleIdGuid, type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, ], original_time: '2020-04-20T21:27:45+0000', @@ -306,6 +326,7 @@ describe('buildBulkBody', () => { throttle: 'no_actions', exceptions_list: getListArrayMock(), }, + depth: 1, }, }; expect(fakeSignalSourceHit).toEqual(expected); @@ -346,19 +367,25 @@ describe('buildBulkBody', () => { kind: 'event', }, parent: { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: sampleIdGuid, type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, + parents: [ + { + id: sampleIdGuid, + type: 'event', + index: 'myFakeSignalIndex', + depth: 0, + }, + ], ancestors: [ { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: sampleIdGuid, type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, ], original_time: '2020-04-20T21:27:45+0000', @@ -400,6 +427,7 @@ describe('buildBulkBody', () => { throttle: 'no_actions', exceptions_list: getListArrayMock(), }, + depth: 1, }, }; expect(fakeSignalSourceHit).toEqual(expected); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts index 218750ac30a2a..7be97e46f91f2 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_bulk_body.ts @@ -4,9 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { SignalSourceHit, SignalHit } from './types'; +import { SignalSourceHit, SignalHit, Signal } from './types'; import { buildRule } from './build_rule'; -import { buildSignal } from './build_signal'; +import { additionalSignalFields, buildSignal } from './build_signal'; import { buildEventTypeSignal } from './build_event_type_signal'; import { RuleAlertAction } from '../../../../common/detection_engine/types'; import { RuleTypeParams } from '../types'; @@ -58,7 +58,10 @@ export const buildBulkBody = ({ tags, throttle, }); - const signal = buildSignal(doc, rule); + const signal: Signal = { + ...buildSignal([doc], rule), + ...additionalSignalFields(doc), + }; const event = buildEventTypeSignal(doc); const signalHit: SignalHit = { ...doc._source, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts index 7257e5952ff05..ba815a0b62f0d 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.test.ts @@ -4,10 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { buildRule } from './build_rule'; +import { buildRule, removeInternalTagsFromRule } from './build_rule'; import { sampleDocNoSortId, sampleRuleAlertParams, sampleRuleGuid } from './__mocks__/es_results'; import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; import { getListArrayMock } from '../../../../common/detection_engine/schemas/types/lists.mock'; +import { INTERNAL_RULE_ID_KEY, INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants'; +import { getPartialRulesSchemaMock } from '../../../../common/detection_engine/schemas/response/rules_schema.mocks'; describe('buildRule', () => { beforeEach(() => { @@ -208,4 +210,102 @@ describe('buildRule', () => { }; expect(rule).toEqual(expected); }); + + test('it builds a rule and removes internal tags', () => { + const ruleParams = sampleRuleAlertParams(); + const rule = buildRule({ + actions: [], + doc: sampleDocNoSortId(), + ruleParams, + name: 'some-name', + id: sampleRuleGuid, + enabled: false, + createdAt: '2020-01-28T15:58:34.810Z', + updatedAt: '2020-01-28T15:59:14.004Z', + createdBy: 'elastic', + updatedBy: 'elastic', + interval: 'some interval', + tags: [ + 'some fake tag 1', + 'some fake tag 2', + `${INTERNAL_RULE_ID_KEY}:rule-1`, + `${INTERNAL_IMMUTABLE_KEY}:true`, + ], + throttle: 'no_actions', + }); + const expected: Partial = { + actions: [], + author: ['Elastic'], + building_block_type: 'default', + created_by: 'elastic', + description: 'Detecting root and admin users', + enabled: false, + false_positives: [], + from: 'now-6m', + id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + immutable: false, + index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], + interval: 'some interval', + language: 'kuery', + license: 'Elastic License', + max_signals: 10000, + name: 'some-name', + output_index: '.siem-signals', + query: 'user.name: root or user.name: admin', + references: ['http://google.com'], + risk_score: 50, + risk_score_mapping: [], + rule_id: 'rule-1', + severity: 'high', + severity_mapping: [], + tags: ['some fake tag 1', 'some fake tag 2'], + threat: [], + to: 'now', + type: 'query', + note: '', + updated_by: 'elastic', + updated_at: rule.updated_at, + created_at: rule.created_at, + throttle: 'no_actions', + exceptions_list: getListArrayMock(), + version: 1, + }; + expect(rule).toEqual(expected); + }); + + test('it removes internal tags from a typical rule', () => { + const rule = getPartialRulesSchemaMock(); + rule.tags = [ + 'some fake tag 1', + 'some fake tag 2', + `${INTERNAL_RULE_ID_KEY}:rule-1`, + `${INTERNAL_IMMUTABLE_KEY}:true`, + ]; + const noInternals = removeInternalTagsFromRule(rule); + expect(noInternals).toEqual(getPartialRulesSchemaMock()); + }); + + test('it works with an empty array', () => { + const rule = getPartialRulesSchemaMock(); + rule.tags = []; + const noInternals = removeInternalTagsFromRule(rule); + const expected = getPartialRulesSchemaMock(); + expected.tags = []; + expect(noInternals).toEqual(expected); + }); + + test('it works if tags does not exist', () => { + const rule = getPartialRulesSchemaMock(); + delete rule.tags; + const noInternals = removeInternalTagsFromRule(rule); + const expected = getPartialRulesSchemaMock(); + delete expected.tags; + expect(noInternals).toEqual(expected); + }); + + test('it works if tags contains normal values and no internal values', () => { + const rule = getPartialRulesSchemaMock(); + const noInternals = removeInternalTagsFromRule(rule); + expect(noInternals).toEqual(rule); + }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts index e02a0154d63c9..aacf9b8be31b4 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_rule.ts @@ -12,6 +12,7 @@ import { buildRiskScoreFromMapping } from './mappings/build_risk_score_from_mapp import { SignalSourceHit } from './types'; import { buildSeverityFromMapping } from './mappings/build_severity_from_mapping'; import { buildRuleNameFromMapping } from './mappings/build_rule_name_from_mapping'; +import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; interface BuildRuleParams { ruleParams: RuleTypeParams; @@ -64,7 +65,7 @@ export const buildRule = ({ const meta = { ...ruleParams.meta, ...riskScoreMeta, ...severityMeta, ...ruleNameMeta }; - return pickBy((value: unknown) => value != null, { + const rule = pickBy((value: unknown) => value != null, { id, rule_id: ruleParams.ruleId ?? '(unknown rule_id)', actions, @@ -111,4 +112,17 @@ export const buildRule = ({ anomaly_threshold: ruleParams.anomalyThreshold, threshold: ruleParams.threshold, }); + return removeInternalTagsFromRule(rule); +}; + +export const removeInternalTagsFromRule = (rule: Partial): Partial => { + if (rule.tags == null) { + return rule; + } else { + const ruleWithoutInternalTags: Partial = { + ...rule, + tags: rule.tags.filter((tag) => !tag.startsWith(INTERNAL_IDENTIFIER)), + }; + return ruleWithoutInternalTags; + } }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts index 6aebf8815659a..d684807a09126 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.test.ts @@ -5,14 +5,8 @@ */ import { sampleDocNoSortId } from './__mocks__/es_results'; -import { - buildSignal, - buildAncestor, - buildAncestorsSignal, - removeInternalTagsFromRule, -} from './build_signal'; +import { buildSignal, buildParent, buildAncestors, additionalSignalFields } from './build_signal'; import { Signal, Ancestor } from './types'; -import { INTERNAL_RULE_ID_KEY, INTERNAL_IMMUTABLE_KEY } from '../../../../common/constants'; import { getPartialRulesSchemaMock } from '../../../../common/detection_engine/schemas/response/rules_schema.mocks'; describe('buildSignal', () => { @@ -24,22 +18,31 @@ describe('buildSignal', () => { const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); delete doc._source.event; const rule = getPartialRulesSchemaMock(); - const signal = buildSignal(doc, rule); + const signal = { + ...buildSignal([doc], rule), + ...additionalSignalFields(doc), + }; const expected: Signal = { parent: { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, + parents: [ + { + id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', + type: 'event', + index: 'myFakeSignalIndex', + depth: 0, + }, + ], ancestors: [ { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, ], original_time: '2020-04-20T21:27:45+0000', @@ -71,6 +74,7 @@ describe('buildSignal', () => { updated_at: signal.rule.updated_at, created_at: signal.rule.created_at, }, + depth: 1, }; expect(signal).toEqual(expected); }); @@ -84,94 +88,31 @@ describe('buildSignal', () => { module: 'system', }; const rule = getPartialRulesSchemaMock(); - const signal = buildSignal(doc, rule); + const signal = { + ...buildSignal([doc], rule), + ...additionalSignalFields(doc), + }; const expected: Signal = { parent: { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, - ancestors: [ + parents: [ { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, ], - original_time: '2020-04-20T21:27:45+0000', - original_event: { - action: 'socket_opened', - dataset: 'socket', - kind: 'event', - module: 'system', - }, - status: 'open', - rule: { - created_by: 'elastic', - description: 'Detecting root and admin users', - enabled: true, - false_positives: [], - from: 'now-6m', - id: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - immutable: false, - index: ['auditbeat-*', 'filebeat-*', 'packetbeat-*', 'winlogbeat-*'], - interval: '5m', - risk_score: 50, - rule_id: 'rule-1', - language: 'kuery', - max_signals: 100, - name: 'Detect Root/Admin Users', - output_index: '.siem-signals', - query: 'user.name: root or user.name: admin', - references: ['http://www.example.com', 'https://ww.example.com'], - severity: 'high', - updated_by: 'elastic', - tags: ['some fake tag 1', 'some fake tag 2'], - to: 'now', - type: 'query', - note: '', - updated_at: signal.rule.updated_at, - created_at: signal.rule.created_at, - }, - }; - expect(signal).toEqual(expected); - }); - - test('it builds a signal as expected with original_event if is present and without internal tags in them', () => { - const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); - doc._source.event = { - action: 'socket_opened', - dataset: 'socket', - kind: 'event', - module: 'system', - }; - const rule = getPartialRulesSchemaMock(); - rule.tags = [ - 'some fake tag 1', - 'some fake tag 2', - `${INTERNAL_RULE_ID_KEY}:rule-1`, - `${INTERNAL_IMMUTABLE_KEY}:true`, - ]; - const signal = buildSignal(doc, rule); - const expected: Signal = { - parent: { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'event', - index: 'myFakeSignalIndex', - depth: 1, - }, ancestors: [ { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, ], original_time: '2020-04-20T21:27:45+0000', @@ -209,6 +150,7 @@ describe('buildSignal', () => { updated_at: signal.rule.updated_at, created_at: signal.rule.created_at, }, + depth: 1, }; expect(signal).toEqual(expected); }); @@ -221,14 +163,12 @@ describe('buildSignal', () => { kind: 'event', module: 'system', }; - const rule = getPartialRulesSchemaMock(); - const signal = buildAncestor(doc, rule); + const signal = buildParent(doc); const expected: Ancestor = { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }; expect(signal).toEqual(expected); }); @@ -242,76 +182,34 @@ describe('buildSignal', () => { module: 'system', }; doc._source.signal = { - parent: { - rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b', - id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', - type: 'event', - index: 'myFakeSignalIndex', - depth: 1, - }, - ancestors: [ + parents: [ { - rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b', id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, ], - }; - const rule = getPartialRulesSchemaMock(); - const signal = buildAncestor(doc, rule); - const expected: Ancestor = { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'signal', - index: 'myFakeSignalIndex', - depth: 2, - }; - expect(signal).toEqual(expected); - }); - - test('it builds a ancestor correctly if the parent does exist without internal tags in them', () => { - const doc = sampleDocNoSortId('d5e8eb51-a6a0-456d-8a15-4b79bfec3d71'); - doc._source.event = { - action: 'socket_opened', - dataset: 'socket', - kind: 'event', - module: 'system', - }; - doc._source.signal = { - parent: { - rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b', - id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', - type: 'event', - index: 'myFakeSignalIndex', - depth: 1, - }, ancestors: [ { - rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b', id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, ], + depth: 1, + rule: { + id: '98c0bf9e-4d38-46f4-9a6a-8a820426256b', + }, }; - const rule = getPartialRulesSchemaMock(); - rule.tags = [ - 'some fake tag 1', - 'some fake tag 2', - `${INTERNAL_RULE_ID_KEY}:rule-1`, - `${INTERNAL_IMMUTABLE_KEY}:true`, - ]; - - const signal = buildAncestor(doc, rule); + const signal = buildParent(doc); const expected: Ancestor = { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b', id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', type: 'signal', index: 'myFakeSignalIndex', - depth: 2, + depth: 1, }; expect(signal).toEqual(expected); }); @@ -324,15 +222,13 @@ describe('buildSignal', () => { kind: 'event', module: 'system', }; - const rule = getPartialRulesSchemaMock(); - const signal = buildAncestorsSignal(doc, rule); + const signal = buildAncestors(doc); const expected: Ancestor[] = [ { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, ]; expect(signal).toEqual(expected); @@ -347,77 +243,43 @@ describe('buildSignal', () => { module: 'system', }; doc._source.signal = { - parent: { - rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b', - id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', - type: 'event', - index: 'myFakeSignalIndex', - depth: 1, - }, + parents: [ + { + id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', + type: 'event', + index: 'myFakeSignalIndex', + depth: 0, + }, + ], ancestors: [ { - rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b', id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, ], + rule: { + id: '98c0bf9e-4d38-46f4-9a6a-8a820426256b', + }, + depth: 1, }; - const rule = getPartialRulesSchemaMock(); - const signal = buildAncestorsSignal(doc, rule); + const signal = buildAncestors(doc); const expected: Ancestor[] = [ { - rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b', id: '730ddf9e-5a00-4f85-9ddf-5878ca511a87', type: 'event', index: 'myFakeSignalIndex', - depth: 1, + depth: 0, }, { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', + rule: '98c0bf9e-4d38-46f4-9a6a-8a820426256b', id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', type: 'signal', index: 'myFakeSignalIndex', - depth: 2, + depth: 1, }, ]; expect(signal).toEqual(expected); }); - - test('it removes internal tags from a typical rule', () => { - const rule = getPartialRulesSchemaMock(); - rule.tags = [ - 'some fake tag 1', - 'some fake tag 2', - `${INTERNAL_RULE_ID_KEY}:rule-1`, - `${INTERNAL_IMMUTABLE_KEY}:true`, - ]; - const noInternals = removeInternalTagsFromRule(rule); - expect(noInternals).toEqual(getPartialRulesSchemaMock()); - }); - - test('it works with an empty array', () => { - const rule = getPartialRulesSchemaMock(); - rule.tags = []; - const noInternals = removeInternalTagsFromRule(rule); - const expected = getPartialRulesSchemaMock(); - expected.tags = []; - expect(noInternals).toEqual(expected); - }); - - test('it works if tags does not exist', () => { - const rule = getPartialRulesSchemaMock(); - delete rule.tags; - const noInternals = removeInternalTagsFromRule(rule); - const expected = getPartialRulesSchemaMock(); - delete expected.tags; - expect(noInternals).toEqual(expected); - }); - - test('it works if tags contains normal values and no internal values', () => { - const rule = getPartialRulesSchemaMock(); - const noInternals = removeInternalTagsFromRule(rule); - expect(noInternals).toEqual(rule); - }); }); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts index e7098c015c165..78818779dd661 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/build_signal.ts @@ -5,35 +5,41 @@ */ import { RulesSchema } from '../../../../common/detection_engine/schemas/response/rules_schema'; -import { INTERNAL_IDENTIFIER } from '../../../../common/constants'; import { SignalSourceHit, Signal, Ancestor } from './types'; -export const buildAncestor = (doc: SignalSourceHit, rule: Partial): Ancestor => { - const existingSignal = doc._source.signal?.parent; - if (existingSignal != null) { +/** + * Takes a parent signal or event document and extracts the information needed for the corresponding entry in the child + * signal's `signal.parents` array. + * @param doc The parent signal or event + */ +export const buildParent = (doc: SignalSourceHit): Ancestor => { + if (doc._source.signal != null) { return { - rule: rule.id != null ? rule.id : '', + rule: doc._source.signal.rule.id, id: doc._id, type: 'signal', index: doc._index, - depth: existingSignal.depth + 1, + // We first look for signal.depth and use that if it exists. If it doesn't exist, this should be a pre-7.10 signal + // and should have signal.parent.depth instead. signal.parent.depth in this case is treated as equivalent to signal.depth. + depth: doc._source.signal.depth ?? doc._source.signal.parent?.depth ?? 1, }; } else { return { - rule: rule.id != null ? rule.id : '', id: doc._id, type: 'event', index: doc._index, - depth: 1, + depth: 0, }; } }; -export const buildAncestorsSignal = ( - doc: SignalSourceHit, - rule: Partial -): Signal['ancestors'] => { - const newAncestor = buildAncestor(doc, rule); +/** + * Takes a parent signal or event document with N ancestors and adds the parent document to the ancestry array, + * creating an array of N+1 ancestors. + * @param doc The parent signal/event for which to extend the ancestry. + */ +export const buildAncestors = (doc: SignalSourceHit): Ancestor[] => { + const newAncestor = buildParent(doc); const existingAncestors = doc._source.signal?.ancestors; if (existingAncestors != null) { return [...existingAncestors, newAncestor]; @@ -42,35 +48,33 @@ export const buildAncestorsSignal = ( } }; -export const buildSignal = (doc: SignalSourceHit, rule: Partial): Signal => { - const ruleWithoutInternalTags = removeInternalTagsFromRule(rule); - const parent = buildAncestor(doc, rule); - const ancestors = buildAncestorsSignal(doc, rule); - let signal: Signal = { - parent, +/** + * Builds the `signal.*` fields that are common across all signals. + * @param docs The parent signals/events of the new signal to be built. + * @param rule The rule that is generating the new signal. + */ +export const buildSignal = (docs: SignalSourceHit[], rule: Partial): Signal => { + const parents = docs.map(buildParent); + const depth = parents.reduce((acc, parent) => Math.max(parent.depth, acc), 0) + 1; + const ancestors = docs.reduce((acc: Ancestor[], doc) => acc.concat(buildAncestors(doc)), []); + return { + parents, ancestors, - original_time: doc._source['@timestamp'], status: 'open', - rule: ruleWithoutInternalTags, + rule, + depth, }; - if (doc._source.event != null) { - signal = { ...signal, original_event: doc._source.event }; - } - if (doc._source.threshold_count != null) { - signal = { ...signal, threshold_count: doc._source.threshold_count }; - delete doc._source.threshold_count; - } - return signal; }; -export const removeInternalTagsFromRule = (rule: Partial): Partial => { - if (rule.tags == null) { - return rule; - } else { - const ruleWithoutInternalTags: Partial = { - ...rule, - tags: rule.tags.filter((tag) => !tag.startsWith(INTERNAL_IDENTIFIER)), - }; - return ruleWithoutInternalTags; - } +/** + * Creates signal fields that are only available in the special case where a signal has only 1 parent signal/event. + * @param doc The parent signal/event of the new signal to be built. + */ +export const additionalSignalFields = (doc: SignalSourceHit) => { + return { + parent: buildParent(doc), + original_time: doc._source['@timestamp'], + original_event: doc._source.event ?? undefined, + threshold_count: doc._source.threshold_count ?? undefined, + }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts index da17d4a1f123a..7ee157beec789 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/signal_rule_alert_type.ts @@ -120,7 +120,6 @@ export const signalRulesAlertType = ({ enabled, schedule: { interval }, throttle, - params: ruleParams, } = savedObject.attributes; const updatedAt = savedObject.updated_at ?? ''; const refresh = actions.length ? 'wait_for' : false; @@ -343,7 +342,7 @@ export const signalRulesAlertType = ({ if (result.success) { if (actions.length) { const notificationRuleParams: NotificationRuleTypeParams = { - ...ruleParams, + ...params, name, id: savedObject.id, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts index 8b9fb0574efe9..41c825ea4d978 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.test.ts @@ -291,37 +291,7 @@ describe('singleBulkCreate', () => { test('filter duplicate rules will return nothing filtered when the two rule ids do not match with each other', () => { const filtered = filterDuplicateRules('some id', sampleDocWithAncestors()); - expect(filtered).toEqual([ - { - _index: 'myFakeSignalIndex', - _type: 'doc', - _score: 100, - _version: 1, - _id: 'e1e08ddc-5e37-49ff-a258-5393aa44435a', - _source: { - someKey: 'someValue', - '@timestamp': '2020-04-20T21:27:45+0000', - signal: { - parent: { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'event', - index: 'myFakeSignalIndex', - depth: 1, - }, - ancestors: [ - { - rule: '04128c15-0d1b-4716-a4c5-46997ac7f3bd', - id: 'd5e8eb51-a6a0-456d-8a15-4b79bfec3d71', - type: 'event', - index: 'myFakeSignalIndex', - depth: 1, - }, - ], - }, - }, - }, - ]); + expect(filtered).toEqual(sampleDocWithAncestors().hits.hits); }); test('filters duplicate rules will return empty array when the two rule ids match each other', () => { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts index 74709f31563ee..be71c67615a4c 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/single_bulk_create.ts @@ -51,7 +51,10 @@ export const filterDuplicateRules = ( if (doc._source.signal == null) { return true; } else { - return !doc._source.signal.ancestors.some((ancestor) => ancestor.rule === ruleId); + return !( + doc._source.signal.ancestors.some((ancestor) => ancestor.rule === ruleId) || + doc._source.signal.rule.id === ruleId + ); } }); }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts index aecdbe10695d2..700a8fb5022d7 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/types.ts @@ -44,8 +44,16 @@ export interface SignalSource { [key: string]: SearchTypes; '@timestamp': string; signal?: { - parent: Ancestor; + // parent is deprecated: new signals should populate parents instead + // both are optional until all signals with parent are gone and we can safely remove it + parent?: Ancestor; + parents?: Ancestor[]; ancestors: Ancestor[]; + rule: { + id: string; + }; + // signal.depth doesn't exist on pre-7.10 signals + depth?: number; }; } @@ -113,7 +121,7 @@ export type SignalRuleAlertTypeDefinition = Omit & { }; export interface Ancestor { - rule: string; + rule?: string; id: string; type: string; index: string; @@ -122,12 +130,15 @@ export interface Ancestor { export interface Signal { rule: Partial; - parent: Ancestor; + // DEPRECATED: use parents instead of parent + parent?: Ancestor; + parents: Ancestor[]; ancestors: Ancestor[]; - original_time: string; + original_time?: string; original_event?: SearchTypes; status: Status; threshold_count?: SearchTypes; + depth: number; } export interface SignalHit { diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index 24cf1f8746d89..d203c6dcc48c4 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -171,7 +171,7 @@ export class Plugin implements IPlugin = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 14, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { total: -1, max_score: 0, hits: [] }, + aggregations: { + group_by_users: { + doc_count_error_upper_bound: -1, + sum_other_doc_count: 408, + buckets: [ + { + key: 'SYSTEM', + doc_count: 281, + failures: { + meta: {}, + doc_count: 0, + lastFailure: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + successes: { + meta: {}, + doc_count: 4, + lastSuccess: { + hits: { + total: 4, + max_score: 0, + hits: [ + { + _index: 'winlogbeat-8.0.0-2020.09.02-000001', + _id: 'zqY7WXQBA6bGZw2uLeKI', + _score: null, + _source: { + process: { + name: 'services.exe', + pid: 564, + executable: 'C:\\Windows\\System32\\services.exe', + }, + agent: { + build_date: '2020-07-16 09:16:27 +0000 UTC ', + name: 'siem-windows', + commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', + id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', + type: 'winlogbeat', + ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', + version: '8.0.0', + user: { name: 'inside_winlogbeat_user' }, + }, + winlog: { + computer_name: 'siem-windows', + process: { pid: 576, thread: { id: 880 } }, + keywords: ['Audit Success'], + logon: { id: '0x3e7', type: 'Service' }, + channel: 'Security', + event_data: { + LogonGuid: '{00000000-0000-0000-0000-000000000000}', + TargetOutboundDomainName: '-', + VirtualAccount: '%%1843', + LogonType: '5', + IpPort: '-', + TransmittedServices: '-', + SubjectLogonId: '0x3e7', + LmPackageName: '-', + TargetOutboundUserName: '-', + KeyLength: '0', + TargetLogonId: '0x3e7', + RestrictedAdminMode: '-', + SubjectUserName: 'SIEM-WINDOWS$', + TargetLinkedLogonId: '0x0', + ElevatedToken: '%%1842', + SubjectDomainName: 'WORKGROUP', + IpAddress: '-', + ImpersonationLevel: '%%1833', + TargetUserName: 'SYSTEM', + LogonProcessName: 'Advapi ', + TargetDomainName: 'NT AUTHORITY', + SubjectUserSid: 'S-1-5-18', + TargetUserSid: 'S-1-5-18', + AuthenticationPackageName: 'Negotiate', + }, + opcode: 'Info', + version: 2, + record_id: 57818, + task: 'Logon', + event_id: 4624, + provider_guid: '{54849625-5478-4994-a5ba-3e3b0328c30d}', + activity_id: '{d2485217-6bac-0000-8fbb-3f7e2571d601}', + api: 'wineventlog', + provider_name: 'Microsoft-Windows-Security-Auditing', + }, + log: { level: 'information' }, + source: { domain: '-' }, + message: + 'An account was successfully logged on.\n\nSubject:\n\tSecurity ID:\t\tS-1-5-18\n\tAccount Name:\t\tSIEM-WINDOWS$\n\tAccount Domain:\t\tWORKGROUP\n\tLogon ID:\t\t0x3E7\n\nLogon Information:\n\tLogon Type:\t\t5\n\tRestricted Admin Mode:\t-\n\tVirtual Account:\t\tNo\n\tElevated Token:\t\tYes\n\nImpersonation Level:\t\tImpersonation\n\nNew Logon:\n\tSecurity ID:\t\tS-1-5-18\n\tAccount Name:\t\tSYSTEM\n\tAccount Domain:\t\tNT AUTHORITY\n\tLogon ID:\t\t0x3E7\n\tLinked Logon ID:\t\t0x0\n\tNetwork Account Name:\t-\n\tNetwork Account Domain:\t-\n\tLogon GUID:\t\t{00000000-0000-0000-0000-000000000000}\n\nProcess Information:\n\tProcess ID:\t\t0x234\n\tProcess Name:\t\tC:\\Windows\\System32\\services.exe\n\nNetwork Information:\n\tWorkstation Name:\t-\n\tSource Network Address:\t-\n\tSource Port:\t\t-\n\nDetailed Authentication Information:\n\tLogon Process:\t\tAdvapi \n\tAuthentication Package:\tNegotiate\n\tTransited Services:\t-\n\tPackage Name (NTLM only):\t-\n\tKey Length:\t\t0\n\nThis event is generated when a logon session is created. It is generated on the computer that was accessed.\n\nThe subject fields indicate the account on the local system which requested the logon. This is most commonly a service such as the Server service, or a local process such as Winlogon.exe or Services.exe.\n\nThe logon type field indicates the kind of logon that occurred. The most common types are 2 (interactive) and 3 (network).\n\nThe New Logon fields indicate the account for whom the new logon was created, i.e. the account that was logged on.\n\nThe network fields indicate where a remote logon request originated. Workstation name is not always available and may be left blank in some cases.\n\nThe impersonation level field indicates the extent to which a process in the logon session can impersonate.\n\nThe authentication information fields provide detailed information about this specific logon request.\n\t- Logon GUID is a unique identifier that can be used to correlate this event with a KDC event.\n\t- Transited services indicate which intermediate services have participated in this logon request.\n\t- Package name indicates which sub-protocol was used among the NTLM protocols.\n\t- Key length indicates the length of the generated session key. This will be 0 if no session key was requested.', + cloud: { + availability_zone: 'us-central1-c', + instance: { name: 'siem-windows', id: '9156726559029788564' }, + provider: 'gcp', + machine: { type: 'g1-small' }, + project: { id: 'elastic-siem' }, + }, + '@timestamp': '2020-09-04T13:08:02.532Z', + related: { user: ['SYSTEM', 'SIEM-WINDOWS$'] }, + ecs: { version: '1.5.0' }, + host: { + hostname: 'siem-windows', + os: { + build: '17763.1397', + kernel: '10.0.17763.1397 (WinBuild.160101.0800)', + name: 'Windows Server 2019 Datacenter', + family: 'windows', + version: '10.0', + platform: 'windows', + }, + ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], + name: 'siem-windows', + id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', + mac: ['42:01:0a:c8:00:0f'], + architecture: 'x86_64', + }, + event: { + code: 4624, + provider: 'Microsoft-Windows-Security-Auditing', + created: '2020-09-04T13:08:03.638Z', + kind: 'event', + module: 'security', + action: 'logged-in', + category: 'authentication', + type: 'start', + outcome: 'success', + }, + user: { domain: 'NT AUTHORITY', name: 'SYSTEM', id: 'S-1-5-18' }, + }, + sort: [1599224882532], + }, + ], + }, + }, + }, + }, + { + key: 'tsg', + doc_count: 1, + failures: { + doc_count: 0, + lastFailure: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + successes: { + doc_count: 1, + lastSuccess: { + hits: { + total: 1, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: '9_sfWXQBc39KFIJbIsDh', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + type: 'filebeat', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 20764 }, + log: { file: { path: '/var/log/auth.log' }, offset: 552463 }, + source: { + geo: { + continent_name: 'Europe', + region_iso_code: 'DE-BE', + city_name: 'Berlin', + country_iso_code: 'DE', + region_name: 'Land Berlin', + location: { lon: 13.3512, lat: 52.5727 }, + }, + as: { number: 6805, organization: { name: 'Telefonica Germany' } }, + port: 57457, + ip: '77.183.42.188', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T11:49:21.000Z', + system: { + auth: { + ssh: { + method: 'publickey', + signature: 'RSA SHA256:vv64JNLzKZWYA9vonnGWuW7zxWhyZrL/BFxyIGbISx8', + event: 'Accepted', + }, + }, + }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_success', + category: 'authentication', + dataset: 'system.auth', + outcome: 'success', + }, + user: { name: 'tsg' }, + }, + sort: [1599220161000], + }, + ], + }, + }, + }, + }, + { + key: 'admin', + doc_count: 23, + failures: { + doc_count: 23, + lastFailure: { + hits: { + total: 23, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: 'ZfxZWXQBc39KFIJbLN5U', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + type: 'filebeat', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 22913 }, + log: { file: { path: '/var/log/auth.log' }, offset: 562910 }, + source: { + geo: { + continent_name: 'Asia', + region_iso_code: 'KR-28', + city_name: 'Incheon', + country_iso_code: 'KR', + region_name: 'Incheon', + location: { lon: 126.7288, lat: 37.4562 }, + }, + as: { number: 4766, organization: { name: 'Korea Telecom' } }, + ip: '59.15.3.197', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T13:40:46.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_failure', + category: 'authentication', + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'admin' }, + }, + sort: [1599226846000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'user', + doc_count: 21, + failures: { + doc_count: 21, + lastFailure: { + hits: { + total: 21, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'M_xLWXQBc39KFIJbY7Cb', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 20671 }, + log: { file: { path: '/var/log/auth.log' }, offset: 1028103 }, + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-NY', + city_name: 'New York', + country_iso_code: 'US', + region_name: 'New York', + location: { lon: -74, lat: 40.7157 }, + }, + ip: '64.227.88.245', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T13:25:43.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['64.227.88.245'], user: ['user'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T13:25:47.034172Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'user' }, + }, + sort: [1599225943000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'ubuntu', + doc_count: 18, + failures: { + doc_count: 18, + lastFailure: { + hits: { + total: 18, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'nPxKWXQBc39KFIJb7q4w', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + type: 'filebeat', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 20665 }, + log: { file: { path: '/var/log/auth.log' }, offset: 1027372 }, + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-NY', + city_name: 'New York', + country_iso_code: 'US', + region_name: 'New York', + location: { lon: -74, lat: 40.7157 }, + }, + ip: '64.227.88.245', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T13:25:07.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['64.227.88.245'], user: ['ubuntu'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T13:25:16.974606Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'ubuntu' }, + }, + sort: [1599225907000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'odoo', + doc_count: 17, + failures: { + doc_count: 17, + lastFailure: { + hits: { + total: 17, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: 'mPsfWXQBc39KFIJbI8HI', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + type: 'filebeat', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 21506 }, + log: { file: { path: '/var/log/auth.log' }, offset: 556761 }, + source: { + geo: { + continent_name: 'Asia', + region_iso_code: 'IN-DL', + city_name: 'New Delhi', + country_iso_code: 'IN', + region_name: 'National Capital Territory of Delhi', + location: { lon: 77.2245, lat: 28.6358 }, + }, + as: { number: 10029, organization: { name: 'SHYAM SPECTRA PVT LTD' } }, + ip: '180.151.228.166', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T12:26:36.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_failure', + category: 'authentication', + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'odoo' }, + }, + sort: [1599222396000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'pi', + doc_count: 17, + failures: { + doc_count: 17, + lastFailure: { + hits: { + total: 17, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'aaToWHQBA6bGZw2uR-St', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 20475 }, + log: { file: { path: '/var/log/auth.log' }, offset: 1019218 }, + source: { + geo: { + continent_name: 'Europe', + region_iso_code: 'SE-AB', + city_name: 'Stockholm', + country_iso_code: 'SE', + region_name: 'Stockholm', + location: { lon: 17.7833, lat: 59.25 }, + }, + as: { number: 8473, organization: { name: 'Bahnhof AB' } }, + ip: '178.174.148.58', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T11:37:22.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['178.174.148.58'], user: ['pi'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T11:37:31.797423Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'pi' }, + }, + sort: [1599219442000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'demo', + doc_count: 14, + failures: { + doc_count: 14, + lastFailure: { + hits: { + total: 14, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'VaP_V3QBA6bGZw2upUbg', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 19849 }, + log: { file: { path: '/var/log/auth.log' }, offset: 981036 }, + source: { + geo: { + continent_name: 'Europe', + country_iso_code: 'HR', + location: { lon: 15.5, lat: 45.1667 }, + }, + as: { + number: 42864, + organization: { name: 'Giganet Internet Szolgaltato Kft' }, + }, + ip: '45.95.168.157', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T07:23:22.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['45.95.168.157'], user: ['demo'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T07:23:26.046346Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'demo' }, + }, + sort: [1599204202000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'git', + doc_count: 13, + failures: { + doc_count: 13, + lastFailure: { + hits: { + total: 13, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: 'PqYfWXQBA6bGZw2uIhVU', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + type: 'filebeat', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 20396 }, + log: { file: { path: '/var/log/auth.log' }, offset: 550795 }, + source: { + geo: { + continent_name: 'Asia', + region_iso_code: 'CN-BJ', + city_name: 'Beijing', + country_iso_code: 'CN', + region_name: 'Beijing', + location: { lon: 116.3889, lat: 39.9288 }, + }, + as: { + number: 45090, + organization: { + name: 'Shenzhen Tencent Computer Systems Company Limited', + }, + }, + ip: '123.206.30.76', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T11:20:26.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_failure', + category: 'authentication', + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'git' }, + }, + sort: [1599218426000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'webadmin', + doc_count: 13, + failures: { + doc_count: 13, + lastFailure: { + hits: { + total: 13, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'iMABWHQBB-gskclyitP-', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 19870 }, + log: { file: { path: '/var/log/auth.log' }, offset: 984133 }, + source: { + geo: { + continent_name: 'Europe', + country_iso_code: 'HR', + location: { lon: 15.5, lat: 45.1667 }, + }, + as: { + number: 42864, + organization: { name: 'Giganet Internet Szolgaltato Kft' }, + }, + ip: '45.95.168.157', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T07:25:28.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['45.95.168.157'], user: ['webadmin'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T07:25:30.236651Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'webadmin' }, + }, + sort: [1599204328000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + ], + }, + user_count: { value: 188 }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedSearchStrategyResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 14, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { total: -1, max_score: 0, hits: [] }, + aggregations: { + group_by_users: { + doc_count_error_upper_bound: -1, + sum_other_doc_count: 408, + buckets: [ + { + key: 'SYSTEM', + doc_count: 281, + failures: { + meta: {}, + doc_count: 0, + lastFailure: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + successes: { + meta: {}, + doc_count: 4, + lastSuccess: { + hits: { + total: 4, + max_score: 0, + hits: [ + { + _index: 'winlogbeat-8.0.0-2020.09.02-000001', + _id: 'zqY7WXQBA6bGZw2uLeKI', + _score: null, + _source: { + process: { + name: 'services.exe', + pid: 564, + executable: 'C:\\Windows\\System32\\services.exe', + }, + agent: { + build_date: '2020-07-16 09:16:27 +0000 UTC ', + name: 'siem-windows', + commit: '4dcbde39492bdc3843034bba8db811c68cb44b97 ', + id: '05e1bff7-d7a8-416a-8554-aa10288fa07d', + type: 'winlogbeat', + ephemeral_id: '655abd6c-6c33-435d-a2eb-79b2a01e6d61', + version: '8.0.0', + user: { name: 'inside_winlogbeat_user' }, + }, + winlog: { + computer_name: 'siem-windows', + process: { pid: 576, thread: { id: 880 } }, + keywords: ['Audit Success'], + logon: { id: '0x3e7', type: 'Service' }, + channel: 'Security', + event_data: { + LogonGuid: '{00000000-0000-0000-0000-000000000000}', + TargetOutboundDomainName: '-', + VirtualAccount: '%%1843', + LogonType: '5', + IpPort: '-', + TransmittedServices: '-', + SubjectLogonId: '0x3e7', + LmPackageName: '-', + TargetOutboundUserName: '-', + KeyLength: '0', + TargetLogonId: '0x3e7', + RestrictedAdminMode: '-', + SubjectUserName: 'SIEM-WINDOWS$', + TargetLinkedLogonId: '0x0', + ElevatedToken: '%%1842', + SubjectDomainName: 'WORKGROUP', + IpAddress: '-', + ImpersonationLevel: '%%1833', + TargetUserName: 'SYSTEM', + LogonProcessName: 'Advapi ', + TargetDomainName: 'NT AUTHORITY', + SubjectUserSid: 'S-1-5-18', + TargetUserSid: 'S-1-5-18', + AuthenticationPackageName: 'Negotiate', + }, + opcode: 'Info', + version: 2, + record_id: 57818, + task: 'Logon', + event_id: 4624, + provider_guid: '{54849625-5478-4994-a5ba-3e3b0328c30d}', + activity_id: '{d2485217-6bac-0000-8fbb-3f7e2571d601}', + api: 'wineventlog', + provider_name: 'Microsoft-Windows-Security-Auditing', + }, + log: { level: 'information' }, + source: { domain: '-' }, + message: + 'An account was successfully logged on.\n\nSubject:\n\tSecurity ID:\t\tS-1-5-18\n\tAccount Name:\t\tSIEM-WINDOWS$\n\tAccount Domain:\t\tWORKGROUP\n\tLogon ID:\t\t0x3E7\n\nLogon Information:\n\tLogon Type:\t\t5\n\tRestricted Admin Mode:\t-\n\tVirtual Account:\t\tNo\n\tElevated Token:\t\tYes\n\nImpersonation Level:\t\tImpersonation\n\nNew Logon:\n\tSecurity ID:\t\tS-1-5-18\n\tAccount Name:\t\tSYSTEM\n\tAccount Domain:\t\tNT AUTHORITY\n\tLogon ID:\t\t0x3E7\n\tLinked Logon ID:\t\t0x0\n\tNetwork Account Name:\t-\n\tNetwork Account Domain:\t-\n\tLogon GUID:\t\t{00000000-0000-0000-0000-000000000000}\n\nProcess Information:\n\tProcess ID:\t\t0x234\n\tProcess Name:\t\tC:\\Windows\\System32\\services.exe\n\nNetwork Information:\n\tWorkstation Name:\t-\n\tSource Network Address:\t-\n\tSource Port:\t\t-\n\nDetailed Authentication Information:\n\tLogon Process:\t\tAdvapi \n\tAuthentication Package:\tNegotiate\n\tTransited Services:\t-\n\tPackage Name (NTLM only):\t-\n\tKey Length:\t\t0\n\nThis event is generated when a logon session is created. It is generated on the computer that was accessed.\n\nThe subject fields indicate the account on the local system which requested the logon. This is most commonly a service such as the Server service, or a local process such as Winlogon.exe or Services.exe.\n\nThe logon type field indicates the kind of logon that occurred. The most common types are 2 (interactive) and 3 (network).\n\nThe New Logon fields indicate the account for whom the new logon was created, i.e. the account that was logged on.\n\nThe network fields indicate where a remote logon request originated. Workstation name is not always available and may be left blank in some cases.\n\nThe impersonation level field indicates the extent to which a process in the logon session can impersonate.\n\nThe authentication information fields provide detailed information about this specific logon request.\n\t- Logon GUID is a unique identifier that can be used to correlate this event with a KDC event.\n\t- Transited services indicate which intermediate services have participated in this logon request.\n\t- Package name indicates which sub-protocol was used among the NTLM protocols.\n\t- Key length indicates the length of the generated session key. This will be 0 if no session key was requested.', + cloud: { + availability_zone: 'us-central1-c', + instance: { name: 'siem-windows', id: '9156726559029788564' }, + provider: 'gcp', + machine: { type: 'g1-small' }, + project: { id: 'elastic-siem' }, + }, + '@timestamp': '2020-09-04T13:08:02.532Z', + related: { user: ['SYSTEM', 'SIEM-WINDOWS$'] }, + ecs: { version: '1.5.0' }, + host: { + hostname: 'siem-windows', + os: { + build: '17763.1397', + kernel: '10.0.17763.1397 (WinBuild.160101.0800)', + name: 'Windows Server 2019 Datacenter', + family: 'windows', + version: '10.0', + platform: 'windows', + }, + ip: ['fe80::ecf5:decc:3ec3:767e', '10.200.0.15'], + name: 'siem-windows', + id: 'ce1d3c9b-a815-4643-9641-ada0f2c00609', + mac: ['42:01:0a:c8:00:0f'], + architecture: 'x86_64', + }, + event: { + code: 4624, + provider: 'Microsoft-Windows-Security-Auditing', + created: '2020-09-04T13:08:03.638Z', + kind: 'event', + module: 'security', + action: 'logged-in', + category: 'authentication', + type: 'start', + outcome: 'success', + }, + user: { domain: 'NT AUTHORITY', name: 'SYSTEM', id: 'S-1-5-18' }, + }, + sort: [1599224882532], + }, + ], + }, + }, + }, + }, + { + key: 'tsg', + doc_count: 1, + failures: { doc_count: 0, lastFailure: { hits: { total: 0, max_score: 0, hits: [] } } }, + successes: { + doc_count: 1, + lastSuccess: { + hits: { + total: 1, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: '9_sfWXQBc39KFIJbIsDh', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + type: 'filebeat', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 20764 }, + log: { file: { path: '/var/log/auth.log' }, offset: 552463 }, + source: { + geo: { + continent_name: 'Europe', + region_iso_code: 'DE-BE', + city_name: 'Berlin', + country_iso_code: 'DE', + region_name: 'Land Berlin', + location: { lon: 13.3512, lat: 52.5727 }, + }, + as: { number: 6805, organization: { name: 'Telefonica Germany' } }, + port: 57457, + ip: '77.183.42.188', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T11:49:21.000Z', + system: { + auth: { + ssh: { + method: 'publickey', + signature: 'RSA SHA256:vv64JNLzKZWYA9vonnGWuW7zxWhyZrL/BFxyIGbISx8', + event: 'Accepted', + }, + }, + }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_success', + category: 'authentication', + dataset: 'system.auth', + outcome: 'success', + }, + user: { name: 'tsg' }, + }, + sort: [1599220161000], + }, + ], + }, + }, + }, + }, + { + key: 'admin', + doc_count: 23, + failures: { + doc_count: 23, + lastFailure: { + hits: { + total: 23, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: 'ZfxZWXQBc39KFIJbLN5U', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + type: 'filebeat', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 22913 }, + log: { file: { path: '/var/log/auth.log' }, offset: 562910 }, + source: { + geo: { + continent_name: 'Asia', + region_iso_code: 'KR-28', + city_name: 'Incheon', + country_iso_code: 'KR', + region_name: 'Incheon', + location: { lon: 126.7288, lat: 37.4562 }, + }, + as: { number: 4766, organization: { name: 'Korea Telecom' } }, + ip: '59.15.3.197', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T13:40:46.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_failure', + category: 'authentication', + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'admin' }, + }, + sort: [1599226846000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'user', + doc_count: 21, + failures: { + doc_count: 21, + lastFailure: { + hits: { + total: 21, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'M_xLWXQBc39KFIJbY7Cb', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 20671 }, + log: { file: { path: '/var/log/auth.log' }, offset: 1028103 }, + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-NY', + city_name: 'New York', + country_iso_code: 'US', + region_name: 'New York', + location: { lon: -74, lat: 40.7157 }, + }, + ip: '64.227.88.245', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T13:25:43.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['64.227.88.245'], user: ['user'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T13:25:47.034172Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'user' }, + }, + sort: [1599225943000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'ubuntu', + doc_count: 18, + failures: { + doc_count: 18, + lastFailure: { + hits: { + total: 18, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'nPxKWXQBc39KFIJb7q4w', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + type: 'filebeat', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 20665 }, + log: { file: { path: '/var/log/auth.log' }, offset: 1027372 }, + source: { + geo: { + continent_name: 'North America', + region_iso_code: 'US-NY', + city_name: 'New York', + country_iso_code: 'US', + region_name: 'New York', + location: { lon: -74, lat: 40.7157 }, + }, + ip: '64.227.88.245', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T13:25:07.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['64.227.88.245'], user: ['ubuntu'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T13:25:16.974606Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'ubuntu' }, + }, + sort: [1599225907000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'odoo', + doc_count: 17, + failures: { + doc_count: 17, + lastFailure: { + hits: { + total: 17, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: 'mPsfWXQBc39KFIJbI8HI', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + type: 'filebeat', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 21506 }, + log: { file: { path: '/var/log/auth.log' }, offset: 556761 }, + source: { + geo: { + continent_name: 'Asia', + region_iso_code: 'IN-DL', + city_name: 'New Delhi', + country_iso_code: 'IN', + region_name: 'National Capital Territory of Delhi', + location: { lon: 77.2245, lat: 28.6358 }, + }, + as: { number: 10029, organization: { name: 'SHYAM SPECTRA PVT LTD' } }, + ip: '180.151.228.166', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T12:26:36.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_failure', + category: 'authentication', + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'odoo' }, + }, + sort: [1599222396000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'pi', + doc_count: 17, + failures: { + doc_count: 17, + lastFailure: { + hits: { + total: 17, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'aaToWHQBA6bGZw2uR-St', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 20475 }, + log: { file: { path: '/var/log/auth.log' }, offset: 1019218 }, + source: { + geo: { + continent_name: 'Europe', + region_iso_code: 'SE-AB', + city_name: 'Stockholm', + country_iso_code: 'SE', + region_name: 'Stockholm', + location: { lon: 17.7833, lat: 59.25 }, + }, + as: { number: 8473, organization: { name: 'Bahnhof AB' } }, + ip: '178.174.148.58', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T11:37:22.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['178.174.148.58'], user: ['pi'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T11:37:31.797423Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'pi' }, + }, + sort: [1599219442000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'demo', + doc_count: 14, + failures: { + doc_count: 14, + lastFailure: { + hits: { + total: 14, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'VaP_V3QBA6bGZw2upUbg', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 19849 }, + log: { file: { path: '/var/log/auth.log' }, offset: 981036 }, + source: { + geo: { + continent_name: 'Europe', + country_iso_code: 'HR', + location: { lon: 15.5, lat: 45.1667 }, + }, + as: { + number: 42864, + organization: { name: 'Giganet Internet Szolgaltato Kft' }, + }, + ip: '45.95.168.157', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T07:23:22.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['45.95.168.157'], user: ['demo'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T07:23:26.046346Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'demo' }, + }, + sort: [1599204202000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'git', + doc_count: 13, + failures: { + doc_count: 13, + lastFailure: { + hits: { + total: 13, + max_score: 0, + hits: [ + { + _index: '.ds-logs-system.auth-default-000001', + _id: 'PqYfWXQBA6bGZw2uIhVU', + _score: null, + _source: { + agent: { + hostname: 'siem-kibana', + name: 'siem-kibana', + id: 'aa3d9dc7-fef1-4c2f-a68d-25785d624e35', + ephemeral_id: 'e503bd85-11c7-4bc9-ae7d-70be1d919fb7', + type: 'filebeat', + version: '7.9.1', + }, + process: { name: 'sshd', pid: 20396 }, + log: { file: { path: '/var/log/auth.log' }, offset: 550795 }, + source: { + geo: { + continent_name: 'Asia', + region_iso_code: 'CN-BJ', + city_name: 'Beijing', + country_iso_code: 'CN', + region_name: 'Beijing', + location: { lon: 116.3889, lat: 39.9288 }, + }, + as: { + number: 45090, + organization: { + name: 'Shenzhen Tencent Computer Systems Company Limited', + }, + }, + ip: '123.206.30.76', + }, + cloud: { + availability_zone: 'us-east1-b', + instance: { name: 'siem-kibana', id: '5412578377715150143' }, + provider: 'gcp', + machine: { type: 'n1-standard-2' }, + project: { id: 'elastic-beats' }, + }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T11:20:26.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + data_stream: { namespace: 'default', type: 'logs', dataset: 'system.auth' }, + host: { + hostname: 'siem-kibana', + os: { + kernel: '4.9.0-8-amd64', + codename: 'stretch', + name: 'Debian GNU/Linux', + family: 'debian', + version: '9 (stretch)', + platform: 'debian', + }, + containerized: false, + ip: ['10.142.0.7', 'fe80::4001:aff:fe8e:7'], + name: 'siem-kibana', + id: 'aa7ca589f1b8220002f2fc61c64cfbf1', + mac: ['42:01:0a:8e:00:07'], + architecture: 'x86_64', + }, + event: { + timezone: '+00:00', + action: 'ssh_login', + type: 'authentication_failure', + category: 'authentication', + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'git' }, + }, + sort: [1599218426000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + { + key: 'webadmin', + doc_count: 13, + failures: { + doc_count: 13, + lastFailure: { + hits: { + total: 13, + max_score: 0, + hits: [ + { + _index: 'filebeat-8.0.0-2020.09.02-000001', + _id: 'iMABWHQBB-gskclyitP-', + _score: null, + _source: { + agent: { + name: 'bastion00.siem.estc.dev', + id: 'f9a321c1-ec27-49fa-aacf-6a50ef6d836f', + type: 'filebeat', + ephemeral_id: '734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc', + version: '8.0.0', + }, + process: { name: 'sshd', pid: 19870 }, + log: { file: { path: '/var/log/auth.log' }, offset: 984133 }, + source: { + geo: { + continent_name: 'Europe', + country_iso_code: 'HR', + location: { lon: 15.5, lat: 45.1667 }, + }, + as: { + number: 42864, + organization: { name: 'Giganet Internet Szolgaltato Kft' }, + }, + ip: '45.95.168.157', + }, + fileset: { name: 'auth' }, + input: { type: 'log' }, + '@timestamp': '2020-09-04T07:25:28.000Z', + system: { auth: { ssh: { event: 'Invalid' } } }, + ecs: { version: '1.5.0' }, + related: { ip: ['45.95.168.157'], user: ['webadmin'] }, + service: { type: 'system' }, + host: { hostname: 'bastion00', name: 'bastion00.siem.estc.dev' }, + event: { + ingested: '2020-09-04T07:25:30.236651Z', + timezone: '+00:00', + kind: 'event', + module: 'system', + action: 'ssh_login', + type: ['authentication_failure', 'info'], + category: ['authentication'], + dataset: 'system.auth', + outcome: 'failure', + }, + user: { name: 'webadmin' }, + }, + sort: [1599204328000], + }, + ], + }, + }, + }, + successes: { + doc_count: 0, + lastSuccess: { hits: { total: 0, max_score: 0, hits: [] } }, + }, + }, + ], + }, + user_count: { value: 188 }, + }, + }, + total: 21, + loaded: 21, + inspect: { + dsl: [ + '{\n "allowNoIndices": true,\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "ignoreUnavailable": true,\n "body": {\n "aggregations": {\n "host_architecture": {\n "terms": {\n "field": "host.architecture",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_id": {\n "terms": {\n "field": "host.id",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_ip": {\n "terms": {\n "field": "host.ip",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_mac": {\n "terms": {\n "field": "host.mac",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_name": {\n "terms": {\n "field": "host.name",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_family": {\n "terms": {\n "field": "host.os.family",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_name": {\n "terms": {\n "field": "host.os.name",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_platform": {\n "terms": {\n "field": "host.os.platform",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "host_os_version": {\n "terms": {\n "field": "host.os.version",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_instance_id": {\n "terms": {\n "field": "cloud.instance.id",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_machine_type": {\n "terms": {\n "field": "cloud.machine.type",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_provider": {\n "terms": {\n "field": "cloud.provider",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n },\n "cloud_region": {\n "terms": {\n "field": "cloud.region",\n "size": 10,\n "order": {\n "timestamp": "desc"\n }\n },\n "aggs": {\n "timestamp": {\n "max": {\n "field": "@timestamp"\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n {\n "term": {\n "host.name": "bastion00"\n }\n },\n {\n "range": {\n "@timestamp": {\n "format": "strict_date_optional_time",\n "gte": "2020-09-02T15:17:13.678Z",\n "lte": "2020-09-03T15:17:13.678Z"\n }\n }\n }\n ]\n }\n },\n "size": 0,\n "track_total_hits": false\n }\n}', + ], + response: [ + '{\n "isPartial": false,\n "isRunning": false,\n "rawResponse": {\n "took": 14,\n "timed_out": false,\n "_shards": {\n "total": 21,\n "successful": 21,\n "skipped": 0,\n "failed": 0\n },\n "hits": {\n "total": -1,\n "max_score": 0,\n "hits": []\n },\n "aggregations": {\n "group_by_users": {\n "doc_count_error_upper_bound": -1,\n "sum_other_doc_count": 408,\n "buckets": [\n {\n "key": "SYSTEM",\n "doc_count": 281,\n "failures": {\n "meta": {},\n "doc_count": 0,\n "lastFailure": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n },\n "successes": {\n "meta": {},\n "doc_count": 4,\n "lastSuccess": {\n "hits": {\n "total": 4,\n "max_score": 0,\n "hits": [\n {\n "_index": "winlogbeat-8.0.0-2020.09.02-000001",\n "_id": "zqY7WXQBA6bGZw2uLeKI",\n "_score": null,\n "_source": {\n "process": {\n "name": "services.exe",\n "pid": 564,\n "executable": "C:\\\\Windows\\\\System32\\\\services.exe"\n },\n "agent": {\n "build_date": "2020-07-16 09:16:27 +0000 UTC ",\n "name": "siem-windows",\n "commit": "4dcbde39492bdc3843034bba8db811c68cb44b97 ",\n "id": "05e1bff7-d7a8-416a-8554-aa10288fa07d",\n "type": "winlogbeat",\n "ephemeral_id": "655abd6c-6c33-435d-a2eb-79b2a01e6d61",\n "version": "8.0.0",\n "user": {\n "name": "inside_winlogbeat_user"\n }\n },\n "winlog": {\n "computer_name": "siem-windows",\n "process": {\n "pid": 576,\n "thread": {\n "id": 880\n }\n },\n "keywords": [\n "Audit Success"\n ],\n "logon": {\n "id": "0x3e7",\n "type": "Service"\n },\n "channel": "Security",\n "event_data": {\n "LogonGuid": "{00000000-0000-0000-0000-000000000000}",\n "TargetOutboundDomainName": "-",\n "VirtualAccount": "%%1843",\n "LogonType": "5",\n "IpPort": "-",\n "TransmittedServices": "-",\n "SubjectLogonId": "0x3e7",\n "LmPackageName": "-",\n "TargetOutboundUserName": "-",\n "KeyLength": "0",\n "TargetLogonId": "0x3e7",\n "RestrictedAdminMode": "-",\n "SubjectUserName": "SIEM-WINDOWS$",\n "TargetLinkedLogonId": "0x0",\n "ElevatedToken": "%%1842",\n "SubjectDomainName": "WORKGROUP",\n "IpAddress": "-",\n "ImpersonationLevel": "%%1833",\n "TargetUserName": "SYSTEM",\n "LogonProcessName": "Advapi ",\n "TargetDomainName": "NT AUTHORITY",\n "SubjectUserSid": "S-1-5-18",\n "TargetUserSid": "S-1-5-18",\n "AuthenticationPackageName": "Negotiate"\n },\n "opcode": "Info",\n "version": 2,\n "record_id": 57818,\n "task": "Logon",\n "event_id": 4624,\n "provider_guid": "{54849625-5478-4994-a5ba-3e3b0328c30d}",\n "activity_id": "{d2485217-6bac-0000-8fbb-3f7e2571d601}",\n "api": "wineventlog",\n "provider_name": "Microsoft-Windows-Security-Auditing"\n },\n "log": {\n "level": "information"\n },\n "source": {\n "domain": "-"\n },\n "message": "An account was successfully logged on.\\n\\nSubject:\\n\\tSecurity ID:\\t\\tS-1-5-18\\n\\tAccount Name:\\t\\tSIEM-WINDOWS$\\n\\tAccount Domain:\\t\\tWORKGROUP\\n\\tLogon ID:\\t\\t0x3E7\\n\\nLogon Information:\\n\\tLogon Type:\\t\\t5\\n\\tRestricted Admin Mode:\\t-\\n\\tVirtual Account:\\t\\tNo\\n\\tElevated Token:\\t\\tYes\\n\\nImpersonation Level:\\t\\tImpersonation\\n\\nNew Logon:\\n\\tSecurity ID:\\t\\tS-1-5-18\\n\\tAccount Name:\\t\\tSYSTEM\\n\\tAccount Domain:\\t\\tNT AUTHORITY\\n\\tLogon ID:\\t\\t0x3E7\\n\\tLinked Logon ID:\\t\\t0x0\\n\\tNetwork Account Name:\\t-\\n\\tNetwork Account Domain:\\t-\\n\\tLogon GUID:\\t\\t{00000000-0000-0000-0000-000000000000}\\n\\nProcess Information:\\n\\tProcess ID:\\t\\t0x234\\n\\tProcess Name:\\t\\tC:\\\\Windows\\\\System32\\\\services.exe\\n\\nNetwork Information:\\n\\tWorkstation Name:\\t-\\n\\tSource Network Address:\\t-\\n\\tSource Port:\\t\\t-\\n\\nDetailed Authentication Information:\\n\\tLogon Process:\\t\\tAdvapi \\n\\tAuthentication Package:\\tNegotiate\\n\\tTransited Services:\\t-\\n\\tPackage Name (NTLM only):\\t-\\n\\tKey Length:\\t\\t0\\n\\nThis event is generated when a logon session is created. It is generated on the computer that was accessed.\\n\\nThe subject fields indicate the account on the local system which requested the logon. This is most commonly a service such as the Server service, or a local process such as Winlogon.exe or Services.exe.\\n\\nThe logon type field indicates the kind of logon that occurred. The most common types are 2 (interactive) and 3 (network).\\n\\nThe New Logon fields indicate the account for whom the new logon was created, i.e. the account that was logged on.\\n\\nThe network fields indicate where a remote logon request originated. Workstation name is not always available and may be left blank in some cases.\\n\\nThe impersonation level field indicates the extent to which a process in the logon session can impersonate.\\n\\nThe authentication information fields provide detailed information about this specific logon request.\\n\\t- Logon GUID is a unique identifier that can be used to correlate this event with a KDC event.\\n\\t- Transited services indicate which intermediate services have participated in this logon request.\\n\\t- Package name indicates which sub-protocol was used among the NTLM protocols.\\n\\t- Key length indicates the length of the generated session key. This will be 0 if no session key was requested.",\n "cloud": {\n "availability_zone": "us-central1-c",\n "instance": {\n "name": "siem-windows",\n "id": "9156726559029788564"\n },\n "provider": "gcp",\n "machine": {\n "type": "g1-small"\n },\n "project": {\n "id": "elastic-siem"\n }\n },\n "@timestamp": "2020-09-04T13:08:02.532Z",\n "related": {\n "user": [\n "SYSTEM",\n "SIEM-WINDOWS$"\n ]\n },\n "ecs": {\n "version": "1.5.0"\n },\n "host": {\n "hostname": "siem-windows",\n "os": {\n "build": "17763.1397",\n "kernel": "10.0.17763.1397 (WinBuild.160101.0800)",\n "name": "Windows Server 2019 Datacenter",\n "family": "windows",\n "version": "10.0",\n "platform": "windows"\n },\n "ip": [\n "fe80::ecf5:decc:3ec3:767e",\n "10.200.0.15"\n ],\n "name": "siem-windows",\n "id": "ce1d3c9b-a815-4643-9641-ada0f2c00609",\n "mac": [\n "42:01:0a:c8:00:0f"\n ],\n "architecture": "x86_64"\n },\n "event": {\n "code": 4624,\n "provider": "Microsoft-Windows-Security-Auditing",\n "created": "2020-09-04T13:08:03.638Z",\n "kind": "event",\n "module": "security",\n "action": "logged-in",\n "category": "authentication",\n "type": "start",\n "outcome": "success"\n },\n "user": {\n "domain": "NT AUTHORITY",\n "name": "SYSTEM",\n "id": "S-1-5-18"\n }\n },\n "sort": [\n 1599224882532\n ]\n }\n ]\n }\n }\n }\n },\n {\n "key": "tsg",\n "doc_count": 1,\n "failures": {\n "doc_count": 0,\n "lastFailure": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n },\n "successes": {\n "doc_count": 1,\n "lastSuccess": {\n "hits": {\n "total": 1,\n "max_score": 0,\n "hits": [\n {\n "_index": ".ds-logs-system.auth-default-000001",\n "_id": "9_sfWXQBc39KFIJbIsDh",\n "_score": null,\n "_source": {\n "agent": {\n "hostname": "siem-kibana",\n "name": "siem-kibana",\n "id": "aa3d9dc7-fef1-4c2f-a68d-25785d624e35",\n "ephemeral_id": "e503bd85-11c7-4bc9-ae7d-70be1d919fb7",\n "type": "filebeat",\n "version": "7.9.1"\n },\n "process": {\n "name": "sshd",\n "pid": 20764\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 552463\n },\n "source": {\n "geo": {\n "continent_name": "Europe",\n "region_iso_code": "DE-BE",\n "city_name": "Berlin",\n "country_iso_code": "DE",\n "region_name": "Land Berlin",\n "location": {\n "lon": 13.3512,\n "lat": 52.5727\n }\n },\n "as": {\n "number": 6805,\n "organization": {\n "name": "Telefonica Germany"\n }\n },\n "port": 57457,\n "ip": "77.183.42.188"\n },\n "cloud": {\n "availability_zone": "us-east1-b",\n "instance": {\n "name": "siem-kibana",\n "id": "5412578377715150143"\n },\n "provider": "gcp",\n "machine": {\n "type": "n1-standard-2"\n },\n "project": {\n "id": "elastic-beats"\n }\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T11:49:21.000Z",\n "system": {\n "auth": {\n "ssh": {\n "method": "publickey",\n "signature": "RSA SHA256:vv64JNLzKZWYA9vonnGWuW7zxWhyZrL/BFxyIGbISx8",\n "event": "Accepted"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "data_stream": {\n "namespace": "default",\n "type": "logs",\n "dataset": "system.auth"\n },\n "host": {\n "hostname": "siem-kibana",\n "os": {\n "kernel": "4.9.0-8-amd64",\n "codename": "stretch",\n "name": "Debian GNU/Linux",\n "family": "debian",\n "version": "9 (stretch)",\n "platform": "debian"\n },\n "containerized": false,\n "ip": [\n "10.142.0.7",\n "fe80::4001:aff:fe8e:7"\n ],\n "name": "siem-kibana",\n "id": "aa7ca589f1b8220002f2fc61c64cfbf1",\n "mac": [\n "42:01:0a:8e:00:07"\n ],\n "architecture": "x86_64"\n },\n "event": {\n "timezone": "+00:00",\n "action": "ssh_login",\n "type": "authentication_success",\n "category": "authentication",\n "dataset": "system.auth",\n "outcome": "success"\n },\n "user": {\n "name": "tsg"\n }\n },\n "sort": [\n 1599220161000\n ]\n }\n ]\n }\n }\n }\n },\n {\n "key": "admin",\n "doc_count": 23,\n "failures": {\n "doc_count": 23,\n "lastFailure": {\n "hits": {\n "total": 23,\n "max_score": 0,\n "hits": [\n {\n "_index": ".ds-logs-system.auth-default-000001",\n "_id": "ZfxZWXQBc39KFIJbLN5U",\n "_score": null,\n "_source": {\n "agent": {\n "hostname": "siem-kibana",\n "name": "siem-kibana",\n "id": "aa3d9dc7-fef1-4c2f-a68d-25785d624e35",\n "ephemeral_id": "e503bd85-11c7-4bc9-ae7d-70be1d919fb7",\n "type": "filebeat",\n "version": "7.9.1"\n },\n "process": {\n "name": "sshd",\n "pid": 22913\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 562910\n },\n "source": {\n "geo": {\n "continent_name": "Asia",\n "region_iso_code": "KR-28",\n "city_name": "Incheon",\n "country_iso_code": "KR",\n "region_name": "Incheon",\n "location": {\n "lon": 126.7288,\n "lat": 37.4562\n }\n },\n "as": {\n "number": 4766,\n "organization": {\n "name": "Korea Telecom"\n }\n },\n "ip": "59.15.3.197"\n },\n "cloud": {\n "availability_zone": "us-east1-b",\n "instance": {\n "name": "siem-kibana",\n "id": "5412578377715150143"\n },\n "provider": "gcp",\n "machine": {\n "type": "n1-standard-2"\n },\n "project": {\n "id": "elastic-beats"\n }\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T13:40:46.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "data_stream": {\n "namespace": "default",\n "type": "logs",\n "dataset": "system.auth"\n },\n "host": {\n "hostname": "siem-kibana",\n "os": {\n "kernel": "4.9.0-8-amd64",\n "codename": "stretch",\n "name": "Debian GNU/Linux",\n "family": "debian",\n "version": "9 (stretch)",\n "platform": "debian"\n },\n "containerized": false,\n "ip": [\n "10.142.0.7",\n "fe80::4001:aff:fe8e:7"\n ],\n "name": "siem-kibana",\n "id": "aa7ca589f1b8220002f2fc61c64cfbf1",\n "mac": [\n "42:01:0a:8e:00:07"\n ],\n "architecture": "x86_64"\n },\n "event": {\n "timezone": "+00:00",\n "action": "ssh_login",\n "type": "authentication_failure",\n "category": "authentication",\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "admin"\n }\n },\n "sort": [\n 1599226846000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n },\n {\n "key": "user",\n "doc_count": 21,\n "failures": {\n "doc_count": 21,\n "lastFailure": {\n "hits": {\n "total": 21,\n "max_score": 0,\n "hits": [\n {\n "_index": "filebeat-8.0.0-2020.09.02-000001",\n "_id": "M_xLWXQBc39KFIJbY7Cb",\n "_score": null,\n "_source": {\n "agent": {\n "name": "bastion00.siem.estc.dev",\n "id": "f9a321c1-ec27-49fa-aacf-6a50ef6d836f",\n "type": "filebeat",\n "ephemeral_id": "734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc",\n "version": "8.0.0"\n },\n "process": {\n "name": "sshd",\n "pid": 20671\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 1028103\n },\n "source": {\n "geo": {\n "continent_name": "North America",\n "region_iso_code": "US-NY",\n "city_name": "New York",\n "country_iso_code": "US",\n "region_name": "New York",\n "location": {\n "lon": -74,\n "lat": 40.7157\n }\n },\n "ip": "64.227.88.245"\n },\n "fileset": {\n "name": "auth"\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T13:25:43.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "related": {\n "ip": [\n "64.227.88.245"\n ],\n "user": [\n "user"\n ]\n },\n "service": {\n "type": "system"\n },\n "host": {\n "hostname": "bastion00",\n "name": "bastion00.siem.estc.dev"\n },\n "event": {\n "ingested": "2020-09-04T13:25:47.034172Z",\n "timezone": "+00:00",\n "kind": "event",\n "module": "system",\n "action": "ssh_login",\n "type": [\n "authentication_failure",\n "info"\n ],\n "category": [\n "authentication"\n ],\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "user"\n }\n },\n "sort": [\n 1599225943000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n },\n {\n "key": "ubuntu",\n "doc_count": 18,\n "failures": {\n "doc_count": 18,\n "lastFailure": {\n "hits": {\n "total": 18,\n "max_score": 0,\n "hits": [\n {\n "_index": "filebeat-8.0.0-2020.09.02-000001",\n "_id": "nPxKWXQBc39KFIJb7q4w",\n "_score": null,\n "_source": {\n "agent": {\n "name": "bastion00.siem.estc.dev",\n "id": "f9a321c1-ec27-49fa-aacf-6a50ef6d836f",\n "ephemeral_id": "734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc",\n "type": "filebeat",\n "version": "8.0.0"\n },\n "process": {\n "name": "sshd",\n "pid": 20665\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 1027372\n },\n "source": {\n "geo": {\n "continent_name": "North America",\n "region_iso_code": "US-NY",\n "city_name": "New York",\n "country_iso_code": "US",\n "region_name": "New York",\n "location": {\n "lon": -74,\n "lat": 40.7157\n }\n },\n "ip": "64.227.88.245"\n },\n "fileset": {\n "name": "auth"\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T13:25:07.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "related": {\n "ip": [\n "64.227.88.245"\n ],\n "user": [\n "ubuntu"\n ]\n },\n "service": {\n "type": "system"\n },\n "host": {\n "hostname": "bastion00",\n "name": "bastion00.siem.estc.dev"\n },\n "event": {\n "ingested": "2020-09-04T13:25:16.974606Z",\n "timezone": "+00:00",\n "kind": "event",\n "module": "system",\n "action": "ssh_login",\n "type": [\n "authentication_failure",\n "info"\n ],\n "category": [\n "authentication"\n ],\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "ubuntu"\n }\n },\n "sort": [\n 1599225907000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n },\n {\n "key": "odoo",\n "doc_count": 17,\n "failures": {\n "doc_count": 17,\n "lastFailure": {\n "hits": {\n "total": 17,\n "max_score": 0,\n "hits": [\n {\n "_index": ".ds-logs-system.auth-default-000001",\n "_id": "mPsfWXQBc39KFIJbI8HI",\n "_score": null,\n "_source": {\n "agent": {\n "hostname": "siem-kibana",\n "name": "siem-kibana",\n "id": "aa3d9dc7-fef1-4c2f-a68d-25785d624e35",\n "type": "filebeat",\n "ephemeral_id": "e503bd85-11c7-4bc9-ae7d-70be1d919fb7",\n "version": "7.9.1"\n },\n "process": {\n "name": "sshd",\n "pid": 21506\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 556761\n },\n "source": {\n "geo": {\n "continent_name": "Asia",\n "region_iso_code": "IN-DL",\n "city_name": "New Delhi",\n "country_iso_code": "IN",\n "region_name": "National Capital Territory of Delhi",\n "location": {\n "lon": 77.2245,\n "lat": 28.6358\n }\n },\n "as": {\n "number": 10029,\n "organization": {\n "name": "SHYAM SPECTRA PVT LTD"\n }\n },\n "ip": "180.151.228.166"\n },\n "cloud": {\n "availability_zone": "us-east1-b",\n "instance": {\n "name": "siem-kibana",\n "id": "5412578377715150143"\n },\n "provider": "gcp",\n "machine": {\n "type": "n1-standard-2"\n },\n "project": {\n "id": "elastic-beats"\n }\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T12:26:36.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "data_stream": {\n "namespace": "default",\n "type": "logs",\n "dataset": "system.auth"\n },\n "host": {\n "hostname": "siem-kibana",\n "os": {\n "kernel": "4.9.0-8-amd64",\n "codename": "stretch",\n "name": "Debian GNU/Linux",\n "family": "debian",\n "version": "9 (stretch)",\n "platform": "debian"\n },\n "containerized": false,\n "ip": [\n "10.142.0.7",\n "fe80::4001:aff:fe8e:7"\n ],\n "name": "siem-kibana",\n "id": "aa7ca589f1b8220002f2fc61c64cfbf1",\n "mac": [\n "42:01:0a:8e:00:07"\n ],\n "architecture": "x86_64"\n },\n "event": {\n "timezone": "+00:00",\n "action": "ssh_login",\n "type": "authentication_failure",\n "category": "authentication",\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "odoo"\n }\n },\n "sort": [\n 1599222396000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n },\n {\n "key": "pi",\n "doc_count": 17,\n "failures": {\n "doc_count": 17,\n "lastFailure": {\n "hits": {\n "total": 17,\n "max_score": 0,\n "hits": [\n {\n "_index": "filebeat-8.0.0-2020.09.02-000001",\n "_id": "aaToWHQBA6bGZw2uR-St",\n "_score": null,\n "_source": {\n "agent": {\n "name": "bastion00.siem.estc.dev",\n "id": "f9a321c1-ec27-49fa-aacf-6a50ef6d836f",\n "type": "filebeat",\n "ephemeral_id": "734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc",\n "version": "8.0.0"\n },\n "process": {\n "name": "sshd",\n "pid": 20475\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 1019218\n },\n "source": {\n "geo": {\n "continent_name": "Europe",\n "region_iso_code": "SE-AB",\n "city_name": "Stockholm",\n "country_iso_code": "SE",\n "region_name": "Stockholm",\n "location": {\n "lon": 17.7833,\n "lat": 59.25\n }\n },\n "as": {\n "number": 8473,\n "organization": {\n "name": "Bahnhof AB"\n }\n },\n "ip": "178.174.148.58"\n },\n "fileset": {\n "name": "auth"\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T11:37:22.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "related": {\n "ip": [\n "178.174.148.58"\n ],\n "user": [\n "pi"\n ]\n },\n "service": {\n "type": "system"\n },\n "host": {\n "hostname": "bastion00",\n "name": "bastion00.siem.estc.dev"\n },\n "event": {\n "ingested": "2020-09-04T11:37:31.797423Z",\n "timezone": "+00:00",\n "kind": "event",\n "module": "system",\n "action": "ssh_login",\n "type": [\n "authentication_failure",\n "info"\n ],\n "category": [\n "authentication"\n ],\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "pi"\n }\n },\n "sort": [\n 1599219442000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n },\n {\n "key": "demo",\n "doc_count": 14,\n "failures": {\n "doc_count": 14,\n "lastFailure": {\n "hits": {\n "total": 14,\n "max_score": 0,\n "hits": [\n {\n "_index": "filebeat-8.0.0-2020.09.02-000001",\n "_id": "VaP_V3QBA6bGZw2upUbg",\n "_score": null,\n "_source": {\n "agent": {\n "name": "bastion00.siem.estc.dev",\n "id": "f9a321c1-ec27-49fa-aacf-6a50ef6d836f",\n "type": "filebeat",\n "ephemeral_id": "734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc",\n "version": "8.0.0"\n },\n "process": {\n "name": "sshd",\n "pid": 19849\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 981036\n },\n "source": {\n "geo": {\n "continent_name": "Europe",\n "country_iso_code": "HR",\n "location": {\n "lon": 15.5,\n "lat": 45.1667\n }\n },\n "as": {\n "number": 42864,\n "organization": {\n "name": "Giganet Internet Szolgaltato Kft"\n }\n },\n "ip": "45.95.168.157"\n },\n "fileset": {\n "name": "auth"\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T07:23:22.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "related": {\n "ip": [\n "45.95.168.157"\n ],\n "user": [\n "demo"\n ]\n },\n "service": {\n "type": "system"\n },\n "host": {\n "hostname": "bastion00",\n "name": "bastion00.siem.estc.dev"\n },\n "event": {\n "ingested": "2020-09-04T07:23:26.046346Z",\n "timezone": "+00:00",\n "kind": "event",\n "module": "system",\n "action": "ssh_login",\n "type": [\n "authentication_failure",\n "info"\n ],\n "category": [\n "authentication"\n ],\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "demo"\n }\n },\n "sort": [\n 1599204202000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n },\n {\n "key": "git",\n "doc_count": 13,\n "failures": {\n "doc_count": 13,\n "lastFailure": {\n "hits": {\n "total": 13,\n "max_score": 0,\n "hits": [\n {\n "_index": ".ds-logs-system.auth-default-000001",\n "_id": "PqYfWXQBA6bGZw2uIhVU",\n "_score": null,\n "_source": {\n "agent": {\n "hostname": "siem-kibana",\n "name": "siem-kibana",\n "id": "aa3d9dc7-fef1-4c2f-a68d-25785d624e35",\n "ephemeral_id": "e503bd85-11c7-4bc9-ae7d-70be1d919fb7",\n "type": "filebeat",\n "version": "7.9.1"\n },\n "process": {\n "name": "sshd",\n "pid": 20396\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 550795\n },\n "source": {\n "geo": {\n "continent_name": "Asia",\n "region_iso_code": "CN-BJ",\n "city_name": "Beijing",\n "country_iso_code": "CN",\n "region_name": "Beijing",\n "location": {\n "lon": 116.3889,\n "lat": 39.9288\n }\n },\n "as": {\n "number": 45090,\n "organization": {\n "name": "Shenzhen Tencent Computer Systems Company Limited"\n }\n },\n "ip": "123.206.30.76"\n },\n "cloud": {\n "availability_zone": "us-east1-b",\n "instance": {\n "name": "siem-kibana",\n "id": "5412578377715150143"\n },\n "provider": "gcp",\n "machine": {\n "type": "n1-standard-2"\n },\n "project": {\n "id": "elastic-beats"\n }\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T11:20:26.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "data_stream": {\n "namespace": "default",\n "type": "logs",\n "dataset": "system.auth"\n },\n "host": {\n "hostname": "siem-kibana",\n "os": {\n "kernel": "4.9.0-8-amd64",\n "codename": "stretch",\n "name": "Debian GNU/Linux",\n "family": "debian",\n "version": "9 (stretch)",\n "platform": "debian"\n },\n "containerized": false,\n "ip": [\n "10.142.0.7",\n "fe80::4001:aff:fe8e:7"\n ],\n "name": "siem-kibana",\n "id": "aa7ca589f1b8220002f2fc61c64cfbf1",\n "mac": [\n "42:01:0a:8e:00:07"\n ],\n "architecture": "x86_64"\n },\n "event": {\n "timezone": "+00:00",\n "action": "ssh_login",\n "type": "authentication_failure",\n "category": "authentication",\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "git"\n }\n },\n "sort": [\n 1599218426000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n },\n {\n "key": "webadmin",\n "doc_count": 13,\n "failures": {\n "doc_count": 13,\n "lastFailure": {\n "hits": {\n "total": 13,\n "max_score": 0,\n "hits": [\n {\n "_index": "filebeat-8.0.0-2020.09.02-000001",\n "_id": "iMABWHQBB-gskclyitP-",\n "_score": null,\n "_source": {\n "agent": {\n "name": "bastion00.siem.estc.dev",\n "id": "f9a321c1-ec27-49fa-aacf-6a50ef6d836f",\n "type": "filebeat",\n "ephemeral_id": "734ee3da-1a4f-4bc9-b400-e0cf0e5eeebc",\n "version": "8.0.0"\n },\n "process": {\n "name": "sshd",\n "pid": 19870\n },\n "log": {\n "file": {\n "path": "/var/log/auth.log"\n },\n "offset": 984133\n },\n "source": {\n "geo": {\n "continent_name": "Europe",\n "country_iso_code": "HR",\n "location": {\n "lon": 15.5,\n "lat": 45.1667\n }\n },\n "as": {\n "number": 42864,\n "organization": {\n "name": "Giganet Internet Szolgaltato Kft"\n }\n },\n "ip": "45.95.168.157"\n },\n "fileset": {\n "name": "auth"\n },\n "input": {\n "type": "log"\n },\n "@timestamp": "2020-09-04T07:25:28.000Z",\n "system": {\n "auth": {\n "ssh": {\n "event": "Invalid"\n }\n }\n },\n "ecs": {\n "version": "1.5.0"\n },\n "related": {\n "ip": [\n "45.95.168.157"\n ],\n "user": [\n "webadmin"\n ]\n },\n "service": {\n "type": "system"\n },\n "host": {\n "hostname": "bastion00",\n "name": "bastion00.siem.estc.dev"\n },\n "event": {\n "ingested": "2020-09-04T07:25:30.236651Z",\n "timezone": "+00:00",\n "kind": "event",\n "module": "system",\n "action": "ssh_login",\n "type": [\n "authentication_failure",\n "info"\n ],\n "category": [\n "authentication"\n ],\n "dataset": "system.auth",\n "outcome": "failure"\n },\n "user": {\n "name": "webadmin"\n }\n },\n "sort": [\n 1599204328000\n ]\n }\n ]\n }\n }\n },\n "successes": {\n "doc_count": 0,\n "lastSuccess": {\n "hits": {\n "total": 0,\n "max_score": 0,\n "hits": []\n }\n }\n }\n }\n ]\n },\n "user_count": {\n "value": 188\n }\n }\n },\n "total": 21,\n "loaded": 21\n}', + ], + }, + hostDetails: {}, +}; + +export const expectedDsl = { + allowNoIndices: true, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + ignoreUnavailable: true, + body: { + aggregations: { + host_architecture: { + terms: { field: 'host.architecture', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_id: { + terms: { field: 'host.id', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_ip: { + terms: { field: 'host.ip', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_mac: { + terms: { field: 'host.mac', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_name: { + terms: { field: 'host.name', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_os_family: { + terms: { field: 'host.os.family', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_os_name: { + terms: { field: 'host.os.name', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_os_platform: { + terms: { field: 'host.os.platform', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + host_os_version: { + terms: { field: 'host.os.version', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + cloud_instance_id: { + terms: { field: 'cloud.instance.id', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + cloud_machine_type: { + terms: { field: 'cloud.machine.type', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + cloud_provider: { + terms: { field: 'cloud.provider', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + cloud_region: { + terms: { field: 'cloud.region', size: 10, order: { timestamp: 'desc' } }, + aggs: { timestamp: { max: { field: '@timestamp' } } }, + }, + }, + query: { + bool: { + filter: [ + { term: { 'host.name': 'bastion00' } }, + { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: '2020-09-02T15:17:13.678Z', + lte: '2020-09-03T15:17:13.678Z', + }, + }, + }, + ], + }, + }, + size: 0, + track_total_hits: false, + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx new file mode 100644 index 0000000000000..816b9b2081c63 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/index.test.tsx @@ -0,0 +1,35 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as buildQuery from './query.host_details.dsl'; +import { hostDetails } from '.'; +import { + mockOptions, + mockSearchStrategyResponse, + formattedSearchStrategyResponse, +} from './__mocks__'; + +describe('hostDetails search strategy', () => { + const buildHostDetailsQuery = jest.spyOn(buildQuery, 'buildHostDetailsQuery'); + + afterEach(() => { + buildHostDetailsQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + hostDetails.buildDsl(mockOptions); + expect(buildHostDetailsQuery).toHaveBeenCalledWith(mockOptions); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await hostDetails.parse(mockOptions, mockSearchStrategyResponse); + expect(result).toMatchObject(formattedSearchStrategyResponse); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.test.ts new file mode 100644 index 0000000000000..eab1966434859 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/details/query.host_details.dsl.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildHostDetailsQuery as buildQuery } from './query.host_details.dsl'; +import { mockOptions, expectedDsl } from './__mocks__/'; + +describe('buildQuery', () => { + test('build query from options correctly', () => { + expect(buildQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/__mocks__/index.ts new file mode 100644 index 0000000000000..73cf74087aad6 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/__mocks__/index.ts @@ -0,0 +1,1305 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; + +import { MatrixHistogramStrategyResponse } from '../../../../../../common/search_strategy'; + +export const mockAlertsSearchStrategyResponse: IEsSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 11, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { total: 0, max_score: 0, hits: [] }, + aggregations: { + alertsGroup: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedAlertsSearchStrategyResponse: MatrixHistogramStrategyResponse = { + ...mockAlertsSearchStrategyResponse, + inspect: { + dsl: [ + '{\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "allowNoIndices": true,\n "ignoreUnavailable": true,\n "body": {\n "aggregations": {\n "alertsGroup": {\n "terms": {\n "field": "event.module",\n "missing": "All others",\n "order": {\n "_count": "desc"\n },\n "size": 10\n },\n "aggs": {\n "alerts": {\n "date_histogram": {\n "field": "@timestamp",\n "fixed_interval": "2700000ms",\n "min_doc_count": 0,\n "extended_bounds": {\n "min": 1599574984482,\n "max": 1599661384482\n }\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n "{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"match_all\\":{}},{\\"bool\\":{\\"filter\\":[{\\"bool\\":{\\"should\\":[{\\"exists\\":{\\"field\\":\\"host.name\\"}}],\\"minimum_should_match\\":1}}]}}],\\"should\\":[],\\"must_not\\":[]}}",\n {\n "bool": {\n "filter": [\n {\n "bool": {\n "should": [\n {\n "match": {\n "event.kind": "alert"\n }\n }\n ],\n "minimum_should_match": 1\n }\n }\n ]\n }\n },\n {\n "range": {\n "@timestamp": {\n "gte": "2020-09-08T14:23:04.482Z",\n "lte": "2020-09-09T14:23:04.482Z",\n "format": "strict_date_optional_time"\n }\n }\n }\n ]\n }\n },\n "size": 0,\n "track_total_hits": true\n }\n}', + ], + }, + matrixHistogramData: [], + totalCount: 0, +}; + +export const expectedDsl = { + allowNoIndices: true, + body: { + aggregations: { + host_count: { cardinality: { field: 'host.name' } }, + host_data: { + aggs: { + lastSeen: { max: { field: '@timestamp' } }, + os: { + top_hits: { + _source: { includes: ['host.os.*'] }, + size: 1, + sort: [{ '@timestamp': { order: 'desc' } }], + }, + }, + }, + terms: { field: 'host.name', order: { lastSeen: 'desc' }, size: 10 }, + }, + }, + query: { + bool: { + filter: [ + { bool: { filter: [{ match_all: {} }], must: [], must_not: [], should: [] } }, + { + range: { + '@timestamp': { + format: 'strict_date_optional_time', + gte: '2020-09-03T09:15:21.415Z', + lte: '2020-09-04T09:15:21.415Z', + }, + }, + }, + ], + }, + }, + size: 0, + track_total_hits: false, + }, + ignoreUnavailable: true, + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], +}; + +export const mockAnomaliesSearchStrategyResponse: IEsSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 9, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { total: 0, max_score: 0, hits: [] }, + aggregations: { + anomalyActionGroup: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedAnomaliesSearchStrategyResponse: MatrixHistogramStrategyResponse = { + ...mockAnomaliesSearchStrategyResponse, + inspect: { + dsl: [ + '{\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "allowNoIndices": true,\n "ignoreUnavailable": true,\n "body": {\n "aggs": {\n "anomalyActionGroup": {\n "terms": {\n "field": "job_id",\n "order": {\n "_count": "desc"\n },\n "size": 10\n },\n "aggs": {\n "anomalies": {\n "date_histogram": {\n "field": "timestamp",\n "fixed_interval": "2700000ms",\n "min_doc_count": 0,\n "extended_bounds": {\n "min": 1599578075566,\n "max": 1599664475566\n }\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n "{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"match_all\\":{}},{\\"bool\\":{\\"should\\":[],\\"minimum_should_match\\":1}},{\\"match_phrase\\":{\\"result_type\\":\\"record\\"}},null,{\\"range\\":{\\"record_score\\":{\\"gte\\":50}}}],\\"should\\":[{\\"exists\\":{\\"field\\":\\"source.ip\\"}},{\\"exists\\":{\\"field\\":\\"destination.ip\\"}}],\\"must_not\\":[],\\"minimum_should_match\\":1}}",\n {\n "range": {\n "timestamp": {\n "gte": "2020-09-08T15:14:35.566Z",\n "lte": "2020-09-09T15:14:35.566Z",\n "format": "strict_date_optional_time"\n }\n }\n }\n ]\n }\n },\n "size": 0,\n "track_total_hits": true\n }\n}', + ], + }, + matrixHistogramData: [], + totalCount: 0, +}; + +export const mockAuthenticationsSearchStrategyResponse: IEsSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 6, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { total: 0, max_score: 0, hits: [] }, + aggregations: { + eventActionGroup: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'failure', + doc_count: 379, + events: { + buckets: [ + { key_as_string: '2020-09-08T15:00:00.000Z', key: 1599577200000, doc_count: 1 }, + { key_as_string: '2020-09-08T15:45:00.000Z', key: 1599579900000, doc_count: 4 }, + { key_as_string: '2020-09-08T16:30:00.000Z', key: 1599582600000, doc_count: 34 }, + { key_as_string: '2020-09-08T17:15:00.000Z', key: 1599585300000, doc_count: 1 }, + { key_as_string: '2020-09-08T18:00:00.000Z', key: 1599588000000, doc_count: 3 }, + { key_as_string: '2020-09-08T18:45:00.000Z', key: 1599590700000, doc_count: 20 }, + { key_as_string: '2020-09-08T19:30:00.000Z', key: 1599593400000, doc_count: 24 }, + { key_as_string: '2020-09-08T20:15:00.000Z', key: 1599596100000, doc_count: 37 }, + { key_as_string: '2020-09-08T21:00:00.000Z', key: 1599598800000, doc_count: 4 }, + { key_as_string: '2020-09-08T21:45:00.000Z', key: 1599601500000, doc_count: 22 }, + { key_as_string: '2020-09-08T22:30:00.000Z', key: 1599604200000, doc_count: 3 }, + { key_as_string: '2020-09-08T23:15:00.000Z', key: 1599606900000, doc_count: 0 }, + { key_as_string: '2020-09-09T00:00:00.000Z', key: 1599609600000, doc_count: 2 }, + { key_as_string: '2020-09-09T00:45:00.000Z', key: 1599612300000, doc_count: 21 }, + { key_as_string: '2020-09-09T01:30:00.000Z', key: 1599615000000, doc_count: 28 }, + { key_as_string: '2020-09-09T02:15:00.000Z', key: 1599617700000, doc_count: 30 }, + { key_as_string: '2020-09-09T03:00:00.000Z', key: 1599620400000, doc_count: 19 }, + { key_as_string: '2020-09-09T03:45:00.000Z', key: 1599623100000, doc_count: 4 }, + { key_as_string: '2020-09-09T04:30:00.000Z', key: 1599625800000, doc_count: 1 }, + { key_as_string: '2020-09-09T05:15:00.000Z', key: 1599628500000, doc_count: 6 }, + { key_as_string: '2020-09-09T06:00:00.000Z', key: 1599631200000, doc_count: 18 }, + { key_as_string: '2020-09-09T06:45:00.000Z', key: 1599633900000, doc_count: 5 }, + { key_as_string: '2020-09-09T07:30:00.000Z', key: 1599636600000, doc_count: 23 }, + { key_as_string: '2020-09-09T08:15:00.000Z', key: 1599639300000, doc_count: 15 }, + { key_as_string: '2020-09-09T09:00:00.000Z', key: 1599642000000, doc_count: 2 }, + { key_as_string: '2020-09-09T09:45:00.000Z', key: 1599644700000, doc_count: 0 }, + { key_as_string: '2020-09-09T10:30:00.000Z', key: 1599647400000, doc_count: 5 }, + { key_as_string: '2020-09-09T11:15:00.000Z', key: 1599650100000, doc_count: 2 }, + { key_as_string: '2020-09-09T12:00:00.000Z', key: 1599652800000, doc_count: 4 }, + { key_as_string: '2020-09-09T12:45:00.000Z', key: 1599655500000, doc_count: 6 }, + { key_as_string: '2020-09-09T13:30:00.000Z', key: 1599658200000, doc_count: 11 }, + { key_as_string: '2020-09-09T14:15:00.000Z', key: 1599660900000, doc_count: 0 }, + { key_as_string: '2020-09-09T15:00:00.000Z', key: 1599663600000, doc_count: 24 }, + ], + }, + }, + { + key: 'success', + doc_count: 191, + events: { + buckets: [ + { key_as_string: '2020-09-08T15:00:00.000Z', key: 1599577200000, doc_count: 2 }, + { key_as_string: '2020-09-08T15:45:00.000Z', key: 1599579900000, doc_count: 5 }, + { key_as_string: '2020-09-08T16:30:00.000Z', key: 1599582600000, doc_count: 5 }, + { key_as_string: '2020-09-08T17:15:00.000Z', key: 1599585300000, doc_count: 2 }, + { key_as_string: '2020-09-08T18:00:00.000Z', key: 1599588000000, doc_count: 4 }, + { key_as_string: '2020-09-08T18:45:00.000Z', key: 1599590700000, doc_count: 6 }, + { key_as_string: '2020-09-08T19:30:00.000Z', key: 1599593400000, doc_count: 4 }, + { key_as_string: '2020-09-08T20:15:00.000Z', key: 1599596100000, doc_count: 13 }, + { key_as_string: '2020-09-08T21:00:00.000Z', key: 1599598800000, doc_count: 6 }, + { key_as_string: '2020-09-08T21:45:00.000Z', key: 1599601500000, doc_count: 3 }, + { key_as_string: '2020-09-08T22:30:00.000Z', key: 1599604200000, doc_count: 1 }, + { key_as_string: '2020-09-08T23:15:00.000Z', key: 1599606900000, doc_count: 9 }, + { key_as_string: '2020-09-09T00:00:00.000Z', key: 1599609600000, doc_count: 5 }, + { key_as_string: '2020-09-09T00:45:00.000Z', key: 1599612300000, doc_count: 6 }, + { key_as_string: '2020-09-09T01:30:00.000Z', key: 1599615000000, doc_count: 8 }, + { key_as_string: '2020-09-09T02:15:00.000Z', key: 1599617700000, doc_count: 2 }, + { key_as_string: '2020-09-09T03:00:00.000Z', key: 1599620400000, doc_count: 9 }, + { key_as_string: '2020-09-09T03:45:00.000Z', key: 1599623100000, doc_count: 2 }, + { key_as_string: '2020-09-09T04:30:00.000Z', key: 1599625800000, doc_count: 5 }, + { key_as_string: '2020-09-09T05:15:00.000Z', key: 1599628500000, doc_count: 2 }, + { key_as_string: '2020-09-09T06:00:00.000Z', key: 1599631200000, doc_count: 14 }, + { key_as_string: '2020-09-09T06:45:00.000Z', key: 1599633900000, doc_count: 7 }, + { key_as_string: '2020-09-09T07:30:00.000Z', key: 1599636600000, doc_count: 13 }, + { key_as_string: '2020-09-09T08:15:00.000Z', key: 1599639300000, doc_count: 10 }, + { key_as_string: '2020-09-09T09:00:00.000Z', key: 1599642000000, doc_count: 5 }, + { key_as_string: '2020-09-09T09:45:00.000Z', key: 1599644700000, doc_count: 2 }, + { key_as_string: '2020-09-09T10:30:00.000Z', key: 1599647400000, doc_count: 6 }, + { key_as_string: '2020-09-09T11:15:00.000Z', key: 1599650100000, doc_count: 7 }, + { key_as_string: '2020-09-09T12:00:00.000Z', key: 1599652800000, doc_count: 5 }, + { key_as_string: '2020-09-09T12:45:00.000Z', key: 1599655500000, doc_count: 6 }, + { key_as_string: '2020-09-09T13:30:00.000Z', key: 1599658200000, doc_count: 5 }, + { key_as_string: '2020-09-09T14:15:00.000Z', key: 1599660900000, doc_count: 10 }, + { key_as_string: '2020-09-09T15:00:00.000Z', key: 1599663600000, doc_count: 2 }, + ], + }, + }, + ], + }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedAuthenticationsSearchStrategyResponse: MatrixHistogramStrategyResponse = { + ...mockAuthenticationsSearchStrategyResponse, + inspect: { + dsl: [ + '{\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "allowNoIndices": true,\n "ignoreUnavailable": true,\n "body": {\n "aggregations": {\n "eventActionGroup": {\n "terms": {\n "field": "event.outcome",\n "include": [\n "success",\n "failure"\n ],\n "order": {\n "_count": "desc"\n },\n "size": 2\n },\n "aggs": {\n "events": {\n "date_histogram": {\n "field": "@timestamp",\n "fixed_interval": "2700000ms",\n "min_doc_count": 0,\n "extended_bounds": {\n "min": 1599578520325,\n "max": 1599664920325\n }\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n "{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"match_all\\":{}}],\\"should\\":[],\\"must_not\\":[]}}",\n {\n "bool": {\n "must": [\n {\n "term": {\n "event.category": "authentication"\n }\n }\n ]\n }\n },\n {\n "range": {\n "@timestamp": {\n "gte": "2020-09-08T15:22:00.325Z",\n "lte": "2020-09-09T15:22:00.325Z",\n "format": "strict_date_optional_time"\n }\n }\n }\n ]\n }\n },\n "size": 0,\n "track_total_hits": true\n }\n}', + ], + }, + matrixHistogramData: [ + { x: 1599577200000, y: 1, g: 'failure' }, + { x: 1599579900000, y: 4, g: 'failure' }, + { x: 1599582600000, y: 34, g: 'failure' }, + { x: 1599585300000, y: 1, g: 'failure' }, + { x: 1599588000000, y: 3, g: 'failure' }, + { x: 1599590700000, y: 20, g: 'failure' }, + { x: 1599593400000, y: 24, g: 'failure' }, + { x: 1599596100000, y: 37, g: 'failure' }, + { x: 1599598800000, y: 4, g: 'failure' }, + { x: 1599601500000, y: 22, g: 'failure' }, + { x: 1599604200000, y: 3, g: 'failure' }, + { x: 1599606900000, y: 0, g: 'failure' }, + { x: 1599609600000, y: 2, g: 'failure' }, + { x: 1599612300000, y: 21, g: 'failure' }, + { x: 1599615000000, y: 28, g: 'failure' }, + { x: 1599617700000, y: 30, g: 'failure' }, + { x: 1599620400000, y: 19, g: 'failure' }, + { x: 1599623100000, y: 4, g: 'failure' }, + { x: 1599625800000, y: 1, g: 'failure' }, + { x: 1599628500000, y: 6, g: 'failure' }, + { x: 1599631200000, y: 18, g: 'failure' }, + { x: 1599633900000, y: 5, g: 'failure' }, + { x: 1599636600000, y: 23, g: 'failure' }, + { x: 1599639300000, y: 15, g: 'failure' }, + { x: 1599642000000, y: 2, g: 'failure' }, + { x: 1599644700000, y: 0, g: 'failure' }, + { x: 1599647400000, y: 5, g: 'failure' }, + { x: 1599650100000, y: 2, g: 'failure' }, + { x: 1599652800000, y: 4, g: 'failure' }, + { x: 1599655500000, y: 6, g: 'failure' }, + { x: 1599658200000, y: 11, g: 'failure' }, + { x: 1599660900000, y: 0, g: 'failure' }, + { x: 1599663600000, y: 24, g: 'failure' }, + { x: 1599577200000, y: 2, g: 'success' }, + { x: 1599579900000, y: 5, g: 'success' }, + { x: 1599582600000, y: 5, g: 'success' }, + { x: 1599585300000, y: 2, g: 'success' }, + { x: 1599588000000, y: 4, g: 'success' }, + { x: 1599590700000, y: 6, g: 'success' }, + { x: 1599593400000, y: 4, g: 'success' }, + { x: 1599596100000, y: 13, g: 'success' }, + { x: 1599598800000, y: 6, g: 'success' }, + { x: 1599601500000, y: 3, g: 'success' }, + { x: 1599604200000, y: 1, g: 'success' }, + { x: 1599606900000, y: 9, g: 'success' }, + { x: 1599609600000, y: 5, g: 'success' }, + { x: 1599612300000, y: 6, g: 'success' }, + { x: 1599615000000, y: 8, g: 'success' }, + { x: 1599617700000, y: 2, g: 'success' }, + { x: 1599620400000, y: 9, g: 'success' }, + { x: 1599623100000, y: 2, g: 'success' }, + { x: 1599625800000, y: 5, g: 'success' }, + { x: 1599628500000, y: 2, g: 'success' }, + { x: 1599631200000, y: 14, g: 'success' }, + { x: 1599633900000, y: 7, g: 'success' }, + { x: 1599636600000, y: 13, g: 'success' }, + { x: 1599639300000, y: 10, g: 'success' }, + { x: 1599642000000, y: 5, g: 'success' }, + { x: 1599644700000, y: 2, g: 'success' }, + { x: 1599647400000, y: 6, g: 'success' }, + { x: 1599650100000, y: 7, g: 'success' }, + { x: 1599652800000, y: 5, g: 'success' }, + { x: 1599655500000, y: 6, g: 'success' }, + { x: 1599658200000, y: 5, g: 'success' }, + { x: 1599660900000, y: 10, g: 'success' }, + { x: 1599663600000, y: 2, g: 'success' }, + ], + totalCount: 0, +}; + +export const mockEventsSearchStrategyResponse: IEsSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 198, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { total: 0, max_score: 0, hits: [] }, + aggregations: { + eventActionGroup: { + doc_count_error_upper_bound: 3, + sum_other_doc_count: 4090, + buckets: [ + { + key: 'All others', + doc_count: 1556741, + events: { + buckets: [ + { key_as_string: '2020-09-08T15:45:00.000Z', key: 1599579900000, doc_count: 26124 }, + { key_as_string: '2020-09-08T16:30:00.000Z', key: 1599582600000, doc_count: 62910 }, + { key_as_string: '2020-09-08T17:15:00.000Z', key: 1599585300000, doc_count: 60326 }, + { key_as_string: '2020-09-08T18:00:00.000Z', key: 1599588000000, doc_count: 56144 }, + { key_as_string: '2020-09-08T18:45:00.000Z', key: 1599590700000, doc_count: 53614 }, + { key_as_string: '2020-09-08T19:30:00.000Z', key: 1599593400000, doc_count: 53228 }, + { key_as_string: '2020-09-08T20:15:00.000Z', key: 1599596100000, doc_count: 61195 }, + { key_as_string: '2020-09-08T21:00:00.000Z', key: 1599598800000, doc_count: 52082 }, + { key_as_string: '2020-09-08T21:45:00.000Z', key: 1599601500000, doc_count: 52697 }, + { key_as_string: '2020-09-08T22:30:00.000Z', key: 1599604200000, doc_count: 41094 }, + { key_as_string: '2020-09-08T23:15:00.000Z', key: 1599606900000, doc_count: 50164 }, + { key_as_string: '2020-09-09T00:00:00.000Z', key: 1599609600000, doc_count: 41500 }, + { key_as_string: '2020-09-09T00:45:00.000Z', key: 1599612300000, doc_count: 42373 }, + { key_as_string: '2020-09-09T01:30:00.000Z', key: 1599615000000, doc_count: 49785 }, + { key_as_string: '2020-09-09T02:15:00.000Z', key: 1599617700000, doc_count: 42237 }, + { key_as_string: '2020-09-09T03:00:00.000Z', key: 1599620400000, doc_count: 43114 }, + { key_as_string: '2020-09-09T03:45:00.000Z', key: 1599623100000, doc_count: 40716 }, + { key_as_string: '2020-09-09T04:30:00.000Z', key: 1599625800000, doc_count: 39248 }, + { key_as_string: '2020-09-09T05:15:00.000Z', key: 1599628500000, doc_count: 37674 }, + { key_as_string: '2020-09-09T06:00:00.000Z', key: 1599631200000, doc_count: 41072 }, + { key_as_string: '2020-09-09T06:45:00.000Z', key: 1599633900000, doc_count: 37049 }, + { key_as_string: '2020-09-09T07:30:00.000Z', key: 1599636600000, doc_count: 38561 }, + { key_as_string: '2020-09-09T08:15:00.000Z', key: 1599639300000, doc_count: 40895 }, + { key_as_string: '2020-09-09T09:00:00.000Z', key: 1599642000000, doc_count: 45490 }, + { key_as_string: '2020-09-09T09:45:00.000Z', key: 1599644700000, doc_count: 46559 }, + { key_as_string: '2020-09-09T10:30:00.000Z', key: 1599647400000, doc_count: 40020 }, + { key_as_string: '2020-09-09T11:15:00.000Z', key: 1599650100000, doc_count: 44335 }, + { key_as_string: '2020-09-09T12:00:00.000Z', key: 1599652800000, doc_count: 47252 }, + { key_as_string: '2020-09-09T12:45:00.000Z', key: 1599655500000, doc_count: 48744 }, + { key_as_string: '2020-09-09T13:30:00.000Z', key: 1599658200000, doc_count: 55756 }, + { key_as_string: '2020-09-09T14:15:00.000Z', key: 1599660900000, doc_count: 56887 }, + { key_as_string: '2020-09-09T15:00:00.000Z', key: 1599663600000, doc_count: 66920 }, + { key_as_string: '2020-09-09T15:45:00.000Z', key: 1599666300000, doc_count: 40976 }, + ], + }, + }, + { + key: 'end', + doc_count: 18413, + events: { + buckets: [ + { key_as_string: '2020-09-08T15:45:00.000Z', key: 1599579900000, doc_count: 226 }, + { key_as_string: '2020-09-08T16:30:00.000Z', key: 1599582600000, doc_count: 547 }, + { key_as_string: '2020-09-08T17:15:00.000Z', key: 1599585300000, doc_count: 532 }, + { key_as_string: '2020-09-08T18:00:00.000Z', key: 1599588000000, doc_count: 551 }, + { key_as_string: '2020-09-08T18:45:00.000Z', key: 1599590700000, doc_count: 543 }, + { key_as_string: '2020-09-08T19:30:00.000Z', key: 1599593400000, doc_count: 547 }, + { key_as_string: '2020-09-08T20:15:00.000Z', key: 1599596100000, doc_count: 656 }, + { key_as_string: '2020-09-08T21:00:00.000Z', key: 1599598800000, doc_count: 543 }, + { key_as_string: '2020-09-08T21:45:00.000Z', key: 1599601500000, doc_count: 616 }, + { key_as_string: '2020-09-08T22:30:00.000Z', key: 1599604200000, doc_count: 539 }, + { key_as_string: '2020-09-08T23:15:00.000Z', key: 1599606900000, doc_count: 539 }, + { key_as_string: '2020-09-09T00:00:00.000Z', key: 1599609600000, doc_count: 547 }, + { key_as_string: '2020-09-09T00:45:00.000Z', key: 1599612300000, doc_count: 616 }, + { key_as_string: '2020-09-09T01:30:00.000Z', key: 1599615000000, doc_count: 640 }, + { key_as_string: '2020-09-09T02:15:00.000Z', key: 1599617700000, doc_count: 614 }, + { key_as_string: '2020-09-09T03:00:00.000Z', key: 1599620400000, doc_count: 545 }, + { key_as_string: '2020-09-09T03:45:00.000Z', key: 1599623100000, doc_count: 537 }, + { key_as_string: '2020-09-09T04:30:00.000Z', key: 1599625800000, doc_count: 544 }, + { key_as_string: '2020-09-09T05:15:00.000Z', key: 1599628500000, doc_count: 571 }, + { key_as_string: '2020-09-09T06:00:00.000Z', key: 1599631200000, doc_count: 743 }, + { key_as_string: '2020-09-09T06:45:00.000Z', key: 1599633900000, doc_count: 560 }, + { key_as_string: '2020-09-09T07:30:00.000Z', key: 1599636600000, doc_count: 598 }, + { key_as_string: '2020-09-09T08:15:00.000Z', key: 1599639300000, doc_count: 613 }, + { key_as_string: '2020-09-09T09:00:00.000Z', key: 1599642000000, doc_count: 563 }, + { key_as_string: '2020-09-09T09:45:00.000Z', key: 1599644700000, doc_count: 540 }, + { key_as_string: '2020-09-09T10:30:00.000Z', key: 1599647400000, doc_count: 538 }, + { key_as_string: '2020-09-09T11:15:00.000Z', key: 1599650100000, doc_count: 549 }, + { key_as_string: '2020-09-09T12:00:00.000Z', key: 1599652800000, doc_count: 561 }, + { key_as_string: '2020-09-09T12:45:00.000Z', key: 1599655500000, doc_count: 554 }, + { key_as_string: '2020-09-09T13:30:00.000Z', key: 1599658200000, doc_count: 561 }, + { key_as_string: '2020-09-09T14:15:00.000Z', key: 1599660900000, doc_count: 542 }, + { key_as_string: '2020-09-09T15:00:00.000Z', key: 1599663600000, doc_count: 712 }, + { key_as_string: '2020-09-09T15:45:00.000Z', key: 1599666300000, doc_count: 326 }, + ], + }, + }, + { + key: 'fork', + doc_count: 18412, + events: { + buckets: [ + { key_as_string: '2020-09-08T15:45:00.000Z', key: 1599579900000, doc_count: 226 }, + { key_as_string: '2020-09-08T16:30:00.000Z', key: 1599582600000, doc_count: 546 }, + { key_as_string: '2020-09-08T17:15:00.000Z', key: 1599585300000, doc_count: 532 }, + { key_as_string: '2020-09-08T18:00:00.000Z', key: 1599588000000, doc_count: 551 }, + { key_as_string: '2020-09-08T18:45:00.000Z', key: 1599590700000, doc_count: 543 }, + { key_as_string: '2020-09-08T19:30:00.000Z', key: 1599593400000, doc_count: 547 }, + { key_as_string: '2020-09-08T20:15:00.000Z', key: 1599596100000, doc_count: 656 }, + { key_as_string: '2020-09-08T21:00:00.000Z', key: 1599598800000, doc_count: 543 }, + { key_as_string: '2020-09-08T21:45:00.000Z', key: 1599601500000, doc_count: 616 }, + { key_as_string: '2020-09-08T22:30:00.000Z', key: 1599604200000, doc_count: 539 }, + { key_as_string: '2020-09-08T23:15:00.000Z', key: 1599606900000, doc_count: 539 }, + { key_as_string: '2020-09-09T00:00:00.000Z', key: 1599609600000, doc_count: 547 }, + { key_as_string: '2020-09-09T00:45:00.000Z', key: 1599612300000, doc_count: 616 }, + { key_as_string: '2020-09-09T01:30:00.000Z', key: 1599615000000, doc_count: 640 }, + { key_as_string: '2020-09-09T02:15:00.000Z', key: 1599617700000, doc_count: 614 }, + { key_as_string: '2020-09-09T03:00:00.000Z', key: 1599620400000, doc_count: 545 }, + { key_as_string: '2020-09-09T03:45:00.000Z', key: 1599623100000, doc_count: 537 }, + { key_as_string: '2020-09-09T04:30:00.000Z', key: 1599625800000, doc_count: 544 }, + { key_as_string: '2020-09-09T05:15:00.000Z', key: 1599628500000, doc_count: 571 }, + { key_as_string: '2020-09-09T06:00:00.000Z', key: 1599631200000, doc_count: 743 }, + { key_as_string: '2020-09-09T06:45:00.000Z', key: 1599633900000, doc_count: 560 }, + { key_as_string: '2020-09-09T07:30:00.000Z', key: 1599636600000, doc_count: 598 }, + { key_as_string: '2020-09-09T08:15:00.000Z', key: 1599639300000, doc_count: 613 }, + { key_as_string: '2020-09-09T09:00:00.000Z', key: 1599642000000, doc_count: 563 }, + { key_as_string: '2020-09-09T09:45:00.000Z', key: 1599644700000, doc_count: 540 }, + { key_as_string: '2020-09-09T10:30:00.000Z', key: 1599647400000, doc_count: 538 }, + { key_as_string: '2020-09-09T11:15:00.000Z', key: 1599650100000, doc_count: 549 }, + { key_as_string: '2020-09-09T12:00:00.000Z', key: 1599652800000, doc_count: 561 }, + { key_as_string: '2020-09-09T12:45:00.000Z', key: 1599655500000, doc_count: 554 }, + { key_as_string: '2020-09-09T13:30:00.000Z', key: 1599658200000, doc_count: 561 }, + { key_as_string: '2020-09-09T14:15:00.000Z', key: 1599660900000, doc_count: 542 }, + { key_as_string: '2020-09-09T15:00:00.000Z', key: 1599663600000, doc_count: 712 }, + { key_as_string: '2020-09-09T15:45:00.000Z', key: 1599666300000, doc_count: 326 }, + ], + }, + }, + { + key: 'exec', + doc_count: 15183, + events: { + buckets: [ + { key_as_string: '2020-09-08T15:45:00.000Z', key: 1599579900000, doc_count: 189 }, + { key_as_string: '2020-09-08T16:30:00.000Z', key: 1599582600000, doc_count: 456 }, + { key_as_string: '2020-09-08T17:15:00.000Z', key: 1599585300000, doc_count: 445 }, + { key_as_string: '2020-09-08T18:00:00.000Z', key: 1599588000000, doc_count: 458 }, + { key_as_string: '2020-09-08T18:45:00.000Z', key: 1599590700000, doc_count: 455 }, + { key_as_string: '2020-09-08T19:30:00.000Z', key: 1599593400000, doc_count: 457 }, + { key_as_string: '2020-09-08T20:15:00.000Z', key: 1599596100000, doc_count: 511 }, + { key_as_string: '2020-09-08T21:00:00.000Z', key: 1599598800000, doc_count: 455 }, + { key_as_string: '2020-09-08T21:45:00.000Z', key: 1599601500000, doc_count: 493 }, + { key_as_string: '2020-09-08T22:30:00.000Z', key: 1599604200000, doc_count: 451 }, + { key_as_string: '2020-09-08T23:15:00.000Z', key: 1599606900000, doc_count: 453 }, + { key_as_string: '2020-09-09T00:00:00.000Z', key: 1599609600000, doc_count: 460 }, + { key_as_string: '2020-09-09T00:45:00.000Z', key: 1599612300000, doc_count: 521 }, + { key_as_string: '2020-09-09T01:30:00.000Z', key: 1599615000000, doc_count: 504 }, + { key_as_string: '2020-09-09T02:15:00.000Z', key: 1599617700000, doc_count: 490 }, + { key_as_string: '2020-09-09T03:00:00.000Z', key: 1599620400000, doc_count: 457 }, + { key_as_string: '2020-09-09T03:45:00.000Z', key: 1599623100000, doc_count: 447 }, + { key_as_string: '2020-09-09T04:30:00.000Z', key: 1599625800000, doc_count: 454 }, + { key_as_string: '2020-09-09T05:15:00.000Z', key: 1599628500000, doc_count: 469 }, + { key_as_string: '2020-09-09T06:00:00.000Z', key: 1599631200000, doc_count: 642 }, + { key_as_string: '2020-09-09T06:45:00.000Z', key: 1599633900000, doc_count: 465 }, + { key_as_string: '2020-09-09T07:30:00.000Z', key: 1599636600000, doc_count: 481 }, + { key_as_string: '2020-09-09T08:15:00.000Z', key: 1599639300000, doc_count: 489 }, + { key_as_string: '2020-09-09T09:00:00.000Z', key: 1599642000000, doc_count: 466 }, + { key_as_string: '2020-09-09T09:45:00.000Z', key: 1599644700000, doc_count: 452 }, + { key_as_string: '2020-09-09T10:30:00.000Z', key: 1599647400000, doc_count: 448 }, + { key_as_string: '2020-09-09T11:15:00.000Z', key: 1599650100000, doc_count: 457 }, + { key_as_string: '2020-09-09T12:00:00.000Z', key: 1599652800000, doc_count: 471 }, + { key_as_string: '2020-09-09T12:45:00.000Z', key: 1599655500000, doc_count: 460 }, + { key_as_string: '2020-09-09T13:30:00.000Z', key: 1599658200000, doc_count: 463 }, + { key_as_string: '2020-09-09T14:15:00.000Z', key: 1599660900000, doc_count: 455 }, + { key_as_string: '2020-09-09T15:00:00.000Z', key: 1599663600000, doc_count: 547 }, + { key_as_string: '2020-09-09T15:45:00.000Z', key: 1599666300000, doc_count: 262 }, + ], + }, + }, + { + key: 'disconnect_received', + doc_count: 4998, + events: { + buckets: [ + { key_as_string: '2020-09-08T15:45:00.000Z', key: 1599579900000, doc_count: 59 }, + { key_as_string: '2020-09-08T16:30:00.000Z', key: 1599582600000, doc_count: 151 }, + { key_as_string: '2020-09-08T17:15:00.000Z', key: 1599585300000, doc_count: 139 }, + { key_as_string: '2020-09-08T18:00:00.000Z', key: 1599588000000, doc_count: 144 }, + { key_as_string: '2020-09-08T18:45:00.000Z', key: 1599590700000, doc_count: 143 }, + { key_as_string: '2020-09-08T19:30:00.000Z', key: 1599593400000, doc_count: 144 }, + { key_as_string: '2020-09-08T20:15:00.000Z', key: 1599596100000, doc_count: 202 }, + { key_as_string: '2020-09-08T21:00:00.000Z', key: 1599598800000, doc_count: 142 }, + { key_as_string: '2020-09-08T21:45:00.000Z', key: 1599601500000, doc_count: 180 }, + { key_as_string: '2020-09-08T22:30:00.000Z', key: 1599604200000, doc_count: 144 }, + { key_as_string: '2020-09-08T23:15:00.000Z', key: 1599606900000, doc_count: 143 }, + { key_as_string: '2020-09-09T00:00:00.000Z', key: 1599609600000, doc_count: 137 }, + { key_as_string: '2020-09-09T00:45:00.000Z', key: 1599612300000, doc_count: 150 }, + { key_as_string: '2020-09-09T01:30:00.000Z', key: 1599615000000, doc_count: 195 }, + { key_as_string: '2020-09-09T02:15:00.000Z', key: 1599617700000, doc_count: 178 }, + { key_as_string: '2020-09-09T03:00:00.000Z', key: 1599620400000, doc_count: 144 }, + { key_as_string: '2020-09-09T03:45:00.000Z', key: 1599623100000, doc_count: 143 }, + { key_as_string: '2020-09-09T04:30:00.000Z', key: 1599625800000, doc_count: 142 }, + { key_as_string: '2020-09-09T05:15:00.000Z', key: 1599628500000, doc_count: 157 }, + { key_as_string: '2020-09-09T06:00:00.000Z', key: 1599631200000, doc_count: 166 }, + { key_as_string: '2020-09-09T06:45:00.000Z', key: 1599633900000, doc_count: 153 }, + { key_as_string: '2020-09-09T07:30:00.000Z', key: 1599636600000, doc_count: 168 }, + { key_as_string: '2020-09-09T08:15:00.000Z', key: 1599639300000, doc_count: 175 }, + { key_as_string: '2020-09-09T09:00:00.000Z', key: 1599642000000, doc_count: 158 }, + { key_as_string: '2020-09-09T09:45:00.000Z', key: 1599644700000, doc_count: 142 }, + { key_as_string: '2020-09-09T10:30:00.000Z', key: 1599647400000, doc_count: 144 }, + { key_as_string: '2020-09-09T11:15:00.000Z', key: 1599650100000, doc_count: 147 }, + { key_as_string: '2020-09-09T12:00:00.000Z', key: 1599652800000, doc_count: 139 }, + { key_as_string: '2020-09-09T12:45:00.000Z', key: 1599655500000, doc_count: 145 }, + { key_as_string: '2020-09-09T13:30:00.000Z', key: 1599658200000, doc_count: 158 }, + { key_as_string: '2020-09-09T14:15:00.000Z', key: 1599660900000, doc_count: 137 }, + { key_as_string: '2020-09-09T15:00:00.000Z', key: 1599663600000, doc_count: 234 }, + { key_as_string: '2020-09-09T15:45:00.000Z', key: 1599666300000, doc_count: 95 }, + ], + }, + }, + { + key: 'connection_attempted', + doc_count: 4534, + events: { + buckets: [ + { key_as_string: '2020-09-08T15:45:00.000Z', key: 1599579900000, doc_count: 60 }, + { key_as_string: '2020-09-08T16:30:00.000Z', key: 1599582600000, doc_count: 145 }, + { key_as_string: '2020-09-08T17:15:00.000Z', key: 1599585300000, doc_count: 138 }, + { key_as_string: '2020-09-08T18:00:00.000Z', key: 1599588000000, doc_count: 144 }, + { key_as_string: '2020-09-08T18:45:00.000Z', key: 1599590700000, doc_count: 140 }, + { key_as_string: '2020-09-08T19:30:00.000Z', key: 1599593400000, doc_count: 144 }, + { key_as_string: '2020-09-08T20:15:00.000Z', key: 1599596100000, doc_count: 145 }, + { key_as_string: '2020-09-08T21:00:00.000Z', key: 1599598800000, doc_count: 137 }, + { key_as_string: '2020-09-08T21:45:00.000Z', key: 1599601500000, doc_count: 142 }, + { key_as_string: '2020-09-08T22:30:00.000Z', key: 1599604200000, doc_count: 142 }, + { key_as_string: '2020-09-08T23:15:00.000Z', key: 1599606900000, doc_count: 143 }, + { key_as_string: '2020-09-09T00:00:00.000Z', key: 1599609600000, doc_count: 132 }, + { key_as_string: '2020-09-09T00:45:00.000Z', key: 1599612300000, doc_count: 153 }, + { key_as_string: '2020-09-09T01:30:00.000Z', key: 1599615000000, doc_count: 143 }, + { key_as_string: '2020-09-09T02:15:00.000Z', key: 1599617700000, doc_count: 142 }, + { key_as_string: '2020-09-09T03:00:00.000Z', key: 1599620400000, doc_count: 143 }, + { key_as_string: '2020-09-09T03:45:00.000Z', key: 1599623100000, doc_count: 142 }, + { key_as_string: '2020-09-09T04:30:00.000Z', key: 1599625800000, doc_count: 140 }, + { key_as_string: '2020-09-09T05:15:00.000Z', key: 1599628500000, doc_count: 140 }, + { key_as_string: '2020-09-09T06:00:00.000Z', key: 1599631200000, doc_count: 148 }, + { key_as_string: '2020-09-09T06:45:00.000Z', key: 1599633900000, doc_count: 142 }, + { key_as_string: '2020-09-09T07:30:00.000Z', key: 1599636600000, doc_count: 139 }, + { key_as_string: '2020-09-09T08:15:00.000Z', key: 1599639300000, doc_count: 139 }, + { key_as_string: '2020-09-09T09:00:00.000Z', key: 1599642000000, doc_count: 142 }, + { key_as_string: '2020-09-09T09:45:00.000Z', key: 1599644700000, doc_count: 142 }, + { key_as_string: '2020-09-09T10:30:00.000Z', key: 1599647400000, doc_count: 143 }, + { key_as_string: '2020-09-09T11:15:00.000Z', key: 1599650100000, doc_count: 141 }, + { key_as_string: '2020-09-09T12:00:00.000Z', key: 1599652800000, doc_count: 137 }, + { key_as_string: '2020-09-09T12:45:00.000Z', key: 1599655500000, doc_count: 141 }, + { key_as_string: '2020-09-09T13:30:00.000Z', key: 1599658200000, doc_count: 144 }, + { key_as_string: '2020-09-09T14:15:00.000Z', key: 1599660900000, doc_count: 138 }, + { key_as_string: '2020-09-09T15:00:00.000Z', key: 1599663600000, doc_count: 145 }, + { key_as_string: '2020-09-09T15:45:00.000Z', key: 1599666300000, doc_count: 78 }, + ], + }, + }, + { + key: 'creation', + doc_count: 1880, + events: { + buckets: [ + { key_as_string: '2020-09-08T15:45:00.000Z', key: 1599579900000, doc_count: 24 }, + { key_as_string: '2020-09-08T16:30:00.000Z', key: 1599582600000, doc_count: 53 }, + { key_as_string: '2020-09-08T17:15:00.000Z', key: 1599585300000, doc_count: 50 }, + { key_as_string: '2020-09-08T18:00:00.000Z', key: 1599588000000, doc_count: 54 }, + { key_as_string: '2020-09-08T18:45:00.000Z', key: 1599590700000, doc_count: 55 }, + { key_as_string: '2020-09-08T19:30:00.000Z', key: 1599593400000, doc_count: 53 }, + { key_as_string: '2020-09-08T20:15:00.000Z', key: 1599596100000, doc_count: 54 }, + { key_as_string: '2020-09-08T21:00:00.000Z', key: 1599598800000, doc_count: 54 }, + { key_as_string: '2020-09-08T21:45:00.000Z', key: 1599601500000, doc_count: 55 }, + { key_as_string: '2020-09-08T22:30:00.000Z', key: 1599604200000, doc_count: 52 }, + { key_as_string: '2020-09-08T23:15:00.000Z', key: 1599606900000, doc_count: 51 }, + { key_as_string: '2020-09-09T00:00:00.000Z', key: 1599609600000, doc_count: 58 }, + { key_as_string: '2020-09-09T00:45:00.000Z', key: 1599612300000, doc_count: 122 }, + { key_as_string: '2020-09-09T01:30:00.000Z', key: 1599615000000, doc_count: 54 }, + { key_as_string: '2020-09-09T02:15:00.000Z', key: 1599617700000, doc_count: 54 }, + { key_as_string: '2020-09-09T03:00:00.000Z', key: 1599620400000, doc_count: 56 }, + { key_as_string: '2020-09-09T03:45:00.000Z', key: 1599623100000, doc_count: 53 }, + { key_as_string: '2020-09-09T04:30:00.000Z', key: 1599625800000, doc_count: 55 }, + { key_as_string: '2020-09-09T05:15:00.000Z', key: 1599628500000, doc_count: 51 }, + { key_as_string: '2020-09-09T06:00:00.000Z', key: 1599631200000, doc_count: 144 }, + { key_as_string: '2020-09-09T06:45:00.000Z', key: 1599633900000, doc_count: 54 }, + { key_as_string: '2020-09-09T07:30:00.000Z', key: 1599636600000, doc_count: 53 }, + { key_as_string: '2020-09-09T08:15:00.000Z', key: 1599639300000, doc_count: 51 }, + { key_as_string: '2020-09-09T09:00:00.000Z', key: 1599642000000, doc_count: 57 }, + { key_as_string: '2020-09-09T09:45:00.000Z', key: 1599644700000, doc_count: 55 }, + { key_as_string: '2020-09-09T10:30:00.000Z', key: 1599647400000, doc_count: 52 }, + { key_as_string: '2020-09-09T11:15:00.000Z', key: 1599650100000, doc_count: 52 }, + { key_as_string: '2020-09-09T12:00:00.000Z', key: 1599652800000, doc_count: 57 }, + { key_as_string: '2020-09-09T12:45:00.000Z', key: 1599655500000, doc_count: 56 }, + { key_as_string: '2020-09-09T13:30:00.000Z', key: 1599658200000, doc_count: 53 }, + { key_as_string: '2020-09-09T14:15:00.000Z', key: 1599660900000, doc_count: 51 }, + { key_as_string: '2020-09-09T15:00:00.000Z', key: 1599663600000, doc_count: 56 }, + { key_as_string: '2020-09-09T15:45:00.000Z', key: 1599666300000, doc_count: 31 }, + ], + }, + }, + { + key: 'deletion', + doc_count: 1869, + events: { + buckets: [ + { key_as_string: '2020-09-08T15:45:00.000Z', key: 1599579900000, doc_count: 23 }, + { key_as_string: '2020-09-08T16:30:00.000Z', key: 1599582600000, doc_count: 53 }, + { key_as_string: '2020-09-08T17:15:00.000Z', key: 1599585300000, doc_count: 50 }, + { key_as_string: '2020-09-08T18:00:00.000Z', key: 1599588000000, doc_count: 54 }, + { key_as_string: '2020-09-08T18:45:00.000Z', key: 1599590700000, doc_count: 54 }, + { key_as_string: '2020-09-08T19:30:00.000Z', key: 1599593400000, doc_count: 53 }, + { key_as_string: '2020-09-08T20:15:00.000Z', key: 1599596100000, doc_count: 53 }, + { key_as_string: '2020-09-08T21:00:00.000Z', key: 1599598800000, doc_count: 54 }, + { key_as_string: '2020-09-08T21:45:00.000Z', key: 1599601500000, doc_count: 55 }, + { key_as_string: '2020-09-08T22:30:00.000Z', key: 1599604200000, doc_count: 52 }, + { key_as_string: '2020-09-08T23:15:00.000Z', key: 1599606900000, doc_count: 51 }, + { key_as_string: '2020-09-09T00:00:00.000Z', key: 1599609600000, doc_count: 55 }, + { key_as_string: '2020-09-09T00:45:00.000Z', key: 1599612300000, doc_count: 121 }, + { key_as_string: '2020-09-09T01:30:00.000Z', key: 1599615000000, doc_count: 54 }, + { key_as_string: '2020-09-09T02:15:00.000Z', key: 1599617700000, doc_count: 53 }, + { key_as_string: '2020-09-09T03:00:00.000Z', key: 1599620400000, doc_count: 55 }, + { key_as_string: '2020-09-09T03:45:00.000Z', key: 1599623100000, doc_count: 53 }, + { key_as_string: '2020-09-09T04:30:00.000Z', key: 1599625800000, doc_count: 54 }, + { key_as_string: '2020-09-09T05:15:00.000Z', key: 1599628500000, doc_count: 51 }, + { key_as_string: '2020-09-09T06:00:00.000Z', key: 1599631200000, doc_count: 146 }, + { key_as_string: '2020-09-09T06:45:00.000Z', key: 1599633900000, doc_count: 54 }, + { key_as_string: '2020-09-09T07:30:00.000Z', key: 1599636600000, doc_count: 53 }, + { key_as_string: '2020-09-09T08:15:00.000Z', key: 1599639300000, doc_count: 51 }, + { key_as_string: '2020-09-09T09:00:00.000Z', key: 1599642000000, doc_count: 55 }, + { key_as_string: '2020-09-09T09:45:00.000Z', key: 1599644700000, doc_count: 55 }, + { key_as_string: '2020-09-09T10:30:00.000Z', key: 1599647400000, doc_count: 52 }, + { key_as_string: '2020-09-09T11:15:00.000Z', key: 1599650100000, doc_count: 55 }, + { key_as_string: '2020-09-09T12:00:00.000Z', key: 1599652800000, doc_count: 55 }, + { key_as_string: '2020-09-09T12:45:00.000Z', key: 1599655500000, doc_count: 55 }, + { key_as_string: '2020-09-09T13:30:00.000Z', key: 1599658200000, doc_count: 53 }, + { key_as_string: '2020-09-09T14:15:00.000Z', key: 1599660900000, doc_count: 51 }, + { key_as_string: '2020-09-09T15:00:00.000Z', key: 1599663600000, doc_count: 55 }, + { key_as_string: '2020-09-09T15:45:00.000Z', key: 1599666300000, doc_count: 31 }, + ], + }, + }, + { + key: 'File Delete (rule: FileDelete)', + doc_count: 1831, + events: { + buckets: [ + { key_as_string: '2020-09-08T15:45:00.000Z', key: 1599579900000, doc_count: 19 }, + { key_as_string: '2020-09-08T16:30:00.000Z', key: 1599582600000, doc_count: 46 }, + { key_as_string: '2020-09-08T17:15:00.000Z', key: 1599585300000, doc_count: 47 }, + { key_as_string: '2020-09-08T18:00:00.000Z', key: 1599588000000, doc_count: 47 }, + { key_as_string: '2020-09-08T18:45:00.000Z', key: 1599590700000, doc_count: 47 }, + { key_as_string: '2020-09-08T19:30:00.000Z', key: 1599593400000, doc_count: 45 }, + { key_as_string: '2020-09-08T20:15:00.000Z', key: 1599596100000, doc_count: 48 }, + { key_as_string: '2020-09-08T21:00:00.000Z', key: 1599598800000, doc_count: 46 }, + { key_as_string: '2020-09-08T21:45:00.000Z', key: 1599601500000, doc_count: 45 }, + { key_as_string: '2020-09-08T22:30:00.000Z', key: 1599604200000, doc_count: 47 }, + { key_as_string: '2020-09-08T23:15:00.000Z', key: 1599606900000, doc_count: 47 }, + { key_as_string: '2020-09-09T00:00:00.000Z', key: 1599609600000, doc_count: 60 }, + { key_as_string: '2020-09-09T00:45:00.000Z', key: 1599612300000, doc_count: 45 }, + { key_as_string: '2020-09-09T01:30:00.000Z', key: 1599615000000, doc_count: 46 }, + { key_as_string: '2020-09-09T02:15:00.000Z', key: 1599617700000, doc_count: 46 }, + { key_as_string: '2020-09-09T03:00:00.000Z', key: 1599620400000, doc_count: 47 }, + { key_as_string: '2020-09-09T03:45:00.000Z', key: 1599623100000, doc_count: 88 }, + { key_as_string: '2020-09-09T04:30:00.000Z', key: 1599625800000, doc_count: 53 }, + { key_as_string: '2020-09-09T05:15:00.000Z', key: 1599628500000, doc_count: 46 }, + { key_as_string: '2020-09-09T06:00:00.000Z', key: 1599631200000, doc_count: 49 }, + { key_as_string: '2020-09-09T06:45:00.000Z', key: 1599633900000, doc_count: 45 }, + { key_as_string: '2020-09-09T07:30:00.000Z', key: 1599636600000, doc_count: 48 }, + { key_as_string: '2020-09-09T08:15:00.000Z', key: 1599639300000, doc_count: 46 }, + { key_as_string: '2020-09-09T09:00:00.000Z', key: 1599642000000, doc_count: 46 }, + { key_as_string: '2020-09-09T09:45:00.000Z', key: 1599644700000, doc_count: 45 }, + { key_as_string: '2020-09-09T10:30:00.000Z', key: 1599647400000, doc_count: 47 }, + { key_as_string: '2020-09-09T11:15:00.000Z', key: 1599650100000, doc_count: 47 }, + { key_as_string: '2020-09-09T12:00:00.000Z', key: 1599652800000, doc_count: 45 }, + { key_as_string: '2020-09-09T12:45:00.000Z', key: 1599655500000, doc_count: 331 }, + { key_as_string: '2020-09-09T13:30:00.000Z', key: 1599658200000, doc_count: 45 }, + { key_as_string: '2020-09-09T14:15:00.000Z', key: 1599660900000, doc_count: 47 }, + { key_as_string: '2020-09-09T15:00:00.000Z', key: 1599663600000, doc_count: 47 }, + { key_as_string: '2020-09-09T15:45:00.000Z', key: 1599666300000, doc_count: 28 }, + ], + }, + }, + { + key: 'session_id_change', + doc_count: 647, + events: { + buckets: [ + { key_as_string: '2020-09-08T15:45:00.000Z', key: 1599579900000, doc_count: 3 }, + { key_as_string: '2020-09-08T16:30:00.000Z', key: 1599582600000, doc_count: 9 }, + { key_as_string: '2020-09-08T17:15:00.000Z', key: 1599585300000, doc_count: 7 }, + { key_as_string: '2020-09-08T18:00:00.000Z', key: 1599588000000, doc_count: 10 }, + { key_as_string: '2020-09-08T18:45:00.000Z', key: 1599590700000, doc_count: 7 }, + { key_as_string: '2020-09-08T19:30:00.000Z', key: 1599593400000, doc_count: 10 }, + { key_as_string: '2020-09-08T20:15:00.000Z', key: 1599596100000, doc_count: 63 }, + { key_as_string: '2020-09-08T21:00:00.000Z', key: 1599598800000, doc_count: 7 }, + { key_as_string: '2020-09-08T21:45:00.000Z', key: 1599601500000, doc_count: 45 }, + { key_as_string: '2020-09-08T22:30:00.000Z', key: 1599604200000, doc_count: 4 }, + { key_as_string: '2020-09-08T23:15:00.000Z', key: 1599606900000, doc_count: 5 }, + { key_as_string: '2020-09-09T00:00:00.000Z', key: 1599609600000, doc_count: 6 }, + { key_as_string: '2020-09-09T00:45:00.000Z', key: 1599612300000, doc_count: 6 }, + { key_as_string: '2020-09-09T01:30:00.000Z', key: 1599615000000, doc_count: 55 }, + { key_as_string: '2020-09-09T02:15:00.000Z', key: 1599617700000, doc_count: 43 }, + { key_as_string: '2020-09-09T03:00:00.000Z', key: 1599620400000, doc_count: 8 }, + { key_as_string: '2020-09-09T03:45:00.000Z', key: 1599623100000, doc_count: 9 }, + { key_as_string: '2020-09-09T04:30:00.000Z', key: 1599625800000, doc_count: 7 }, + { key_as_string: '2020-09-09T05:15:00.000Z', key: 1599628500000, doc_count: 21 }, + { key_as_string: '2020-09-09T06:00:00.000Z', key: 1599631200000, doc_count: 26 }, + { key_as_string: '2020-09-09T06:45:00.000Z', key: 1599633900000, doc_count: 17 }, + { key_as_string: '2020-09-09T07:30:00.000Z', key: 1599636600000, doc_count: 34 }, + { key_as_string: '2020-09-09T08:15:00.000Z', key: 1599639300000, doc_count: 41 }, + { key_as_string: '2020-09-09T09:00:00.000Z', key: 1599642000000, doc_count: 18 }, + { key_as_string: '2020-09-09T09:45:00.000Z', key: 1599644700000, doc_count: 4 }, + { key_as_string: '2020-09-09T10:30:00.000Z', key: 1599647400000, doc_count: 11 }, + { key_as_string: '2020-09-09T11:15:00.000Z', key: 1599650100000, doc_count: 9 }, + { key_as_string: '2020-09-09T12:00:00.000Z', key: 1599652800000, doc_count: 7 }, + { key_as_string: '2020-09-09T12:45:00.000Z', key: 1599655500000, doc_count: 12 }, + { key_as_string: '2020-09-09T13:30:00.000Z', key: 1599658200000, doc_count: 16 }, + { key_as_string: '2020-09-09T14:15:00.000Z', key: 1599660900000, doc_count: 7 }, + { key_as_string: '2020-09-09T15:00:00.000Z', key: 1599663600000, doc_count: 99 }, + { key_as_string: '2020-09-09T15:45:00.000Z', key: 1599666300000, doc_count: 21 }, + ], + }, + }, + ], + }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedEventsSearchStrategyResponse: MatrixHistogramStrategyResponse = { + ...mockEventsSearchStrategyResponse, + inspect: { + dsl: [ + '{\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "allowNoIndices": true,\n "ignoreUnavailable": true,\n "body": {\n "aggregations": {\n "eventActionGroup": {\n "terms": {\n "field": "event.action",\n "missing": "All others",\n "order": {\n "_count": "desc"\n },\n "size": 10\n },\n "aggs": {\n "events": {\n "date_histogram": {\n "field": "@timestamp",\n "fixed_interval": "2700000ms",\n "min_doc_count": 0,\n "extended_bounds": {\n "min": 1599581486215,\n "max": 1599667886215\n }\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n "{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"match_all\\":{}}],\\"should\\":[],\\"must_not\\":[]}}",\n {\n "range": {\n "@timestamp": {\n "gte": "2020-09-08T16:11:26.215Z",\n "lte": "2020-09-09T16:11:26.215Z",\n "format": "strict_date_optional_time"\n }\n }\n }\n ]\n }\n },\n "size": 0,\n "track_total_hits": true\n }\n}', + ], + }, + totalCount: 0, + matrixHistogramData: [ + { x: 1599579900000, y: 26124, g: 'All others' }, + { x: 1599582600000, y: 62910, g: 'All others' }, + { x: 1599585300000, y: 60326, g: 'All others' }, + { x: 1599588000000, y: 56144, g: 'All others' }, + { x: 1599590700000, y: 53614, g: 'All others' }, + { x: 1599593400000, y: 53228, g: 'All others' }, + { x: 1599596100000, y: 61195, g: 'All others' }, + { x: 1599598800000, y: 52082, g: 'All others' }, + { x: 1599601500000, y: 52697, g: 'All others' }, + { x: 1599604200000, y: 41094, g: 'All others' }, + { x: 1599606900000, y: 50164, g: 'All others' }, + { x: 1599609600000, y: 41500, g: 'All others' }, + { x: 1599612300000, y: 42373, g: 'All others' }, + { x: 1599615000000, y: 49785, g: 'All others' }, + { x: 1599617700000, y: 42237, g: 'All others' }, + { x: 1599620400000, y: 43114, g: 'All others' }, + { x: 1599623100000, y: 40716, g: 'All others' }, + { x: 1599625800000, y: 39248, g: 'All others' }, + { x: 1599628500000, y: 37674, g: 'All others' }, + { x: 1599631200000, y: 41072, g: 'All others' }, + { x: 1599633900000, y: 37049, g: 'All others' }, + { x: 1599636600000, y: 38561, g: 'All others' }, + { x: 1599639300000, y: 40895, g: 'All others' }, + { x: 1599642000000, y: 45490, g: 'All others' }, + { x: 1599644700000, y: 46559, g: 'All others' }, + { x: 1599647400000, y: 40020, g: 'All others' }, + { x: 1599650100000, y: 44335, g: 'All others' }, + { x: 1599652800000, y: 47252, g: 'All others' }, + { x: 1599655500000, y: 48744, g: 'All others' }, + { x: 1599658200000, y: 55756, g: 'All others' }, + { x: 1599660900000, y: 56887, g: 'All others' }, + { x: 1599663600000, y: 66920, g: 'All others' }, + { x: 1599666300000, y: 40976, g: 'All others' }, + { x: 1599579900000, y: 226, g: 'end' }, + { x: 1599582600000, y: 547, g: 'end' }, + { x: 1599585300000, y: 532, g: 'end' }, + { x: 1599588000000, y: 551, g: 'end' }, + { x: 1599590700000, y: 543, g: 'end' }, + { x: 1599593400000, y: 547, g: 'end' }, + { x: 1599596100000, y: 656, g: 'end' }, + { x: 1599598800000, y: 543, g: 'end' }, + { x: 1599601500000, y: 616, g: 'end' }, + { x: 1599604200000, y: 539, g: 'end' }, + { x: 1599606900000, y: 539, g: 'end' }, + { x: 1599609600000, y: 547, g: 'end' }, + { x: 1599612300000, y: 616, g: 'end' }, + { x: 1599615000000, y: 640, g: 'end' }, + { x: 1599617700000, y: 614, g: 'end' }, + { x: 1599620400000, y: 545, g: 'end' }, + { x: 1599623100000, y: 537, g: 'end' }, + { x: 1599625800000, y: 544, g: 'end' }, + { x: 1599628500000, y: 571, g: 'end' }, + { x: 1599631200000, y: 743, g: 'end' }, + { x: 1599633900000, y: 560, g: 'end' }, + { x: 1599636600000, y: 598, g: 'end' }, + { x: 1599639300000, y: 613, g: 'end' }, + { x: 1599642000000, y: 563, g: 'end' }, + { x: 1599644700000, y: 540, g: 'end' }, + { x: 1599647400000, y: 538, g: 'end' }, + { x: 1599650100000, y: 549, g: 'end' }, + { x: 1599652800000, y: 561, g: 'end' }, + { x: 1599655500000, y: 554, g: 'end' }, + { x: 1599658200000, y: 561, g: 'end' }, + { x: 1599660900000, y: 542, g: 'end' }, + { x: 1599663600000, y: 712, g: 'end' }, + { x: 1599666300000, y: 326, g: 'end' }, + { x: 1599579900000, y: 226, g: 'fork' }, + { x: 1599582600000, y: 546, g: 'fork' }, + { x: 1599585300000, y: 532, g: 'fork' }, + { x: 1599588000000, y: 551, g: 'fork' }, + { x: 1599590700000, y: 543, g: 'fork' }, + { x: 1599593400000, y: 547, g: 'fork' }, + { x: 1599596100000, y: 656, g: 'fork' }, + { x: 1599598800000, y: 543, g: 'fork' }, + { x: 1599601500000, y: 616, g: 'fork' }, + { x: 1599604200000, y: 539, g: 'fork' }, + { x: 1599606900000, y: 539, g: 'fork' }, + { x: 1599609600000, y: 547, g: 'fork' }, + { x: 1599612300000, y: 616, g: 'fork' }, + { x: 1599615000000, y: 640, g: 'fork' }, + { x: 1599617700000, y: 614, g: 'fork' }, + { x: 1599620400000, y: 545, g: 'fork' }, + { x: 1599623100000, y: 537, g: 'fork' }, + { x: 1599625800000, y: 544, g: 'fork' }, + { x: 1599628500000, y: 571, g: 'fork' }, + { x: 1599631200000, y: 743, g: 'fork' }, + { x: 1599633900000, y: 560, g: 'fork' }, + { x: 1599636600000, y: 598, g: 'fork' }, + { x: 1599639300000, y: 613, g: 'fork' }, + { x: 1599642000000, y: 563, g: 'fork' }, + { x: 1599644700000, y: 540, g: 'fork' }, + { x: 1599647400000, y: 538, g: 'fork' }, + { x: 1599650100000, y: 549, g: 'fork' }, + { x: 1599652800000, y: 561, g: 'fork' }, + { x: 1599655500000, y: 554, g: 'fork' }, + { x: 1599658200000, y: 561, g: 'fork' }, + { x: 1599660900000, y: 542, g: 'fork' }, + { x: 1599663600000, y: 712, g: 'fork' }, + { x: 1599666300000, y: 326, g: 'fork' }, + { x: 1599579900000, y: 189, g: 'exec' }, + { x: 1599582600000, y: 456, g: 'exec' }, + { x: 1599585300000, y: 445, g: 'exec' }, + { x: 1599588000000, y: 458, g: 'exec' }, + { x: 1599590700000, y: 455, g: 'exec' }, + { x: 1599593400000, y: 457, g: 'exec' }, + { x: 1599596100000, y: 511, g: 'exec' }, + { x: 1599598800000, y: 455, g: 'exec' }, + { x: 1599601500000, y: 493, g: 'exec' }, + { x: 1599604200000, y: 451, g: 'exec' }, + { x: 1599606900000, y: 453, g: 'exec' }, + { x: 1599609600000, y: 460, g: 'exec' }, + { x: 1599612300000, y: 521, g: 'exec' }, + { x: 1599615000000, y: 504, g: 'exec' }, + { x: 1599617700000, y: 490, g: 'exec' }, + { x: 1599620400000, y: 457, g: 'exec' }, + { x: 1599623100000, y: 447, g: 'exec' }, + { x: 1599625800000, y: 454, g: 'exec' }, + { x: 1599628500000, y: 469, g: 'exec' }, + { x: 1599631200000, y: 642, g: 'exec' }, + { x: 1599633900000, y: 465, g: 'exec' }, + { x: 1599636600000, y: 481, g: 'exec' }, + { x: 1599639300000, y: 489, g: 'exec' }, + { x: 1599642000000, y: 466, g: 'exec' }, + { x: 1599644700000, y: 452, g: 'exec' }, + { x: 1599647400000, y: 448, g: 'exec' }, + { x: 1599650100000, y: 457, g: 'exec' }, + { x: 1599652800000, y: 471, g: 'exec' }, + { x: 1599655500000, y: 460, g: 'exec' }, + { x: 1599658200000, y: 463, g: 'exec' }, + { x: 1599660900000, y: 455, g: 'exec' }, + { x: 1599663600000, y: 547, g: 'exec' }, + { x: 1599666300000, y: 262, g: 'exec' }, + { x: 1599579900000, y: 59, g: 'disconnect_received' }, + { x: 1599582600000, y: 151, g: 'disconnect_received' }, + { x: 1599585300000, y: 139, g: 'disconnect_received' }, + { x: 1599588000000, y: 144, g: 'disconnect_received' }, + { x: 1599590700000, y: 143, g: 'disconnect_received' }, + { x: 1599593400000, y: 144, g: 'disconnect_received' }, + { x: 1599596100000, y: 202, g: 'disconnect_received' }, + { x: 1599598800000, y: 142, g: 'disconnect_received' }, + { x: 1599601500000, y: 180, g: 'disconnect_received' }, + { x: 1599604200000, y: 144, g: 'disconnect_received' }, + { x: 1599606900000, y: 143, g: 'disconnect_received' }, + { x: 1599609600000, y: 137, g: 'disconnect_received' }, + { x: 1599612300000, y: 150, g: 'disconnect_received' }, + { x: 1599615000000, y: 195, g: 'disconnect_received' }, + { x: 1599617700000, y: 178, g: 'disconnect_received' }, + { x: 1599620400000, y: 144, g: 'disconnect_received' }, + { x: 1599623100000, y: 143, g: 'disconnect_received' }, + { x: 1599625800000, y: 142, g: 'disconnect_received' }, + { x: 1599628500000, y: 157, g: 'disconnect_received' }, + { x: 1599631200000, y: 166, g: 'disconnect_received' }, + { x: 1599633900000, y: 153, g: 'disconnect_received' }, + { x: 1599636600000, y: 168, g: 'disconnect_received' }, + { x: 1599639300000, y: 175, g: 'disconnect_received' }, + { x: 1599642000000, y: 158, g: 'disconnect_received' }, + { x: 1599644700000, y: 142, g: 'disconnect_received' }, + { x: 1599647400000, y: 144, g: 'disconnect_received' }, + { x: 1599650100000, y: 147, g: 'disconnect_received' }, + { x: 1599652800000, y: 139, g: 'disconnect_received' }, + { x: 1599655500000, y: 145, g: 'disconnect_received' }, + { x: 1599658200000, y: 158, g: 'disconnect_received' }, + { x: 1599660900000, y: 137, g: 'disconnect_received' }, + { x: 1599663600000, y: 234, g: 'disconnect_received' }, + { x: 1599666300000, y: 95, g: 'disconnect_received' }, + { x: 1599579900000, y: 60, g: 'connection_attempted' }, + { x: 1599582600000, y: 145, g: 'connection_attempted' }, + { x: 1599585300000, y: 138, g: 'connection_attempted' }, + { x: 1599588000000, y: 144, g: 'connection_attempted' }, + { x: 1599590700000, y: 140, g: 'connection_attempted' }, + { x: 1599593400000, y: 144, g: 'connection_attempted' }, + { x: 1599596100000, y: 145, g: 'connection_attempted' }, + { x: 1599598800000, y: 137, g: 'connection_attempted' }, + { x: 1599601500000, y: 142, g: 'connection_attempted' }, + { x: 1599604200000, y: 142, g: 'connection_attempted' }, + { x: 1599606900000, y: 143, g: 'connection_attempted' }, + { x: 1599609600000, y: 132, g: 'connection_attempted' }, + { x: 1599612300000, y: 153, g: 'connection_attempted' }, + { x: 1599615000000, y: 143, g: 'connection_attempted' }, + { x: 1599617700000, y: 142, g: 'connection_attempted' }, + { x: 1599620400000, y: 143, g: 'connection_attempted' }, + { x: 1599623100000, y: 142, g: 'connection_attempted' }, + { x: 1599625800000, y: 140, g: 'connection_attempted' }, + { x: 1599628500000, y: 140, g: 'connection_attempted' }, + { x: 1599631200000, y: 148, g: 'connection_attempted' }, + { x: 1599633900000, y: 142, g: 'connection_attempted' }, + { x: 1599636600000, y: 139, g: 'connection_attempted' }, + { x: 1599639300000, y: 139, g: 'connection_attempted' }, + { x: 1599642000000, y: 142, g: 'connection_attempted' }, + { x: 1599644700000, y: 142, g: 'connection_attempted' }, + { x: 1599647400000, y: 143, g: 'connection_attempted' }, + { x: 1599650100000, y: 141, g: 'connection_attempted' }, + { x: 1599652800000, y: 137, g: 'connection_attempted' }, + { x: 1599655500000, y: 141, g: 'connection_attempted' }, + { x: 1599658200000, y: 144, g: 'connection_attempted' }, + { x: 1599660900000, y: 138, g: 'connection_attempted' }, + { x: 1599663600000, y: 145, g: 'connection_attempted' }, + { x: 1599666300000, y: 78, g: 'connection_attempted' }, + { x: 1599579900000, y: 24, g: 'creation' }, + { x: 1599582600000, y: 53, g: 'creation' }, + { x: 1599585300000, y: 50, g: 'creation' }, + { x: 1599588000000, y: 54, g: 'creation' }, + { x: 1599590700000, y: 55, g: 'creation' }, + { x: 1599593400000, y: 53, g: 'creation' }, + { x: 1599596100000, y: 54, g: 'creation' }, + { x: 1599598800000, y: 54, g: 'creation' }, + { x: 1599601500000, y: 55, g: 'creation' }, + { x: 1599604200000, y: 52, g: 'creation' }, + { x: 1599606900000, y: 51, g: 'creation' }, + { x: 1599609600000, y: 58, g: 'creation' }, + { x: 1599612300000, y: 122, g: 'creation' }, + { x: 1599615000000, y: 54, g: 'creation' }, + { x: 1599617700000, y: 54, g: 'creation' }, + { x: 1599620400000, y: 56, g: 'creation' }, + { x: 1599623100000, y: 53, g: 'creation' }, + { x: 1599625800000, y: 55, g: 'creation' }, + { x: 1599628500000, y: 51, g: 'creation' }, + { x: 1599631200000, y: 144, g: 'creation' }, + { x: 1599633900000, y: 54, g: 'creation' }, + { x: 1599636600000, y: 53, g: 'creation' }, + { x: 1599639300000, y: 51, g: 'creation' }, + { x: 1599642000000, y: 57, g: 'creation' }, + { x: 1599644700000, y: 55, g: 'creation' }, + { x: 1599647400000, y: 52, g: 'creation' }, + { x: 1599650100000, y: 52, g: 'creation' }, + { x: 1599652800000, y: 57, g: 'creation' }, + { x: 1599655500000, y: 56, g: 'creation' }, + { x: 1599658200000, y: 53, g: 'creation' }, + { x: 1599660900000, y: 51, g: 'creation' }, + { x: 1599663600000, y: 56, g: 'creation' }, + { x: 1599666300000, y: 31, g: 'creation' }, + { x: 1599579900000, y: 23, g: 'deletion' }, + { x: 1599582600000, y: 53, g: 'deletion' }, + { x: 1599585300000, y: 50, g: 'deletion' }, + { x: 1599588000000, y: 54, g: 'deletion' }, + { x: 1599590700000, y: 54, g: 'deletion' }, + { x: 1599593400000, y: 53, g: 'deletion' }, + { x: 1599596100000, y: 53, g: 'deletion' }, + { x: 1599598800000, y: 54, g: 'deletion' }, + { x: 1599601500000, y: 55, g: 'deletion' }, + { x: 1599604200000, y: 52, g: 'deletion' }, + { x: 1599606900000, y: 51, g: 'deletion' }, + { x: 1599609600000, y: 55, g: 'deletion' }, + { x: 1599612300000, y: 121, g: 'deletion' }, + { x: 1599615000000, y: 54, g: 'deletion' }, + { x: 1599617700000, y: 53, g: 'deletion' }, + { x: 1599620400000, y: 55, g: 'deletion' }, + { x: 1599623100000, y: 53, g: 'deletion' }, + { x: 1599625800000, y: 54, g: 'deletion' }, + { x: 1599628500000, y: 51, g: 'deletion' }, + { x: 1599631200000, y: 146, g: 'deletion' }, + { x: 1599633900000, y: 54, g: 'deletion' }, + { x: 1599636600000, y: 53, g: 'deletion' }, + { x: 1599639300000, y: 51, g: 'deletion' }, + { x: 1599642000000, y: 55, g: 'deletion' }, + { x: 1599644700000, y: 55, g: 'deletion' }, + { x: 1599647400000, y: 52, g: 'deletion' }, + { x: 1599650100000, y: 55, g: 'deletion' }, + { x: 1599652800000, y: 55, g: 'deletion' }, + { x: 1599655500000, y: 55, g: 'deletion' }, + { x: 1599658200000, y: 53, g: 'deletion' }, + { x: 1599660900000, y: 51, g: 'deletion' }, + { x: 1599663600000, y: 55, g: 'deletion' }, + { x: 1599666300000, y: 31, g: 'deletion' }, + { x: 1599579900000, y: 19, g: 'File Delete (rule: FileDelete)' }, + { x: 1599582600000, y: 46, g: 'File Delete (rule: FileDelete)' }, + { x: 1599585300000, y: 47, g: 'File Delete (rule: FileDelete)' }, + { x: 1599588000000, y: 47, g: 'File Delete (rule: FileDelete)' }, + { x: 1599590700000, y: 47, g: 'File Delete (rule: FileDelete)' }, + { x: 1599593400000, y: 45, g: 'File Delete (rule: FileDelete)' }, + { x: 1599596100000, y: 48, g: 'File Delete (rule: FileDelete)' }, + { x: 1599598800000, y: 46, g: 'File Delete (rule: FileDelete)' }, + { x: 1599601500000, y: 45, g: 'File Delete (rule: FileDelete)' }, + { x: 1599604200000, y: 47, g: 'File Delete (rule: FileDelete)' }, + { x: 1599606900000, y: 47, g: 'File Delete (rule: FileDelete)' }, + { x: 1599609600000, y: 60, g: 'File Delete (rule: FileDelete)' }, + { x: 1599612300000, y: 45, g: 'File Delete (rule: FileDelete)' }, + { x: 1599615000000, y: 46, g: 'File Delete (rule: FileDelete)' }, + { x: 1599617700000, y: 46, g: 'File Delete (rule: FileDelete)' }, + { x: 1599620400000, y: 47, g: 'File Delete (rule: FileDelete)' }, + { x: 1599623100000, y: 88, g: 'File Delete (rule: FileDelete)' }, + { x: 1599625800000, y: 53, g: 'File Delete (rule: FileDelete)' }, + { x: 1599628500000, y: 46, g: 'File Delete (rule: FileDelete)' }, + { x: 1599631200000, y: 49, g: 'File Delete (rule: FileDelete)' }, + { x: 1599633900000, y: 45, g: 'File Delete (rule: FileDelete)' }, + { x: 1599636600000, y: 48, g: 'File Delete (rule: FileDelete)' }, + { x: 1599639300000, y: 46, g: 'File Delete (rule: FileDelete)' }, + { x: 1599642000000, y: 46, g: 'File Delete (rule: FileDelete)' }, + { x: 1599644700000, y: 45, g: 'File Delete (rule: FileDelete)' }, + { x: 1599647400000, y: 47, g: 'File Delete (rule: FileDelete)' }, + { x: 1599650100000, y: 47, g: 'File Delete (rule: FileDelete)' }, + { x: 1599652800000, y: 45, g: 'File Delete (rule: FileDelete)' }, + { x: 1599655500000, y: 331, g: 'File Delete (rule: FileDelete)' }, + { x: 1599658200000, y: 45, g: 'File Delete (rule: FileDelete)' }, + { x: 1599660900000, y: 47, g: 'File Delete (rule: FileDelete)' }, + { x: 1599663600000, y: 47, g: 'File Delete (rule: FileDelete)' }, + { x: 1599666300000, y: 28, g: 'File Delete (rule: FileDelete)' }, + { x: 1599579900000, y: 3, g: 'session_id_change' }, + { x: 1599582600000, y: 9, g: 'session_id_change' }, + { x: 1599585300000, y: 7, g: 'session_id_change' }, + { x: 1599588000000, y: 10, g: 'session_id_change' }, + { x: 1599590700000, y: 7, g: 'session_id_change' }, + { x: 1599593400000, y: 10, g: 'session_id_change' }, + { x: 1599596100000, y: 63, g: 'session_id_change' }, + { x: 1599598800000, y: 7, g: 'session_id_change' }, + { x: 1599601500000, y: 45, g: 'session_id_change' }, + { x: 1599604200000, y: 4, g: 'session_id_change' }, + { x: 1599606900000, y: 5, g: 'session_id_change' }, + { x: 1599609600000, y: 6, g: 'session_id_change' }, + { x: 1599612300000, y: 6, g: 'session_id_change' }, + { x: 1599615000000, y: 55, g: 'session_id_change' }, + { x: 1599617700000, y: 43, g: 'session_id_change' }, + { x: 1599620400000, y: 8, g: 'session_id_change' }, + { x: 1599623100000, y: 9, g: 'session_id_change' }, + { x: 1599625800000, y: 7, g: 'session_id_change' }, + { x: 1599628500000, y: 21, g: 'session_id_change' }, + { x: 1599631200000, y: 26, g: 'session_id_change' }, + { x: 1599633900000, y: 17, g: 'session_id_change' }, + { x: 1599636600000, y: 34, g: 'session_id_change' }, + { x: 1599639300000, y: 41, g: 'session_id_change' }, + { x: 1599642000000, y: 18, g: 'session_id_change' }, + { x: 1599644700000, y: 4, g: 'session_id_change' }, + { x: 1599647400000, y: 11, g: 'session_id_change' }, + { x: 1599650100000, y: 9, g: 'session_id_change' }, + { x: 1599652800000, y: 7, g: 'session_id_change' }, + { x: 1599655500000, y: 12, g: 'session_id_change' }, + { x: 1599658200000, y: 16, g: 'session_id_change' }, + { x: 1599660900000, y: 7, g: 'session_id_change' }, + { x: 1599663600000, y: 99, g: 'session_id_change' }, + { x: 1599666300000, y: 21, g: 'session_id_change' }, + ], +}; + +export const mockDnsSearchStrategyResponse: IEsSearchResponse = { + isPartial: false, + isRunning: false, + rawResponse: { + took: 150, + timed_out: false, + _shards: { total: 21, successful: 21, skipped: 0, failed: 0 }, + hits: { total: 0, max_score: 0, hits: [] }, + aggregations: { + NetworkDns: { + buckets: [ + { + key_as_string: '2020-09-08T15:00:00.000Z', + key: 1599577200000, + doc_count: 7083, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-08T15:45:00.000Z', + key: 1599579900000, + doc_count: 146148, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-08T16:30:00.000Z', + key: 1599582600000, + doc_count: 65025, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-08T17:15:00.000Z', + key: 1599585300000, + doc_count: 62317, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-08T18:00:00.000Z', + key: 1599588000000, + doc_count: 58223, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-08T18:45:00.000Z', + key: 1599590700000, + doc_count: 55712, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-08T19:30:00.000Z', + key: 1599593400000, + doc_count: 55328, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-08T20:15:00.000Z', + key: 1599596100000, + doc_count: 63878, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-08T21:00:00.000Z', + key: 1599598800000, + doc_count: 54151, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-08T21:45:00.000Z', + key: 1599601500000, + doc_count: 55170, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-08T22:30:00.000Z', + key: 1599604200000, + doc_count: 43115, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-08T23:15:00.000Z', + key: 1599606900000, + doc_count: 52204, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T00:00:00.000Z', + key: 1599609600000, + doc_count: 43609, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T00:45:00.000Z', + key: 1599612300000, + doc_count: 44825, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T01:30:00.000Z', + key: 1599615000000, + doc_count: 52374, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T02:15:00.000Z', + key: 1599617700000, + doc_count: 44667, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T03:00:00.000Z', + key: 1599620400000, + doc_count: 45231, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T03:45:00.000Z', + key: 1599623100000, + doc_count: 42871, + dns: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { key: 'google.com', doc_count: 1, orderAgg: { value: 1 } }, + { key: 'google.internal', doc_count: 1, orderAgg: { value: 1 } }, + ], + }, + }, + { + key_as_string: '2020-09-09T04:30:00.000Z', + key: 1599625800000, + doc_count: 41327, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T05:15:00.000Z', + key: 1599628500000, + doc_count: 39860, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T06:00:00.000Z', + key: 1599631200000, + doc_count: 44061, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T06:45:00.000Z', + key: 1599633900000, + doc_count: 39193, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T07:30:00.000Z', + key: 1599636600000, + doc_count: 40909, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T08:15:00.000Z', + key: 1599639300000, + doc_count: 43293, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T09:00:00.000Z', + key: 1599642000000, + doc_count: 47640, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T09:45:00.000Z', + key: 1599644700000, + doc_count: 48605, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T10:30:00.000Z', + key: 1599647400000, + doc_count: 42072, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T11:15:00.000Z', + key: 1599650100000, + doc_count: 46398, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T12:00:00.000Z', + key: 1599652800000, + doc_count: 49378, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T12:45:00.000Z', + key: 1599655500000, + doc_count: 51171, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T13:30:00.000Z', + key: 1599658200000, + doc_count: 57911, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T14:15:00.000Z', + key: 1599660900000, + doc_count: 58909, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + { + key_as_string: '2020-09-09T15:00:00.000Z', + key: 1599663600000, + doc_count: 62358, + dns: { doc_count_error_upper_bound: 0, sum_other_doc_count: 0, buckets: [] }, + }, + ], + }, + }, + }, + total: 21, + loaded: 21, +}; + +export const formattedDnsSearchStrategyResponse: MatrixHistogramStrategyResponse = { + ...mockDnsSearchStrategyResponse, + inspect: { + dsl: [ + '{\n "index": [\n "apm-*-transaction*",\n "auditbeat-*",\n "endgame-*",\n "filebeat-*",\n "logs-*",\n "packetbeat-*",\n "winlogbeat-*"\n ],\n "allowNoIndices": true,\n "ignoreUnavailable": true,\n "body": {\n "aggregations": {\n "NetworkDns": {\n "date_histogram": {\n "field": "@timestamp",\n "fixed_interval": "2700000ms"\n },\n "aggs": {\n "dns": {\n "terms": {\n "field": "dns.question.registered_domain",\n "order": {\n "orderAgg": "desc"\n },\n "size": 10\n },\n "aggs": {\n "orderAgg": {\n "cardinality": {\n "field": "dns.question.name"\n }\n }\n }\n }\n }\n }\n },\n "query": {\n "bool": {\n "filter": [\n "{\\"bool\\":{\\"must\\":[],\\"filter\\":[{\\"match_all\\":{}}],\\"should\\":[],\\"must_not\\":[]}}",\n {\n "range": {\n "@timestamp": {\n "gte": "2020-09-08T15:41:15.528Z",\n "lte": "2020-09-09T15:41:15.529Z",\n "format": "strict_date_optional_time"\n }\n }\n }\n ]\n }\n },\n "size": 0,\n "track_total_hits": true\n }\n}', + ], + }, + matrixHistogramData: [ + { x: 1599623100000, y: 1, g: 'google.com' }, + { x: 1599623100000, y: 1, g: 'google.internal' }, + ], + totalCount: 0, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/__mocks__/index.ts new file mode 100644 index 0000000000000..8b2e666ad0103 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/__mocks__/index.ts @@ -0,0 +1,87 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MatrixHistogramType } from '../../../../../../../common/search_strategy'; + +export const mockOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + filterQuery: + '{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"filter":[{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}', + histogramType: MatrixHistogramType.alerts, + timerange: { interval: '12h', from: '2020-09-08T14:23:04.482Z', to: '2020-09-09T14:23:04.482Z' }, + stackByField: 'event.module', +}; + +export const expectedDsl = { + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: { + alertsGroup: { + terms: { + field: 'event.module', + missing: 'All others', + order: { _count: 'desc' }, + size: 10, + }, + aggs: { + alerts: { + date_histogram: { + field: '@timestamp', + fixed_interval: '2700000ms', + min_doc_count: 0, + extended_bounds: { min: 1599574984482, max: 1599661384482 }, + }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + '{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"filter":[{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}', + { + bool: { + filter: [ + { + bool: { should: [{ match: { 'event.kind': 'alert' } }], minimum_should_match: 1 }, + }, + ], + }, + }, + { + range: { + '@timestamp': { + gte: '2020-09-08T14:23:04.482Z', + lte: '2020-09-09T14:23:04.482Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + size: 0, + track_total_hits: true, + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/index.test.ts new file mode 100644 index 0000000000000..3b1e57ea50a87 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/index.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { alertsMatrixHistogramConfig } from '.'; +import { buildAlertsHistogramQuery } from './query.alerts_histogram.dsl'; + +jest.mock('./query.alerts_histogram.dsl', () => ({ + buildAlertsHistogramQuery: jest.fn(), +})); + +describe('alertsMatrixHistogramConfig', () => { + test('should export alertsMatrixHistogramConfig corrrectly', () => { + expect(alertsMatrixHistogramConfig).toEqual({ + aggName: 'aggregations.alertsGroup.buckets', + parseKey: 'alerts.buckets', + buildDsl: buildAlertsHistogramQuery, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/query.alerts_histogram.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/query.alerts_histogram.dsl.test.ts new file mode 100644 index 0000000000000..89a28b10dd684 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/alerts/query.alerts_histogram.dsl.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildAlertsHistogramQuery } from './query.alerts_histogram.dsl'; +import { mockOptions, expectedDsl } from './__mocks__/'; + +describe('buildAlertsHistogramQuery', () => { + test('build query from options correctly', () => { + expect(buildAlertsHistogramQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/__mocks__/index.ts new file mode 100644 index 0000000000000..6ca3c785e2e75 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/__mocks__/index.ts @@ -0,0 +1,73 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MatrixHistogramType } from '../../../../../../../common/search_strategy'; + +export const mockOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + filterQuery: + '{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"should":[],"minimum_should_match":1}},{"match_phrase":{"result_type":"record"}},null,{"range":{"record_score":{"gte":50}}}],"should":[{"exists":{"field":"source.ip"}},{"exists":{"field":"destination.ip"}}],"must_not":[],"minimum_should_match":1}}', + histogramType: MatrixHistogramType.anomalies, + timerange: { interval: '12h', from: '2020-09-08T15:14:35.566Z', to: '2020-09-09T15:14:35.566Z' }, + stackByField: 'job_id', +}; + +export const expectedDsl = { + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggs: { + anomalyActionGroup: { + terms: { field: 'job_id', order: { _count: 'desc' }, size: 10 }, + aggs: { + anomalies: { + date_histogram: { + field: 'timestamp', + fixed_interval: '2700000ms', + min_doc_count: 0, + extended_bounds: { min: 1599578075566, max: 1599664475566 }, + }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + '{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"should":[],"minimum_should_match":1}},{"match_phrase":{"result_type":"record"}},null,{"range":{"record_score":{"gte":50}}}],"should":[{"exists":{"field":"source.ip"}},{"exists":{"field":"destination.ip"}}],"must_not":[],"minimum_should_match":1}}', + { + range: { + timestamp: { + gte: '2020-09-08T15:14:35.566Z', + lte: '2020-09-09T15:14:35.566Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + size: 0, + track_total_hits: true, + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/index.test.ts new file mode 100644 index 0000000000000..d1466a057553d --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/index.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { anomaliesMatrixHistogramConfig } from '.'; +import { buildAnomaliesHistogramQuery } from './query.anomalies_histogram.dsl'; + +jest.mock('./query.anomalies_histogram.dsl', () => ({ + buildAnomaliesHistogramQuery: jest.fn(), +})); + +describe('anomaliesMatrixHistogramConfig', () => { + test('should export anomaliesMatrixHistogramConfig corrrectly', () => { + expect(anomaliesMatrixHistogramConfig).toEqual({ + aggName: 'aggregations.anomalyActionGroup.buckets', + parseKey: 'anomalies.buckets', + buildDsl: buildAnomaliesHistogramQuery, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/query.anomalies_histogram.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/query.anomalies_histogram.dsl.test.ts new file mode 100644 index 0000000000000..7c10005853b26 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/anomalies/query.anomalies_histogram.dsl.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildAnomaliesHistogramQuery } from './query.anomalies_histogram.dsl'; +import { mockOptions, expectedDsl } from './__mocks__'; + +describe('buildAnomaliesHistogramQuery', () => { + test('build query from options correctly', () => { + expect(buildAnomaliesHistogramQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/__mocks__/index.ts new file mode 100644 index 0000000000000..1fd420dbb94cb --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/__mocks__/index.ts @@ -0,0 +1,78 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MatrixHistogramType } from '../../../../../../../common/search_strategy'; + +export const mockOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + histogramType: MatrixHistogramType.authentications, + timerange: { interval: '12h', from: '2020-09-08T15:22:00.325Z', to: '2020-09-09T15:22:00.325Z' }, + stackByField: 'event.outcome', +}; + +export const expectedDsl = { + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: { + eventActionGroup: { + terms: { + field: 'event.outcome', + include: ['success', 'failure'], + order: { _count: 'desc' }, + size: 2, + }, + aggs: { + events: { + date_histogram: { + field: '@timestamp', + fixed_interval: '2700000ms', + min_doc_count: 0, + extended_bounds: { min: 1599578520325, max: 1599664920325 }, + }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + { bool: { must: [{ term: { 'event.category': 'authentication' } }] } }, + { + range: { + '@timestamp': { + gte: '2020-09-08T15:22:00.325Z', + lte: '2020-09-09T15:22:00.325Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + size: 0, + track_total_hits: true, + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/index.test.ts new file mode 100644 index 0000000000000..54f1459b24933 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/index.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { authenticationsMatrixHistogramConfig } from '.'; +import { buildAuthenticationsHistogramQuery } from './query.authentications_histogram.dsl'; + +jest.mock('./query.authentications_histogram.dsl', () => ({ + buildAuthenticationsHistogramQuery: jest.fn(), +})); + +describe('authenticationsMatrixHistogramConfig', () => { + test('should export authenticationsMatrixHistogramConfig corrrectly', () => { + expect(authenticationsMatrixHistogramConfig).toEqual({ + aggName: 'aggregations.eventActionGroup.buckets', + parseKey: 'events.buckets', + buildDsl: buildAuthenticationsHistogramQuery, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/query.authentications_histogram.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/query.authentications_histogram.dsl.test.ts new file mode 100644 index 0000000000000..67d681d8ba870 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/authentications/query.authentications_histogram.dsl.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildAuthenticationsHistogramQuery } from './query.authentications_histogram.dsl'; +import { mockOptions, expectedDsl } from './__mocks__/'; + +describe('buildAuthenticationsHistogramQuery', () => { + test('build query from options correctly', () => { + expect(buildAuthenticationsHistogramQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/__mocks__/index.ts new file mode 100644 index 0000000000000..94ba20327a404 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/__mocks__/index.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { MatrixHistogramType } from '../../../../../../../common/search_strategy'; + +export const mockOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + histogramType: MatrixHistogramType.dns, + timerange: { interval: '12h', from: '2020-09-08T15:41:15.528Z', to: '2020-09-09T15:41:15.529Z' }, + stackByField: 'dns.question.registered_domain', +}; + +export const expectedDsl = { + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: { + NetworkDns: { + date_histogram: { field: '@timestamp', fixed_interval: '2700000ms' }, + aggs: { + dns: { + terms: { + field: 'dns.question.registered_domain', + order: { orderAgg: 'desc' }, + size: 10, + }, + aggs: { orderAgg: { cardinality: { field: 'dns.question.name' } } }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + { + range: { + '@timestamp': { + gte: '2020-09-08T15:41:15.528Z', + lte: '2020-09-09T15:41:15.529Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + size: 0, + track_total_hits: true, + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/index.test.ts new file mode 100644 index 0000000000000..8afc764d97f87 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/index.test.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { dnsMatrixHistogramConfig } from '.'; +import { buildDnsHistogramQuery } from './query.dns_histogram.dsl'; +import { getDnsParsedData } from './helpers'; + +jest.mock('./query.dns_histogram.dsl', () => ({ + buildDnsHistogramQuery: jest.fn(), +})); + +jest.mock('./helpers', () => ({ + getDnsParsedData: jest.fn(), +})); + +describe('dnsMatrixHistogramConfig', () => { + test('should export dnsMatrixHistogramConfig corrrectly', () => { + expect(dnsMatrixHistogramConfig).toEqual({ + aggName: 'aggregations.NetworkDns.buckets', + parseKey: 'dns.buckets', + buildDsl: buildDnsHistogramQuery, + parser: getDnsParsedData, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/query.dns_histogram.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/query.dns_histogram.dsl.test.ts new file mode 100644 index 0000000000000..a3d562a28d07f --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/dns/query.dns_histogram.dsl.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildDnsHistogramQuery } from './query.dns_histogram.dsl'; +import { mockOptions, expectedDsl } from './__mocks__/'; + +describe('buildDnsHistogramQuery', () => { + test('build query from options correctly', () => { + expect(buildDnsHistogramQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/__mocks__/index.ts new file mode 100644 index 0000000000000..09b710ab33c76 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/__mocks__/index.ts @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + MatrixHistogramQuery, + MatrixHistogramRequestOptions, + MatrixHistogramType, +} from '../../../../../../../common/search_strategy'; + +export const mockOptions: MatrixHistogramRequestOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + factoryQueryType: MatrixHistogramQuery, + filterQuery: '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + histogramType: MatrixHistogramType.events, + timerange: { interval: '12h', from: '2020-09-08T16:11:26.215Z', to: '2020-09-09T16:11:26.215Z' }, + stackByField: 'event.action', +}; + +export const expectedDsl = { + index: [ + 'apm-*-transaction*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + allowNoIndices: true, + ignoreUnavailable: true, + body: { + aggregations: { + eventActionGroup: { + terms: { + field: 'event.action', + missing: 'All others', + order: { _count: 'desc' }, + size: 10, + }, + aggs: { + events: { + date_histogram: { + field: '@timestamp', + fixed_interval: '2700000ms', + min_doc_count: 0, + extended_bounds: { min: 1599581486215, max: 1599667886215 }, + }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + '{"bool":{"must":[],"filter":[{"match_all":{}}],"should":[],"must_not":[]}}', + { + range: { + '@timestamp': { + gte: '2020-09-08T16:11:26.215Z', + lte: '2020-09-09T16:11:26.215Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + size: 0, + track_total_hits: true, + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/index.test.ts new file mode 100644 index 0000000000000..f67307eac67ed --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/index.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { eventsMatrixHistogramConfig } from '.'; +import { buildEventsHistogramQuery } from './query.events_histogram.dsl'; + +jest.mock('./query.events_histogram.dsl.ts', () => ({ + buildEventsHistogramQuery: jest.fn(), +})); + +describe('eventsMatrixHistogramConfig', () => { + test('should export eventsMatrixHistogramConfig corrrectly', () => { + expect(eventsMatrixHistogramConfig).toEqual({ + aggName: 'aggregations.eventActionGroup.buckets', + parseKey: 'events.buckets', + buildDsl: buildEventsHistogramQuery, + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.test.ts new file mode 100644 index 0000000000000..72cb9de9f0e7a --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/events/query.events_histogram.dsl.test.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { buildEventsHistogramQuery } from './query.events_histogram.dsl'; +import { mockOptions, expectedDsl } from './__mocks__/'; + +describe('buildEventsHistogramQuery', () => { + test('build query from options correctly', () => { + expect(buildEventsHistogramQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.test.ts new file mode 100644 index 0000000000000..3fd7240eba93b --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/matrix_histogram/index.test.ts @@ -0,0 +1,211 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + MatrixHistogramRequestOptions, + MatrixHistogramType, +} from '../../../../../common/search_strategy/security_solution'; +import { matrixHistogram } from '.'; +import { + formattedAlertsSearchStrategyResponse, + formattedAnomaliesSearchStrategyResponse, + formattedAuthenticationsSearchStrategyResponse, + formattedEventsSearchStrategyResponse, + formattedDnsSearchStrategyResponse, + mockAlertsSearchStrategyResponse, + mockAnomaliesSearchStrategyResponse, + mockAuthenticationsSearchStrategyResponse, + mockEventsSearchStrategyResponse, + mockDnsSearchStrategyResponse, +} from './__mocks__'; +import { alertsMatrixHistogramConfig } from './alerts'; +import { anomaliesMatrixHistogramConfig } from './anomalies'; +import { authenticationsMatrixHistogramConfig } from './authentications'; +import { eventsMatrixHistogramConfig } from './events'; +import { dnsMatrixHistogramConfig } from './dns'; + +import { mockOptions as mockAlertsOptions } from './alerts/__mocks__'; +import { mockOptions as mockAnomaliesOptions } from './anomalies/__mocks__'; +import { mockOptions as mockAuthenticationsOptions } from './authentications/__mocks__'; +import { mockOptions as mockEventsOptions } from './events/__mocks__'; +import { mockOptions as mockDnsOptions } from './dns/__mocks__'; + +describe('Alerts matrixHistogram search strategy', () => { + const buildMatrixHistogramQuery = jest.spyOn(alertsMatrixHistogramConfig, 'buildDsl'); + + afterEach(() => { + buildMatrixHistogramQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + matrixHistogram.buildDsl(mockAlertsOptions); + expect(buildMatrixHistogramQuery).toHaveBeenCalledWith(mockAlertsOptions); + }); + + test('should throw error if histogramType is invalid', () => { + const invalidOptions: MatrixHistogramRequestOptions = { + ...mockAlertsOptions, + histogramType: 'xxx' as MatrixHistogramType, + } as MatrixHistogramRequestOptions; + + expect(() => { + matrixHistogram.buildDsl(invalidOptions); + }).toThrowError(`This histogram type xxx is unknown to the server side`); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await matrixHistogram.parse( + mockAlertsOptions, + mockAlertsSearchStrategyResponse + ); + expect(result).toMatchObject(formattedAlertsSearchStrategyResponse); + }); + }); +}); + +describe('Anomalies matrixHistogram search strategy', () => { + const buildMatrixHistogramQuery = jest.spyOn(anomaliesMatrixHistogramConfig, 'buildDsl'); + + afterEach(() => { + buildMatrixHistogramQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + matrixHistogram.buildDsl(mockAnomaliesOptions); + expect(buildMatrixHistogramQuery).toHaveBeenCalledWith(mockAnomaliesOptions); + }); + + test('should throw error if histogramType is invalid', () => { + const invalidOptions: MatrixHistogramRequestOptions = { + ...mockAnomaliesOptions, + histogramType: 'xxx' as MatrixHistogramType, + } as MatrixHistogramRequestOptions; + + expect(() => { + matrixHistogram.buildDsl(invalidOptions); + }).toThrowError(`This histogram type xxx is unknown to the server side`); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await matrixHistogram.parse( + mockAnomaliesOptions, + mockAnomaliesSearchStrategyResponse + ); + expect(result).toMatchObject(formattedAnomaliesSearchStrategyResponse); + }); + }); +}); + +describe('Authentications matrixHistogram search strategy', () => { + const buildMatrixHistogramQuery = jest.spyOn(authenticationsMatrixHistogramConfig, 'buildDsl'); + + afterEach(() => { + buildMatrixHistogramQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + matrixHistogram.buildDsl(mockAuthenticationsOptions); + expect(buildMatrixHistogramQuery).toHaveBeenCalledWith(mockAuthenticationsOptions); + }); + + test('should throw error if histogramType is invalid', () => { + const invalidOptions = { + ...mockAuthenticationsOptions, + histogramType: 'xxx' as MatrixHistogramType, + } as MatrixHistogramRequestOptions; + + expect(() => { + matrixHistogram.buildDsl(invalidOptions); + }).toThrowError(`This histogram type xxx is unknown to the server side`); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await matrixHistogram.parse( + mockAuthenticationsOptions, + mockAuthenticationsSearchStrategyResponse + ); + expect(result).toMatchObject(formattedAuthenticationsSearchStrategyResponse); + }); + }); +}); + +describe('Events matrixHistogram search strategy', () => { + const buildMatrixHistogramQuery = jest.spyOn(eventsMatrixHistogramConfig, 'buildDsl'); + + afterEach(() => { + buildMatrixHistogramQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + matrixHistogram.buildDsl(mockEventsOptions); + expect(buildMatrixHistogramQuery).toHaveBeenCalledWith(mockEventsOptions); + }); + + test('should throw error if histogramType is invalid', () => { + const invalidOptions = { + ...mockEventsOptions, + histogramType: 'xxx' as MatrixHistogramType, + } as MatrixHistogramRequestOptions; + + expect(() => { + matrixHistogram.buildDsl(invalidOptions); + }).toThrowError(`This histogram type xxx is unknown to the server side`); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await matrixHistogram.parse( + mockEventsOptions, + mockEventsSearchStrategyResponse + ); + expect(result).toMatchObject(formattedEventsSearchStrategyResponse); + }); + }); +}); + +describe('Dns matrixHistogram search strategy', () => { + const buildMatrixHistogramQuery = jest.spyOn(dnsMatrixHistogramConfig, 'buildDsl'); + + afterEach(() => { + buildMatrixHistogramQuery.mockClear(); + }); + + describe('buildDsl', () => { + test('should build dsl query', () => { + matrixHistogram.buildDsl(mockDnsOptions); + expect(buildMatrixHistogramQuery).toHaveBeenCalledWith(mockDnsOptions); + }); + + test('should throw error if histogramType is invalid', () => { + const invalidOptions = { + ...mockDnsOptions, + histogramType: 'xxx' as MatrixHistogramType, + } as MatrixHistogramRequestOptions; + + expect(() => { + matrixHistogram.buildDsl(invalidOptions); + }).toThrowError(`This histogram type xxx is unknown to the server side`); + }); + }); + + describe('parse', () => { + test('should parse data correctly', async () => { + const result = await matrixHistogram.parse(mockDnsOptions, mockDnsSearchStrategyResponse); + expect(result).toMatchObject(formattedDnsSearchStrategyResponse); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts new file mode 100644 index 0000000000000..f232812ac7e40 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/helpers.ts @@ -0,0 +1,55 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; +import { GeoEcs } from '../../../../../../common/ecs/geo'; +import { HostEcs } from '../../../../../../common/ecs/host'; +import { + AutonomousSystem, + NetworkDetailsHostHit, + NetworkHit, +} from '../../../../../../common/search_strategy/security_solution/network'; + +export const getNetworkDetailsAgg = (type: string, networkHit: NetworkHit | {}) => { + const firstSeen = getOr(null, `firstSeen.value_as_string`, networkHit); + const lastSeen = getOr(null, `lastSeen.value_as_string`, networkHit); + const autonomousSystem: AutonomousSystem | null = getOr( + null, + `as.results.hits.hits[0]._source.${type}.as`, + networkHit + ); + const geoFields: GeoEcs | null = getOr( + null, + `geo.results.hits.hits[0]._source.${type}.geo`, + networkHit + ); + + return { + [type]: { + firstSeen, + lastSeen, + autonomousSystem: { + ...autonomousSystem, + }, + geo: { + ...geoFields, + }, + }, + }; +}; + +export const getNetworkDetailsHostAgg = (hostDetailsHit: NetworkDetailsHostHit | {}) => { + const hostFields: HostEcs | null = getOr( + null, + `results.hits.hits[0]._source.host`, + hostDetailsHit + ); + return { + host: { + ...hostFields, + }, + }; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/index.ts new file mode 100644 index 0000000000000..8b2b79c4cd403 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/index.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; + +import { + NetworkDetailsStrategyResponse, + NetworkQueries, + NetworkDetailsRequestOptions, +} from '../../../../../../common/search_strategy/security_solution/network'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; + +import { getNetworkDetailsAgg, getNetworkDetailsHostAgg } from './helpers'; +import { buildNetworkDetailsQuery } from './query.details_network.dsl'; + +export const networkDetails: SecuritySolutionFactory = { + buildDsl: (options: NetworkDetailsRequestOptions) => buildNetworkDetailsQuery(options), + parse: async ( + options: NetworkDetailsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const inspect = { + dsl: [inspectStringifyObject(buildNetworkDetailsQuery(options))], + }; + + return { + ...response, + inspect, + networkDetails: { + ...getNetworkDetailsAgg('source', getOr({}, 'aggregations.source', response.rawResponse)), + ...getNetworkDetailsAgg( + 'destination', + getOr({}, 'aggregations.destination', response.rawResponse) + ), + ...getNetworkDetailsHostAgg(getOr({}, 'aggregations.host', response.rawResponse)), + }, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/query.details_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/query.details_network.dsl.ts new file mode 100644 index 0000000000000..67aeba60c4d2f --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/details/query.details_network.dsl.ts @@ -0,0 +1,126 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { isEmpty } from 'lodash/fp'; +import { NetworkDetailsRequestOptions } from '../../../../../../common/search_strategy/security_solution/network'; + +const getAggs = (type: string, ip: string) => { + return { + [type]: { + filter: { + term: { + [`${type}.ip`]: ip, + }, + }, + aggs: { + firstSeen: { + min: { + field: '@timestamp', + }, + }, + lastSeen: { + max: { + field: '@timestamp', + }, + }, + as: { + filter: { + exists: { + field: `${type}.as`, + }, + }, + aggs: { + results: { + top_hits: { + size: 1, + _source: [`${type}.as`], + sort: [ + { + '@timestamp': 'desc', + }, + ], + }, + }, + }, + }, + geo: { + filter: { + exists: { + field: `${type}.geo`, + }, + }, + aggs: { + results: { + top_hits: { + size: 1, + _source: [`${type}.geo`], + sort: [ + { + '@timestamp': 'desc', + }, + ], + }, + }, + }, + }, + }, + }, + }; +}; + +const getHostAggs = (ip: string) => { + return { + host: { + filter: { + term: { + 'host.ip': ip, + }, + }, + aggs: { + results: { + top_hits: { + size: 1, + _source: ['host'], + sort: [ + { + '@timestamp': 'desc', + }, + ], + }, + }, + }, + }, + }; +}; + +export const buildNetworkDetailsQuery = ({ + defaultIndex, + docValueFields, + ip, +}: NetworkDetailsRequestOptions) => { + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + ...(isEmpty(docValueFields) ? { docvalue_fields: docValueFields } : {}), + aggs: { + ...getAggs('source', ip), + ...getAggs('destination', ip), + ...getHostAggs(ip), + }, + query: { + bool: { + should: [], + }, + }, + size: 0, + track_total_hits: false, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts index 3b927b8589999..bf5321d32b58f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/index.ts @@ -10,18 +10,22 @@ import { } from '../../../../../common/search_strategy/security_solution'; import { SecuritySolutionFactory } from '../types'; +import { networkDetails } from './details'; import { networkDns } from './dns'; import { networkHttp } from './http'; import { networkOverview } from './overview'; import { networkTls } from './tls'; import { networkTopCountries } from './top_countries'; import { networkTopNFlow } from './top_n_flow'; +import { networkUsers } from './users'; export const networkFactory: Record> = { + [NetworkQueries.details]: networkDetails, [NetworkQueries.dns]: networkDns, [NetworkQueries.http]: networkHttp, [NetworkQueries.overview]: networkOverview, [NetworkQueries.tls]: networkTls, [NetworkQueries.topCountries]: networkTopCountries, [NetworkQueries.topNFlow]: networkTopNFlow, + [NetworkQueries.users]: networkUsers, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/helpers.ts index 59359fd35a34e..40653007ef43f 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/helpers.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/helpers.ts @@ -8,16 +8,16 @@ import { getOr } from 'lodash/fp'; import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; import { - TlsBuckets, - TlsEdges, + NetworkTlsBuckets, + NetworkTlsEdges, } from '../../../../../../common/search_strategy/security_solution/network'; -export const getTlsEdges = (response: IEsSearchResponse): TlsEdges[] => - formatTlsEdges(getOr([], 'aggregations.sha1.buckets', response.rawResponse)); +export const getNetworkTlsEdges = (response: IEsSearchResponse): NetworkTlsEdges[] => + formatNetworkTlsEdges(getOr([], 'aggregations.sha1.buckets', response.rawResponse)); -export const formatTlsEdges = (buckets: TlsBuckets[]): TlsEdges[] => - buckets.map((bucket: TlsBuckets) => { - const edge: TlsEdges = { +export const formatNetworkTlsEdges = (buckets: NetworkTlsBuckets[]): NetworkTlsEdges[] => + buckets.map((bucket: NetworkTlsBuckets) => { + const edge: NetworkTlsEdges = { node: { _id: bucket.key, subjects: bucket.subjects.buckets.map(({ key }) => key), diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/index.ts index 32836c0ef6869..46556f46d3a50 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/index.ts @@ -13,21 +13,21 @@ import { NetworkTlsStrategyResponse, NetworkQueries, NetworkTlsRequestOptions, - TlsEdges, + NetworkTlsEdges, } from '../../../../../../common/search_strategy/security_solution/network'; import { inspectStringifyObject } from '../../../../../utils/build_query'; import { SecuritySolutionFactory } from '../../types'; -import { getTlsEdges } from './helpers'; -import { buildTlsQuery } from './query.tls_network.dsl'; +import { getNetworkTlsEdges } from './helpers'; +import { buildNetworkTlsQuery } from './query.tls_network.dsl'; export const networkTls: SecuritySolutionFactory = { buildDsl: (options: NetworkTlsRequestOptions) => { if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); } - return buildTlsQuery(options); + return buildNetworkTlsQuery(options); }, parse: async ( options: NetworkTlsRequestOptions, @@ -35,12 +35,11 @@ export const networkTls: SecuritySolutionFactory = { ): Promise => { const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; const totalCount = getOr(0, 'aggregations.count.value', response.rawResponse); - const tlsEdges: TlsEdges[] = getTlsEdges(response); + const networkTlsEdges: NetworkTlsEdges[] = getNetworkTlsEdges(response); const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; - const edges = tlsEdges.splice(cursorStart, querySize - cursorStart); + const edges = networkTlsEdges.splice(cursorStart, querySize - cursorStart); const inspect = { - dsl: [inspectStringifyObject(buildTlsQuery(options))], - response: [inspectStringifyObject(response)], + dsl: [inspectStringifyObject(buildNetworkTlsQuery(options))], }; const showMorePagesIndicator = totalCount > fakeTotalCount; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.ts index 6e5ba0674a0e7..adf11175479e1 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/tls/query.tls_network.dsl.ts @@ -8,13 +8,13 @@ import { assertUnreachable } from '../../../../../../common/utility_types'; import { createQueryFilterClauses } from '../../../../../utils/build_query'; import { + Direction, NetworkTlsRequestOptions, + NetworkTlsFields, SortField, - Direction, - TlsFields, } from '../../../../../../common/search_strategy'; -const getAggs = (querySize: number, sort: SortField) => ({ +const getAggs = (querySize: number, sort: SortField) => ({ count: { cardinality: { field: 'tls.server.hash.sha1', @@ -53,7 +53,7 @@ const getAggs = (querySize: number, sort: SortField) => ({ }, }); -export const buildTlsQuery = ({ +export const buildNetworkTlsQuery = ({ ip, sort, filterQuery, @@ -98,9 +98,9 @@ interface QueryOrder { _key: Direction; } -const getQueryOrder = (sort: SortField): QueryOrder => { +const getQueryOrder = (sort: SortField): QueryOrder => { switch (sort.field) { - case TlsFields._id: + case NetworkTlsFields._id: return { _key: sort.direction }; default: return assertUnreachable(sort.field); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/helpers.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/helpers.ts new file mode 100644 index 0000000000000..4068721e667a2 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/helpers.ts @@ -0,0 +1,36 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { get, getOr } from 'lodash/fp'; +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { + NetworkUsersBucketsItem, + NetworkUsersEdges, +} from '../../../../../../common/search_strategy/security_solution/network'; + +export const getUsersEdges = (response: IEsSearchResponse): NetworkUsersEdges[] => + getOr([], `aggregations.users.buckets`, response.rawResponse).map( + (bucket: NetworkUsersBucketsItem) => ({ + node: { + _id: bucket.key, + user: { + id: getOr([], 'id.buckets', bucket).map((id: NetworkUsersBucketsItem) => id.key), + name: bucket.key, + groupId: getOr([], 'groupId.buckets', bucket).map( + (groupId: NetworkUsersBucketsItem) => groupId.key + ), + groupName: getOr([], 'groupName.buckets', bucket).map( + (groupName: NetworkUsersBucketsItem) => groupName.key + ), + count: get('doc_count', bucket), + }, + }, + cursor: { + value: bucket.key, + tiebreaker: null, + }, + }) + ); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/index.ts new file mode 100644 index 0000000000000..f9b9a2fd2747f --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/index.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { getOr } from 'lodash/fp'; + +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; + +import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../../../common/constants'; +import { + NetworkUsersStrategyResponse, + NetworkQueries, + NetworkUsersRequestOptions, +} from '../../../../../../common/search_strategy/security_solution/network'; + +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { SecuritySolutionFactory } from '../../types'; + +import { getUsersEdges } from './helpers'; +import { buildUsersQuery } from './query.users_network.dsl'; + +export const networkUsers: SecuritySolutionFactory = { + buildDsl: (options: NetworkUsersRequestOptions) => { + if (options.pagination && options.pagination.querySize >= DEFAULT_MAX_TABLE_QUERY_SIZE) { + throw new Error(`No query size above ${DEFAULT_MAX_TABLE_QUERY_SIZE}`); + } + return buildUsersQuery(options); + }, + parse: async ( + options: NetworkUsersRequestOptions, + response: IEsSearchResponse + ): Promise => { + const { activePage, cursorStart, fakePossibleCount, querySize } = options.pagination; + const totalCount = getOr(0, 'aggregations.user_count.value', response.rawResponse); + const usersEdges = getUsersEdges(response); + const fakeTotalCount = fakePossibleCount <= totalCount ? fakePossibleCount : totalCount; + const edges = usersEdges.splice(cursorStart, querySize - cursorStart); + const inspect = { + dsl: [inspectStringifyObject(buildUsersQuery(options))], + }; + const showMorePagesIndicator = totalCount > fakeTotalCount; + + return { + ...response, + edges, + inspect, + pageInfo: { + activePage: activePage ? activePage : 0, + fakeTotalCount, + showMorePagesIndicator, + }, + totalCount, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/query.users_network.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/query.users_network.dsl.ts new file mode 100644 index 0000000000000..386e83b955043 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/network/users/query.users_network.dsl.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { assertUnreachable } from '../../../../../../common/utility_types'; +import { + Direction, + SortField, + NetworkUsersRequestOptions, + NetworkUsersFields, +} from '../../../../../../common/search_strategy'; +import { createQueryFilterClauses } from '../../../../../utils/build_query'; + +export const buildUsersQuery = ({ + ip, + sort, + filterQuery, + flowTarget, + pagination: { querySize }, + defaultIndex, + timerange: { from, to }, +}: NetworkUsersRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { gte: from, lte: to, format: 'strict_date_optional_time' }, + }, + }, + { term: { [`${flowTarget}.ip`]: ip } }, + ]; + + const dslQuery = { + allowNoIndices: true, + index: defaultIndex, + ignoreUnavailable: true, + body: { + aggs: { + user_count: { + cardinality: { + field: 'user.name', + }, + }, + users: { + terms: { + field: 'user.name', + size: querySize, + order: { + ...getQueryOrder(sort), + }, + }, + aggs: { + id: { + terms: { + field: 'user.id', + }, + }, + groupId: { + terms: { + field: 'user.group.id', + }, + }, + groupName: { + terms: { + field: 'user.group.name', + }, + }, + }, + }, + }, + query: { + bool: { + filter, + must_not: [ + { + term: { + 'event.category': 'authentication', + }, + }, + ], + }, + }, + size: 0, + track_total_hits: false, + }, + }; + + return dslQuery; +}; + +type QueryOrder = { _count: Direction } | { _key: Direction }; + +const getQueryOrder = (sort: SortField): QueryOrder => { + switch (sort.field) { + case NetworkUsersFields.name: + return { _key: sort.direction }; + case NetworkUsersFields.count: + return { _count: sort.direction }; + default: + return assertUnreachable(sort.field); + } +}; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts index a3ab829ab642c..ab2963223f678 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/helpers/policy_form.helpers.ts @@ -56,4 +56,6 @@ export type PolicyFormTestSubjects = | 'showAdvancedCronLink' | 'snapshotNameInput' | 'dataStreamBadge' + | 'repositoryNotFoundWarning' + | 'repositorySelect' | 'submitButton'; diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts index dc568161d4fb4..f7d6a60703bc4 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_add.test.ts @@ -67,6 +67,11 @@ describe('', () => { expect(find('nextButton').props().disabled).toBe(true); }); + test('should not show repository-not-found warning', () => { + const { exists } = testBed; + expect(exists('repositoryNotFoundWarning')).toBe(false); + }); + describe('form validation', () => { describe('logistics (step 1)', () => { test('should require a policy name', async () => { diff --git a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts index a5b2ec73b85cd..7c095256bd10f 100644 --- a/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts +++ b/x-pack/plugins/snapshot_restore/__jest__/client_integration/policy_edit.test.ts @@ -52,6 +52,44 @@ describe('', () => { expect(find('pageTitle').text()).toEqual('Edit policy'); }); + describe('policy with pre-existing repository that was deleted', () => { + beforeEach(async () => { + httpRequestsMockHelpers.setGetPolicyResponse({ policy: POLICY_EDIT }); + httpRequestsMockHelpers.setLoadIndicesResponse({ + indices: ['my_index'], + dataStreams: ['my_data_stream'], + }); + httpRequestsMockHelpers.setLoadRepositoriesResponse({ + repositories: [{ name: 'this-is-a-new-repository' }], + }); + + testBed = await setup(); + + await act(async () => { + await nextTick(); + testBed.component.update(); + }); + }); + + test('should show repository-not-found warning', () => { + const { exists, find } = testBed; + expect(exists('repositoryNotFoundWarning')).toBe(true); + // The select should be an empty string to allow users to select a new repository + expect(find('repositorySelect').props().value).toBe(''); + }); + + describe('validation', () => { + test('should block navigating to next step', () => { + const { exists, find, actions } = testBed; + actions.clickNextButton(); + // Assert that we are still on the repository configuration step + expect(exists('repositoryNotFoundWarning')).toBe(true); + // The select should be an empty string to allow users to select a new repository + expect(find('repositorySelect').props().value).toBe(''); + }); + }); + }); + /** * As the "edit" policy component uses the same form underneath that * the "create" policy, we won't test it again but simply make sure that diff --git a/x-pack/plugins/snapshot_restore/kibana.json b/x-pack/plugins/snapshot_restore/kibana.json index e0a29581ea076..a8a3881929f40 100644 --- a/x-pack/plugins/snapshot_restore/kibana.json +++ b/x-pack/plugins/snapshot_restore/kibana.json @@ -5,7 +5,8 @@ "ui": true, "requiredPlugins": [ "licensing", - "management" + "management", + "features" ], "optionalPlugins": [ "usageCollection", diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/navigation.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/navigation.tsx index 64f5a8fa0871b..d1e3c21399d5f 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/navigation.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/navigation.tsx @@ -11,12 +11,14 @@ interface Props { currentStep: number; maxCompletedStep: number; updateCurrentStep: (step: number) => void; + isFormValid: boolean; } export const PolicyNavigation: React.FunctionComponent = ({ currentStep, maxCompletedStep, updateCurrentStep, + isFormValid, }) => { const { i18n } = useServices(); @@ -27,6 +29,7 @@ export const PolicyNavigation: React.FunctionComponent = ({ }), isComplete: maxCompletedStep >= 1, isSelected: currentStep === 1, + disabled: !isFormValid && currentStep !== 1, onClick: () => updateCurrentStep(1), }, { @@ -35,7 +38,7 @@ export const PolicyNavigation: React.FunctionComponent = ({ }), isComplete: maxCompletedStep >= 2, isSelected: currentStep === 2, - disabled: maxCompletedStep < 1, + disabled: maxCompletedStep < 1 || (!isFormValid && currentStep !== 2), onClick: () => updateCurrentStep(2), }, { @@ -44,7 +47,7 @@ export const PolicyNavigation: React.FunctionComponent = ({ }), isComplete: maxCompletedStep >= 3, isSelected: currentStep === 3, - disabled: maxCompletedStep < 2, + disabled: maxCompletedStep < 2 || (!isFormValid && currentStep !== 3), onClick: () => updateCurrentStep(3), }, { @@ -53,7 +56,7 @@ export const PolicyNavigation: React.FunctionComponent = ({ }), isComplete: maxCompletedStep >= 3, isSelected: currentStep === 4, - disabled: maxCompletedStep < 3, + disabled: maxCompletedStep < 3 || (!isFormValid && currentStep !== 4), onClick: () => updateCurrentStep(4), }, ]; diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx index 3e1fb9b6500b3..c6b841c9ce7f8 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/policy_form.tsx @@ -130,6 +130,7 @@ export const PolicyForm: React.FunctionComponent = ({ currentStep={currentStep} maxCompletedStep={maxCompletedStep} updateCurrentStep={updateCurrentStep} + isFormValid={validation.isValid} /> diff --git a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx index 8a7338f4db4e7..7d3ba92cf2ad7 100644 --- a/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/components/policy_form/steps/step_logistics.tsx @@ -18,6 +18,8 @@ import { EuiLink, EuiSpacer, EuiText, + EuiCallOut, + EuiCode, } from '@elastic/eui'; import { Repository } from '../../../../../common/types'; @@ -49,11 +51,15 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ name: undefined, }, }, - sendRequest: reloadRepositories, + resendRequest: reloadRepositories, } = useLoadRepositories(); const { i18n, history } = useServices(); + const [showRepositoryNotFoundWarning, setShowRepositoryNotFoundWarning] = useState( + false + ); + // State for touched inputs const [touched, setTouched] = useState({ name: false, @@ -256,13 +262,26 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ } } + const doesRepositoryExist = + !!policy.repository && + repositories.some((r: { name: string }) => r.name === policy.repository); + + if (!doesRepositoryExist && !errors.repository) { + updatePolicy(policy, { repositoryDoesNotExist: true }); + } + + if (showRepositoryNotFoundWarning !== !doesRepositoryExist) { + setShowRepositoryNotFoundWarning(!doesRepositoryExist); + } + return ( ({ value: name, text: name, }))} - value={policy.repository || repositories[0].name} + hasNoInitialSelection={!doesRepositoryExist} + value={!doesRepositoryExist ? '' : policy.repository} onBlur={() => setTouched({ ...touched, repository: true })} onChange={(e) => { updatePolicy( @@ -541,8 +560,31 @@ export const PolicyStepLogistics: React.FunctionComponent = ({ - + {showRepositoryNotFoundWarning && ( + <> + + + } + color="danger" + iconType="alert" + > + {policy.repository} }} + /> + + + )} + + {renderNameField()} {renderSnapshotNameField()} {renderRepositoryField()} diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx index f67e8eb586238..b4612c9df42ff 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_details/policy_details.tsx @@ -65,7 +65,7 @@ export const PolicyDetails: React.FunctionComponent = ({ onPolicyExecuted, }) => { const { i18n, uiMetricService, history } = useServices(); - const { error, data: policyDetails, sendRequest: reload } = useLoadPolicy(policyName); + const { error, data: policyDetails, resendRequest: reload } = useLoadPolicy(policyName); const [activeTab, setActiveTab] = useState(TAB_SUMMARY); const [isPopoverOpen, setIsPopoverOpen] = useState(false); diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx index 655bd0e9d8bb9..57f18ccbf8150 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_list.tsx @@ -45,7 +45,7 @@ export const PolicyList: React.FunctionComponent { diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx index d55bbf0b324cf..e7e4a9b54ada7 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/policy_list/policy_table/policy_table.tsx @@ -21,7 +21,7 @@ import { } from '@elastic/eui'; import { SlmPolicy } from '../../../../../../common/types'; -import { Error } from '../../../../../shared_imports'; +import { UseRequestResponse } from '../../../../../shared_imports'; import { UIM_POLICY_SHOW_DETAILS_CLICK } from '../../../../constants'; import { useServices } from '../../../../app_context'; import { @@ -30,13 +30,12 @@ import { PolicyDeleteProvider, } from '../../../../components'; import { linkToAddPolicy, linkToEditPolicy } from '../../../../services/navigation'; -import { SendRequestResponse } from '../../../../../shared_imports'; import { reactRouterNavigate } from '../../../../../../../../../src/plugins/kibana_react/public'; interface Props { policies: SlmPolicy[]; - reload: () => Promise>; + reload: UseRequestResponse['resendRequest']; openPolicyDetailsUrl: (name: SlmPolicy['name']) => string; onPolicyDeleted: (policiesDeleted: Array) => void; onPolicyExecuted: () => void; diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx index 9afdad3806def..a3f57ce4fbf5e 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/repository_list/repository_list.tsx @@ -40,7 +40,7 @@ export const RepositoryList: React.FunctionComponent Promise>; + reload: UseRequestResponse['resendRequest']; openRepositoryDetailsUrl: (name: Repository['name']) => string; onRepositoryDeleted: (repositoriesDeleted: Array) => void; } diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_list.tsx index d7a82386926c1..d9507a101bbac 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/restore_list/restore_list.tsx @@ -52,9 +52,13 @@ export const RestoreList: React.FunctionComponent = () => { const [currentInterval, setCurrentInterval] = useState(INTERVAL_OPTIONS[1]); // Load restores - const { error, isLoading, data: restores = [], isInitialRequest, sendRequest } = useLoadRestores( - currentInterval - ); + const { + error, + isLoading, + data: restores = [], + isInitialRequest, + resendRequest, + } = useLoadRestores(currentInterval); const { uiMetricService, history } = useServices(); @@ -174,7 +178,7 @@ export const RestoreList: React.FunctionComponent = () => { key={interval} icon="empty" onClick={() => { - sendRequest(); + resendRequest(); setCurrentInterval(interval); setIsIntervalMenuOpen(false); }} diff --git a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx index d13188fc44730..97def33ffe8f6 100644 --- a/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx +++ b/x-pack/plugins/snapshot_restore/public/application/sections/home/snapshot_list/snapshot_list.tsx @@ -44,7 +44,7 @@ export const SnapshotList: React.FunctionComponent Promise>; + reload: UseRequestResponse['resendRequest']; openSnapshotDetailsUrl: (repositoryName: string, snapshotId: string) => string; repositoryFilter?: string; policyFilter?: string; diff --git a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts index 4314b703722f6..b371ec9f8fe82 100644 --- a/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts +++ b/x-pack/plugins/snapshot_restore/public/application/services/validation/validate_policy.ts @@ -41,6 +41,12 @@ export interface ValidatePolicyHelperData { * are not configuring this value - like when they are on a previous step. */ validateIndicesCount?: boolean; + + /** + * A policy might be configured with a repository that no longer exists. We want the form to + * block in this case because just having a repository configured is not enough for validity. + */ + repositoryDoesNotExist?: boolean; } export const validatePolicy = ( @@ -50,7 +56,13 @@ export const validatePolicy = ( const i18n = textService.i18n; const { name, snapshotName, schedule, repository, config, retention } = policy; - const { managedRepository, isEditing, policyName, validateIndicesCount } = validationHelperData; + const { + managedRepository, + isEditing, + policyName, + validateIndicesCount, + repositoryDoesNotExist, + } = validationHelperData; const validation: PolicyValidation = { isValid: true, @@ -99,7 +111,7 @@ export const validatePolicy = ( ); } - if (isStringEmpty(repository)) { + if (isStringEmpty(repository) || repositoryDoesNotExist) { validation.errors.repository.push( i18n.translate('xpack.snapshotRestore.policyValidation.repositoryRequiredErrorMessage', { defaultMessage: 'Repository is required.', diff --git a/x-pack/plugins/snapshot_restore/public/shared_imports.ts b/x-pack/plugins/snapshot_restore/public/shared_imports.ts index cad8ce147bd25..bd1c0e0cd395b 100644 --- a/x-pack/plugins/snapshot_restore/public/shared_imports.ts +++ b/x-pack/plugins/snapshot_restore/public/shared_imports.ts @@ -14,6 +14,7 @@ export { sendRequest, SendRequestConfig, SendRequestResponse, + UseRequestResponse, useAuthorizationContext, useRequest, UseRequestConfig, diff --git a/x-pack/plugins/snapshot_restore/server/plugin.ts b/x-pack/plugins/snapshot_restore/server/plugin.ts index cf7504363b823..411c0cb9a5ebd 100644 --- a/x-pack/plugins/snapshot_restore/server/plugin.ts +++ b/x-pack/plugins/snapshot_restore/server/plugin.ts @@ -20,7 +20,7 @@ import { ILegacyScopedClusterClient, } from 'kibana/server'; -import { PLUGIN } from '../common'; +import { PLUGIN, APP_REQUIRED_CLUSTER_PRIVILEGES } from '../common'; import { License } from './services'; import { ApiRoutes } from './routes'; import { wrapEsError } from './lib'; @@ -54,7 +54,7 @@ export class SnapshotRestoreServerPlugin implements Plugin public async setup( { http, getStartServices }: CoreSetup, - { licensing, security, cloud }: Dependencies + { licensing, features, security, cloud }: Dependencies ): Promise { const pluginConfig = await this.context.config .create() @@ -81,6 +81,19 @@ export class SnapshotRestoreServerPlugin implements Plugin } ); + features.registerElasticsearchFeature({ + id: PLUGIN.id, + management: { + data: [PLUGIN.id], + }, + privileges: [ + { + requiredClusterPrivileges: [...APP_REQUIRED_CLUSTER_PRIVILEGES], + ui: [], + }, + ], + }); + http.registerRouteHandlerContext('snapshotRestore', async (ctx, request) => { this.snapshotRestoreESClient = this.snapshotRestoreESClient ?? (await getCustomEsClient(getStartServices)); diff --git a/x-pack/plugins/snapshot_restore/server/types.ts b/x-pack/plugins/snapshot_restore/server/types.ts index 8cfcaec1a2cd1..eb51f086deacc 100644 --- a/x-pack/plugins/snapshot_restore/server/types.ts +++ b/x-pack/plugins/snapshot_restore/server/types.ts @@ -7,12 +7,14 @@ import { LegacyScopedClusterClient, IRouter } from 'src/core/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { SecurityPluginSetup } from '../../security/server'; import { CloudSetup } from '../../cloud/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { License } from './services'; import { wrapEsError } from './lib'; import { isEsError } from './shared_imports'; export interface Dependencies { licensing: LicensingPluginSetup; + features: FeaturesPluginSetup; security?: SecurityPluginSetup; cloud?: CloudSetup; } diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.tsx index 42fbf8954396e..c3e631e335ea7 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_mode_control.tsx @@ -37,8 +37,7 @@ const createNewCopiesDisabled = { tooltip: i18n.translate( 'xpack.spaces.management.copyToSpace.copyModeControl.createNewCopies.disabledText', { - defaultMessage: - 'Check if each object was previously copied or imported into the destination space.', + defaultMessage: 'Check if objects were previously copied or imported into the space.', } ), }; @@ -50,33 +49,36 @@ const createNewCopiesEnabled = { ), tooltip: i18n.translate( 'xpack.spaces.management.copyToSpace.copyModeControl.createNewCopies.enabledText', - { defaultMessage: 'All copied objects will be created with new random IDs.' } + { + defaultMessage: + 'Use this option to create one or more copies of the object in the same space.', + } ), }; const overwriteEnabled = { id: 'overwriteEnabled', label: i18n.translate( 'xpack.spaces.management.copyToSpace.copyModeControl.overwrite.enabledLabel', - { defaultMessage: 'Automatically try to overwrite conflicts' } + { defaultMessage: 'Automatically overwrite conflicts' } ), }; const overwriteDisabled = { id: 'overwriteDisabled', label: i18n.translate( 'xpack.spaces.management.copyToSpace.copyModeControl.overwrite.disabledLabel', - { defaultMessage: 'Request action when conflict occurs' } + { defaultMessage: 'Request action on conflict' } ), }; const includeRelated = { id: 'includeRelated', text: i18n.translate('xpack.spaces.management.copyToSpace.copyModeControl.includeRelated.title', { - defaultMessage: 'Include related saved objects', + defaultMessage: 'Include related objects', }), tooltip: i18n.translate( 'xpack.spaces.management.copyToSpace.copyModeControl.includeRelated.text', { defaultMessage: - 'This will copy any other objects this has references to -- for example, a dashboard may have references to multiple visualizations.', + 'Copy this object and its related objects. For dashboards, related visualizations, index patterns, and saved searches are also copied.', } ), }; @@ -86,7 +88,7 @@ const copyOptionsTitle = i18n.translate( ); const relationshipOptionsTitle = i18n.translate( 'xpack.spaces.management.copyToSpace.copyModeControl.relationshipOptionsTitle', - { defaultMessage: 'Relationship options' } + { defaultMessage: 'Relationship' } ); const createLabel = ({ text, tooltip }: { text: string; tooltip: string }) => ( diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_indicator.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_indicator.tsx index 158d7a9a43ef6..d9e1418cdd412 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_indicator.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_indicator.tsx @@ -5,7 +5,7 @@ */ import React, { Fragment } from 'react'; -import { EuiLoadingSpinner, EuiIconTip, EuiSpacer } from '@elastic/eui'; +import { EuiLoadingSpinner, EuiIconTip } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; import { ImportRetry } from '../types'; import { SummarizedCopyToSpaceResult, SummarizedSavedObjectResult } from '..'; @@ -36,13 +36,13 @@ export const CopyStatusIndicator = (props: Props) => { // the object was overwritten ) : ( // the object was not overwritten ); return ; @@ -53,19 +53,19 @@ export const CopyStatusIndicator = (props: Props) => { // this is an "automatic overwrite", e.g., the "Overwrite all conflicts" option was selected ) : pendingObjectRetry?.overwrite ? ( // this is a manual overwrite, e.g., the individual "Overwrite?" switch was enabled ) : ( // this object is pending success, but it will not result in an overwrite ); return ; @@ -80,7 +80,7 @@ export const CopyStatusIndicator = (props: Props) => { content={ } /> @@ -88,28 +88,38 @@ export const CopyStatusIndicator = (props: Props) => { } if (hasConflicts) { - return ( - - - - - - } - /> - ); + switch (conflict!.error.type) { + case 'conflict': + return ( + + + + } + /> + ); + case 'ambiguous_conflict': + return ( + + + + } + /> + ); + } } return hasMissingReferences ? ( @@ -121,17 +131,17 @@ export const CopyStatusIndicator = (props: Props) => { overwrite ? ( ) : conflict ? ( ) : ( ) } diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx index 4bc7e5cfaf31a..4dfb14e756cd6 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_status_summary_indicator.tsx @@ -55,7 +55,7 @@ const renderIcon = (props: Props) => { content={ } @@ -93,7 +93,7 @@ const renderIcon = (props: Props) => { content={ } @@ -119,7 +119,7 @@ const renderIcon = (props: Props) => { content={ } diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx index dfc908d81887a..e214db22aa9cc 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_flyout.test.tsx @@ -510,7 +510,7 @@ describe('CopyToSpaceFlyout', () => { Object { "color": "warning", "content": , @@ -604,7 +604,7 @@ describe('CopyToSpaceFlyout', () => { Object { "color": "danger", "content": , diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx index fdc8d8c73e324..551573feebcdb 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/copy_to_space_form.tsx @@ -51,7 +51,7 @@ export const CopyToSpaceForm = (props: Props) => { label={ } fullWidth diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx index ceaa1dc9f5e21..ab8fb0b438f67 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/processing_copy_to_space.tsx @@ -45,36 +45,30 @@ const renderCopyOptions = ({ createNewCopies, overwrite, includeRelated }: CopyO defaultMessage="Check for existing objects" /> ); - const overwriteLabel = overwrite ? ( - - ) : ( - - ); - const includeRelatedLabel = includeRelated ? ( - - ) : ( - - ); return ( {!createNewCopies && ( - + + } + /> )} - + + } + /> ); }; diff --git a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx index 2a8b5e660f38c..d4e12b31b5b4f 100644 --- a/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx +++ b/x-pack/plugins/spaces/public/copy_saved_objects_to_space/components/selectable_spaces_control.tsx @@ -31,7 +31,7 @@ export const SelectableSpacesControl = (props: Props) => { content={ } position="left" diff --git a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap index 6e422bc13f06b..b0d0933614d12 100644 --- a/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/components/confirm_delete_modal/__snapshots__/confirm_delete_modal.test.tsx.snap @@ -55,6 +55,7 @@ exports[`ConfirmDeleteModal renders as expected 1`] = ` labelType="label" > { })} > { values={{ rolesLink: ( diff --git a/x-pack/plugins/spaces/public/management/components/unauthorized_prompt/__snapshots__/unauthorized_prompt.test.tsx.snap b/x-pack/plugins/spaces/public/management/components/unauthorized_prompt/__snapshots__/unauthorized_prompt.test.tsx.snap index b53b4d6ec7caf..1e761a0f14fc9 100644 --- a/x-pack/plugins/spaces/public/management/components/unauthorized_prompt/__snapshots__/unauthorized_prompt.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/components/unauthorized_prompt/__snapshots__/unauthorized_prompt.test.tsx.snap @@ -7,7 +7,7 @@ exports[`UnauthorizedPrompt renders as expected 1`] = ` data-test-subj="permissionDeniedMessage" > diff --git a/x-pack/plugins/spaces/public/management/components/unauthorized_prompt/unauthorized_prompt.tsx b/x-pack/plugins/spaces/public/management/components/unauthorized_prompt/unauthorized_prompt.tsx index dd3f96d94eab4..50f10ec3860df 100644 --- a/x-pack/plugins/spaces/public/management/components/unauthorized_prompt/unauthorized_prompt.tsx +++ b/x-pack/plugins/spaces/public/management/components/unauthorized_prompt/unauthorized_prompt.tsx @@ -24,7 +24,7 @@ export const UnauthorizedPrompt = () => (

} diff --git a/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap b/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap index 269b2b6908183..058b9ecdd0f8f 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/edit_space/customize_space/__snapshots__/customize_space_avatar.test.tsx.snap @@ -14,6 +14,7 @@ exports[`renders without crashing 1`] = ` labelType="label" > { > { )} { fullWidth > { > { > { > { id="xpack.spaces.management.spaceIdentifier.urlIdentifierLabel" defaultMessage="URL identifier " /> - + {editLinkText}

diff --git a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap index 43ae75b74c882..3835fa085c26e 100644 --- a/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/edit_space/enabled_features/__snapshots__/enabled_features.test.tsx.snap @@ -68,6 +68,7 @@ exports[`EnabledFeatures renders as expected 1`] = ` values={ Object { "rolesLink": ; - features: FeatureConfig[]; + features: KibanaFeatureConfig[]; securityEnabled: boolean; onChange: (space: Partial) => void; getUrlForApp: ApplicationStart['getUrlForApp']; @@ -139,6 +139,7 @@ export class EnabledFeatures extends Component { values={{ rolesLink: ( ; - features: FeatureConfig[]; + features: KibanaFeatureConfig[]; onChange: (space: Partial) => void; } @@ -70,8 +70,8 @@ export class FeatureTable extends Component { defaultMessage: 'Feature', }), render: ( - feature: FeatureConfig, - _item: { feature: FeatureConfig; space: Props['space'] } + feature: KibanaFeatureConfig, + _item: { feature: KibanaFeatureConfig; space: Props['space'] } ) => { return ( @@ -101,6 +101,7 @@ export class FeatureTable extends Component { return ( { return ( ; - features: Feature[]; + features: KibanaFeature[]; originalSpace?: Partial; showAlteringActiveSpaceDialog: boolean; isLoading: boolean; @@ -312,7 +312,7 @@ export class ManageSpacePage extends Component { } }; - private loadSpace = async (spaceId: string, featuresPromise: Promise) => { + private loadSpace = async (spaceId: string, featuresPromise: Promise) => { const { spacesManager, onLoadSpace } = this.props; try { diff --git a/x-pack/plugins/spaces/public/management/lib/feature_utils.test.ts b/x-pack/plugins/spaces/public/management/lib/feature_utils.test.ts index 20d419e5c90e4..212ffe96cdbf6 100644 --- a/x-pack/plugins/spaces/public/management/lib/feature_utils.test.ts +++ b/x-pack/plugins/spaces/public/management/lib/feature_utils.test.ts @@ -5,7 +5,7 @@ */ import { getEnabledFeatures } from './feature_utils'; -import { FeatureConfig } from '../../../../features/public'; +import { KibanaFeatureConfig } from '../../../../features/public'; const buildFeatures = () => [ @@ -25,7 +25,7 @@ const buildFeatures = () => id: 'feature4', name: 'feature 4', }, - ] as FeatureConfig[]; + ] as KibanaFeatureConfig[]; const buildSpace = (disabledFeatures = [] as string[]) => ({ id: 'space', diff --git a/x-pack/plugins/spaces/public/management/lib/feature_utils.ts b/x-pack/plugins/spaces/public/management/lib/feature_utils.ts index 273ea7e60bc5e..c6f7031976a9b 100644 --- a/x-pack/plugins/spaces/public/management/lib/feature_utils.ts +++ b/x-pack/plugins/spaces/public/management/lib/feature_utils.ts @@ -4,10 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { FeatureConfig } from '../../../../features/common'; +import { KibanaFeatureConfig } from '../../../../features/common'; import { Space } from '../..'; -export function getEnabledFeatures(features: FeatureConfig[], space: Partial) { +export function getEnabledFeatures(features: KibanaFeatureConfig[], space: Partial) { return features.filter((feature) => !(space.disabledFeatures || []).includes(feature.id)); } diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap b/x-pack/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap index 200c9c55a4f7c..f4f92a26028ad 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap +++ b/x-pack/plugins/spaces/public/management/spaces_grid/__snapshots__/spaces_grid_pages.test.tsx.snap @@ -38,6 +38,7 @@ exports[`SpacesGridPage renders as expected 1`] = ` grow={false} > diff --git a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx index a98fae2561827..b40f34273d99f 100644 --- a/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx +++ b/x-pack/plugins/spaces/public/management/spaces_grid/spaces_grid_page.tsx @@ -21,7 +21,7 @@ import { import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n/react'; import { ApplicationStart, Capabilities, NotificationsStart, ScopedHistory } from 'src/core/public'; -import { Feature, FeaturesPluginStart } from '../../../../features/public'; +import { KibanaFeature, FeaturesPluginStart } from '../../../../features/public'; import { isReservedSpace } from '../../../common'; import { DEFAULT_SPACE_ID } from '../../../common/constants'; import { Space } from '../../../common/model/space'; @@ -46,7 +46,7 @@ interface Props { interface State { spaces: Space[]; - features: Feature[]; + features: KibanaFeature[]; loading: boolean; showConfirmDeleteModal: boolean; selectedSpace: Space | null; @@ -140,7 +140,11 @@ export class SpacesGridPage extends Component { public getPrimaryActionButton() { return ( - + { { render: (record: Space) => ( { available: (record: Space) => !isReservedSpace(record), render: (record: Space) => ( diff --git a/x-pack/plugins/spaces/public/nav_control/components/manage_spaces_button.tsx b/x-pack/plugins/spaces/public/nav_control/components/manage_spaces_button.tsx index 98467ebdb790b..6ac637d6b31d6 100644 --- a/x-pack/plugins/spaces/public/nav_control/components/manage_spaces_button.tsx +++ b/x-pack/plugins/spaces/public/nav_control/components/manage_spaces_button.tsx @@ -32,6 +32,7 @@ export class ManageSpacesButton extends Component { isDisabled={this.props.isDisabled} onClick={this.navigateToManageSpaces} style={this.props.style} + data-test-subj="manageSpaces" > { return ( { const title = currentNamespaces.length === 1 ? i18n.translate('xpack.spaces.management.shareToSpace.shareNewSuccessTitle', { - defaultMessage: 'Saved Object is now shared!', + defaultMessage: 'Object is now shared', }) : i18n.translate('xpack.spaces.management.shareToSpace.shareEditSuccessTitle', { - defaultMessage: 'Saved Object updated', + defaultMessage: 'Object was updated', }); if (spacesToAdd.length > 0) { await spacesManager.shareSavedObjectAdd({ type, id }, spacesToAdd); const spaceNames = spacesToAdd.map( (spaceId) => spaces.find((space) => space.id === spaceId)!.name ); - const text = i18n.translate('xpack.spaces.management.shareToSpace.shareAddSuccessText', { - defaultMessage: `'{object}' was added to the following spaces:\n{spaces}`, - values: { object: meta.title, spaces: spaceNames.join(', ') }, - }); + const spaceCount = spaceNames.length; + const text = + spaceCount === 1 + ? i18n.translate('xpack.spaces.management.shareToSpace.shareAddSuccessTextSingular', { + defaultMessage: `'{object}' was added to 1 space.`, + values: { object: meta.title }, + }) + : i18n.translate('xpack.spaces.management.shareToSpace.shareAddSuccessTextPlural', { + defaultMessage: `'{object}' was added to {spaceCount} spaces.`, + values: { object: meta.title, spaceCount }, + }); toastNotifications.addSuccess({ title, text }); } if (spacesToRemove.length > 0) { @@ -125,10 +132,20 @@ export const ShareSavedObjectsToSpaceFlyout = (props: Props) => { const spaceNames = spacesToRemove.map( (spaceId) => spaces.find((space) => space.id === spaceId)!.name ); - const text = i18n.translate('xpack.spaces.management.shareToSpace.shareRemoveSuccessText', { - defaultMessage: `'{object}' was removed from the following spaces:\n{spaces}`, - values: { object: meta.title, spaces: spaceNames.join(', ') }, - }); + const spaceCount = spaceNames.length; + const text = + spaceCount === 1 + ? i18n.translate( + 'xpack.spaces.management.shareToSpace.shareRemoveSuccessTextSingular', + { + defaultMessage: `'{object}' was removed from 1 space.`, + values: { object: meta.title }, + } + ) + : i18n.translate('xpack.spaces.management.shareToSpace.shareRemoveSuccessTextPlural', { + defaultMessage: `'{object}' was removed from {spaceCount} spaces.`, + values: { object: meta.title, spaceCount }, + }); toastNotifications.addSuccess({ title, text }); } onObjectUpdated(); @@ -210,7 +227,7 @@ export const ShareSavedObjectsToSpaceFlyout = (props: Props) => {

diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.tsx index 24402fec8d771..ad84ea85d5e54 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/components/share_to_space_form.tsx @@ -35,14 +35,14 @@ export const ShareToSpaceForm = (props: Props) => { title={ } color="warning" > { label={ } labelAppend={ diff --git a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx index e8649faa120be..93d7bb0170519 100644 --- a/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx +++ b/x-pack/plugins/spaces/public/share_saved_objects_to_space/share_saved_objects_to_space_column.tsx @@ -52,7 +52,7 @@ const ColumnDisplay = ({ namespaces, data }: ColumnDataProps) => { const unauthorizedCount = (namespaces?.filter((namespace) => namespace === '?') ?? []).length; const unauthorizedTooltip = i18n.translate( 'xpack.spaces.management.shareToSpace.columnUnauthorizedLabel', - { defaultMessage: 'You do not have permission to view these spaces' } + { defaultMessage: `You don't have permission to view these spaces.` } ); const displayedSpaces = isExpanded diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts index 8678bdceb70f9..b0b89afa79d5d 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.test.ts @@ -10,6 +10,9 @@ describe('Capabilities provider', () => { it('provides the expected capabilities', () => { expect(capabilitiesProvider()).toMatchInlineSnapshot(` Object { + "catalogue": Object { + "spaces": true, + }, "management": Object { "kibana": Object { "spaces": true, diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts index 5976aabfa66e8..1aaf2ad1df925 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_provider.ts @@ -8,6 +8,9 @@ export const capabilitiesProvider = () => ({ spaces: { manage: true, }, + catalogue: { + spaces: true, + }, management: { kibana: { spaces: true, diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts index c9ea1b44e723d..bf0b51b7e2503 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { Feature } from '../../../../plugins/features/server'; +import { KibanaFeature } from '../../../../plugins/features/server'; import { Space } from '../../common/model/space'; import { setupCapabilitiesSwitcher } from './capabilities_switcher'; import { Capabilities, CoreSetup } from 'src/core/server'; @@ -80,7 +80,7 @@ const features = ([ }, }, }, -] as unknown) as Feature[]; +] as unknown) as KibanaFeature[]; const buildCapabilities = () => Object.freeze({ @@ -121,7 +121,7 @@ const setup = (space: Space) => { const coreSetup = coreMock.createSetup(); const featuresStart = featuresPluginMock.createStart(); - featuresStart.getFeatures.mockReturnValue(features); + featuresStart.getKibanaFeatures.mockReturnValue(features); coreSetup.getStartServices.mockResolvedValue([ coreMock.createStart(), diff --git a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts index e8d964b22010c..8b0b955c40d92 100644 --- a/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts +++ b/x-pack/plugins/spaces/server/capabilities/capabilities_switcher.ts @@ -5,7 +5,7 @@ */ import _ from 'lodash'; import { Capabilities, CapabilitiesSwitcher, CoreSetup, Logger } from 'src/core/server'; -import { Feature } from '../../../../plugins/features/server'; +import { KibanaFeature } from '../../../../plugins/features/server'; import { Space } from '../../common/model/space'; import { SpacesServiceSetup } from '../spaces_service'; import { PluginsStart } from '../plugin'; @@ -28,7 +28,7 @@ export function setupCapabilitiesSwitcher( core.getStartServices(), ]); - const registeredFeatures = features.getFeatures(); + const registeredFeatures = features.getKibanaFeatures(); // try to retrieve capabilities for authenticated or "maybe authenticated" users return toggleCapabilities(registeredFeatures, capabilities, activeSpace); @@ -39,7 +39,11 @@ export function setupCapabilitiesSwitcher( }; } -function toggleCapabilities(features: Feature[], capabilities: Capabilities, activeSpace: Space) { +function toggleCapabilities( + features: KibanaFeature[], + capabilities: Capabilities, + activeSpace: Space +) { const clonedCapabilities = _.cloneDeep(capabilities); toggleDisabledFeatures(features, clonedCapabilities, activeSpace); @@ -48,7 +52,7 @@ function toggleCapabilities(features: Feature[], capabilities: Capabilities, act } function toggleDisabledFeatures( - features: Feature[], + features: KibanaFeature[], capabilities: Capabilities, activeSpace: Space ) { @@ -61,7 +65,7 @@ function toggleDisabledFeatures( } return [[...acc[0], feature], acc[1]]; }, - [[], []] as [Feature[], Feature[]] + [[], []] as [KibanaFeature[], KibanaFeature[]] ); const navLinks = capabilities.navLinks; diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts index d49dfa2015dc6..1cec7b769fa26 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/copy_to_spaces.test.ts @@ -20,6 +20,7 @@ import { copySavedObjectsToSpacesFactory } from './copy_to_spaces'; jest.mock('../../../../../../src/core/server', () => { return { + ...(jest.requireActual('../../../../../../src/core/server') as Record), exportSavedObjectsToStream: jest.fn(), importSavedObjectsFromStream: jest.fn(), }; diff --git a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts index 6a77bf7397cb5..37181c9d81649 100644 --- a/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts +++ b/x-pack/plugins/spaces/server/lib/copy_to_spaces/resolve_copy_conflicts.test.ts @@ -20,6 +20,7 @@ import { resolveCopySavedObjectsToSpacesConflictsFactory } from './resolve_copy_ jest.mock('../../../../../../src/core/server', () => { return { + ...(jest.requireActual('../../../../../../src/core/server') as Record), exportSavedObjectsToStream: jest.fn(), resolveSavedObjectsImportErrors: jest.fn(), }; diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts index dabdcf553edb4..fe1acd93570f6 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.test.ts @@ -25,7 +25,7 @@ import { SpacesService } from '../../spaces_service'; import { SpacesAuditLogger } from '../audit_logger'; import { convertSavedObjectToSpace } from '../../routes/lib'; import { initSpacesOnPostAuthRequestInterceptor } from './on_post_auth_interceptor'; -import { Feature } from '../../../../features/server'; +import { KibanaFeature } from '../../../../features/server'; import { spacesConfig } from '../__fixtures__'; import { securityMock } from '../../../../security/server/mocks'; import { featuresPluginMock } from '../../../../features/server/mocks'; @@ -124,7 +124,7 @@ describe.skip('onPostAuthInterceptor', () => { const loggingMock = loggingSystemMock.create().asLoggerFactory().get('xpack', 'spaces'); const featuresPlugin = featuresPluginMock.createSetup(); - featuresPlugin.getFeatures.mockReturnValue(([ + featuresPlugin.getKibanaFeatures.mockReturnValue(([ { id: 'feature-1', name: 'feature 1', @@ -145,7 +145,7 @@ describe.skip('onPostAuthInterceptor', () => { name: 'feature 4', app: ['kibana'], }, - ] as unknown) as Feature[]); + ] as unknown) as KibanaFeature[]); const mockRepository = jest.fn().mockImplementation(() => { return { diff --git a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts index 3d6084d37a384..e4ca0f8072f96 100644 --- a/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts +++ b/x-pack/plugins/spaces/server/lib/request_interceptors/on_post_auth_interceptor.ts @@ -108,7 +108,7 @@ export function initSpacesOnPostAuthRequestInterceptor({ if (appId !== 'kibana' && space && space.disabledFeatures.length > 0) { log.debug(`Verifying application is available: "${appId}"`); - const allFeatures = features.getFeatures(); + const allFeatures = features.getKibanaFeatures(); const isRegisteredApp = allFeatures.some((feature) => feature.app.includes(appId)); if (isRegisteredApp) { diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts index 90ce2b01bfd20..1090b029069d2 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.test.ts @@ -260,10 +260,12 @@ describe('#getAll', () => { mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); mockCheckPrivilegesAtSpaces.mockReturnValue({ username, - privileges: [ - { resource: savedObjects[0].id, privilege, authorized: false }, - { resource: savedObjects[1].id, privilege, authorized: false }, - ], + privileges: { + kibana: [ + { resource: savedObjects[0].id, privilege, authorized: false }, + { resource: savedObjects[1].id, privilege, authorized: false }, + ], + }, }); const maxSpaces = 1234; const mockConfig = createMockConfig({ @@ -298,7 +300,7 @@ describe('#getAll', () => { expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledWith( savedObjects.map((savedObject) => savedObject.id), - [privilege] + { kibana: [privilege] } ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith( username, @@ -318,10 +320,12 @@ describe('#getAll', () => { mockAuthorization.mode.useRbacForRequest.mockReturnValue(true); mockCheckPrivilegesAtSpaces.mockReturnValue({ username, - privileges: [ - { resource: savedObjects[0].id, privilege, authorized: true }, - { resource: savedObjects[1].id, privilege, authorized: false }, - ], + privileges: { + kibana: [ + { resource: savedObjects[0].id, privilege, authorized: true }, + { resource: savedObjects[1].id, privilege, authorized: false }, + ], + }, }); const mockInternalRepository = { find: jest.fn().mockReturnValue({ @@ -357,7 +361,7 @@ describe('#getAll', () => { expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); expect(mockCheckPrivilegesAtSpaces).toHaveBeenCalledWith( savedObjects.map((savedObject) => savedObject.id), - [privilege] + { kibana: [privilege] } ); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith( @@ -451,9 +455,9 @@ describe('#canEnumerateSpaces', () => { expect(canEnumerateSpaces).toEqual(false); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); @@ -486,9 +490,9 @@ describe('#canEnumerateSpaces', () => { expect(canEnumerateSpaces).toEqual(true); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); @@ -603,7 +607,9 @@ describe('#get', () => { await expect(client.get(id)).rejects.toThrowErrorMatchingSnapshot(); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesAtSpace).toHaveBeenCalledWith(id, mockAuthorization.actions.login); + expect(mockCheckPrivilegesAtSpace).toHaveBeenCalledWith(id, { + kibana: mockAuthorization.actions.login, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'get', [ id, ]); @@ -641,7 +647,9 @@ describe('#get', () => { expect(space).toEqual(expectedSpace); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesAtSpace).toHaveBeenCalledWith(id, mockAuthorization.actions.login); + expect(mockCheckPrivilegesAtSpace).toHaveBeenCalledWith(id, { + kibana: mockAuthorization.actions.login, + }); expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'get', [ @@ -886,9 +894,9 @@ describe('#create', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'create'); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); }); @@ -939,9 +947,9 @@ describe('#create', () => { }); expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'create'); }); @@ -989,9 +997,9 @@ describe('#create', () => { expect(mockInternalRepository.create).not.toHaveBeenCalled(); expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'create'); }); @@ -1128,9 +1136,9 @@ describe('#update', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'update'); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); }); @@ -1167,9 +1175,9 @@ describe('#update', () => { expect(actualSpace).toEqual(expectedReturnedSpace); expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockInternalRepository.update).toHaveBeenCalledWith('space', id, attributes); expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); @@ -1353,9 +1361,9 @@ describe('#delete', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledWith(username, 'delete'); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledTimes(0); }); @@ -1389,9 +1397,9 @@ describe('#delete', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); expect(mockAuditLogger.spacesAuthorizationFailure).toHaveBeenCalledTimes(0); expect(mockAuditLogger.spacesAuthorizationSuccess).toHaveBeenCalledWith(username, 'delete'); @@ -1429,9 +1437,9 @@ describe('#delete', () => { expect(mockAuthorization.mode.useRbacForRequest).toHaveBeenCalledWith(request); expect(mockAuthorization.checkPrivilegesWithRequest).toHaveBeenCalledWith(request); - expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith( - mockAuthorization.actions.space.manage - ); + expect(mockCheckPrivilegesGlobally).toHaveBeenCalledWith({ + kibana: mockAuthorization.actions.space.manage, + }); expect(mockInternalRepository.get).toHaveBeenCalledWith('space', id); expect(mockInternalRepository.delete).toHaveBeenCalledWith('space', id); expect(mockInternalRepository.deleteByNamespace).toHaveBeenCalledWith(id); diff --git a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts index b1d6e3200ab3a..acb00a87bf7d9 100644 --- a/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts +++ b/x-pack/plugins/spaces/server/lib/spaces_client/spaces_client.ts @@ -50,9 +50,9 @@ export class SpacesClient { public async canEnumerateSpaces(): Promise { if (this.useRbac()) { const checkPrivileges = this.authorization!.checkPrivilegesWithRequest(this.request); - const { hasAllRequested } = await checkPrivileges.globally( - this.authorization!.actions.space.manage - ); + const { hasAllRequested } = await checkPrivileges.globally({ + kibana: this.authorization!.actions.space.manage, + }); this.debugLogger(`SpacesClient.canEnumerateSpaces, using RBAC. Result: ${hasAllRequested}`); return hasAllRequested; } @@ -87,9 +87,11 @@ export class SpacesClient { const privilege = privilegeFactory(this.authorization!); - const { username, privileges } = await checkPrivileges.atSpaces(spaceIds, privilege); + const { username, privileges } = await checkPrivileges.atSpaces(spaceIds, { + kibana: privilege, + }); - const authorized = privileges.filter((x) => x.authorized).map((x) => x.resource); + const authorized = privileges.kibana.filter((x) => x.authorized).map((x) => x.resource); this.debugLogger( `SpacesClient.getAll(), authorized for ${ @@ -234,7 +236,7 @@ export class SpacesClient { private async ensureAuthorizedGlobally(action: string, method: string, forbiddenMessage: string) { const checkPrivileges = this.authorization!.checkPrivilegesWithRequest(this.request); - const { username, hasAllRequested } = await checkPrivileges.globally(action); + const { username, hasAllRequested } = await checkPrivileges.globally({ kibana: action }); if (hasAllRequested) { this.auditLogger.spacesAuthorizationSuccess(username, method); @@ -252,7 +254,9 @@ export class SpacesClient { forbiddenMessage: string ) { const checkPrivileges = this.authorization!.checkPrivilegesWithRequest(this.request); - const { username, hasAllRequested } = await checkPrivileges.atSpace(spaceId, action); + const { username, hasAllRequested } = await checkPrivileges.atSpace(spaceId, { + kibana: action, + }); if (hasAllRequested) { this.auditLogger.spacesAuthorizationSuccess(username, method, [spaceId]); diff --git a/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts b/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts new file mode 100644 index 0000000000000..2b93e6d87a7af --- /dev/null +++ b/x-pack/plugins/spaces/server/lib/utils/__mocks__/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +const mockNamespaceIdToString = jest.fn(); +const mockNamespaceStringToId = jest.fn(); +jest.mock('../../../../../../../src/core/server', () => ({ + SavedObjectsUtils: { + namespaceIdToString: mockNamespaceIdToString, + namespaceStringToId: mockNamespaceStringToId, + }, +})); + +export { mockNamespaceIdToString, mockNamespaceStringToId }; diff --git a/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts b/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts index a81a5f3cee187..79d3dda301045 100644 --- a/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts +++ b/x-pack/plugins/spaces/server/lib/utils/namespace.test.ts @@ -4,45 +4,29 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { mockNamespaceIdToString, mockNamespaceStringToId } from './__mocks__'; import { spaceIdToNamespace, namespaceToSpaceId } from './namespace'; -describe('#spaceIdToNamespace', () => { - it('converts the default space to undefined', () => { - expect(spaceIdToNamespace(DEFAULT_SPACE_ID)).toBeUndefined(); - }); - - it('returns non-default spaces as-is', () => { - expect(spaceIdToNamespace('foo')).toEqual('foo'); - }); - - it('throws an error when a spaceId is not provided', () => { - // @ts-ignore ts knows this isn't right - expect(() => spaceIdToNamespace()).toThrowErrorMatchingInlineSnapshot(`"spaceId is required"`); +beforeEach(() => { + jest.clearAllMocks(); +}); - // @ts-ignore ts knows this isn't right - expect(() => spaceIdToNamespace(null)).toThrowErrorMatchingInlineSnapshot( - `"spaceId is required"` - ); +describe('#spaceIdToNamespace', () => { + it('returns result of namespaceStringToId', () => { + mockNamespaceStringToId.mockReturnValue('bar'); - expect(() => spaceIdToNamespace('')).toThrowErrorMatchingInlineSnapshot( - `"spaceId is required"` - ); + const result = spaceIdToNamespace('foo'); + expect(mockNamespaceStringToId).toHaveBeenCalledWith('foo'); + expect(result).toEqual('bar'); }); }); describe('#namespaceToSpaceId', () => { - it('returns the default space id for undefined namespaces', () => { - expect(namespaceToSpaceId(undefined)).toEqual(DEFAULT_SPACE_ID); - }); - - it('returns all other namespaces as-is', () => { - expect(namespaceToSpaceId('foo')).toEqual('foo'); - }); + it('returns result of namespaceIdToString', () => { + mockNamespaceIdToString.mockReturnValue('bar'); - it('throws an error when an empty string is provided', () => { - expect(() => namespaceToSpaceId('')).toThrowErrorMatchingInlineSnapshot( - `"namespace cannot be an empty string"` - ); + const result = namespaceToSpaceId('foo'); + expect(mockNamespaceIdToString).toHaveBeenCalledWith('foo'); + expect(result).toEqual('bar'); }); }); diff --git a/x-pack/plugins/spaces/server/lib/utils/namespace.ts b/x-pack/plugins/spaces/server/lib/utils/namespace.ts index 8c7ed2ea1797d..344da18846f3b 100644 --- a/x-pack/plugins/spaces/server/lib/utils/namespace.ts +++ b/x-pack/plugins/spaces/server/lib/utils/namespace.ts @@ -4,28 +4,22 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DEFAULT_SPACE_ID } from '../../../common/constants'; +import { SavedObjectsUtils } from '../../../../../../src/core/server'; -export function spaceIdToNamespace(spaceId: string): string | undefined { - if (!spaceId) { - throw new TypeError('spaceId is required'); - } - - if (spaceId === DEFAULT_SPACE_ID) { - return undefined; - } - - return spaceId; +/** + * Converts a Space ID string to its namespace ID representation. Note that a Space ID string is equivalent to a namespace string. + * + * See also: {@link namespaceStringToId}. + */ +export function spaceIdToNamespace(spaceId: string) { + return SavedObjectsUtils.namespaceStringToId(spaceId); } -export function namespaceToSpaceId(namespace: string | undefined): string { - if (namespace === '') { - throw new TypeError('namespace cannot be an empty string'); - } - - if (!namespace) { - return DEFAULT_SPACE_ID; - } - - return namespace; +/** + * Converts a namespace ID to its Space ID string representation. Note that a Space ID string is equivalent to a namespace string. + * + * See also: {@link namespaceIdToString}. + */ +export function namespaceToSpaceId(namespace?: string) { + return SavedObjectsUtils.namespaceIdToString(namespace); } diff --git a/x-pack/plugins/spaces/server/plugin.test.ts b/x-pack/plugins/spaces/server/plugin.test.ts index a82f2370cc124..b650a114ed978 100644 --- a/x-pack/plugins/spaces/server/plugin.test.ts +++ b/x-pack/plugins/spaces/server/plugin.test.ts @@ -8,14 +8,14 @@ import { CoreSetup } from 'src/core/server'; import { coreMock } from 'src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; import { licensingMock } from '../../licensing/server/mocks'; -import { Plugin, PluginsSetup } from './plugin'; +import { Plugin, PluginsStart } from './plugin'; import { usageCollectionPluginMock } from '../../../../src/plugins/usage_collection/server/mocks'; describe('Spaces Plugin', () => { describe('#setup', () => { it('can setup with all optional plugins disabled, exposing the expected contract', async () => { const initializerContext = coreMock.createPluginInitializerContext({}); - const core = coreMock.createSetup() as CoreSetup; + const core = coreMock.createSetup() as CoreSetup; const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); @@ -38,7 +38,7 @@ describe('Spaces Plugin', () => { it('registers the capabilities provider and switcher', async () => { const initializerContext = coreMock.createPluginInitializerContext({}); - const core = coreMock.createSetup() as CoreSetup; + const core = coreMock.createSetup() as CoreSetup; const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); @@ -52,7 +52,7 @@ describe('Spaces Plugin', () => { it('registers the usage collector', async () => { const initializerContext = coreMock.createPluginInitializerContext({}); - const core = coreMock.createSetup() as CoreSetup; + const core = coreMock.createSetup() as CoreSetup; const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); @@ -67,7 +67,7 @@ describe('Spaces Plugin', () => { it('registers the "space" saved object type and client wrapper', async () => { const initializerContext = coreMock.createPluginInitializerContext({}); - const core = coreMock.createSetup() as CoreSetup; + const core = coreMock.createSetup() as CoreSetup; const features = featuresPluginMock.createSetup(); const licensing = licensingMock.createSetup(); diff --git a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts index bec3a5dcb0b71..dce6de908cfcb 100644 --- a/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts +++ b/x-pack/plugins/spaces/server/routes/api/external/copy_to_space.test.ts @@ -30,10 +30,10 @@ import { securityMock } from '../../../../../security/server/mocks'; import { ObjectType } from '@kbn/config-schema'; jest.mock('../../../../../../../src/core/server', () => { return { + ...(jest.requireActual('../../../../../../../src/core/server') as Record), exportSavedObjectsToStream: jest.fn(), importSavedObjectsFromStream: jest.fn(), resolveSavedObjectsImportErrors: jest.fn(), - kibanaResponseFactory: jest.requireActual('src/core/server').kibanaResponseFactory, }; }); import { diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts index 57ec688ab70e8..fddd7f92b7f27 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.test.ts @@ -7,18 +7,18 @@ import { getSpacesUsageCollector, UsageStats } from './spaces_usage_collector'; import * as Rx from 'rxjs'; import { PluginsSetup } from '../plugin'; -import { Feature } from '../../../features/server'; +import { KibanaFeature } from '../../../features/server'; import { ILicense, LicensingPluginSetup } from '../../../licensing/server'; import { pluginInitializerContextConfigMock } from 'src/core/server/mocks'; interface SetupOpts { license?: Partial; - features?: Feature[]; + features?: KibanaFeature[]; } function setup({ license = { isAvailable: true }, - features = [{ id: 'feature1' } as Feature, { id: 'feature2' } as Feature], + features = [{ id: 'feature1' } as KibanaFeature, { id: 'feature2' } as KibanaFeature], }: SetupOpts = {}) { class MockUsageCollector { private fetch: any; @@ -37,7 +37,7 @@ function setup({ } as LicensingPluginSetup; const featuresSetup = ({ - getFeatures: jest.fn().mockReturnValue(features), + getKibanaFeatures: jest.fn().mockReturnValue(features), } as unknown) as PluginsSetup['features']; return { diff --git a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts index 3ea4693d9e9d7..36d46c3d01baf 100644 --- a/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts +++ b/x-pack/plugins/spaces/server/usage_collection/spaces_usage_collector.ts @@ -46,7 +46,7 @@ async function getSpacesUsage( return null; } - const knownFeatureIds = features.getFeatures().map((feature) => feature.id); + const knownFeatureIds = features.getKibanaFeatures().map((feature) => feature.id); let resp: SpacesAggregationResponse | undefined; try { diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 2435d8a9aaf04..904b14a7459ad 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -51,6 +51,27 @@ } } }, + "enterprise_search": { + "properties": { + "ui_viewed": { + "properties": { + "overview": { + "type": "long" + } + } + }, + "ui_clicked": { + "properties": { + "app_search": { + "type": "long" + }, + "workplace_search": { + "type": "long" + } + } + } + } + }, "workplace_search": { "properties": { "ui_viewed": { @@ -276,6 +297,28 @@ } } }, + "security": { + "properties": { + "auditLoggingEnabled": { + "type": "boolean" + }, + "loginSelectorEnabled": { + "type": "boolean" + }, + "accessAgreementEnabled": { + "type": "boolean" + }, + "authProviderCount": { + "type": "number" + }, + "enabledAuthProviders": { + "type": "keyword" + }, + "httpAuthSchemes": { + "type": "keyword" + } + } + }, "spaces": { "properties": { "usesFeatureControls": { diff --git a/x-pack/plugins/infra/server/lib/snapshot/constants.ts b/x-pack/plugins/transform/common/api_schemas/audit_messages.ts similarity index 65% rename from x-pack/plugins/infra/server/lib/snapshot/constants.ts rename to x-pack/plugins/transform/common/api_schemas/audit_messages.ts index 0420878dbcf50..76e63af262674 100644 --- a/x-pack/plugins/infra/server/lib/snapshot/constants.ts +++ b/x-pack/plugins/transform/common/api_schemas/audit_messages.ts @@ -4,6 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -// TODO: Make SNAPSHOT_COMPOSITE_REQUEST_SIZE configurable from kibana.yml +import { TransformMessage } from '../types/messages'; -export const SNAPSHOT_COMPOSITE_REQUEST_SIZE = 75; +export type GetTransformsAuditMessagesResponseSchema = TransformMessage[]; diff --git a/x-pack/plugins/transform/common/api_schemas/common.ts b/x-pack/plugins/transform/common/api_schemas/common.ts new file mode 100644 index 0000000000000..80b14ce6adee8 --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/common.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +import { TRANSFORM_STATE } from '../constants'; + +export const transformIdsSchema = schema.arrayOf( + schema.object({ + id: schema.string(), + }) +); + +export type TransformIdsSchema = TypeOf; + +export const transformStateSchema = schema.oneOf([ + schema.literal(TRANSFORM_STATE.ABORTING), + schema.literal(TRANSFORM_STATE.FAILED), + schema.literal(TRANSFORM_STATE.INDEXING), + schema.literal(TRANSFORM_STATE.STARTED), + schema.literal(TRANSFORM_STATE.STOPPED), + schema.literal(TRANSFORM_STATE.STOPPING), +]); + +export const indexPatternTitleSchema = schema.object({ + /** Title of the index pattern for which to return stats. */ + indexPatternTitle: schema.string(), +}); + +export type IndexPatternTitleSchema = TypeOf; + +export const transformIdParamSchema = schema.object({ + transformId: schema.string(), +}); + +export type TransformIdParamSchema = TypeOf; + +export interface ResponseStatus { + success: boolean; + error?: any; +} + +export interface CommonResponseStatusSchema { + [key: string]: ResponseStatus; +} diff --git a/x-pack/plugins/transform/common/api_schemas/delete_transforms.ts b/x-pack/plugins/transform/common/api_schemas/delete_transforms.ts new file mode 100644 index 0000000000000..c4d1a1f5f7587 --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/delete_transforms.ts @@ -0,0 +1,37 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +import { transformStateSchema, ResponseStatus } from './common'; + +export const deleteTransformsRequestSchema = schema.object({ + /** + * Delete Transform & Destination Index + */ + transformsInfo: schema.arrayOf( + schema.object({ + id: schema.string(), + state: transformStateSchema, + }) + ), + deleteDestIndex: schema.maybe(schema.boolean()), + deleteDestIndexPattern: schema.maybe(schema.boolean()), + forceDelete: schema.maybe(schema.boolean()), +}); + +export type DeleteTransformsRequestSchema = TypeOf; + +export interface DeleteTransformStatus { + transformDeleted: ResponseStatus; + destIndexDeleted?: ResponseStatus; + destIndexPatternDeleted?: ResponseStatus; + destinationIndex?: string | undefined; +} + +export interface DeleteTransformsResponseSchema { + [key: string]: DeleteTransformStatus; +} diff --git a/x-pack/plugins/transform/common/api_schemas/field_histograms.ts b/x-pack/plugins/transform/common/api_schemas/field_histograms.ts new file mode 100644 index 0000000000000..3bdbb5f1ff702 --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/field_histograms.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +export const fieldHistogramsRequestSchema = schema.object({ + /** Query to match documents in the index. */ + query: schema.any(), + /** The fields to return histogram data. */ + fields: schema.arrayOf(schema.any()), + /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */ + samplerShardSize: schema.number(), +}); + +export type FieldHistogramsRequestSchema = TypeOf; +export type FieldHistogramsResponseSchema = any[]; diff --git a/x-pack/plugins/transform/common/api_schemas/start_transforms.ts b/x-pack/plugins/transform/common/api_schemas/start_transforms.ts new file mode 100644 index 0000000000000..b9611636e61a8 --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/start_transforms.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf } from '@kbn/config-schema'; + +import { transformIdsSchema, CommonResponseStatusSchema } from './common'; + +export const startTransformsRequestSchema = transformIdsSchema; +export type StartTransformsRequestSchema = TypeOf; +export type StartTransformsResponseSchema = CommonResponseStatusSchema; diff --git a/x-pack/plugins/transform/common/api_schemas/stop_transforms.ts b/x-pack/plugins/transform/common/api_schemas/stop_transforms.ts new file mode 100644 index 0000000000000..56956de20b49e --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/stop_transforms.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +import { transformStateSchema, CommonResponseStatusSchema } from './common'; + +export const stopTransformsRequestSchema = schema.arrayOf( + schema.object({ + id: schema.string(), + state: transformStateSchema, + }) +); + +export type StopTransformsRequestSchema = TypeOf; +export type StopTransformsResponseSchema = CommonResponseStatusSchema; diff --git a/x-pack/plugins/transform/common/api_schemas/transforms.ts b/x-pack/plugins/transform/common/api_schemas/transforms.ts new file mode 100644 index 0000000000000..155807a5c445f --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/transforms.ts @@ -0,0 +1,127 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +import type { ES_FIELD_TYPES } from '../../../../../src/plugins/data/common'; + +import type { Dictionary } from '../types/common'; +import type { PivotAggDict } from '../types/pivot_aggs'; +import type { PivotGroupByDict } from '../types/pivot_group_by'; +import type { TransformId, TransformPivotConfig } from '../types/transform'; + +import { transformStateSchema } from './common'; + +// GET transforms +export const getTransformsRequestSchema = schema.arrayOf( + schema.object({ + id: schema.string(), + state: transformStateSchema, + }) +); + +export type GetTransformsRequestSchema = TypeOf; + +export interface GetTransformsResponseSchema { + count: number; + transforms: TransformPivotConfig[]; +} + +// schemas shared by parts of the preview, create and update endpoint +export const destSchema = schema.object({ + index: schema.string(), + pipeline: schema.maybe(schema.string()), +}); +export const pivotSchema = schema.object({ + group_by: schema.any(), + aggregations: schema.any(), +}); +export const settingsSchema = schema.object({ + max_page_search_size: schema.maybe(schema.number()), + // The default value is null, which disables throttling. + docs_per_second: schema.maybe(schema.nullable(schema.number())), +}); +export const sourceSchema = schema.object({ + index: schema.oneOf([schema.string(), schema.arrayOf(schema.string())]), + query: schema.maybe(schema.recordOf(schema.string(), schema.any())), +}); +export const syncSchema = schema.object({ + time: schema.object({ + delay: schema.maybe(schema.string()), + field: schema.string(), + }), +}); + +// PUT transforms/{transformId} +export const putTransformsRequestSchema = schema.object({ + description: schema.maybe(schema.string()), + dest: destSchema, + frequency: schema.maybe(schema.string()), + pivot: pivotSchema, + settings: schema.maybe(settingsSchema), + source: sourceSchema, + sync: schema.maybe(syncSchema), +}); + +export interface PutTransformsRequestSchema extends TypeOf { + pivot: { + group_by: PivotGroupByDict; + aggregations: PivotAggDict; + }; +} + +interface TransformCreated { + transform: TransformId; +} +interface TransformCreatedError { + id: TransformId; + error: any; +} +export interface PutTransformsResponseSchema { + transformsCreated: TransformCreated[]; + errors: TransformCreatedError[]; +} + +// POST transforms/_preview +export const postTransformsPreviewRequestSchema = schema.object({ + pivot: pivotSchema, + source: sourceSchema, +}); + +export interface PostTransformsPreviewRequestSchema + extends TypeOf { + pivot: { + group_by: PivotGroupByDict; + aggregations: PivotAggDict; + }; +} + +interface EsMappingType { + type: ES_FIELD_TYPES; +} + +export type PreviewItem = Dictionary; +export type PreviewData = PreviewItem[]; +export type PreviewMappingsProperties = Dictionary; + +export interface PostTransformsPreviewResponseSchema { + generated_dest_index: { + mappings: { + _meta: { + _transform: { + transform: string; + version: { create: string }; + creation_date_in_millis: number; + }; + created_by: string; + }; + properties: PreviewMappingsProperties; + }; + settings: { index: { number_of_shards: string; auto_expand_replicas: string } }; + aliases: Record; + }; + preview: PreviewData; +} diff --git a/x-pack/plugins/transform/common/api_schemas/transforms_stats.ts b/x-pack/plugins/transform/common/api_schemas/transforms_stats.ts new file mode 100644 index 0000000000000..30661a8a407da --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/transforms_stats.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TypeOf } from '@kbn/config-schema'; + +import { TransformStats } from '../types/transform_stats'; + +import { getTransformsRequestSchema } from './transforms'; + +export const getTransformsStatsRequestSchema = getTransformsRequestSchema; + +export type GetTransformsRequestSchema = TypeOf; + +export interface GetTransformsStatsResponseSchema { + node_failures?: object; + count: number; + transforms: TransformStats[]; +} diff --git a/x-pack/plugins/transform/common/api_schemas/type_guards.ts b/x-pack/plugins/transform/common/api_schemas/type_guards.ts new file mode 100644 index 0000000000000..f9753a412527e --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/type_guards.ts @@ -0,0 +1,114 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { SearchResponse7 } from '../../../ml/common'; + +import type { EsIndex } from '../types/es_index'; + +// To be able to use the type guards on the client side, we need to make sure we don't import +// the code of '@kbn/config-schema' but just its types, otherwise the client side code will +// fail to build. +import type { FieldHistogramsResponseSchema } from './field_histograms'; +import type { GetTransformsAuditMessagesResponseSchema } from './audit_messages'; +import type { DeleteTransformsResponseSchema } from './delete_transforms'; +import type { StartTransformsResponseSchema } from './start_transforms'; +import type { StopTransformsResponseSchema } from './stop_transforms'; +import type { + GetTransformsResponseSchema, + PostTransformsPreviewResponseSchema, + PutTransformsResponseSchema, +} from './transforms'; +import type { GetTransformsStatsResponseSchema } from './transforms_stats'; +import type { PostTransformsUpdateResponseSchema } from './update_transforms'; + +const isBasicObject = (arg: any) => { + return typeof arg === 'object' && arg !== null; +}; + +const isGenericResponseSchema = (arg: any): arg is T => { + return ( + isBasicObject(arg) && + {}.hasOwnProperty.call(arg, 'count') && + {}.hasOwnProperty.call(arg, 'transforms') && + Array.isArray(arg.transforms) + ); +}; + +export const isGetTransformsResponseSchema = (arg: any): arg is GetTransformsResponseSchema => { + return isGenericResponseSchema(arg); +}; + +export const isGetTransformsStatsResponseSchema = ( + arg: any +): arg is GetTransformsStatsResponseSchema => { + return isGenericResponseSchema(arg); +}; + +export const isDeleteTransformsResponseSchema = ( + arg: any +): arg is DeleteTransformsResponseSchema => { + return ( + isBasicObject(arg) && + Object.values(arg).every((d) => ({}.hasOwnProperty.call(d, 'transformDeleted'))) + ); +}; + +export const isEsIndices = (arg: any): arg is EsIndex[] => { + return Array.isArray(arg); +}; + +export const isEsSearchResponse = (arg: any): arg is SearchResponse7 => { + return isBasicObject(arg) && {}.hasOwnProperty.call(arg, 'hits'); +}; + +export const isFieldHistogramsResponseSchema = (arg: any): arg is FieldHistogramsResponseSchema => { + return Array.isArray(arg); +}; + +export const isGetTransformsAuditMessagesResponseSchema = ( + arg: any +): arg is GetTransformsAuditMessagesResponseSchema => { + return Array.isArray(arg); +}; + +export const isPostTransformsPreviewResponseSchema = ( + arg: any +): arg is PostTransformsPreviewResponseSchema => { + return ( + isBasicObject(arg) && + {}.hasOwnProperty.call(arg, 'generated_dest_index') && + {}.hasOwnProperty.call(arg, 'preview') && + typeof arg.generated_dest_index !== undefined && + Array.isArray(arg.preview) + ); +}; + +export const isPostTransformsUpdateResponseSchema = ( + arg: any +): arg is PostTransformsUpdateResponseSchema => { + return isBasicObject(arg) && {}.hasOwnProperty.call(arg, 'id') && typeof arg.id === 'string'; +}; + +export const isPutTransformsResponseSchema = (arg: any): arg is PutTransformsResponseSchema => { + return ( + isBasicObject(arg) && + {}.hasOwnProperty.call(arg, 'transformsCreated') && + {}.hasOwnProperty.call(arg, 'errors') && + Array.isArray(arg.transformsCreated) && + Array.isArray(arg.errors) + ); +}; + +const isGenericSuccessResponseSchema = (arg: any) => + isBasicObject(arg) && Object.values(arg).every((d) => ({}.hasOwnProperty.call(d, 'success'))); + +export const isStartTransformsResponseSchema = (arg: any): arg is StartTransformsResponseSchema => { + return isGenericSuccessResponseSchema(arg); +}; + +export const isStopTransformsResponseSchema = (arg: any): arg is StopTransformsResponseSchema => { + return isGenericSuccessResponseSchema(arg); +}; diff --git a/x-pack/plugins/transform/common/api_schemas/update_transforms.ts b/x-pack/plugins/transform/common/api_schemas/update_transforms.ts new file mode 100644 index 0000000000000..e303d94ef0536 --- /dev/null +++ b/x-pack/plugins/transform/common/api_schemas/update_transforms.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +import { TransformPivotConfig } from '../types/transform'; + +import { destSchema, settingsSchema, sourceSchema, syncSchema } from './transforms'; + +// POST _transform/{transform_id}/_update +export const postTransformsUpdateRequestSchema = schema.object({ + description: schema.maybe(schema.string()), + dest: schema.maybe(destSchema), + frequency: schema.maybe(schema.string()), + settings: schema.maybe(settingsSchema), + source: schema.maybe(sourceSchema), + sync: schema.maybe(syncSchema), +}); + +export type PostTransformsUpdateRequestSchema = TypeOf; +export type PostTransformsUpdateResponseSchema = TransformPivotConfig; diff --git a/x-pack/plugins/transform/common/constants.ts b/x-pack/plugins/transform/common/constants.ts index b01a82dffa04a..5efb6f31c1e3f 100644 --- a/x-pack/plugins/transform/common/constants.ts +++ b/x-pack/plugins/transform/common/constants.ts @@ -75,3 +75,24 @@ export const APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES = [ ]; export const APP_INDEX_PRIVILEGES = ['monitor']; + +// reflects https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/dataframe/transforms/DataFrameTransformStats.java#L243 +export const TRANSFORM_STATE = { + ABORTING: 'aborting', + FAILED: 'failed', + INDEXING: 'indexing', + STARTED: 'started', + STOPPED: 'stopped', + STOPPING: 'stopping', +} as const; + +const transformStates = Object.values(TRANSFORM_STATE); +export type TransformState = typeof transformStates[number]; + +export const TRANSFORM_MODE = { + BATCH: 'batch', + CONTINUOUS: 'continuous', +} as const; + +const transformModes = Object.values(TRANSFORM_MODE); +export type TransformMode = typeof transformModes[number]; diff --git a/x-pack/plugins/transform/common/index.ts b/x-pack/plugins/transform/common/index.ts deleted file mode 100644 index 08bb4022c7016..0000000000000 --- a/x-pack/plugins/transform/common/index.ts +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export interface MissingPrivileges { - [key: string]: string[] | undefined; -} - -export interface Privileges { - hasAllPrivileges: boolean; - missingPrivileges: MissingPrivileges; -} - -export type TransformId = string; - -// reflects https://github.com/elastic/elasticsearch/blob/master/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/dataframe/transforms/DataFrameTransformStats.java#L243 -export enum TRANSFORM_STATE { - ABORTING = 'aborting', - FAILED = 'failed', - INDEXING = 'indexing', - STARTED = 'started', - STOPPED = 'stopped', - STOPPING = 'stopping', -} - -export interface TransformEndpointRequest { - id: TransformId; - state?: TRANSFORM_STATE; -} - -export interface ResultData { - success: boolean; - error?: any; -} - -export interface TransformEndpointResult { - [key: string]: ResultData; -} - -export interface DeleteTransformEndpointRequest { - transformsInfo: TransformEndpointRequest[]; - deleteDestIndex?: boolean; - deleteDestIndexPattern?: boolean; - forceDelete?: boolean; -} - -export interface DeleteTransformStatus { - transformDeleted: ResultData; - destIndexDeleted?: ResultData; - destIndexPatternDeleted?: ResultData; - destinationIndex?: string | undefined; -} - -export interface DeleteTransformEndpointResult { - [key: string]: DeleteTransformStatus; -} diff --git a/x-pack/plugins/transform/common/shared_imports.ts b/x-pack/plugins/transform/common/shared_imports.ts new file mode 100644 index 0000000000000..8681204755c36 --- /dev/null +++ b/x-pack/plugins/transform/common/shared_imports.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type { SearchResponse7 } from '../../ml/common'; diff --git a/x-pack/plugins/transform/common/types/aggregations.ts b/x-pack/plugins/transform/common/types/aggregations.ts new file mode 100644 index 0000000000000..77b7e55e3ba94 --- /dev/null +++ b/x-pack/plugins/transform/common/types/aggregations.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type AggName = string; diff --git a/x-pack/plugins/transform/public/app/hooks/use_api_types.ts b/x-pack/plugins/transform/common/types/es_index.ts similarity index 100% rename from x-pack/plugins/transform/public/app/hooks/use_api_types.ts rename to x-pack/plugins/transform/common/types/es_index.ts diff --git a/x-pack/plugins/transform/common/types/fields.ts b/x-pack/plugins/transform/common/types/fields.ts new file mode 100644 index 0000000000000..2c274f3bd9b48 --- /dev/null +++ b/x-pack/plugins/transform/common/types/fields.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export type EsFieldName = string; diff --git a/x-pack/plugins/transform/common/types/pivot_aggs.ts b/x-pack/plugins/transform/common/types/pivot_aggs.ts new file mode 100644 index 0000000000000..d50609da6a5dc --- /dev/null +++ b/x-pack/plugins/transform/common/types/pivot_aggs.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { AggName } from './aggregations'; +import { EsFieldName } from './fields'; + +export const PIVOT_SUPPORTED_AGGS = { + AVG: 'avg', + CARDINALITY: 'cardinality', + MAX: 'max', + MIN: 'min', + PERCENTILES: 'percentiles', + SUM: 'sum', + VALUE_COUNT: 'value_count', + FILTER: 'filter', +} as const; + +export type PivotSupportedAggs = typeof PIVOT_SUPPORTED_AGGS[keyof typeof PIVOT_SUPPORTED_AGGS]; + +export type PivotAgg = { + [key in PivotSupportedAggs]?: { + field: EsFieldName; + }; +}; + +export type PivotAggDict = { + [key in AggName]: PivotAgg; +}; diff --git a/x-pack/plugins/transform/common/types/pivot_group_by.ts b/x-pack/plugins/transform/common/types/pivot_group_by.ts new file mode 100644 index 0000000000000..bfaf17a32b580 --- /dev/null +++ b/x-pack/plugins/transform/common/types/pivot_group_by.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { Dictionary } from './common'; +import { EsFieldName } from './fields'; + +export type GenericAgg = object; + +export interface TermsAgg { + terms: { + field: EsFieldName; + }; +} + +export interface HistogramAgg { + histogram: { + field: EsFieldName; + interval: string; + }; +} + +export interface DateHistogramAgg { + date_histogram: { + field: EsFieldName; + calendar_interval: string; + }; +} + +export type PivotGroupBy = GenericAgg | TermsAgg | HistogramAgg | DateHistogramAgg; +export type PivotGroupByDict = Dictionary; diff --git a/x-pack/plugins/ml/public/application/components/messagebar/messagebar_service.d.ts b/x-pack/plugins/transform/common/types/privileges.ts similarity index 58% rename from x-pack/plugins/ml/public/application/components/messagebar/messagebar_service.d.ts rename to x-pack/plugins/transform/common/types/privileges.ts index 29a537a7ca8d8..bf710b8225599 100644 --- a/x-pack/plugins/ml/public/application/components/messagebar/messagebar_service.d.ts +++ b/x-pack/plugins/transform/common/types/privileges.ts @@ -4,10 +4,11 @@ * you may not use this file except in compliance with the Elastic License. */ -declare interface MlMessageBarService { - notify: { - error(text: any, resp?: any): void; - }; +export interface MissingPrivileges { + [key: string]: string[] | undefined; } -export const mlMessageBarService: MlMessageBarService; +export interface Privileges { + hasAllPrivileges: boolean; + missingPrivileges: MissingPrivileges; +} diff --git a/x-pack/plugins/transform/common/types/transform.ts b/x-pack/plugins/transform/common/types/transform.ts new file mode 100644 index 0000000000000..6b31705442706 --- /dev/null +++ b/x-pack/plugins/transform/common/types/transform.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { PutTransformsRequestSchema } from '../api_schemas/transforms'; + +export type IndexName = string; +export type IndexPattern = string; +export type TransformId = string; + +export interface TransformPivotConfig extends PutTransformsRequestSchema { + id: TransformId; + create_time?: number; + version?: string; +} diff --git a/x-pack/plugins/transform/common/types/transform_stats.ts b/x-pack/plugins/transform/common/types/transform_stats.ts new file mode 100644 index 0000000000000..5bd2fd955845c --- /dev/null +++ b/x-pack/plugins/transform/common/types/transform_stats.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { TransformState, TRANSFORM_STATE } from '../constants'; +import { TransformId } from './transform'; + +export interface TransformStats { + id: TransformId; + checkpointing: { + last: { + checkpoint: number; + timestamp_millis?: number; + }; + next?: { + checkpoint: number; + checkpoint_progress?: { + total_docs: number; + docs_remaining: number; + percent_complete: number; + }; + }; + operations_behind: number; + }; + node?: { + id: string; + name: string; + ephemeral_id: string; + transport_address: string; + attributes: Record; + }; + stats: { + documents_indexed: number; + documents_processed: number; + index_failures: number; + index_time_in_ms: number; + index_total: number; + pages_processed: number; + search_failures: number; + search_time_in_ms: number; + search_total: number; + trigger_count: number; + processing_time_in_ms: number; + processing_total: number; + exponential_avg_checkpoint_duration_ms: number; + exponential_avg_documents_indexed: number; + exponential_avg_documents_processed: number; + }; + reason?: string; + state: TransformState; +} + +export function isTransformStats(arg: any): arg is TransformStats { + return ( + typeof arg === 'object' && + arg !== null && + {}.hasOwnProperty.call(arg, 'state') && + Object.values(TRANSFORM_STATE).includes(arg.state) + ); +} diff --git a/x-pack/plugins/transform/common/utils/errors.ts b/x-pack/plugins/transform/common/utils/errors.ts new file mode 100644 index 0000000000000..0c31d7e1584f0 --- /dev/null +++ b/x-pack/plugins/transform/common/utils/errors.ts @@ -0,0 +1,31 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export interface ErrorResponse { + body: { + statusCode: number; + error: string; + message: string; + attributes?: any; + }; + name: string; +} + +export function isErrorResponse(arg: any): arg is ErrorResponse { + return arg?.body?.error !== undefined && arg?.body?.message !== undefined; +} + +export function getErrorMessage(error: any) { + if (isErrorResponse(error)) { + return `${error.body.error}: ${error.body.message}`; + } + + if (typeof error === 'object' && typeof error.message === 'string') { + return error.message; + } + + return JSON.stringify(error); +} diff --git a/x-pack/plugins/transform/kibana.json b/x-pack/plugins/transform/kibana.json index d7e7a7fabba4f..2efe0bb25bc68 100644 --- a/x-pack/plugins/transform/kibana.json +++ b/x-pack/plugins/transform/kibana.json @@ -7,7 +7,8 @@ "data", "home", "licensing", - "management" + "management", + "features" ], "optionalPlugins": [ "security", diff --git a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts index e115e086f45b5..470c42d5de7fa 100644 --- a/x-pack/plugins/transform/public/__mocks__/shared_imports.ts +++ b/x-pack/plugins/transform/public/__mocks__/shared_imports.ts @@ -15,7 +15,6 @@ export const useRequest = jest.fn(() => ({ // just passing through the reimports export { - getErrorMessage, getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, multiColumnSortFactory, @@ -24,7 +23,6 @@ export { DataGrid, EsSorting, RenderCellValue, - SearchResponse7, UseDataGridReturnType, UseIndexDataReturnType, INDEX_STATUS, diff --git a/x-pack/plugins/transform/public/app/common/aggregations.ts b/x-pack/plugins/transform/public/app/common/aggregations.ts index 397a58006f1d1..507579d374353 100644 --- a/x-pack/plugins/transform/public/app/common/aggregations.ts +++ b/x-pack/plugins/transform/public/app/common/aggregations.ts @@ -6,7 +6,7 @@ import { composeValidators, patternValidator } from '../../../../ml/public'; -export type AggName = string; +import { AggName } from '../../../common/types/aggregations'; export function isAggName(arg: any): arg is AggName { // allow all characters except `[]>` and must not start or end with a space. diff --git a/x-pack/plugins/transform/public/app/common/data_grid.test.ts b/x-pack/plugins/transform/public/app/common/data_grid.test.ts index 0e5ecb5d3b214..6d96f614b28a4 100644 --- a/x-pack/plugins/transform/public/app/common/data_grid.test.ts +++ b/x-pack/plugins/transform/public/app/common/data_grid.test.ts @@ -4,11 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs'; + import { - getPreviewRequestBody, + getPreviewTransformRequestBody, PivotAggsConfig, PivotGroupByConfig, - PIVOT_SUPPORTED_AGGS, PIVOT_SUPPORTED_GROUP_BY_AGGS, SimpleQuery, } from '../common'; @@ -35,7 +36,12 @@ describe('Transform: Data Grid', () => { aggName: 'the-agg-agg-name', dropDownName: 'the-agg-drop-down-name', }; - const request = getPreviewRequestBody('the-index-pattern-title', query, [groupBy], [agg]); + const request = getPreviewTransformRequestBody( + 'the-index-pattern-title', + query, + [groupBy], + [agg] + ); const pivotPreviewDevConsoleStatement = getPivotPreviewDevConsoleStatement(request); expect(pivotPreviewDevConsoleStatement).toBe(`POST _transform/_preview diff --git a/x-pack/plugins/transform/public/app/common/data_grid.ts b/x-pack/plugins/transform/public/app/common/data_grid.ts index cf9ba5d6f5853..08f834431fa8b 100644 --- a/x-pack/plugins/transform/public/app/common/data_grid.ts +++ b/x-pack/plugins/transform/public/app/common/data_grid.ts @@ -4,12 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ +import type { PostTransformsPreviewRequestSchema } from '../../../common/api_schemas/transforms'; + import { PivotQuery } from './request'; -import { PreviewRequestBody } from './transform'; export const INIT_MAX_COLUMNS = 20; -export const getPivotPreviewDevConsoleStatement = (request: PreviewRequestBody) => { +export const getPivotPreviewDevConsoleStatement = (request: PostTransformsPreviewRequestSchema) => { return `POST _transform/_preview\n${JSON.stringify(request, null, 2)}\n`; }; diff --git a/x-pack/plugins/transform/public/app/common/fields.ts b/x-pack/plugins/transform/public/app/common/fields.ts index b22aae255b9fa..778750e1f97e4 100644 --- a/x-pack/plugins/transform/public/app/common/fields.ts +++ b/x-pack/plugins/transform/public/app/common/fields.ts @@ -5,10 +5,10 @@ */ import { Dictionary } from '../../../common/types/common'; +import { EsFieldName } from '../../../common/types/fields'; export type EsId = string; export type EsDocSource = Dictionary; -export type EsFieldName = string; export interface EsDoc extends Dictionary { _id: EsId; diff --git a/x-pack/plugins/transform/public/app/common/index.ts b/x-pack/plugins/transform/public/app/common/index.ts index 45ddc440057b2..0fc947eaf33b0 100644 --- a/x-pack/plugins/transform/public/app/common/index.ts +++ b/x-pack/plugins/transform/public/app/common/index.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -export { AggName, isAggName } from './aggregations'; +export { isAggName } from './aggregations'; export { getIndexDevConsoleStatement, getPivotPreviewDevConsoleStatement, @@ -17,44 +17,28 @@ export { toggleSelectedField, EsDoc, EsDocSource, - EsFieldName, } from './fields'; export { DropDownLabel, DropDownOption, Label } from './dropdown'; export { isTransformIdValid, refreshTransformList$, useRefreshTransformList, - CreateRequestBody, - PreviewRequestBody, - TransformPivotConfig, - IndexName, - IndexPattern, REFRESH_TRANSFORM_LIST_STATE, } from './transform'; export { TRANSFORM_LIST_COLUMN, TransformListAction, TransformListRow } from './transform_list'; -export { - getTransformProgress, - isCompletedBatchTransform, - isTransformStats, - TransformStats, - TRANSFORM_MODE, -} from './transform_stats'; +export { getTransformProgress, isCompletedBatchTransform } from './transform_stats'; export { getDiscoverUrl } from './navigation'; -export { GetTransformsResponse, PreviewData, PreviewMappings } from './pivot_preview'; export { getEsAggFromAggConfig, isPivotAggsConfigWithUiSupport, isPivotAggsConfigPercentiles, PERCENTILES_AGG_DEFAULT_PERCENTS, - PivotAgg, - PivotAggDict, PivotAggsConfig, PivotAggsConfigDict, PivotAggsConfigBase, PivotAggsConfigWithUiSupport, PivotAggsConfigWithUiSupportDict, pivotAggsFieldSupport, - PIVOT_SUPPORTED_AGGS, } from './pivot_aggs'; export { dateHistogramIntervalFormatRegex, @@ -65,25 +49,19 @@ export { isGroupByHistogram, isGroupByTerms, pivotGroupByFieldSupport, - DateHistogramAgg, - GenericAgg, GroupByConfigWithInterval, GroupByConfigWithUiSupport, - HistogramAgg, - PivotGroupBy, PivotGroupByConfig, - PivotGroupByDict, PivotGroupByConfigDict, PivotGroupByConfigWithUiSupportDict, PivotSupportedGroupByAggs, PivotSupportedGroupByAggsWithInterval, PIVOT_SUPPORTED_GROUP_BY_AGGS, - TermsAgg, } from './pivot_group_by'; export { defaultQuery, - getPreviewRequestBody, - getCreateRequestBody, + getPreviewTransformRequestBody, + getCreateTransformRequestBody, getPivotQuery, isDefaultQuery, isMatchAllQuery, diff --git a/x-pack/plugins/transform/public/app/common/pivot_aggs.ts b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts index ec52de4b9da92..7a7bb4c65b306 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_aggs.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_aggs.ts @@ -5,31 +5,22 @@ */ import { FC } from 'react'; -import { Dictionary } from '../../../common/types/common'; + import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; -import { AggName } from './aggregations'; -import { EsFieldName } from './fields'; +import type { AggName } from '../../../common/types/aggregations'; +import type { Dictionary } from '../../../common/types/common'; +import type { EsFieldName } from '../../../common/types/fields'; +import type { PivotAgg, PivotSupportedAggs } from '../../../common/types/pivot_aggs'; +import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs'; + import { getAggFormConfig } from '../sections/create_transform/components/step_define/common/get_agg_form_config'; import { PivotAggsConfigFilter } from '../sections/create_transform/components/step_define/common/filter_agg/types'; -export type PivotSupportedAggs = typeof PIVOT_SUPPORTED_AGGS[keyof typeof PIVOT_SUPPORTED_AGGS]; - export function isPivotSupportedAggs(arg: any): arg is PivotSupportedAggs { return Object.values(PIVOT_SUPPORTED_AGGS).includes(arg); } -export const PIVOT_SUPPORTED_AGGS = { - AVG: 'avg', - CARDINALITY: 'cardinality', - MAX: 'max', - MIN: 'min', - PERCENTILES: 'percentiles', - SUM: 'sum', - VALUE_COUNT: 'value_count', - FILTER: 'filter', -} as const; - export const PERCENTILES_AGG_DEFAULT_PERCENTS = [1, 5, 25, 50, 75, 95, 99]; export const pivotAggsFieldSupport = { @@ -69,16 +60,6 @@ export const pivotAggsFieldSupport = { [KBN_FIELD_TYPES.CONFLICT]: [PIVOT_SUPPORTED_AGGS.VALUE_COUNT, PIVOT_SUPPORTED_AGGS.FILTER], }; -export type PivotAgg = { - [key in PivotSupportedAggs]?: { - field: EsFieldName; - }; -}; - -export type PivotAggDict = { - [key in AggName]: PivotAgg; -}; - /** * The maximum level of sub-aggregations */ diff --git a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts index 7da52fc018338..2c2bac369c72d 100644 --- a/x-pack/plugins/transform/public/app/common/pivot_group_by.ts +++ b/x-pack/plugins/transform/public/app/common/pivot_group_by.ts @@ -4,12 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ +import { AggName } from '../../../common/types/aggregations'; import { Dictionary } from '../../../common/types/common'; +import { EsFieldName } from '../../../common/types/fields'; +import { GenericAgg } from '../../../common/types/pivot_group_by'; import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; -import { AggName } from './aggregations'; -import { EsFieldName } from './fields'; - export enum PIVOT_SUPPORTED_GROUP_BY_AGGS { DATE_HISTOGRAM = 'date_histogram', HISTOGRAM = 'histogram', @@ -106,31 +106,6 @@ export function isPivotGroupByConfigWithUiSupport(arg: any): arg is GroupByConfi return isGroupByDateHistogram(arg) || isGroupByHistogram(arg) || isGroupByTerms(arg); } -export type GenericAgg = object; - -export interface TermsAgg { - terms: { - field: EsFieldName; - }; -} - -export interface HistogramAgg { - histogram: { - field: EsFieldName; - interval: string; - }; -} - -export interface DateHistogramAgg { - date_histogram: { - field: EsFieldName; - calendar_interval: string; - }; -} - -export type PivotGroupBy = GenericAgg | TermsAgg | HistogramAgg | DateHistogramAgg; -export type PivotGroupByDict = Dictionary; - export function getEsAggFromGroupByConfig(groupByConfig: GroupByConfigBase): GenericAgg { const { agg, aggName, dropDownName, ...esAgg } = groupByConfig; diff --git a/x-pack/plugins/transform/public/app/common/pivot_preview.ts b/x-pack/plugins/transform/public/app/common/pivot_preview.ts deleted file mode 100644 index 14368a80b0131..0000000000000 --- a/x-pack/plugins/transform/public/app/common/pivot_preview.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; - -import { Dictionary } from '../../../common/types/common'; - -interface EsMappingType { - type: ES_FIELD_TYPES; -} - -export type PreviewItem = Dictionary; -export type PreviewData = PreviewItem[]; -export interface PreviewMappings { - properties: Dictionary; -} - -export interface GetTransformsResponse { - preview: PreviewData; - generated_dest_index: { - mappings: PreviewMappings; - // Not in use yet - aliases: any; - settings: any; - }; -} diff --git a/x-pack/plugins/transform/public/app/common/request.test.ts b/x-pack/plugins/transform/public/app/common/request.test.ts index 63f1f8b10ad44..416927c460842 100644 --- a/x-pack/plugins/transform/public/app/common/request.test.ts +++ b/x-pack/plugins/transform/public/app/common/request.test.ts @@ -4,17 +4,19 @@ * you may not use this file except in compliance with the Elastic License. */ +import { PIVOT_SUPPORTED_AGGS } from '../../../common/types/pivot_aggs'; + import { PivotGroupByConfig } from '../common'; import { StepDefineExposedState } from '../sections/create_transform/components/step_define'; import { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form'; import { PIVOT_SUPPORTED_GROUP_BY_AGGS } from './pivot_group_by'; -import { PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from './pivot_aggs'; +import { PivotAggsConfig } from './pivot_aggs'; import { defaultQuery, - getPreviewRequestBody, - getCreateRequestBody, + getPreviewTransformRequestBody, + getCreateTransformRequestBody, getPivotQuery, isDefaultQuery, isMatchAllQuery, @@ -55,7 +57,7 @@ describe('Transform: Common', () => { }); }); - test('getPreviewRequestBody()', () => { + test('getPreviewTransformRequestBody()', () => { const query = getPivotQuery('the-query'); const groupBy: PivotGroupByConfig[] = [ { @@ -73,7 +75,7 @@ describe('Transform: Common', () => { dropDownName: 'the-agg-drop-down-name', }, ]; - const request = getPreviewRequestBody('the-index-pattern-title', query, groupBy, aggs); + const request = getPreviewTransformRequestBody('the-index-pattern-title', query, groupBy, aggs); expect(request).toEqual({ pivot: { @@ -87,7 +89,7 @@ describe('Transform: Common', () => { }); }); - test('getPreviewRequestBody() with comma-separated index pattern', () => { + test('getPreviewTransformRequestBody() with comma-separated index pattern', () => { const query = getPivotQuery('the-query'); const groupBy: PivotGroupByConfig[] = [ { @@ -105,7 +107,7 @@ describe('Transform: Common', () => { dropDownName: 'the-agg-drop-down-name', }, ]; - const request = getPreviewRequestBody( + const request = getPreviewTransformRequestBody( 'the-index-pattern-title,the-other-title', query, groupBy, @@ -124,7 +126,7 @@ describe('Transform: Common', () => { }); }); - test('getCreateRequestBody()', () => { + test('getCreateTransformRequestBody()', () => { const groupBy: PivotGroupByConfig = { agg: PIVOT_SUPPORTED_GROUP_BY_AGGS.TERMS, field: 'the-group-by-field', @@ -160,7 +162,7 @@ describe('Transform: Common', () => { valid: true, }; - const request = getCreateRequestBody( + const request = getCreateTransformRequestBody( 'the-index-pattern-title', pivotState, transformDetailsState diff --git a/x-pack/plugins/transform/public/app/common/request.ts b/x-pack/plugins/transform/public/app/common/request.ts index 9a0084c2ebffb..10f3a63477029 100644 --- a/x-pack/plugins/transform/public/app/common/request.ts +++ b/x-pack/plugins/transform/public/app/common/request.ts @@ -4,15 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { DefaultOperator } from 'elasticsearch'; - +import type { DefaultOperator } from 'elasticsearch'; + +import { HttpFetchError } from '../../../../../../src/core/public'; +import type { IndexPattern } from '../../../../../../src/plugins/data/public'; + +import type { + PostTransformsPreviewRequestSchema, + PutTransformsRequestSchema, +} from '../../../common/api_schemas/transforms'; +import type { + DateHistogramAgg, + HistogramAgg, + TermsAgg, +} from '../../../common/types/pivot_group_by'; import { dictionaryToArray } from '../../../common/types/common'; -import { SavedSearchQuery } from '../hooks/use_search_items'; - -import { StepDefineExposedState } from '../sections/create_transform/components/step_define'; -import { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form'; -import { IndexPattern } from '../../../../../../src/plugins/data/public'; +import type { SavedSearchQuery } from '../hooks/use_search_items'; +import type { StepDefineExposedState } from '../sections/create_transform/components/step_define'; +import type { StepDetailsExposedState } from '../sections/create_transform/components/step_details/step_details_form'; import { getEsAggFromAggConfig, @@ -24,8 +34,6 @@ import { } from '../common'; import { PivotAggsConfig } from './pivot_aggs'; -import { DateHistogramAgg, HistogramAgg, TermsAgg } from './pivot_group_by'; -import { PreviewRequestBody, CreateRequestBody } from './transform'; export interface SimpleQuery { query_string: { @@ -63,17 +71,18 @@ export function isDefaultQuery(query: PivotQuery): boolean { return isSimpleQuery(query) && query.query_string.query === '*'; } -export function getPreviewRequestBody( +export function getPreviewTransformRequestBody( indexPatternTitle: IndexPattern['title'], query: PivotQuery, groupBy: PivotGroupByConfig[], aggs: PivotAggsConfig[] -): PreviewRequestBody { +): PostTransformsPreviewRequestSchema { const index = indexPatternTitle.split(',').map((name: string) => name.trim()); - const request: PreviewRequestBody = { + const request: PostTransformsPreviewRequestSchema = { source: { index, + ...(!isDefaultQuery(query) && !isMatchAllQuery(query) ? { query } : {}), }, pivot: { group_by: {}, @@ -81,10 +90,6 @@ export function getPreviewRequestBody( }, }; - if (!isDefaultQuery(query) && !isMatchAllQuery(query)) { - request.source.query = query; - } - groupBy.forEach((g) => { if (isGroupByTerms(g)) { const termsAgg: TermsAgg = { @@ -125,37 +130,41 @@ export function getPreviewRequestBody( return request; } -export function getCreateRequestBody( +export const getCreateTransformRequestBody = ( indexPatternTitle: IndexPattern['title'], pivotState: StepDefineExposedState, transformDetailsState: StepDetailsExposedState -): CreateRequestBody { - const request: CreateRequestBody = { - ...getPreviewRequestBody( - indexPatternTitle, - getPivotQuery(pivotState.searchQuery), - dictionaryToArray(pivotState.groupByList), - dictionaryToArray(pivotState.aggList) - ), - // conditionally add optional description - ...(transformDetailsState.transformDescription !== '' - ? { description: transformDetailsState.transformDescription } - : {}), - dest: { - index: transformDetailsState.destinationIndex, - }, - // conditionally add continuous mode config - ...(transformDetailsState.isContinuousModeEnabled - ? { - sync: { - time: { - field: transformDetailsState.continuousModeDateField, - delay: transformDetailsState.continuousModeDelay, - }, +): PutTransformsRequestSchema => ({ + ...getPreviewTransformRequestBody( + indexPatternTitle, + getPivotQuery(pivotState.searchQuery), + dictionaryToArray(pivotState.groupByList), + dictionaryToArray(pivotState.aggList) + ), + // conditionally add optional description + ...(transformDetailsState.transformDescription !== '' + ? { description: transformDetailsState.transformDescription } + : {}), + dest: { + index: transformDetailsState.destinationIndex, + }, + // conditionally add continuous mode config + ...(transformDetailsState.isContinuousModeEnabled + ? { + sync: { + time: { + field: transformDetailsState.continuousModeDateField, + delay: transformDetailsState.continuousModeDelay, }, - } - : {}), - }; - - return request; + }, + } + : {}), +}); + +export function isHttpFetchError(error: any): error is HttpFetchError { + return ( + error instanceof HttpFetchError && + typeof error.name === 'string' && + typeof error.message !== 'undefined' + ); } diff --git a/x-pack/plugins/transform/public/app/common/transform.ts b/x-pack/plugins/transform/public/app/common/transform.ts index a02bed2fa65e7..b71bab62096b6 100644 --- a/x-pack/plugins/transform/public/app/common/transform.ts +++ b/x-pack/plugins/transform/public/app/common/transform.ts @@ -9,13 +9,7 @@ import { BehaviorSubject } from 'rxjs'; import { filter, distinctUntilChanged } from 'rxjs/operators'; import { Subscription } from 'rxjs'; -import { TransformId } from '../../../common'; - -import { PivotAggDict } from './pivot_aggs'; -import { PivotGroupByDict } from './pivot_group_by'; - -export type IndexName = string; -export type IndexPattern = string; +import { TransformId } from '../../../common/types/transform'; // Transform name must contain lowercase alphanumeric (a-z and 0-9), hyphens or underscores; // It must also start and end with an alphanumeric character. @@ -23,41 +17,6 @@ export function isTransformIdValid(transformId: TransformId) { return /^[a-z0-9\-\_]+$/g.test(transformId) && !/^([_-].*)?(.*[_-])?$/g.test(transformId); } -export interface PreviewRequestBody { - pivot: { - group_by: PivotGroupByDict; - aggregations: PivotAggDict; - }; - source: { - index: IndexPattern | IndexPattern[]; - query?: any; - }; -} - -export interface CreateRequestBody extends PreviewRequestBody { - description?: string; - dest: { - index: IndexName; - }; - frequency?: string; - settings?: { - max_page_search_size?: number; - docs_per_second?: number; - }; - sync?: { - time: { - field: string; - delay: string; - }; - }; -} - -export interface TransformPivotConfig extends CreateRequestBody { - id: TransformId; - create_time?: number; - version?: string; -} - export enum REFRESH_TRANSFORM_LIST_STATE { ERROR = 'error', IDLE = 'idle', diff --git a/x-pack/plugins/transform/public/app/common/transform_list.ts b/x-pack/plugins/transform/public/app/common/transform_list.ts index a2a762a7e2dfb..b32803fea1501 100644 --- a/x-pack/plugins/transform/public/app/common/transform_list.ts +++ b/x-pack/plugins/transform/public/app/common/transform_list.ts @@ -6,9 +6,8 @@ import { EuiTableActionsColumnType } from '@elastic/eui'; -import { TransformId } from '../../../common'; -import { TransformPivotConfig } from './transform'; -import { TransformStats } from './transform_stats'; +import { TransformId, TransformPivotConfig } from '../../../common/types/transform'; +import { TransformStats } from '../../../common/types/transform_stats'; // Used to pass on attribute names to table columns export enum TRANSFORM_LIST_COLUMN { diff --git a/x-pack/plugins/transform/public/app/common/transform_stats.ts b/x-pack/plugins/transform/public/app/common/transform_stats.ts index 72df6d3985e23..aaf7f97399d44 100644 --- a/x-pack/plugins/transform/public/app/common/transform_stats.ts +++ b/x-pack/plugins/transform/public/app/common/transform_stats.ts @@ -4,64 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TransformId, TRANSFORM_STATE } from '../../../common'; +import { TRANSFORM_STATE } from '../../../common/constants'; import { TransformListRow } from './transform_list'; -export enum TRANSFORM_MODE { - BATCH = 'batch', - CONTINUOUS = 'continuous', -} - -export interface TransformStats { - id: TransformId; - checkpointing: { - last: { - checkpoint: number; - timestamp_millis?: number; - }; - next?: { - checkpoint: number; - checkpoint_progress?: { - total_docs: number; - docs_remaining: number; - percent_complete: number; - }; - }; - operations_behind: number; - }; - node?: { - id: string; - name: string; - ephemeral_id: string; - transport_address: string; - attributes: Record; - }; - stats: { - documents_indexed: number; - documents_processed: number; - index_failures: number; - index_time_in_ms: number; - index_total: number; - pages_processed: number; - search_failures: number; - search_time_in_ms: number; - search_total: number; - trigger_count: number; - }; - reason?: string; - state: TRANSFORM_STATE; -} - -export function isTransformStats(arg: any): arg is TransformStats { - return ( - typeof arg === 'object' && - arg !== null && - {}.hasOwnProperty.call(arg, 'state') && - Object.values(TRANSFORM_STATE).includes(arg.state) - ); -} - export function getTransformProgress(item: TransformListRow) { if (isCompletedBatchTransform(item)) { return 100; diff --git a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts index a5cccd58211c5..40a6ab2b65862 100644 --- a/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts +++ b/x-pack/plugins/transform/public/app/hooks/__mocks__/use_api.ts @@ -4,67 +4,162 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TransformId, TransformEndpointRequest } from '../../../../common'; +import { HttpFetchError } from 'kibana/public'; -import { PreviewRequestBody } from '../../common'; +import { KBN_FIELD_TYPES } from '../../../../../../../src/plugins/data/public'; + +import { TransformId } from '../../../../common/types/transform'; +import type { FieldHistogramsResponseSchema } from '../../../../common/api_schemas/field_histograms'; +import type { GetTransformsAuditMessagesResponseSchema } from '../../../../common/api_schemas/audit_messages'; +import type { + DeleteTransformsRequestSchema, + DeleteTransformsResponseSchema, +} from '../../../../common/api_schemas/delete_transforms'; +import type { + StartTransformsRequestSchema, + StartTransformsResponseSchema, +} from '../../../../common/api_schemas/start_transforms'; +import type { + StopTransformsRequestSchema, + StopTransformsResponseSchema, +} from '../../../../common/api_schemas/stop_transforms'; +import type { + GetTransformsResponseSchema, + PostTransformsPreviewRequestSchema, + PostTransformsPreviewResponseSchema, + PutTransformsRequestSchema, + PutTransformsResponseSchema, +} from '../../../../common/api_schemas/transforms'; +import type { GetTransformsStatsResponseSchema } from '../../../../common/api_schemas/transforms_stats'; +import type { + PostTransformsUpdateRequestSchema, + PostTransformsUpdateResponseSchema, +} from '../../../../common/api_schemas/update_transforms'; + +import type { SearchResponse7 } from '../../../../common/shared_imports'; +import { EsIndex } from '../../../../common/types/es_index'; + +import type { SavedSearchQuery } from '../use_search_items'; + +// Default sampler shard size used for field histograms +export const DEFAULT_SAMPLER_SHARD_SIZE = 5000; + +export interface FieldHistogramRequestConfig { + fieldName: string; + type?: KBN_FIELD_TYPES; +} const apiFactory = () => ({ - getTransforms(transformId?: TransformId): Promise { - return new Promise((resolve, reject) => { - resolve([]); - }); + async getTransform( + transformId: TransformId + ): Promise { + return Promise.resolve({ count: 0, transforms: [] }); }, - getTransformsStats(transformId?: TransformId): Promise { - if (transformId !== undefined) { - return new Promise((resolve, reject) => { - resolve([]); - }); - } - - return new Promise((resolve, reject) => { - resolve([]); - }); + async getTransforms(): Promise { + return Promise.resolve({ count: 0, transforms: [] }); }, - createTransform(transformId: TransformId, transformConfig: any): Promise { - return new Promise((resolve, reject) => { - resolve([]); - }); + async getTransformStats( + transformId: TransformId + ): Promise { + return Promise.resolve({ count: 0, transforms: [] }); }, - deleteTransforms(transformsInfo: TransformEndpointRequest[]) { - return new Promise((resolve, reject) => { - resolve([]); - }); + async getTransformsStats(): Promise { + return Promise.resolve({ count: 0, transforms: [] }); }, - getTransformsPreview(obj: PreviewRequestBody): Promise { - return new Promise((resolve, reject) => { - resolve([]); - }); + async createTransform( + transformId: TransformId, + transformConfig: PutTransformsRequestSchema + ): Promise { + return Promise.resolve({ transformsCreated: [], errors: [] }); }, - startTransforms(transformsInfo: TransformEndpointRequest[]) { - return new Promise((resolve, reject) => { - resolve([]); + async updateTransform( + transformId: TransformId, + transformConfig: PostTransformsUpdateRequestSchema + ): Promise { + return Promise.resolve({ + id: 'the-test-id', + source: { index: ['the-index-name'], query: { match_all: {} } }, + dest: { index: 'user-the-destination-index-name' }, + frequency: '10m', + pivot: { + group_by: { the_group: { terms: { field: 'the-group-by-field' } } }, + aggregations: { the_agg: { value_count: { field: 'the-agg-field' } } }, + }, + description: 'the-description', + settings: { docs_per_second: null }, + version: '8.0.0', + create_time: 1598860879097, }); }, - stopTransforms(transformsInfo: TransformEndpointRequest[]) { - return new Promise((resolve, reject) => { - resolve([]); - }); + async deleteTransforms( + reqBody: DeleteTransformsRequestSchema + ): Promise { + return Promise.resolve({}); }, - getTransformAuditMessages(transformId: TransformId): Promise { - return new Promise((resolve, reject) => { - resolve([]); + async getTransformsPreview( + obj: PostTransformsPreviewRequestSchema + ): Promise { + return Promise.resolve({ + generated_dest_index: { + mappings: { + _meta: { + _transform: { + transform: 'the-transform', + version: { create: 'the-version' }, + creation_date_in_millis: 0, + }, + created_by: 'mock', + }, + properties: {}, + }, + settings: { index: { number_of_shards: '1', auto_expand_replicas: '0-1' } }, + aliases: {}, + }, + preview: [], }); }, - esSearch(payload: any) { - return new Promise((resolve, reject) => { - resolve([]); - }); + async startTransforms( + reqBody: StartTransformsRequestSchema + ): Promise { + return Promise.resolve({}); + }, + async stopTransforms( + transformsInfo: StopTransformsRequestSchema + ): Promise { + return Promise.resolve({}); }, - getIndices() { - return new Promise((resolve, reject) => { - resolve([]); + async getTransformAuditMessages( + transformId: TransformId + ): Promise { + return Promise.resolve([]); + }, + async esSearch(payload: any): Promise { + return Promise.resolve({ + hits: { + hits: [], + total: { + value: 0, + relation: 'the-relation', + }, + max_score: 0, + }, + timed_out: false, + took: 10, + _shards: { total: 1, successful: 1, failed: 0, skipped: 0 }, }); }, + + async getEsIndices(): Promise { + return Promise.resolve([]); + }, + async getHistogramsForFields( + indexPatternTitle: string, + fields: FieldHistogramRequestConfig[], + query: string | SavedSearchQuery, + samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE + ): Promise { + return Promise.resolve([]); + }, }); export const useApi = () => { diff --git a/x-pack/plugins/transform/public/app/hooks/use_api.ts b/x-pack/plugins/transform/public/app/hooks/use_api.ts index 1d2752b9e939d..4cff5dd9b648e 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_api.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_api.ts @@ -6,20 +6,43 @@ import { useMemo } from 'react'; +import { HttpFetchError } from 'kibana/public'; + import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; -import { - TransformId, - TransformEndpointRequest, - TransformEndpointResult, - DeleteTransformEndpointResult, -} from '../../../common'; +import type { GetTransformsAuditMessagesResponseSchema } from '../../../common/api_schemas/audit_messages'; +import type { + DeleteTransformsRequestSchema, + DeleteTransformsResponseSchema, +} from '../../../common/api_schemas/delete_transforms'; +import type { FieldHistogramsResponseSchema } from '../../../common/api_schemas/field_histograms'; +import type { + StartTransformsRequestSchema, + StartTransformsResponseSchema, +} from '../../../common/api_schemas/start_transforms'; +import type { + StopTransformsRequestSchema, + StopTransformsResponseSchema, +} from '../../../common/api_schemas/stop_transforms'; +import type { + GetTransformsResponseSchema, + PostTransformsPreviewRequestSchema, + PostTransformsPreviewResponseSchema, + PutTransformsRequestSchema, + PutTransformsResponseSchema, +} from '../../../common/api_schemas/transforms'; +import type { + PostTransformsUpdateRequestSchema, + PostTransformsUpdateResponseSchema, +} from '../../../common/api_schemas/update_transforms'; +import type { GetTransformsStatsResponseSchema } from '../../../common/api_schemas/transforms_stats'; +import { TransformId } from '../../../common/types/transform'; import { API_BASE_PATH } from '../../../common/constants'; +import { EsIndex } from '../../../common/types/es_index'; +import type { SearchResponse7 } from '../../../common/shared_imports'; import { useAppDependencies } from '../app_dependencies'; -import { GetTransformsResponse, PreviewRequestBody } from '../common'; -import { EsIndex } from './use_api_types'; import { SavedSearchQuery } from './use_search_items'; // Default sampler shard size used for field histograms @@ -35,81 +58,146 @@ export const useApi = () => { return useMemo( () => ({ - getTransforms(transformId?: TransformId): Promise { - const transformIdString = transformId !== undefined ? `/${transformId}` : ''; - return http.get(`${API_BASE_PATH}transforms${transformIdString}`); + async getTransform( + transformId: TransformId + ): Promise { + try { + return await http.get(`${API_BASE_PATH}transforms/${transformId}`); + } catch (e) { + return e; + } }, - getTransformsStats(transformId?: TransformId): Promise { - if (transformId !== undefined) { - return http.get(`${API_BASE_PATH}transforms/${transformId}/_stats`); + async getTransforms(): Promise { + try { + return await http.get(`${API_BASE_PATH}transforms`); + } catch (e) { + return e; } - - return http.get(`${API_BASE_PATH}transforms/_stats`); }, - createTransform(transformId: TransformId, transformConfig: any): Promise { - return http.put(`${API_BASE_PATH}transforms/${transformId}`, { - body: JSON.stringify(transformConfig), - }); + async getTransformStats( + transformId: TransformId + ): Promise { + try { + return await http.get(`${API_BASE_PATH}transforms/${transformId}/_stats`); + } catch (e) { + return e; + } }, - updateTransform(transformId: TransformId, transformConfig: any): Promise { - return http.post(`${API_BASE_PATH}transforms/${transformId}/_update`, { - body: JSON.stringify(transformConfig), - }); + async getTransformsStats(): Promise { + try { + return await http.get(`${API_BASE_PATH}transforms/_stats`); + } catch (e) { + return e; + } }, - deleteTransforms( - transformsInfo: TransformEndpointRequest[], - deleteDestIndex: boolean | undefined, - deleteDestIndexPattern: boolean | undefined, - forceDelete: boolean - ): Promise { - return http.post(`${API_BASE_PATH}delete_transforms`, { - body: JSON.stringify({ - transformsInfo, - deleteDestIndex, - deleteDestIndexPattern, - forceDelete, - }), - }); + async createTransform( + transformId: TransformId, + transformConfig: PutTransformsRequestSchema + ): Promise { + try { + return await http.put(`${API_BASE_PATH}transforms/${transformId}`, { + body: JSON.stringify(transformConfig), + }); + } catch (e) { + return e; + } }, - getTransformsPreview(obj: PreviewRequestBody): Promise { - return http.post(`${API_BASE_PATH}transforms/_preview`, { - body: JSON.stringify(obj), - }); + async updateTransform( + transformId: TransformId, + transformConfig: PostTransformsUpdateRequestSchema + ): Promise { + try { + return await http.post(`${API_BASE_PATH}transforms/${transformId}/_update`, { + body: JSON.stringify(transformConfig), + }); + } catch (e) { + return e; + } }, - startTransforms( - transformsInfo: TransformEndpointRequest[] - ): Promise { - return http.post(`${API_BASE_PATH}start_transforms`, { - body: JSON.stringify(transformsInfo), - }); + async deleteTransforms( + reqBody: DeleteTransformsRequestSchema + ): Promise { + try { + return await http.post(`${API_BASE_PATH}delete_transforms`, { + body: JSON.stringify(reqBody), + }); + } catch (e) { + return e; + } }, - stopTransforms(transformsInfo: TransformEndpointRequest[]): Promise { - return http.post(`${API_BASE_PATH}stop_transforms`, { - body: JSON.stringify(transformsInfo), - }); + async getTransformsPreview( + obj: PostTransformsPreviewRequestSchema + ): Promise { + try { + return await http.post(`${API_BASE_PATH}transforms/_preview`, { + body: JSON.stringify(obj), + }); + } catch (e) { + return e; + } }, - getTransformAuditMessages(transformId: TransformId): Promise { - return http.get(`${API_BASE_PATH}transforms/${transformId}/messages`); + async startTransforms( + reqBody: StartTransformsRequestSchema + ): Promise { + try { + return await http.post(`${API_BASE_PATH}start_transforms`, { + body: JSON.stringify(reqBody), + }); + } catch (e) { + return e; + } + }, + async stopTransforms( + transformsInfo: StopTransformsRequestSchema + ): Promise { + try { + return await http.post(`${API_BASE_PATH}stop_transforms`, { + body: JSON.stringify(transformsInfo), + }); + } catch (e) { + return e; + } + }, + async getTransformAuditMessages( + transformId: TransformId + ): Promise { + try { + return await http.get(`${API_BASE_PATH}transforms/${transformId}/messages`); + } catch (e) { + return e; + } }, - esSearch(payload: any): Promise { - return http.post(`${API_BASE_PATH}es_search`, { body: JSON.stringify(payload) }); + async esSearch(payload: any): Promise { + try { + return await http.post(`${API_BASE_PATH}es_search`, { body: JSON.stringify(payload) }); + } catch (e) { + return e; + } }, - getIndices(): Promise { - return http.get(`/api/index_management/indices`); + async getEsIndices(): Promise { + try { + return await http.get(`/api/index_management/indices`); + } catch (e) { + return e; + } }, - getHistogramsForFields( + async getHistogramsForFields( indexPatternTitle: string, fields: FieldHistogramRequestConfig[], query: string | SavedSearchQuery, samplerShardSize = DEFAULT_SAMPLER_SHARD_SIZE - ) { - return http.post(`${API_BASE_PATH}field_histograms/${indexPatternTitle}`, { - body: JSON.stringify({ - query, - fields, - samplerShardSize, - }), - }); + ): Promise { + try { + return await http.post(`${API_BASE_PATH}field_histograms/${indexPatternTitle}`, { + body: JSON.stringify({ + query, + fields, + samplerShardSize, + }), + }); + } catch (e) { + return e; + } }, }), [http] diff --git a/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx index 43c5ae6fad1b1..1a97ba7806fef 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx +++ b/x-pack/plugins/transform/public/app/hooks/use_delete_transform.tsx @@ -7,12 +7,13 @@ import React, { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; -import { - DeleteTransformEndpointResult, +import type { DeleteTransformStatus, - TransformEndpointRequest, -} from '../../../common'; -import { extractErrorMessage, getErrorMessage } from '../../shared_imports'; + DeleteTransformsRequestSchema, +} from '../../../common/api_schemas/delete_transforms'; +import { isDeleteTransformsResponseSchema } from '../../../common/api_schemas/type_guards'; +import { extractErrorMessage } from '../../shared_imports'; +import { getErrorMessage } from '../../../common/utils/errors'; import { useAppDependencies, useToastNotifications } from '../app_dependencies'; import { REFRESH_TRANSFORM_LIST_STATE, refreshTransformList$, TransformListRow } from '../common'; import { ToastNotificationText } from '../components'; @@ -108,173 +109,157 @@ export const useDeleteTransforms = () => { const toastNotifications = useToastNotifications(); const api = useApi(); - return async ( - transforms: TransformListRow[], - shouldDeleteDestIndex: boolean, - shouldDeleteDestIndexPattern: boolean, - shouldForceDelete = false - ) => { - const transformsInfo: TransformEndpointRequest[] = transforms.map((tf) => ({ - id: tf.config.id, - state: tf.stats.state, - })); + return async (reqBody: DeleteTransformsRequestSchema) => { + const results = await api.deleteTransforms(reqBody); - try { - const results: DeleteTransformEndpointResult = await api.deleteTransforms( - transformsInfo, - shouldDeleteDestIndex, - shouldDeleteDestIndexPattern, - shouldForceDelete - ); - const isBulk = Object.keys(results).length > 1; - const successCount: Record = { - transformDeleted: 0, - destIndexDeleted: 0, - destIndexPatternDeleted: 0, - }; - for (const transformId in results) { - // hasOwnProperty check to ensure only properties on object itself, and not its prototypes - if (results.hasOwnProperty(transformId)) { - const status = results[transformId]; - const destinationIndex = status.destinationIndex; + if (!isDeleteTransformsResponseSchema(results)) { + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.transformList.deleteTransformGenericErrorMessage', { + defaultMessage: 'An error occurred calling the API endpoint to delete transforms.', + }), + text: toMountPoint( + + ), + }); + return; + } - // if we are only deleting one transform, show the success toast messages - if (!isBulk && status.transformDeleted) { - if (status.transformDeleted?.success) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.deleteTransformSuccessMessage', { - defaultMessage: 'Request to delete transform {transformId} acknowledged.', - values: { transformId }, - }) - ); - } - if (status.destIndexDeleted?.success) { - toastNotifications.addSuccess( - i18n.translate( - 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage', - { - defaultMessage: - 'Request to delete destination index {destinationIndex} acknowledged.', - values: { destinationIndex }, - } - ) - ); - } - if (status.destIndexPatternDeleted?.success) { - toastNotifications.addSuccess( - i18n.translate( - 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternSuccessMessage', - { - defaultMessage: - 'Request to delete index pattern {destinationIndex} acknowledged.', - values: { destinationIndex }, - } - ) - ); - } - } else { - (Object.keys(successCount) as SuccessCountField[]).forEach((key) => { - if (status[key]?.success) { - successCount[key] = successCount[key] + 1; - } - }); - } - if (status.transformDeleted?.error) { - const error = extractErrorMessage(status.transformDeleted.error); - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.transformList.deleteTransformErrorMessage', { - defaultMessage: 'An error occurred deleting the transform {transformId}', + const isBulk = Object.keys(results).length > 1; + const successCount: Record = { + transformDeleted: 0, + destIndexDeleted: 0, + destIndexPatternDeleted: 0, + }; + for (const transformId in results) { + // hasOwnProperty check to ensure only properties on object itself, and not its prototypes + if (results.hasOwnProperty(transformId)) { + const status = results[transformId]; + const destinationIndex = status.destinationIndex; + + // if we are only deleting one transform, show the success toast messages + if (!isBulk && status.transformDeleted) { + if (status.transformDeleted?.success) { + toastNotifications.addSuccess( + i18n.translate('xpack.transform.transformList.deleteTransformSuccessMessage', { + defaultMessage: 'Request to delete transform {transformId} acknowledged.', values: { transformId }, - }), - text: toMountPoint( - - ), - }); + }) + ); } - - if (status.destIndexDeleted?.error) { - const error = extractErrorMessage(status.destIndexDeleted.error); - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage', + if (status.destIndexDeleted?.success) { + toastNotifications.addSuccess( + i18n.translate( + 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexSuccessMessage', { - defaultMessage: 'An error occurred deleting destination index {destinationIndex}', + defaultMessage: + 'Request to delete destination index {destinationIndex} acknowledged.', values: { destinationIndex }, } - ), - text: toMountPoint( - - ), - }); + ) + ); } - - if (status.destIndexPatternDeleted?.error) { - const error = extractErrorMessage(status.destIndexPatternDeleted.error); - toastNotifications.addDanger({ - title: i18n.translate( - 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternErrorMessage', + if (status.destIndexPatternDeleted?.success) { + toastNotifications.addSuccess( + i18n.translate( + 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternSuccessMessage', { - defaultMessage: 'An error occurred deleting index pattern {destinationIndex}', + defaultMessage: + 'Request to delete index pattern {destinationIndex} acknowledged.', values: { destinationIndex }, } - ), - text: toMountPoint( - - ), - }); + ) + ); } + } else { + (Object.keys(successCount) as SuccessCountField[]).forEach((key) => { + if (status[key]?.success) { + successCount[key] = successCount[key] + 1; + } + }); } - } - - // if we are deleting multiple transforms, combine the success messages - if (isBulk) { - if (successCount.transformDeleted > 0) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.bulkDeleteTransformSuccessMessage', { - defaultMessage: - 'Successfully deleted {count} {count, plural, one {transform} other {transforms}}.', - values: { count: successCount.transformDeleted }, - }) - ); + if (status.transformDeleted?.error) { + const error = extractErrorMessage(status.transformDeleted.error); + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.transformList.deleteTransformErrorMessage', { + defaultMessage: 'An error occurred deleting the transform {transformId}', + values: { transformId }, + }), + text: toMountPoint( + + ), + }); } - if (successCount.destIndexDeleted > 0) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage', { - defaultMessage: - 'Successfully deleted {count} destination {count, plural, one {index} other {indices}}.', - values: { count: successCount.destIndexDeleted }, - }) - ); + if (status.destIndexDeleted?.error) { + const error = extractErrorMessage(status.destIndexDeleted.error); + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexErrorMessage', + { + defaultMessage: 'An error occurred deleting destination index {destinationIndex}', + values: { destinationIndex }, + } + ), + text: toMountPoint( + + ), + }); } - if (successCount.destIndexPatternDeleted > 0) { - toastNotifications.addSuccess( - i18n.translate( - 'xpack.transform.transformList.bulkDeleteDestIndexPatternSuccessMessage', + + if (status.destIndexPatternDeleted?.error) { + const error = extractErrorMessage(status.destIndexPatternDeleted.error); + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.transform.deleteTransform.deleteAnalyticsWithIndexPatternErrorMessage', { - defaultMessage: - 'Successfully deleted {count} destination index {count, plural, one {pattern} other {patterns}}.', - values: { count: successCount.destIndexPatternDeleted }, + defaultMessage: 'An error occurred deleting index pattern {destinationIndex}', + values: { destinationIndex }, } - ) - ); + ), + text: toMountPoint( + + ), + }); } } + } - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.transformList.deleteTransformGenericErrorMessage', { - defaultMessage: 'An error occurred calling the API endpoint to delete transforms.', - }), - text: toMountPoint( - - ), - }); + // if we are deleting multiple transforms, combine the success messages + if (isBulk) { + if (successCount.transformDeleted > 0) { + toastNotifications.addSuccess( + i18n.translate('xpack.transform.transformList.bulkDeleteTransformSuccessMessage', { + defaultMessage: + 'Successfully deleted {count} {count, plural, one {transform} other {transforms}}.', + values: { count: successCount.transformDeleted }, + }) + ); + } + + if (successCount.destIndexDeleted > 0) { + toastNotifications.addSuccess( + i18n.translate('xpack.transform.transformList.bulkDeleteDestIndexSuccessMessage', { + defaultMessage: + 'Successfully deleted {count} destination {count, plural, one {index} other {indices}}.', + values: { count: successCount.destIndexDeleted }, + }) + ); + } + if (successCount.destIndexPatternDeleted > 0) { + toastNotifications.addSuccess( + i18n.translate('xpack.transform.transformList.bulkDeleteDestIndexPatternSuccessMessage', { + defaultMessage: + 'Successfully deleted {count} destination index {count, plural, one {pattern} other {patterns}}.', + values: { count: successCount.destIndexPatternDeleted }, + }) + ); + } } + + refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); }; }; diff --git a/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts b/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts index bd19a7f8bf4d8..5f3a9a6abfdb4 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_get_transforms.ts @@ -4,52 +4,24 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - TransformListRow, - TransformStats, - TRANSFORM_MODE, - isTransformStats, - TransformPivotConfig, - refreshTransformList$, - REFRESH_TRANSFORM_LIST_STATE, -} from '../common'; - -import { useApi } from './use_api'; +import { HttpFetchError } from 'src/core/public'; -interface GetTransformsResponse { - count: number; - transforms: TransformPivotConfig[]; -} - -interface GetTransformsStatsResponseOk { - node_failures?: object; - count: number; - transforms: TransformStats[]; -} - -const isGetTransformsStatsResponseOk = (arg: any): arg is GetTransformsStatsResponseOk => { - return ( - {}.hasOwnProperty.call(arg, 'count') && - {}.hasOwnProperty.call(arg, 'transforms') && - Array.isArray(arg.transforms) - ); -}; +import { + isGetTransformsResponseSchema, + isGetTransformsStatsResponseSchema, +} from '../../../common/api_schemas/type_guards'; +import { TRANSFORM_MODE } from '../../../common/constants'; +import { isTransformStats } from '../../../common/types/transform_stats'; -interface GetTransformsStatsResponseError { - statusCode: number; - error: string; - message: string; -} +import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; -type GetTransformsStatsResponse = GetTransformsStatsResponseOk | GetTransformsStatsResponseError; +import { useApi } from './use_api'; export type GetTransforms = (forceRefresh?: boolean) => void; export const useGetTransforms = ( setTransforms: React.Dispatch>, - setErrorMessage: React.Dispatch< - React.SetStateAction - >, + setErrorMessage: React.Dispatch>, setIsInitialized: React.Dispatch>, blockRefresh: boolean ): GetTransforms => { @@ -66,45 +38,57 @@ export const useGetTransforms = ( return; } - try { - const transformConfigs: GetTransformsResponse = await api.getTransforms(); - const transformStats: GetTransformsStatsResponse = await api.getTransformsStats(); - - const tableRows = transformConfigs.transforms.reduce((reducedtableRows, config) => { - const stats = isGetTransformsStatsResponseOk(transformStats) - ? transformStats.transforms.find((d) => config.id === d.id) - : undefined; - - // A newly created transform might not have corresponding stats yet. - // If that's the case we just skip the transform and don't add it to the transform list yet. - if (!isTransformStats(stats)) { - return reducedtableRows; - } - - // Table with expandable rows requires `id` on the outer most level - reducedtableRows.push({ - id: config.id, - config, - mode: - typeof config.sync !== 'undefined' ? TRANSFORM_MODE.CONTINUOUS : TRANSFORM_MODE.BATCH, - stats, - }); - return reducedtableRows; - }, [] as TransformListRow[]); + const transformConfigs = await api.getTransforms(); + const transformStats = await api.getTransformsStats(); - setTransforms(tableRows); - setErrorMessage(undefined); - setIsInitialized(true); - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE); - } catch (e) { + if ( + !isGetTransformsResponseSchema(transformConfigs) || + !isGetTransformsStatsResponseSchema(transformStats) + ) { // An error is followed immediately by setting the state to idle. // This way we're able to treat ERROR as a one-time-event like REFRESH. refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.ERROR); refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE); setTransforms([]); - setErrorMessage(e); + setIsInitialized(true); + + if (!isGetTransformsResponseSchema(transformConfigs)) { + setErrorMessage(transformConfigs); + } else if (!isGetTransformsStatsResponseSchema(transformStats)) { + setErrorMessage(transformStats); + } + + return; } + + const tableRows = transformConfigs.transforms.reduce((reducedtableRows, config) => { + const stats = isGetTransformsStatsResponseSchema(transformStats) + ? transformStats.transforms.find((d) => config.id === d.id) + : undefined; + + // A newly created transform might not have corresponding stats yet. + // If that's the case we just skip the transform and don't add it to the transform list yet. + if (!isTransformStats(stats)) { + return reducedtableRows; + } + + // Table with expandable rows requires `id` on the outer most level + reducedtableRows.push({ + id: config.id, + config, + mode: + typeof config.sync !== 'undefined' ? TRANSFORM_MODE.CONTINUOUS : TRANSFORM_MODE.BATCH, + stats, + }); + return reducedtableRows; + }, [] as TransformListRow[]); + + setTransforms(tableRows); + setErrorMessage(undefined); + setIsInitialized(true); + refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.IDLE); + concurrentLoads--; if (concurrentLoads > 0) { diff --git a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts index ad5850f26be2e..ce233d0cf7caa 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_index_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_index_data.ts @@ -8,19 +8,23 @@ import { useEffect } from 'react'; import { EuiDataGridColumn } from '@elastic/eui'; +import { + isEsSearchResponse, + isFieldHistogramsResponseSchema, +} from '../../../common/api_schemas/type_guards'; + import { getFieldType, getDataGridSchemaFromKibanaFieldType, getFieldsFromKibanaIndexPattern, - getErrorMessage, showDataGridColumnChartErrorMessageToast, useDataGrid, useRenderCellValue, EsSorting, - SearchResponse7, UseIndexDataReturnType, INDEX_STATUS, } from '../../shared_imports'; +import { getErrorMessage } from '../../../common/utils/errors'; import { isDefaultQuery, matchAllQuery, PivotQuery } from '../common'; @@ -29,8 +33,6 @@ import { useApi } from './use_api'; import { useToastNotifications } from '../app_dependencies'; -type IndexSearchResponse = SearchResponse7; - export const useIndexData = ( indexPattern: SearchItems['indexPattern'], query: PivotQuery @@ -90,37 +92,39 @@ export const useIndexData = ( }, }; - try { - const resp: IndexSearchResponse = await api.esSearch(esSearchRequest); - - const docs = resp.hits.hits.map((d) => d._source); + const resp = await api.esSearch(esSearchRequest); - setRowCount(resp.hits.total.value); - setTableItems(docs); - setStatus(INDEX_STATUS.LOADED); - } catch (e) { - setErrorMessage(getErrorMessage(e)); + if (!isEsSearchResponse(resp)) { + setErrorMessage(getErrorMessage(resp)); setStatus(INDEX_STATUS.ERROR); return; } + + const docs = resp.hits.hits.map((d) => d._source); + + setRowCount(resp.hits.total.value); + setTableItems(docs); + setStatus(INDEX_STATUS.LOADED); }; const fetchColumnChartsData = async function () { - try { - const columnChartsData = await api.getHistogramsForFields( - indexPattern.title, - columns - .filter((cT) => dataGrid.visibleColumns.includes(cT.id)) - .map((cT) => ({ - fieldName: cT.id, - type: getFieldType(cT.schema), - })), - isDefaultQuery(query) ? matchAllQuery : query - ); - setColumnCharts(columnChartsData); - } catch (e) { - showDataGridColumnChartErrorMessageToast(e, toastNotifications); + const columnChartsData = await api.getHistogramsForFields( + indexPattern.title, + columns + .filter((cT) => dataGrid.visibleColumns.includes(cT.id)) + .map((cT) => ({ + fieldName: cT.id, + type: getFieldType(cT.schema), + })), + isDefaultQuery(query) ? matchAllQuery : query + ); + + if (!isFieldHistogramsResponseSchema(columnChartsData)) { + showDataGridColumnChartErrorMessageToast(columnChartsData, toastNotifications); + return; } + + setColumnCharts(columnChartsData); }; useEffect(() => { diff --git a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts index a9f34996b9b51..c51bf7d7e6741 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_pivot_data.ts @@ -13,26 +13,27 @@ import { i18n } from '@kbn/i18n'; import { ES_FIELD_TYPES } from '../../../../../../src/plugins/data/common'; +import type { PreviewMappingsProperties } from '../../../common/api_schemas/transforms'; +import { isPostTransformsPreviewResponseSchema } from '../../../common/api_schemas/type_guards'; import { dictionaryToArray } from '../../../common/types/common'; -import { formatHumanReadableDateTimeSeconds } from '../../shared_imports'; import { getNestedProperty } from '../../../common/utils/object_utils'; import { - getErrorMessage, + formatHumanReadableDateTimeSeconds, multiColumnSortFactory, useDataGrid, RenderCellValue, UseIndexDataReturnType, INDEX_STATUS, } from '../../shared_imports'; +import { getErrorMessage } from '../../../common/utils/errors'; import { - getPreviewRequestBody, + getPreviewTransformRequestBody, PivotAggsConfigDict, PivotGroupByConfigDict, PivotGroupByConfig, PivotQuery, - PreviewMappings, PivotAggsConfig, } from '../common'; @@ -74,21 +75,23 @@ export const usePivotData = ( aggs: PivotAggsConfigDict, groupBy: PivotGroupByConfigDict ): UseIndexDataReturnType => { - const [previewMappings, setPreviewMappings] = useState({ properties: {} }); + const [previewMappingsProperties, setPreviewMappingsProperties] = useState< + PreviewMappingsProperties + >({}); const api = useApi(); const aggsArr = useMemo(() => dictionaryToArray(aggs), [aggs]); const groupByArr = useMemo(() => dictionaryToArray(groupBy), [groupBy]); // Filters mapping properties of type `object`, which get returned for nested field parents. - const columnKeys = Object.keys(previewMappings.properties).filter( - (key) => previewMappings.properties[key].type !== 'object' + const columnKeys = Object.keys(previewMappingsProperties).filter( + (key) => previewMappingsProperties[key].type !== 'object' ); columnKeys.sort(sortColumns(groupByArr)); // EuiDataGrid State const columns: EuiDataGridColumn[] = columnKeys.map((id) => { - const field = previewMappings.properties[id]; + const field = previewMappingsProperties[id]; // Built-in values are ['boolean', 'currency', 'datetime', 'numeric', 'json'] // To fall back to the default string schema it needs to be undefined. @@ -159,28 +162,35 @@ export const usePivotData = ( setNoDataMessage(''); setStatus(INDEX_STATUS.LOADING); - try { - const previewRequest = getPreviewRequestBody(indexPatternTitle, query, groupByArr, aggsArr); - const resp = await api.getTransformsPreview(previewRequest); - setTableItems(resp.preview); - setRowCount(resp.preview.length); - setPreviewMappings(resp.generated_dest_index.mappings); - setStatus(INDEX_STATUS.LOADED); - - if (resp.preview.length === 0) { - setNoDataMessage( - i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', { - defaultMessage: - 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', - }) - ); - } - } catch (e) { - setErrorMessage(getErrorMessage(e)); + const previewRequest = getPreviewTransformRequestBody( + indexPatternTitle, + query, + groupByArr, + aggsArr + ); + const resp = await api.getTransformsPreview(previewRequest); + + if (!isPostTransformsPreviewResponseSchema(resp)) { + setErrorMessage(getErrorMessage(resp)); setTableItems([]); setRowCount(0); - setPreviewMappings({ properties: {} }); + setPreviewMappingsProperties({}); setStatus(INDEX_STATUS.ERROR); + return; + } + + setTableItems(resp.preview); + setRowCount(resp.preview.length); + setPreviewMappingsProperties(resp.generated_dest_index.mappings.properties); + setStatus(INDEX_STATUS.LOADED); + + if (resp.preview.length === 0) { + setNoDataMessage( + i18n.translate('xpack.transform.pivotPreview.PivotPreviewNoDataCalloutBody', { + defaultMessage: + 'The preview request did not return any data. Please ensure the optional query returns data and that values exist for the field used by group-by and aggregation fields.', + }) + ); } }; @@ -236,19 +246,19 @@ export const usePivotData = ( if ( [ES_FIELD_TYPES.DATE, ES_FIELD_TYPES.DATE_NANOS].includes( - previewMappings.properties[columnId].type + previewMappingsProperties[columnId].type ) ) { return formatHumanReadableDateTimeSeconds(moment(cellValue).unix() * 1000); } - if (previewMappings.properties[columnId].type === ES_FIELD_TYPES.BOOLEAN) { + if (previewMappingsProperties[columnId].type === ES_FIELD_TYPES.BOOLEAN) { return cellValue ? 'true' : 'false'; } return cellValue; }; - }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappings.properties]); + }, [pageData, pagination.pageIndex, pagination.pageSize, previewMappingsProperties]); return { ...dataGrid, diff --git a/x-pack/plugins/transform/public/app/hooks/use_start_transform.ts b/x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx similarity index 52% rename from x-pack/plugins/transform/public/app/hooks/use_start_transform.ts rename to x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx index a0ffe1fdfa336..71ed220b6b4df 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_start_transform.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_start_transform.tsx @@ -4,25 +4,45 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; + import { i18n } from '@kbn/i18n'; -import { TransformEndpointRequest, TransformEndpointResult } from '../../../common'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; + +import type { StartTransformsRequestSchema } from '../../../common/api_schemas/start_transforms'; +import { isStartTransformsResponseSchema } from '../../../common/api_schemas/type_guards'; + +import { getErrorMessage } from '../../../common/utils/errors'; -import { useToastNotifications } from '../app_dependencies'; -import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; +import { useAppDependencies, useToastNotifications } from '../app_dependencies'; +import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; +import { ToastNotificationText } from '../components'; import { useApi } from './use_api'; export const useStartTransforms = () => { + const deps = useAppDependencies(); const toastNotifications = useToastNotifications(); const api = useApi(); - return async (transforms: TransformListRow[]) => { - const transformsInfo: TransformEndpointRequest[] = transforms.map((tf) => ({ - id: tf.config.id, - state: tf.stats.state, - })); - const results: TransformEndpointResult = await api.startTransforms(transformsInfo); + return async (transformsInfo: StartTransformsRequestSchema) => { + const results = await api.startTransforms(transformsInfo); + + if (!isStartTransformsResponseSchema(results)) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.transform.stepCreateForm.startTransformResponseSchemaErrorMessage', + { + defaultMessage: 'An error occurred calling the start transforms request.', + } + ), + text: toMountPoint( + + ), + }); + return; + } for (const transformId in results) { // hasOwnProperty check to ensure only properties on object itself, and not its prototypes diff --git a/x-pack/plugins/transform/public/app/hooks/use_stop_transform.ts b/x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx similarity index 53% rename from x-pack/plugins/transform/public/app/hooks/use_stop_transform.ts rename to x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx index 0df9834647704..be223c5eddfdd 100644 --- a/x-pack/plugins/transform/public/app/hooks/use_stop_transform.ts +++ b/x-pack/plugins/transform/public/app/hooks/use_stop_transform.tsx @@ -4,25 +4,45 @@ * you may not use this file except in compliance with the Elastic License. */ +import React from 'react'; + import { i18n } from '@kbn/i18n'; -import { TransformEndpointRequest, TransformEndpointResult } from '../../../common'; +import { toMountPoint } from '../../../../../../src/plugins/kibana_react/public'; + +import type { StopTransformsRequestSchema } from '../../../common/api_schemas/stop_transforms'; +import { isStopTransformsResponseSchema } from '../../../common/api_schemas/type_guards'; + +import { getErrorMessage } from '../../../common/utils/errors'; -import { useToastNotifications } from '../app_dependencies'; -import { TransformListRow, refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; +import { useAppDependencies, useToastNotifications } from '../app_dependencies'; +import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../common'; +import { ToastNotificationText } from '../components'; import { useApi } from './use_api'; export const useStopTransforms = () => { + const deps = useAppDependencies(); const toastNotifications = useToastNotifications(); const api = useApi(); - return async (transforms: TransformListRow[]) => { - const transformsInfo: TransformEndpointRequest[] = transforms.map((df) => ({ - id: df.config.id, - state: df.stats.state, - })); - const results: TransformEndpointResult = await api.stopTransforms(transformsInfo); + return async (transformsInfo: StopTransformsRequestSchema) => { + const results = await api.stopTransforms(transformsInfo); + + if (!isStopTransformsResponseSchema(results)) { + toastNotifications.addDanger({ + title: i18n.translate( + 'xpack.transform.transformList.stopTransformResponseSchemaErrorMessage', + { + defaultMessage: 'An error occurred called the stop transforms request.', + } + ), + text: toMountPoint( + + ), + }); + return; + } for (const transformId in results) { // hasOwnProperty check to ensure only properties on object itself, and not its prototypes diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx b/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx index 6553d4474d392..790fcaf5fa83c 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/authorization_provider.tsx @@ -6,7 +6,7 @@ import React, { createContext } from 'react'; -import { Privileges } from '../../../../../common'; +import { Privileges } from '../../../../../common/types/privileges'; import { useRequest } from '../../../hooks'; diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts index 282a737d0bf1e..841c6ed01766a 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/common.ts @@ -6,7 +6,7 @@ import { i18n } from '@kbn/i18n'; -import { Privileges } from '../../../../../common'; +import { Privileges } from '../../../../../common/types/privileges'; export interface Capabilities { canGetTransform: boolean; diff --git a/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx b/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx index 89c6ac3a054f7..beeacc76bdc95 100644 --- a/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx +++ b/x-pack/plugins/transform/public/app/lib/authorization/components/with_privileges.tsx @@ -10,7 +10,7 @@ import { EuiPageContent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n/react'; -import { MissingPrivileges } from '../../../../../common'; +import { MissingPrivileges } from '../../../../../common/types/privileges'; import { SectionLoading } from '../../../components'; diff --git a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx index 19ba31d36e6e9..9a97c66bfb10b 100644 --- a/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx +++ b/x-pack/plugins/transform/public/app/sections/clone_transform/clone_transform_section.tsx @@ -21,40 +21,20 @@ import { EuiTitle, } from '@elastic/eui'; +import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; +import { TransformPivotConfig } from '../../../../common/types/transform'; + +import { isHttpFetchError } from '../../common/request'; import { useApi } from '../../hooks/use_api'; import { useDocumentationLinks } from '../../hooks/use_documentation_links'; import { useSearchItems } from '../../hooks/use_search_items'; -import { APP_CREATE_TRANSFORM_CLUSTER_PRIVILEGES } from '../../../../common/constants'; - import { useAppDependencies } from '../../app_dependencies'; -import { TransformPivotConfig } from '../../common'; import { breadcrumbService, docTitleService, BREADCRUMB_SECTION } from '../../services/navigation'; import { PrivilegesWrapper } from '../../lib/authorization'; import { Wizard } from '../create_transform/components/wizard'; -interface GetTransformsResponseOk { - count: number; - transforms: TransformPivotConfig[]; -} - -interface GetTransformsResponseError { - error: { - msg: string; - path: string; - query: any; - statusCode: number; - response: string; - }; -} - -function isGetTransformsResponseError(arg: any): arg is GetTransformsResponseError { - return arg.error !== undefined; -} - -type GetTransformsResponse = GetTransformsResponseOk | GetTransformsResponseError; - type Props = RouteComponentProps<{ transformId: string }>; export const CloneTransformSection: FC = ({ match }) => { // Set breadcrumb and page title @@ -84,15 +64,15 @@ export const CloneTransformSection: FC = ({ match }) => { } = useSearchItems(undefined); const fetchTransformConfig = async () => { - try { - const transformConfigs: GetTransformsResponse = await api.getTransforms(transformId); - if (isGetTransformsResponseError(transformConfigs)) { - setTransformConfig(undefined); - setErrorMessage(transformConfigs.error.msg); - setIsInitialized(true); - return; - } + const transformConfigs = await api.getTransform(transformId); + if (isHttpFetchError(transformConfigs)) { + setTransformConfig(undefined); + setErrorMessage(transformConfigs.message); + setIsInitialized(true); + return; + } + try { await loadIndexPatterns(savedObjectsClient, indexPatterns); const indexPatternTitle = Array.isArray(transformConfigs.transforms[0].source.index) ? transformConfigs.transforms[0].source.index.join(',') diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx index fa0fe7bdf6126..49d59706befb8 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.test.tsx @@ -7,7 +7,10 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { AggName, PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from '../../../../common'; +import { AggName } from '../../../../../../common/types/aggregations'; +import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; + +import { PivotAggsConfig } from '../../../../common'; import { AggLabelForm } from './agg_label_form'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx index e50ba9e137331..4e5e3f71cd6e2 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/agg_label_form.tsx @@ -10,8 +10,9 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPopover, EuiTextColor } from '@elastic/eui'; +import { AggName } from '../../../../../../common/types/aggregations'; + import { - AggName, isPivotAggsConfigWithUiSupport, PivotAggsConfig, PivotAggsConfigWithUiSupportDict, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx index 32c7ca5972e00..93de3d4fcfc9f 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.test.tsx @@ -7,7 +7,9 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from '../../../../common'; +import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; + +import { PivotAggsConfig } from '../../../../common'; import { AggListForm, AggListProps } from './list_form'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx index a02f4455250d7..f6ae1f292b0e6 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_form.tsx @@ -8,8 +8,9 @@ import React, { Fragment } from 'react'; import { EuiPanel, EuiSpacer } from '@elastic/eui'; +import { AggName } from '../../../../../../common/types/aggregations'; + import { - AggName, PivotAggsConfig, PivotAggsConfigDict, PivotAggsConfigWithUiSupportDict, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx index 923d52ba5cec1..8c644c358e658 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.test.tsx @@ -7,7 +7,9 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { PivotAggsConfig, PIVOT_SUPPORTED_AGGS } from '../../../../common'; +import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; + +import { PivotAggsConfig } from '../../../../common'; import { AggListSummary, AggListSummaryProps } from './list_summary'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx index 7d07d79e7d283..fb6e141a54b04 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/list_summary.tsx @@ -8,7 +8,9 @@ import React, { Fragment } from 'react'; import { EuiForm, EuiPanel, EuiSpacer } from '@elastic/eui'; -import { AggName, PivotAggsConfigDict } from '../../../../common'; +import { AggName } from '../../../../../../common/types/aggregations'; + +import { PivotAggsConfigDict } from '../../../../common'; export interface AggListSummaryProps { list: PivotAggsConfigDict; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx index b3e770a269681..8f2fbfb7084e6 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.test.tsx @@ -7,7 +7,10 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { AggName, PIVOT_SUPPORTED_AGGS, PivotAggsConfig } from '../../../../common'; +import { AggName } from '../../../../../../common/types/aggregations'; +import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; + +import { PivotAggsConfig } from '../../../../common'; import { PopoverForm } from './popover_form'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx index 50064274cf98e..30e8c2b594db7 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/aggregation_list/popover_form.tsx @@ -20,10 +20,14 @@ import { import { cloneDeep } from 'lodash'; import { useUpdateEffect } from 'react-use'; +import { AggName } from '../../../../../../common/types/aggregations'; import { dictionaryToArray } from '../../../../../../common/types/common'; +import { + PivotSupportedAggs, + PIVOT_SUPPORTED_AGGS, +} from '../../../../../../common/types/pivot_aggs'; import { - AggName, isAggName, isPivotAggsConfigPercentiles, isPivotAggsConfigWithUiSupport, @@ -31,9 +35,8 @@ import { PERCENTILES_AGG_DEFAULT_PERCENTS, PivotAggsConfig, PivotAggsConfigWithUiSupportDict, - PIVOT_SUPPORTED_AGGS, } from '../../../../common'; -import { isPivotAggsWithExtendedForm, PivotSupportedAggs } from '../../../../common/pivot_aggs'; +import { isPivotAggsWithExtendedForm } from '../../../../common/pivot_aggs'; import { getAggFormConfig } from '../step_define/common/get_agg_form_config'; interface Props { diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx index c79da06ac8080..ff66ed6779e14 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/group_by_label_form.tsx @@ -10,8 +10,9 @@ import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiFlexGroup, EuiFlexItem, EuiPopover, EuiTextColor } from '@elastic/eui'; +import { AggName } from '../../../../../../common/types/aggregations'; + import { - AggName, isGroupByDateHistogram, isGroupByHistogram, PivotGroupByConfig, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx index 2dc1a4332f6ad..a60989c76ab13 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/list_form.tsx @@ -8,8 +8,9 @@ import React, { Fragment } from 'react'; import { EuiPanel, EuiSpacer } from '@elastic/eui'; +import { AggName } from '../../../../../../common/types/aggregations'; + import { - AggName, PivotGroupByConfig, PivotGroupByConfigDict, PivotGroupByConfigWithUiSupportDict, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx index 090f3b19f47fb..13829222f11f5 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.test.tsx @@ -7,7 +7,9 @@ import { shallow } from 'enzyme'; import React from 'react'; -import { AggName, PIVOT_SUPPORTED_GROUP_BY_AGGS, PivotGroupByConfig } from '../../../../common'; +import { AggName } from '../../../../../../common/types/aggregations'; + +import { PIVOT_SUPPORTED_GROUP_BY_AGGS, PivotGroupByConfig } from '../../../../common'; import { isIntervalValid, PopoverForm } from './popover_form'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx index 0452638e90dfb..f0a96fa6ab875 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/group_by_list/popover_form.tsx @@ -18,10 +18,10 @@ import { EuiSpacer, } from '@elastic/eui'; +import { AggName } from '../../../../../../common/types/aggregations'; import { dictionaryToArray } from '../../../../../../common/types/common'; import { - AggName, dateHistogramIntervalFormatRegex, getEsAggFromGroupByConfig, isGroupByDateHistogram, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx index 255a245081d5a..675bd0f9f88ed 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_create/step_create_form.tsx @@ -30,9 +30,15 @@ import { import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; +import type { PutTransformsResponseSchema } from '../../../../../../common/api_schemas/transforms'; +import { + isGetTransformsStatsResponseSchema, + isPutTransformsResponseSchema, + isStartTransformsResponseSchema, +} from '../../../../../../common/api_schemas/type_guards'; import { PROGRESS_REFRESH_INTERVAL_MS } from '../../../../../../common/constants'; -import { getErrorMessage } from '../../../../../shared_imports'; +import { getErrorMessage } from '../../../../../../common/utils/errors'; import { getTransformProgress, getDiscoverUrl } from '../../../../common'; import { useApi } from '../../../../hooks/use_api'; @@ -93,34 +99,28 @@ export const StepCreateForm: FC = React.memo( async function createTransform() { setLoading(true); - try { - const resp = await api.createTransform(transformId, transformConfig); - if (resp.errors !== undefined && Array.isArray(resp.errors)) { - if (resp.errors.length === 1) { - throw resp.errors[0]; - } + const resp = await api.createTransform(transformId, transformConfig); - if (resp.errors.length > 1) { - throw resp.errors; - } + if (!isPutTransformsResponseSchema(resp) || resp.errors.length > 0) { + let respErrors: + | PutTransformsResponseSchema['errors'] + | PutTransformsResponseSchema['errors'][number] + | undefined; + + if (isPutTransformsResponseSchema(resp) && resp.errors.length > 0) { + respErrors = resp.errors.length === 1 ? resp.errors[0] : resp.errors; } - toastNotifications.addSuccess( - i18n.translate('xpack.transform.stepCreateForm.createTransformSuccessMessage', { - defaultMessage: 'Request to create transform {transformId} acknowledged.', - values: { transformId }, - }) - ); - setCreated(true); - setLoading(false); - } catch (e) { toastNotifications.addDanger({ title: i18n.translate('xpack.transform.stepCreateForm.createTransformErrorMessage', { defaultMessage: 'An error occurred creating the transform {transformId}:', values: { transformId }, }), text: toMountPoint( - + ), }); setCreated(false); @@ -128,6 +128,15 @@ export const StepCreateForm: FC = React.memo( return false; } + toastNotifications.addSuccess( + i18n.translate('xpack.transform.stepCreateForm.createTransformSuccessMessage', { + defaultMessage: 'Request to create transform {transformId} acknowledged.', + values: { transformId }, + }) + ); + setCreated(true); + setLoading(false); + if (createIndexPattern) { createKibanaIndexPattern(); } @@ -138,37 +147,36 @@ export const StepCreateForm: FC = React.memo( async function startTransform() { setLoading(true); - try { - const resp = await api.startTransforms([{ id: transformId }]); - if (typeof resp === 'object' && resp !== null && resp[transformId]?.success === true) { - toastNotifications.addSuccess( - i18n.translate('xpack.transform.stepCreateForm.startTransformSuccessMessage', { - defaultMessage: 'Request to start transform {transformId} acknowledged.', - values: { transformId }, - }) - ); - setStarted(true); - setLoading(false); - } else { - const errorMessage = - typeof resp === 'object' && resp !== null && resp[transformId]?.success === false - ? resp[transformId].error - : resp; - throw new Error(errorMessage); - } - } catch (e) { - toastNotifications.addDanger({ - title: i18n.translate('xpack.transform.stepCreateForm.startTransformErrorMessage', { - defaultMessage: 'An error occurred starting the transform {transformId}:', + const resp = await api.startTransforms([{ id: transformId }]); + + if (isStartTransformsResponseSchema(resp) && resp[transformId]?.success === true) { + toastNotifications.addSuccess( + i18n.translate('xpack.transform.stepCreateForm.startTransformSuccessMessage', { + defaultMessage: 'Request to start transform {transformId} acknowledged.', values: { transformId }, - }), - text: toMountPoint( - - ), - }); - setStarted(false); + }) + ); + setStarted(true); setLoading(false); + return; } + + const errorMessage = + isStartTransformsResponseSchema(resp) && resp[transformId]?.success === false + ? resp[transformId].error + : resp; + + toastNotifications.addDanger({ + title: i18n.translate('xpack.transform.stepCreateForm.startTransformErrorMessage', { + defaultMessage: 'An error occurred starting the transform {transformId}:', + values: { transformId }, + }), + text: toMountPoint( + + ), + }); + setStarted(false); + setLoading(false); } async function createAndStartTransform() { @@ -250,27 +258,30 @@ export const StepCreateForm: FC = React.memo( // wrapping in function so we can keep the interval id in local scope function startProgressBar() { const interval = setInterval(async () => { - try { - const stats = await api.getTransformsStats(transformId); - if (stats && Array.isArray(stats.transforms) && stats.transforms.length > 0) { - const percent = - getTransformProgress({ - id: transformConfig.id, - config: transformConfig, - stats: stats.transforms[0], - }) || 0; - setProgressPercentComplete(percent); - if (percent >= 100) { - clearInterval(interval); - } + const stats = await api.getTransformStats(transformId); + + if ( + isGetTransformsStatsResponseSchema(stats) && + Array.isArray(stats.transforms) && + stats.transforms.length > 0 + ) { + const percent = + getTransformProgress({ + id: transformConfig.id, + config: transformConfig, + stats: stats.transforms[0], + }) || 0; + setProgressPercentComplete(percent); + if (percent >= 100) { + clearInterval(interval); } - } catch (e) { + } else { toastNotifications.addDanger({ title: i18n.translate('xpack.transform.stepCreateForm.progressErrorMessage', { defaultMessage: 'An error occurred getting the progress percentage:', }), text: toMountPoint( - + ), }); clearInterval(interval); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts index fba703b1540f9..1523a0d9a89f9 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/apply_transform_config_to_define_state.ts @@ -6,19 +6,21 @@ import { isEqual } from 'lodash'; +import { Dictionary } from '../../../../../../../common/types/common'; +import { PivotSupportedAggs } from '../../../../../../../common/types/pivot_aggs'; +import { TransformPivotConfig } from '../../../../../../../common/types/transform'; + import { matchAllQuery, PivotAggsConfig, PivotAggsConfigDict, PivotGroupByConfig, PivotGroupByConfigDict, - TransformPivotConfig, PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../../common'; -import { Dictionary } from '../../../../../../../common/types/common'; import { StepDefineExposedState } from './types'; -import { getAggConfigFromEsAgg, PivotSupportedAggs } from '../../../../../common/pivot_aggs'; +import { getAggConfigFromEsAgg } from '../../../../../common/pivot_aggs'; export function applyTransformConfigToDefineState( state: StepDefineExposedState, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx index 9d3ab44aa5708..d59f99192621c 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/filter_agg/components/filter_term_form.tsx @@ -10,6 +10,7 @@ import { FormattedMessage } from '@kbn/i18n/react'; import { debounce } from 'lodash'; import { useUpdateEffect } from 'react-use'; import { i18n } from '@kbn/i18n'; +import { isEsSearchResponse } from '../../../../../../../../../common/api_schemas/type_guards'; import { useApi } from '../../../../../../../hooks'; import { CreateTransformWizardContext } from '../../../../wizard/wizard'; import { FilterAggConfigTerm } from '../types'; @@ -55,22 +56,24 @@ export const FilterTermForm: FilterAggConfigTerm['aggTypeConfig']['FilterAggForm }, }; - try { - const response = await api.esSearch(esSearchRequest); - setOptions( - response.aggregations.field_values.buckets.map( - (value: { key: string; doc_count: number }) => ({ label: value.key }) - ) - ); - } catch (e) { + const response = await api.esSearch(esSearchRequest); + + setIsLoading(false); + + if (!isEsSearchResponse(response)) { toastNotifications.addWarning( i18n.translate('xpack.transform.agg.popoverForm.filerAgg.term.errorFetchSuggestions', { defaultMessage: 'Unable to fetch suggestions', }) ); + return; } - setIsLoading(false); + setOptions( + response.aggregations.field_values.buckets.map( + (value: { key: string; doc_count: number }) => ({ label: value.key }) + ) + ); }, 600), [selectedField] ); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_form_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_form_config.ts index 2839c1181c333..5575e6d814daf 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_form_config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_form_config.ts @@ -5,11 +5,11 @@ */ import { - PIVOT_SUPPORTED_AGGS, - PivotAggsConfigBase, - PivotAggsConfigWithUiBase, PivotSupportedAggs, -} from '../../../../../common/pivot_aggs'; + PIVOT_SUPPORTED_AGGS, +} from '../../../../../../../common/types/pivot_aggs'; + +import { PivotAggsConfigBase, PivotAggsConfigWithUiBase } from '../../../../../common/pivot_aggs'; import { getFilterAggConfig } from './filter_agg/config'; /** diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_name_conflict_toast_messages.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_name_conflict_toast_messages.ts index 57f9397089f1d..03cbf2e358736 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_name_conflict_toast_messages.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_agg_name_conflict_toast_messages.ts @@ -6,7 +6,8 @@ import { i18n } from '@kbn/i18n'; -import { AggName, PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common'; +import { AggName } from '../../../../../../../common/types/aggregations'; +import { PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common'; export function getAggNameConflictToastMessages( aggName: AggName, @@ -36,7 +37,7 @@ export function getAggNameConflictToastMessages( // check the new aggName against existing aggs and groupbys const aggNameSplit = aggName.split('.'); let aggNameCheck: string; - aggNameSplit.forEach((aggNamePart) => { + aggNameSplit.forEach((aggNamePart: string) => { aggNameCheck = aggNameCheck === undefined ? aggNamePart : `${aggNameCheck}.${aggNamePart}`; if (aggList[aggNameCheck] !== undefined || groupByList[aggNameCheck] !== undefined) { conflicts.push( diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts index 460164c9afe73..14c03aebe892a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_aggregation_config.ts @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { EsFieldName } from '../../../../../../../common/types/fields'; import { - EsFieldName, - PERCENTILES_AGG_DEFAULT_PERCENTS, + PivotSupportedAggs, PIVOT_SUPPORTED_AGGS, +} from '../../../../../../../common/types/pivot_aggs'; +import { + PERCENTILES_AGG_DEFAULT_PERCENTS, PivotAggsConfigWithUiSupport, } from '../../../../../common'; -import { PivotSupportedAggs } from '../../../../../common/pivot_aggs'; import { getFilterAggConfig } from './filter_agg/config'; /** diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_group_by_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_group_by_config.ts index 712a745ff6e77..657e8c935b875 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_group_by_config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/get_default_group_by_config.ts @@ -4,11 +4,9 @@ * you may not use this file except in compliance with the Elastic License. */ -import { - EsFieldName, - GroupByConfigWithUiSupport, - PIVOT_SUPPORTED_GROUP_BY_AGGS, -} from '../../../../../common'; +import { EsFieldName } from '../../../../../../../common/types/fields'; + +import { GroupByConfigWithUiSupport, PIVOT_SUPPORTED_GROUP_BY_AGGS } from '../../../../../common'; export function getDefaultGroupByConfig( aggName: string, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts index 56fde98cd4c71..955982aae6007 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/common/types.ts @@ -6,7 +6,9 @@ import { KBN_FIELD_TYPES } from '../../../../../../../../../../src/plugins/data/public'; -import { EsFieldName, PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common'; +import { EsFieldName } from '../../../../../../../common/types/fields'; + +import { PivotAggsConfigDict, PivotGroupByConfigDict } from '../../../../../common'; import { SavedSearchQuery } from '../../../../../hooks/use_search_items'; import { QUERY_LANGUAGE } from './constants'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts index 2e92114286599..41b84f04db852 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_pivot_editor.ts @@ -8,13 +8,13 @@ import { useEffect, useState } from 'react'; import { useXJsonMode } from '../../../../../../../../../../src/plugins/es_ui_shared/static/ace_x_json/hooks'; -import { PreviewRequestBody } from '../../../../../common'; +import { PostTransformsPreviewRequestSchema } from '../../../../../../../common/api_schemas/transforms'; import { StepDefineExposedState } from '../common'; export const useAdvancedPivotEditor = ( defaults: StepDefineExposedState, - previewRequest: PreviewRequestBody + previewRequest: PostTransformsPreviewRequestSchema ) => { const stringifiedPivotConfig = JSON.stringify(previewRequest.pivot, null, 2); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_source_editor.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_source_editor.ts index 1ea8a45248fb9..3f930711b970a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_source_editor.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_advanced_source_editor.ts @@ -6,13 +6,13 @@ import { useState } from 'react'; -import { PreviewRequestBody } from '../../../../../common'; +import { PostTransformsPreviewRequestSchema } from '../../../../../../../common/api_schemas/transforms'; import { StepDefineExposedState } from '../common'; export const useAdvancedSourceEditor = ( defaults: StepDefineExposedState, - previewRequest: PreviewRequestBody + previewRequest: PostTransformsPreviewRequestSchema ) => { const stringifiedSourceConfig = JSON.stringify(previewRequest.source.query, null, 2); diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts index d35d567fc8469..90b28f0e305a5 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_pivot_config.ts @@ -6,11 +6,11 @@ import { useCallback, useMemo, useState } from 'react'; +import { AggName } from '../../../../../../../common/types/aggregations'; import { dictionaryToArray } from '../../../../../../../common/types/common'; import { useToastNotifications } from '../../../../../app_dependencies'; import { - AggName, DropDownLabel, PivotAggsConfig, PivotAggsConfigDict, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts index f5980ae2243d3..7c10201fc3a6e 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/hooks/use_step_define_form.ts @@ -6,7 +6,7 @@ import { useEffect } from 'react'; -import { getPreviewRequestBody } from '../../../../../common'; +import { getPreviewTransformRequestBody } from '../../../../../common'; import { getDefaultStepDefineState } from '../common'; @@ -26,7 +26,7 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi const searchBar = useSearchBar(defaults, indexPattern); const pivotConfig = usePivotConfig(defaults, indexPattern); - const previewRequest = getPreviewRequestBody( + const previewRequest = getPreviewTransformRequestBody( indexPattern.title, searchBar.state.pivotQuery, pivotConfig.state.pivotGroupByArr, @@ -41,7 +41,7 @@ export const useStepDefineForm = ({ overrides, onChange, searchItems }: StepDefi useEffect(() => { if (!advancedSourceEditor.state.isAdvancedSourceEditorEnabled) { - const previewRequestUpdate = getPreviewRequestBody( + const previewRequestUpdate = getPreviewTransformRequestBody( indexPattern.title, searchBar.state.pivotQuery, pivotConfig.state.pivotGroupByArr, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx index 8c919a5185d7e..986ac0a212e8a 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.test.tsx @@ -15,10 +15,11 @@ import { coreMock } from '../../../../../../../../../src/core/public/mocks'; import { dataPluginMock } from '../../../../../../../../../src/plugins/data/public/mocks'; const startMock = coreMock.createStart(); +import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; + import { PivotAggsConfigDict, PivotGroupByConfigDict, - PIVOT_SUPPORTED_AGGS, PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../common'; import { SearchItems } from '../../../../hooks/use_search_items'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx index e0b350542a8f8..10f473074b4d7 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_form.tsx @@ -22,6 +22,9 @@ import { EuiText, } from '@elastic/eui'; +import { PivotAggDict } from '../../../../../../common/types/pivot_aggs'; +import { PivotGroupByDict } from '../../../../../../common/types/pivot_group_by'; + import { DataGrid } from '../../../../../shared_imports'; import { @@ -30,10 +33,8 @@ import { } from '../../../../common/data_grid'; import { - getPreviewRequestBody, - PivotAggDict, + getPreviewTransformRequestBody, PivotAggsConfigDict, - PivotGroupByDict, PivotGroupByConfigDict, PivotSupportedGroupByAggs, PivotAggsConfig, @@ -87,7 +88,7 @@ export const StepDefineForm: FC = React.memo((props) => { toastNotifications, }; - const previewRequest = getPreviewRequestBody( + const previewRequest = getPreviewTransformRequestBody( indexPattern.title, pivotQuery, pivotGroupByArr, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx index dc3d950938c9e..f8a060e0007b8 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.test.tsx @@ -7,10 +7,11 @@ import React from 'react'; import { render, wait } from '@testing-library/react'; +import { PIVOT_SUPPORTED_AGGS } from '../../../../../../common/types/pivot_aggs'; + import { PivotAggsConfig, PivotGroupByConfig, - PIVOT_SUPPORTED_AGGS, PIVOT_SUPPORTED_GROUP_BY_AGGS, } from '../../../../common'; import { SearchItems } from '../../../../hooks/use_search_items'; diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx index 414f6e37504da..fa4f8a7e09690 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_define/step_define_summary.tsx @@ -18,7 +18,7 @@ import { useToastNotifications } from '../../../../app_dependencies'; import { getPivotQuery, getPivotPreviewDevConsoleStatement, - getPreviewRequestBody, + getPreviewTransformRequestBody, isDefaultQuery, isMatchAllQuery, } from '../../../../common'; @@ -44,7 +44,7 @@ export const StepDefineSummary: FC = ({ const pivotGroupByArr = dictionaryToArray(groupByList); const pivotQuery = getPivotQuery(searchQuery); - const previewRequest = getPreviewRequestBody( + const previewRequest = getPreviewTransformRequestBody( searchItems.indexPattern.title, pivotQuery, pivotGroupByArr, diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx index 271fde27f519a..43d4f11cffc9d 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/step_details/step_details_form.tsx @@ -11,24 +11,28 @@ import { i18n } from '@kbn/i18n'; import { EuiLink, EuiSwitch, EuiFieldText, EuiForm, EuiFormRow, EuiSelect } from '@elastic/eui'; import { KBN_FIELD_TYPES } from '../../../../../../../../../src/plugins/data/common'; - import { toMountPoint } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { TransformId } from '../../../../../../common'; + +import { + isEsIndices, + isPostTransformsPreviewResponseSchema, +} from '../../../../../../common/api_schemas/type_guards'; +import { TransformId, TransformPivotConfig } from '../../../../../../common/types/transform'; import { isValidIndexName } from '../../../../../../common/utils/es_utils'; -import { getErrorMessage } from '../../../../../shared_imports'; +import { getErrorMessage } from '../../../../../../common/utils/errors'; import { useAppDependencies, useToastNotifications } from '../../../../app_dependencies'; import { ToastNotificationText } from '../../../../components'; +import { isHttpFetchError } from '../../../../common/request'; import { useDocumentationLinks } from '../../../../hooks/use_documentation_links'; import { SearchItems } from '../../../../hooks/use_search_items'; import { useApi } from '../../../../hooks/use_api'; import { StepDetailsTimeField } from './step_details_time_field'; import { getPivotQuery, - getPreviewRequestBody, + getPreviewTransformRequestBody, isTransformIdValid, - TransformPivotConfig, } from '../../../../common'; import { EsIndexName, IndexPatternTitle } from './common'; import { delayValidator } from '../../../../common/validators'; @@ -48,10 +52,12 @@ export interface StepDetailsExposedState { indexPatternDateField?: string | undefined; } +const defaultContinuousModeDelay = '60s'; + export function getDefaultStepDetailsState(): StepDetailsExposedState { return { continuousModeDateField: '', - continuousModeDelay: '60s', + continuousModeDelay: defaultContinuousModeDelay, createIndexPattern: true, isContinuousModeEnabled: false, transformId: '', @@ -72,7 +78,7 @@ export function applyTransformConfigToDetailsState( const time = transformConfig.sync?.time; if (time !== undefined) { state.continuousModeDateField = time.field; - state.continuousModeDelay = time.delay; + state.continuousModeDelay = time?.delay ?? defaultContinuousModeDelay; state.isContinuousModeEnabled = true; } } @@ -137,19 +143,20 @@ export const StepDetailsForm: FC = React.memo( useEffect(() => { // use an IIFE to avoid returning a Promise to useEffect. (async function () { - try { - const { searchQuery, groupByList, aggList } = stepDefineState; - const pivotAggsArr = dictionaryToArray(aggList); - const pivotGroupByArr = dictionaryToArray(groupByList); - const pivotQuery = getPivotQuery(searchQuery); - const previewRequest = getPreviewRequestBody( - searchItems.indexPattern.title, - pivotQuery, - pivotGroupByArr, - pivotAggsArr - ); - - const transformPreview = await api.getTransformsPreview(previewRequest); + const { searchQuery, groupByList, aggList } = stepDefineState; + const pivotAggsArr = dictionaryToArray(aggList); + const pivotGroupByArr = dictionaryToArray(groupByList); + const pivotQuery = getPivotQuery(searchQuery); + const previewRequest = getPreviewTransformRequestBody( + searchItems.indexPattern.title, + pivotQuery, + pivotGroupByArr, + pivotAggsArr + ); + + const transformPreview = await api.getTransformsPreview(previewRequest); + + if (isPostTransformsPreviewResponseSchema(transformPreview)) { const properties = transformPreview.generated_dest_index.mappings.properties; const datetimeColumns: string[] = Object.keys(properties).filter( (col) => properties[col].type === 'date' @@ -157,43 +164,46 @@ export const StepDetailsForm: FC = React.memo( setPreviewDateColumns(datetimeColumns); setIndexPatternDateField(datetimeColumns[0]); - } catch (e) { + } else { toastNotifications.addDanger({ title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformPreview', { - defaultMessage: 'An error occurred getting transform preview', + defaultMessage: 'An error occurred fetching the transform preview', }), text: toMountPoint( - + ), }); } - try { - setTransformIds( - (await api.getTransforms()).transforms.map( - (transform: TransformPivotConfig) => transform.id - ) - ); - } catch (e) { + const resp = await api.getTransforms(); + + if (isHttpFetchError(resp)) { toastNotifications.addDanger({ title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingTransformList', { defaultMessage: 'An error occurred getting the existing transform IDs:', }), text: toMountPoint( - + ), }); + } else { + setTransformIds(resp.transforms.map((transform: TransformPivotConfig) => transform.id)); } - try { - setIndexNames((await api.getIndices()).map((index) => index.name)); - } catch (e) { + const indices = await api.getEsIndices(); + + if (isEsIndices(indices)) { + setIndexNames(indices.map((index) => index.name)); + } else { toastNotifications.addDanger({ title: i18n.translate('xpack.transform.stepDetailsForm.errorGettingIndexNames', { defaultMessage: 'An error occurred getting the existing index names:', }), text: toMountPoint( - + ), }); } diff --git a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx index 806dcbfa75604..0ca018972cac9 100644 --- a/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx +++ b/x-pack/plugins/transform/public/app/sections/create_transform/components/wizard/wizard.tsx @@ -10,7 +10,9 @@ import { i18n } from '@kbn/i18n'; import { EuiSteps, EuiStepStatus } from '@elastic/eui'; -import { getCreateRequestBody, TransformPivotConfig } from '../../../../common'; +import { TransformPivotConfig } from '../../../../../../common/types/transform'; + +import { getCreateTransformRequestBody } from '../../../../common'; import { SearchItems } from '../../../../hooks/use_search_items'; import { @@ -149,7 +151,7 @@ export const Wizard: FC = React.memo(({ cloneConfig, searchItems }) } }, []); - const transformConfig = getCreateRequestBody( + const transformConfig = getCreateTransformRequestBody( indexPattern.title, stepDefineState, stepDetailsState diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.tsx index d8ab72f15c59c..75868fb8fcabd 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/delete_action_name.tsx @@ -7,7 +7,7 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip } from '@elastic/eui'; -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TransformState, TRANSFORM_STATE } from '../../../../../../common/constants'; import { createCapabilityFailureMessage } from '../../../../lib/authorization'; import { TransformListRow } from '../../../../common'; @@ -18,8 +18,8 @@ export const deleteActionNameText = i18n.translate( } ); -const transformCanNotBeDeleted = (item: TransformListRow) => - ![TRANSFORM_STATE.STOPPED, TRANSFORM_STATE.FAILED].includes(item.stats.state); +const transformCanNotBeDeleted = (i: TransformListRow) => + !([TRANSFORM_STATE.STOPPED, TRANSFORM_STATE.FAILED] as TransformState[]).includes(i.stats.state); export const isDeleteActionDisabled = (items: TransformListRow[], forceDisable: boolean) => { const disabled = items.some(transformCanNotBeDeleted); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.tsx index e573709fa6e63..7e8e099b69f82 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_delete/use_delete_action.tsx @@ -6,7 +6,7 @@ import React, { useContext, useMemo, useState } from 'react'; -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TRANSFORM_STATE } from '../../../../../../common/constants'; import { TransformListAction, TransformListRow } from '../../../../common'; import { useDeleteIndexAndTargetIndex, useDeleteTransforms } from '../../../../hooks'; @@ -55,7 +55,16 @@ export const useDeleteAction = (forceDisable: boolean) => { const forceDelete = isBulkAction ? shouldForceDelete : items[0] && items[0] && items[0].stats.state === TRANSFORM_STATE.FAILED; - deleteTransforms(items, shouldDeleteDestIndex, shouldDeleteDestIndexPattern, forceDelete); + + deleteTransforms({ + transformsInfo: items.map((i) => ({ + id: i.config.id, + state: i.stats.state, + })), + deleteDestIndex: shouldDeleteDestIndex, + deleteDestIndexPattern: shouldDeleteDestIndexPattern, + forceDelete, + }); }; const openModal = (newItems: TransformListRow[]) => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx index 1fe20f1acae5a..192ff7ac74c57 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_edit/use_edit_action.tsx @@ -6,7 +6,9 @@ import React, { useContext, useMemo, useState } from 'react'; -import { TransformListAction, TransformListRow, TransformPivotConfig } from '../../../../common'; +import { TransformPivotConfig } from '../../../../../../common/types/transform'; + +import { TransformListAction, TransformListRow } from '../../../../common'; import { AuthorizationContext } from '../../../../lib/authorization'; import { editActionNameText, EditActionName } from './edit_action_name'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx index 191df0c16cba0..ca1c90b9b8fae 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/start_action_name.tsx @@ -8,7 +8,7 @@ import React, { FC, useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip } from '@elastic/eui'; -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TRANSFORM_STATE } from '../../../../../../common/constants'; import { createCapabilityFailureMessage, diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx index 8d6a4376c55b3..96af60778d6a4 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_start/use_start_action.tsx @@ -6,7 +6,7 @@ import React, { useContext, useMemo, useState } from 'react'; -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TRANSFORM_STATE } from '../../../../../../common/constants'; import { AuthorizationContext } from '../../../../lib/authorization'; import { TransformListAction, TransformListRow } from '../../../../common'; @@ -27,7 +27,7 @@ export const useStartAction = (forceDisable: boolean) => { const startAndCloseModal = () => { setModalVisible(false); - startTransforms(items); + startTransforms(items.map((i) => ({ id: i.id }))); }; const openModal = (newItems: TransformListRow[]) => { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.tsx index e1ea82cb371e8..4ec30faa4d76b 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/stop_action_name.tsx @@ -8,7 +8,7 @@ import React, { FC, useContext } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiToolTip } from '@elastic/eui'; -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TRANSFORM_STATE } from '../../../../../../common/constants'; import { TransformListRow } from '../../../../common'; import { diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx index e0a7e0b489ab6..4c872114a82ab 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/action_stop/use_stop_action.tsx @@ -6,7 +6,7 @@ import React, { useCallback, useContext, useMemo } from 'react'; -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TRANSFORM_STATE } from '../../../../../../common/constants'; import { AuthorizationContext } from '../../../../lib/authorization'; import { TransformListAction, TransformListRow } from '../../../../common'; @@ -20,9 +20,10 @@ export const useStopAction = (forceDisable: boolean) => { const stopTransforms = useStopTransforms(); - const clickHandler = useCallback((item: TransformListRow) => stopTransforms([item]), [ - stopTransforms, - ]); + const clickHandler = useCallback( + (i: TransformListRow) => stopTransforms([{ id: i.id, state: i.stats.state }]), + [stopTransforms] + ); const action: TransformListAction = useMemo( () => ({ diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx index 77a7ae25ce887..f9cdac51b6582 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/edit_transform_flyout.tsx @@ -23,13 +23,12 @@ import { EuiTitle, } from '@elastic/eui'; -import { getErrorMessage } from '../../../../../shared_imports'; +import { isPostTransformsUpdateResponseSchema } from '../../../../../../common/api_schemas/type_guards'; +import { TransformPivotConfig } from '../../../../../../common/types/transform'; -import { - refreshTransformList$, - TransformPivotConfig, - REFRESH_TRANSFORM_LIST_STATE, -} from '../../../../common'; +import { getErrorMessage } from '../../../../../../common/utils/errors'; + +import { refreshTransformList$, REFRESH_TRANSFORM_LIST_STATE } from '../../../../common'; import { useToastNotifications } from '../../../../app_dependencies'; import { useApi } from '../../../../hooks/use_api'; @@ -58,19 +57,21 @@ export const EditTransformFlyout: FC = ({ closeFlyout, const requestConfig = applyFormFieldsToTransformConfig(config, state.formFields); const transformId = config.id; - try { - await api.updateTransform(transformId, requestConfig); - toastNotifications.addSuccess( - i18n.translate('xpack.transform.transformList.editTransformSuccessMessage', { - defaultMessage: 'Transform {transformId} updated.', - values: { transformId }, - }) - ); - closeFlyout(); - refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); - } catch (e) { - setErrorMessage(getErrorMessage(e)); + const resp = await api.updateTransform(transformId, requestConfig); + + if (!isPostTransformsUpdateResponseSchema(resp)) { + setErrorMessage(getErrorMessage(resp)); + return; } + + toastNotifications.addSuccess( + i18n.translate('xpack.transform.transformList.editTransformSuccessMessage', { + defaultMessage: 'Transform {transformId} updated.', + values: { transformId }, + }) + ); + closeFlyout(); + refreshTransformList$.next(REFRESH_TRANSFORM_LIST_STATE.REFRESH); } const isUpdateButtonDisabled = !state.isFormValid || !state.isFormTouched; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.ts index 4a8b26b601ae2..12e60c2af5556 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TransformPivotConfig } from '../../../../common'; +import { TransformPivotConfig } from '../../../../../../common/types/transform'; import { applyFormFieldsToTransformConfig, @@ -86,9 +86,7 @@ describe('Transform: applyFormFieldsToTransformConfig()', () => { }); test('should include previously nonexisting attributes', () => { - const transformConfigMock = getTransformConfigMock(); - delete transformConfigMock.description; - delete transformConfigMock.frequency; + const { description, frequency, ...transformConfigMock } = getTransformConfigMock(); const updateConfig = applyFormFieldsToTransformConfig(transformConfigMock, { description: getDescriptionFieldMock('the-new-description'), diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts index 649db51e6ea78..d622a7e9cc040 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/edit_transform_flyout/use_edit_transform_flyout.ts @@ -9,7 +9,8 @@ import { useReducer } from 'react'; import { i18n } from '@kbn/i18n'; -import { TransformPivotConfig } from '../../../../common'; +import { PostTransformsUpdateRequestSchema } from '../../../../../../common/api_schemas/update_transforms'; +import { TransformPivotConfig } from '../../../../../../common/types/transform'; // A Validator function takes in a value to check and returns an array of error messages. // If no messages (empty array) get returned, the value is valid. @@ -118,53 +119,35 @@ interface Action { value: string; } -// Some attributes can have a value of `null` to trigger -// a reset to the default value, or in the case of `docs_per_second` -// `null` is used to disable throttling. -interface UpdateTransformPivotConfig { - description: string; - frequency: string; - settings: { - docs_per_second: number | null; - }; -} - // Takes in the form configuration and returns a // request object suitable to be sent to the // transform update API endpoint. export const applyFormFieldsToTransformConfig = ( config: TransformPivotConfig, { description, docsPerSecond, frequency }: EditTransformFlyoutFieldsState -): Partial => { - const updateConfig: Partial = {}; - - // set the values only if they changed from the default - // and actually differ from the previous value. - if ( - !(config.frequency === undefined && frequency.value === '') && - config.frequency !== frequency.value - ) { - updateConfig.frequency = frequency.value; - } - - if ( - !(config.description === undefined && description.value === '') && - config.description !== description.value - ) { - updateConfig.description = description.value; - } - +): PostTransformsUpdateRequestSchema => { // if the input field was left empty, // fall back to the default value of `null` // which will disable throttling. const docsPerSecondFormValue = docsPerSecond.value !== '' ? parseInt(docsPerSecond.value, 10) : null; const docsPerSecondConfigValue = config.settings?.docs_per_second ?? null; - if (docsPerSecondFormValue !== docsPerSecondConfigValue) { - updateConfig.settings = { docs_per_second: docsPerSecondFormValue }; - } - return updateConfig; + return { + // set the values only if they changed from the default + // and actually differ from the previous value. + ...(!(config.frequency === undefined && frequency.value === '') && + config.frequency !== frequency.value + ? { frequency: frequency.value } + : {}), + ...(!(config.description === undefined && description.value === '') && + config.description !== description.value + ? { description: description.value } + : {}), + ...(docsPerSecondFormValue !== docsPerSecondConfigValue + ? { settings: { docs_per_second: docsPerSecondFormValue } } + : {}), + }; }; // Takes in a transform configuration and returns diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/common.test.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/common.test.ts index 11e4dc3dfa2b8..f6708f7c36f26 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/common.test.ts +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/common.test.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TRANSFORM_STATE } from '../../../../../../common/constants'; import mockTransformListRow from '../../../../common/__mocks__/transform_list_row.json'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx index 08545c288ba96..02bad50dc0dfd 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_messages_pane.tsx @@ -9,11 +9,15 @@ import React, { useState } from 'react'; import { EuiSpacer, EuiBasicTable } from '@elastic/eui'; // @ts-ignore import { formatDate } from '@elastic/eui/lib/services/format'; -import { i18n } from '@kbn/i18n'; import theme from '@elastic/eui/dist/eui_theme_light.json'; + +import { i18n } from '@kbn/i18n'; + +import { isGetTransformsAuditMessagesResponseSchema } from '../../../../../../common/api_schemas/type_guards'; +import { TransformMessage } from '../../../../../../common/types/messages'; + import { useApi } from '../../../../hooks/use_api'; import { JobIcon } from '../../../../components/job_icon'; -import { TransformMessage } from '../../../../../../common/types/messages'; import { useRefreshTransformList } from '../../../../common'; const TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss'; @@ -36,25 +40,16 @@ export const ExpandedRowMessagesPane: React.FC = ({ transformId }) => { let concurrentLoads = 0; return async function getMessages() { - try { - concurrentLoads++; - - if (concurrentLoads > 1) { - return; - } + concurrentLoads++; - setIsLoading(true); - const messagesResp = await api.getTransformAuditMessages(transformId); - setIsLoading(false); - setMessages(messagesResp as any[]); + if (concurrentLoads > 1) { + return; + } - concurrentLoads--; + setIsLoading(true); + const messagesResp = await api.getTransformAuditMessages(transformId); - if (concurrentLoads > 0) { - concurrentLoads = 0; - getMessages(); - } - } catch (error) { + if (!isGetTransformsAuditMessagesResponseSchema(messagesResp)) { setIsLoading(false); setErrorMessage( i18n.translate( @@ -64,6 +59,17 @@ export const ExpandedRowMessagesPane: React.FC = ({ transformId }) => { } ) ); + return; + } + + setIsLoading(false); + setMessages(messagesResp as any[]); + + concurrentLoads--; + + if (concurrentLoads > 0) { + concurrentLoads = 0; + getMessages(); } }; }; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx index a917fc73ad8fb..87d9a25dababd 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/expanded_row_preview_pane.tsx @@ -6,10 +6,11 @@ import React, { useMemo, FC } from 'react'; +import { TransformPivotConfig } from '../../../../../../common/types/transform'; import { DataGrid } from '../../../../../shared_imports'; import { useToastNotifications } from '../../../../app_dependencies'; -import { getPivotQuery, TransformPivotConfig } from '../../../../common'; +import { getPivotQuery } from '../../../../common'; import { usePivotData } from '../../../../hooks/use_pivot_data'; import { SearchItems } from '../../../../hooks/use_search_items'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx index cf32bf0455a1b..12836c0a18ce2 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_list.tsx @@ -4,31 +4,30 @@ * you may not use this file except in compliance with the Elastic License. */ -import React, { MouseEventHandler, FC, useContext, useState } from 'react'; +import React, { MouseEventHandler, FC, useContext, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { - Direction, - EuiBadge, + EuiBasicTable, + EuiBasicTableProps, EuiButtonEmpty, EuiButtonIcon, EuiCallOut, EuiEmptyPrompt, EuiFlexGroup, EuiFlexItem, - EuiInMemoryTable, - EuiSearchBarProps, + EuiSearchBar, + EuiSpacer, EuiPopover, EuiTitle, } from '@elastic/eui'; -import { TransformId, TRANSFORM_STATE } from '../../../../../../common'; +import { TransformId } from '../../../../../../common/types/transform'; import { useRefreshTransformList, TransformListRow, - TRANSFORM_MODE, TRANSFORM_LIST_COLUMN, } from '../../../../common'; import { useStopTransforms } from '../../../../hooks'; @@ -45,9 +44,11 @@ import { import { useStartAction, StartActionName, StartActionModal } from '../action_start'; import { StopActionName } from '../action_stop'; -import { ItemIdToExpandedRowMap, Clause, TermClause, FieldClause, Value } from './common'; -import { getTaskStateBadge, useColumns } from './use_columns'; +import { ItemIdToExpandedRowMap } from './common'; +import { useColumns } from './use_columns'; import { ExpandedRow } from './expanded_row'; +import { TransformSearchBar, filterTransforms } from './transform_search_bar'; +import { useTableSettings } from './use_table_settings'; function getItemIdToExpandedRowMap( itemIds: TransformId[], @@ -62,14 +63,6 @@ function getItemIdToExpandedRowMap( }, {} as ItemIdToExpandedRowMap); } -function stringMatch(str: string | undefined, substr: any) { - return ( - typeof str === 'string' && - typeof substr === 'string' && - (str.toLowerCase().match(substr.toLowerCase()) === null) === false - ); -} - interface Props { errorMessage: any; isInitialized: boolean; @@ -88,24 +81,14 @@ export const TransformList: FC = ({ const [isLoading, setIsLoading] = useState(false); const { refresh } = useRefreshTransformList({ isLoading: setIsLoading }); - const [filterActive, setFilterActive] = useState(false); - + const [searchQueryText, setSearchQueryText] = useState(''); const [filteredTransforms, setFilteredTransforms] = useState([]); const [expandedRowItemIds, setExpandedRowItemIds] = useState([]); - const [transformSelection, setTransformSelection] = useState([]); const [isActionsMenuOpen, setIsActionsMenuOpen] = useState(false); const bulkStartAction = useStartAction(false); const bulkDeleteAction = useDeleteAction(false); - const [searchError, setSearchError] = useState(undefined); - - const [pageIndex, setPageIndex] = useState(0); - const [pageSize, setPageSize] = useState(10); - - const [sortField, setSortField] = useState(TRANSFORM_LIST_COLUMN.ID); - const [sortDirection, setSortDirection] = useState('asc'); - const stopTransforms = useStopTransforms(); const { capabilities } = useContext(AuthorizationContext); @@ -114,90 +97,41 @@ export const TransformList: FC = ({ !capabilities.canPreviewTransform || !capabilities.canStartStopTransform; - const onQueryChange = ({ - query, - error, - }: Parameters>[0]) => { - if (error) { - setSearchError(error.message); + const { columns, modals: singleActionModals } = useColumns( + expandedRowItemIds, + setExpandedRowItemIds, + transformSelection + ); + + const updateFilteredItems = (queryClauses: any) => { + if (queryClauses.length) { + const filtered = filterTransforms(transforms, queryClauses); + setFilteredTransforms(filtered); } else { - let clauses: Clause[] = []; - if (query && query.ast !== undefined && query.ast.clauses !== undefined) { - clauses = query.ast.clauses; - } - if (clauses.length > 0) { - setFilterActive(true); - filterTransforms(clauses as Array); - } else { - setFilterActive(false); - } - setSearchError(undefined); + setFilteredTransforms(transforms); } }; - const filterTransforms = (clauses: Array) => { - setIsLoading(true); - // keep count of the number of matches we make as we're looping over the clauses - // we only want to return transforms which match all clauses, i.e. each search term is ANDed - // { transform-one: { transform: { id: transform-one, config: {}, state: {}, ... }, count: 0 }, transform-two: {...} } - const matches: Record = transforms.reduce((p: Record, c) => { - p[c.id] = { - transform: c, - count: 0, - }; - return p; - }, {}); - - clauses.forEach((c) => { - // the search term could be negated with a minus, e.g. -bananas - const bool = c.match === 'must'; - let ts = []; - - if (c.type === 'term') { - // filter term based clauses, e.g. bananas - // match on ID and description - // if the term has been negated, AND the matches - if (bool === true) { - ts = transforms.filter( - (transform) => - stringMatch(transform.id, c.value) === bool || - stringMatch(transform.config.description, c.value) === bool - ); - } else { - ts = transforms.filter( - (transform) => - stringMatch(transform.id, c.value) === bool && - stringMatch(transform.config.description, c.value) === bool - ); + useEffect(() => { + const filterList = () => { + if (searchQueryText !== '') { + const query = EuiSearchBar.Query.parse(searchQueryText); + let clauses: any = []; + if (query && query.ast !== undefined && query.ast.clauses !== undefined) { + clauses = query.ast.clauses; } + updateFilteredItems(clauses); } else { - // filter other clauses, i.e. the mode and status filters - if (Array.isArray(c.value)) { - // the status value is an array of string(s) e.g. ['failed', 'stopped'] - ts = transforms.filter((transform) => - (c.value as Value[]).includes(transform.stats.state) - ); - } else { - ts = transforms.filter((transform) => transform.mode === c.value); - } + updateFilteredItems([]); } - - ts.forEach((t) => matches[t.id].count++); - }); - - // loop through the matches and return only transforms which have match all the clauses - const filtered = Object.values(matches) - .filter((m) => (m && m.count) >= clauses.length) - .map((m) => m.transform); - - setFilteredTransforms(filtered); - setIsLoading(false); - }; - - const { columns, modals: singleActionModals } = useColumns( - expandedRowItemIds, - setExpandedRowItemIds, - transformSelection + }; + filterList(); + // eslint-disable-next-line + }, [searchQueryText, transforms]); // missing dependency updateFilteredItems + + const { onTableChange, pageOfItems, pagination, sorting } = useTableSettings( + TRANSFORM_LIST_COLUMN.ID, + filteredTransforms ); // Before the transforms have been loaded for the first time, display the loading indicator only. @@ -246,23 +180,8 @@ export const TransformList: FC = ({ ); } - const sorting = { - sort: { - field: sortField, - direction: sortDirection, - }, - }; - const itemIdToExpandedRowMap = getItemIdToExpandedRowMap(expandedRowItemIds, transforms); - const pagination = { - initialPageIndex: pageIndex, - initialPageSize: pageSize, - totalItemCount: transforms.length, - pageSizeOptions: [10, 20, 50], - hidePerPageOptions: false, - }; - const bulkActionMenuItems = [
bulkStartAction.openModal(transformSelection)}> @@ -270,7 +189,11 @@ export const TransformList: FC = ({
,
- stopTransforms(transformSelection)}> + + stopTransforms(transformSelection.map((t) => ({ id: t.id, state: t.stats.state }))) + } + >
, @@ -331,7 +254,7 @@ export const TransformList: FC = ({ ]; }; - const renderToolsRight = () => ( + const toolsRight = ( @@ -342,56 +265,6 @@ export const TransformList: FC = ({ ); - const search = { - toolsLeft: transformSelection.length > 0 ? renderToolsLeft() : undefined, - toolsRight: renderToolsRight(), - onChange: onQueryChange, - box: { - incremental: true, - }, - filters: [ - { - type: 'field_value_selection' as const, - field: 'state.state', - name: i18n.translate('xpack.transform.statusFilter', { defaultMessage: 'Status' }), - multiSelect: 'or' as const, - options: Object.values(TRANSFORM_STATE).map((val) => ({ - value: val, - name: val, - view: getTaskStateBadge(val), - })), - }, - { - type: 'field_value_selection' as const, - field: 'mode', - name: i18n.translate('xpack.transform.modeFilter', { defaultMessage: 'Mode' }), - multiSelect: false, - options: Object.values(TRANSFORM_MODE).map((val) => ({ - value: val, - name: val, - view: ( - - {val} - - ), - })), - }, - ], - }; - - const onTableChange = ({ - page = { index: 0, size: 10 }, - sort = { field: TRANSFORM_LIST_COLUMN.ID as string, direction: 'asc' }, - }) => { - const { index, size } = page; - setPageIndex(index); - setPageSize(size); - - const { field, direction } = sort; - setSortField(field as string); - setSortDirection(direction as Direction); - }; - const selection = { onSelectionChange: (selected: TransformListRow[]) => setTransformSelection(selected), }; @@ -404,30 +277,38 @@ export const TransformList: FC = ({ {/* Single Action Modals */} {singleActionModals} - - + {transformSelection.length > 0 ? ( + {renderToolsLeft()} + ) : null} + + + + {toolsRight} + + + columns={columns} - error={searchError} hasActions={false} isExpandable={true} isSelectable={false} - items={filterActive ? filteredTransforms : transforms} + items={pageOfItems as TransformListRow[]} itemId={TRANSFORM_LIST_COLUMN.ID} itemIdToExpandedRowMap={itemIdToExpandedRowMap} loading={isLoading || transformsLoading} - onTableChange={onTableChange} - pagination={pagination} - rowProps={(item) => ({ - 'data-test-subj': `transformListRow row-${item.id}`, - })} + onChange={onTableChange as EuiBasicTableProps['onChange']} selection={selection} + pagination={pagination!} sorting={sorting} - search={search} data-test-subj={`transformListTable ${ isLoading || transformsLoading ? 'loading' : 'loaded' }`} + rowProps={(item) => ({ + 'data-test-subj': `transformListRow row-${item.id}`, + })} />
); diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar.tsx new file mode 100644 index 0000000000000..fdcb9ba5f0aff --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transform_search_bar.tsx @@ -0,0 +1,181 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Dispatch, SetStateAction, FC, Fragment, useState } from 'react'; +import { + EuiBadge, + EuiSearchBar, + EuiSearchBarProps, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + SearchFilterConfig, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { TermClause, FieldClause, Value } from './common'; +import { TRANSFORM_MODE, TRANSFORM_STATE } from '../../../../../../common/constants'; +import { TransformListRow } from '../../../../common'; +import { getTaskStateBadge } from './use_columns'; + +const filters: SearchFilterConfig[] = [ + { + type: 'field_value_selection', + field: 'state.state', + name: i18n.translate('xpack.transform.statusFilter', { defaultMessage: 'Status' }), + multiSelect: 'or', + options: Object.values(TRANSFORM_STATE).map((val) => ({ + value: val, + name: val, + view: getTaskStateBadge(val), + })), + }, + { + type: 'field_value_selection', + field: 'mode', + name: i18n.translate('xpack.transform.modeFilter', { defaultMessage: 'Mode' }), + multiSelect: false, + options: Object.values(TRANSFORM_MODE).map((val) => ({ + value: val, + name: val, + view: ( + + {val} + + ), + })), + }, +]; + +function stringMatch(str: string | undefined, substr: any) { + return ( + typeof str === 'string' && + typeof substr === 'string' && + (str.toLowerCase().match(substr.toLowerCase()) === null) === false + ); +} + +export const filterTransforms = ( + transforms: TransformListRow[], + clauses: Array +) => { + // keep count of the number of matches we make as we're looping over the clauses + // we only want to return transforms which match all clauses, i.e. each search term is ANDed + // { transform-one: { transform: { id: transform-one, config: {}, state: {}, ... }, count: 0 }, transform-two: {...} } + const matches: Record = transforms.reduce((p: Record, c) => { + p[c.id] = { + transform: c, + count: 0, + }; + return p; + }, {}); + + clauses.forEach((c) => { + // the search term could be negated with a minus, e.g. -bananas + const bool = c.match === 'must'; + let ts = []; + + if (c.type === 'term') { + // filter term based clauses, e.g. bananas + // match on ID and description + // if the term has been negated, AND the matches + if (bool === true) { + ts = transforms.filter( + (transform) => + stringMatch(transform.id, c.value) === bool || + stringMatch(transform.config.description, c.value) === bool + ); + } else { + ts = transforms.filter( + (transform) => + stringMatch(transform.id, c.value) === bool && + stringMatch(transform.config.description, c.value) === bool + ); + } + } else { + // filter other clauses, i.e. the mode and status filters + if (Array.isArray(c.value)) { + // the status value is an array of string(s) e.g. ['failed', 'stopped'] + ts = transforms.filter((transform) => (c.value as Value[]).includes(transform.stats.state)); + } else { + ts = transforms.filter((transform) => transform.mode === c.value); + } + } + + ts.forEach((t) => matches[t.id].count++); + }); + + // loop through the matches and return only transforms which have match all the clauses + const filtered = Object.values(matches) + .filter((m) => (m && m.count) >= clauses.length) + .map((m) => m.transform); + + return filtered; +}; + +function getError(errorMessage: string | null) { + if (errorMessage) { + return i18n.translate('xpack.transform.transformList.searchBar.invalidSearchErrorMessage', { + defaultMessage: 'Invalid search: {errorMessage}', + values: { errorMessage }, + }); + } + + return ''; +} + +interface Props { + searchQueryText: string; + setSearchQueryText: Dispatch>; +} + +export const TransformSearchBar: FC = ({ searchQueryText, setSearchQueryText }) => { + const [errorMessage, setErrorMessage] = useState(null); + + const onChange: EuiSearchBarProps['onChange'] = ({ query, error }) => { + if (error) { + setErrorMessage(error.message); + } else if (query !== null && query.text !== undefined) { + setSearchQueryText(query.text); + setErrorMessage(null); + } + }; + + return ( + + + {searchQueryText === undefined && ( + + )} + {searchQueryText !== undefined && ( + + )} + + + + + + ); +}; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx index bce01b954c83e..313668d4c5180 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/transforms_stats_bar.tsx @@ -7,9 +7,9 @@ import React, { FC } from 'react'; import { i18n } from '@kbn/i18n'; -import { TRANSFORM_STATE } from '../../../../../../common'; +import { TRANSFORM_MODE, TRANSFORM_STATE } from '../../../../../../common/constants'; -import { TRANSFORM_MODE, TransformListRow } from '../../../../common'; +import { TransformListRow } from '../../../../common'; import { StatsBar, TransformStatsBarStats } from '../stats_bar'; diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx index d2d8c7084941d..040e502ce4888 100644 --- a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_columns.tsx @@ -22,24 +22,21 @@ import { RIGHT_ALIGNMENT, } from '@elastic/eui'; -import { TransformId, TRANSFORM_STATE } from '../../../../../../common'; +import { TransformId } from '../../../../../../common/types/transform'; +import { TransformStats } from '../../../../../../common/types/transform_stats'; +import { TRANSFORM_STATE } from '../../../../../../common/constants'; -import { - getTransformProgress, - TransformListRow, - TransformStats, - TRANSFORM_LIST_COLUMN, -} from '../../../../common'; +import { getTransformProgress, TransformListRow, TRANSFORM_LIST_COLUMN } from '../../../../common'; import { useActions } from './use_actions'; -enum STATE_COLOR { - aborting = 'warning', - failed = 'danger', - indexing = 'primary', - started = 'primary', - stopped = 'hollow', - stopping = 'hollow', -} +const STATE_COLOR = { + aborting: 'warning', + failed: 'danger', + indexing: 'primary', + started: 'primary', + stopped: 'hollow', + stopping: 'hollow', +} as const; export const getTaskStateBadge = ( state: TransformStats['state'], diff --git a/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_table_settings.ts b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_table_settings.ts new file mode 100644 index 0000000000000..ad40a3f8103b9 --- /dev/null +++ b/x-pack/plugins/transform/public/app/sections/transform_management/components/transform_list/use_table_settings.ts @@ -0,0 +1,135 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState } from 'react'; +import { Direction, EuiBasicTableProps, EuiTableSortingType } from '@elastic/eui'; +import sortBy from 'lodash/sortBy'; +import get from 'lodash/get'; + +const PAGE_SIZE = 10; +const PAGE_SIZE_OPTIONS = [10, 25, 50]; + +const propertyMap = { + Mode: 'mode', +}; + +// Copying from EUI EuiBasicTable types as type is not correctly picked up for table's onChange +// Can be removed when https://github.com/elastic/eui/issues/4011 is addressed in EUI +export interface Criteria { + page?: { + index: number; + size: number; + }; + sort?: { + field: keyof T; + direction: Direction; + }; +} +export interface CriteriaWithPagination extends Criteria { + page: { + index: number; + size: number; + }; +} + +interface AnalyticsBasicTableSettings { + pageIndex: number; + pageSize: number; + totalItemCount: number; + hidePerPageOptions: boolean; + sortField: keyof T; + sortDirection: Direction; +} + +interface UseTableSettingsReturnValue { + onTableChange: EuiBasicTableProps['onChange']; + pageOfItems: T[]; + pagination: EuiBasicTableProps['pagination']; + sorting: EuiTableSortingType; +} + +export function useTableSettings( + sortByField: keyof TypeOfItem, + items: TypeOfItem[] +): UseTableSettingsReturnValue { + const [tableSettings, setTableSettings] = useState>({ + pageIndex: 0, + pageSize: PAGE_SIZE, + totalItemCount: 0, + hidePerPageOptions: false, + sortField: sortByField, + sortDirection: 'asc', + }); + + const getPageOfItems = ( + list: TypeOfItem[], + index: number, + size: number, + sortField: keyof TypeOfItem, + sortDirection: Direction + ) => { + list = sortBy(list, (item) => + get(item, propertyMap[sortField as keyof typeof propertyMap] || sortField) + ); + list = sortDirection === 'asc' ? list : list.reverse(); + const listLength = list.length; + + let pageStart = index * size; + if (pageStart >= listLength && listLength !== 0) { + // if the page start is larger than the number of items due to + // filters being applied or items being deleted, calculate a new page start + pageStart = Math.floor((listLength - 1) / size) * size; + + setTableSettings({ ...tableSettings, pageIndex: pageStart / size }); + } + return { + pageOfItems: list.slice(pageStart, pageStart + size), + totalItemCount: listLength, + }; + }; + + const onTableChange: EuiBasicTableProps['onChange'] = ({ + page = { index: 0, size: PAGE_SIZE }, + sort = { field: sortByField, direction: 'asc' }, + }: CriteriaWithPagination) => { + const { index, size } = page; + const { field, direction } = sort; + + setTableSettings({ + ...tableSettings, + pageIndex: index, + pageSize: size, + sortField: field, + sortDirection: direction, + }); + }; + + const { pageIndex, pageSize, sortField, sortDirection } = tableSettings; + + const { pageOfItems, totalItemCount } = getPageOfItems( + items, + pageIndex, + pageSize, + sortField, + sortDirection + ); + + const pagination = { + pageIndex, + pageSize, + totalItemCount, + pageSizeOptions: PAGE_SIZE_OPTIONS, + }; + + const sorting = { + sort: { + field: sortField, + direction: sortDirection, + }, + }; + + return { onTableChange, pageOfItems, pagination, sorting }; +} diff --git a/x-pack/plugins/transform/public/shared_imports.ts b/x-pack/plugins/transform/public/shared_imports.ts index abbc39dd6c728..4737787dbd9ee 100644 --- a/x-pack/plugins/transform/public/shared_imports.ts +++ b/x-pack/plugins/transform/public/shared_imports.ts @@ -15,7 +15,6 @@ export { export { getFieldType, - getErrorMessage, extractErrorMessage, formatHumanReadableDateTimeSeconds, getDataGridSchemaFromKibanaFieldType, @@ -28,7 +27,6 @@ export { DataGrid, EsSorting, RenderCellValue, - SearchResponse7, UseDataGridReturnType, UseIndexDataReturnType, INDEX_STATUS, diff --git a/x-pack/plugins/transform/server/README.md b/x-pack/plugins/transform/server/README.md new file mode 100644 index 0000000000000..1142c1fea094d --- /dev/null +++ b/x-pack/plugins/transform/server/README.md @@ -0,0 +1,19 @@ +# Transform Kibana API routes + +This folder contains Transform API routes in Kibana. + +Each route handler requires [apiDoc](https://github.com/apidoc/apidoc) annotations in order +to generate documentation. +The [apidoc-markdown](https://github.com/rigwild/apidoc-markdown) package is also required in order to generate the markdown. + +There are custom parser and worker (`x-pack/plugins/transform/server/routes/apidoc_scripts`) to process api schemas for each documentation entry. It's written with typescript so make sure all the scripts in the folder are compiled before executing `apidoc` command. + +Make sure you have run `yarn kbn bootstrap` to get all requires dev dependencies. Then execute the following command from the transform plugin folder: +``` +yarn run apiDocs +``` +It compiles all the required scripts and generates the documentation both in HTML and Markdown formats. + + +It will create a new directory `routes_doc` (next to the `routes` folder) which contains the documentation in HTML format +as well as `Transform_API.md` file. diff --git a/x-pack/plugins/transform/server/plugin.ts b/x-pack/plugins/transform/server/plugin.ts index 79e9be239c798..988750f70efe0 100644 --- a/x-pack/plugins/transform/server/plugin.ts +++ b/x-pack/plugins/transform/server/plugin.ts @@ -58,7 +58,7 @@ export class TransformServerPlugin implements Plugin<{}, void, any, any> { this.license = new License(); } - setup({ http, getStartServices }: CoreSetup, { licensing }: Dependencies): {} { + setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies): {} { const router = http.createRouter(); this.license.setup( @@ -75,6 +75,20 @@ export class TransformServerPlugin implements Plugin<{}, void, any, any> { } ); + features.registerElasticsearchFeature({ + id: PLUGIN.id, + management: { + data: [PLUGIN.id], + }, + catalogue: [PLUGIN.id], + privileges: [ + { + requiredClusterPrivileges: ['monitor_transform'], + ui: [], + }, + ], + }); + this.apiRoutes.setup({ router, license: this.license, diff --git a/x-pack/plugins/transform/server/routes/api/error_utils.ts b/x-pack/plugins/transform/server/routes/api/error_utils.ts index 5a479e4f429f6..269cd28c4bda6 100644 --- a/x-pack/plugins/transform/server/routes/api/error_utils.ts +++ b/x-pack/plugins/transform/server/routes/api/error_utils.ts @@ -10,11 +10,8 @@ import { i18n } from '@kbn/i18n'; import { ResponseError, CustomHttpResponseOptions } from 'src/core/server'; -import { - TransformEndpointRequest, - TransformEndpointResult, - DeleteTransformEndpointResult, -} from '../../../common'; +import { CommonResponseStatusSchema, TransformIdsSchema } from '../../../common/api_schemas/common'; +import { DeleteTransformsResponseSchema } from '../../../common/api_schemas/delete_transforms'; const REQUEST_TIMEOUT = 'RequestTimeout'; @@ -23,9 +20,9 @@ export function isRequestTimeout(error: any) { } interface Params { - results: TransformEndpointResult | DeleteTransformEndpointResult; + results: CommonResponseStatusSchema | DeleteTransformsResponseSchema; id: string; - items: TransformEndpointRequest[]; + items: TransformIdsSchema; action: string; } @@ -63,7 +60,7 @@ export function fillResultsWithTimeouts({ results, id, items, action }: Params) }, }; - const newResults: TransformEndpointResult | DeleteTransformEndpointResult = {}; + const newResults: CommonResponseStatusSchema | DeleteTransformsResponseSchema = {}; return items.reduce((accumResults, currentVal) => { if (results[currentVal.id] === undefined) { diff --git a/x-pack/plugins/transform/server/routes/api/field_histograms.ts b/x-pack/plugins/transform/server/routes/api/field_histograms.ts index 2642040c4cd0d..88352ec4af129 100644 --- a/x-pack/plugins/transform/server/routes/api/field_histograms.ts +++ b/x-pack/plugins/transform/server/routes/api/field_histograms.ts @@ -11,40 +11,49 @@ import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; +import { + indexPatternTitleSchema, + IndexPatternTitleSchema, +} from '../../../common/api_schemas/common'; +import { + fieldHistogramsRequestSchema, + FieldHistogramsRequestSchema, +} from '../../../common/api_schemas/field_histograms'; import { getHistogramsForFields } from '../../shared_imports'; import { RouteDependencies } from '../../types'; import { addBasePath } from '../index'; import { wrapError } from './error_utils'; -import { fieldHistogramsSchema, indexPatternTitleSchema, IndexPatternTitleSchema } from './schema'; export function registerFieldHistogramsRoutes({ router, license }: RouteDependencies) { - router.post( + router.post( { path: addBasePath('field_histograms/{indexPatternTitle}'), validate: { params: indexPatternTitleSchema, - body: fieldHistogramsSchema, + body: fieldHistogramsRequestSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { - const { indexPatternTitle } = req.params as IndexPatternTitleSchema; - const { query, fields, samplerShardSize } = req.body; + license.guardApiRoute( + async (ctx, req, res) => { + const { indexPatternTitle } = req.params; + const { query, fields, samplerShardSize } = req.body; - try { - const resp = await getHistogramsForFields( - ctx.core.elasticsearch.client, - indexPatternTitle, - query, - fields, - samplerShardSize - ); + try { + const resp = await getHistogramsForFields( + ctx.core.elasticsearch.client, + indexPatternTitle, + query, + fields, + samplerShardSize + ); - return res.ok({ body: resp }); - } catch (e) { - return res.customError(wrapError(wrapEsError(e))); + return res.ok({ body: resp }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } } - }) + ) ); } diff --git a/x-pack/plugins/transform/server/routes/api/privileges.ts b/x-pack/plugins/transform/server/routes/api/privileges.ts index 2b7b0544a8bf9..605cbde356fdf 100644 --- a/x-pack/plugins/transform/server/routes/api/privileges.ts +++ b/x-pack/plugins/transform/server/routes/api/privileges.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import { APP_CLUSTER_PRIVILEGES, APP_INDEX_PRIVILEGES } from '../../../common/constants'; -import { Privileges } from '../../../common'; +import { Privileges } from '../../../common/types/privileges'; import { RouteDependencies } from '../../types'; import { addBasePath } from '../index'; diff --git a/x-pack/plugins/transform/server/routes/api/schema.ts b/x-pack/plugins/transform/server/routes/api/schema.ts deleted file mode 100644 index 8aadef81b221b..0000000000000 --- a/x-pack/plugins/transform/server/routes/api/schema.ts +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ -import { schema } from '@kbn/config-schema'; - -export const fieldHistogramsSchema = schema.object({ - /** Query to match documents in the index. */ - query: schema.any(), - /** The fields to return histogram data. */ - fields: schema.arrayOf(schema.any()), - /** Number of documents to be collected in the sample processed on each shard, or -1 for no sampling. */ - samplerShardSize: schema.number(), -}); - -export const indexPatternTitleSchema = schema.object({ - /** Title of the index pattern for which to return stats. */ - indexPatternTitle: schema.string(), -}); - -export interface IndexPatternTitleSchema { - indexPatternTitle: string; -} - -export const schemaTransformId = { - params: schema.object({ - transformId: schema.string(), - }), -}; - -export interface SchemaTransformId { - transformId: string; -} - -export const deleteTransformSchema = schema.object({ - /** - * Delete Transform & Destination Index - */ - transformsInfo: schema.arrayOf( - schema.object({ - id: schema.string(), - state: schema.maybe(schema.string()), - }) - ), - deleteDestIndex: schema.maybe(schema.boolean()), - deleteDestIndexPattern: schema.maybe(schema.boolean()), - forceDelete: schema.maybe(schema.boolean()), -}); diff --git a/x-pack/plugins/transform/server/routes/api/transforms.ts b/x-pack/plugins/transform/server/routes/api/transforms.ts index efbe813db5e67..c02bc06ad6060 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms.ts @@ -14,22 +14,47 @@ import { import { CallCluster } from 'src/legacy/core_plugins/elasticsearch'; import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; +import { TRANSFORM_STATE } from '../../../common/constants'; +import { TransformId } from '../../../common/types/transform'; import { - TransformEndpointRequest, - TransformEndpointResult, - TransformId, - TRANSFORM_STATE, - DeleteTransformEndpointRequest, - DeleteTransformStatus, - ResultData, -} from '../../../common'; + transformIdParamSchema, + ResponseStatus, + TransformIdParamSchema, +} from '../../../common/api_schemas/common'; +import { + deleteTransformsRequestSchema, + DeleteTransformsRequestSchema, + DeleteTransformsResponseSchema, +} from '../../../common/api_schemas/delete_transforms'; +import { + startTransformsRequestSchema, + StartTransformsRequestSchema, + StartTransformsResponseSchema, +} from '../../../common/api_schemas/start_transforms'; +import { + stopTransformsRequestSchema, + StopTransformsRequestSchema, + StopTransformsResponseSchema, +} from '../../../common/api_schemas/stop_transforms'; +import { + postTransformsUpdateRequestSchema, + PostTransformsUpdateRequestSchema, + PostTransformsUpdateResponseSchema, +} from '../../../common/api_schemas/update_transforms'; +import { + GetTransformsResponseSchema, + postTransformsPreviewRequestSchema, + PostTransformsPreviewRequestSchema, + putTransformsRequestSchema, + PutTransformsRequestSchema, + PutTransformsResponseSchema, +} from '../../../common/api_schemas/transforms'; import { RouteDependencies } from '../../types'; import { addBasePath } from '../index'; import { isRequestTimeout, fillResultsWithTimeouts, wrapError } from './error_utils'; -import { deleteTransformSchema, schemaTransformId, SchemaTransformId } from './schema'; import { registerTransformsAuditMessagesRoutes } from './transforms_audit_messages'; import { IIndexPattern } from '../../../../../../src/plugins/data/common/index_patterns'; @@ -47,6 +72,16 @@ interface StopOptions { export function registerTransformsRoutes(routeDependencies: RouteDependencies) { const { router, license } = routeDependencies; + /** + * @apiGroup Transforms + * + * @api {get} /api/transform/transforms Get transforms + * @apiName GetTransforms + * @apiDescription Returns transforms + * + * @apiSchema (params) jobAuditMessagesJobIdSchema + * @apiSchema (query) jobAuditMessagesQuerySchema + */ router.get( { path: addBasePath('transforms'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { @@ -62,16 +97,24 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { } }) ); - router.get( + + /** + * @apiGroup Transforms + * + * @api {get} /api/transform/transforms/:transformId Get transform + * @apiName GetTransform + * @apiDescription Returns a single transform + * + * @apiSchema (params) transformIdParamSchema + */ + router.get( { path: addBasePath('transforms/{transformId}'), - validate: schemaTransformId, + validate: { params: transformIdParamSchema }, }, - license.guardApiRoute(async (ctx, req, res) => { - const { transformId } = req.params as SchemaTransformId; - const options = { - ...(transformId !== undefined ? { transformId } : {}), - }; + license.guardApiRoute(async (ctx, req, res) => { + const { transformId } = req.params; + const options = transformId !== undefined ? { transformId } : {}; try { const transforms = await getTransforms( options, @@ -83,6 +126,14 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { } }) ); + + /** + * @apiGroup Transforms + * + * @api {get} /api/transform/transforms/_stats Get transforms stats + * @apiName GetTransformsStats + * @apiDescription Returns transforms stats + */ router.get( { path: addBasePath('transforms/_stats'), validate: false }, license.guardApiRoute(async (ctx, req, res) => { @@ -98,13 +149,23 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { } }) ); - router.get( + + /** + * @apiGroup Transforms + * + * @api {get} /api/transform/transforms/:transformId/_stats Get transform stats + * @apiName GetTransformStats + * @apiDescription Returns stats for a single transform + * + * @apiSchema (params) transformIdParamSchema + */ + router.get( { path: addBasePath('transforms/{transformId}/_stats'), - validate: schemaTransformId, + validate: { params: transformIdParamSchema }, }, - license.guardApiRoute(async (ctx, req, res) => { - const { transformId } = req.params as SchemaTransformId; + license.guardApiRoute(async (ctx, req, res) => { + const { transformId } = req.params; const options = { ...(transformId !== undefined ? { transformId } : {}), }; @@ -120,134 +181,198 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { }) ); registerTransformsAuditMessagesRoutes(routeDependencies); - router.put( + + /** + * @apiGroup Transforms + * + * @api {put} /api/transform/transforms/:transformId Put transform + * @apiName PutTransform + * @apiDescription Creates a transform + * + * @apiSchema (params) transformIdParamSchema + * @apiSchema (body) putTransformsRequestSchema + */ + router.put( { path: addBasePath('transforms/{transformId}'), validate: { - ...schemaTransformId, - body: schema.maybe(schema.any()), + params: transformIdParamSchema, + body: putTransformsRequestSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { - const { transformId } = req.params as SchemaTransformId; - - const response: { - transformsCreated: Array<{ transform: string }>; - errors: any[]; - } = { - transformsCreated: [], - errors: [], - }; + license.guardApiRoute( + async (ctx, req, res) => { + const { transformId } = req.params; - await ctx - .transform!.dataClient.callAsCurrentUser('transform.createTransform', { - body: req.body, - transformId, - }) - .then(() => response.transformsCreated.push({ transform: transformId })) - .catch((e) => - response.errors.push({ - id: transformId, - error: wrapEsError(e), + const response: PutTransformsResponseSchema = { + transformsCreated: [], + errors: [], + }; + + await ctx + .transform!.dataClient.callAsCurrentUser('transform.createTransform', { + body: req.body, + transformId, }) - ); + .then(() => response.transformsCreated.push({ transform: transformId })) + .catch((e) => + response.errors.push({ + id: transformId, + error: wrapEsError(e), + }) + ); - return res.ok({ body: response }); - }) + return res.ok({ body: response }); + } + ) ); - router.post( + + /** + * @apiGroup Transforms + * + * @api {post} /api/transform/transforms/:transformId/_update Post transform update + * @apiName PostTransformUpdate + * @apiDescription Updates a transform + * + * @apiSchema (params) transformIdParamSchema + * @apiSchema (body) postTransformsUpdateRequestSchema + */ + router.post( { path: addBasePath('transforms/{transformId}/_update'), validate: { - ...schemaTransformId, - body: schema.maybe(schema.any()), + params: transformIdParamSchema, + body: postTransformsUpdateRequestSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { - const { transformId } = req.params as SchemaTransformId; + license.guardApiRoute( + async (ctx, req, res) => { + const { transformId } = req.params; - try { - return res.ok({ - body: await ctx.transform!.dataClient.callAsCurrentUser('transform.updateTransform', { - body: req.body, - transformId, - }), - }); - } catch (e) { - return res.customError(wrapError(e)); + try { + return res.ok({ + body: (await ctx.transform!.dataClient.callAsCurrentUser('transform.updateTransform', { + body: req.body, + transformId, + })) as PostTransformsUpdateResponseSchema, + }); + } catch (e) { + return res.customError(wrapError(e)); + } } - }) + ) ); - router.post( + + /** + * @apiGroup Transforms + * + * @api {post} /api/transform/delete_transforms Post delete transforms + * @apiName DeleteTransforms + * @apiDescription Deletes transforms + * + * @apiSchema (body) deleteTransformsRequestSchema + */ + router.post( { path: addBasePath('delete_transforms'), validate: { - body: deleteTransformSchema, + body: deleteTransformsRequestSchema, }, }, - license.guardApiRoute(async (ctx, req, res) => { - const { - transformsInfo, - deleteDestIndex, - deleteDestIndexPattern, - forceDelete, - } = req.body as DeleteTransformEndpointRequest; - - try { - const body = await deleteTransforms( - transformsInfo, - deleteDestIndex, - deleteDestIndexPattern, - forceDelete, - ctx, - license, - res - ); - - if (body && body.status) { - if (body.status === 404) { - return res.notFound(); - } - if (body.status === 403) { - return res.forbidden(); + license.guardApiRoute( + async (ctx, req, res) => { + try { + const body = await deleteTransforms(req.body, ctx, res); + + if (body && body.status) { + if (body.status === 404) { + return res.notFound(); + } + if (body.status === 403) { + return res.forbidden(); + } } - } - return res.ok({ - body, - }); - } catch (e) { - return res.customError(wrapError(wrapEsError(e))); + return res.ok({ + body, + }); + } catch (e) { + return res.customError(wrapError(wrapEsError(e))); + } } - }) + ) ); - router.post( + + /** + * @apiGroup Transforms + * + * @api {post} /api/transform/transforms/_preview Preview transform + * @apiName PreviewTransform + * @apiDescription Previews transform + * + * @apiSchema (body) postTransformsPreviewRequestSchema + */ + router.post( { path: addBasePath('transforms/_preview'), validate: { - body: schema.maybe(schema.any()), + body: postTransformsPreviewRequestSchema, }, }, - license.guardApiRoute(previewTransformHandler) + license.guardApiRoute( + previewTransformHandler + ) ); - router.post( + + /** + * @apiGroup Transforms + * + * @api {post} /api/transform/start_transforms Start transforms + * @apiName PostStartTransforms + * @apiDescription Starts transform + * + * @apiSchema (body) startTransformsRequestSchema + */ + router.post( { path: addBasePath('start_transforms'), validate: { - body: schema.maybe(schema.any()), + body: startTransformsRequestSchema, }, }, - license.guardApiRoute(startTransformsHandler) + license.guardApiRoute( + startTransformsHandler + ) ); - router.post( + + /** + * @apiGroup Transforms + * + * @api {post} /api/transform/stop_transforms Stop transforms + * @apiName PostStopTransforms + * @apiDescription Stops transform + * + * @apiSchema (body) stopTransformsRequestSchema + */ + router.post( { path: addBasePath('stop_transforms'), validate: { - body: schema.maybe(schema.any()), + body: stopTransformsRequestSchema, }, }, - license.guardApiRoute(stopTransformsHandler) + license.guardApiRoute(stopTransformsHandler) ); + + /** + * @apiGroup Transforms + * + * @api {post} /api/transform/es_search Transform ES Search Proxy + * @apiName PostTransformEsSearchProxy + * @apiDescription ES Search Proxy + * + * @apiSchema (body) any + */ router.post( { path: addBasePath('es_search'), @@ -267,7 +392,10 @@ export function registerTransformsRoutes(routeDependencies: RouteDependencies) { ); } -const getTransforms = async (options: { transformId?: string }, callAsCurrentUser: CallCluster) => { +const getTransforms = async ( + options: { transformId?: string }, + callAsCurrentUser: CallCluster +): Promise => { return await callAsCurrentUser('transform.getTransforms', options); }; @@ -294,22 +422,25 @@ async function deleteDestIndexPatternById( } async function deleteTransforms( - transformsInfo: TransformEndpointRequest[], - deleteDestIndex: boolean | undefined, - deleteDestIndexPattern: boolean | undefined, - shouldForceDelete: boolean = false, + reqBody: DeleteTransformsRequestSchema, ctx: RequestHandlerContext, - license: RouteDependencies['license'], response: KibanaResponseFactory ) { - const results: Record = {}; + const { transformsInfo } = reqBody; + + // Cast possible undefineds as booleans + const deleteDestIndex = !!reqBody.deleteDestIndex; + const deleteDestIndexPattern = !!reqBody.deleteDestIndexPattern; + const shouldForceDelete = !!reqBody.forceDelete; + + const results: DeleteTransformsResponseSchema = {}; for (const transformInfo of transformsInfo) { let destinationIndex: string | undefined; - const transformDeleted: ResultData = { success: false }; - const destIndexDeleted: ResultData = { success: false }; - const destIndexPatternDeleted: ResultData = { + const transformDeleted: ResponseStatus = { success: false }; + const destIndexDeleted: ResponseStatus = { success: false }; + const destIndexPatternDeleted: ResponseStatus = { success: false, }; const transformId = transformInfo.id; @@ -405,7 +536,11 @@ async function deleteTransforms( return results; } -const previewTransformHandler: RequestHandler = async (ctx, req, res) => { +const previewTransformHandler: RequestHandler< + undefined, + undefined, + PostTransformsPreviewRequestSchema +> = async (ctx, req, res) => { try { return res.ok({ body: await ctx.transform!.dataClient.callAsCurrentUser('transform.getTransformsPreview', { @@ -417,8 +552,12 @@ const previewTransformHandler: RequestHandler = async (ctx, req, res) => { } }; -const startTransformsHandler: RequestHandler = async (ctx, req, res) => { - const transformsInfo = req.body as TransformEndpointRequest[]; +const startTransformsHandler: RequestHandler< + undefined, + undefined, + StartTransformsRequestSchema +> = async (ctx, req, res) => { + const transformsInfo = req.body; try { return res.ok({ @@ -430,15 +569,15 @@ const startTransformsHandler: RequestHandler = async (ctx, req, res) => { }; async function startTransforms( - transformsInfo: TransformEndpointRequest[], + transformsInfo: StartTransformsRequestSchema, callAsCurrentUser: CallCluster ) { - const results: TransformEndpointResult = {}; + const results: StartTransformsResponseSchema = {}; for (const transformInfo of transformsInfo) { const transformId = transformInfo.id; try { - await callAsCurrentUser('transform.startTransform', { transformId } as SchemaTransformId); + await callAsCurrentUser('transform.startTransform', { transformId }); results[transformId] = { success: true }; } catch (e) { if (isRequestTimeout(e)) { @@ -455,8 +594,12 @@ async function startTransforms( return results; } -const stopTransformsHandler: RequestHandler = async (ctx, req, res) => { - const transformsInfo = req.body as TransformEndpointRequest[]; +const stopTransformsHandler: RequestHandler< + undefined, + undefined, + StopTransformsRequestSchema +> = async (ctx, req, res) => { + const transformsInfo = req.body; try { return res.ok({ @@ -468,10 +611,10 @@ const stopTransformsHandler: RequestHandler = async (ctx, req, res) => { }; async function stopTransforms( - transformsInfo: TransformEndpointRequest[], + transformsInfo: StopTransformsRequestSchema, callAsCurrentUser: CallCluster ) { - const results: TransformEndpointResult = {}; + const results: StopTransformsResponseSchema = {}; for (const transformInfo of transformsInfo) { const transformId = transformInfo.id; diff --git a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts index 722a3f52376b4..f01b2bdb73fd5 100644 --- a/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts +++ b/x-pack/plugins/transform/server/routes/api/transforms_audit_messages.ts @@ -4,7 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ -import { AuditMessage } from '../../../common/types/messages'; +import { transformIdParamSchema, TransformIdParamSchema } from '../../../common/api_schemas/common'; +import { AuditMessage, TransformMessage } from '../../../common/types/messages'; import { wrapEsError } from '../../../../../legacy/server/lib/create_router/error_wrappers'; import { RouteDependencies } from '../../types'; @@ -12,7 +13,6 @@ import { RouteDependencies } from '../../types'; import { addBasePath } from '../index'; import { wrapError } from './error_utils'; -import { schemaTransformId, SchemaTransformId } from './schema'; const ML_DF_NOTIFICATION_INDEX_PATTERN = '.transform-notifications-read'; const SIZE = 500; @@ -22,10 +22,22 @@ interface BoolQuery { } export function registerTransformsAuditMessagesRoutes({ router, license }: RouteDependencies) { - router.get( - { path: addBasePath('transforms/{transformId}/messages'), validate: schemaTransformId }, - license.guardApiRoute(async (ctx, req, res) => { - const { transformId } = req.params as SchemaTransformId; + /** + * @apiGroup Transforms Audit Messages + * + * @api {get} /api/transform/transforms/:transformId/messages Transforms Messages + * @apiName GetTransformsMessages + * @apiDescription Get transforms audit messages + * + * @apiSchema (params) transformIdParamSchema + */ + router.get( + { + path: addBasePath('transforms/{transformId}/messages'), + validate: { params: transformIdParamSchema }, + }, + license.guardApiRoute(async (ctx, req, res) => { + const { transformId } = req.params; // search for audit messages, // transformId is optional. without it, all transforms will be listed. @@ -77,7 +89,7 @@ export function registerTransformsAuditMessagesRoutes({ router, license }: Route }, }); - let messages = []; + let messages: TransformMessage[] = []; if (resp.hits.total !== 0) { messages = resp.hits.hits.map((hit: AuditMessage) => hit._source); messages.reverse(); diff --git a/x-pack/plugins/transform/server/routes/apidoc.json b/x-pack/plugins/transform/server/routes/apidoc.json new file mode 100644 index 0000000000000..ce76b5b302f93 --- /dev/null +++ b/x-pack/plugins/transform/server/routes/apidoc.json @@ -0,0 +1,21 @@ +{ + "name": "transform_kibana_api", + "version": "7.10.0", + "description": "This is the documentation of the REST API provided by the Transform Kibana plugin. Each API is experimental and can include breaking changes in any version.", + "title": "Transform Kibana API", + "order": [ + "GetTransforms", + "GetTransform", + "GetTransformsStats", + "GetTransformStats", + "PutTransform", + "PostTransformUpdate", + "DeleteTransforms", + "PreviewTransform", + "PostStartTransforms", + "PostStopTransforms", + "PostTransformEsSearchProxy", + "DeleteDataFrameAnalytics", + "GetTransformsMessages" + ] +} diff --git a/x-pack/plugins/transform/server/services/license.ts b/x-pack/plugins/transform/server/services/license.ts index 1a2768999fdc4..bacf9724a6253 100644 --- a/x-pack/plugins/transform/server/services/license.ts +++ b/x-pack/plugins/transform/server/services/license.ts @@ -62,12 +62,12 @@ export class License { }); } - guardApiRoute(handler: RequestHandler) { + guardApiRoute(handler: RequestHandler) { const license = this; return function licenseCheck( ctx: RequestHandlerContext, - request: KibanaRequest, + request: KibanaRequest, response: KibanaResponseFactory ): IKibanaResponse | Promise> { const licenseStatus = license.getStatus(); diff --git a/x-pack/plugins/transform/server/types.ts b/x-pack/plugins/transform/server/types.ts index 5fcc23a6d9f48..c3d7434f14f45 100644 --- a/x-pack/plugins/transform/server/types.ts +++ b/x-pack/plugins/transform/server/types.ts @@ -6,10 +6,12 @@ import { IRouter } from 'src/core/server'; import { LicensingPluginSetup } from '../../licensing/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { License } from './services'; export interface Dependencies { licensing: LicensingPluginSetup; + features: FeaturesPluginSetup; } export interface RouteDependencies { diff --git a/x-pack/plugins/transform/tsconfig.json b/x-pack/plugins/transform/tsconfig.json new file mode 100644 index 0000000000000..6f83eb665f830 --- /dev/null +++ b/x-pack/plugins/transform/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json", +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 8b9409f01087c..3938ed163f6cc 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -816,8 +816,6 @@ "data.query.queryBar.KQLNestedQuerySyntaxInfoTitle": "KQL ネストされたクエリ構文", "data.query.queryBar.kqlOffLabel": "オフ", "data.query.queryBar.kqlOnLabel": "オン", - "data.query.queryBar.licenseOptions": "ライセンスオプションに進む", - "data.query.queryBar.longQueryMessage": "ライセンスをアップグレードすれば、リクエストの完了までに十分な時間を確保できます。", "data.query.queryBar.luceneLanguageName": "Lucene", "data.query.queryBar.luceneSyntaxWarningMessage": "Lucene クエリ構文を使用しているようですが、Kibana クエリ言語 (KQL) が選択されています。KQL ドキュメント {link} を確認してください。", "data.query.queryBar.luceneSyntaxWarningOptOutText": "今後表示しない", @@ -2880,7 +2878,6 @@ "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsDescription": "影響されるオブジェクトのサンプル", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsName": "影響されるオブジェクトのサンプル", "savedObjectsManagement.objectsTable.flyout.resolveImportErrorsFileErrorMessage": "ファイルを処理できませんでした。", - "savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel": "インポートするファイルを選択してください", "savedObjectsManagement.objectsTable.header.exportButtonLabel": "{filteredCount, plural, one{# オブジェクト} other {# オブジェクト}}をエクスポート", "savedObjectsManagement.objectsTable.header.importButtonLabel": "インポート", "savedObjectsManagement.objectsTable.header.refreshButtonLabel": "更新", @@ -4446,7 +4443,6 @@ "xpack.actions.builtin.case.connectorApiNullError": "コネクター[apiUrl]が必要です", "xpack.actions.builtin.case.jiraTitle": "Jira", "xpack.actions.builtin.case.resilientTitle": "IBM Resilient", - "xpack.actions.builtin.configuration.apiWhitelistError": "コネクターアクションの構成エラー:{message}", "xpack.actions.builtin.email.errorSendingErrorMessage": "エラー送信メールアドレス", "xpack.actions.builtin.emailTitle": "メール", "xpack.actions.builtin.esIndex.errorIndexingErrorMessage": "エラーインデックス作成ドキュメント", @@ -4862,12 +4858,9 @@ "xpack.apm.serviceOverview.upgradeAssistantLink": "アップグレードアシスタント", "xpack.apm.servicesTable.7xOldDataMessage": "また、移行が必要な古いデータがある可能性もあります。", "xpack.apm.servicesTable.7xUpgradeServerMessage": "バージョン7.xより前からのアップグレードですか?また、\n APMサーバーインスタンスを7.0以降にアップグレードしていることも確認してください。", - "xpack.apm.servicesTable.agentColumnLabel": "エージェント", "xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均応答時間", "xpack.apm.servicesTable.environmentColumnLabel": "環境", "xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 個の環境} other {# 個の環境}}", - "xpack.apm.servicesTable.errorsPerMinuteColumnLabel": "1 分あたりのエラー", - "xpack.apm.servicesTable.errorsPerMinuteUnitLabel": "エラー", "xpack.apm.servicesTable.nameColumnLabel": "名前", "xpack.apm.servicesTable.noServicesLabel": "APM サービスがインストールされていないようです。追加しましょう!", "xpack.apm.servicesTable.notFoundLabel": "サービスが見つかりません", @@ -6678,8 +6671,6 @@ "xpack.data.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText": "より小さいまたは等しい", "xpack.data.kueryAutocomplete.orOperatorDescription": "{oneOrMoreArguments} が true であることを条件とする", "xpack.data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "1つ以上の引数", - "xpack.data.query.queryBar.cancelLongQuery": "キャンセル", - "xpack.data.query.queryBar.runBeyond": "タイムアウトを越えて実行", "xpack.discover.FlyoutCreateDrilldownAction.displayName": "基本データを調査", "xpack.embeddableEnhanced.actions.panelNotifications.manyDrilldowns": "パネルには{count}個のドリルダウンがあります", "xpack.embeddableEnhanced.actions.panelNotifications.oneDrilldown": "パネルには1個のドリルダウンがあります", @@ -7583,7 +7574,6 @@ "xpack.idxMgmt.mappingsEditor.fielddata.frequencyFilterPercentageFieldLabel": "パーセンテージベースの頻度範囲", "xpack.idxMgmt.mappingsEditor.fielddata.useAbsoluteValuesFieldLabel": "絶対値の使用", "xpack.idxMgmt.mappingsEditor.fieldsTabLabel": "マッピングされたフィールド", - "xpack.idxMgmt.mappingsEditor.flattened.ignoreAboveDocLinkText": "上記ドキュメントの無視", "xpack.idxMgmt.mappingsEditor.formatDocLinkText": "フォーマットのドキュメンテーション", "xpack.idxMgmt.mappingsEditor.formatFieldLabel": "フォーマット", "xpack.idxMgmt.mappingsEditor.formatHelpText": "{dateSyntax}構文を使用し、カスタムフォーマットを指定します。", @@ -7714,10 +7704,6 @@ "xpack.idxMgmt.mappingsEditor.joinType.relationshipTable.parentFieldAriaLabel": "親フィールド", "xpack.idxMgmt.mappingsEditor.joinType.relationshipTable.removeRelationshipTooltipLabel": "関係を削除", "xpack.idxMgmt.mappingsEditor.largestShingleSizeFieldLabel": "最大シングルサイズ", - "xpack.idxMgmt.mappingsEditor.leafLengthLimitFieldDescription": "特定の長さ以上のリーフ値のインデックスを無効化。これは、Luceneの文字制限(8,191 UTF-8 文字)に対する保護に役立ちます。", - "xpack.idxMgmt.mappingsEditor.leafLengthLimitFieldTitle": "長さ制限の設定", - "xpack.idxMgmt.mappingsEditor.lengthLimitFieldDescription": "この値よりも長い文字列はインデックスされません。これは、Luceneの文字制限(8,191 UTF-8 文字)に対する保護に役立ちます。", - "xpack.idxMgmt.mappingsEditor.lengthLimitFieldTitle": "長さ制限の設定", "xpack.idxMgmt.mappingsEditor.loadFromJsonButtonLabel": "JSONの読み込み", "xpack.idxMgmt.mappingsEditor.loadJsonModal.acceptWarningLabel": "読み込みの続行", "xpack.idxMgmt.mappingsEditor.loadJsonModal.cancelButtonLabel": "キャンセル", @@ -10892,7 +10878,6 @@ "xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage": "データフレーム分析ジョブ{analyticsId}の削除中にエラーが発生しました。", "xpack.ml.dataframe.analyticsList.deleteAnalyticsPrivilegeErrorMessage": "ユーザーはインデックス{indexName}を削除する権限がありません。{error}", "xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage": "データフレーム分析ジョブ{analyticsId}の削除リクエストが受け付けられました。", - "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexErrorMessage": "ディスティネーションインデックス{destinationIndex}の削除中にエラーが発生しました。{error}", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternErrorMessage": "インデックスパターン{destinationIndex}の削除中にエラーが発生しました。{error}", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternSuccessMessage": "インデックスパターン{destinationIndex}を削除する要求が確認されました。", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexSuccessMessage": "ディスティネーションインデックス{destinationIndex}を削除する要求が確認されました。", @@ -11360,16 +11345,9 @@ "xpack.ml.jobService.activeDatafeedsLabel": "アクティブなデータフィード", "xpack.ml.jobService.activeMLNodesLabel": "アクティブな ML ノード", "xpack.ml.jobService.closedJobsLabel": "ジョブを作成", - "xpack.ml.jobService.couldNotStartDatafeedErrorMessage": "{jobId} のデータフィードを開始できませんでした", - "xpack.ml.jobService.couldNotStopDatafeedErrorMessage": "{jobId} のデータフィードを停止できませんでした", - "xpack.ml.jobService.couldNotUpdateDatafeedErrorMessage": "データフィードを更新できませんでした: {datafeedId}", - "xpack.ml.jobService.datafeedsListCouldNotBeRetrievedErrorMessage": "データフィードリストを取得できませんでした", "xpack.ml.jobService.failedJobsLabel": "失敗したジョブ", - "xpack.ml.jobService.jobsListCouldNotBeRetrievedErrorMessage": "ジョブリストを取得できませんでした", "xpack.ml.jobService.openJobsLabel": "ジョブを開く", - "xpack.ml.jobService.requestMayHaveTimedOutErrorMessage": "リクエストがタイムアウトし、まだバックグラウンドで実行中の可能性があります。", "xpack.ml.jobService.totalJobsLabel": "合計ジョブ数", - "xpack.ml.jobService.updateJobErrorTitle": "ジョブを更新できませんでした: {jobId}", "xpack.ml.jobService.validateJobErrorTitle": "ジョブ検証エラー", "xpack.ml.jobsList.actionExecuteSuccessfullyNotificationMessage": "{successesJobsCount, plural, one{{successJob}} other{# 件のジョブ}} {actionTextPT}成功", "xpack.ml.jobsList.actionFailedNotificationMessage": "{failureId} が {actionText} に失敗しました", @@ -11574,7 +11552,6 @@ "xpack.ml.maxFileSizeSettingsDescription": "ファイルデータビジュアライザーでデータをインポートするときのファイルサイズ上限を設定します。この設定でサポートされている最大値は1 GBです。", "xpack.ml.maxFileSizeSettingsError": "200 MB、1 GBなどの有効なデータサイズにしてください。", "xpack.ml.maxFileSizeSettingsName": "ファイルデータビジュアライザーの最大ファイルアップロードサイズ", - "xpack.ml.messagebarService.errorTitle": "エラーが発生しました", "xpack.ml.models.jobService.allOtherRequestsCancelledDescription": " 他のすべてのリクエストはキャンセルされました。", "xpack.ml.models.jobService.categorization.messages.failureToGetTokens": "フィールド値の例のサンプルをトークン化することができませんでした。{message}", "xpack.ml.models.jobService.categorization.messages.insufficientPrivileges": "権限が不十分なため、フィールド値の例のトークン化を実行できませんでした。そのため、フィールド値を確認し、カテゴリー分けジョブでの使用が適当かを確認することができません。", @@ -13721,7 +13698,6 @@ "xpack.monitoring.updateLicenseTitle": "ライセンスの更新", "xpack.monitoring.useAvailableLicenseDescription": "既に新しいライセンスがある場合は、今すぐアップロードしてください。", "xpack.monitoring.wedLabel": "水", - "xpack.observability.beta": "ベータ", "xpack.observability.emptySection.apps.alert.description": "503エラーが累積していますか?サービスは応答していますか?CPUとRAMの使用量が跳ね上がっていますか?このような警告を、事後にではなく、発生と同時に把握しましょう。", "xpack.observability.emptySection.apps.alert.link": "アラートの作成", "xpack.observability.emptySection.apps.alert.title": "アラートが見つかりません。", @@ -14081,8 +14057,6 @@ "xpack.reporting.screencapture.waitingForRenderComplete": "レンダリングの完了を待っています", "xpack.reporting.screencapture.waitingForRenderedElements": "レンダリングされた {itemsCount} 個の要素が DOM に入るのを待っています", "xpack.reporting.screenCapturePanelContent.optimizeForPrintingLabel": "印刷用に最適化", - "xpack.reporting.selfCheck.ok": "レポートプラグイン自己チェックOK!", - "xpack.reporting.selfCheck.warning": "レポートプラグイン自己チェックで警告が発生しました: {err}", "xpack.reporting.serverConfig.autoSet.sandboxDisabled": "Chromiumサンドボックスは保護が強化されていますが、{osName} OSではサポートされていません。自動的に'{configKey}: true'を設定しています。", "xpack.reporting.serverConfig.autoSet.sandboxEnabled": "Chromiumサンドボックスは保護が強化され、{osName} OSでサポートされています。自動的にChromiumサンドボックスを有効にしています。", "xpack.reporting.serverConfig.invalidServerHostname": "Kibana構成で「server.host:\"0\"」が見つかりました。これはReportingと互換性がありません。レポートが動作するように、「{configKey}:0.0.0.0」が自動的に構成になります。設定を「server.host:0.0.0.0」に変更するか、kibana.ymlに「{configKey}:0.0.0.0'」を追加して、このメッセージが表示されないようにすることができます。", @@ -15111,11 +15085,7 @@ "xpack.securitySolution.case.configureCases.incidentManagementSystemDesc": "オプションとして、セキュリティケースを選択した外部のインシデント管理システムに接続できます。そうすると、選択したサードパーティシステム内でケースデータをインシデントとしてプッシュできます。", "xpack.securitySolution.case.configureCases.incidentManagementSystemLabel": "インシデント管理システム", "xpack.securitySolution.case.configureCases.incidentManagementSystemTitle": "外部のインシデント管理システムに接続", - "xpack.securitySolution.case.configureCases.mappingFieldComments": "コメント", - "xpack.securitySolution.case.configureCases.mappingFieldDescription": "説明", - "xpack.securitySolution.case.configureCases.mappingFieldName": "名前", "xpack.securitySolution.case.configureCases.mappingFieldNotMapped": "マップされません", - "xpack.securitySolution.case.configureCases.mappingFieldSummary": "まとめ", "xpack.securitySolution.case.configureCases.noConnector": "コネクターを選択していません", "xpack.securitySolution.case.configureCases.updateConnector": "コネクターを更新", "xpack.securitySolution.case.configureCases.updateSelectedConnector": "{ connectorName }を更新", @@ -15128,33 +15098,6 @@ "xpack.securitySolution.case.confirmDeleteCase.deleteCases": "ケースを削除", "xpack.securitySolution.case.confirmDeleteCase.deleteTitle": "「{caseTitle}」を削除", "xpack.securitySolution.case.confirmDeleteCase.selectedCases": "選択したケースを削除", - "xpack.securitySolution.case.connectors.common.apiTokenTextFieldLabel": "APIトークン", - "xpack.securitySolution.case.connectors.common.apiUrlTextFieldLabel": "URL", - "xpack.securitySolution.case.connectors.common.emailTextFieldLabel": "メール", - "xpack.securitySolution.case.connectors.common.invalidApiUrlTextField": "URLが無効です", - "xpack.securitySolution.case.connectors.common.passwordTextFieldLabel": "パスワード", - "xpack.securitySolution.case.connectors.common.requiredApiTokenTextField": "APIトークンが必要です", - "xpack.securitySolution.case.connectors.common.requiredApiUrlTextField": "URLが必要です", - "xpack.securitySolution.case.connectors.common.requiredEmailTextField": "電子メールが必要です", - "xpack.securitySolution.case.connectors.common.requiredPasswordTextField": "パスワードが必要です", - "xpack.securitySolution.case.connectors.common.requiredUsernameTextField": "ユーザー名が必要です", - "xpack.securitySolution.case.connectors.common.usernameTextFieldLabel": "ユーザー名", - "xpack.securitySolution.case.connectors.jira.actionTypeTitle": "Jira", - "xpack.securitySolution.case.connectors.jira.apiTokenTextFieldLabel": "APIトークンまたはパスワード", - "xpack.securitySolution.case.connectors.jira.emailTextFieldLabel": "電子メールアドレスまたはユーザー名", - "xpack.securitySolution.case.connectors.jira.projectKey": "プロジェクトキー", - "xpack.securitySolution.case.connectors.jira.requiredApiTokenTextField": "APIトークンまたはパスワードが必要です", - "xpack.securitySolution.case.connectors.jira.requiredEmailTextField": "電子メールアドレスまたはユーザー名が必要です", - "xpack.securitySolution.case.connectors.jira.requiredProjectKeyTextField": "プロジェクトキーが必要です", - "xpack.securitySolution.case.connectors.jira.selectMessageText": "Jiraでセキュリティケースデータを更新するか、新しいインシデントにプッシュ", - "xpack.securitySolution.case.connectors.resilient.actionTypeTitle": "IBM Resilient", - "xpack.securitySolution.case.connectors.resilient.apiKeyId": "APIキーID", - "xpack.securitySolution.case.connectors.resilient.apiKeySecret": "APIキーシークレット", - "xpack.securitySolution.case.connectors.resilient.orgId": "組織ID", - "xpack.securitySolution.case.connectors.resilient.requiredApiKeyIdTextField": "APIキーIDが必要です", - "xpack.securitySolution.case.connectors.resilient.requiredApiKeySecretTextField": "APIキーシークレットが必要です", - "xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField": "組織IDが必要です", - "xpack.securitySolution.case.connectors.resilient.selectMessageText": "Resilientでセキュリティケースデータを更新するか、新しいインシデントにプッシュ", "xpack.securitySolution.case.createCase.descriptionFieldRequiredError": "説明が必要です。", "xpack.securitySolution.case.createCase.fieldTagsHelpText": "このケースの1つ以上のカスタム識別タグを入力します。新しいタグを開始するには、各タグの後でEnterを押します。", "xpack.securitySolution.case.createCase.titleFieldRequiredError": "タイトルが必要です。", @@ -17600,8 +17543,6 @@ "xpack.spaces.management.copyToSpace.actionTitle": "スペースにコピー", "xpack.spaces.management.copyToSpace.copyErrorTitle": "保存されたオブジェクトのコピー中にエラーが発生", "xpack.spaces.management.copyToSpace.copyResultsLabel": "コピー結果", - "xpack.spaces.management.copyToSpace.copyStatus.conflictsMessage": "このスペースには同じID({id})の保存されたオブジェクトが既に存在します。", - "xpack.spaces.management.copyToSpace.copyStatus.conflictsOverwriteMessage": "「上書き」をクリックしてこのバージョンをコピーされたバージョンに置き換えます。", "xpack.spaces.management.copyToSpace.copyStatus.pendingOverwriteMessage": "保存されたオブジェクトは上書きされます。「スキップ」をクリックしてこの操作をキャンセルします。", "xpack.spaces.management.copyToSpace.copyStatus.successMessage": "保存されたオブジェクトがコピーされました。", "xpack.spaces.management.copyToSpace.copyStatus.unresolvableErrorMessage": "この保存されたオブジェクトのコピー中にエラーが発生しました。", @@ -17610,8 +17551,6 @@ "xpack.spaces.management.copyToSpace.copyStatusSummary.successMessage": "{space}スペースにコピーされました。", "xpack.spaces.management.copyToSpace.copyToSpacesButton": "{spaceCount} {spaceCount, plural, one {スペース} other {スペース}}にコピー", "xpack.spaces.management.copyToSpace.disabledCopyToSpacesButton": "コピー", - "xpack.spaces.management.copyToSpace.dontIncludeRelatedLabel": "関連性のある保存されたオブジェクトを含みません", - "xpack.spaces.management.copyToSpace.dontOverwriteLabel": "保存されたオブジェクトを上書きしません", "xpack.spaces.management.copyToSpace.finishCopyToSpacesButton": "終了", "xpack.spaces.management.copyToSpace.finishedButtonLabel": "コピーが完了しました。", "xpack.spaces.management.copyToSpace.finishPendingOverwritesCopyToSpacesButton": "{overwriteCount}件のオブジェクトを上書き", @@ -17619,10 +17558,8 @@ "xpack.spaces.management.copyToSpace.inProgressButtonLabel": "コピーが進行中です。お待ちください。", "xpack.spaces.management.copyToSpace.noSpacesBody": "コピーできるスペースがありません。", "xpack.spaces.management.copyToSpace.noSpacesTitle": "スペースがありません", - "xpack.spaces.management.copyToSpace.overwriteLabel": "保存されたオブジェクトを自動的に上書きしています", "xpack.spaces.management.copyToSpace.resolveCopyErrorTitle": "保存されたオブジェクトの矛盾の解決中にエラーが発生", "xpack.spaces.management.copyToSpace.resolveCopySuccessTitle": "上書き成功", - "xpack.spaces.management.copyToSpace.selectSpacesLabel": "コピー先のスペースを選択してください", "xpack.spaces.management.copyToSpace.spacesLoadErrorTitle": "利用可能なスペースを読み込み中にエラーが発生", "xpack.spaces.management.copyToSpaceFlyoutFooter.errorCount": "エラー", "xpack.spaces.management.copyToSpaceFlyoutFooter.pendingCount": "保留中", @@ -17701,7 +17638,6 @@ "xpack.spaces.management.spacesGridPage.spacesTitle": "スペース", "xpack.spaces.management.spacesGridPage.spaceSuccessfullyDeletedNotificationMessage": "「{spaceName}」 スペースが削除されました。", "xpack.spaces.management.toggleAllFeaturesLink": "(すべて変更)", - "xpack.spaces.management.unauthorizedPrompt.permissionDeniedDescription": "スペースを管理するパーミッションがありません。", "xpack.spaces.management.unauthorizedPrompt.permissionDeniedTitle": "パーミッションが拒否されました", "xpack.spaces.management.validateSpace.describeMaxLengthErrorMessage": "説明は 2000 文字以内でなければなりません。", "xpack.spaces.management.validateSpace.nameMaxLengthErrorMessage": "名前はは 1024 文字以内でなければなりません。", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index b9fb6340e38cf..c8eefb45ea9f5 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -817,8 +817,6 @@ "data.query.queryBar.KQLNestedQuerySyntaxInfoTitle": "KQL 嵌套查询语法", "data.query.queryBar.kqlOffLabel": "关闭", "data.query.queryBar.kqlOnLabel": "开启", - "data.query.queryBar.licenseOptions": "前往许可证选项", - "data.query.queryBar.longQueryMessage": "使用升级的许可证,您可以确保有足够的时间来完成请求。", "data.query.queryBar.luceneLanguageName": "Lucene", "data.query.queryBar.luceneSyntaxWarningMessage": "尽管您选择了 Kibana 查询语言 (KQL),但似乎您正在尝试使用 Lucene 查询语法。请查看 KQL 文档 {link}。", "data.query.queryBar.luceneSyntaxWarningOptOutText": "不再显示", @@ -2881,7 +2879,6 @@ "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsDescription": "受影响对象样例", "savedObjectsManagement.objectsTable.flyout.renderConflicts.columnSampleOfAffectedObjectsName": "受影响对象样例", "savedObjectsManagement.objectsTable.flyout.resolveImportErrorsFileErrorMessage": "无法处理该文件。", - "savedObjectsManagement.objectsTable.flyout.selectFileToImportFormRowLabel": "请选择要导入的文件", "savedObjectsManagement.objectsTable.header.exportButtonLabel": "导出 {filteredCount, plural, one{# 个对象} other {# 个对象}}", "savedObjectsManagement.objectsTable.header.importButtonLabel": "导入", "savedObjectsManagement.objectsTable.header.refreshButtonLabel": "刷新", @@ -4447,7 +4444,6 @@ "xpack.actions.builtin.case.connectorApiNullError": "需要指定连接器 [apiUrl]", "xpack.actions.builtin.case.jiraTitle": "Jira", "xpack.actions.builtin.case.resilientTitle": "IBM Resilient", - "xpack.actions.builtin.configuration.apiWhitelistError": "配置连接器操作时出错:{message}", "xpack.actions.builtin.email.errorSendingErrorMessage": "发送电子邮件时出错", "xpack.actions.builtin.emailTitle": "电子邮件", "xpack.actions.builtin.esIndex.errorIndexingErrorMessage": "索引文档时出错", @@ -4865,12 +4861,9 @@ "xpack.apm.serviceOverview.upgradeAssistantLink": "升级助手", "xpack.apm.servicesTable.7xOldDataMessage": "可能还有需要迁移的旧数据。", "xpack.apm.servicesTable.7xUpgradeServerMessage": "从 7.x 之前的版本升级?另外,确保您已将\n APM Server 实例升级到至少 7.0。", - "xpack.apm.servicesTable.agentColumnLabel": "代理", "xpack.apm.servicesTable.avgResponseTimeColumnLabel": "平均响应时间", "xpack.apm.servicesTable.environmentColumnLabel": "环境", "xpack.apm.servicesTable.environmentCount": "{environmentCount, plural, one {1 个环境} other {# 个环境}}", - "xpack.apm.servicesTable.errorsPerMinuteColumnLabel": "每分钟错误数", - "xpack.apm.servicesTable.errorsPerMinuteUnitLabel": "错误", "xpack.apm.servicesTable.nameColumnLabel": "名称", "xpack.apm.servicesTable.noServicesLabel": "似乎您没有安装任何 APM 服务。让我们添加一些!", "xpack.apm.servicesTable.notFoundLabel": "未找到任何服务", @@ -6681,8 +6674,6 @@ "xpack.data.kueryAutocomplete.lessThanOrEqualOperatorDescription.lessThanOrEqualToText": "小于或等于", "xpack.data.kueryAutocomplete.orOperatorDescription": "需要{oneOrMoreArguments}为 true", "xpack.data.kueryAutocomplete.orOperatorDescription.oneOrMoreArgumentsText": "一个或多个参数", - "xpack.data.query.queryBar.cancelLongQuery": "取消", - "xpack.data.query.queryBar.runBeyond": "运行超时", "xpack.discover.FlyoutCreateDrilldownAction.displayName": "浏览底层数据", "xpack.embeddableEnhanced.actions.panelNotifications.manyDrilldowns": "面板有 {count} 个向下钻取", "xpack.embeddableEnhanced.actions.panelNotifications.oneDrilldown": "面板有 1 个向下钻取", @@ -7586,7 +7577,6 @@ "xpack.idxMgmt.mappingsEditor.fielddata.frequencyFilterPercentageFieldLabel": "基于百分比的频率范围", "xpack.idxMgmt.mappingsEditor.fielddata.useAbsoluteValuesFieldLabel": "使用绝对值", "xpack.idxMgmt.mappingsEditor.fieldsTabLabel": "已映射字段", - "xpack.idxMgmt.mappingsEditor.flattened.ignoreAboveDocLinkText": "“忽略上述”文档", "xpack.idxMgmt.mappingsEditor.formatDocLinkText": "“格式”文档", "xpack.idxMgmt.mappingsEditor.formatFieldLabel": "格式", "xpack.idxMgmt.mappingsEditor.formatHelpText": "使用 {dateSyntax} 语法指定定制格式。", @@ -7717,10 +7707,6 @@ "xpack.idxMgmt.mappingsEditor.joinType.relationshipTable.parentFieldAriaLabel": "父项字段", "xpack.idxMgmt.mappingsEditor.joinType.relationshipTable.removeRelationshipTooltipLabel": "移除关系", "xpack.idxMgmt.mappingsEditor.largestShingleSizeFieldLabel": "最大瓦形大小", - "xpack.idxMgmt.mappingsEditor.leafLengthLimitFieldDescription": "如果叶值超过一定长度,则阻止叶值索引。这用于防止超出 Lucene 的字词字符长度限制,即 8,191 个 UTF-8 字符。", - "xpack.idxMgmt.mappingsEditor.leafLengthLimitFieldTitle": "设置长度限制", - "xpack.idxMgmt.mappingsEditor.lengthLimitFieldDescription": "将不索引超过此值的字符串。这用于防止超出 Lucene 的字词字符长度限制,即 8,191 个 UTF-8 字符。", - "xpack.idxMgmt.mappingsEditor.lengthLimitFieldTitle": "设置长度限制", "xpack.idxMgmt.mappingsEditor.loadFromJsonButtonLabel": "加载 JSON", "xpack.idxMgmt.mappingsEditor.loadJsonModal.acceptWarningLabel": "继续加载", "xpack.idxMgmt.mappingsEditor.loadJsonModal.cancelButtonLabel": "取消", @@ -10898,7 +10884,6 @@ "xpack.ml.dataframe.analyticsList.deleteAnalyticsErrorMessage": "删除数据帧分析作业 {analyticsId} 时发生错误", "xpack.ml.dataframe.analyticsList.deleteAnalyticsPrivilegeErrorMessage": "用户无权删除索引 {indexName}:{error}", "xpack.ml.dataframe.analyticsList.deleteAnalyticsSuccessMessage": "删除的数据帧分析作业 {analyticsId} 的请求已确认。", - "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexErrorMessage": "删除目标索引 {destinationIndex} 时发生错误:{error}", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternErrorMessage": "删除索引模式 {destinationIndex} 时发生错误:{error}", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexPatternSuccessMessage": "删除索引模式 {destinationIndex} 的请求已确认。", "xpack.ml.dataframe.analyticsList.deleteAnalyticsWithIndexSuccessMessage": "删除目标索引 {destinationIndex} 的请求已确认。", @@ -11367,16 +11352,9 @@ "xpack.ml.jobService.activeDatafeedsLabel": "活动数据馈送", "xpack.ml.jobService.activeMLNodesLabel": "活动 ML 节点", "xpack.ml.jobService.closedJobsLabel": "已关闭的作业", - "xpack.ml.jobService.couldNotStartDatafeedErrorMessage": "无法开始 {jobId} 的数据馈送", - "xpack.ml.jobService.couldNotStopDatafeedErrorMessage": "无法停止 {jobId} 的数据馈送", - "xpack.ml.jobService.couldNotUpdateDatafeedErrorMessage": "无法更新数据馈送:{datafeedId}", - "xpack.ml.jobService.datafeedsListCouldNotBeRetrievedErrorMessage": "无法检索数据馈送列表", "xpack.ml.jobService.failedJobsLabel": "失败的作业", - "xpack.ml.jobService.jobsListCouldNotBeRetrievedErrorMessage": "无法检索作业列表", "xpack.ml.jobService.openJobsLabel": "打开的作业", - "xpack.ml.jobService.requestMayHaveTimedOutErrorMessage": "请求可能已超时,并可能仍在后台运行。", "xpack.ml.jobService.totalJobsLabel": "总计作业数", - "xpack.ml.jobService.updateJobErrorTitle": "无法更新作业:{jobId}", "xpack.ml.jobService.validateJobErrorTitle": "作业验证错误", "xpack.ml.jobsList.actionExecuteSuccessfullyNotificationMessage": "{successesJobsCount, plural, one{{successJob}} other{# 个作业}}{actionTextPT}已成功", "xpack.ml.jobsList.actionFailedNotificationMessage": "{failureId} 未能{actionText}", @@ -11581,7 +11559,6 @@ "xpack.ml.maxFileSizeSettingsDescription": "设置在文件数据可视化工具中导入数据时的文件大小限制。此设置支持的最高值为 1GB。", "xpack.ml.maxFileSizeSettingsError": "应为有效的数据大小。如 200MB、1GB", "xpack.ml.maxFileSizeSettingsName": "文件数据可视化工具最大文件上传大小", - "xpack.ml.messagebarService.errorTitle": "发生了错误", "xpack.ml.models.jobService.allOtherRequestsCancelledDescription": " 所有其他请求已取消。", "xpack.ml.models.jobService.categorization.messages.failureToGetTokens": "无法对示例字段值样本进行分词。{message}", "xpack.ml.models.jobService.categorization.messages.insufficientPrivileges": "由于权限不足,无法对字段值示例执行分词。因此,无法检查字段值是否适合用于归类作业。", @@ -13730,7 +13707,6 @@ "xpack.monitoring.updateLicenseTitle": "更新您的许可证", "xpack.monitoring.useAvailableLicenseDescription": "如果已有新的许可证,请立即上传。", "xpack.monitoring.wedLabel": "周三", - "xpack.observability.beta": "公测版", "xpack.observability.emptySection.apps.alert.description": "503 错误是否越来越多?服务是否响应?CPU 和 RAM 利用率是否激增?实时查看警告,而不是事后再进行剖析。", "xpack.observability.emptySection.apps.alert.link": "创建告警", "xpack.observability.emptySection.apps.alert.title": "未找到告警。", @@ -14090,8 +14066,6 @@ "xpack.reporting.screencapture.waitingForRenderComplete": "正在等候渲染完成", "xpack.reporting.screencapture.waitingForRenderedElements": "正在等候 {itemsCount} 个已渲染元素进入 DOM", "xpack.reporting.screenCapturePanelContent.optimizeForPrintingLabel": "打印优化", - "xpack.reporting.selfCheck.ok": "Reporting 插件自检正常!", - "xpack.reporting.selfCheck.warning": "Reporting 插件自检生成警告:{err}", "xpack.reporting.serverConfig.autoSet.sandboxDisabled": "Chromium 沙盒提供附加保护层,但不受 {osName} OS 支持。自动设置“{configKey}: true”。", "xpack.reporting.serverConfig.autoSet.sandboxEnabled": "Chromium 沙盒提供附加保护层,受 {osName} OS 支持。自动启用 Chromium 沙盒。", "xpack.reporting.serverConfig.invalidServerHostname": "在 Kibana 配置中找到“server.host:\"0\"”。其不与 Reporting 兼容。要使 Reporting 运行,“{configKey}:0.0.0.0”将自动添加到配置中。可以将该设置更改为“server.host:0.0.0.0”或在 kibana.yml 中添加“{configKey}:0.0.0.0”,以阻止此消息。", @@ -15120,11 +15094,7 @@ "xpack.securitySolution.case.configureCases.incidentManagementSystemDesc": "您可能会根据需要将 Security 案例连接到选择的外部事件管理系统。这将允许您将案例数据作为事件推送到所选第三方系统。", "xpack.securitySolution.case.configureCases.incidentManagementSystemLabel": "事件管理系统", "xpack.securitySolution.case.configureCases.incidentManagementSystemTitle": "连接到外部事件管理系统", - "xpack.securitySolution.case.configureCases.mappingFieldComments": "注释", - "xpack.securitySolution.case.configureCases.mappingFieldDescription": "描述", - "xpack.securitySolution.case.configureCases.mappingFieldName": "名称", "xpack.securitySolution.case.configureCases.mappingFieldNotMapped": "未映射", - "xpack.securitySolution.case.configureCases.mappingFieldSummary": "摘要", "xpack.securitySolution.case.configureCases.noConnector": "未选择连接器", "xpack.securitySolution.case.configureCases.updateConnector": "更新连接器", "xpack.securitySolution.case.configureCases.updateSelectedConnector": "更新 { connectorName }", @@ -15137,33 +15107,6 @@ "xpack.securitySolution.case.confirmDeleteCase.deleteCases": "删除案例", "xpack.securitySolution.case.confirmDeleteCase.deleteTitle": "删除“{caseTitle}”", "xpack.securitySolution.case.confirmDeleteCase.selectedCases": "删除选定案例", - "xpack.securitySolution.case.connectors.common.apiTokenTextFieldLabel": "API 令牌", - "xpack.securitySolution.case.connectors.common.apiUrlTextFieldLabel": "URL", - "xpack.securitySolution.case.connectors.common.emailTextFieldLabel": "电子邮件", - "xpack.securitySolution.case.connectors.common.invalidApiUrlTextField": "URL 无效", - "xpack.securitySolution.case.connectors.common.passwordTextFieldLabel": "密码", - "xpack.securitySolution.case.connectors.common.requiredApiTokenTextField": "“API 令牌”必填", - "xpack.securitySolution.case.connectors.common.requiredApiUrlTextField": "“URL”必填", - "xpack.securitySolution.case.connectors.common.requiredEmailTextField": "“电子邮件”必填", - "xpack.securitySolution.case.connectors.common.requiredPasswordTextField": "“密码”必填", - "xpack.securitySolution.case.connectors.common.requiredUsernameTextField": "“用户名”必填", - "xpack.securitySolution.case.connectors.common.usernameTextFieldLabel": "用户名", - "xpack.securitySolution.case.connectors.jira.actionTypeTitle": "Jira", - "xpack.securitySolution.case.connectors.jira.apiTokenTextFieldLabel": "API 令牌或密码", - "xpack.securitySolution.case.connectors.jira.emailTextFieldLabel": "电子邮件或用户名", - "xpack.securitySolution.case.connectors.jira.projectKey": "项目键", - "xpack.securitySolution.case.connectors.jira.requiredApiTokenTextField": "“API 令牌或密码”必填", - "xpack.securitySolution.case.connectors.jira.requiredEmailTextField": "“电子邮件或用户名”必填", - "xpack.securitySolution.case.connectors.jira.requiredProjectKeyTextField": "“项目键”必填", - "xpack.securitySolution.case.connectors.jira.selectMessageText": "将 Security 案例数据推送或更新到 Jira 中的新问题", - "xpack.securitySolution.case.connectors.resilient.actionTypeTitle": "IBM Resilient", - "xpack.securitySolution.case.connectors.resilient.apiKeyId": "API 密钥 ID", - "xpack.securitySolution.case.connectors.resilient.apiKeySecret": "API 密钥密码", - "xpack.securitySolution.case.connectors.resilient.orgId": "组织 ID", - "xpack.securitySolution.case.connectors.resilient.requiredApiKeyIdTextField": "“API 密钥 ID”必填", - "xpack.securitySolution.case.connectors.resilient.requiredApiKeySecretTextField": "“API 密钥密码”必填", - "xpack.securitySolution.case.connectors.resilient.requiredOrgIdTextField": "“组织 ID”必填", - "xpack.securitySolution.case.connectors.resilient.selectMessageText": "将 Security 案例数据推送或更新到 Resilient 中的新问题", "xpack.securitySolution.case.createCase.descriptionFieldRequiredError": "描述必填。", "xpack.securitySolution.case.createCase.fieldTagsHelpText": "为此案例键入一个或多个定制识别标记。在每个标记后按 Enter 键可开始新的标记。", "xpack.securitySolution.case.createCase.titleFieldRequiredError": "标题必填。", @@ -17610,8 +17553,6 @@ "xpack.spaces.management.copyToSpace.actionTitle": "复制到工作区", "xpack.spaces.management.copyToSpace.copyErrorTitle": "复制已保存对象时出错", "xpack.spaces.management.copyToSpace.copyResultsLabel": "复制结果", - "xpack.spaces.management.copyToSpace.copyStatus.conflictsMessage": "具有匹配 ID ({id}) 的已保存对象在此工作区中已存在。", - "xpack.spaces.management.copyToSpace.copyStatus.conflictsOverwriteMessage": "单击“覆盖”可将此版本替换为复制的版本。", "xpack.spaces.management.copyToSpace.copyStatus.pendingOverwriteMessage": "已保存对象将被覆盖。单击“跳过”可取消此操作。", "xpack.spaces.management.copyToSpace.copyStatus.successMessage": "已保存对象成功复制。", "xpack.spaces.management.copyToSpace.copyStatus.unresolvableErrorMessage": "复制此已保存对象时出错。", @@ -17620,8 +17561,6 @@ "xpack.spaces.management.copyToSpace.copyStatusSummary.successMessage": "已成功复制到 {space} 工作区。", "xpack.spaces.management.copyToSpace.copyToSpacesButton": "复制到 {spaceCount} {spaceCount, plural, one {个工作区} other {个工作区}}", "xpack.spaces.management.copyToSpace.disabledCopyToSpacesButton": "复制", - "xpack.spaces.management.copyToSpace.dontIncludeRelatedLabel": "不包括相关已保存对象", - "xpack.spaces.management.copyToSpace.dontOverwriteLabel": "未覆盖已保存对象", "xpack.spaces.management.copyToSpace.finishCopyToSpacesButton": "完成", "xpack.spaces.management.copyToSpace.finishedButtonLabel": "复制已完成。", "xpack.spaces.management.copyToSpace.finishPendingOverwritesCopyToSpacesButton": "覆盖 {overwriteCount} 个对象", @@ -17629,10 +17568,8 @@ "xpack.spaces.management.copyToSpace.inProgressButtonLabel": "复制正在进行中。请稍候。", "xpack.spaces.management.copyToSpace.noSpacesBody": "没有可向其中进行复制的合格工作区。", "xpack.spaces.management.copyToSpace.noSpacesTitle": "没有可用的工作区", - "xpack.spaces.management.copyToSpace.overwriteLabel": "正在自动覆盖已保存对象", "xpack.spaces.management.copyToSpace.resolveCopyErrorTitle": "解决已保存对象冲突时出错", "xpack.spaces.management.copyToSpace.resolveCopySuccessTitle": "覆盖成功", - "xpack.spaces.management.copyToSpace.selectSpacesLabel": "选择要向其中进行复制的工作区", "xpack.spaces.management.copyToSpace.spacesLoadErrorTitle": "加载可用工作区时出错", "xpack.spaces.management.copyToSpaceFlyoutFooter.errorCount": "错误", "xpack.spaces.management.copyToSpaceFlyoutFooter.pendingCount": "待处理", @@ -17711,7 +17648,6 @@ "xpack.spaces.management.spacesGridPage.spacesTitle": "工作区", "xpack.spaces.management.spacesGridPage.spaceSuccessfullyDeletedNotificationMessage": "已删除 “{spaceName}” 空间。", "xpack.spaces.management.toggleAllFeaturesLink": "(全部更改)", - "xpack.spaces.management.unauthorizedPrompt.permissionDeniedDescription": "您没有权限管理空间。", "xpack.spaces.management.unauthorizedPrompt.permissionDeniedTitle": "权限被拒绝", "xpack.spaces.management.validateSpace.describeMaxLengthErrorMessage": "描述不能超过 2000 个字符。", "xpack.spaces.management.validateSpace.nameMaxLengthErrorMessage": "名称不能超过 1024 个字符。", diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping.tsx similarity index 95% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping.tsx index 52b881a1eb75f..a3382513d2bcb 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping.tsx @@ -13,9 +13,8 @@ import * as i18n from './translations'; import { setActionTypeToMapping, setThirdPartyToMapping } from './utils'; import { ThirdPartyField as ConnectorConfigurationThirdPartyField } from './types'; -import { CasesConfigurationMapping } from '../types'; -import { connectorConfiguration } from '../config'; -import { createDefaultMapping } from '../servicenow_connectors'; +import { CasesConfigurationMapping } from './types'; +import { createDefaultMapping } from './utils'; const FieldRowWrapper = styled.div` margin-top: 8px; @@ -70,15 +69,15 @@ const getThirdPartyOptions = ( export interface FieldMappingProps { disabled: boolean; mapping: CasesConfigurationMapping[] | null; - connectorActionTypeId: string; onChangeMapping: (newMapping: CasesConfigurationMapping[]) => void; + connectorConfiguration: Record; } const FieldMappingComponent: React.FC = ({ disabled, mapping, onChangeMapping, - connectorActionTypeId, + connectorConfiguration, }) => { const onChangeActionType = useCallback( (caseField: string, newActionType: string) => { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping_row.tsx similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/field_mapping_row.tsx rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/field_mapping_row.tsx diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/index.ts new file mode 100644 index 0000000000000..2de9b87ead3fe --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/index.ts @@ -0,0 +1,10 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export * from './types'; +export * from './field_mapping'; +export * from './field_mapping_row'; +export * from './utils'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/translations.ts similarity index 100% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/translations.ts rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/translations.ts diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/types.ts similarity index 72% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/types.ts index 6cd2200e1dc74..3571db39b596a 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/types.ts @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ActionType } from '../../../../../types'; +import { ActionType } from '../../../../types'; export { ActionType }; @@ -14,3 +14,8 @@ export interface ThirdPartyField { defaultSourceField: string; defaultActionType: string; } +export interface CasesConfigurationMapping { + source: string; + target: string; + actionType: string; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/utils.ts similarity index 76% rename from x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/utils.ts index a173d90515302..b14b1b76427c6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/case_mappings/utils.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/case_mappings/utils.ts @@ -3,7 +3,7 @@ * or more contributor license agreements. Licensed under the Elastic License; * you may not use this file except in compliance with the Elastic License. */ -import { CasesConfigurationMapping } from '../types'; +import { CasesConfigurationMapping } from './types'; export const setActionTypeToMapping = ( caseField: string, @@ -36,3 +36,13 @@ export const setThirdPartyToMapping = ( } return item; }); + +export const createDefaultMapping = (fields: Record): CasesConfigurationMapping[] => + Object.keys(fields).map( + (key) => + ({ + source: fields[key].defaultSourceField, + target: key, + actionType: fields[key].defaultActionType, + } as CasesConfigurationMapping) + ); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx index ecdfefa109f58..8c37dc940a238 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/email/email_params.test.tsx @@ -17,6 +17,7 @@ describe('EmailParamsFields renders', () => { subject: 'test', message: 'test message', }; + const wrapper = mountWithIntl( { docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} /> ); + expect(wrapper.find('[data-test-subj="toEmailAddressInput"]').length > 0).toBeTruthy(); expect( wrapper.find('[data-test-subj="toEmailAddressInput"]').first().prop('selectedOptions') diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx index 25c04bda3f536..a882e3bc43f34 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/es_index/es_index_params.test.tsx @@ -13,6 +13,7 @@ describe('IndexParamsFields renders', () => { const actionParams = { documents: [{ test: 123 }], }; + const wrapper = mountWithIntl( > { + return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + body: JSON.stringify({ + params: { subAction: 'issueTypes', subActionParams: {} }, + }), + signal, + }); +} + +export async function getFieldsByIssueType({ + http, + signal, + connectorId, + id, +}: { + http: HttpSetup; + signal: AbortSignal; + connectorId: string; + id: string; +}): Promise> { + return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + body: JSON.stringify({ + params: { subAction: 'fieldsByIssueType', subActionParams: { id } }, + }), + signal, + }); +} diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts similarity index 87% rename from x-pack/plugins/security_solution/public/common/lib/connectors/jira/config.ts rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts index e6151a54bff74..628600ee91c8e 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/config.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/config.ts @@ -4,19 +4,17 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConnectorConfiguration } from './types'; - import * as i18n from './translations'; import logo from './logo.svg'; -export const connector: ConnectorConfiguration = { +export const connectorConfiguration = { id: '.jira', name: i18n.JIRA_TITLE, logo, enabled: true, enabledInConfig: true, enabledInLicense: true, - minimumLicenseRequired: 'platinum', + minimumLicenseRequired: 'gold', fields: { summary: { label: i18n.MAPPING_FIELD_SUMMARY, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/index.ts new file mode 100644 index 0000000000000..a0170f9d84e9b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getActionType as getJiraActionType } from './jira'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx new file mode 100644 index 0000000000000..61923d8f78b51 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.test.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { JiraActionConnector } from './types'; + +const ACTION_TYPE_ID = '.jira'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + }); +}); + +describe('jira connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: { + email: 'email', + apiToken: 'apiToken', + }, + id: 'test', + actionTypeId: '.jira', + name: 'jira', + isPreconfigured: false, + config: { + apiUrl: 'https://siem-kibana.atlassian.net', + projectKey: 'CK', + }, + } as JiraActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + apiUrl: [], + email: [], + apiToken: [], + projectKey: [], + }, + }); + }); + + test('connector validation fails when connector config is not valid', () => { + const actionConnector = ({ + secrets: { + email: 'user', + }, + id: '.jira', + actionTypeId: '.jira', + name: 'jira', + config: {}, + } as unknown) as JiraActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + apiUrl: ['URL is required.'], + email: [], + apiToken: ['API token or Password is required'], + projectKey: ['Project key is required'], + }, + }); + }); +}); + +describe('jira action params validation', () => { + test('action params validation succeeds when action params is valid', () => { + const actionParams = { + subActionParams: { title: 'some title {{test}}' }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { title: [] }, + }); + }); + + test('params validation fails when body is not valid', () => { + const actionParams = { + subActionParams: { title: '' }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + title: ['Title is required.'], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx new file mode 100644 index 0000000000000..fd36bd6aeab0a --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { lazy } from 'react'; +import { ValidationResult, ActionTypeModel } from '../../../../types'; +import { connectorConfiguration } from './config'; +import logo from './logo.svg'; +import { JiraActionConnector, JiraActionParams } from './types'; +import * as i18n from './translations'; +import { isValidUrl } from '../../../lib/value_validators'; + +const validateConnector = (action: JiraActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + apiUrl: new Array(), + projectKey: new Array(), + email: new Array(), + apiToken: new Array(), + }; + validationResult.errors = errors; + + if (!action.config.apiUrl) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_REQUIRED]; + } + + if (action.config.apiUrl && !isValidUrl(action.config.apiUrl, 'https:')) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_INVALID]; + } + + if (!action.config.projectKey) { + errors.projectKey = [...errors.projectKey, i18n.JIRA_PROJECT_KEY_REQUIRED]; + } + + if (!action.secrets.email) { + errors.email = [...errors.email, i18n.JIRA_EMAIL_REQUIRED]; + } + + if (!action.secrets.apiToken) { + errors.apiToken = [...errors.apiToken, i18n.JIRA_API_TOKEN_REQUIRED]; + } + + return validationResult; +}; + +export function getActionType(): ActionTypeModel { + return { + id: connectorConfiguration.id, + iconClass: logo, + selectMessage: i18n.JIRA_DESC, + actionTypeTitle: connectorConfiguration.name, + validateConnector, + actionConnectorFields: lazy(() => import('./jira_connectors')), + validateParams: (actionParams: JiraActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + title: new Array(), + }; + validationResult.errors = errors; + if (actionParams.subActionParams && !actionParams.subActionParams.title?.length) { + errors.title.push(i18n.TITLE_REQUIRED); + } + return validationResult; + }, + actionParamsFields: lazy(() => import('./jira_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx new file mode 100644 index 0000000000000..2cac1819d552d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx @@ -0,0 +1,99 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { DocLinksStart } from 'kibana/public'; +import JiraConnectorFields from './jira_connectors'; +import { JiraActionConnector } from './types'; + +describe('JiraActionConnectorFields renders', () => { + test('alerting Jira connector fields is rendered', () => { + const actionConnector = { + secrets: { + email: 'email', + apiToken: 'token', + }, + id: 'test', + actionTypeId: '.jira', + isPreconfigured: false, + name: 'jira', + config: { + apiUrl: 'https://test/', + projectKey: 'CK', + }, + } as JiraActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + + expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="connector-jira-project-key-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-jira-email-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-jira-apiToken-form-input"]').length > 0 + ).toBeTruthy(); + }); + + test('case specific Jira connector fields is rendered', () => { + const actionConnector = { + secrets: { + email: 'email', + apiToken: 'token', + }, + id: 'test', + actionTypeId: '.jira', + isPreconfigured: false, + name: 'jira', + config: { + apiUrl: 'https://test/', + projectKey: 'CK', + }, + } as JiraActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + consumer={'case'} + /> + ); + expect(wrapper.find('[data-test-subj="case-jira-mappings"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="connector-jira-project-key-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-jira-email-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-jira-apiToken-form-input"]').length > 0 + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx new file mode 100644 index 0000000000000..2ab9843c143b9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.tsx @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useCallback } from 'react'; + +import { + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldPassword, + EuiSpacer, +} from '@elastic/eui'; + +import { isEmpty } from 'lodash'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { CasesConfigurationMapping, FieldMapping, createDefaultMapping } from '../case_mappings'; + +import * as i18n from './translations'; +import { JiraActionConnector } from './types'; +import { connectorConfiguration } from './config'; + +const JiraConnectorFields: React.FC> = ({ + action, + editActionSecrets, + editActionConfig, + errors, + consumer, + readOnly, + docLinks, +}) => { + // TODO: remove incidentConfiguration later, when Case Jira will move their fields to the level of action execution + const { apiUrl, projectKey, incidentConfiguration, isCaseOwned } = action.config; + const mapping = incidentConfiguration ? incidentConfiguration.mapping : []; + + const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; + + const { email, apiToken } = action.secrets; + + const isProjectKeyInvalid: boolean = errors.projectKey.length > 0 && projectKey != null; + const isEmailInvalid: boolean = errors.email.length > 0 && email != null; + const isApiTokenInvalid: boolean = errors.apiToken.length > 0 && apiToken != null; + + // TODO: remove this block later, when Case ServiceNow will move their fields to the level of action execution + if (consumer === 'case') { + if (isEmpty(mapping)) { + editActionConfig('incidentConfiguration', { + mapping: createDefaultMapping(connectorConfiguration.fields as any), + }); + } + if (!isCaseOwned) { + editActionConfig('isCaseOwned', true); + } + } + + const handleOnChangeActionConfig = useCallback( + (key: string, value: string) => editActionConfig(key, value), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const handleOnChangeSecretConfig = useCallback( + (key: string, value: string) => editActionSecrets(key, value), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const handleOnChangeMappingConfig = useCallback( + (newMapping: CasesConfigurationMapping[]) => + editActionConfig('incidentConfiguration', { + ...action.config.incidentConfiguration, + mapping: newMapping, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [action.config] + ); + + return ( + <> + + + + handleOnChangeActionConfig('apiUrl', evt.target.value)} + onBlur={() => { + if (!apiUrl) { + editActionConfig('apiUrl', ''); + } + }} + /> + + + + + + + + handleOnChangeActionConfig('projectKey', evt.target.value)} + onBlur={() => { + if (!projectKey) { + editActionConfig('projectKey', ''); + } + }} + /> + + + + + + + + handleOnChangeSecretConfig('email', evt.target.value)} + onBlur={() => { + if (!email) { + editActionSecrets('email', ''); + } + }} + /> + + + + + + + + handleOnChangeSecretConfig('apiToken', evt.target.value)} + onBlur={() => { + if (!apiToken) { + editActionSecrets('apiToken', ''); + } + }} + /> + + + + {consumer === 'case' && ( // TODO: remove this block later, when Case Jira will move their fields to the level of action execution + <> + + + + + + + + )} + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { JiraConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx new file mode 100644 index 0000000000000..26d358310741c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.test.tsx @@ -0,0 +1,233 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import JiraParamsFields from './jira_params'; +import { DocLinksStart } from 'kibana/public'; + +import { useGetIssueTypes } from './use_get_issue_types'; +import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type'; + +jest.mock('../../../app_context', () => { + const post = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ http: { post } })), + }; +}); + +jest.mock('./use_get_issue_types'); +jest.mock('./use_get_fields_by_issue_type'); + +const useGetIssueTypesMock = useGetIssueTypes as jest.Mock; +const useGetFieldsByIssueTypeMock = useGetFieldsByIssueType as jest.Mock; + +const actionParams = { + subAction: 'pushToService', + subActionParams: { + title: 'sn title', + description: 'some description', + comments: [{ commentId: '1', comment: 'comment for jira' }], + issueType: '10006', + labels: ['kibana'], + priority: 'High', + savedObjectId: '123', + externalId: null, + }, +}; +const connector = { + secrets: {}, + config: {}, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, +}; + +describe('JiraParamsFields renders', () => { + const useGetIssueTypesResponse = { + isLoading: false, + issueTypes: [ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Bug', + }, + ], + }; + + const useGetFieldsByIssueTypeResponse = { + isLoading: false, + fields: { + summary: { allowedValues: [], defaultValue: {} }, + labels: { allowedValues: [], defaultValue: {} }, + description: { allowedValues: [], defaultValue: {} }, + priority: { + allowedValues: [ + { + name: 'Medium', + id: '3', + }, + ], + defaultValue: { name: 'Medium', id: '3' }, + }, + }, + }; + + beforeEach(() => { + useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); + useGetFieldsByIssueTypeMock.mockReturnValue(useGetFieldsByIssueTypeResponse); + }); + + test('all params fields are rendered', () => { + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + expect(wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('value')).toStrictEqual( + '10006' + ); + expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('value')).toStrictEqual( + 'High' + ); + expect(wrapper.find('[data-test-subj="titleInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="descriptionTextArea"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="labelsComboBox"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="commentsTextArea"]').length > 0).toBeTruthy(); + }); + + test('it shows loading when loading issue types', () => { + useGetIssueTypesMock.mockReturnValue({ ...useGetIssueTypesResponse, isLoading: true }); + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect( + wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('isLoading') + ).toBeTruthy(); + }); + + test('it shows loading when loading fields', () => { + useGetFieldsByIssueTypeMock.mockReturnValue({ + ...useGetFieldsByIssueTypeResponse, + isLoading: true, + }); + + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect( + wrapper.find('[data-test-subj="prioritySelect"]').first().prop('isLoading') + ).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="labelsComboBox"]').first().prop('isLoading') + ).toBeTruthy(); + }); + + test('it disabled the fields when loading issue types', () => { + useGetIssueTypesMock.mockReturnValue({ ...useGetIssueTypesResponse, isLoading: true }); + + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect( + wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('disabled') + ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('disabled')).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="labelsComboBox"]').first().prop('isDisabled') + ).toBeTruthy(); + }); + + test('it disabled the fields when loading fields', () => { + useGetFieldsByIssueTypeMock.mockReturnValue({ + ...useGetFieldsByIssueTypeResponse, + isLoading: true, + }); + + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect( + wrapper.find('[data-test-subj="issueTypeSelect"]').first().prop('disabled') + ).toBeTruthy(); + expect(wrapper.find('[data-test-subj="prioritySelect"]').first().prop('disabled')).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="labelsComboBox"]').first().prop('isDisabled') + ).toBeTruthy(); + }); + + test('hide unsupported fields', () => { + useGetIssueTypesMock.mockReturnValue(useGetIssueTypesResponse); + useGetFieldsByIssueTypeMock.mockReturnValue({ + ...useGetFieldsByIssueTypeResponse, + fields: {}, + }); + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect(wrapper.find('[data-test-subj="issueTypeSelect"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="titleInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="commentsTextArea"]').exists()).toBeTruthy(); + + expect(wrapper.find('[data-test-subj="prioritySelect"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="descriptionTextArea"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="labelsComboBox"]').exists()).toBeFalsy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx new file mode 100644 index 0000000000000..bde3d67ffd65f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -0,0 +1,319 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, useEffect, useState, useMemo } from 'react'; +import { map } from 'lodash/fp'; +import { EuiFormRow, EuiComboBox, EuiSelectOption, EuiHorizontalRule } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiSelect } from '@elastic/eui'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { EuiSpacer } from '@elastic/eui'; + +import { useAppDependencies } from '../../../app_context'; +import { ActionParamsProps } from '../../../../types'; +import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; +import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; +import { JiraActionParams } from './types'; +import { useGetIssueTypes } from './use_get_issue_types'; +import { useGetFieldsByIssueType } from './use_get_fields_by_issue_type'; + +const JiraParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + errors, + messageVariables, + actionConnector, +}) => { + const { title, description, comments, issueType, priority, labels, savedObjectId } = + actionParams.subActionParams || {}; + + const [issueTypesSelectOptions, setIssueTypesSelectOptions] = useState([]); + const [firstLoad, setFirstLoad] = useState(false); + const [prioritiesSelectOptions, setPrioritiesSelectOptions] = useState([]); + const { http, toastNotifications } = useAppDependencies(); + + useEffect(() => { + setFirstLoad(true); + }, []); + + const { isLoading: isLoadingIssueTypes, issueTypes } = useGetIssueTypes({ + http, + toastNotifications, + actionConnector, + }); + + const { isLoading: isLoadingFields, fields } = useGetFieldsByIssueType({ + http, + toastNotifications, + actionConnector, + issueType, + }); + + const hasLabels = useMemo(() => Object.prototype.hasOwnProperty.call(fields, 'labels'), [fields]); + const hasDescription = useMemo( + () => Object.prototype.hasOwnProperty.call(fields, 'description'), + [fields] + ); + const hasPriority = useMemo(() => Object.prototype.hasOwnProperty.call(fields, 'priority'), [ + fields, + ]); + + useEffect(() => { + const options = issueTypes.map((type) => ({ + value: type.id ?? '', + text: type.name ?? '', + })); + + setIssueTypesSelectOptions(options); + }, [issueTypes]); + + useEffect(() => { + if (issueType != null && fields != null) { + const priorities = fields.priority?.allowedValues ?? []; + const options = map( + (p) => ({ + value: p.name, + text: p.name, + }), + priorities + ); + setPrioritiesSelectOptions(options); + } + }, [fields, issueType]); + + const labelOptions = useMemo(() => (labels ? labels.map((label: string) => ({ label })) : []), [ + labels, + ]); + + const editSubActionProperty = (key: string, value: any) => { + const newProps = { ...actionParams.subActionParams, [key]: value }; + editAction('subActionParams', newProps, index); + }; + + // Reset parameters when changing connector + useEffect(() => { + if (!firstLoad) { + return; + } + + setIssueTypesSelectOptions([]); + editAction('subActionParams', { title, comments, description: '', savedObjectId }, index); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionConnector]); + + // Reset fields when changing connector or issue type + useEffect(() => { + if (!firstLoad) { + return; + } + + setPrioritiesSelectOptions([]); + editAction( + 'subActionParams', + { title, issueType, comments, description: '', savedObjectId }, + index + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [issueType, savedObjectId]); + + useEffect(() => { + if (!actionParams.subAction) { + editAction('subAction', 'pushToService', index); + } + if (!savedObjectId && messageVariables?.find((variable) => variable.name === 'alertId')) { + editSubActionProperty('savedObjectId', '{{alertId}}'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + actionConnector, + actionParams.subAction, + index, + savedObjectId, + issueTypesSelectOptions, + issueType, + ]); + + // Set default issue type + useEffect(() => { + if (!issueType && issueTypesSelectOptions.length > 0) { + editSubActionProperty('issueType', issueTypesSelectOptions[0].value as string); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [issueTypes, issueTypesSelectOptions]); + + // Set default priority + useEffect(() => { + if (!priority && prioritiesSelectOptions.length > 0) { + editSubActionProperty('priority', prioritiesSelectOptions[0].value as string); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionConnector, issueType, prioritiesSelectOptions]); + + return ( + + <> + + { + editSubActionProperty('issueType', e.target.value); + }} + /> + + + <> + {hasPriority && ( + <> + + + + { + editSubActionProperty('priority', e.target.value); + }} + /> + + + + + + )} + 0 && title !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.titleFieldLabel', + { + defaultMessage: 'Summary', + } + )} + > + + + + {hasLabels && ( + <> + + + + { + const newOptions = [...labelOptions, { label: searchValue }]; + editSubActionProperty( + 'labels', + newOptions.map((newOption) => newOption.label) + ); + }} + onChange={(selectedOptions: Array<{ label: string }>) => { + editSubActionProperty( + 'labels', + selectedOptions.map((selectedOption) => selectedOption.label) + ); + }} + onBlur={() => { + if (!labels) { + editSubActionProperty('labels', []); + } + }} + isClearable={true} + data-test-subj="labelsComboBox" + /> + + + + + + )} + {hasDescription && ( + + )} + { + editSubActionProperty(key, [{ commentId: '1', comment: value }]); + }} + messageVariables={messageVariables} + paramsProperty={'comments'} + inputTargetValue={comments && comments.length > 0 ? comments[0].comment : ''} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.commentsTextAreaFieldLabel', + { + defaultMessage: 'Additional comments (optional)', + } + )} + errors={errors.comments as string[]} + /> + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { JiraParamsFields as default }; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/jira/logo.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/logo.svg similarity index 100% rename from x-pack/plugins/security_solution/public/common/lib/connectors/jira/logo.svg rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/logo.svg diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts new file mode 100644 index 0000000000000..bfcb72d1cb977 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/translations.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const JIRA_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.selectMessageText', + { + defaultMessage: 'Push or update data to a new issue in Jira', + } +); + +export const JIRA_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.actionTypeTitle', + { + defaultMessage: 'Jira', + } +); + +export const API_URL_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.apiUrlTextFieldLabel', + { + defaultMessage: 'URL', + } +); + +export const API_URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiUrlTextField', + { + defaultMessage: 'URL is required.', + } +); + +export const API_URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.invalidApiUrlTextField', + { + defaultMessage: 'URL is invalid.', + } +); + +export const JIRA_PROJECT_KEY_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.projectKey', + { + defaultMessage: 'Project key', + } +); + +export const JIRA_PROJECT_KEY_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredProjectKeyTextField', + { + defaultMessage: 'Project key is required', + } +); + +export const JIRA_EMAIL_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.emailTextFieldLabel', + { + defaultMessage: 'Email or Username', + } +); + +export const JIRA_EMAIL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredEmailTextField', + { + defaultMessage: 'Email or Username is required', + } +); + +export const JIRA_API_TOKEN_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.apiTokenTextFieldLabel', + { + defaultMessage: 'API token or Password', + } +); + +export const JIRA_API_TOKEN_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredApiTokenTextField', + { + defaultMessage: 'API token or Password is required', + } +); + +export const MAPPING_FIELD_SUMMARY = i18n.translate( + 'xpack.triggersActionsUI.case.configureCases.mappingFieldSummary', + { + defaultMessage: 'Summary', + } +); + +export const DESCRIPTION_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredDescriptionTextField', + { + defaultMessage: 'Description is required.', + } +); + +export const TITLE_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.requiredTitleTextField', + { + defaultMessage: 'Title is required.', + } +); + +export const MAPPING_FIELD_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldDescription', + { + defaultMessage: 'Description', + } +); + +export const MAPPING_FIELD_COMMENTS = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.mappingFieldComments', + { + defaultMessage: 'Comments', + } +); + +export const ISSUE_TYPES_API_ERROR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetIssueTypesMessage', + { + defaultMessage: 'Unable to get issue types', + } +); + +export const FIELDS_API_ERROR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.jira.unableToGetFieldsMessage', + { + defaultMessage: 'Unable to get fields', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts new file mode 100644 index 0000000000000..ff11199f35fea --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/types.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CasesConfigurationMapping } from '../case_mappings'; + +export interface JiraActionConnector { + config: JiraConfig; + secrets: JiraSecrets; +} + +export interface JiraActionParams { + subAction: string; + subActionParams: { + savedObjectId: string; + title: string; + description: string; + comments: Array<{ commentId: string; comment: string }>; + externalId: string | null; + issueType: string; + priority: string; + labels: string[]; + }; +} + +interface IncidentConfiguration { + mapping: CasesConfigurationMapping[]; +} + +interface JiraConfig { + apiUrl: string; + projectKey: string; + incidentConfiguration?: IncidentConfiguration; + isCaseOwned?: boolean; +} + +interface JiraSecrets { + email: string; + apiToken: string; +} + +// to remove diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx new file mode 100644 index 0000000000000..08715822e5277 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_fields_by_issue_type.tsx @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState, useEffect, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../../types'; +import { getFieldsByIssueType } from './api'; +import * as i18n from './translations'; + +interface Fields { + [key: string]: { + allowedValues: Array<{ name: string; id: string }> | []; + defaultValue: { name: string; id: string } | {}; + }; +} + +interface Props { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + issueType: string; + actionConnector?: ActionConnector; +} + +export interface UseGetFieldsByIssueType { + fields: Fields; + isLoading: boolean; +} + +export const useGetFieldsByIssueType = ({ + http, + toastNotifications, + actionConnector, + issueType, +}: Props): UseGetFieldsByIssueType => { + const [isLoading, setIsLoading] = useState(true); + const [fields, setFields] = useState({}); + const abortCtrl = useRef(new AbortController()); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + if (!actionConnector || !issueType) { + setIsLoading(false); + return; + } + + abortCtrl.current = new AbortController(); + setIsLoading(true); + try { + const res = await getFieldsByIssueType({ + http, + signal: abortCtrl.current.signal, + connectorId: actionConnector.id, + id: issueType, + }); + + if (!didCancel) { + setIsLoading(false); + setFields(res.data ?? {}); + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.FIELDS_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } + } + } catch (error) { + if (!didCancel) { + toastNotifications.addDanger({ + title: i18n.FIELDS_API_ERROR, + text: error.message, + }); + } + } + }; + + abortCtrl.current.abort(); + fetchData(); + + return () => { + didCancel = true; + setIsLoading(false); + abortCtrl.current.abort(); + }; + }, [http, actionConnector, issueType, toastNotifications]); + + return { + isLoading, + fields, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx new file mode 100644 index 0000000000000..9ebaf5882d9b9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/use_get_issue_types.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState, useEffect, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../../types'; +import { getIssueTypes } from './api'; +import * as i18n from './translations'; + +type IssueTypes = Array<{ id: string; name: string }>; + +interface Props { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + actionConnector?: ActionConnector; +} + +export interface UseGetIssueTypes { + issueTypes: IssueTypes; + isLoading: boolean; +} + +export const useGetIssueTypes = ({ + http, + actionConnector, + toastNotifications, +}: Props): UseGetIssueTypes => { + const [isLoading, setIsLoading] = useState(true); + const [issueTypes, setIssueTypes] = useState([]); + const abortCtrl = useRef(new AbortController()); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + if (!actionConnector) { + setIsLoading(false); + return; + } + + abortCtrl.current = new AbortController(); + setIsLoading(true); + + try { + const res = await getIssueTypes({ + http, + signal: abortCtrl.current.signal, + connectorId: actionConnector.id, + }); + + if (!didCancel) { + setIsLoading(false); + setIssueTypes(res.data ?? []); + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.ISSUE_TYPES_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } + } + } catch (error) { + if (!didCancel) { + toastNotifications.addDanger({ + title: i18n.ISSUE_TYPES_API_ERROR, + text: error.message, + }); + } + } + }; + + abortCtrl.current.abort(); + fetchData(); + + return () => { + didCancel = true; + setIsLoading(false); + abortCtrl.current.abort(); + }; + }, [http, actionConnector, toastNotifications]); + + return { + issueTypes, + isLoading, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx index 9e37047ccda50..6a11dc8d0d6a5 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/pagerduty/pagerduty_params.test.tsx @@ -22,6 +22,7 @@ describe('PagerDutyParamsFields renders', () => { group: 'group', class: 'test class', }; + const wrapper = mountWithIntl( > { + return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + body: JSON.stringify({ + params: { subAction: 'incidentTypes', subActionParams: {} }, + }), + signal, + }); +} + +export async function getSeverity({ + http, + signal, + connectorId, +}: { + http: HttpSetup; + signal: AbortSignal; + connectorId: string; +}): Promise> { + return await http.post(`${BASE_ACTION_API_PATH}/action/${connectorId}/_execute`, { + body: JSON.stringify({ + params: { subAction: 'severity', subActionParams: {} }, + }), + signal, + }); +} diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/config.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/config.ts similarity index 88% rename from x-pack/plugins/security_solution/public/common/lib/connectors/resilient/config.ts rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/config.ts index 7d4edbf624877..a2054585c19b8 100644 --- a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/config.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/config.ts @@ -4,14 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ConnectorConfiguration } from './types'; - import * as i18n from './translations'; import logo from './logo.svg'; -export const connector: ConnectorConfiguration = { +export const connectorConfiguration = { id: '.resilient', - name: i18n.RESILIENT_TITLE, + name: i18n.TITLE, logo, enabled: true, enabledInConfig: true, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/index.ts new file mode 100644 index 0000000000000..0905bd29493e7 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/index.ts @@ -0,0 +1,7 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export { getActionType as getResilientActionType } from './resilient'; diff --git a/x-pack/plugins/security_solution/public/common/lib/connectors/resilient/logo.svg b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/logo.svg similarity index 100% rename from x-pack/plugins/security_solution/public/common/lib/connectors/resilient/logo.svg rename to x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/logo.svg diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx new file mode 100644 index 0000000000000..b73eb72f137c1 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.test.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { ResilientActionConnector } from './types'; + +const ACTION_TYPE_ID = '.resilient'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + }); +}); + +describe('resilient connector validation', () => { + test('connector validation succeeds when connector config is valid', () => { + const actionConnector = { + secrets: { + apiKeyId: 'email', + apiKeySecret: 'token', + }, + id: 'test', + actionTypeId: '.resilient', + isPreconfigured: false, + name: 'resilient', + config: { + apiUrl: 'https://test/', + orgId: '201', + }, + } as ResilientActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + apiUrl: [], + apiKeyId: [], + apiKeySecret: [], + orgId: [], + }, + }); + }); + + test('connector validation fails when connector config is not valid', () => { + const actionConnector = ({ + secrets: { + apiKeyId: 'user', + }, + id: '.jira', + actionTypeId: '.jira', + name: 'jira', + config: {}, + } as unknown) as ResilientActionConnector; + + expect(actionTypeModel.validateConnector(actionConnector)).toEqual({ + errors: { + apiUrl: ['URL is required.'], + apiKeyId: [], + apiKeySecret: ['API key secret is required'], + orgId: ['Organization ID is required'], + }, + }); + }); +}); + +describe('resilient action params validation', () => { + test('action params validation succeeds when action params is valid', () => { + const actionParams = { + subActionParams: { title: 'some title {{test}}' }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { title: [] }, + }); + }); + + test('params validation fails when body is not valid', () => { + const actionParams = { + subActionParams: { title: '' }, + }; + + expect(actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + title: ['Title is required.'], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx new file mode 100644 index 0000000000000..cda6935f3b73d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient.tsx @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { lazy } from 'react'; +import { ValidationResult, ActionTypeModel } from '../../../../types'; +import { connectorConfiguration } from './config'; +import logo from './logo.svg'; +import { ResilientActionConnector, ResilientActionParams } from './types'; +import * as i18n from './translations'; +import { isValidUrl } from '../../../lib/value_validators'; + +const validateConnector = (action: ResilientActionConnector): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + apiUrl: new Array(), + orgId: new Array(), + apiKeyId: new Array(), + apiKeySecret: new Array(), + }; + validationResult.errors = errors; + + if (!action.config.apiUrl) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_REQUIRED]; + } + + if (action.config.apiUrl && !isValidUrl(action.config.apiUrl, 'https:')) { + errors.apiUrl = [...errors.apiUrl, i18n.API_URL_INVALID]; + } + + if (!action.config.orgId) { + errors.orgId = [...errors.orgId, i18n.ORG_ID_REQUIRED]; + } + + if (!action.secrets.apiKeyId) { + errors.apiKeyId = [...errors.apiKeyId, i18n.API_KEY_ID_REQUIRED]; + } + + if (!action.secrets.apiKeySecret) { + errors.apiKeySecret = [...errors.apiKeySecret, i18n.API_KEY_SECRET_REQUIRED]; + } + + return validationResult; +}; + +export function getActionType(): ActionTypeModel { + return { + id: connectorConfiguration.id, + iconClass: logo, + selectMessage: i18n.DESC, + actionTypeTitle: connectorConfiguration.name, + validateConnector, + actionConnectorFields: lazy(() => import('./resilient_connectors')), + validateParams: (actionParams: ResilientActionParams): ValidationResult => { + const validationResult = { errors: {} }; + const errors = { + title: new Array(), + }; + validationResult.errors = errors; + if (actionParams.subActionParams && !actionParams.subActionParams.title?.length) { + errors.title.push(i18n.TITLE_REQUIRED); + } + return validationResult; + }, + actionParamsFields: lazy(() => import('./resilient_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx new file mode 100644 index 0000000000000..7e242f1f501d8 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx @@ -0,0 +1,100 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import { DocLinksStart } from 'kibana/public'; +import ResilientConnectorFields from './resilient_connectors'; +import { ResilientActionConnector } from './types'; + +describe('ResilientActionConnectorFields renders', () => { + test('alerting Resilient connector fields is rendered', () => { + const actionConnector = { + secrets: { + apiKeyId: 'key', + apiKeySecret: 'secret', + }, + id: 'test', + actionTypeId: '.resilient', + isPreconfigured: false, + name: 'resilient', + config: { + apiUrl: 'https://test/', + orgId: '201', + }, + } as ResilientActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + /> + ); + + expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="connector-resilient-orgId-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0 + ).toBeTruthy(); + }); + + test('case specific Resilient connector fields is rendered', () => { + const actionConnector = { + secrets: { + apiKeyId: 'email', + apiKeySecret: 'token', + }, + id: 'test', + actionTypeId: '.resilient', + isPreconfigured: false, + name: 'resilient', + config: { + apiUrl: 'https://test/', + orgId: '201', + }, + } as ResilientActionConnector; + const deps = { + docLinks: { ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart, + }; + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + docLinks={deps!.docLinks} + readOnly={false} + consumer={'case'} + /> + ); + + expect(wrapper.find('[data-test-subj="case-resilient-mappings"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="apiUrlFromInput"]').length > 0).toBeTruthy(); + expect( + wrapper.find('[data-test-subj="connector-resilient-orgId-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0 + ).toBeTruthy(); + + expect( + wrapper.find('[data-test-subj="connector-resilient-apiKeySecret-form-input"]').length > 0 + ).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx new file mode 100644 index 0000000000000..7965e216f1d6c --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.tsx @@ -0,0 +1,209 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React, { useCallback } from 'react'; + +import { + EuiFieldText, + EuiFlexGroup, + EuiFlexItem, + EuiFormRow, + EuiFieldPassword, + EuiSpacer, +} from '@elastic/eui'; + +import { isEmpty } from 'lodash'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import * as i18n from './translations'; +import { ResilientActionConnector } from './types'; +import { connectorConfiguration } from './config'; +import { FieldMapping, CasesConfigurationMapping, createDefaultMapping } from '../case_mappings'; + +const ResilientConnectorFields: React.FC> = ({ + action, + editActionSecrets, + editActionConfig, + errors, + consumer, + readOnly, + docLinks, +}) => { + // TODO: remove incidentConfiguration later, when Case Resilient will move their fields to the level of action execution + const { apiUrl, orgId, incidentConfiguration, isCaseOwned } = action.config; + const mapping = incidentConfiguration ? incidentConfiguration.mapping : []; + + const isApiUrlInvalid: boolean = errors.apiUrl.length > 0 && apiUrl != null; + + const { apiKeyId, apiKeySecret } = action.secrets; + + const isOrgIdInvalid: boolean = errors.orgId.length > 0 && orgId != null; + const isApiKeyInvalid: boolean = errors.apiKeyId.length > 0 && apiKeyId != null; + const isApiKeySecretInvalid: boolean = errors.apiKeySecret.length > 0 && apiKeySecret != null; + + // TODO: remove this block later, when Case ServiceNow will move their fields to the level of action execution + if (consumer === 'case') { + if (isEmpty(mapping)) { + editActionConfig('incidentConfiguration', { + mapping: createDefaultMapping(connectorConfiguration.fields as any), + }); + } + + if (!isCaseOwned) { + editActionConfig('isCaseOwned', true); + } + } + + const handleOnChangeActionConfig = useCallback( + (key: string, value: string) => editActionConfig(key, value), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const handleOnChangeSecretConfig = useCallback( + (key: string, value: string) => editActionSecrets(key, value), + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + + const handleOnChangeMappingConfig = useCallback( + (newMapping: CasesConfigurationMapping[]) => + editActionConfig('incidentConfiguration', { + ...action.config.incidentConfiguration, + mapping: newMapping, + }), + // eslint-disable-next-line react-hooks/exhaustive-deps + [action.config] + ); + + return ( + <> + + + + handleOnChangeActionConfig('apiUrl', evt.target.value)} + onBlur={() => { + if (!apiUrl) { + editActionConfig('apiUrl', ''); + } + }} + /> + + + + + + + + handleOnChangeActionConfig('orgId', evt.target.value)} + onBlur={() => { + if (!orgId) { + editActionConfig('orgId', ''); + } + }} + /> + + + + + + + + handleOnChangeSecretConfig('apiKeyId', evt.target.value)} + onBlur={() => { + if (!apiKeyId) { + editActionSecrets('apiKeyId', ''); + } + }} + /> + + + + + + + + handleOnChangeSecretConfig('apiKeySecret', evt.target.value)} + onBlur={() => { + if (!apiKeySecret) { + editActionSecrets('apiKeySecret', ''); + } + }} + /> + + + + {consumer === 'case' && ( // TODO: remove this block later, when Case Resilient will move their fields to the level of action execution + <> + + + + + + + + )} + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { ResilientConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx new file mode 100644 index 0000000000000..17020805757f9 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.test.tsx @@ -0,0 +1,189 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import React from 'react'; +import { mountWithIntl } from 'test_utils/enzyme_helpers'; +import ResilientParamsFields from './resilient_params'; +import { DocLinksStart } from 'kibana/public'; + +import { useGetIncidentTypes } from './use_get_incident_types'; +import { useGetSeverity } from './use_get_severity'; + +jest.mock('../../../app_context', () => { + const post = jest.fn(); + return { + useAppDependencies: jest.fn(() => ({ http: { post } })), + }; +}); + +jest.mock('./use_get_incident_types'); +jest.mock('./use_get_severity'); + +const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; +const useGetSeverityMock = useGetSeverity as jest.Mock; + +const actionParams = { + subAction: 'pushToService', + subActionParams: { + title: 'title', + description: 'some description', + comments: [{ commentId: '1', comment: 'comment for resilient' }], + incidentTypes: [1001], + severityCode: 6, + savedObjectId: '123', + externalId: null, + }, +}; +const connector = { + secrets: {}, + config: {}, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, +}; + +describe('ResilientParamsFields renders', () => { + const useGetIncidentTypesResponse = { + isLoading: false, + incidentTypes: [ + { + id: 19, + name: 'Malware', + }, + { + id: 21, + name: 'Denial of Service', + }, + ], + }; + + const useGetSeverityResponse = { + isLoading: false, + severity: [ + { + id: 4, + name: 'Low', + }, + { + id: 5, + name: 'Medium', + }, + { + id: 6, + name: 'High', + }, + ], + }; + + beforeEach(() => { + useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); + useGetSeverityMock.mockReturnValue(useGetSeverityResponse); + }); + + test('all params fields are rendered', () => { + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + expect(wrapper.find('[data-test-subj="incidentTypeComboBox"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('value')).toStrictEqual( + 6 + ); + expect(wrapper.find('[data-test-subj="titleInput"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="descriptionTextArea"]').length > 0).toBeTruthy(); + expect(wrapper.find('[data-test-subj="commentsTextArea"]').length > 0).toBeTruthy(); + }); + + test('it shows loading when loading incident types', () => { + useGetIncidentTypesMock.mockReturnValue({ ...useGetIncidentTypesResponse, isLoading: true }); + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect( + wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('isLoading') + ).toBeTruthy(); + }); + + test('it shows loading when loading severity', () => { + useGetSeverityMock.mockReturnValue({ + ...useGetSeverityResponse, + isLoading: true, + }); + + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect( + wrapper.find('[data-test-subj="severitySelect"]').first().prop('isLoading') + ).toBeTruthy(); + }); + + test('it disabled the fields when loading issue types', () => { + useGetIncidentTypesMock.mockReturnValue({ ...useGetIncidentTypesResponse, isLoading: true }); + + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect( + wrapper.find('[data-test-subj="incidentTypeComboBox"]').first().prop('isDisabled') + ).toBeTruthy(); + }); + + test('it disabled the fields when loading severity', () => { + useGetSeverityMock.mockReturnValue({ + ...useGetSeverityResponse, + isLoading: true, + }); + + const wrapper = mountWithIntl( + {}} + index={0} + messageVariables={[]} + docLinks={{ ELASTIC_WEBSITE_URL: '', DOC_LINK_VERSION: '' } as DocLinksStart} + actionConnector={connector} + /> + ); + + expect(wrapper.find('[data-test-subj="severitySelect"]').first().prop('disabled')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx new file mode 100644 index 0000000000000..4b157c6999985 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx @@ -0,0 +1,256 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import React, { Fragment, useEffect, useState } from 'react'; +import { + EuiFormRow, + EuiComboBox, + EuiSelect, + EuiSpacer, + EuiTitle, + EuiComboBoxOptionOption, + EuiSelectOption, +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; + +import { ActionParamsProps } from '../../../../types'; +import { useAppDependencies } from '../../../app_context'; +import { ResilientActionParams } from './types'; +import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; +import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; + +import { useGetIncidentTypes } from './use_get_incident_types'; +import { useGetSeverity } from './use_get_severity'; + +const ResilientParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + errors, + messageVariables, + actionConnector, +}) => { + const [firstLoad, setFirstLoad] = useState(false); + const { http, toastNotifications } = useAppDependencies(); + const { title, description, comments, incidentTypes, severityCode, savedObjectId } = + actionParams.subActionParams || {}; + + const [incidentTypesComboBoxOptions, setIncidentTypesComboBoxOptions] = useState< + Array> + >([]); + + const [selectedIncidentTypesComboBoxOptions, setSelectedIncidentTypesComboBoxOptions] = useState< + Array> + >([]); + + const [severitySelectOptions, setSeveritySelectOptions] = useState([]); + + useEffect(() => { + setFirstLoad(true); + }, []); + + const { + isLoading: isLoadingIncidentTypes, + incidentTypes: allIncidentTypes, + } = useGetIncidentTypes({ + http, + toastNotifications, + actionConnector, + }); + + const { isLoading: isLoadingSeverity, severity } = useGetSeverity({ + http, + toastNotifications, + actionConnector, + }); + + const editSubActionProperty = (key: string, value: {}) => { + const newProps = { ...actionParams.subActionParams, [key]: value }; + editAction('subActionParams', newProps, index); + }; + + useEffect(() => { + const options = severity.map((s) => ({ + value: s.id.toString(), + text: s.name, + })); + + setSeveritySelectOptions(options); + }, [actionConnector, severity]); + + // Reset parameters when changing connector + useEffect(() => { + if (!firstLoad) { + return; + } + + setIncidentTypesComboBoxOptions([]); + setSelectedIncidentTypesComboBoxOptions([]); + setSeveritySelectOptions([]); + editAction('subActionParams', { title, comments, description: '', savedObjectId }, index); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionConnector]); + + useEffect(() => { + if (!actionParams.subAction) { + editAction('subAction', 'pushToService', index); + } + if (!savedObjectId && messageVariables?.find((variable) => variable.name === 'alertId')) { + editSubActionProperty('savedObjectId', '{{alertId}}'); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionConnector, savedObjectId]); + + useEffect(() => { + setIncidentTypesComboBoxOptions( + allIncidentTypes + ? allIncidentTypes.map((type: { id: number; name: string }) => ({ + label: type.name, + value: type.id.toString(), + })) + : [] + ); + + const allIncidentTypesAsObject = allIncidentTypes.reduce( + (acc, type) => ({ ...acc, [type.id.toString()]: type.name }), + {} as Record + ); + + setSelectedIncidentTypesComboBoxOptions( + incidentTypes + ? incidentTypes + .map((type) => ({ + label: allIncidentTypesAsObject[type.toString()], + value: type.toString(), + })) + .filter((type) => type.label != null) + : [] + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionConnector, allIncidentTypes]); + + return ( + + +

Incident

+
+ + + ) => { + setSelectedIncidentTypesComboBoxOptions( + selectedOptions.map((selectedOption) => ({ + label: selectedOption.label, + value: selectedOption.value, + })) + ); + + editSubActionProperty( + 'incidentTypes', + selectedOptions.map((selectedOption) => selectedOption.value ?? selectedOption.label) + ); + }} + onBlur={() => { + if (!incidentTypes) { + editSubActionProperty('incidentTypes', []); + } + }} + isClearable={true} + /> + + + + { + editSubActionProperty('severityCode', e.target.value); + }} + /> + + + 0 && title !== undefined} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.titleFieldLabel', + { + defaultMessage: 'Name', + } + )} + > + + + + { + editSubActionProperty(key, [{ commentId: 'alert-comment', comment: value }]); + }} + messageVariables={messageVariables} + paramsProperty={'comments'} + inputTargetValue={comments && comments.length > 0 ? comments[0].comment : ''} + label={i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.commentsTextAreaFieldLabel', + { + defaultMessage: 'Additional comments (optional)', + } + )} + errors={errors.comments as string[]} + /> +
+ ); +}; + +// eslint-disable-next-line import/no-default-export +export { ResilientParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts new file mode 100644 index 0000000000000..71ad05abfdecf --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/translations.ts @@ -0,0 +1,133 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { i18n } from '@kbn/i18n'; + +export const DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.selectMessageText', + { + defaultMessage: 'Push or update data to a new incident in Resilient.', + } +); + +export const TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.actionTypeTitle', + { + defaultMessage: 'Resilient', + } +); + +export const API_URL_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiUrlTextFieldLabel', + { + defaultMessage: 'URL', + } +); + +export const API_URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiUrlTextField', + { + defaultMessage: 'URL is required.', + } +); + +export const API_URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.invalidApiUrlTextField', + { + defaultMessage: 'URL is invalid.', + } +); + +export const ORG_ID_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.orgId', + { + defaultMessage: 'Organization ID', + } +); + +export const ORG_ID_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredOrgIdTextField', + { + defaultMessage: 'Organization ID is required', + } +); + +export const API_KEY_ID_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeyId', + { + defaultMessage: 'API key ID', + } +); + +export const API_KEY_ID_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeyIdTextField', + { + defaultMessage: 'API key ID is required', + } +); + +export const API_KEY_SECRET_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.apiKeySecret', + { + defaultMessage: 'API key secret', + } +); + +export const API_KEY_SECRET_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.requiredApiKeySecretTextField', + { + defaultMessage: 'API key secret is required', + } +); + +export const MAPPING_FIELD_NAME = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldShortDescription', + { + defaultMessage: 'Name', + } +); + +export const MAPPING_FIELD_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldDescription', + { + defaultMessage: 'Description', + } +); + +export const MAPPING_FIELD_COMMENTS = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.mappingFieldComments', + { + defaultMessage: 'Comments', + } +); + +export const DESCRIPTION_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredDescriptionTextField', + { + defaultMessage: 'Description is required.', + } +); + +export const TITLE_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.common.requiredTitleTextField', + { + defaultMessage: 'Title is required.', + } +); + +export const INCIDENT_TYPES_API_ERROR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetIncidentTypesMessage', + { + defaultMessage: 'Unable to get incident types', + } +); + +export const SEVERITY_API_ERROR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.resilient.unableToGetSeverityMessage', + { + defaultMessage: 'Unable to get severity', + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/types.ts new file mode 100644 index 0000000000000..37516f5bac372 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/types.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { CasesConfigurationMapping } from '../case_mappings'; + +export interface ResilientActionConnector { + config: ResilientConfig; + secrets: ResilientSecrets; +} + +export interface ResilientActionParams { + subAction: string; + subActionParams: { + savedObjectId: string; + title: string; + description: string; + externalId: string | null; + incidentTypes: number[]; + severityCode: number; + comments: Array<{ commentId: string; comment: string }>; + }; +} + +interface IncidentConfiguration { + mapping: CasesConfigurationMapping[]; +} + +interface ResilientConfig { + apiUrl: string; + orgId: string; + incidentConfiguration?: IncidentConfiguration; + isCaseOwned?: boolean; +} + +interface ResilientSecrets { + apiKeyId: string; + apiKeySecret: string; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/use_get_incident_types.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/use_get_incident_types.tsx new file mode 100644 index 0000000000000..219c6ac77d08d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/use_get_incident_types.tsx @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState, useEffect, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../../types'; +import { getIncidentTypes } from './api'; +import * as i18n from './translations'; + +type IncidentTypes = Array<{ id: number; name: string }>; + +interface Props { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + actionConnector?: ActionConnector; +} + +export interface UseGetIncidentTypes { + incidentTypes: IncidentTypes; + isLoading: boolean; +} + +export const useGetIncidentTypes = ({ + http, + toastNotifications, + actionConnector, +}: Props): UseGetIncidentTypes => { + const [isLoading, setIsLoading] = useState(true); + const [incidentTypes, setIncidentTypes] = useState([]); + const abortCtrl = useRef(new AbortController()); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + if (!actionConnector) { + setIsLoading(false); + return; + } + + abortCtrl.current = new AbortController(); + setIsLoading(true); + + try { + const res = await getIncidentTypes({ + http, + signal: abortCtrl.current.signal, + connectorId: actionConnector.id, + }); + + if (!didCancel) { + setIsLoading(false); + setIncidentTypes(res.data ?? []); + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.INCIDENT_TYPES_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } + } + } catch (error) { + if (!didCancel) { + toastNotifications.addDanger({ + title: i18n.INCIDENT_TYPES_API_ERROR, + text: error.message, + }); + } + } + }; + + abortCtrl.current.abort(); + fetchData(); + + return () => { + didCancel = true; + setIsLoading(false); + abortCtrl.current.abort(); + }; + }, [http, actionConnector, toastNotifications]); + + return { + incidentTypes, + isLoading, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/use_get_severity.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/use_get_severity.tsx new file mode 100644 index 0000000000000..83689254f000f --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/use_get_severity.tsx @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { useState, useEffect, useRef } from 'react'; +import { HttpSetup, ToastsApi } from 'kibana/public'; +import { ActionConnector } from '../../../../types'; +import { getSeverity } from './api'; +import * as i18n from './translations'; + +type Severity = Array<{ id: number; name: string }>; + +interface Props { + http: HttpSetup; + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + actionConnector?: ActionConnector; +} + +export interface UseGetSeverity { + severity: Severity; + isLoading: boolean; +} + +export const useGetSeverity = ({ + http, + toastNotifications, + actionConnector, +}: Props): UseGetSeverity => { + const [isLoading, setIsLoading] = useState(true); + const [severity, setSeverity] = useState([]); + const abortCtrl = useRef(new AbortController()); + + useEffect(() => { + let didCancel = false; + const fetchData = async () => { + if (!actionConnector) { + setIsLoading(false); + return; + } + + abortCtrl.current = new AbortController(); + setIsLoading(true); + + try { + const res = await getSeverity({ + http, + signal: abortCtrl.current.signal, + connectorId: actionConnector.id, + }); + + if (!didCancel) { + setIsLoading(false); + setSeverity(res.data ?? []); + + if (res.status && res.status === 'error') { + toastNotifications.addDanger({ + title: i18n.SEVERITY_API_ERROR, + text: `${res.serviceMessage ?? res.message}`, + }); + } + } + } catch (error) { + if (!didCancel) { + toastNotifications.addDanger({ + title: i18n.SEVERITY_API_ERROR, + text: error.message, + }); + } + } + }; + + abortCtrl.current.abort(); + fetchData(); + + return () => { + didCancel = true; + setIsLoading(false); + abortCtrl.current.abort(); + }; + }, [http, actionConnector, toastNotifications]); + + return { + severity, + isLoading, + }; +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx index f99a276305d75..a8f1ed8d55447 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.tsx @@ -18,10 +18,11 @@ import { import { isEmpty } from 'lodash'; import { FormattedMessage } from '@kbn/i18n/react'; import { ActionConnectorFieldsProps } from '../../../../types'; +import { CasesConfigurationMapping, FieldMapping, createDefaultMapping } from '../case_mappings'; + import * as i18n from './translations'; -import { ServiceNowActionConnector, CasesConfigurationMapping } from './types'; +import { ServiceNowActionConnector } from './types'; import { connectorConfiguration } from './config'; -import { FieldMapping } from './case_mappings/field_mapping'; const ServiceNowConnectorFields: React.FC @@ -184,15 +185,5 @@ const ServiceNowConnectorFields: React.FC): CasesConfigurationMapping[] => - Object.keys(fields).map( - (key) => - ({ - source: fields[key].defaultSourceField, - target: key, - actionType: fields[key].defaultActionType, - } as CasesConfigurationMapping) - ); - // eslint-disable-next-line import/no-default-export export { ServiceNowConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx index 1fc856b1e1ab2..f4d831d7234e7 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_params.test.tsx @@ -23,6 +23,7 @@ describe('ServiceNowParamsFields renders', () => { externalId: null, }, }; + const wrapper = mountWithIntl( -

Incident

+

+ {i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.serviceNow.title', + { + defaultMessage: 'Incident', + } + )} +

{ const actionParams = { message: 'test message', }; + const wrapper = mountWithIntl( { const actionParams = { body: 'test message', }; + const wrapper = mountWithIntl( ) : null} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx index 3803fcebbb92d..8ac80c4ad2880 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.test.tsx @@ -11,7 +11,7 @@ import { EuiFormLabel } from '@elastic/eui'; import { coreMock } from '../../../../../../../src/core/public/mocks'; import AlertAdd from './alert_add'; import { actionTypeRegistryMock } from '../../action_type_registry.mock'; -import { ValidationResult } from '../../../types'; +import { Alert, ValidationResult } from '../../../types'; import { AlertsContextProvider, useAlertsContext } from '../../context/alerts_context'; import { alertTypeRegistryMock } from '../../alert_type_registry.mock'; import { chartPluginMock } from '../../../../../../../src/plugins/charts/public/mocks'; @@ -46,7 +46,7 @@ describe('alert_add', () => { let deps: any; let wrapper: ReactWrapper; - async function setup() { + async function setup(initialValues?: Partial) { const mocks = coreMock.createSetup(); const { loadAlertTypes } = jest.requireMock('../../lib/alert_api'); const alertTypes = [ @@ -155,6 +155,7 @@ describe('alert_add', () => { consumer={ALERTS_FEATURE_ID} addFlyoutVisible={true} setAddFlyoutVisibility={() => {}} + initialValues={initialValues} /> @@ -180,5 +181,31 @@ describe('alert_add', () => { wrapper.find('[data-test-subj="my-alert-type-SelectOption"]').first().simulate('click'); expect(wrapper.contains('Metadata: some value. Fields: test.')).toBeTruthy(); + + expect(wrapper.find('input#alertName').props().value).toBe(''); + + expect(wrapper.find('[data-test-subj="tagsComboBox"]').first().text()).toBe(''); + + expect(wrapper.find('.euiSelect').first().props().value).toBe('m'); + }); + + it('renders alert add flyout with initial values', async () => { + await setup({ + name: 'Simple status alert', + tags: ['uptime', 'logs'], + schedule: { + interval: '1h', + }, + }); + + await new Promise((resolve) => { + setTimeout(resolve, 1000); + }); + + expect(wrapper.find('input#alertName').props().value).toBe('Simple status alert'); + + expect(wrapper.find('[data-test-subj="tagsComboBox"]').first().text()).toBe('uptimelogs'); + + expect(wrapper.find('.euiSelect').first().props().value).toBe('h'); }); }); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx index 20cbd42e34b67..97dcfec5ed3c6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alert_form/alert_add.tsx @@ -34,6 +34,7 @@ interface AlertAddProps { setAddFlyoutVisibility: React.Dispatch>; alertTypeId?: string; canChangeTrigger?: boolean; + initialValues?: Partial; } export const AlertAdd = ({ @@ -42,6 +43,7 @@ export const AlertAdd = ({ setAddFlyoutVisibility, canChangeTrigger, alertTypeId, + initialValues, }: AlertAddProps) => { const initialAlert = ({ params: {}, @@ -52,6 +54,7 @@ export const AlertAdd = ({ }, actions: [], tags: [], + ...(initialValues ? initialValues : {}), } as unknown) as Alert; const [{ alert }, dispatch] = useReducer(alertReducer, { alert: initialAlert }); diff --git a/x-pack/plugins/triggers_actions_ui/public/common/index.ts b/x-pack/plugins/triggers_actions_ui/public/common/index.ts index 9dd3fd787f860..32b323334654e 100644 --- a/x-pack/plugins/triggers_actions_ui/public/common/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/common/index.ts @@ -7,3 +7,5 @@ export * from './expression_items'; export { connectorConfiguration as ServiceNowConnectorConfiguration } from '../application/components/builtin_action_types/servicenow/config'; +export { connectorConfiguration as JiraConnectorConfiguration } from '../application/components/builtin_action_types/jira/config'; +export { connectorConfiguration as ResilientConnectorConfiguration } from '../application/components/builtin_action_types/resilient/config'; diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 762f41ba3691c..109d473c56e66 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -54,6 +54,7 @@ export interface ActionParamsProps { messageVariables?: ActionVariable[]; defaultMessage?: string; docLinks: DocLinksStart; + actionConnector?: ActionConnector; } export interface Pagination { diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx index 8154ec45b8ae1..6f9eccde8bdb0 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/components/connected_flyout_manage_drilldowns/connected_flyout_manage_drilldowns.tsx @@ -64,6 +64,7 @@ export function createFlyoutManageDrilldowns({ storage, toastService, docsLink, + triggerPickerDocsLink, getTrigger, }: { actionFactories: ActionFactory[]; @@ -71,6 +72,7 @@ export function createFlyoutManageDrilldowns({ storage: IStorageWrapper; toastService: ToastsStart; docsLink?: string; + triggerPickerDocsLink?: string; }) { const allActionFactoriesById = allActionFactories.reduce((acc, next) => { acc[next.id] = next; @@ -161,6 +163,7 @@ export function createFlyoutManageDrilldowns({ return ( ; + /** + * General overview of drilldowns + */ docsLink?: string; + /** + * Link that explains different triggers + */ + triggerPickerDocsLink?: string; + getTrigger: (triggerId: TriggerId) => Trigger; /** @@ -145,6 +153,7 @@ export function FlyoutDrilldownWizard) { @@ -217,7 +226,7 @@ export function FlyoutDrilldownWizard {mode === 'edit' && ( <> diff --git a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx index dabf09e4b6e9f..bd0191443d785 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx +++ b/x-pack/plugins/ui_actions_enhanced/public/drilldowns/url_drilldown/components/url_drilldown_collect_config/url_drilldown_collect_config.tsx @@ -41,6 +41,7 @@ export interface UrlDrilldownCollectConfig { onConfig: (newConfig: UrlDrilldownConfig) => void; scope: UrlDrilldownScope; syntaxHelpDocsLink?: string; + variablesHelpDocsLink?: string; } export const UrlDrilldownCollectConfig: React.FC = ({ @@ -48,6 +49,7 @@ export const UrlDrilldownCollectConfig: React.FC = ({ onConfig, scope, syntaxHelpDocsLink, + variablesHelpDocsLink, }) => { const textAreaRef = useRef(null); const urlTemplate = config.url.template ?? ''; @@ -95,7 +97,7 @@ export const UrlDrilldownCollectConfig: React.FC = ({ labelAppend={ { if (textAreaRef.current) { updateUrlTemplate( diff --git a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts index 015531aab9743..b38bc44abe2b0 100644 --- a/x-pack/plugins/ui_actions_enhanced/public/plugin.ts +++ b/x-pack/plugins/ui_actions_enhanced/public/plugin.ts @@ -132,6 +132,7 @@ export class AdvancedUiActionsPublicPlugin storage: new Storage(window?.localStorage), toastService: core.notifications.toasts, docsLink: core.docLinks.links.dashboard.drilldowns, + triggerPickerDocsLink: core.docLinks.links.dashboard.drilldownsTriggerPicker, }), }; } diff --git a/x-pack/plugins/upgrade_assistant/kibana.json b/x-pack/plugins/upgrade_assistant/kibana.json index 273036a653aeb..c4c6f23611f2b 100644 --- a/x-pack/plugins/upgrade_assistant/kibana.json +++ b/x-pack/plugins/upgrade_assistant/kibana.json @@ -4,6 +4,6 @@ "server": true, "ui": true, "configPath": ["xpack", "upgrade_assistant"], - "requiredPlugins": ["management", "licensing"], + "requiredPlugins": ["management", "licensing", "features"], "optionalPlugins": ["cloud", "usageCollection"] } diff --git a/x-pack/plugins/upgrade_assistant/server/plugin.ts b/x-pack/plugins/upgrade_assistant/server/plugin.ts index 0cdf1ca05feac..9ef0f250da8ef 100644 --- a/x-pack/plugins/upgrade_assistant/server/plugin.ts +++ b/x-pack/plugins/upgrade_assistant/server/plugin.ts @@ -16,6 +16,7 @@ import { } from '../../../../src/core/server'; import { CloudSetup } from '../../cloud/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { CredentialStore, credentialStoreFactory } from './lib/reindexing/credential_store'; @@ -32,6 +33,7 @@ import { RouteDependencies } from './types'; interface PluginsSetup { usageCollection: UsageCollectionSetup; licensing: LicensingPluginSetup; + features: FeaturesPluginSetup; cloud?: CloudSetup; } @@ -60,13 +62,26 @@ export class UpgradeAssistantServerPlugin implements Plugin { setup( { http, getStartServices, capabilities, savedObjects }: CoreSetup, - { usageCollection, cloud, licensing }: PluginsSetup + { usageCollection, cloud, features, licensing }: PluginsSetup ) { this.licensing = licensing; savedObjects.registerType(reindexOperationSavedObjectType); savedObjects.registerType(telemetrySavedObjectType); + features.registerElasticsearchFeature({ + id: 'upgrade_assistant', + management: { + stack: ['upgrade_assistant'], + }, + privileges: [ + { + requiredClusterPrivileges: ['manage'], + ui: [], + }, + ], + }); + const router = http.createRouter(); const dependencies: RouteDependencies = { diff --git a/x-pack/plugins/uptime/server/kibana.index.ts b/x-pack/plugins/uptime/server/kibana.index.ts index 76359a3b60a6a..5c3211eff3b4e 100644 --- a/x-pack/plugins/uptime/server/kibana.index.ts +++ b/x-pack/plugins/uptime/server/kibana.index.ts @@ -27,7 +27,7 @@ export const initServerWithKibana = (server: UptimeCoreSetup, plugins: UptimeCor const { features } = plugins; const libs = compose(server); - features.registerFeature({ + features.registerKibanaFeature({ id: PLUGIN.ID, name: PLUGIN.NAME, order: 1000, diff --git a/x-pack/plugins/watcher/kibana.json b/x-pack/plugins/watcher/kibana.json index ba6a9bfa5e194..695686715cb6a 100644 --- a/x-pack/plugins/watcher/kibana.json +++ b/x-pack/plugins/watcher/kibana.json @@ -7,7 +7,8 @@ "licensing", "management", "charts", - "data" + "data", + "features" ], "server": true, "ui": true, diff --git a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx index 2ff0f53d07e91..935f0209e73c2 100644 --- a/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx +++ b/x-pack/plugins/watcher/public/application/sections/watch_edit/components/threshold_watch_edit/watch_visualization.tsx @@ -126,7 +126,7 @@ export const WatchVisualization = () => { isLoading, data: watchVisualizationData, error, - sendRequest: reload, + resendRequest: reload, } = useGetWatchVisualizationData(watchWithoutActions, visualizeOptions); useEffect( diff --git a/x-pack/plugins/watcher/server/plugin.ts b/x-pack/plugins/watcher/server/plugin.ts index 70c4f980580e8..9ff46283a72a6 100644 --- a/x-pack/plugins/watcher/server/plugin.ts +++ b/x-pack/plugins/watcher/server/plugin.ts @@ -18,7 +18,7 @@ import { Plugin, PluginInitializerContext, } from 'kibana/server'; -import { PLUGIN } from '../common/constants'; +import { PLUGIN, INDEX_NAMES } from '../common/constants'; import { Dependencies, LicenseStatus, RouteDependencies } from './types'; import { registerSettingsRoutes } from './routes/api/settings'; @@ -52,13 +52,39 @@ export class WatcherServerPlugin implements Plugin { this.log = ctx.logger.get(); } - async setup({ http, getStartServices }: CoreSetup, { licensing }: Dependencies) { + async setup({ http, getStartServices }: CoreSetup, { licensing, features }: Dependencies) { const router = http.createRouter(); const routeDependencies: RouteDependencies = { router, getLicenseStatus: () => this.licenseStatus, }; + features.registerElasticsearchFeature({ + id: 'watcher', + management: { + insightsAndAlerting: ['watcher'], + }, + catalogue: ['watcher'], + privileges: [ + { + requiredClusterPrivileges: ['manage_watcher'], + requiredIndexPrivileges: { + [INDEX_NAMES.WATCHES]: ['read'], + [INDEX_NAMES.WATCHER_HISTORY]: ['read'], + }, + ui: [], + }, + { + requiredClusterPrivileges: ['monitor_watcher'], + requiredIndexPrivileges: { + [INDEX_NAMES.WATCHES]: ['read'], + [INDEX_NAMES.WATCHER_HISTORY]: ['read'], + }, + ui: [], + }, + ], + }); + http.registerRouteHandlerContext('watcher', async (ctx, request) => { this.watcherESClient = this.watcherESClient ?? (await getCustomEsClient(getStartServices)); return { diff --git a/x-pack/plugins/watcher/server/types.ts b/x-pack/plugins/watcher/server/types.ts index dd941054114a8..167dcb3ab64c3 100644 --- a/x-pack/plugins/watcher/server/types.ts +++ b/x-pack/plugins/watcher/server/types.ts @@ -5,12 +5,14 @@ */ import { IRouter } from 'kibana/server'; +import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { XPackMainPlugin } from '../../../legacy/plugins/xpack_main/server/xpack_main'; export interface Dependencies { licensing: LicensingPluginSetup; + features: FeaturesPluginSetup; } export interface ServerShim { diff --git a/x-pack/run_functional_tests.sh b/x-pack/run_functional_tests.sh deleted file mode 100755 index e94f283ea0394..0000000000000 --- a/x-pack/run_functional_tests.sh +++ /dev/null @@ -1,3 +0,0 @@ -export TEST_KIBANA_URL="http://elastic:mlqa_admin@localhost:5601" -export TEST_ES_URL="http://elastic:mlqa_admin@localhost:9200" -node ../scripts/functional_test_runner --include-tag walterra diff --git a/x-pack/test/accessibility/apps/spaces.ts b/x-pack/test/accessibility/apps/spaces.ts new file mode 100644 index 0000000000000..e11de1376e400 --- /dev/null +++ b/x-pack/test/accessibility/apps/spaces.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +// a11y tests for spaces, space selection and spacce creation and feature controls + +import { FtrProviderContext } from '../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const PageObjects = getPageObjects(['common', 'spaceSelector', 'home', 'header', 'security']); + const a11y = getService('a11y'); + const browser = getService('browser'); + const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); + const retry = getService('retry'); + const toasts = getService('toasts'); + + describe('Kibana spaces page meets a11y validations', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + it('a11y test for manage spaces menu from top nav on Kibana home', async () => { + await PageObjects.spaceSelector.openSpacesNav(); + await retry.waitFor( + 'Manage spaces option visible', + async () => await testSubjects.exists('manageSpaces') + ); + await a11y.testAppSnapshot(); + }); + + it('a11y test for manage spaces page', async () => { + await PageObjects.spaceSelector.clickManageSpaces(); + await PageObjects.header.waitUntilLoadingHasFinished(); + await toasts.dismissAllToasts(); + await retry.waitFor( + 'Manage spaces page visible', + async () => await testSubjects.exists('createSpace') + ); + await a11y.testAppSnapshot(); + }); + + it('a11y test for click on create space page', async () => { + await PageObjects.spaceSelector.clickCreateSpace(); + await a11y.testAppSnapshot(); + }); + + it('a11y test for for customize space card', async () => { + await PageObjects.spaceSelector.clickEnterSpaceName(); + await PageObjects.spaceSelector.addSpaceName('space_a'); + await PageObjects.spaceSelector.clickSpaceAcustomAvatar(); + await a11y.testAppSnapshot(); + await browser.pressKeys(browser.keys.ESCAPE); + }); + + // EUI issue - https://github.com/elastic/eui/issues/3999 + it.skip('a11y test for color picker', async () => { + await PageObjects.spaceSelector.clickColorPicker(); + await a11y.testAppSnapshot(); + await browser.pressKeys(browser.keys.ESCAPE); + }); + + it('a11y test for customize and reset space URL identifier', async () => { + await PageObjects.spaceSelector.clickOnCustomizeURL(); + await a11y.testAppSnapshot(); + await PageObjects.spaceSelector.clickOnCustomizeURL(); + await a11y.testAppSnapshot(); + }); + + it('a11y test for describe space text space', async () => { + await PageObjects.spaceSelector.clickOnDescriptionOfSpace(); + await a11y.testAppSnapshot(); + }); + + it('a11y test for click on "show" button to open customize feature display', async () => { + await retry.waitFor( + 'show button is visible', + async () => await testSubjects.exists('show-hide-section-link') + ); + await PageObjects.spaceSelector.clickShowFeatures(); + await a11y.testAppSnapshot(); + }); + + it('a11y test for change all option for feature visibility popover', async () => { + await PageObjects.spaceSelector.clickFeaturesVisibilityButton(); + await a11y.testAppSnapshot(); + }); + + it('a11y test for hide all feature visibility popover option', async () => { + await PageObjects.spaceSelector.clickHideAllFeatures(); + await a11y.testAppSnapshot(); + }); + + it('a11y test for toggle individual feature - using enterprise feature visibility', async () => { + await PageObjects.spaceSelector.clickFeaturesVisibilityButton(); + await PageObjects.spaceSelector.clickShowAllFeatures(); + await PageObjects.spaceSelector.toggleFeatureVisibility('enterpriseSearch'); + await a11y.testAppSnapshot(); + }); + + it('a11y test for space listing page', async () => { + await PageObjects.spaceSelector.clickSaveSpaceCreation(); + await a11y.testAppSnapshot(); + }); + + it('a11y test for updating a space', async () => { + await PageObjects.spaceSelector.clickSpaceEditButton('space_a'); + await a11y.testAppSnapshot(); + await PageObjects.spaceSelector.clickCancelSpaceCreation(); + }); + + // creating space b and making it the current space so space selector page gets displayed when space b gets deleted + it('a11y test for delete space button', async () => { + await PageObjects.spaceSelector.clickCreateSpace(); + await PageObjects.spaceSelector.clickEnterSpaceName(); + await PageObjects.spaceSelector.addSpaceName('space_b'); + await PageObjects.spaceSelector.clickSaveSpaceCreation(); + await PageObjects.common.navigateToApp('home'); + await PageObjects.spaceSelector.openSpacesNav(); + await PageObjects.spaceSelector.clickSpaceAvatar('space_b'); + await PageObjects.header.waitUntilLoadingHasFinished(); + await PageObjects.spaceSelector.openSpacesNav(); + await PageObjects.spaceSelector.clickManageSpaces(); + await PageObjects.spaceSelector.clickOnDeleteSpaceButton('space_b'); + await a11y.testAppSnapshot(); + // a11y test for no space name in confirm dialogue box + await PageObjects.spaceSelector.confirmDeletingSpace(); + await a11y.testAppSnapshot(); + }); + + // test starts with deleting space b so we can get the space selection page instead of logging out in the test + it('a11y test for space selection page', async () => { + await PageObjects.spaceSelector.setSpaceNameTobeDeleted('space_b'); + await PageObjects.spaceSelector.confirmDeletingSpace(); + await a11y.testAppSnapshot(); + await PageObjects.spaceSelector.clickSpaceCard('default'); + }); + }); +} diff --git a/x-pack/test/accessibility/config.ts b/x-pack/test/accessibility/config.ts index 0a95805754314..bae7b688fd28c 100644 --- a/x-pack/test/accessibility/config.ts +++ b/x-pack/test/accessibility/config.ts @@ -20,7 +20,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { require.resolve('./apps/grok_debugger'), require.resolve('./apps/search_profiler'), require.resolve('./apps/uptime'), - require.resolve('./apps/painless_lab'), + require.resolve('./apps/spaces'), ], pageObjects, services, diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts new file mode 100644 index 0000000000000..025fd558ee1ca --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/jira.ts @@ -0,0 +1,97 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + getExternalServiceSimulatorPath, + ExternalServiceSimulator, +} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; + +// node ../scripts/functional_test_runner.js --grep "Actions.servicenddd" --config=test/alerting_api_integration/security_and_spaces/config.ts + +const mapping = [ + { + source: 'title', + target: 'summary', + actionType: 'nothing', + }, + { + source: 'description', + target: 'description', + actionType: 'nothing', + }, + { + source: 'comments', + target: 'comments', + actionType: 'nothing', + }, +]; + +// eslint-disable-next-line import/no-default-export +export default function jiraTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const mockJira = { + config: { + apiUrl: 'www.jiraisinkibanaactions.com', + incidentConfiguration: { mapping: [...mapping] }, + isCaseOwned: true, + }, + secrets: { + email: 'elastic', + apiToken: 'changeme', + }, + params: { + savedObjectId: '123', + title: 'a title', + description: 'a description', + labels: ['kibana'], + issueType: '10006', + priority: 'High', + externalId: null, + comments: [ + { + commentId: '456', + comment: 'first comment', + }, + ], + }, + }; + describe('jira', () => { + let jiraSimulatorURL: string = ''; + + // need to wait for kibanaServer to settle ... + before(() => { + jiraSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.JIRA) + ); + }); + + it('should return 403 when creating a jira action', async () => { + await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A jira action', + actionTypeId: '.jira', + config: { + apiUrl: jiraSimulatorURL, + projectKey: 'CK', + incidentConfiguration: { ...mockJira.config.incidentConfiguration }, + isCaseOwned: true, + }, + secrets: mockJira.secrets, + }) + .expect(403, { + statusCode: 403, + error: 'Forbidden', + message: + 'Action type .jira is disabled because your basic license does not support it. Please upgrade your license.', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/resilient.ts new file mode 100644 index 0000000000000..576ed4bbc5dfe --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/resilient.ts @@ -0,0 +1,93 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +import { + getExternalServiceSimulatorPath, + ExternalServiceSimulator, +} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; + +const mapping = [ + { + source: 'title', + target: 'description', + actionType: 'nothing', + }, + { + source: 'description', + target: 'short_description', + actionType: 'nothing', + }, + { + source: 'comments', + target: 'comments', + actionType: 'nothing', + }, +]; + +// eslint-disable-next-line import/no-default-export +export default function resilientTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const mockResilient = { + config: { + apiUrl: 'www.resilientisinkibanaactions.com', + orgId: '201', + incidentConfiguration: { mapping: [...mapping] }, + isCaseOwned: true, + }, + secrets: { + apiKeyId: 'elastic', + apiKeySecret: 'changeme', + }, + params: { + savedObjectId: '123', + title: 'a title', + description: 'a description', + incidentTypes: [1001], + severityCode: 'High', + comments: [ + { + commentId: '456', + comment: 'first comment', + }, + ], + }, + }; + describe('resilient', () => { + let resilientSimulatorURL: string = ''; + + // need to wait for kibanaServer to settle ... + before(() => { + resilientSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.RESILIENT) + ); + }); + + it('should return 403 when creating a resilient action', async () => { + await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A resilient action', + actionTypeId: '.resilient', + config: { + apiUrl: resilientSimulatorURL, + incidentConfiguration: { ...mockResilient.config.incidentConfiguration }, + isCaseOwned: true, + }, + secrets: mockResilient.secrets, + }) + .expect(403, { + statusCode: 403, + error: 'Forbidden', + message: + 'Action type .resilient is disabled because your basic license does not support it. Please upgrade your license.', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts index 1788a12afebf2..8f31e7f96b562 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts @@ -11,6 +11,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { describe('Actions', () => { loadTestFile(require.resolve('./builtin_action_types/email')); loadTestFile(require.resolve('./builtin_action_types/es_index')); + loadTestFile(require.resolve('./builtin_action_types/jira')); loadTestFile(require.resolve('./builtin_action_types/pagerduty')); loadTestFile(require.resolve('./builtin_action_types/server_log')); loadTestFile(require.resolve('./builtin_action_types/servicenow')); diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts index 4b65b7a8f2636..6041251dc28a4 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/jira_simulation.ts @@ -105,6 +105,57 @@ export function initPlugin(router: IRouter, path: string) { }); } ); + + router.get( + { + path: `${path}/rest/capabilities`, + options: { + authRequired: false, + }, + validate: {}, + }, + async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + return jsonResponse(res, 200, { + capabilities: {}, + }); + } + ); + + router.get( + { + path: `${path}/rest/api/2/issue/createmeta`, + options: { + authRequired: false, + }, + validate: {}, + }, + async function ( + context: RequestHandlerContext, + req: KibanaRequest, + res: KibanaResponseFactory + ): Promise> { + return jsonResponse(res, 200, { + projects: [ + { + issuetypes: [ + { + id: '10006', + name: 'Task', + }, + { + id: '10007', + name: 'Sub-task', + }, + ], + }, + ], + }); + } + ); } function jsonResponse( diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index 0f7acf5ead1a1..68ff3dad9ae86 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -38,6 +38,7 @@ export function getAllExternalServiceSimulatorPaths(): string[] { ); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.SERVICENOW}/api/now/v2/table/incident`); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/issue`); + allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.JIRA}/rest/api/2/createmeta`); allPaths.push(`/api/_${NAME}/${ExternalServiceSimulator.RESILIENT}/rest/orgs/201/incidents`); return allPaths; } @@ -71,7 +72,7 @@ export class FixturePlugin implements Plugin { public setup(core: CoreSetup, { features, actions, alerts }: FixtureSetupDeps) { - features.registerFeature({ + features.registerKibanaFeature({ id: 'alertsFixture', name: 'Alerts', app: ['alerts', 'kibana'], diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts index e297733fb47eb..e1ef1255c6e13 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/alerts_restricted/server/plugin.ts @@ -23,7 +23,7 @@ export interface FixtureStartDeps { export class FixturePlugin implements Plugin { public setup(core: CoreSetup, { features, alerts }: FixtureSetupDeps) { - features.registerFeature({ + features.registerKibanaFeature({ id: 'alertsRestrictedFixture', name: 'AlertRestricted', app: ['alerts', 'kibana'], diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts index 3ffd58b945ddb..84fad699525a9 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/jira.ts @@ -43,7 +43,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: 'www.jiraisinkibanaactions.com', projectKey: 'CK', - casesConfiguration: { mapping }, + incidentConfiguration: { mapping }, }, secrets: { apiToken: 'elastic', @@ -94,6 +94,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { ...mockJira.config, apiUrl: jiraSimulatorURL, + incidentConfiguration: mockJira.config.incidentConfiguration, + isCaseOwned: true, }, secrets: mockJira.secrets, }) @@ -107,7 +109,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - casesConfiguration: mockJira.config.casesConfiguration, + incidentConfiguration: mockJira.config.incidentConfiguration, + isCaseOwned: true, }, }); @@ -123,7 +126,8 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - casesConfiguration: mockJira.config.casesConfiguration, + incidentConfiguration: mockJira.config.incidentConfiguration, + isCaseOwned: true, }, }); }); @@ -178,7 +182,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: 'http://jira.mynonexistent.com', projectKey: mockJira.config.projectKey, - casesConfiguration: mockJira.config.casesConfiguration, + incidentConfiguration: mockJira.config.incidentConfiguration, }, secrets: mockJira.secrets, }) @@ -203,7 +207,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - casesConfiguration: mockJira.config.casesConfiguration, + incidentConfiguration: mockJira.config.incidentConfiguration, }, }) .expect(400) @@ -217,30 +221,6 @@ export default function jiraTest({ getService }: FtrProviderContext) { }); }); - it('should respond with a 400 Bad Request when creating a jira action without casesConfiguration', async () => { - await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'A jira action', - actionTypeId: '.jira', - config: { - apiUrl: jiraSimulatorURL, - projectKey: mockJira.config.projectKey, - }, - secrets: mockJira.secrets, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [casesConfiguration.mapping]: expected value of type [array] but got [undefined]', - }); - }); - }); - it('should respond with a 400 Bad Request when creating a jira action with empty mapping', async () => { await supertest .post('/api/actions/action') @@ -251,7 +231,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - casesConfiguration: { mapping: [] }, + incidentConfiguration: { mapping: [] }, }, secrets: mockJira.secrets, }) @@ -261,7 +241,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: [casesConfiguration.mapping]: expected non-empty but got empty', + 'error validating action type config: [incidentConfiguration.mapping]: expected non-empty but got empty', }); }); }); @@ -276,7 +256,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - casesConfiguration: { + incidentConfiguration: { mapping: [ { source: 'title', @@ -307,7 +287,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { config: { apiUrl: jiraSimulatorURL, projectKey: mockJira.config.projectKey, - casesConfiguration: mockJira.config.casesConfiguration, + incidentConfiguration: mockJira.config.incidentConfiguration, }, secrets: mockJira.secrets, }); @@ -353,7 +333,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subAction]: expected value to equal [pushToService]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subAction]: expected value to equal [pushToService]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', }); }); }); @@ -371,7 +351,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', }); }); }); @@ -389,7 +369,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', }); }); }); @@ -412,31 +392,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.title]: expected value of type [string] but got [undefined]', - }); - }); - }); - - it('should handle failing with a simulated success without createdAt', async () => { - await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockJira.params, - subActionParams: { - savedObjectId: 'success', - title: 'success', - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.createdAt]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.title]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', }); }); }); @@ -464,7 +420,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', }); }); }); @@ -492,35 +448,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', - }); - }); - }); - - it('should handle failing with a simulated success without comment.createdAt', async () => { - await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockJira.params, - subActionParams: { - ...mockJira.params.subActionParams, - savedObjectId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{ commentId: 'success', comment: 'success' }], - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.createdAt]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [3.subAction]: expected value to equal [issueTypes]\n- [4.subAction]: expected value to equal [fieldsByIssueType]', }); }); }); @@ -537,6 +465,7 @@ export default function jiraTest({ getService }: FtrProviderContext) { subActionParams: { ...mockJira.params.subActionParams, comments: [], + issueType: '10006', }, }, }) diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts index 9cbc2373ef943..d1d19da423e65 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/resilient.ts @@ -41,9 +41,10 @@ export default function resilientTest({ getService }: FtrProviderContext) { const mockResilient = { config: { - apiUrl: 'www.jiraisinkibanaactions.com', + apiUrl: 'www.resilientisinkibanaactions.com', orgId: '201', - casesConfiguration: { mapping }, + incidentConfiguration: { mapping }, + isCaseOwned: true, }, secrets: { apiKeyId: 'key', @@ -55,6 +56,8 @@ export default function resilientTest({ getService }: FtrProviderContext) { savedObjectId: '123', title: 'a title', description: 'a description', + incidentTypes: [1001], + severityCode: 6, createdAt: '2020-03-13T08:34:53.450Z', createdBy: { fullName: 'Elastic User', username: 'elastic' }, updatedAt: null, @@ -108,7 +111,8 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - casesConfiguration: mockResilient.config.casesConfiguration, + incidentConfiguration: mockResilient.config.incidentConfiguration, + isCaseOwned: true, }, }); @@ -124,7 +128,8 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - casesConfiguration: mockResilient.config.casesConfiguration, + incidentConfiguration: mockResilient.config.incidentConfiguration, + isCaseOwned: true, }, }); }); @@ -179,7 +184,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: 'http://resilient.mynonexistent.com', orgId: mockResilient.config.orgId, - casesConfiguration: mockResilient.config.casesConfiguration, + incidentConfiguration: mockResilient.config.incidentConfiguration, }, secrets: mockResilient.secrets, }) @@ -204,7 +209,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - casesConfiguration: mockResilient.config.casesConfiguration, + incidentConfiguration: mockResilient.config.incidentConfiguration, }, }) .expect(400) @@ -218,30 +223,6 @@ export default function resilientTest({ getService }: FtrProviderContext) { }); }); - it('should respond with a 400 Bad Request when creating a ibm resilient action without casesConfiguration', async () => { - await supertest - .post('/api/actions/action') - .set('kbn-xsrf', 'foo') - .send({ - name: 'An IBM Resilient', - actionTypeId: '.resilient', - config: { - apiUrl: resilientSimulatorURL, - orgId: mockResilient.config.orgId, - }, - secrets: mockResilient.secrets, - }) - .expect(400) - .then((resp: any) => { - expect(resp.body).to.eql({ - statusCode: 400, - error: 'Bad Request', - message: - 'error validating action type config: [casesConfiguration.mapping]: expected value of type [array] but got [undefined]', - }); - }); - }); - it('should respond with a 400 Bad Request when creating a ibm resilient action with empty mapping', async () => { await supertest .post('/api/actions/action') @@ -252,7 +233,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - casesConfiguration: { mapping: [] }, + incidentConfiguration: { mapping: [] }, }, secrets: mockResilient.secrets, }) @@ -262,7 +243,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { statusCode: 400, error: 'Bad Request', message: - 'error validating action type config: [casesConfiguration.mapping]: expected non-empty but got empty', + 'error validating action type config: [incidentConfiguration.mapping]: expected non-empty but got empty', }); }); }); @@ -277,7 +258,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - casesConfiguration: { + incidentConfiguration: { mapping: [ { source: 'title', @@ -307,7 +288,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { config: { apiUrl: resilientSimulatorURL, orgId: mockResilient.config.orgId, - casesConfiguration: mockResilient.config.casesConfiguration, + incidentConfiguration: mockResilient.config.incidentConfiguration, }, secrets: mockResilient.secrets, }); @@ -353,7 +334,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subAction]: expected value to equal [pushToService]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subAction]: expected value to equal [pushToService]\n- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]', }); }); }); @@ -371,7 +352,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]', }); }); }); @@ -389,7 +370,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.savedObjectId]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]', }); }); }); @@ -412,31 +393,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.title]: expected value of type [string] but got [undefined]', - }); - }); - }); - - it('should handle failing with a simulated success without createdAt', async () => { - await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockResilient.params, - subActionParams: { - savedObjectId: 'success', - title: 'success', - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.createdAt]: expected value of type [string] but got [undefined]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.title]: expected value of type [string] but got [undefined]\n- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]', }); }); }); @@ -464,7 +421,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]', }); }); }); @@ -492,35 +449,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { status: 'error', retry: false, message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', - }); - }); - }); - - it('should handle failing with a simulated success without comment.createdAt', async () => { - await supertest - .post(`/api/actions/action/${simulatedActionId}/_execute`) - .set('kbn-xsrf', 'foo') - .send({ - params: { - ...mockResilient.params, - subActionParams: { - ...mockResilient.params.subActionParams, - savedObjectId: 'success', - title: 'success', - createdAt: 'success', - createdBy: { username: 'elastic' }, - comments: [{ commentId: 'success', comment: 'success' }], - }, - }, - }) - .then((resp: any) => { - expect(resp.body).to.eql({ - actionId: simulatedActionId, - status: 'error', - retry: false, - message: - 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.createdAt]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]', + 'error validating action params: types that failed validation:\n- [0.subAction]: expected value to equal [getIncident]\n- [1.subAction]: expected value to equal [handshake]\n- [2.subActionParams.comments]: types that failed validation:\n - [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n - [subActionParams.comments.1]: expected value to equal [null]\n- [3.subAction]: expected value to equal [incidentTypes]\n- [4.subAction]: expected value to equal [severity]', }); }); }); @@ -536,7 +465,7 @@ export default function resilientTest({ getService }: FtrProviderContext) { ...mockResilient.params, subActionParams: { ...mockResilient.params.subActionParams, - comments: [], + comments: null, }, }, }) diff --git a/x-pack/test/api_integration/apis/features/features/features.ts b/x-pack/test/api_integration/apis/features/features/features.ts index 9c44bfeb810fa..37809a3b7aeb7 100644 --- a/x-pack/test/api_integration/apis/features/features/features.ts +++ b/x-pack/test/api_integration/apis/features/features/features.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { Feature } from '../../../../../plugins/features/server'; +import { KibanaFeature } from '../../../../../plugins/features/server'; import { FtrProviderContext } from '../../../ftr_provider_context'; export default function ({ getService }: FtrProviderContext) { @@ -90,7 +90,7 @@ export default function ({ getService }: FtrProviderContext) { expect(body).to.be.an(Array); - const featureIds = body.map((b: Feature) => b.id); + const featureIds = body.map((b: KibanaFeature) => b.id); expect(featureIds.sort()).to.eql( [ 'discover', diff --git a/x-pack/test/api_integration/apis/lens/field_stats.ts b/x-pack/test/api_integration/apis/lens/field_stats.ts index 87c9d97be9b60..ccaea03691f01 100644 --- a/x-pack/test/api_integration/apis/lens/field_stats.ts +++ b/x-pack/test/api_integration/apis/lens/field_stats.ts @@ -279,6 +279,139 @@ export default ({ getService }: FtrProviderContext) => { }); }); + it('should return top values for ip fields', async () => { + const { body } = await supertest + .post('/api/lens/index_stats/logstash-2015.09.22/field') + .set(COMMON_HEADERS) + .send({ + dslQuery: { match_all: {} }, + fromDate: TEST_START_TIME, + toDate: TEST_END_TIME, + timeFieldName: '@timestamp', + field: { + name: 'ip', + type: 'ip', + }, + }) + .expect(200); + + expect(body).to.eql({ + totalDocuments: 4634, + sampledDocuments: 4634, + sampledValues: 4633, + topValues: { + buckets: [ + { + count: 13, + key: '177.194.175.66', + }, + { + count: 12, + key: '18.55.141.62', + }, + { + count: 12, + key: '53.55.251.105', + }, + { + count: 11, + key: '21.111.249.239', + }, + { + count: 11, + key: '97.63.84.25', + }, + { + count: 11, + key: '100.99.207.174', + }, + { + count: 11, + key: '112.34.138.226', + }, + { + count: 11, + key: '194.68.89.92', + }, + { + count: 11, + key: '235.186.79.201', + }, + { + count: 10, + key: '57.79.108.136', + }, + ], + }, + }); + }); + + it('should return histograms for scripted date fields', async () => { + const { body } = await supertest + .post('/api/lens/index_stats/logstash-2015.09.22/field') + .set(COMMON_HEADERS) + .send({ + dslQuery: { match_all: {} }, + fromDate: TEST_START_TIME, + toDate: TEST_END_TIME, + timeFieldName: '@timestamp', + field: { + name: 'scripted date', + type: 'date', + scripted: true, + script: '1234', + lang: 'painless', + }, + }) + .expect(200); + + expect(body).to.eql({ + histogram: { + buckets: [ + { + count: 4634, + key: 0, + }, + ], + }, + totalDocuments: 4634, + }); + }); + + it('should return top values for scripted string fields', async () => { + const { body } = await supertest + .post('/api/lens/index_stats/logstash-2015.09.22/field') + .set(COMMON_HEADERS) + .send({ + dslQuery: { match_all: {} }, + fromDate: TEST_START_TIME, + toDate: TEST_END_TIME, + timeFieldName: '@timestamp', + field: { + name: 'scripted string', + type: 'string', + scripted: true, + script: 'return "hello"', + lang: 'painless', + }, + }) + .expect(200); + + expect(body).to.eql({ + totalDocuments: 4634, + sampledDocuments: 4634, + sampledValues: 4634, + topValues: { + buckets: [ + { + count: 4634, + key: 'hello', + }, + ], + }, + }); + }); + it('should apply filters and queries', async () => { const { body } = await supertest .post('/api/lens/index_stats/logstash-2015.09.22/field') diff --git a/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts b/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts index bb0934b73a4c7..7339c142fb028 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/snapshot.ts @@ -67,7 +67,6 @@ export default function ({ getService }: FtrProviderContext) { 'value', '242fddb9d376bbf0e38025d81764847ee5ec0308adfa095918fd3266f9d06c6a' ); - expect(first(firstNode.path)).to.have.property('label', 'docker-autodiscovery_nginx_1'); expect(firstNode).to.have.property('metrics'); expect(firstNode.metrics).to.eql([ { @@ -136,7 +135,7 @@ export default function ({ getService }: FtrProviderContext) { expect(snapshot).to.have.property('nodes'); if (snapshot) { const { nodes } = snapshot; - expect(nodes.length).to.equal(136); + expect(nodes.length).to.equal(135); const firstNode = first(nodes) as any; expect(firstNode).to.have.property('path'); expect(firstNode.path.length).to.equal(1); @@ -295,7 +294,7 @@ export default function ({ getService }: FtrProviderContext) { expect(firstNode).to.have.property('metrics'); expect(firstNode.metrics).to.eql([ { - name: 'custom', + name: 'custom_0', value: 0.0016, max: 0.0018333333333333333, avg: 0.0013666666666666669, diff --git a/x-pack/test/api_integration/apis/ml/data_frame_analytics/delete.ts b/x-pack/test/api_integration/apis/ml/data_frame_analytics/delete.ts index c6043b7a282d4..53a9d9e790d67 100644 --- a/x-pack/test/api_integration/apis/ml/data_frame_analytics/delete.ts +++ b/x-pack/test/api_integration/apis/ml/data_frame_analytics/delete.ts @@ -120,7 +120,7 @@ export default ({ getService }: FtrProviderContext) => { .expect(404); expect(body.error).to.eql('Not Found'); - expect(body.message).to.eql('Not Found'); + expect(body.message).to.eql('resource_not_found_exception'); }); describe('with deleteDestIndex setting', function () { diff --git a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts index 4ec8ae0429a6b..5fe0dd5ee5e4f 100644 --- a/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts +++ b/x-pack/test/api_integration/apis/ml/data_visualizer/get_field_stats.ts @@ -33,7 +33,7 @@ export default ({ getService }: FtrProviderContext) => { ], samplerShardSize: -1, // No sampling, as otherwise counts could vary on each run. timeFieldName: '@timestamp', - interval: '1d', + interval: 86400000, maxExamples: 10, }, expected: { @@ -41,7 +41,7 @@ export default ({ getService }: FtrProviderContext) => { responseBody: [ { documentCounts: { - interval: '1d', + interval: 86400000, buckets: { '1454803200000': 846, '1454889600000': 846, @@ -145,6 +145,7 @@ export default ({ getService }: FtrProviderContext) => { ], samplerShardSize: -1, // No sampling, as otherwise counts could vary on each run. timeFieldName: '@timestamp', + interval: 86400000, maxExamples: 10, }, expected: { diff --git a/x-pack/test/api_integration/apis/ml/jobs/jobs_exist.ts b/x-pack/test/api_integration/apis/ml/jobs/jobs_exist.ts new file mode 100644 index 0000000000000..c48376b6a14f3 --- /dev/null +++ b/x-pack/test/api_integration/apis/ml/jobs/jobs_exist.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; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api'; +import { USER } from '../../../../functional/services/ml/security_common'; +import { SINGLE_METRIC_JOB_CONFIG, DATAFEED_CONFIG } from './common_jobs'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const ml = getService('ml'); + + const testSetupJobConfigs = [SINGLE_METRIC_JOB_CONFIG]; + + const responseBody = { + [SINGLE_METRIC_JOB_CONFIG.job_id]: true, + [`${SINGLE_METRIC_JOB_CONFIG.job_id.slice(0, 10)}*`]: true, // wildcard, use first 10 chars + [`${SINGLE_METRIC_JOB_CONFIG.job_id}_fail`]: false, + [`${SINGLE_METRIC_JOB_CONFIG.job_id.slice(0, 10)}_fail*`]: false, // wildcard, use first 10 chars + }; + + const testDataList = [ + { + testTitle: 'as ML Poweruser', + user: USER.ML_POWERUSER, + requestBody: { + jobIds: Object.keys(responseBody), + }, + expected: { + responseCode: 200, + responseBody, + }, + }, + { + testTitle: 'as ML Viewer', + user: USER.ML_VIEWER, + requestBody: { + jobIds: Object.keys(responseBody), + }, + expected: { + responseCode: 200, + responseBody, + }, + }, + ]; + + const testDataListUnauthorized = [ + { + testTitle: 'as ML Unauthorized user', + user: USER.ML_UNAUTHORIZED, + requestBody: { + jobIds: Object.keys(responseBody), + }, + expected: { + responseCode: 404, + error: 'Not Found', + }, + }, + ]; + + async function runJobsExistRequest( + user: USER, + requestBody: object, + expectedResponsecode: number + ): Promise { + const { body } = await supertest + .post('/api/ml/jobs/jobs_exist') + .auth(user, ml.securityCommon.getPasswordForUser(user)) + .set(COMMON_REQUEST_HEADERS) + .send(requestBody) + .expect(expectedResponsecode); + + return body; + } + + describe('jobs_exist', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await ml.testResources.createIndexPatternIfNeeded('ft_farequote', '@timestamp'); + await ml.testResources.setKibanaTimeZoneToUTC(); + }); + + after(async () => { + await ml.api.cleanMlIndices(); + }); + + it('sets up jobs', async () => { + for (const job of testSetupJobConfigs) { + const datafeedId = `datafeed-${job.job_id}`; + await ml.api.createAnomalyDetectionJob(job); + await ml.api.openAnomalyDetectionJob(job.job_id); + await ml.api.createDatafeed({ + ...DATAFEED_CONFIG, + datafeed_id: datafeedId, + job_id: job.job_id, + }); + } + }); + + describe('jobs exist', function () { + for (const testData of testDataList) { + it(`${testData.testTitle}`, async () => { + const body = await runJobsExistRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + const expectedResponse = testData.expected.responseBody; + const expectedRspJobIds = Object.keys(expectedResponse).sort((a, b) => + a.localeCompare(b) + ); + const actualRspJobIds = Object.keys(body).sort((a, b) => a.localeCompare(b)); + + expect(actualRspJobIds).to.have.length(expectedRspJobIds.length); + expect(actualRspJobIds).to.eql(expectedRspJobIds); + expectedRspJobIds.forEach((id) => { + expect(body[id]).to.eql(testData.expected.responseBody[id]); + }); + }); + } + }); + + describe('rejects request', function () { + for (const testData of testDataListUnauthorized) { + describe('fails to check jobs exist', function () { + it(`${testData.testTitle}`, async () => { + const body = await runJobsExistRequest( + testData.user, + testData.requestBody, + testData.expected.responseCode + ); + + expect(body).to.have.property('error').eql(testData.expected.error); + }); + }); + } + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json index b7c3aee5471d7..a000324d121ea 100644 --- a/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json +++ b/x-pack/test/api_integration/apis/monitoring/cluster/fixtures/multicluster.json @@ -102,7 +102,7 @@ }, "alerts": { "alertsMeta": { - "enabled": true + "enabled": false }, "clusterMeta": { "enabled": false, diff --git a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json index f938479578801..7091e584344e7 100644 --- a/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json +++ b/x-pack/test/api_integration/apis/monitoring/standalone_cluster/fixtures/clusters.json @@ -102,7 +102,7 @@ }, "alerts": { "alertsMeta": { - "enabled": true + "enabled": false }, "clusterMeta": { "enabled": false, @@ -170,7 +170,7 @@ }, "alerts": { "alertsMeta": { - "enabled": true + "enabled": false }, "clusterMeta": { "enabled": false, diff --git a/x-pack/test/api_integration/apis/security/basic_login.js b/x-pack/test/api_integration/apis/security/basic_login.js index 4b39b1bf32d5b..43ef8e6b81eac 100644 --- a/x-pack/test/api_integration/apis/security/basic_login.js +++ b/x-pack/test/api_integration/apis/security/basic_login.js @@ -148,11 +148,8 @@ export default function ({ getService }) { ]); expect(apiResponse.body.username).to.be(validUsername); expect(apiResponse.body.authentication_provider).to.eql('__http__'); - expect(apiResponse.body.authentication_realm).to.eql({ - name: 'reserved', - type: 'reserved', - }); expect(apiResponse.body.authentication_type).to.be('realm'); + // Do not assert on the `authentication_realm`, as the value differes for on-prem vs cloud }); describe('with session cookie', () => { @@ -197,11 +194,8 @@ export default function ({ getService }) { ]); expect(apiResponse.body.username).to.be(validUsername); expect(apiResponse.body.authentication_provider).to.eql('basic'); - expect(apiResponse.body.authentication_realm).to.eql({ - name: 'reserved', - type: 'reserved', - }); expect(apiResponse.body.authentication_type).to.be('realm'); + // Do not assert on the `authentication_realm`, as the value differes for on-prem vs cloud }); it('should extend cookie on every successful non-system API call', async () => { diff --git a/x-pack/test/api_integration/apis/security_solution/ip_overview.ts b/x-pack/test/api_integration/apis/security_solution/network_details.ts similarity index 97% rename from x-pack/test/api_integration/apis/security_solution/ip_overview.ts rename to x-pack/test/api_integration/apis/security_solution/network_details.ts index 6493c07617991..cffcd790fa19c 100644 --- a/x-pack/test/api_integration/apis/security_solution/ip_overview.ts +++ b/x-pack/test/api_integration/apis/security_solution/network_details.ts @@ -5,7 +5,7 @@ */ import expect from '@kbn/expect'; -import { ipOverviewQuery } from '../../../../plugins/security_solution/public/network/containers/ip_overview/index.gql_query'; +import { ipOverviewQuery } from '../../../../plugins/security_solution/public/network/containers/details/index.gql_query'; import { GetIpOverviewQuery } from '../../../../plugins/security_solution/public/graphql/types'; import { FtrProviderContext } from '../../ftr_provider_context'; diff --git a/x-pack/test/api_integration/apis/transform/common.ts b/x-pack/test/api_integration/apis/transform/common.ts new file mode 100644 index 0000000000000..1a48ee987bc77 --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/common.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import type { PutTransformsRequestSchema } from '../../../../plugins/transform/common/api_schemas/transforms'; + +export async function asyncForEach(array: any[], callback: Function) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array); + } +} + +export function generateDestIndex(transformId: string): string { + return `user-${transformId}`; +} + +export function generateTransformConfig(transformId: string): PutTransformsRequestSchema { + const destinationIndex = generateDestIndex(transformId); + + return { + source: { index: ['ft_farequote'] }, + pivot: { + group_by: { airline: { terms: { field: 'airline' } } }, + aggregations: { '@timestamp.value_count': { value_count: { field: '@timestamp' } } }, + }, + dest: { index: destinationIndex }, + }; +} diff --git a/x-pack/test/api_integration/apis/transform/delete_transforms.ts b/x-pack/test/api_integration/apis/transform/delete_transforms.ts index 7f01d2741ad15..41b2bffb1f0ad 100644 --- a/x-pack/test/api_integration/apis/transform/delete_transforms.ts +++ b/x-pack/test/api_integration/apis/transform/delete_transforms.ts @@ -4,41 +4,28 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { TransformEndpointRequest } from '../../../../plugins/transform/common'; -import { FtrProviderContext } from '../../ftr_provider_context'; + +import { DeleteTransformsRequestSchema } from '../../../../plugins/transform/common/api_schemas/delete_transforms'; +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; import { USER } from '../../../functional/services/transform/security_common'; -async function asyncForEach(array: any[], callback: Function) { - for (let index = 0; index < array.length; index++) { - await callback(array[index], index, array); - } -} +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { asyncForEach, generateDestIndex, generateTransformConfig } from './common'; export default ({ getService }: FtrProviderContext) => { const esArchiver = getService('esArchiver'); const supertest = getService('supertestWithoutAuth'); const transform = getService('transform'); - function generateDestIndex(transformId: string): string { - return `user-${transformId}`; + async function createTransform(transformId: string) { + const config = generateTransformConfig(transformId); + await transform.api.createTransform(transformId, config); } - async function createTransform(transformId: string, destinationIndex: string) { - const config = { - id: transformId, - source: { index: ['farequote-*'] }, - pivot: { - group_by: { airline: { terms: { field: 'airline' } } }, - aggregations: { '@timestamp.value_count': { value_count: { field: '@timestamp' } } }, - }, - dest: { index: destinationIndex }, - }; - - await transform.api.createTransform(config); - } - - describe('delete_transforms', function () { + describe('/api/transform/delete_transforms', function () { before(async () => { await esArchiver.loadIfNeeded('ml/farequote'); await transform.testResources.setKibanaTimeZoneToUTC(); @@ -49,11 +36,11 @@ export default ({ getService }: FtrProviderContext) => { }); describe('single transform deletion', function () { - const transformId = 'test1'; + const transformId = 'transform-test-delete'; const destinationIndex = generateDestIndex(transformId); beforeEach(async () => { - await createTransform(transformId, destinationIndex); + await createTransform(transformId); await transform.api.createIndices(destinationIndex); }); @@ -62,7 +49,9 @@ export default ({ getService }: FtrProviderContext) => { }); it('should delete transform by transformId', async () => { - const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }]; + const reqBody: DeleteTransformsRequestSchema = { + transformsInfo: [{ id: transformId, state: TRANSFORM_STATE.STOPPED }], + }; const { body } = await supertest .post(`/api/transform/delete_transforms`) .auth( @@ -70,9 +59,7 @@ export default ({ getService }: FtrProviderContext) => { transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) ) .set(COMMON_REQUEST_HEADERS) - .send({ - transformsInfo, - }) + .send(reqBody) .expect(200); expect(body[transformId].transformDeleted.success).to.eql(true); @@ -83,7 +70,9 @@ export default ({ getService }: FtrProviderContext) => { }); it('should return 403 for unauthorized user', async () => { - const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }]; + const reqBody: DeleteTransformsRequestSchema = { + transformsInfo: [{ id: transformId, state: TRANSFORM_STATE.STOPPED }], + }; await supertest .post(`/api/transform/delete_transforms`) .auth( @@ -91,9 +80,7 @@ export default ({ getService }: FtrProviderContext) => { transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) ) .set(COMMON_REQUEST_HEADERS) - .send({ - transformsInfo, - }) + .send(reqBody) .expect(403); await transform.api.waitForTransformToExist(transformId); await transform.api.waitForIndicesToExist(destinationIndex); @@ -102,7 +89,9 @@ export default ({ getService }: FtrProviderContext) => { describe('single transform deletion with invalid transformId', function () { it('should return 200 with error in response if invalid transformId', async () => { - const transformsInfo: TransformEndpointRequest[] = [{ id: 'invalid_transform_id' }]; + const reqBody: DeleteTransformsRequestSchema = { + transformsInfo: [{ id: 'invalid_transform_id', state: TRANSFORM_STATE.STOPPED }], + }; const { body } = await supertest .post(`/api/transform/delete_transforms`) .auth( @@ -110,9 +99,7 @@ export default ({ getService }: FtrProviderContext) => { transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) ) .set(COMMON_REQUEST_HEADERS) - .send({ - transformsInfo, - }) + .send(reqBody) .expect(200); expect(body.invalid_transform_id.transformDeleted.success).to.eql(false); expect(body.invalid_transform_id.transformDeleted).to.have.property('error'); @@ -120,15 +107,17 @@ export default ({ getService }: FtrProviderContext) => { }); describe('bulk deletion', function () { - const transformsInfo: TransformEndpointRequest[] = [ - { id: 'bulk_delete_test_1' }, - { id: 'bulk_delete_test_2' }, - ]; - const destinationIndices = transformsInfo.map((d) => generateDestIndex(d.id)); + const reqBody: DeleteTransformsRequestSchema = { + transformsInfo: [ + { id: 'bulk_delete_test_1', state: TRANSFORM_STATE.STOPPED }, + { id: 'bulk_delete_test_2', state: TRANSFORM_STATE.STOPPED }, + ], + }; + const destinationIndices = reqBody.transformsInfo.map((d) => generateDestIndex(d.id)); beforeEach(async () => { - await asyncForEach(transformsInfo, async ({ id }: { id: string }, idx: number) => { - await createTransform(id, destinationIndices[idx]); + await asyncForEach(reqBody.transformsInfo, async ({ id }: { id: string }, idx: number) => { + await createTransform(id); await transform.api.createIndices(destinationIndices[idx]); }); }); @@ -147,13 +136,11 @@ export default ({ getService }: FtrProviderContext) => { transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) ) .set(COMMON_REQUEST_HEADERS) - .send({ - transformsInfo, - }) + .send(reqBody) .expect(200); await asyncForEach( - transformsInfo, + reqBody.transformsInfo, async ({ id: transformId }: { id: string }, idx: number) => { expect(body[transformId].transformDeleted.success).to.eql(true); expect(body[transformId].destIndexDeleted.success).to.eql(false); @@ -174,16 +161,16 @@ export default ({ getService }: FtrProviderContext) => { ) .set(COMMON_REQUEST_HEADERS) .send({ + ...reqBody, transformsInfo: [ - { id: transformsInfo[0].id }, - { id: invalidTransformId }, - { id: transformsInfo[1].id }, + ...reqBody.transformsInfo, + { id: invalidTransformId, state: TRANSFORM_STATE.STOPPED }, ], }) .expect(200); await asyncForEach( - transformsInfo, + reqBody.transformsInfo, async ({ id: transformId }: { id: string }, idx: number) => { expect(body[transformId].transformDeleted.success).to.eql(true); expect(body[transformId].destIndexDeleted.success).to.eql(false); @@ -203,7 +190,7 @@ export default ({ getService }: FtrProviderContext) => { const destinationIndex = generateDestIndex(transformId); before(async () => { - await createTransform(transformId, destinationIndex); + await createTransform(transformId); await transform.api.createIndices(destinationIndex); }); @@ -212,7 +199,10 @@ export default ({ getService }: FtrProviderContext) => { }); it('should delete transform and destination index', async () => { - const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }]; + const reqBody: DeleteTransformsRequestSchema = { + transformsInfo: [{ id: transformId, state: TRANSFORM_STATE.STOPPED }], + deleteDestIndex: true, + }; const { body } = await supertest .post(`/api/transform/delete_transforms`) .auth( @@ -220,10 +210,7 @@ export default ({ getService }: FtrProviderContext) => { transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) ) .set(COMMON_REQUEST_HEADERS) - .send({ - transformsInfo, - deleteDestIndex: true, - }) + .send(reqBody) .expect(200); expect(body[transformId].transformDeleted.success).to.eql(true); @@ -239,7 +226,7 @@ export default ({ getService }: FtrProviderContext) => { const destinationIndex = generateDestIndex(transformId); before(async () => { - await createTransform(transformId, destinationIndex); + await createTransform(transformId); await transform.api.createIndices(destinationIndex); await transform.testResources.createIndexPatternIfNeeded(destinationIndex); }); @@ -250,7 +237,11 @@ export default ({ getService }: FtrProviderContext) => { }); it('should delete transform and destination index pattern', async () => { - const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }]; + const reqBody: DeleteTransformsRequestSchema = { + transformsInfo: [{ id: transformId, state: TRANSFORM_STATE.STOPPED }], + deleteDestIndex: false, + deleteDestIndexPattern: true, + }; const { body } = await supertest .post(`/api/transform/delete_transforms`) .auth( @@ -258,11 +249,7 @@ export default ({ getService }: FtrProviderContext) => { transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) ) .set(COMMON_REQUEST_HEADERS) - .send({ - transformsInfo, - deleteDestIndex: false, - deleteDestIndexPattern: true, - }) + .send(reqBody) .expect(200); expect(body[transformId].transformDeleted.success).to.eql(true); @@ -279,7 +266,7 @@ export default ({ getService }: FtrProviderContext) => { const destinationIndex = generateDestIndex(transformId); before(async () => { - await createTransform(transformId, destinationIndex); + await createTransform(transformId); await transform.api.createIndices(destinationIndex); await transform.testResources.createIndexPatternIfNeeded(destinationIndex); }); @@ -290,7 +277,11 @@ export default ({ getService }: FtrProviderContext) => { }); it('should delete transform, destination index, & destination index pattern', async () => { - const transformsInfo: TransformEndpointRequest[] = [{ id: transformId }]; + const reqBody: DeleteTransformsRequestSchema = { + transformsInfo: [{ id: transformId, state: TRANSFORM_STATE.STOPPED }], + deleteDestIndex: true, + deleteDestIndexPattern: true, + }; const { body } = await supertest .post(`/api/transform/delete_transforms`) .auth( @@ -298,11 +289,7 @@ export default ({ getService }: FtrProviderContext) => { transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) ) .set(COMMON_REQUEST_HEADERS) - .send({ - transformsInfo, - deleteDestIndex: true, - deleteDestIndexPattern: true, - }) + .send(reqBody) .expect(200); expect(body[transformId].transformDeleted.success).to.eql(true); diff --git a/x-pack/test/api_integration/apis/transform/index.ts b/x-pack/test/api_integration/apis/transform/index.ts index 93a951a55ece1..ef08883534d10 100644 --- a/x-pack/test/api_integration/apis/transform/index.ts +++ b/x-pack/test/api_integration/apis/transform/index.ts @@ -28,5 +28,11 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { }); loadTestFile(require.resolve('./delete_transforms')); + loadTestFile(require.resolve('./start_transforms')); + loadTestFile(require.resolve('./stop_transforms')); + loadTestFile(require.resolve('./transforms')); + loadTestFile(require.resolve('./transforms_preview')); + loadTestFile(require.resolve('./transforms_stats')); + loadTestFile(require.resolve('./transforms_update')); }); } diff --git a/x-pack/test/api_integration/apis/transform/start_transforms.ts b/x-pack/test/api_integration/apis/transform/start_transforms.ts new file mode 100644 index 0000000000000..288a3caae390e --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/start_transforms.ts @@ -0,0 +1,164 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import { StartTransformsRequestSchema } from '../../../../plugins/transform/common/api_schemas/start_transforms'; +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + +import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { USER } from '../../../functional/services/transform/security_common'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { asyncForEach, generateDestIndex, generateTransformConfig } from './common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const transform = getService('transform'); + + async function createTransform(transformId: string) { + const config = generateTransformConfig(transformId); + await transform.api.createTransform(transformId, config); + } + + describe('/api/transform/start_transforms', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await transform.testResources.setKibanaTimeZoneToUTC(); + }); + + describe('single transform start', function () { + const transformId = 'transform-test-start'; + const destinationIndex = generateDestIndex(transformId); + + beforeEach(async () => { + await createTransform(transformId); + }); + + afterEach(async () => { + await transform.api.cleanTransformIndices(); + await transform.api.deleteIndices(destinationIndex); + }); + + it('should start the transform by transformId', async () => { + const reqBody: StartTransformsRequestSchema = [{ id: transformId }]; + const { body } = await supertest + .post(`/api/transform/start_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + expect(body[transformId].success).to.eql(true); + expect(typeof body[transformId].error).to.eql('undefined'); + await transform.api.waitForBatchTransformToComplete(transformId); + await transform.api.waitForIndicesToExist(destinationIndex); + }); + + it('should return 200 with success:false for unauthorized user', async () => { + const reqBody: StartTransformsRequestSchema = [{ id: transformId }]; + const { body } = await supertest + .post(`/api/transform/start_transforms`) + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + expect(body[transformId].success).to.eql(false); + expect(typeof body[transformId].error).to.eql('string'); + + await transform.api.waitForTransformState(transformId, TRANSFORM_STATE.STOPPED); + await transform.api.waitForIndicesNotToExist(destinationIndex); + }); + }); + + describe('single transform start with invalid transformId', function () { + it('should return 200 with error in response if invalid transformId', async () => { + const reqBody: StartTransformsRequestSchema = [{ id: 'invalid_transform_id' }]; + const { body } = await supertest + .post(`/api/transform/start_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + expect(body.invalid_transform_id.success).to.eql(false); + expect(body.invalid_transform_id).to.have.property('error'); + }); + }); + + describe('bulk start', function () { + const reqBody: StartTransformsRequestSchema = [ + { id: 'bulk_start_test_1' }, + { id: 'bulk_start_test_2' }, + ]; + const destinationIndices = reqBody.map((d) => generateDestIndex(d.id)); + + beforeEach(async () => { + await asyncForEach(reqBody, async ({ id }: { id: string }, idx: number) => { + await createTransform(id); + }); + }); + + afterEach(async () => { + await transform.api.cleanTransformIndices(); + await asyncForEach(destinationIndices, async (destinationIndex: string) => { + await transform.api.deleteIndices(destinationIndex); + }); + }); + + it('should start multiple transforms by transformIds', async () => { + const { body } = await supertest + .post(`/api/transform/start_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + await asyncForEach(reqBody, async ({ id: transformId }: { id: string }, idx: number) => { + expect(body[transformId].success).to.eql(true); + await transform.api.waitForBatchTransformToComplete(transformId); + await transform.api.waitForIndicesToExist(destinationIndices[idx]); + }); + }); + + it('should start multiple transforms by transformIds, even if one of the transformIds is invalid', async () => { + const invalidTransformId = 'invalid_transform_id'; + const { body } = await supertest + .post(`/api/transform/start_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send([{ id: reqBody[0].id }, { id: invalidTransformId }, { id: reqBody[1].id }]) + .expect(200); + + await asyncForEach(reqBody, async ({ id: transformId }: { id: string }, idx: number) => { + expect(body[transformId].success).to.eql(true); + await transform.api.waitForBatchTransformToComplete(transformId); + await transform.api.waitForIndicesToExist(destinationIndices[idx]); + }); + + expect(body[invalidTransformId].success).to.eql(false); + expect(body[invalidTransformId]).to.have.property('error'); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/transform/stop_transforms.ts b/x-pack/test/api_integration/apis/transform/stop_transforms.ts new file mode 100644 index 0000000000000..4f30db0794ea4 --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/stop_transforms.ts @@ -0,0 +1,197 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import type { PutTransformsRequestSchema } from '../../../../plugins/transform/common/api_schemas/transforms'; +import type { StopTransformsRequestSchema } from '../../../../plugins/transform/common/api_schemas/stop_transforms'; +import { isStopTransformsResponseSchema } from '../../../../plugins/transform/common/api_schemas/type_guards'; + +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + +import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { USER } from '../../../functional/services/transform/security_common'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { asyncForEach, generateDestIndex, generateTransformConfig } from './common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const transform = getService('transform'); + + async function createAndRunTransform(transformId: string) { + // to be able to test stopping transforms, + // we create a slow continuous transform + // so it doesn't stop automatically. + const config: PutTransformsRequestSchema = { + ...generateTransformConfig(transformId), + settings: { + docs_per_second: 10, + max_page_search_size: 10, + }, + sync: { + time: { field: '@timestamp' }, + }, + }; + + await transform.api.createAndRunTransform(transformId, config); + } + + describe('/api/transform/stop_transforms', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await transform.testResources.setKibanaTimeZoneToUTC(); + }); + + describe('single transform stop', function () { + const transformId = 'transform-test-stop'; + const destinationIndex = generateDestIndex(transformId); + + beforeEach(async () => { + await createAndRunTransform(transformId); + }); + + afterEach(async () => { + await transform.api.cleanTransformIndices(); + await transform.api.deleteIndices(destinationIndex); + }); + + it('should stop the transform by transformId', async () => { + const reqBody: StopTransformsRequestSchema = [ + { id: transformId, state: TRANSFORM_STATE.STARTED }, + ]; + const { body } = await supertest + .post(`/api/transform/stop_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + expect(isStopTransformsResponseSchema(body)).to.eql(true); + expect(body[transformId].success).to.eql(true); + expect(typeof body[transformId].error).to.eql('undefined'); + await transform.api.waitForTransformState(transformId, TRANSFORM_STATE.STOPPED); + await transform.api.waitForIndicesToExist(destinationIndex); + }); + + it('should return 200 with success:false for unauthorized user', async () => { + const reqBody: StopTransformsRequestSchema = [ + { id: transformId, state: TRANSFORM_STATE.STARTED }, + ]; + const { body } = await supertest + .post(`/api/transform/stop_transforms`) + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + expect(isStopTransformsResponseSchema(body)).to.eql(true); + expect(body[transformId].success).to.eql(false); + expect(typeof body[transformId].error).to.eql('string'); + + await transform.api.waitForTransformStateNotToBe(transformId, TRANSFORM_STATE.STOPPED); + await transform.api.waitForIndicesToExist(destinationIndex); + }); + }); + + describe('single transform stop with invalid transformId', function () { + it('should return 200 with error in response if invalid transformId', async () => { + const reqBody: StopTransformsRequestSchema = [ + { id: 'invalid_transform_id', state: TRANSFORM_STATE.STARTED }, + ]; + const { body } = await supertest + .post(`/api/transform/stop_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + expect(isStopTransformsResponseSchema(body)).to.eql(true); + expect(body.invalid_transform_id.success).to.eql(false); + expect(body.invalid_transform_id).to.have.property('error'); + }); + }); + + describe('bulk stop', function () { + const reqBody: StopTransformsRequestSchema = [ + { id: 'bulk_stop_test_1', state: TRANSFORM_STATE.STARTED }, + { id: 'bulk_stop_test_2', state: TRANSFORM_STATE.STARTED }, + ]; + const destinationIndices = reqBody.map((d) => generateDestIndex(d.id)); + + beforeEach(async () => { + await asyncForEach(reqBody, async ({ id }: { id: string }, idx: number) => { + await createAndRunTransform(id); + }); + }); + + afterEach(async () => { + await transform.api.cleanTransformIndices(); + await asyncForEach(destinationIndices, async (destinationIndex: string) => { + await transform.api.deleteIndices(destinationIndex); + }); + }); + + it('should stop multiple transforms by transformIds', async () => { + const { body } = await supertest + .post(`/api/transform/stop_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(reqBody) + .expect(200); + + expect(isStopTransformsResponseSchema(body)).to.eql(true); + + await asyncForEach(reqBody, async ({ id: transformId }: { id: string }, idx: number) => { + expect(body[transformId].success).to.eql(true); + await transform.api.waitForTransformState(transformId, TRANSFORM_STATE.STOPPED); + await transform.api.waitForIndicesToExist(destinationIndices[idx]); + }); + }); + + it('should stop multiple transforms by transformIds, even if one of the transformIds is invalid', async () => { + const invalidTransformId = 'invalid_transform_id'; + const { body } = await supertest + .post(`/api/transform/stop_transforms`) + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send([ + { id: reqBody[0].id, state: reqBody[0].state }, + { id: invalidTransformId, state: TRANSFORM_STATE.STOPPED }, + { id: reqBody[1].id, state: reqBody[1].state }, + ]) + .expect(200); + + expect(isStopTransformsResponseSchema(body)).to.eql(true); + + await asyncForEach(reqBody, async ({ id: transformId }: { id: string }, idx: number) => { + expect(body[transformId].success).to.eql(true); + await transform.api.waitForTransformState(transformId, TRANSFORM_STATE.STOPPED); + await transform.api.waitForIndicesToExist(destinationIndices[idx]); + }); + + expect(body[invalidTransformId].success).to.eql(false); + expect(body[invalidTransformId]).to.have.property('error'); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/transform/transforms.ts b/x-pack/test/api_integration/apis/transform/transforms.ts new file mode 100644 index 0000000000000..c44c2b58e6207 --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/transforms.ts @@ -0,0 +1,165 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import type { GetTransformsResponseSchema } from '../../../../plugins/transform/common/api_schemas/transforms'; +import { isGetTransformsResponseSchema } from '../../../../plugins/transform/common/api_schemas/type_guards'; +import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { USER } from '../../../functional/services/transform/security_common'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { generateTransformConfig } from './common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const transform = getService('transform'); + + const expected = { + apiTransformTransforms: { + count: 2, + transform1: { id: 'transform-test-get-1', destIndex: 'user-transform-test-get-1' }, + transform2: { id: 'transform-test-get-2', destIndex: 'user-transform-test-get-2' }, + typeOfVersion: 'string', + typeOfCreateTime: 'number', + }, + apiTransformTransformsTransformId: { + count: 1, + transform1: { id: 'transform-test-get-1', destIndex: 'user-transform-test-get-1' }, + typeOfVersion: 'string', + typeOfCreateTime: 'number', + }, + }; + + async function createTransform(transformId: string) { + const config = generateTransformConfig(transformId); + await transform.api.createTransform(transformId, config); + } + + function assertTransformsResponseBody(body: GetTransformsResponseSchema) { + expect(isGetTransformsResponseSchema(body)).to.eql(true); + + expect(body.count).to.eql(expected.apiTransformTransforms.count); + expect(body.transforms).to.have.length(expected.apiTransformTransforms.count); + + const transform1 = body.transforms[0]; + expect(transform1.id).to.eql(expected.apiTransformTransforms.transform1.id); + expect(transform1.dest.index).to.eql(expected.apiTransformTransforms.transform1.destIndex); + expect(typeof transform1.version).to.eql(expected.apiTransformTransforms.typeOfVersion); + expect(typeof transform1.create_time).to.eql(expected.apiTransformTransforms.typeOfCreateTime); + + const transform2 = body.transforms[1]; + expect(transform2.id).to.eql(expected.apiTransformTransforms.transform2.id); + expect(transform2.dest.index).to.eql(expected.apiTransformTransforms.transform2.destIndex); + expect(typeof transform2.version).to.eql(expected.apiTransformTransforms.typeOfVersion); + expect(typeof transform2.create_time).to.eql(expected.apiTransformTransforms.typeOfCreateTime); + } + + function assertSingleTransformResponseBody(body: GetTransformsResponseSchema) { + expect(isGetTransformsResponseSchema(body)).to.eql(true); + + expect(body.count).to.eql(expected.apiTransformTransformsTransformId.count); + expect(body.transforms).to.have.length(expected.apiTransformTransformsTransformId.count); + + const transform1 = body.transforms[0]; + expect(transform1.id).to.eql(expected.apiTransformTransformsTransformId.transform1.id); + expect(transform1.dest.index).to.eql( + expected.apiTransformTransformsTransformId.transform1.destIndex + ); + expect(typeof transform1.version).to.eql( + expected.apiTransformTransformsTransformId.typeOfVersion + ); + expect(typeof transform1.create_time).to.eql( + expected.apiTransformTransformsTransformId.typeOfCreateTime + ); + } + + describe('/api/transform/transforms', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await transform.testResources.setKibanaTimeZoneToUTC(); + await createTransform('transform-test-get-1'); + await createTransform('transform-test-get-2'); + }); + + after(async () => { + await transform.api.cleanTransformIndices(); + }); + + describe('/transforms', function () { + it('should return a list of transforms for super-user', async () => { + const { body } = await supertest + .get('/api/transform/transforms') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + assertTransformsResponseBody(body); + }); + + it('should return a list of transforms for transform view-only user', async () => { + const { body } = await supertest + .get(`/api/transform/transforms`) + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + assertTransformsResponseBody(body); + }); + }); + + describe('/transforms/{transformId}', function () { + it('should return a specific transform configuration for super-user', async () => { + const { body } = await supertest + .get('/api/transform/transforms/transform-test-get-1') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + assertSingleTransformResponseBody(body); + }); + + it('should return a specific transform configuration transform view-only user', async () => { + const { body } = await supertest + .get(`/api/transform/transforms/transform-test-get-1`) + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + assertSingleTransformResponseBody(body); + }); + + it('should report 404 for a non-existing transform', async () => { + await supertest + .get('/api/transform/transforms/the-non-existing-transform') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(404); + }); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/transform/transforms_preview.ts b/x-pack/test/api_integration/apis/transform/transforms_preview.ts new file mode 100644 index 0000000000000..d0fc44cf28fdb --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/transforms_preview.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import type { PostTransformsPreviewRequestSchema } from '../../../../plugins/transform/common/api_schemas/transforms'; + +import { FtrProviderContext } from '../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { USER } from '../../../functional/services/transform/security_common'; + +import { generateTransformConfig } from './common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const transform = getService('transform'); + + const expected = { + apiTransformTransformsPreview: { + previewItemCount: 19, + typeOfGeneratedDestIndex: 'object', + }, + }; + + function getTransformPreviewConfig() { + // passing in an empty string for transform id since we will not use + // it as part of the config request schema. Destructuring will + // remove the `dest` part of the config. + const { dest, ...config } = generateTransformConfig(''); + return config as PostTransformsPreviewRequestSchema; + } + + describe('/api/transform/transforms/_preview', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await transform.testResources.setKibanaTimeZoneToUTC(); + await transform.api.waitForIndicesToExist('ft_farequote'); + }); + + it('should return a transform preview', async () => { + const { body } = await supertest + .post('/api/transform/transforms/_preview') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(getTransformPreviewConfig()) + .expect(200); + + expect(body.preview).to.have.length(expected.apiTransformTransformsPreview.previewItemCount); + expect(typeof body.generated_dest_index).to.eql( + expected.apiTransformTransformsPreview.typeOfGeneratedDestIndex + ); + }); + + it('should return 403 for transform view-only user', async () => { + await supertest + .post(`/api/transform/transforms/_preview`) + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(getTransformPreviewConfig()) + .expect(403); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/transform/transforms_stats.ts b/x-pack/test/api_integration/apis/transform/transforms_stats.ts new file mode 100644 index 0000000000000..07856e5095a98 --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/transforms_stats.ts @@ -0,0 +1,101 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; + +import type { GetTransformsStatsResponseSchema } from '../../../../plugins/transform/common/api_schemas/transforms_stats'; +import { isGetTransformsStatsResponseSchema } from '../../../../plugins/transform/common/api_schemas/type_guards'; +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + +import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { USER } from '../../../functional/services/transform/security_common'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +import { generateTransformConfig } from './common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const transform = getService('transform'); + + const expected = { + apiTransformTransforms: { + count: 2, + transform1: { id: 'transform-test-stats-1', state: TRANSFORM_STATE.STOPPED }, + transform2: { id: 'transform-test-stats-2', state: TRANSFORM_STATE.STOPPED }, + typeOfStats: 'object', + typeOfCheckpointing: 'object', + }, + }; + + async function createTransform(transformId: string) { + const config = generateTransformConfig(transformId); + await transform.api.createTransform(transformId, config); + } + + function assertTransformsStatsResponseBody(body: GetTransformsStatsResponseSchema) { + expect(isGetTransformsStatsResponseSchema(body)).to.eql(true); + expect(body.count).to.eql(expected.apiTransformTransforms.count); + expect(body.transforms).to.have.length(expected.apiTransformTransforms.count); + + const transform1 = body.transforms[0]; + expect(transform1.id).to.eql(expected.apiTransformTransforms.transform1.id); + expect(transform1.state).to.eql(expected.apiTransformTransforms.transform1.state); + expect(typeof transform1.stats).to.eql(expected.apiTransformTransforms.typeOfStats); + expect(typeof transform1.checkpointing).to.eql( + expected.apiTransformTransforms.typeOfCheckpointing + ); + + const transform2 = body.transforms[1]; + expect(transform2.id).to.eql(expected.apiTransformTransforms.transform2.id); + expect(transform2.state).to.eql(expected.apiTransformTransforms.transform2.state); + expect(typeof transform2.stats).to.eql(expected.apiTransformTransforms.typeOfStats); + expect(typeof transform2.checkpointing).to.eql( + expected.apiTransformTransforms.typeOfCheckpointing + ); + } + + describe('/api/transform/transforms/_stats', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await transform.testResources.setKibanaTimeZoneToUTC(); + await createTransform('transform-test-stats-1'); + await createTransform('transform-test-stats-2'); + }); + + after(async () => { + await transform.api.cleanTransformIndices(); + }); + + it('should return a list of transforms statistics for super-user', async () => { + const { body } = await supertest + .get('/api/transform/transforms/_stats') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + assertTransformsStatsResponseBody(body); + }); + + it('should return a list of transforms statistics view-only user', async () => { + const { body } = await supertest + .get(`/api/transform/transforms/_stats`) + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + assertTransformsStatsResponseBody(body); + }); + }); +}; diff --git a/x-pack/test/api_integration/apis/transform/transforms_update.ts b/x-pack/test/api_integration/apis/transform/transforms_update.ts new file mode 100644 index 0000000000000..3ad5b5b47c79b --- /dev/null +++ b/x-pack/test/api_integration/apis/transform/transforms_update.ts @@ -0,0 +1,150 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; +import { COMMON_REQUEST_HEADERS } from '../../../functional/services/ml/common_api'; +import { USER } from '../../../functional/services/transform/security_common'; + +import { generateTransformConfig } from './common'; + +export default ({ getService }: FtrProviderContext) => { + const esArchiver = getService('esArchiver'); + const supertest = getService('supertestWithoutAuth'); + const transform = getService('transform'); + + const expected = { + transformOriginalConfig: { + count: 1, + id: 'transform-test-update-1', + source: { + index: ['ft_farequote'], + query: { match_all: {} }, + }, + }, + apiTransformTransformsPreview: { + previewItemCount: 19, + typeOfGeneratedDestIndex: 'object', + }, + }; + + async function createTransform(transformId: string) { + const config = generateTransformConfig(transformId); + await transform.api.createTransform(transformId, config); + } + + function getTransformUpdateConfig() { + return { + source: { + index: 'ft_*', + query: { + term: { + airline: { + value: 'AAL', + }, + }, + }, + }, + description: 'the-updated-description', + dest: { + index: 'user-the-updated-destination-index', + }, + frequency: '60m', + }; + } + + describe('/api/transform/transforms/{transformId}/_update', function () { + before(async () => { + await esArchiver.loadIfNeeded('ml/farequote'); + await transform.testResources.setKibanaTimeZoneToUTC(); + await createTransform('transform-test-update-1'); + }); + + after(async () => { + await transform.api.cleanTransformIndices(); + }); + + it('should update a transform', async () => { + // assert the original transform for comparison + const { body: transformOriginalBody } = await supertest + .get('/api/transform/transforms/transform-test-update-1') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + expect(transformOriginalBody.count).to.eql(expected.transformOriginalConfig.count); + expect(transformOriginalBody.transforms).to.have.length( + expected.transformOriginalConfig.count + ); + + const transformOriginalConfig = transformOriginalBody.transforms[0]; + expect(transformOriginalConfig.id).to.eql(expected.transformOriginalConfig.id); + expect(transformOriginalConfig.source).to.eql(expected.transformOriginalConfig.source); + expect(transformOriginalConfig.description).to.eql(undefined); + expect(transformOriginalConfig.settings).to.eql({}); + + // update the transform and assert the response + const { body: transformUpdateResponseBody } = await supertest + .post('/api/transform/transforms/transform-test-update-1/_update') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(getTransformUpdateConfig()) + .expect(200); + + const expectedUpdateConfig = getTransformUpdateConfig(); + expect(transformUpdateResponseBody.id).to.eql(expected.transformOriginalConfig.id); + expect(transformUpdateResponseBody.source).to.eql({ + ...expectedUpdateConfig.source, + index: ['ft_*'], + }); + expect(transformUpdateResponseBody.description).to.eql(expectedUpdateConfig.description); + expect(transformUpdateResponseBody.settings).to.eql({}); + + // assert the updated transform for comparison + const { body: transformUpdatedBody } = await supertest + .get('/api/transform/transforms/transform-test-update-1') + .auth( + USER.TRANSFORM_POWERUSER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_POWERUSER) + ) + .set(COMMON_REQUEST_HEADERS) + .send() + .expect(200); + + expect(transformUpdatedBody.count).to.eql(expected.transformOriginalConfig.count); + expect(transformUpdatedBody.transforms).to.have.length( + expected.transformOriginalConfig.count + ); + + const transformUpdatedConfig = transformUpdatedBody.transforms[0]; + expect(transformUpdatedConfig.id).to.eql(expected.transformOriginalConfig.id); + expect(transformUpdatedConfig.source).to.eql({ + ...expectedUpdateConfig.source, + index: ['ft_*'], + }); + expect(transformUpdatedConfig.description).to.eql(expectedUpdateConfig.description); + expect(transformUpdatedConfig.settings).to.eql({}); + }); + + it('should return 403 for transform view-only user', async () => { + await supertest + .post('/api/transform/transforms/transform-test-update-1/_update') + .auth( + USER.TRANSFORM_VIEWER, + transform.securityCommon.getPasswordForUser(USER.TRANSFORM_VIEWER) + ) + .set(COMMON_REQUEST_HEADERS) + .send(getTransformUpdateConfig()) + .expect(403); + }); + }); +}; diff --git a/x-pack/test/apm_api_integration/basic/tests/index.ts b/x-pack/test/apm_api_integration/basic/tests/index.ts index 33c00105e74f1..bae94d89e7457 100644 --- a/x-pack/test/apm_api_integration/basic/tests/index.ts +++ b/x-pack/test/apm_api_integration/basic/tests/index.ts @@ -4,9 +4,12 @@ * you may not use this file except in compliance with the Elastic License. */ import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registerMochaHooksForSnapshots } from '../../common/match_snapshot'; export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('APM specs (basic)', function () { + registerMochaHooksForSnapshots(); + this.tags('ciGroup1'); loadTestFile(require.resolve('./feature_controls')); diff --git a/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts b/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts index bd8b0c6126faa..96ac3c3a5e494 100644 --- a/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts +++ b/x-pack/test/apm_api_integration/basic/tests/observability_overview/observability_overview.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { @@ -22,7 +23,12 @@ export default function ApiTest({ getService }: FtrProviderContext) { `/api/apm/observability_overview?start=${start}&end=${end}&bucketSize=${bucketSize}` ); expect(response.status).to.be(200); - expect(response.body).to.eql({ serviceCount: 0, transactionCoordinates: [] }); + expectSnapshot(response.body).toMatchInline(` + Object { + "serviceCount": 0, + "transactionCoordinates": Array [], + } + `); }); }); describe('when data is loaded', () => { @@ -34,13 +40,21 @@ export default function ApiTest({ getService }: FtrProviderContext) { `/api/apm/observability_overview?start=${start}&end=${end}&bucketSize=${bucketSize}` ); expect(response.status).to.be(200); - expect(response.body).to.eql({ - serviceCount: 3, - transactionCoordinates: [ - { x: 1593413220000, y: 0.016666666666666666 }, - { x: 1593413280000, y: 1.0458333333333334 }, - ], - }); + expectSnapshot(response.body).toMatchInline(` + Object { + "serviceCount": 3, + "transactionCoordinates": Array [ + Object { + "x": 1593413220000, + "y": 0.016666666666666666, + }, + Object { + "x": 1593413280000, + "y": 1.0458333333333334, + }, + ], + } + `); }); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts b/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts index 5be5e43b359f5..a87d080e564a2 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/agent_name.ts @@ -6,14 +6,16 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import archives from '../../../common/archives_metadata'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); - // url parameters - const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); - const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + const archiveName = 'apm_8.0.0'; + const range = archives[archiveName]; + const start = encodeURIComponent(range.start); + const end = encodeURIComponent(range.end); describe('Agent name', () => { describe('when data is not loaded ', () => { @@ -28,8 +30,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when data is loaded', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); it('returns the agent name', async () => { const response = await supertest.get( diff --git a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts index ea3ed2539c12f..116b2987db32a 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/top_services.ts @@ -4,17 +4,25 @@ * you may not use this file except in compliance with the Elastic License. */ -import { sortBy } from 'lodash'; import expect from '@kbn/expect'; +import { isEmpty, pick } from 'lodash'; +import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; +import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import archives_metadata from '../../../common/archives_metadata'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const esArchiver = getService('esArchiver'); + const archiveName = 'apm_8.0.0'; + + const range = archives_metadata[archiveName]; + // url parameters - const start = encodeURIComponent('2020-06-29T06:45:00.000Z'); - const end = encodeURIComponent('2020-06-29T06:49:00.000Z'); + const start = encodeURIComponent(range.start); + const end = encodeURIComponent(range.end); + const uiFilters = encodeURIComponent(JSON.stringify({})); describe('APM Services Overview', () => { @@ -30,46 +38,189 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); describe('when data is loaded', () => { - before(() => esArchiver.load('8.0.0')); - after(() => esArchiver.unload('8.0.0')); + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); - it('returns a list of services', async () => { - const response = await supertest.get( - `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` - ); - // sort services to mitigate unstable sort order - const services = sortBy(response.body.items, ['serviceName']); + describe('and fetching a list of services', () => { + let response: PromiseReturnType; + before(async () => { + response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + }); - expect(response.status).to.be(200); - expect(services).to.eql([ - { - serviceName: 'client', - agentName: 'rum-js', - transactionsPerMinute: 2, - errorsPerMinute: 2.75, - avgResponseTime: 116375, - environments: [], - }, - { - serviceName: 'opbeans-java', - agentName: 'java', - transactionsPerMinute: 30.75, - errorsPerMinute: 4.5, - avgResponseTime: 25636.349593495936, - environments: ['production'], - }, - { - serviceName: 'opbeans-node', - agentName: 'nodejs', - transactionsPerMinute: 31, - errorsPerMinute: 3.75, - avgResponseTime: 38682.52419354839, - environments: ['production'], - }, - ]); - - expect(response.body.hasHistoricalData).to.be(true); - expect(response.body.hasLegacyData).to.be(false); + it('the response is successful', () => { + expect(response.status).to.eql(200); + }); + + it('returns hasHistoricalData: true', () => { + expect(response.body.hasHistoricalData).to.be(true); + }); + + it('returns hasLegacyData: false', () => { + expect(response.body.hasLegacyData).to.be(false); + }); + + it('returns the correct service names', () => { + expectSnapshot(response.body.items.map((item: any) => item.serviceName)).toMatchInline(` + Array [ + "opbeans-python", + "opbeans-node", + "opbeans-ruby", + "opbeans-go", + "opbeans-dotnet", + "opbeans-java", + "opbeans-rum", + ] + `); + }); + + it('returns the correct metrics averages', () => { + expectSnapshot( + response.body.items.map((item: any) => + pick( + item, + 'transactionErrorRate.value', + 'avgResponseTime.value', + 'transactionsPerMinute.value' + ) + ) + ).toMatchInline(` + Array [ + Object { + "avgResponseTime": Object { + "value": 208079.9121184089, + }, + "transactionErrorRate": Object { + "value": 0.041666666666666664, + }, + "transactionsPerMinute": Object { + "value": 18.016666666666666, + }, + }, + Object { + "avgResponseTime": Object { + "value": 578297.1431623931, + }, + "transactionErrorRate": Object { + "value": 0.03317535545023697, + }, + "transactionsPerMinute": Object { + "value": 7.8, + }, + }, + Object { + "avgResponseTime": Object { + "value": 60518.587926509186, + }, + "transactionErrorRate": Object { + "value": 0.013123359580052493, + }, + "transactionsPerMinute": Object { + "value": 6.35, + }, + }, + Object { + "avgResponseTime": Object { + "value": 25259.78717201166, + }, + "transactionErrorRate": Object { + "value": 0.014577259475218658, + }, + "transactionsPerMinute": Object { + "value": 5.716666666666667, + }, + }, + Object { + "avgResponseTime": Object { + "value": 527290.3218390804, + }, + "transactionErrorRate": Object { + "value": 0.01532567049808429, + }, + "transactionsPerMinute": Object { + "value": 4.35, + }, + }, + Object { + "avgResponseTime": Object { + "value": 530245.8571428572, + }, + "transactionErrorRate": Object { + "value": 0.15384615384615385, + }, + "transactionsPerMinute": Object { + "value": 3.033333333333333, + }, + }, + Object { + "avgResponseTime": Object { + "value": 896134.328358209, + }, + "transactionsPerMinute": Object { + "value": 2.2333333333333334, + }, + }, + ] + `); + }); + + it('returns environments', () => { + expectSnapshot(response.body.items.map((item: any) => item.environments ?? [])) + .toMatchInline(` + Array [ + Array [ + "production", + ], + Array [ + "testing", + ], + Array [ + "production", + ], + Array [ + "testing", + ], + Array [ + "production", + ], + Array [ + "production", + ], + Array [ + "testing", + ], + ] + `); + }); + + it(`RUM services don't report any transaction error rates`, () => { + // RUM transactions don't have event.outcome set, + // so they should not have an error rate + + const rumServices = response.body.items.filter( + (item: any) => item.agentName === 'rum-js' + ); + + expect(rumServices.length).to.be.greaterThan(0); + + expect(rumServices.every((item: any) => isEmpty(item.transactionErrorRate?.value))); + }); + + it('non-RUM services all report transaction error rates', () => { + const nonRumServices = response.body.items.filter( + (item: any) => item.agentName !== 'rum-js' + ); + + expect( + nonRumServices.every((item: any) => { + return ( + typeof item.transactionErrorRate?.value === 'number' && + item.transactionErrorRate.timeseries.length > 0 + ); + }) + ).to.be(true); + }); }); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts b/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts index 3e8f320ad6b24..a6c6bad21a8b7 100644 --- a/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts +++ b/x-pack/test/apm_api_integration/basic/tests/services/transaction_types.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { @@ -23,7 +24,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.eql({ transactionTypes: [] }); + + expect(response.body.transactionTypes.length).to.be(0); }); }); @@ -37,7 +39,14 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.eql({ transactionTypes: ['request', 'Worker'] }); + expectSnapshot(response.body).toMatchInline(` + Object { + "transactionTypes": Array [ + "request", + "Worker", + ], + } + `); }); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts index d868a2a0e71cc..b178c27467c73 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/no_access_user.ts @@ -25,14 +25,18 @@ export default function apiTest({ getService }: FtrProviderContext) { describe('when calling the endpoint for listing jobs', () => { it('returns an error because the user does not have access', async () => { const { body } = await getAnomalyDetectionJobs(); - expect(body).to.eql({ statusCode: 404, error: 'Not Found', message: 'Not Found' }); + + expect(body.statusCode).to.be(404); + expect(body.error).to.be('Not Found'); }); }); describe('when calling create endpoint', () => { it('returns an error because the user does not have access', async () => { const { body } = await createAnomalyDetectionJobs(['production', 'staging']); - expect(body).to.eql({ statusCode: 404, error: 'Not Found', message: 'Not Found' }); + + expect(body.statusCode).to.be(404); + expect(body.error).to.be('Not Found'); }); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts index 070762a1d9446..60d9fcf7f09c4 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/read_user.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; export default function apiTest({ getService }: FtrProviderContext) { @@ -25,19 +26,21 @@ export default function apiTest({ getService }: FtrProviderContext) { describe('when calling the endpoint for listing jobs', () => { it('returns an error because the user is on basic license', async () => { const { body } = await getAnomalyDetectionJobs(); - expect(body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: - "To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning.", - }); + + expect(body.statusCode).to.be(403); + expect(body.error).to.be('Forbidden'); + + expectSnapshot(body.message).toMatchInline( + `"To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning."` + ); }); }); describe('when calling create endpoint', () => { it('returns an error because the user does not have access', async () => { const { body } = await createAnomalyDetectionJobs(['production', 'staging']); - expect(body).to.eql({ statusCode: 404, error: 'Not Found', message: 'Not Found' }); + expect(body.statusCode).to.be(404); + expect(body.error).to.be('Not Found'); }); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts index c7bd7f0c96fa4..d1dbd15f4dced 100644 --- a/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts +++ b/x-pack/test/apm_api_integration/basic/tests/settings/anomaly_detection/write_user.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; export default function apiTest({ getService }: FtrProviderContext) { @@ -25,24 +26,25 @@ export default function apiTest({ getService }: FtrProviderContext) { describe('when calling the endpoint for listing jobs', () => { it('returns an error because the user is on basic license', async () => { const { body } = await getAnomalyDetectionJobs(); - expect(body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: - "To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning.", - }); + + expect(body.statusCode).to.be(403); + expect(body.error).to.be('Forbidden'); + expectSnapshot(body.message).toMatchInline( + `"To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning."` + ); }); }); describe('when calling create endpoint', () => { it('returns an error because the user is on basic license', async () => { const { body } = await createAnomalyDetectionJobs(['production', 'staging']); - expect(body).to.eql({ - statusCode: 403, - error: 'Forbidden', - message: - "To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning.", - }); + + expect(body.statusCode).to.be(403); + expect(body.error).to.be('Forbidden'); + + expectSnapshot(body.message).toMatchInline( + `"To use anomaly detection, you must be subscribed to an Elastic Platinum license. With it, you'll be able to monitor your services with the aid of machine learning."` + ); }); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap b/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap new file mode 100644 index 0000000000000..5557e0828a338 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/traces/__snapshots__/top_traces.snap @@ -0,0 +1,303 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Top traces when data is loaded returns the correct buckets 1`] = ` +Array [ + Object { + "averageResponseTime": 2577, + "impact": 0, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET /throw-error", + }, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 3147, + "impact": 0.06552270160444405, + "key": Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#orders", + }, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 3392.5, + "impact": 0.09374344413758617, + "key": Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#order", + }, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 4713.5, + "impact": 0.24559517890858723, + "key": Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#product", + }, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 4757, + "impact": 0.25059559560997896, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET /api/products/:id/customers", + }, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 6787, + "impact": 0.4839483750082622, + "key": Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#products", + }, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 4749.666666666667, + "impact": 0.5227447114845778, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET /api/orders/:id", + }, + "transactionsPerMinute": 0.75, + }, + Object { + "averageResponseTime": 7624.5, + "impact": 0.5802207655235637, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET /api/orders", + }, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 5098, + "impact": 0.582807187955318, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET /api/stats", + }, + "transactionsPerMinute": 0.75, + }, + Object { + "averageResponseTime": 8181, + "impact": 0.6441916136689552, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET /api/types/:id", + }, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 20011, + "impact": 0.853921734857215, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "POST /api", + }, + "transactionsPerMinute": 0.25, + }, + Object { + "averageResponseTime": 6583, + "impact": 1.2172278724376455, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET /api/products", + }, + "transactionsPerMinute": 1, + }, + Object { + "averageResponseTime": 33097, + "impact": 1.6060533780113861, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET /api/products/top", + }, + "transactionsPerMinute": 0.25, + }, + Object { + "averageResponseTime": 4825, + "impact": 1.6450221426498186, + "key": Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#topProducts", + }, + "transactionsPerMinute": 1.75, + }, + Object { + "averageResponseTime": 35846, + "impact": 1.7640550505645587, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET /log-error", + }, + "transactionsPerMinute": 0.25, + }, + Object { + "averageResponseTime": 3742.153846153846, + "impact": 2.4998634943716573, + "key": Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#customerWhoBought", + }, + "transactionsPerMinute": 3.25, + }, + Object { + "averageResponseTime": 3492.9285714285716, + "impact": 2.5144049360435208, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET static file", + }, + "transactionsPerMinute": 3.5, + }, + Object { + "averageResponseTime": 26992.5, + "impact": 2.8066131947777255, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET /api/types", + }, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 13516.5, + "impact": 2.8112687551548836, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET /api/products/:id", + }, + "transactionsPerMinute": 1, + }, + Object { + "averageResponseTime": 20092, + "impact": 3.168195050736987, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET /api/customers", + }, + "transactionsPerMinute": 0.75, + }, + Object { + "averageResponseTime": 15535, + "impact": 3.275330415465657, + "key": Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#stats", + }, + "transactionsPerMinute": 1, + }, + Object { + "averageResponseTime": 32667.5, + "impact": 3.458966408120217, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET /log-message", + }, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 16690.75, + "impact": 3.541042213287889, + "key": Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#customers", + }, + "transactionsPerMinute": 1, + }, + Object { + "averageResponseTime": 33500, + "impact": 3.5546640380951287, + "key": Object { + "service.name": "client", + "transaction.name": "/customers", + }, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 77000, + "impact": 4.129424578484989, + "key": Object { + "service.name": "client", + "transaction.name": "/products", + }, + "transactionsPerMinute": 0.25, + }, + Object { + "averageResponseTime": 19370.6, + "impact": 5.270496679320978, + "key": Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#customer", + }, + "transactionsPerMinute": 1.25, + }, + Object { + "averageResponseTime": 81500, + "impact": 9.072365225837785, + "key": Object { + "service.name": "client", + "transaction.name": "/orders", + }, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 14419.42857142857, + "impact": 11.30657439844125, + "key": Object { + "service.name": "opbeans-java", + "transaction.name": "ResourceHttpRequestHandler", + }, + "transactionsPerMinute": 3.5, + }, + Object { + "averageResponseTime": 270684, + "impact": 15.261616628971955, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "POST /api/orders", + }, + "transactionsPerMinute": 0.25, + }, + Object { + "averageResponseTime": 36010.53846153846, + "impact": 26.61043592713186, + "key": Object { + "service.name": "opbeans-java", + "transaction.name": "DispatcherServlet#doGet", + }, + "transactionsPerMinute": 3.25, + }, + Object { + "averageResponseTime": 208000, + "impact": 35.56882613781033, + "key": Object { + "service.name": "client", + "transaction.name": "/dashboard", + }, + "transactionsPerMinute": 0.75, + }, + Object { + "averageResponseTime": 49816.15625, + "impact": 91.32732325394932, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET /api", + }, + "transactionsPerMinute": 8, + }, + Object { + "averageResponseTime": 1745009, + "impact": 100, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "Process payment", + }, + "transactionsPerMinute": 0.25, + }, +] +`; diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/expectation/top_traces.expectation.json b/x-pack/test/apm_api_integration/basic/tests/traces/expectation/top_traces.expectation.json deleted file mode 100644 index 4db040e92e7fa..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/traces/expectation/top_traces.expectation.json +++ /dev/null @@ -1,5160 +0,0 @@ -[ - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "Process payment" - }, - "averageResponseTime": 1745009, - "transactionsPerMinute": 0.25, - "impact": 100, - "sample": { - "@timestamp": "2020-06-29T06:48:29.892Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:39.379730Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "timestamp": { - "us": 1593413309892019 - }, - "trace": { - "id": "bc393b659bef63291b6fa08e6f1d3f14" - }, - "transaction": { - "duration": { - "us": 1745009 - }, - "id": "a58333df6d851cf1", - "name": "Process payment", - "result": "success", - "sampled": true, - "span_count": { - "started": 2 - }, - "type": "Worker" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "GET /api" - }, - "averageResponseTime": 49816.15625, - "transactionsPerMinute": 8, - "impact": 91.32732325394932, - "sample": { - "@timestamp": "2020-06-29T06:48:06.969Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:08.306961Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "0" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:06 GMT" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413286969018 - }, - "trace": { - "id": "87a828bcedd44d9e872d8f552fb04aa6" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 25229 - }, - "id": "b1843afd04271423", - "name": "GET /api", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 1 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/orders/474", - "original": "/api/orders/474", - "path": "/api/orders/474", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "client", - "transaction.name": "/dashboard" - }, - "averageResponseTime": 208000, - "transactionsPerMinute": 0.75, - "impact": 35.56882613781033, - "sample": { - "@timestamp": "2020-06-29T06:48:07.275Z", - "agent": { - "name": "rum-js", - "version": "5.2.0" - }, - "client": { - "ip": "172.18.0.8" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:08.291261Z" - }, - "http": { - "request": { - "referrer": "" - }, - "response": { - "decoded_body_size": 813, - "encoded_body_size": 813, - "transfer_size": 962 - } - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "language": { - "name": "javascript" - }, - "name": "client", - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.8" - }, - "timestamp": { - "us": 1593413287275113 - }, - "trace": { - "id": "ca86ffcac7753ec8733933bd8fd45d11" - }, - "transaction": { - "custom": { - "userConfig": { - "featureFlags": [ - "double-trouble", - "4423-hotfix" - ], - "showDashboard": true - } - }, - "duration": { - "us": 342000 - }, - "id": "c40f735132c8e864", - "marks": { - "agent": { - "domComplete": 335, - "domInteractive": 327, - "timeToFirstByte": 16 - }, - "navigationTiming": { - "connectEnd": 12, - "connectStart": 12, - "domComplete": 335, - "domContentLoadedEventEnd": 327, - "domContentLoadedEventStart": 327, - "domInteractive": 327, - "domLoading": 21, - "domainLookupEnd": 12, - "domainLookupStart": 10, - "fetchStart": 0, - "loadEventEnd": 335, - "loadEventStart": 335, - "requestStart": 12, - "responseEnd": 17, - "responseStart": 16 - } - }, - "name": "/dashboard", - "page": { - "referer": "", - "url": "http://opbeans-node:3000/dashboard" - }, - "sampled": true, - "span_count": { - "started": 9 - }, - "type": "page-load" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/dashboard", - "original": "http://opbeans-node:3000/dashboard", - "path": "/dashboard", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "arthur.dent@example.com", - "id": "1", - "name": "arthurdent" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "HeadlessChrome", - "original": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36", - "os": { - "name": "Linux" - }, - "version": "79.0.3945" - } - } - }, - { - "key": { - "service.name": "opbeans-java", - "transaction.name": "DispatcherServlet#doGet" - }, - "averageResponseTime": 36010.53846153846, - "transactionsPerMinute": 3.25, - "impact": 26.61043592713186, - "sample": { - "@timestamp": "2020-06-29T06:48:10.529Z", - "agent": { - "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", - "name": "java", - "version": "1.17.1-SNAPSHOT" - }, - "client": { - "ip": "172.18.0.9" - }, - "container": { - "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:15.757591Z" - }, - "host": { - "architecture": "amd64", - "hostname": "918ebbd99b4f", - "ip": "172.18.0.6", - "name": "918ebbd99b4f", - "os": { - "platform": "Linux" - } - }, - "http": { - "request": { - "headers": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate" - ], - "Host": [ - "172.18.0.6:3000" - ], - "User-Agent": [ - "Python/3.7 aiohttp/3.3.2" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "172.18.0.9" - } - }, - "response": { - "finished": true, - "headers_sent": false, - "status_code": 200 - }, - "version": "1.1" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "pid": 6, - "ppid": 1, - "title": "/opt/java/openjdk/bin/java" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "Servlet API" - }, - "language": { - "name": "Java", - "version": "11.0.7" - }, - "name": "opbeans-java", - "node": { - "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "runtime": { - "name": "Java", - "version": "11.0.7" - }, - "version": "None" - }, - "source": { - "ip": "172.18.0.9" - }, - "timestamp": { - "us": 1593413290529006 - }, - "trace": { - "id": "66e3db4cf016b138a43d319d15174891" - }, - "transaction": { - "duration": { - "us": 34366 - }, - "id": "7ea720a0175e7ffa", - "name": "DispatcherServlet#doGet", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "dropped": 0, - "started": 1 - }, - "type": "request" - }, - "url": { - "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/products", - "path": "/api/products", - "port": 3000, - "scheme": "http" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "Python/3.7 aiohttp/3.3.2" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "POST /api/orders" - }, - "averageResponseTime": 270684, - "transactionsPerMinute": 0.25, - "impact": 15.261616628971955, - "sample": { - "@timestamp": "2020-06-29T06:48:39.953Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:43.991549Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "body": { - "original": "[REDACTED]" - }, - "headers": { - "Accept": [ - "application/json" - ], - "Connection": [ - "close" - ], - "Content-Length": [ - "129" - ], - "Content-Type": [ - "application/json" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "post", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "13" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:40 GMT" - ], - "Etag": [ - "W/\"d-eEOWU4Cnr5DZ23ErRUeYu9oOIks\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413319953033 - }, - "trace": { - "id": "52b8fda5f6df745b990740ba18378620" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 270684 - }, - "id": "a3afc2a112e9c893", - "name": "POST /api/orders", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 16 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/orders", - "original": "/api/orders", - "path": "/api/orders", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "opbeans-java", - "transaction.name": "ResourceHttpRequestHandler" - }, - "averageResponseTime": 14419.42857142857, - "transactionsPerMinute": 3.5, - "impact": 11.30657439844125, - "sample": { - "@timestamp": "2020-06-29T06:48:06.640Z", - "agent": { - "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", - "name": "java", - "version": "1.17.1-SNAPSHOT" - }, - "client": { - "ip": "172.18.0.9" - }, - "container": { - "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:15.517678Z" - }, - "host": { - "architecture": "amd64", - "hostname": "918ebbd99b4f", - "ip": "172.18.0.6", - "name": "918ebbd99b4f", - "os": { - "platform": "Linux" - } - }, - "http": { - "request": { - "headers": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate" - ], - "Host": [ - "172.18.0.6:3000" - ], - "User-Agent": [ - "Python/3.7 aiohttp/3.3.2" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "172.18.0.9" - } - }, - "response": { - "finished": true, - "headers_sent": true, - "status_code": 404 - }, - "version": "1.1" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "pid": 6, - "ppid": 1, - "title": "/opt/java/openjdk/bin/java" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "Spring Web MVC", - "version": "5.0.6.RELEASE" - }, - "language": { - "name": "Java", - "version": "11.0.7" - }, - "name": "opbeans-java", - "node": { - "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "runtime": { - "name": "Java", - "version": "11.0.7" - }, - "version": "None" - }, - "source": { - "ip": "172.18.0.9" - }, - "timestamp": { - "us": 1593413286640008 - }, - "trace": { - "id": "81d8ffb0a39e755eed400f6486e15672" - }, - "transaction": { - "duration": { - "us": 2953 - }, - "id": "353d42a2f9046e99", - "name": "ResourceHttpRequestHandler", - "result": "HTTP 4xx", - "sampled": true, - "span_count": { - "dropped": 0, - "started": 0 - }, - "type": "request" - }, - "url": { - "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/types/3", - "path": "/api/types/3", - "port": 3000, - "scheme": "http" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "Python/3.7 aiohttp/3.3.2" - } - } - }, - { - "key": { - "service.name": "client", - "transaction.name": "/orders" - }, - "averageResponseTime": 81500, - "transactionsPerMinute": 0.5, - "impact": 9.072365225837785, - "sample": { - "@timestamp": "2020-06-29T06:48:29.296Z", - "agent": { - "name": "rum-js", - "version": "5.2.0" - }, - "client": { - "ip": "172.18.0.8" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:29.986555Z" - }, - "http": { - "request": { - "referrer": "" - } - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "language": { - "name": "javascript" - }, - "name": "client", - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.8" - }, - "timestamp": { - "us": 1593413309296660 - }, - "trace": { - "id": "978b56807e0b7a27cbc41a0dfb665f47" - }, - "transaction": { - "custom": { - "userConfig": { - "featureFlags": [ - "double-trouble", - "4423-hotfix" - ], - "showDashboard": true - } - }, - "duration": { - "us": 23000 - }, - "id": "c3801eadbdef5c7c", - "name": "/orders", - "page": { - "referer": "", - "url": "http://opbeans-node:3000/orders" - }, - "sampled": true, - "span_count": { - "started": 1 - }, - "type": "route-change" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/orders", - "original": "http://opbeans-node:3000/orders", - "path": "/orders", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "arthur.dent@example.com", - "id": "1", - "name": "arthurdent" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "HeadlessChrome", - "original": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36", - "os": { - "name": "Linux" - }, - "version": "79.0.3945" - } - } - }, - { - "key": { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#customer" - }, - "averageResponseTime": 19370.6, - "transactionsPerMinute": 1.25, - "impact": 5.270496679320978, - "sample": { - "@timestamp": "2020-06-29T06:48:08.631Z", - "agent": { - "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", - "name": "java", - "version": "1.17.1-SNAPSHOT" - }, - "client": { - "ip": "172.18.0.9" - }, - "container": { - "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:15.536897Z" - }, - "host": { - "architecture": "amd64", - "hostname": "918ebbd99b4f", - "ip": "172.18.0.6", - "name": "918ebbd99b4f", - "os": { - "platform": "Linux" - } - }, - "http": { - "request": { - "headers": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate" - ], - "Host": [ - "172.18.0.6:3000" - ], - "User-Agent": [ - "Python/3.7 aiohttp/3.3.2" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "172.18.0.9" - } - }, - "response": { - "finished": true, - "headers": { - "Content-Type": [ - "application/json;charset=UTF-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:08 GMT" - ], - "Transfer-Encoding": [ - "chunked" - ] - }, - "headers_sent": true, - "status_code": 200 - }, - "version": "1.1" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "pid": 6, - "ppid": 1, - "title": "/opt/java/openjdk/bin/java" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "Spring Web MVC", - "version": "5.0.6.RELEASE" - }, - "language": { - "name": "Java", - "version": "11.0.7" - }, - "name": "opbeans-java", - "node": { - "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "runtime": { - "name": "Java", - "version": "11.0.7" - }, - "version": "None" - }, - "source": { - "ip": "172.18.0.9" - }, - "timestamp": { - "us": 1593413288631008 - }, - "trace": { - "id": "c00da24c5c793cd679ce3df47cee8f37" - }, - "transaction": { - "duration": { - "us": 76826 - }, - "id": "3c8403055ff75866", - "name": "APIRestController#customer", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "dropped": 0, - "started": 2 - }, - "type": "request" - }, - "url": { - "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/customers/56", - "path": "/api/customers/56", - "port": 3000, - "scheme": "http" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "Python/3.7 aiohttp/3.3.2" - } - } - }, - { - "key": { - "service.name": "client", - "transaction.name": "/products" - }, - "averageResponseTime": 77000, - "transactionsPerMinute": 0.25, - "impact": 4.129424578484989, - "sample": { - "@timestamp": "2020-06-29T06:48:48.824Z", - "agent": { - "name": "rum-js", - "version": "5.2.0" - }, - "client": { - "ip": "172.18.0.8" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:49.293664Z" - }, - "http": { - "request": { - "referrer": "" - }, - "response": { - "decoded_body_size": 813, - "encoded_body_size": 813, - "transfer_size": 962 - } - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "language": { - "name": "javascript" - }, - "name": "client", - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.8" - }, - "timestamp": { - "us": 1593413328824656 - }, - "trace": { - "id": "f6c4a9197bbd080bd45072970f251525" - }, - "transaction": { - "custom": { - "userConfig": { - "featureFlags": [ - "double-trouble", - "4423-hotfix" - ], - "showDashboard": true - } - }, - "duration": { - "us": 77000 - }, - "id": "a11ede1968973bc5", - "marks": { - "agent": { - "domComplete": 68, - "domInteractive": 58, - "timeToFirstByte": 5 - }, - "navigationTiming": { - "connectEnd": 1, - "connectStart": 1, - "domComplete": 68, - "domContentLoadedEventEnd": 59, - "domContentLoadedEventStart": 59, - "domInteractive": 58, - "domLoading": 23, - "domainLookupEnd": 1, - "domainLookupStart": 1, - "fetchStart": 0, - "loadEventEnd": 68, - "loadEventStart": 68, - "requestStart": 2, - "responseEnd": 5, - "responseStart": 5 - } - }, - "name": "/products", - "page": { - "referer": "", - "url": "http://opbeans-node:3000/products" - }, - "sampled": true, - "span_count": { - "started": 5 - }, - "type": "page-load" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/products", - "original": "http://opbeans-node:3000/products", - "path": "/products", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "z@example.com", - "id": "4", - "name": "zaphod" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "HeadlessChrome", - "original": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36", - "os": { - "name": "Linux" - }, - "version": "79.0.3945" - } - } - }, - { - "key": { - "service.name": "client", - "transaction.name": "/customers" - }, - "averageResponseTime": 33500, - "transactionsPerMinute": 0.5, - "impact": 3.5546640380951287, - "sample": { - "@timestamp": "2020-06-29T06:48:35.071Z", - "agent": { - "name": "rum-js", - "version": "5.2.0" - }, - "client": { - "ip": "172.18.0.8" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:36.077184Z" - }, - "http": { - "request": { - "referrer": "" - } - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "language": { - "name": "javascript" - }, - "name": "client", - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.8" - }, - "timestamp": { - "us": 1593413315071116 - }, - "trace": { - "id": "547a92e82a25387321d1b967f2dd0f48" - }, - "transaction": { - "custom": { - "userConfig": { - "featureFlags": [ - "double-trouble", - "4423-hotfix" - ], - "showDashboard": true - } - }, - "duration": { - "us": 28000 - }, - "id": "d24f9b9dacb83450", - "name": "/customers", - "page": { - "referer": "", - "url": "http://opbeans-node:3000/customers" - }, - "sampled": true, - "span_count": { - "started": 1 - }, - "type": "route-change" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/customers", - "original": "http://opbeans-node:3000/customers", - "path": "/customers", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "arthur.dent@example.com", - "id": "1", - "name": "arthurdent" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "HeadlessChrome", - "original": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36", - "os": { - "name": "Linux" - }, - "version": "79.0.3945" - } - } - }, - { - "key": { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#customers" - }, - "averageResponseTime": 16690.75, - "transactionsPerMinute": 1, - "impact": 3.541042213287889, - "sample": { - "@timestamp": "2020-06-29T06:48:22.372Z", - "agent": { - "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", - "name": "java", - "version": "1.17.1-SNAPSHOT" - }, - "client": { - "ip": "172.18.0.9" - }, - "container": { - "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:25.888154Z" - }, - "host": { - "architecture": "amd64", - "hostname": "918ebbd99b4f", - "ip": "172.18.0.6", - "name": "918ebbd99b4f", - "os": { - "platform": "Linux" - } - }, - "http": { - "request": { - "headers": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate" - ], - "Host": [ - "172.18.0.6:3000" - ], - "User-Agent": [ - "Python/3.7 aiohttp/3.3.2" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "172.18.0.9" - } - }, - "response": { - "finished": true, - "headers": { - "Content-Type": [ - "application/json;charset=UTF-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:21 GMT" - ], - "Transfer-Encoding": [ - "chunked" - ] - }, - "headers_sent": true, - "status_code": 500 - }, - "version": "1.1" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "pid": 6, - "ppid": 1, - "title": "/opt/java/openjdk/bin/java" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "Spring Web MVC", - "version": "5.0.6.RELEASE" - }, - "language": { - "name": "Java", - "version": "11.0.7" - }, - "name": "opbeans-java", - "node": { - "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "runtime": { - "name": "Java", - "version": "11.0.7" - }, - "version": "None" - }, - "source": { - "ip": "172.18.0.9" - }, - "timestamp": { - "us": 1593413302372009 - }, - "trace": { - "id": "21dd795dc3a260b1bf7ebbbac1e86fb8" - }, - "transaction": { - "duration": { - "us": 14795 - }, - "id": "0157fc513282138f", - "name": "APIRestController#customers", - "result": "HTTP 5xx", - "sampled": true, - "span_count": { - "dropped": 0, - "started": 2 - }, - "type": "request" - }, - "url": { - "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/customers", - "path": "/api/customers", - "port": 3000, - "scheme": "http" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "Python/3.7 aiohttp/3.3.2" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "GET /log-message" - }, - "averageResponseTime": 32667.5, - "transactionsPerMinute": 0.5, - "impact": 3.458966408120217, - "sample": { - "@timestamp": "2020-06-29T06:48:25.944Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:29.976822Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "24" - ], - "Content-Type": [ - "text/html; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:25 GMT" - ], - "Etag": [ - "W/\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 500 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413305944023 - }, - "trace": { - "id": "cd2ad726ad164d701c5d3103cbab0c81" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 38547 - }, - "id": "9e41667eb64dea55", - "name": "GET /log-message", - "result": "HTTP 5xx", - "sampled": true, - "span_count": { - "started": 0 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/log-message", - "original": "/log-message", - "path": "/log-message", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#stats" - }, - "averageResponseTime": 15535, - "transactionsPerMinute": 1, - "impact": 3.275330415465657, - "sample": { - "@timestamp": "2020-06-29T06:48:09.912Z", - "agent": { - "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", - "name": "java", - "version": "1.17.1-SNAPSHOT" - }, - "client": { - "ip": "172.18.0.9" - }, - "container": { - "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:15.543824Z" - }, - "host": { - "architecture": "amd64", - "hostname": "918ebbd99b4f", - "ip": "172.18.0.6", - "name": "918ebbd99b4f", - "os": { - "platform": "Linux" - } - }, - "http": { - "request": { - "headers": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate" - ], - "Host": [ - "172.18.0.6:3000" - ], - "User-Agent": [ - "Python/3.7 aiohttp/3.3.2" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "172.18.0.9" - } - }, - "response": { - "finished": true, - "headers_sent": true, - "status_code": 500 - }, - "version": "1.1" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "pid": 6, - "ppid": 1, - "title": "/opt/java/openjdk/bin/java" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "Spring Web MVC", - "version": "5.0.6.RELEASE" - }, - "language": { - "name": "Java", - "version": "11.0.7" - }, - "name": "opbeans-java", - "node": { - "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "runtime": { - "name": "Java", - "version": "11.0.7" - }, - "version": "None" - }, - "source": { - "ip": "172.18.0.9" - }, - "timestamp": { - "us": 1593413289912007 - }, - "trace": { - "id": "a17ceae4e18d50430ca15ecca5a3e69f" - }, - "transaction": { - "duration": { - "us": 10930 - }, - "id": "9fb330060bb73271", - "name": "APIRestController#stats", - "result": "HTTP 5xx", - "sampled": true, - "span_count": { - "dropped": 0, - "started": 5 - }, - "type": "request" - }, - "url": { - "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/stats", - "path": "/api/stats", - "port": 3000, - "scheme": "http" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "Python/3.7 aiohttp/3.3.2" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "GET /api/customers" - }, - "averageResponseTime": 20092, - "transactionsPerMinute": 0.75, - "impact": 3.168195050736987, - "sample": { - "@timestamp": "2020-06-29T06:48:28.444Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:29.982737Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "186769" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:28 GMT" - ], - "Etag": [ - "W/\"2d991-yG3J8W/roH7fSxXTudZrO27Ax9s\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413308444015 - }, - "trace": { - "id": "792fb0b00256164e88b277ec40b65e14" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 26471 - }, - "id": "6c1f848752563d2b", - "name": "GET /api/customers", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 2 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/customers", - "original": "/api/customers", - "path": "/api/customers", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products/:id" - }, - "averageResponseTime": 13516.5, - "transactionsPerMinute": 1, - "impact": 2.8112687551548836, - "sample": { - "@timestamp": "2020-06-29T06:47:57.555Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:47:59.085077Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "231" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:47:57 GMT" - ], - "Etag": [ - "W/\"e7-6JlJegaJ+ir0C8I8EmmOjms1dnc\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 87, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413277555176 - }, - "trace": { - "id": "8365e1763f19e4067b88521d4d9247a0" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 37709 - }, - "id": "be2722a418272f10", - "name": "GET /api/products/:id", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 1 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/products/1", - "original": "/api/products/1", - "path": "/api/products/1", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "GET /api/types" - }, - "averageResponseTime": 26992.5, - "transactionsPerMinute": 0.5, - "impact": 2.8066131947777255, - "sample": { - "@timestamp": "2020-06-29T06:47:52.935Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:47:55.471071Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "112" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:47:52 GMT" - ], - "Etag": [ - "W/\"70-1z6hT7P1WHgBgS/BeUEVeHhOCQU\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 63, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413272935117 - }, - "trace": { - "id": "2946c536a33d163d0c984d00d1f3839a" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 45093 - }, - "id": "103482fda88b9400", - "name": "GET /api/types", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 2 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/types", - "original": "/api/types", - "path": "/api/types", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "GET static file" - }, - "averageResponseTime": 3492.9285714285716, - "transactionsPerMinute": 3.5, - "impact": 2.5144049360435208, - "sample": { - "@timestamp": "2020-06-29T06:47:53.427Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:47:55.472070Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Accept-Ranges": [ - "bytes" - ], - "Cache-Control": [ - "public, max-age=0" - ], - "Connection": [ - "close" - ], - "Content-Length": [ - "15086" - ], - "Content-Type": [ - "image/x-icon" - ], - "Date": [ - "Mon, 29 Jun 2020 06:47:53 GMT" - ], - "Etag": [ - "W/\"3aee-1725aff14f0\"" - ], - "Last-Modified": [ - "Thu, 28 May 2020 11:16:06 GMT" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 63, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413273427016 - }, - "trace": { - "id": "ec8a804fedf28fcf81d5682d69a16970" - }, - "transaction": { - "duration": { - "us": 4934 - }, - "id": "ab90a62901b770e6", - "name": "GET static file", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 0 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/favicon.ico", - "original": "/favicon.ico", - "path": "/favicon.ico", - "port": 3000, - "scheme": "http" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#customerWhoBought" - }, - "averageResponseTime": 3742.153846153846, - "transactionsPerMinute": 3.25, - "impact": 2.4998634943716573, - "sample": { - "@timestamp": "2020-06-29T06:48:11.166Z", - "agent": { - "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", - "name": "java", - "version": "1.17.1-SNAPSHOT" - }, - "client": { - "ip": "172.18.0.9" - }, - "container": { - "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:15.763228Z" - }, - "host": { - "architecture": "amd64", - "hostname": "918ebbd99b4f", - "ip": "172.18.0.6", - "name": "918ebbd99b4f", - "os": { - "platform": "Linux" - } - }, - "http": { - "request": { - "headers": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate" - ], - "Host": [ - "172.18.0.6:3000" - ], - "User-Agent": [ - "Python/3.7 aiohttp/3.3.2" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "172.18.0.9" - } - }, - "response": { - "finished": true, - "headers": { - "Content-Type": [ - "application/json;charset=UTF-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:10 GMT" - ], - "Transfer-Encoding": [ - "chunked" - ] - }, - "headers_sent": true, - "status_code": 200 - }, - "version": "1.1" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "pid": 6, - "ppid": 1, - "title": "/opt/java/openjdk/bin/java" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "Spring Web MVC", - "version": "5.0.6.RELEASE" - }, - "language": { - "name": "Java", - "version": "11.0.7" - }, - "name": "opbeans-java", - "node": { - "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "runtime": { - "name": "Java", - "version": "11.0.7" - }, - "version": "None" - }, - "source": { - "ip": "172.18.0.9" - }, - "timestamp": { - "us": 1593413291166005 - }, - "trace": { - "id": "fa0d353eb7967b344ed37674f40b2884" - }, - "transaction": { - "duration": { - "us": 4453 - }, - "id": "bce4ce4b09ded6ca", - "name": "APIRestController#customerWhoBought", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "dropped": 0, - "started": 2 - }, - "type": "request" - }, - "url": { - "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/products/3/customers", - "path": "/api/products/3/customers", - "port": 3000, - "scheme": "http" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "Python/3.7 aiohttp/3.3.2" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "GET /log-error" - }, - "averageResponseTime": 35846, - "transactionsPerMinute": 0.25, - "impact": 1.7640550505645587, - "sample": { - "@timestamp": "2020-06-29T06:48:07.467Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:18.533253Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "24" - ], - "Content-Type": [ - "text/html; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:07 GMT" - ], - "Etag": [ - "W/\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 500 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413287467017 - }, - "trace": { - "id": "d518b2c4d72cd2aaf1e39bad7ebcbdbb" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 35846 - }, - "id": "c7a30c1b076907ec", - "name": "GET /log-error", - "result": "HTTP 5xx", - "sampled": true, - "span_count": { - "started": 0 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/log-error", - "original": "/log-error", - "path": "/log-error", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#topProducts" - }, - "averageResponseTime": 4825, - "transactionsPerMinute": 1.75, - "impact": 1.6450221426498186, - "sample": { - "@timestamp": "2020-06-29T06:48:11.778Z", - "agent": { - "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", - "name": "java", - "version": "1.17.1-SNAPSHOT" - }, - "client": { - "ip": "172.18.0.9" - }, - "container": { - "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:15.764351Z" - }, - "host": { - "architecture": "amd64", - "hostname": "918ebbd99b4f", - "ip": "172.18.0.6", - "name": "918ebbd99b4f", - "os": { - "platform": "Linux" - } - }, - "http": { - "request": { - "headers": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate" - ], - "Host": [ - "172.18.0.6:3000" - ], - "User-Agent": [ - "Python/3.7 aiohttp/3.3.2" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "172.18.0.9" - } - }, - "response": { - "finished": true, - "headers": { - "Content-Type": [ - "application/json;charset=UTF-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:11 GMT" - ], - "Transfer-Encoding": [ - "chunked" - ] - }, - "headers_sent": true, - "status_code": 200 - }, - "version": "1.1" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "pid": 6, - "ppid": 1, - "title": "/opt/java/openjdk/bin/java" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "Spring Web MVC", - "version": "5.0.6.RELEASE" - }, - "language": { - "name": "Java", - "version": "11.0.7" - }, - "name": "opbeans-java", - "node": { - "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "runtime": { - "name": "Java", - "version": "11.0.7" - }, - "version": "None" - }, - "source": { - "ip": "172.18.0.9" - }, - "timestamp": { - "us": 1593413291778008 - }, - "trace": { - "id": "d65e9816f1f6db3961867f7b6d1d4e6a" - }, - "transaction": { - "duration": { - "us": 4168 - }, - "id": "a72f4bb8149ecdc5", - "name": "APIRestController#topProducts", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "dropped": 0, - "started": 2 - }, - "type": "request" - }, - "url": { - "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/products/top", - "path": "/api/products/top", - "port": 3000, - "scheme": "http" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "Python/3.7 aiohttp/3.3.2" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products/top" - }, - "averageResponseTime": 33097, - "transactionsPerMinute": 0.25, - "impact": 1.6060533780113861, - "sample": { - "@timestamp": "2020-06-29T06:48:01.200Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:02.734903Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "2" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:01 GMT" - ], - "Etag": [ - "W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 115, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413281200133 - }, - "trace": { - "id": "195f32efeb6f91e2f71b6bc8bb74ae3a" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 33097 - }, - "id": "22e72956dfc8967a", - "name": "GET /api/products/top", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 1 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/products/top", - "original": "/api/products/top", - "path": "/api/products/top", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products" - }, - "averageResponseTime": 6583, - "transactionsPerMinute": 1, - "impact": 1.2172278724376455, - "sample": { - "@timestamp": "2020-06-29T06:48:21.475Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:26.996210Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "1023" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:21 GMT" - ], - "Etag": [ - "W/\"3ff-VyOxcDApb+a/lnjkm9FeTOGSDrs\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413301475015 - }, - "trace": { - "id": "389b26b16949c7f783223de4f14b788c" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 6775 - }, - "id": "d2d4088a0b104fb4", - "name": "GET /api/products", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 2 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/products", - "original": "/api/products", - "path": "/api/products", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "POST /api" - }, - "averageResponseTime": 20011, - "transactionsPerMinute": 0.25, - "impact": 0.853921734857215, - "sample": { - "@timestamp": "2020-06-29T06:48:25.478Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:27.005671Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "body": { - "original": "[REDACTED]" - }, - "headers": { - "Accept": [ - "application/json" - ], - "Connection": [ - "close" - ], - "Content-Length": [ - "129" - ], - "Content-Type": [ - "application/json" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "post", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Allow": [ - "GET" - ], - "Connection": [ - "close" - ], - "Content-Type": [ - "application/json;charset=UTF-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:25 GMT" - ], - "Transfer-Encoding": [ - "chunked" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 405 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413305478010 - }, - "trace": { - "id": "4bd9027dd1e355ec742970e2d6333124" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 20011 - }, - "id": "94104435cf151478", - "name": "POST /api", - "result": "HTTP 4xx", - "sampled": true, - "span_count": { - "started": 1 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/orders", - "original": "/api/orders", - "path": "/api/orders", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "GET /api/types/:id" - }, - "averageResponseTime": 8181, - "transactionsPerMinute": 0.5, - "impact": 0.6441916136689552, - "sample": { - "@timestamp": "2020-06-29T06:47:53.928Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:47:55.472718Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "205" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:47:53 GMT" - ], - "Etag": [ - "W/\"cd-pFMi1QOVY6YqWe+nwcbZVviCths\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 63, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413273928016 - }, - "trace": { - "id": "0becaafb422bfeb69e047bf7153aa469" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 10062 - }, - "id": "0cee4574091bda3b", - "name": "GET /api/types/:id", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 2 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/types/2", - "original": "/api/types/2", - "path": "/api/types/2", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "GET /api/stats" - }, - "averageResponseTime": 5098, - "transactionsPerMinute": 0.75, - "impact": 0.582807187955318, - "sample": { - "@timestamp": "2020-06-29T06:48:34.949Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:39.479316Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "92" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:34 GMT" - ], - "Etag": [ - "W/\"5c-6I+bqIiLxvkWuwBUnTxhBoK4lBk\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413314949017 - }, - "trace": { - "id": "616b3b77abd5534c61d6c0438469aee2" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 5459 - }, - "id": "5b4971de59d2099d", - "name": "GET /api/stats", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 4 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/stats", - "original": "/api/stats", - "path": "/api/stats", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "GET /api/orders" - }, - "averageResponseTime": 7624.5, - "transactionsPerMinute": 0.5, - "impact": 0.5802207655235637, - "sample": { - "@timestamp": "2020-06-29T06:48:35.450Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:39.483715Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "2" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:35 GMT" - ], - "Etag": [ - "W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413315450014 - }, - "trace": { - "id": "2da70ccf10599b271f65273d169cde9f" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 8784 - }, - "id": "a3f4a4f339758440", - "name": "GET /api/orders", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 2 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/orders", - "original": "/api/orders", - "path": "/api/orders", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "GET /api/orders/:id" - }, - "averageResponseTime": 4749.666666666667, - "transactionsPerMinute": 0.75, - "impact": 0.5227447114845778, - "sample": { - "@timestamp": "2020-06-29T06:48:35.951Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:39.484133Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "0" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:35 GMT" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 404 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413315951017 - }, - "trace": { - "id": "95979caa80e6622cbbb2d308800c3016" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 3210 - }, - "id": "30344988dace0b43", - "name": "GET /api/orders/:id", - "result": "HTTP 4xx", - "sampled": true, - "span_count": { - "started": 1 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/orders/117", - "original": "/api/orders/117", - "path": "/api/orders/117", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#products" - }, - "averageResponseTime": 6787, - "transactionsPerMinute": 0.5, - "impact": 0.4839483750082622, - "sample": { - "@timestamp": "2020-06-29T06:48:13.595Z", - "agent": { - "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", - "name": "java", - "version": "1.17.1-SNAPSHOT" - }, - "client": { - "ip": "172.18.0.9" - }, - "container": { - "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:15.755614Z" - }, - "host": { - "architecture": "amd64", - "hostname": "918ebbd99b4f", - "ip": "172.18.0.6", - "name": "918ebbd99b4f", - "os": { - "platform": "Linux" - } - }, - "http": { - "request": { - "headers": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate" - ], - "Host": [ - "172.18.0.6:3000" - ], - "User-Agent": [ - "Python/3.7 aiohttp/3.3.2" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "172.18.0.9" - } - }, - "response": { - "finished": true, - "headers": { - "Content-Type": [ - "application/json;charset=UTF-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:12 GMT" - ], - "Transfer-Encoding": [ - "chunked" - ] - }, - "headers_sent": true, - "status_code": 200 - }, - "version": "1.1" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "pid": 6, - "ppid": 1, - "title": "/opt/java/openjdk/bin/java" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "Spring Web MVC", - "version": "5.0.6.RELEASE" - }, - "language": { - "name": "Java", - "version": "11.0.7" - }, - "name": "opbeans-java", - "node": { - "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "runtime": { - "name": "Java", - "version": "11.0.7" - }, - "version": "None" - }, - "source": { - "ip": "172.18.0.9" - }, - "timestamp": { - "us": 1593413293595007 - }, - "trace": { - "id": "8519b6c3dbc32a0582228506526e1d74" - }, - "transaction": { - "duration": { - "us": 7929 - }, - "id": "b0354de660cd3698", - "name": "APIRestController#products", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "dropped": 0, - "started": 3 - }, - "type": "request" - }, - "url": { - "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/products", - "path": "/api/products", - "port": 3000, - "scheme": "http" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "Python/3.7 aiohttp/3.3.2" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "GET /api/products/:id/customers" - }, - "averageResponseTime": 4757, - "transactionsPerMinute": 0.5, - "impact": 0.25059559560997896, - "sample": { - "@timestamp": "2020-06-29T06:48:22.977Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:27.000765Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "2" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:22 GMT" - ], - "Etag": [ - "W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413302977008 - }, - "trace": { - "id": "da8f22fe652ccb6680b3029ab6efd284" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 5618 - }, - "id": "bc51c1523afaf57a", - "name": "GET /api/products/:id/customers", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 1 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/products/3/customers", - "original": "/api/products/3/customers", - "path": "/api/products/3/customers", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#product" - }, - "averageResponseTime": 4713.5, - "transactionsPerMinute": 0.5, - "impact": 0.24559517890858723, - "sample": { - "@timestamp": "2020-06-29T06:48:36.383Z", - "agent": { - "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", - "name": "java", - "version": "1.17.1-SNAPSHOT" - }, - "client": { - "ip": "172.18.0.9" - }, - "container": { - "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:46.666467Z" - }, - "host": { - "architecture": "amd64", - "hostname": "918ebbd99b4f", - "ip": "172.18.0.6", - "name": "918ebbd99b4f", - "os": { - "platform": "Linux" - } - }, - "http": { - "request": { - "headers": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate" - ], - "Host": [ - "172.18.0.6:3000" - ], - "User-Agent": [ - "Python/3.7 aiohttp/3.3.2" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "172.18.0.9" - } - }, - "response": { - "finished": true, - "headers": { - "Content-Type": [ - "application/json;charset=UTF-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:36 GMT" - ], - "Transfer-Encoding": [ - "chunked" - ] - }, - "headers_sent": true, - "status_code": 200 - }, - "version": "1.1" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "pid": 6, - "ppid": 1, - "title": "/opt/java/openjdk/bin/java" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "Spring Web MVC", - "version": "5.0.6.RELEASE" - }, - "language": { - "name": "Java", - "version": "11.0.7" - }, - "name": "opbeans-java", - "node": { - "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "runtime": { - "name": "Java", - "version": "11.0.7" - }, - "version": "None" - }, - "source": { - "ip": "172.18.0.9" - }, - "timestamp": { - "us": 1593413316383008 - }, - "trace": { - "id": "386b450aef87fc079b20136eda542af1" - }, - "transaction": { - "duration": { - "us": 4888 - }, - "id": "5a4aa02158b5658c", - "name": "APIRestController#product", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "dropped": 0, - "started": 3 - }, - "type": "request" - }, - "url": { - "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/products/1", - "path": "/api/products/1", - "port": 3000, - "scheme": "http" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "Python/3.7 aiohttp/3.3.2" - } - } - }, - { - "key": { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#order" - }, - "averageResponseTime": 3392.5, - "transactionsPerMinute": 0.5, - "impact": 0.09374344413758617, - "sample": { - "@timestamp": "2020-06-29T06:48:07.416Z", - "agent": { - "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", - "name": "java", - "version": "1.17.1-SNAPSHOT" - }, - "client": { - "ip": "172.18.0.9" - }, - "container": { - "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:15.534378Z" - }, - "host": { - "architecture": "amd64", - "hostname": "918ebbd99b4f", - "ip": "172.18.0.6", - "name": "918ebbd99b4f", - "os": { - "platform": "Linux" - } - }, - "http": { - "request": { - "headers": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate" - ], - "Host": [ - "172.18.0.6:3000" - ], - "User-Agent": [ - "Python/3.7 aiohttp/3.3.2" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "172.18.0.9" - } - }, - "response": { - "finished": true, - "headers_sent": false, - "status_code": 200 - }, - "version": "1.1" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "pid": 6, - "ppid": 1, - "title": "/opt/java/openjdk/bin/java" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "Spring Web MVC", - "version": "5.0.6.RELEASE" - }, - "language": { - "name": "Java", - "version": "11.0.7" - }, - "name": "opbeans-java", - "node": { - "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "runtime": { - "name": "Java", - "version": "11.0.7" - }, - "version": "None" - }, - "source": { - "ip": "172.18.0.9" - }, - "timestamp": { - "us": 1593413287416007 - }, - "trace": { - "id": "25c46380df3d44a192ed07279a08b329" - }, - "transaction": { - "duration": { - "us": 4282 - }, - "id": "d4d5b23c685d2ee5", - "name": "APIRestController#order", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "dropped": 0, - "started": 1 - }, - "type": "request" - }, - "url": { - "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/orders/391", - "path": "/api/orders/391", - "port": 3000, - "scheme": "http" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "Python/3.7 aiohttp/3.3.2" - } - } - }, - { - "key": { - "service.name": "opbeans-java", - "transaction.name": "APIRestController#orders" - }, - "averageResponseTime": 3147, - "transactionsPerMinute": 0.5, - "impact": 0.06552270160444405, - "sample": { - "@timestamp": "2020-06-29T06:48:16.028Z", - "agent": { - "ephemeral_id": "222af346-6dd9-45ef-ac85-d86b67edd2de", - "name": "java", - "version": "1.17.1-SNAPSHOT" - }, - "client": { - "ip": "172.18.0.9" - }, - "container": { - "id": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:25.800962Z" - }, - "host": { - "architecture": "amd64", - "hostname": "918ebbd99b4f", - "ip": "172.18.0.6", - "name": "918ebbd99b4f", - "os": { - "platform": "Linux" - } - }, - "http": { - "request": { - "headers": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate" - ], - "Host": [ - "172.18.0.6:3000" - ], - "User-Agent": [ - "Python/3.7 aiohttp/3.3.2" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "172.18.0.9" - } - }, - "response": { - "finished": true, - "headers": { - "Content-Type": [ - "application/json;charset=UTF-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:15 GMT" - ], - "Transfer-Encoding": [ - "chunked" - ] - }, - "headers_sent": true, - "status_code": 200 - }, - "version": "1.1" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "pid": 6, - "ppid": 1, - "title": "/opt/java/openjdk/bin/java" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "Spring Web MVC", - "version": "5.0.6.RELEASE" - }, - "language": { - "name": "Java", - "version": "11.0.7" - }, - "name": "opbeans-java", - "node": { - "name": "918ebbd99b4f40003cf5713c080bb8120fa3bbe7ac4a96acb3aec558ced91ec0" - }, - "runtime": { - "name": "Java", - "version": "11.0.7" - }, - "version": "None" - }, - "source": { - "ip": "172.18.0.9" - }, - "timestamp": { - "us": 1593413296028008 - }, - "trace": { - "id": "4110227ecacbccf79894165ae5df932d" - }, - "transaction": { - "duration": { - "us": 2903 - }, - "id": "8e3732f0f0da942b", - "name": "APIRestController#orders", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "dropped": 0, - "started": 1 - }, - "type": "request" - }, - "url": { - "domain": "172.18.0.6", - "full": "http://172.18.0.6:3000/api/orders", - "path": "/api/orders", - "port": 3000, - "scheme": "http" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "Python/3.7 aiohttp/3.3.2" - } - } - }, - { - "key": { - "service.name": "opbeans-node", - "transaction.name": "GET /throw-error" - }, - "averageResponseTime": 2577, - "transactionsPerMinute": 0.5, - "impact": 0, - "sample": { - "@timestamp": "2020-06-29T06:48:19.975Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:21.012520Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "148" - ], - "Content-Security-Policy": [ - "default-src 'none'" - ], - "Content-Type": [ - "text/html; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:19 GMT" - ], - "X-Content-Type-Options": [ - "nosniff" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 500 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413299975019 - }, - "trace": { - "id": "106f3a55b0b0ea327d1bbe4be66c3bcc" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 3226 - }, - "id": "247b9141552a9e73", - "name": "GET /throw-error", - "result": "HTTP 5xx", - "sampled": true, - "span_count": { - "started": 0 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/throw-error", - "original": "/throw-error", - "path": "/throw-error", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - } -] diff --git a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts index b4a037436adb8..2935fb8e2839a 100644 --- a/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts +++ b/x-pack/test/apm_api_integration/basic/tests/traces/top_traces.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; import { sortBy, omit } from 'lodash'; +import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import expectTopTraces from './expectation/top_traces.expectation.json'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -25,7 +25,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.eql({ items: [], isAggregationAccurate: true, bucketSize: 1000 }); + expectSnapshot(response.body).toMatchInline(` + Object { + "bucketSize": 1000, + "isAggregationAccurate": true, + "items": Array [], + } + `); }); }); @@ -44,7 +50,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns the correct number of buckets', async () => { - expect(response.body.items.length).to.be(33); + expectSnapshot(response.body.items.length).toMatchInline(`33`); }); it('returns the correct buckets', async () => { @@ -53,12 +59,61 @@ export default function ApiTest({ getService }: FtrProviderContext) { 'impact' ); - const expectedTracesWithoutSamples = sortBy( - expectTopTraces.map((item: any) => omit(item, 'sample')), - 'impact' - ); + const firstItem = responseWithoutSamples[0]; + const lastItem = responseWithoutSamples[responseWithoutSamples.length - 1]; + + const groups = responseWithoutSamples.map((item) => item.key).slice(0, 5); + + expectSnapshot(responseWithoutSamples).toMatch(); + + expectSnapshot(firstItem).toMatchInline(` + Object { + "averageResponseTime": 2577, + "impact": 0, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "GET /throw-error", + }, + "transactionsPerMinute": 0.5, + } + `); + + expectSnapshot(lastItem).toMatchInline(` + Object { + "averageResponseTime": 1745009, + "impact": 100, + "key": Object { + "service.name": "opbeans-node", + "transaction.name": "Process payment", + }, + "transactionsPerMinute": 0.25, + } + `); - expect(responseWithoutSamples).to.eql(expectedTracesWithoutSamples); + expectSnapshot(groups).toMatchInline(` + Array [ + Object { + "service.name": "opbeans-node", + "transaction.name": "GET /throw-error", + }, + Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#orders", + }, + Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#order", + }, + Object { + "service.name": "opbeans-java", + "transaction.name": "APIRestController#product", + }, + Object { + "service.name": "opbeans-node", + "transaction.name": "GET /api/products/:id/customers", + }, + ] + `); }); it('returns a sample', async () => { diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/avg_duration_by_browser.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/avg_duration_by_browser.snap new file mode 100644 index 0000000000000..326797919a095 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/avg_duration_by_browser.snap @@ -0,0 +1,1473 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Average duration by browser when data is loaded returns the average duration by browser 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "x": 1593413100000, + }, + Object { + "x": 1593413101000, + }, + Object { + "x": 1593413102000, + }, + Object { + "x": 1593413103000, + }, + Object { + "x": 1593413104000, + }, + Object { + "x": 1593413105000, + }, + Object { + "x": 1593413106000, + }, + Object { + "x": 1593413107000, + }, + Object { + "x": 1593413108000, + }, + Object { + "x": 1593413109000, + }, + Object { + "x": 1593413110000, + }, + Object { + "x": 1593413111000, + }, + Object { + "x": 1593413112000, + }, + Object { + "x": 1593413113000, + }, + Object { + "x": 1593413114000, + }, + Object { + "x": 1593413115000, + }, + Object { + "x": 1593413116000, + }, + Object { + "x": 1593413117000, + }, + Object { + "x": 1593413118000, + }, + Object { + "x": 1593413119000, + }, + Object { + "x": 1593413120000, + }, + Object { + "x": 1593413121000, + }, + Object { + "x": 1593413122000, + }, + Object { + "x": 1593413123000, + }, + Object { + "x": 1593413124000, + }, + Object { + "x": 1593413125000, + }, + Object { + "x": 1593413126000, + }, + Object { + "x": 1593413127000, + }, + Object { + "x": 1593413128000, + }, + Object { + "x": 1593413129000, + }, + Object { + "x": 1593413130000, + }, + Object { + "x": 1593413131000, + }, + Object { + "x": 1593413132000, + }, + Object { + "x": 1593413133000, + }, + Object { + "x": 1593413134000, + }, + Object { + "x": 1593413135000, + }, + Object { + "x": 1593413136000, + }, + Object { + "x": 1593413137000, + }, + Object { + "x": 1593413138000, + }, + Object { + "x": 1593413139000, + }, + Object { + "x": 1593413140000, + }, + Object { + "x": 1593413141000, + }, + Object { + "x": 1593413142000, + }, + Object { + "x": 1593413143000, + }, + Object { + "x": 1593413144000, + }, + Object { + "x": 1593413145000, + }, + Object { + "x": 1593413146000, + }, + Object { + "x": 1593413147000, + }, + Object { + "x": 1593413148000, + }, + Object { + "x": 1593413149000, + }, + Object { + "x": 1593413150000, + }, + Object { + "x": 1593413151000, + }, + Object { + "x": 1593413152000, + }, + Object { + "x": 1593413153000, + }, + Object { + "x": 1593413154000, + }, + Object { + "x": 1593413155000, + }, + Object { + "x": 1593413156000, + }, + Object { + "x": 1593413157000, + }, + Object { + "x": 1593413158000, + }, + Object { + "x": 1593413159000, + }, + Object { + "x": 1593413160000, + }, + Object { + "x": 1593413161000, + }, + Object { + "x": 1593413162000, + }, + Object { + "x": 1593413163000, + }, + Object { + "x": 1593413164000, + }, + Object { + "x": 1593413165000, + }, + Object { + "x": 1593413166000, + }, + Object { + "x": 1593413167000, + }, + Object { + "x": 1593413168000, + }, + Object { + "x": 1593413169000, + }, + Object { + "x": 1593413170000, + }, + Object { + "x": 1593413171000, + }, + Object { + "x": 1593413172000, + }, + Object { + "x": 1593413173000, + }, + Object { + "x": 1593413174000, + }, + Object { + "x": 1593413175000, + }, + Object { + "x": 1593413176000, + }, + Object { + "x": 1593413177000, + }, + Object { + "x": 1593413178000, + }, + Object { + "x": 1593413179000, + }, + Object { + "x": 1593413180000, + }, + Object { + "x": 1593413181000, + }, + Object { + "x": 1593413182000, + }, + Object { + "x": 1593413183000, + }, + Object { + "x": 1593413184000, + }, + Object { + "x": 1593413185000, + }, + Object { + "x": 1593413186000, + }, + Object { + "x": 1593413187000, + }, + Object { + "x": 1593413188000, + }, + Object { + "x": 1593413189000, + }, + Object { + "x": 1593413190000, + }, + Object { + "x": 1593413191000, + }, + Object { + "x": 1593413192000, + }, + Object { + "x": 1593413193000, + }, + Object { + "x": 1593413194000, + }, + Object { + "x": 1593413195000, + }, + Object { + "x": 1593413196000, + }, + Object { + "x": 1593413197000, + }, + Object { + "x": 1593413198000, + }, + Object { + "x": 1593413199000, + }, + Object { + "x": 1593413200000, + }, + Object { + "x": 1593413201000, + }, + Object { + "x": 1593413202000, + }, + Object { + "x": 1593413203000, + }, + Object { + "x": 1593413204000, + }, + Object { + "x": 1593413205000, + }, + Object { + "x": 1593413206000, + }, + Object { + "x": 1593413207000, + }, + Object { + "x": 1593413208000, + }, + Object { + "x": 1593413209000, + }, + Object { + "x": 1593413210000, + }, + Object { + "x": 1593413211000, + }, + Object { + "x": 1593413212000, + }, + Object { + "x": 1593413213000, + }, + Object { + "x": 1593413214000, + }, + Object { + "x": 1593413215000, + }, + Object { + "x": 1593413216000, + }, + Object { + "x": 1593413217000, + }, + Object { + "x": 1593413218000, + }, + Object { + "x": 1593413219000, + }, + Object { + "x": 1593413220000, + }, + Object { + "x": 1593413221000, + }, + Object { + "x": 1593413222000, + }, + Object { + "x": 1593413223000, + }, + Object { + "x": 1593413224000, + }, + Object { + "x": 1593413225000, + }, + Object { + "x": 1593413226000, + }, + Object { + "x": 1593413227000, + }, + Object { + "x": 1593413228000, + }, + Object { + "x": 1593413229000, + }, + Object { + "x": 1593413230000, + }, + Object { + "x": 1593413231000, + }, + Object { + "x": 1593413232000, + }, + Object { + "x": 1593413233000, + }, + Object { + "x": 1593413234000, + }, + Object { + "x": 1593413235000, + }, + Object { + "x": 1593413236000, + }, + Object { + "x": 1593413237000, + }, + Object { + "x": 1593413238000, + }, + Object { + "x": 1593413239000, + }, + Object { + "x": 1593413240000, + }, + Object { + "x": 1593413241000, + }, + Object { + "x": 1593413242000, + }, + Object { + "x": 1593413243000, + }, + Object { + "x": 1593413244000, + }, + Object { + "x": 1593413245000, + }, + Object { + "x": 1593413246000, + }, + Object { + "x": 1593413247000, + }, + Object { + "x": 1593413248000, + }, + Object { + "x": 1593413249000, + }, + Object { + "x": 1593413250000, + }, + Object { + "x": 1593413251000, + }, + Object { + "x": 1593413252000, + }, + Object { + "x": 1593413253000, + }, + Object { + "x": 1593413254000, + }, + Object { + "x": 1593413255000, + }, + Object { + "x": 1593413256000, + }, + Object { + "x": 1593413257000, + }, + Object { + "x": 1593413258000, + }, + Object { + "x": 1593413259000, + }, + Object { + "x": 1593413260000, + }, + Object { + "x": 1593413261000, + }, + Object { + "x": 1593413262000, + }, + Object { + "x": 1593413263000, + }, + Object { + "x": 1593413264000, + }, + Object { + "x": 1593413265000, + }, + Object { + "x": 1593413266000, + }, + Object { + "x": 1593413267000, + }, + Object { + "x": 1593413268000, + }, + Object { + "x": 1593413269000, + }, + Object { + "x": 1593413270000, + }, + Object { + "x": 1593413271000, + }, + Object { + "x": 1593413272000, + }, + Object { + "x": 1593413273000, + }, + Object { + "x": 1593413274000, + }, + Object { + "x": 1593413275000, + }, + Object { + "x": 1593413276000, + }, + Object { + "x": 1593413277000, + }, + Object { + "x": 1593413278000, + }, + Object { + "x": 1593413279000, + }, + Object { + "x": 1593413280000, + }, + Object { + "x": 1593413281000, + }, + Object { + "x": 1593413282000, + }, + Object { + "x": 1593413283000, + }, + Object { + "x": 1593413284000, + }, + Object { + "x": 1593413285000, + }, + Object { + "x": 1593413286000, + }, + Object { + "x": 1593413287000, + "y": 342000, + }, + Object { + "x": 1593413288000, + }, + Object { + "x": 1593413289000, + }, + Object { + "x": 1593413290000, + }, + Object { + "x": 1593413291000, + }, + Object { + "x": 1593413292000, + }, + Object { + "x": 1593413293000, + }, + Object { + "x": 1593413294000, + }, + Object { + "x": 1593413295000, + }, + Object { + "x": 1593413296000, + }, + Object { + "x": 1593413297000, + }, + Object { + "x": 1593413298000, + "y": 173000, + }, + Object { + "x": 1593413299000, + }, + Object { + "x": 1593413300000, + }, + Object { + "x": 1593413301000, + "y": 109000, + }, + Object { + "x": 1593413302000, + }, + Object { + "x": 1593413303000, + }, + Object { + "x": 1593413304000, + }, + Object { + "x": 1593413305000, + }, + Object { + "x": 1593413306000, + }, + Object { + "x": 1593413307000, + }, + Object { + "x": 1593413308000, + }, + Object { + "x": 1593413309000, + }, + Object { + "x": 1593413310000, + }, + Object { + "x": 1593413311000, + }, + Object { + "x": 1593413312000, + }, + Object { + "x": 1593413313000, + }, + Object { + "x": 1593413314000, + }, + Object { + "x": 1593413315000, + }, + Object { + "x": 1593413316000, + }, + Object { + "x": 1593413317000, + }, + Object { + "x": 1593413318000, + "y": 140000, + }, + Object { + "x": 1593413319000, + }, + Object { + "x": 1593413320000, + }, + Object { + "x": 1593413321000, + }, + Object { + "x": 1593413322000, + }, + Object { + "x": 1593413323000, + }, + Object { + "x": 1593413324000, + }, + Object { + "x": 1593413325000, + }, + Object { + "x": 1593413326000, + }, + Object { + "x": 1593413327000, + }, + Object { + "x": 1593413328000, + "y": 77000, + }, + Object { + "x": 1593413329000, + }, + Object { + "x": 1593413330000, + }, + Object { + "x": 1593413331000, + }, + Object { + "x": 1593413332000, + }, + Object { + "x": 1593413333000, + }, + Object { + "x": 1593413334000, + }, + Object { + "x": 1593413335000, + }, + Object { + "x": 1593413336000, + }, + Object { + "x": 1593413337000, + }, + Object { + "x": 1593413338000, + }, + Object { + "x": 1593413339000, + }, + Object { + "x": 1593413340000, + }, + ], + "title": "HeadlessChrome", + }, +] +`; + +exports[`Average duration by browser when data is loaded returns the average duration by browser filtering by transaction name 1`] = ` +Array [ + Object { + "data": Array [ + Object { + "x": 1593413100000, + }, + Object { + "x": 1593413101000, + }, + Object { + "x": 1593413102000, + }, + Object { + "x": 1593413103000, + }, + Object { + "x": 1593413104000, + }, + Object { + "x": 1593413105000, + }, + Object { + "x": 1593413106000, + }, + Object { + "x": 1593413107000, + }, + Object { + "x": 1593413108000, + }, + Object { + "x": 1593413109000, + }, + Object { + "x": 1593413110000, + }, + Object { + "x": 1593413111000, + }, + Object { + "x": 1593413112000, + }, + Object { + "x": 1593413113000, + }, + Object { + "x": 1593413114000, + }, + Object { + "x": 1593413115000, + }, + Object { + "x": 1593413116000, + }, + Object { + "x": 1593413117000, + }, + Object { + "x": 1593413118000, + }, + Object { + "x": 1593413119000, + }, + Object { + "x": 1593413120000, + }, + Object { + "x": 1593413121000, + }, + Object { + "x": 1593413122000, + }, + Object { + "x": 1593413123000, + }, + Object { + "x": 1593413124000, + }, + Object { + "x": 1593413125000, + }, + Object { + "x": 1593413126000, + }, + Object { + "x": 1593413127000, + }, + Object { + "x": 1593413128000, + }, + Object { + "x": 1593413129000, + }, + Object { + "x": 1593413130000, + }, + Object { + "x": 1593413131000, + }, + Object { + "x": 1593413132000, + }, + Object { + "x": 1593413133000, + }, + Object { + "x": 1593413134000, + }, + Object { + "x": 1593413135000, + }, + Object { + "x": 1593413136000, + }, + Object { + "x": 1593413137000, + }, + Object { + "x": 1593413138000, + }, + Object { + "x": 1593413139000, + }, + Object { + "x": 1593413140000, + }, + Object { + "x": 1593413141000, + }, + Object { + "x": 1593413142000, + }, + Object { + "x": 1593413143000, + }, + Object { + "x": 1593413144000, + }, + Object { + "x": 1593413145000, + }, + Object { + "x": 1593413146000, + }, + Object { + "x": 1593413147000, + }, + Object { + "x": 1593413148000, + }, + Object { + "x": 1593413149000, + }, + Object { + "x": 1593413150000, + }, + Object { + "x": 1593413151000, + }, + Object { + "x": 1593413152000, + }, + Object { + "x": 1593413153000, + }, + Object { + "x": 1593413154000, + }, + Object { + "x": 1593413155000, + }, + Object { + "x": 1593413156000, + }, + Object { + "x": 1593413157000, + }, + Object { + "x": 1593413158000, + }, + Object { + "x": 1593413159000, + }, + Object { + "x": 1593413160000, + }, + Object { + "x": 1593413161000, + }, + Object { + "x": 1593413162000, + }, + Object { + "x": 1593413163000, + }, + Object { + "x": 1593413164000, + }, + Object { + "x": 1593413165000, + }, + Object { + "x": 1593413166000, + }, + Object { + "x": 1593413167000, + }, + Object { + "x": 1593413168000, + }, + Object { + "x": 1593413169000, + }, + Object { + "x": 1593413170000, + }, + Object { + "x": 1593413171000, + }, + Object { + "x": 1593413172000, + }, + Object { + "x": 1593413173000, + }, + Object { + "x": 1593413174000, + }, + Object { + "x": 1593413175000, + }, + Object { + "x": 1593413176000, + }, + Object { + "x": 1593413177000, + }, + Object { + "x": 1593413178000, + }, + Object { + "x": 1593413179000, + }, + Object { + "x": 1593413180000, + }, + Object { + "x": 1593413181000, + }, + Object { + "x": 1593413182000, + }, + Object { + "x": 1593413183000, + }, + Object { + "x": 1593413184000, + }, + Object { + "x": 1593413185000, + }, + Object { + "x": 1593413186000, + }, + Object { + "x": 1593413187000, + }, + Object { + "x": 1593413188000, + }, + Object { + "x": 1593413189000, + }, + Object { + "x": 1593413190000, + }, + Object { + "x": 1593413191000, + }, + Object { + "x": 1593413192000, + }, + Object { + "x": 1593413193000, + }, + Object { + "x": 1593413194000, + }, + Object { + "x": 1593413195000, + }, + Object { + "x": 1593413196000, + }, + Object { + "x": 1593413197000, + }, + Object { + "x": 1593413198000, + }, + Object { + "x": 1593413199000, + }, + Object { + "x": 1593413200000, + }, + Object { + "x": 1593413201000, + }, + Object { + "x": 1593413202000, + }, + Object { + "x": 1593413203000, + }, + Object { + "x": 1593413204000, + }, + Object { + "x": 1593413205000, + }, + Object { + "x": 1593413206000, + }, + Object { + "x": 1593413207000, + }, + Object { + "x": 1593413208000, + }, + Object { + "x": 1593413209000, + }, + Object { + "x": 1593413210000, + }, + Object { + "x": 1593413211000, + }, + Object { + "x": 1593413212000, + }, + Object { + "x": 1593413213000, + }, + Object { + "x": 1593413214000, + }, + Object { + "x": 1593413215000, + }, + Object { + "x": 1593413216000, + }, + Object { + "x": 1593413217000, + }, + Object { + "x": 1593413218000, + }, + Object { + "x": 1593413219000, + }, + Object { + "x": 1593413220000, + }, + Object { + "x": 1593413221000, + }, + Object { + "x": 1593413222000, + }, + Object { + "x": 1593413223000, + }, + Object { + "x": 1593413224000, + }, + Object { + "x": 1593413225000, + }, + Object { + "x": 1593413226000, + }, + Object { + "x": 1593413227000, + }, + Object { + "x": 1593413228000, + }, + Object { + "x": 1593413229000, + }, + Object { + "x": 1593413230000, + }, + Object { + "x": 1593413231000, + }, + Object { + "x": 1593413232000, + }, + Object { + "x": 1593413233000, + }, + Object { + "x": 1593413234000, + }, + Object { + "x": 1593413235000, + }, + Object { + "x": 1593413236000, + }, + Object { + "x": 1593413237000, + }, + Object { + "x": 1593413238000, + }, + Object { + "x": 1593413239000, + }, + Object { + "x": 1593413240000, + }, + Object { + "x": 1593413241000, + }, + Object { + "x": 1593413242000, + }, + Object { + "x": 1593413243000, + }, + Object { + "x": 1593413244000, + }, + Object { + "x": 1593413245000, + }, + Object { + "x": 1593413246000, + }, + Object { + "x": 1593413247000, + }, + Object { + "x": 1593413248000, + }, + Object { + "x": 1593413249000, + }, + Object { + "x": 1593413250000, + }, + Object { + "x": 1593413251000, + }, + Object { + "x": 1593413252000, + }, + Object { + "x": 1593413253000, + }, + Object { + "x": 1593413254000, + }, + Object { + "x": 1593413255000, + }, + Object { + "x": 1593413256000, + }, + Object { + "x": 1593413257000, + }, + Object { + "x": 1593413258000, + }, + Object { + "x": 1593413259000, + }, + Object { + "x": 1593413260000, + }, + Object { + "x": 1593413261000, + }, + Object { + "x": 1593413262000, + }, + Object { + "x": 1593413263000, + }, + Object { + "x": 1593413264000, + }, + Object { + "x": 1593413265000, + }, + Object { + "x": 1593413266000, + }, + Object { + "x": 1593413267000, + }, + Object { + "x": 1593413268000, + }, + Object { + "x": 1593413269000, + }, + Object { + "x": 1593413270000, + }, + Object { + "x": 1593413271000, + }, + Object { + "x": 1593413272000, + }, + Object { + "x": 1593413273000, + }, + Object { + "x": 1593413274000, + }, + Object { + "x": 1593413275000, + }, + Object { + "x": 1593413276000, + }, + Object { + "x": 1593413277000, + }, + Object { + "x": 1593413278000, + }, + Object { + "x": 1593413279000, + }, + Object { + "x": 1593413280000, + }, + Object { + "x": 1593413281000, + }, + Object { + "x": 1593413282000, + }, + Object { + "x": 1593413283000, + }, + Object { + "x": 1593413284000, + }, + Object { + "x": 1593413285000, + }, + Object { + "x": 1593413286000, + }, + Object { + "x": 1593413287000, + }, + Object { + "x": 1593413288000, + }, + Object { + "x": 1593413289000, + }, + Object { + "x": 1593413290000, + }, + Object { + "x": 1593413291000, + }, + Object { + "x": 1593413292000, + }, + Object { + "x": 1593413293000, + }, + Object { + "x": 1593413294000, + }, + Object { + "x": 1593413295000, + }, + Object { + "x": 1593413296000, + }, + Object { + "x": 1593413297000, + }, + Object { + "x": 1593413298000, + }, + Object { + "x": 1593413299000, + }, + Object { + "x": 1593413300000, + }, + Object { + "x": 1593413301000, + }, + Object { + "x": 1593413302000, + }, + Object { + "x": 1593413303000, + }, + Object { + "x": 1593413304000, + }, + Object { + "x": 1593413305000, + }, + Object { + "x": 1593413306000, + }, + Object { + "x": 1593413307000, + }, + Object { + "x": 1593413308000, + }, + Object { + "x": 1593413309000, + }, + Object { + "x": 1593413310000, + }, + Object { + "x": 1593413311000, + }, + Object { + "x": 1593413312000, + }, + Object { + "x": 1593413313000, + }, + Object { + "x": 1593413314000, + }, + Object { + "x": 1593413315000, + }, + Object { + "x": 1593413316000, + }, + Object { + "x": 1593413317000, + }, + Object { + "x": 1593413318000, + }, + Object { + "x": 1593413319000, + }, + Object { + "x": 1593413320000, + }, + Object { + "x": 1593413321000, + }, + Object { + "x": 1593413322000, + }, + Object { + "x": 1593413323000, + }, + Object { + "x": 1593413324000, + }, + Object { + "x": 1593413325000, + }, + Object { + "x": 1593413326000, + }, + Object { + "x": 1593413327000, + }, + Object { + "x": 1593413328000, + "y": 77000, + }, + Object { + "x": 1593413329000, + }, + Object { + "x": 1593413330000, + }, + Object { + "x": 1593413331000, + }, + Object { + "x": 1593413332000, + }, + Object { + "x": 1593413333000, + }, + Object { + "x": 1593413334000, + }, + Object { + "x": 1593413335000, + }, + Object { + "x": 1593413336000, + }, + Object { + "x": 1593413337000, + }, + Object { + "x": 1593413338000, + }, + Object { + "x": 1593413339000, + }, + Object { + "x": 1593413340000, + }, + ], + "title": "HeadlessChrome", + }, +] +`; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap new file mode 100644 index 0000000000000..e204ff41dfa43 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/breakdown.snap @@ -0,0 +1,188 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Breakdown when data is loaded returns the transaction breakdown for a service 1`] = ` +Object { + "timeseries": Array [ + Object { + "color": "#54b399", + "data": Array [ + Object { + "x": 1593413100000, + "y": null, + }, + Object { + "x": 1593413130000, + "y": null, + }, + Object { + "x": 1593413160000, + "y": null, + }, + Object { + "x": 1593413190000, + "y": null, + }, + Object { + "x": 1593413220000, + "y": null, + }, + Object { + "x": 1593413250000, + "y": null, + }, + Object { + "x": 1593413280000, + "y": null, + }, + Object { + "x": 1593413310000, + "y": 0.16700861715223636, + }, + Object { + "x": 1593413340000, + "y": null, + }, + ], + "hideLegend": false, + "legendValue": "17%", + "title": "app", + "type": "areaStacked", + }, + Object { + "color": "#6092c0", + "data": Array [ + Object { + "x": 1593413100000, + "y": null, + }, + Object { + "x": 1593413130000, + "y": null, + }, + Object { + "x": 1593413160000, + "y": null, + }, + Object { + "x": 1593413190000, + "y": null, + }, + Object { + "x": 1593413220000, + "y": null, + }, + Object { + "x": 1593413250000, + "y": null, + }, + Object { + "x": 1593413280000, + "y": null, + }, + Object { + "x": 1593413310000, + "y": 0.7702092736971686, + }, + Object { + "x": 1593413340000, + "y": null, + }, + ], + "hideLegend": false, + "legendValue": "77%", + "title": "http", + "type": "areaStacked", + }, + Object { + "color": "#d36086", + "data": Array [ + Object { + "x": 1593413100000, + "y": null, + }, + Object { + "x": 1593413130000, + "y": null, + }, + Object { + "x": 1593413160000, + "y": null, + }, + Object { + "x": 1593413190000, + "y": null, + }, + Object { + "x": 1593413220000, + "y": null, + }, + Object { + "x": 1593413250000, + "y": null, + }, + Object { + "x": 1593413280000, + "y": null, + }, + Object { + "x": 1593413310000, + "y": 0.0508822322527698, + }, + Object { + "x": 1593413340000, + "y": null, + }, + ], + "hideLegend": false, + "legendValue": "5.1%", + "title": "postgresql", + "type": "areaStacked", + }, + Object { + "color": "#9170b8", + "data": Array [ + Object { + "x": 1593413100000, + "y": null, + }, + Object { + "x": 1593413130000, + "y": null, + }, + Object { + "x": 1593413160000, + "y": null, + }, + Object { + "x": 1593413190000, + "y": null, + }, + Object { + "x": 1593413220000, + "y": null, + }, + Object { + "x": 1593413250000, + "y": null, + }, + Object { + "x": 1593413280000, + "y": null, + }, + Object { + "x": 1593413310000, + "y": 0.011899876897825195, + }, + Object { + "x": 1593413340000, + "y": null, + }, + ], + "hideLegend": false, + "legendValue": "1.2%", + "title": "redis", + "type": "areaStacked", + }, + ], +} +`; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap new file mode 100644 index 0000000000000..16a5640c5305b --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/top_transaction_groups.snap @@ -0,0 +1,132 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Top transaction groups when data is loaded returns the correct buckets (when ignoring samples) 1`] = ` +Array [ + Object { + "averageResponseTime": 2577, + "impact": 0, + "key": "GET /throw-error", + "p95": 3224, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 4757, + "impact": 0.20830834986820673, + "key": "GET /api/products/:id/customers", + "p95": 5616, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 4749.666666666667, + "impact": 0.43453312891085794, + "key": "GET /api/orders/:id", + "p95": 7184, + "transactionsPerMinute": 0.75, + }, + Object { + "averageResponseTime": 8181, + "impact": 0.5354862351657939, + "key": "GET /api/types/:id", + "p95": 10080, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 20011, + "impact": 0.7098250353192541, + "key": "POST /api", + "p95": 19968, + "transactionsPerMinute": 0.25, + }, + Object { + "averageResponseTime": 35846, + "impact": 1.466376117925459, + "key": "GET /log-error", + "p95": 35840, + "transactionsPerMinute": 0.25, + }, + Object { + "averageResponseTime": 7105.333333333333, + "impact": 1.7905918202662048, + "key": "GET /api/stats", + "p95": 15136, + "transactionsPerMinute": 1.5, + }, + Object { + "averageResponseTime": 22958.5, + "impact": 1.9475397398343375, + "key": "GET /api/products/top", + "p95": 33216, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 3492.9285714285716, + "impact": 2.0901067389184496, + "key": "GET static file", + "p95": 11900, + "transactionsPerMinute": 3.5, + }, + Object { + "averageResponseTime": 26992.5, + "impact": 2.3330057413794503, + "key": "GET /api/types", + "p95": 45248, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 13516.5, + "impact": 2.3368756900811305, + "key": "GET /api/products/:id", + "p95": 37856, + "transactionsPerMinute": 1, + }, + Object { + "averageResponseTime": 8585, + "impact": 2.624924094061731, + "key": "GET /api/products", + "p95": 22112, + "transactionsPerMinute": 1.75, + }, + Object { + "averageResponseTime": 7615.625, + "impact": 2.6645791239678345, + "key": "GET /api/orders", + "p95": 11616, + "transactionsPerMinute": 2, + }, + Object { + "averageResponseTime": 3262.95, + "impact": 2.8716452680799467, + "key": "GET /*", + "p95": 4472, + "transactionsPerMinute": 5, + }, + Object { + "averageResponseTime": 32667.5, + "impact": 2.875276331059301, + "key": "GET /log-message", + "p95": 38528, + "transactionsPerMinute": 0.5, + }, + Object { + "averageResponseTime": 16896.8, + "impact": 3.790160870423129, + "key": "GET /api/customers", + "p95": 26432, + "transactionsPerMinute": 1.25, + }, + Object { + "averageResponseTime": 270684, + "impact": 12.686265169840583, + "key": "POST /api/orders", + "p95": 270336, + "transactionsPerMinute": 0.25, + }, + Object { + "averageResponseTime": 51175.73170731707, + "impact": 100, + "key": "GET /api", + "p95": 259040, + "transactionsPerMinute": 10.25, + }, +] +`; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap new file mode 100644 index 0000000000000..0ac7741396fd4 --- /dev/null +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/__snapshots__/transaction_charts.snap @@ -0,0 +1,7761 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Transaction charts when data is loaded returns the transaction charts 1`] = ` +Object { + "apmTimeseries": Object { + "overallAvgDuration": 38682.52419354839, + "responseTimes": Object { + "avg": Array [ + Object { + "x": 1593413100000, + "y": null, + }, + Object { + "x": 1593413101000, + "y": null, + }, + Object { + "x": 1593413102000, + "y": null, + }, + Object { + "x": 1593413103000, + "y": null, + }, + Object { + "x": 1593413104000, + "y": null, + }, + Object { + "x": 1593413105000, + "y": null, + }, + Object { + "x": 1593413106000, + "y": null, + }, + Object { + "x": 1593413107000, + "y": null, + }, + Object { + "x": 1593413108000, + "y": null, + }, + Object { + "x": 1593413109000, + "y": null, + }, + Object { + "x": 1593413110000, + "y": null, + }, + Object { + "x": 1593413111000, + "y": null, + }, + Object { + "x": 1593413112000, + "y": null, + }, + Object { + "x": 1593413113000, + "y": null, + }, + Object { + "x": 1593413114000, + "y": null, + }, + Object { + "x": 1593413115000, + "y": null, + }, + Object { + "x": 1593413116000, + "y": null, + }, + Object { + "x": 1593413117000, + "y": null, + }, + Object { + "x": 1593413118000, + "y": null, + }, + Object { + "x": 1593413119000, + "y": null, + }, + Object { + "x": 1593413120000, + "y": null, + }, + Object { + "x": 1593413121000, + "y": null, + }, + Object { + "x": 1593413122000, + "y": null, + }, + Object { + "x": 1593413123000, + "y": null, + }, + Object { + "x": 1593413124000, + "y": null, + }, + Object { + "x": 1593413125000, + "y": null, + }, + Object { + "x": 1593413126000, + "y": null, + }, + Object { + "x": 1593413127000, + "y": null, + }, + Object { + "x": 1593413128000, + "y": null, + }, + Object { + "x": 1593413129000, + "y": null, + }, + Object { + "x": 1593413130000, + "y": null, + }, + Object { + "x": 1593413131000, + "y": null, + }, + Object { + "x": 1593413132000, + "y": null, + }, + Object { + "x": 1593413133000, + "y": null, + }, + Object { + "x": 1593413134000, + "y": null, + }, + Object { + "x": 1593413135000, + "y": null, + }, + Object { + "x": 1593413136000, + "y": null, + }, + Object { + "x": 1593413137000, + "y": null, + }, + Object { + "x": 1593413138000, + "y": null, + }, + Object { + "x": 1593413139000, + "y": null, + }, + Object { + "x": 1593413140000, + "y": null, + }, + Object { + "x": 1593413141000, + "y": null, + }, + Object { + "x": 1593413142000, + "y": null, + }, + Object { + "x": 1593413143000, + "y": null, + }, + Object { + "x": 1593413144000, + "y": null, + }, + Object { + "x": 1593413145000, + "y": null, + }, + Object { + "x": 1593413146000, + "y": null, + }, + Object { + "x": 1593413147000, + "y": null, + }, + Object { + "x": 1593413148000, + "y": null, + }, + Object { + "x": 1593413149000, + "y": null, + }, + Object { + "x": 1593413150000, + "y": null, + }, + Object { + "x": 1593413151000, + "y": null, + }, + Object { + "x": 1593413152000, + "y": null, + }, + Object { + "x": 1593413153000, + "y": null, + }, + Object { + "x": 1593413154000, + "y": null, + }, + Object { + "x": 1593413155000, + "y": null, + }, + Object { + "x": 1593413156000, + "y": null, + }, + Object { + "x": 1593413157000, + "y": null, + }, + Object { + "x": 1593413158000, + "y": null, + }, + Object { + "x": 1593413159000, + "y": null, + }, + Object { + "x": 1593413160000, + "y": null, + }, + Object { + "x": 1593413161000, + "y": null, + }, + Object { + "x": 1593413162000, + "y": null, + }, + Object { + "x": 1593413163000, + "y": null, + }, + Object { + "x": 1593413164000, + "y": null, + }, + Object { + "x": 1593413165000, + "y": null, + }, + Object { + "x": 1593413166000, + "y": null, + }, + Object { + "x": 1593413167000, + "y": null, + }, + Object { + "x": 1593413168000, + "y": null, + }, + Object { + "x": 1593413169000, + "y": null, + }, + Object { + "x": 1593413170000, + "y": null, + }, + Object { + "x": 1593413171000, + "y": null, + }, + Object { + "x": 1593413172000, + "y": null, + }, + Object { + "x": 1593413173000, + "y": null, + }, + Object { + "x": 1593413174000, + "y": null, + }, + Object { + "x": 1593413175000, + "y": null, + }, + Object { + "x": 1593413176000, + "y": null, + }, + Object { + "x": 1593413177000, + "y": null, + }, + Object { + "x": 1593413178000, + "y": null, + }, + Object { + "x": 1593413179000, + "y": null, + }, + Object { + "x": 1593413180000, + "y": null, + }, + Object { + "x": 1593413181000, + "y": null, + }, + Object { + "x": 1593413182000, + "y": null, + }, + Object { + "x": 1593413183000, + "y": null, + }, + Object { + "x": 1593413184000, + "y": null, + }, + Object { + "x": 1593413185000, + "y": null, + }, + Object { + "x": 1593413186000, + "y": null, + }, + Object { + "x": 1593413187000, + "y": null, + }, + Object { + "x": 1593413188000, + "y": null, + }, + Object { + "x": 1593413189000, + "y": null, + }, + Object { + "x": 1593413190000, + "y": null, + }, + Object { + "x": 1593413191000, + "y": null, + }, + Object { + "x": 1593413192000, + "y": null, + }, + Object { + "x": 1593413193000, + "y": null, + }, + Object { + "x": 1593413194000, + "y": null, + }, + Object { + "x": 1593413195000, + "y": null, + }, + Object { + "x": 1593413196000, + "y": null, + }, + Object { + "x": 1593413197000, + "y": null, + }, + Object { + "x": 1593413198000, + "y": null, + }, + Object { + "x": 1593413199000, + "y": null, + }, + Object { + "x": 1593413200000, + "y": null, + }, + Object { + "x": 1593413201000, + "y": null, + }, + Object { + "x": 1593413202000, + "y": null, + }, + Object { + "x": 1593413203000, + "y": null, + }, + Object { + "x": 1593413204000, + "y": null, + }, + Object { + "x": 1593413205000, + "y": null, + }, + Object { + "x": 1593413206000, + "y": null, + }, + Object { + "x": 1593413207000, + "y": null, + }, + Object { + "x": 1593413208000, + "y": null, + }, + Object { + "x": 1593413209000, + "y": null, + }, + Object { + "x": 1593413210000, + "y": null, + }, + Object { + "x": 1593413211000, + "y": null, + }, + Object { + "x": 1593413212000, + "y": null, + }, + Object { + "x": 1593413213000, + "y": null, + }, + Object { + "x": 1593413214000, + "y": null, + }, + Object { + "x": 1593413215000, + "y": null, + }, + Object { + "x": 1593413216000, + "y": null, + }, + Object { + "x": 1593413217000, + "y": null, + }, + Object { + "x": 1593413218000, + "y": null, + }, + Object { + "x": 1593413219000, + "y": null, + }, + Object { + "x": 1593413220000, + "y": null, + }, + Object { + "x": 1593413221000, + "y": null, + }, + Object { + "x": 1593413222000, + "y": null, + }, + Object { + "x": 1593413223000, + "y": null, + }, + Object { + "x": 1593413224000, + "y": null, + }, + Object { + "x": 1593413225000, + "y": null, + }, + Object { + "x": 1593413226000, + "y": null, + }, + Object { + "x": 1593413227000, + "y": null, + }, + Object { + "x": 1593413228000, + "y": null, + }, + Object { + "x": 1593413229000, + "y": null, + }, + Object { + "x": 1593413230000, + "y": null, + }, + Object { + "x": 1593413231000, + "y": null, + }, + Object { + "x": 1593413232000, + "y": null, + }, + Object { + "x": 1593413233000, + "y": null, + }, + Object { + "x": 1593413234000, + "y": null, + }, + Object { + "x": 1593413235000, + "y": null, + }, + Object { + "x": 1593413236000, + "y": null, + }, + Object { + "x": 1593413237000, + "y": null, + }, + Object { + "x": 1593413238000, + "y": null, + }, + Object { + "x": 1593413239000, + "y": null, + }, + Object { + "x": 1593413240000, + "y": null, + }, + Object { + "x": 1593413241000, + "y": null, + }, + Object { + "x": 1593413242000, + "y": null, + }, + Object { + "x": 1593413243000, + "y": null, + }, + Object { + "x": 1593413244000, + "y": null, + }, + Object { + "x": 1593413245000, + "y": null, + }, + Object { + "x": 1593413246000, + "y": null, + }, + Object { + "x": 1593413247000, + "y": null, + }, + Object { + "x": 1593413248000, + "y": null, + }, + Object { + "x": 1593413249000, + "y": null, + }, + Object { + "x": 1593413250000, + "y": null, + }, + Object { + "x": 1593413251000, + "y": null, + }, + Object { + "x": 1593413252000, + "y": null, + }, + Object { + "x": 1593413253000, + "y": null, + }, + Object { + "x": 1593413254000, + "y": null, + }, + Object { + "x": 1593413255000, + "y": null, + }, + Object { + "x": 1593413256000, + "y": null, + }, + Object { + "x": 1593413257000, + "y": null, + }, + Object { + "x": 1593413258000, + "y": null, + }, + Object { + "x": 1593413259000, + "y": null, + }, + Object { + "x": 1593413260000, + "y": null, + }, + Object { + "x": 1593413261000, + "y": null, + }, + Object { + "x": 1593413262000, + "y": null, + }, + Object { + "x": 1593413263000, + "y": null, + }, + Object { + "x": 1593413264000, + "y": null, + }, + Object { + "x": 1593413265000, + "y": null, + }, + Object { + "x": 1593413266000, + "y": null, + }, + Object { + "x": 1593413267000, + "y": null, + }, + Object { + "x": 1593413268000, + "y": null, + }, + Object { + "x": 1593413269000, + "y": null, + }, + Object { + "x": 1593413270000, + "y": null, + }, + Object { + "x": 1593413271000, + "y": null, + }, + Object { + "x": 1593413272000, + "y": 45093, + }, + Object { + "x": 1593413273000, + "y": 7498, + }, + Object { + "x": 1593413274000, + "y": null, + }, + Object { + "x": 1593413275000, + "y": null, + }, + Object { + "x": 1593413276000, + "y": null, + }, + Object { + "x": 1593413277000, + "y": 37709, + }, + Object { + "x": 1593413278000, + "y": null, + }, + Object { + "x": 1593413279000, + "y": null, + }, + Object { + "x": 1593413280000, + "y": null, + }, + Object { + "x": 1593413281000, + "y": 33097, + }, + Object { + "x": 1593413282000, + "y": null, + }, + Object { + "x": 1593413283000, + "y": null, + }, + Object { + "x": 1593413284000, + "y": 388507, + }, + Object { + "x": 1593413285000, + "y": 42331.5, + }, + Object { + "x": 1593413286000, + "y": 99104.25, + }, + Object { + "x": 1593413287000, + "y": 18939.5, + }, + Object { + "x": 1593413288000, + "y": 23229.5, + }, + Object { + "x": 1593413289000, + "y": 11318, + }, + Object { + "x": 1593413290000, + "y": 15651.25, + }, + Object { + "x": 1593413291000, + "y": 2376, + }, + Object { + "x": 1593413292000, + "y": 7796, + }, + Object { + "x": 1593413293000, + "y": 7571, + }, + Object { + "x": 1593413294000, + "y": 4219.333333333333, + }, + Object { + "x": 1593413295000, + "y": 6827.5, + }, + Object { + "x": 1593413296000, + "y": 10415.5, + }, + Object { + "x": 1593413297000, + "y": 10082, + }, + Object { + "x": 1593413298000, + "y": 6459.375, + }, + Object { + "x": 1593413299000, + "y": 3131.5, + }, + Object { + "x": 1593413300000, + "y": 6713.333333333333, + }, + Object { + "x": 1593413301000, + "y": 8800, + }, + Object { + "x": 1593413302000, + "y": 3743.5, + }, + Object { + "x": 1593413303000, + "y": 9239.5, + }, + Object { + "x": 1593413304000, + "y": 8402, + }, + Object { + "x": 1593413305000, + "y": 20520.666666666668, + }, + Object { + "x": 1593413306000, + "y": 9319.5, + }, + Object { + "x": 1593413307000, + "y": 7694.333333333333, + }, + Object { + "x": 1593413308000, + "y": 20131, + }, + Object { + "x": 1593413309000, + "y": 439937.75, + }, + Object { + "x": 1593413310000, + "y": 11933, + }, + Object { + "x": 1593413311000, + "y": 18670.5, + }, + Object { + "x": 1593413312000, + "y": 9232, + }, + Object { + "x": 1593413313000, + "y": 7602, + }, + Object { + "x": 1593413314000, + "y": 10428.8, + }, + Object { + "x": 1593413315000, + "y": 8405.25, + }, + Object { + "x": 1593413316000, + "y": 10654.5, + }, + Object { + "x": 1593413317000, + "y": 10250, + }, + Object { + "x": 1593413318000, + "y": 5775, + }, + Object { + "x": 1593413319000, + "y": 137867, + }, + Object { + "x": 1593413320000, + "y": 5694.333333333333, + }, + Object { + "x": 1593413321000, + "y": 6115, + }, + Object { + "x": 1593413322000, + "y": 1832.5, + }, + Object { + "x": 1593413323000, + "y": null, + }, + Object { + "x": 1593413324000, + "y": null, + }, + Object { + "x": 1593413325000, + "y": null, + }, + Object { + "x": 1593413326000, + "y": null, + }, + Object { + "x": 1593413327000, + "y": null, + }, + Object { + "x": 1593413328000, + "y": null, + }, + Object { + "x": 1593413329000, + "y": null, + }, + Object { + "x": 1593413330000, + "y": null, + }, + Object { + "x": 1593413331000, + "y": null, + }, + Object { + "x": 1593413332000, + "y": null, + }, + Object { + "x": 1593413333000, + "y": null, + }, + Object { + "x": 1593413334000, + "y": null, + }, + Object { + "x": 1593413335000, + "y": null, + }, + Object { + "x": 1593413336000, + "y": null, + }, + Object { + "x": 1593413337000, + "y": null, + }, + Object { + "x": 1593413338000, + "y": null, + }, + Object { + "x": 1593413339000, + "y": null, + }, + Object { + "x": 1593413340000, + "y": null, + }, + ], + "p95": Array [ + Object { + "x": 1593413100000, + "y": null, + }, + Object { + "x": 1593413101000, + "y": null, + }, + Object { + "x": 1593413102000, + "y": null, + }, + Object { + "x": 1593413103000, + "y": null, + }, + Object { + "x": 1593413104000, + "y": null, + }, + Object { + "x": 1593413105000, + "y": null, + }, + Object { + "x": 1593413106000, + "y": null, + }, + Object { + "x": 1593413107000, + "y": null, + }, + Object { + "x": 1593413108000, + "y": null, + }, + Object { + "x": 1593413109000, + "y": null, + }, + Object { + "x": 1593413110000, + "y": null, + }, + Object { + "x": 1593413111000, + "y": null, + }, + Object { + "x": 1593413112000, + "y": null, + }, + Object { + "x": 1593413113000, + "y": null, + }, + Object { + "x": 1593413114000, + "y": null, + }, + Object { + "x": 1593413115000, + "y": null, + }, + Object { + "x": 1593413116000, + "y": null, + }, + Object { + "x": 1593413117000, + "y": null, + }, + Object { + "x": 1593413118000, + "y": null, + }, + Object { + "x": 1593413119000, + "y": null, + }, + Object { + "x": 1593413120000, + "y": null, + }, + Object { + "x": 1593413121000, + "y": null, + }, + Object { + "x": 1593413122000, + "y": null, + }, + Object { + "x": 1593413123000, + "y": null, + }, + Object { + "x": 1593413124000, + "y": null, + }, + Object { + "x": 1593413125000, + "y": null, + }, + Object { + "x": 1593413126000, + "y": null, + }, + Object { + "x": 1593413127000, + "y": null, + }, + Object { + "x": 1593413128000, + "y": null, + }, + Object { + "x": 1593413129000, + "y": null, + }, + Object { + "x": 1593413130000, + "y": null, + }, + Object { + "x": 1593413131000, + "y": null, + }, + Object { + "x": 1593413132000, + "y": null, + }, + Object { + "x": 1593413133000, + "y": null, + }, + Object { + "x": 1593413134000, + "y": null, + }, + Object { + "x": 1593413135000, + "y": null, + }, + Object { + "x": 1593413136000, + "y": null, + }, + Object { + "x": 1593413137000, + "y": null, + }, + Object { + "x": 1593413138000, + "y": null, + }, + Object { + "x": 1593413139000, + "y": null, + }, + Object { + "x": 1593413140000, + "y": null, + }, + Object { + "x": 1593413141000, + "y": null, + }, + Object { + "x": 1593413142000, + "y": null, + }, + Object { + "x": 1593413143000, + "y": null, + }, + Object { + "x": 1593413144000, + "y": null, + }, + Object { + "x": 1593413145000, + "y": null, + }, + Object { + "x": 1593413146000, + "y": null, + }, + Object { + "x": 1593413147000, + "y": null, + }, + Object { + "x": 1593413148000, + "y": null, + }, + Object { + "x": 1593413149000, + "y": null, + }, + Object { + "x": 1593413150000, + "y": null, + }, + Object { + "x": 1593413151000, + "y": null, + }, + Object { + "x": 1593413152000, + "y": null, + }, + Object { + "x": 1593413153000, + "y": null, + }, + Object { + "x": 1593413154000, + "y": null, + }, + Object { + "x": 1593413155000, + "y": null, + }, + Object { + "x": 1593413156000, + "y": null, + }, + Object { + "x": 1593413157000, + "y": null, + }, + Object { + "x": 1593413158000, + "y": null, + }, + Object { + "x": 1593413159000, + "y": null, + }, + Object { + "x": 1593413160000, + "y": null, + }, + Object { + "x": 1593413161000, + "y": null, + }, + Object { + "x": 1593413162000, + "y": null, + }, + Object { + "x": 1593413163000, + "y": null, + }, + Object { + "x": 1593413164000, + "y": null, + }, + Object { + "x": 1593413165000, + "y": null, + }, + Object { + "x": 1593413166000, + "y": null, + }, + Object { + "x": 1593413167000, + "y": null, + }, + Object { + "x": 1593413168000, + "y": null, + }, + Object { + "x": 1593413169000, + "y": null, + }, + Object { + "x": 1593413170000, + "y": null, + }, + Object { + "x": 1593413171000, + "y": null, + }, + Object { + "x": 1593413172000, + "y": null, + }, + Object { + "x": 1593413173000, + "y": null, + }, + Object { + "x": 1593413174000, + "y": null, + }, + Object { + "x": 1593413175000, + "y": null, + }, + Object { + "x": 1593413176000, + "y": null, + }, + Object { + "x": 1593413177000, + "y": null, + }, + Object { + "x": 1593413178000, + "y": null, + }, + Object { + "x": 1593413179000, + "y": null, + }, + Object { + "x": 1593413180000, + "y": null, + }, + Object { + "x": 1593413181000, + "y": null, + }, + Object { + "x": 1593413182000, + "y": null, + }, + Object { + "x": 1593413183000, + "y": null, + }, + Object { + "x": 1593413184000, + "y": null, + }, + Object { + "x": 1593413185000, + "y": null, + }, + Object { + "x": 1593413186000, + "y": null, + }, + Object { + "x": 1593413187000, + "y": null, + }, + Object { + "x": 1593413188000, + "y": null, + }, + Object { + "x": 1593413189000, + "y": null, + }, + Object { + "x": 1593413190000, + "y": null, + }, + Object { + "x": 1593413191000, + "y": null, + }, + Object { + "x": 1593413192000, + "y": null, + }, + Object { + "x": 1593413193000, + "y": null, + }, + Object { + "x": 1593413194000, + "y": null, + }, + Object { + "x": 1593413195000, + "y": null, + }, + Object { + "x": 1593413196000, + "y": null, + }, + Object { + "x": 1593413197000, + "y": null, + }, + Object { + "x": 1593413198000, + "y": null, + }, + Object { + "x": 1593413199000, + "y": null, + }, + Object { + "x": 1593413200000, + "y": null, + }, + Object { + "x": 1593413201000, + "y": null, + }, + Object { + "x": 1593413202000, + "y": null, + }, + Object { + "x": 1593413203000, + "y": null, + }, + Object { + "x": 1593413204000, + "y": null, + }, + Object { + "x": 1593413205000, + "y": null, + }, + Object { + "x": 1593413206000, + "y": null, + }, + Object { + "x": 1593413207000, + "y": null, + }, + Object { + "x": 1593413208000, + "y": null, + }, + Object { + "x": 1593413209000, + "y": null, + }, + Object { + "x": 1593413210000, + "y": null, + }, + Object { + "x": 1593413211000, + "y": null, + }, + Object { + "x": 1593413212000, + "y": null, + }, + Object { + "x": 1593413213000, + "y": null, + }, + Object { + "x": 1593413214000, + "y": null, + }, + Object { + "x": 1593413215000, + "y": null, + }, + Object { + "x": 1593413216000, + "y": null, + }, + Object { + "x": 1593413217000, + "y": null, + }, + Object { + "x": 1593413218000, + "y": null, + }, + Object { + "x": 1593413219000, + "y": null, + }, + Object { + "x": 1593413220000, + "y": null, + }, + Object { + "x": 1593413221000, + "y": null, + }, + Object { + "x": 1593413222000, + "y": null, + }, + Object { + "x": 1593413223000, + "y": null, + }, + Object { + "x": 1593413224000, + "y": null, + }, + Object { + "x": 1593413225000, + "y": null, + }, + Object { + "x": 1593413226000, + "y": null, + }, + Object { + "x": 1593413227000, + "y": null, + }, + Object { + "x": 1593413228000, + "y": null, + }, + Object { + "x": 1593413229000, + "y": null, + }, + Object { + "x": 1593413230000, + "y": null, + }, + Object { + "x": 1593413231000, + "y": null, + }, + Object { + "x": 1593413232000, + "y": null, + }, + Object { + "x": 1593413233000, + "y": null, + }, + Object { + "x": 1593413234000, + "y": null, + }, + Object { + "x": 1593413235000, + "y": null, + }, + Object { + "x": 1593413236000, + "y": null, + }, + Object { + "x": 1593413237000, + "y": null, + }, + Object { + "x": 1593413238000, + "y": null, + }, + Object { + "x": 1593413239000, + "y": null, + }, + Object { + "x": 1593413240000, + "y": null, + }, + Object { + "x": 1593413241000, + "y": null, + }, + Object { + "x": 1593413242000, + "y": null, + }, + Object { + "x": 1593413243000, + "y": null, + }, + Object { + "x": 1593413244000, + "y": null, + }, + Object { + "x": 1593413245000, + "y": null, + }, + Object { + "x": 1593413246000, + "y": null, + }, + Object { + "x": 1593413247000, + "y": null, + }, + Object { + "x": 1593413248000, + "y": null, + }, + Object { + "x": 1593413249000, + "y": null, + }, + Object { + "x": 1593413250000, + "y": null, + }, + Object { + "x": 1593413251000, + "y": null, + }, + Object { + "x": 1593413252000, + "y": null, + }, + Object { + "x": 1593413253000, + "y": null, + }, + Object { + "x": 1593413254000, + "y": null, + }, + Object { + "x": 1593413255000, + "y": null, + }, + Object { + "x": 1593413256000, + "y": null, + }, + Object { + "x": 1593413257000, + "y": null, + }, + Object { + "x": 1593413258000, + "y": null, + }, + Object { + "x": 1593413259000, + "y": null, + }, + Object { + "x": 1593413260000, + "y": null, + }, + Object { + "x": 1593413261000, + "y": null, + }, + Object { + "x": 1593413262000, + "y": null, + }, + Object { + "x": 1593413263000, + "y": null, + }, + Object { + "x": 1593413264000, + "y": null, + }, + Object { + "x": 1593413265000, + "y": null, + }, + Object { + "x": 1593413266000, + "y": null, + }, + Object { + "x": 1593413267000, + "y": null, + }, + Object { + "x": 1593413268000, + "y": null, + }, + Object { + "x": 1593413269000, + "y": null, + }, + Object { + "x": 1593413270000, + "y": null, + }, + Object { + "x": 1593413271000, + "y": null, + }, + Object { + "x": 1593413272000, + "y": 45056, + }, + Object { + "x": 1593413273000, + "y": 10080, + }, + Object { + "x": 1593413274000, + "y": null, + }, + Object { + "x": 1593413275000, + "y": null, + }, + Object { + "x": 1593413276000, + "y": null, + }, + Object { + "x": 1593413277000, + "y": 37632, + }, + Object { + "x": 1593413278000, + "y": null, + }, + Object { + "x": 1593413279000, + "y": null, + }, + Object { + "x": 1593413280000, + "y": null, + }, + Object { + "x": 1593413281000, + "y": 33024, + }, + Object { + "x": 1593413282000, + "y": null, + }, + Object { + "x": 1593413283000, + "y": null, + }, + Object { + "x": 1593413284000, + "y": 761728, + }, + Object { + "x": 1593413285000, + "y": 81904, + }, + Object { + "x": 1593413286000, + "y": 358384, + }, + Object { + "x": 1593413287000, + "y": 36088, + }, + Object { + "x": 1593413288000, + "y": 44536, + }, + Object { + "x": 1593413289000, + "y": 11648, + }, + Object { + "x": 1593413290000, + "y": 31984, + }, + Object { + "x": 1593413291000, + "y": 2920, + }, + Object { + "x": 1593413292000, + "y": 9312, + }, + Object { + "x": 1593413293000, + "y": 10912, + }, + Object { + "x": 1593413294000, + "y": 6392, + }, + Object { + "x": 1593413295000, + "y": 11704, + }, + Object { + "x": 1593413296000, + "y": 10816, + }, + Object { + "x": 1593413297000, + "y": 12000, + }, + Object { + "x": 1593413298000, + "y": 15164, + }, + Object { + "x": 1593413299000, + "y": 3216, + }, + Object { + "x": 1593413300000, + "y": 9584, + }, + Object { + "x": 1593413301000, + "y": 21240, + }, + Object { + "x": 1593413302000, + "y": 5624, + }, + Object { + "x": 1593413303000, + "y": 11360, + }, + Object { + "x": 1593413304000, + "y": 12320, + }, + Object { + "x": 1593413305000, + "y": 38640, + }, + Object { + "x": 1593413306000, + "y": 9728, + }, + Object { + "x": 1593413307000, + "y": 17016, + }, + Object { + "x": 1593413308000, + "y": 26848, + }, + Object { + "x": 1593413309000, + "y": 1753072, + }, + Object { + "x": 1593413310000, + "y": 16992, + }, + Object { + "x": 1593413311000, + "y": 26560, + }, + Object { + "x": 1593413312000, + "y": 11232, + }, + Object { + "x": 1593413313000, + "y": 11424, + }, + Object { + "x": 1593413314000, + "y": 16096, + }, + Object { + "x": 1593413315000, + "y": 18800, + }, + Object { + "x": 1593413316000, + "y": 12672, + }, + Object { + "x": 1593413317000, + "y": 24316, + }, + Object { + "x": 1593413318000, + "y": 8944, + }, + Object { + "x": 1593413319000, + "y": 272352, + }, + Object { + "x": 1593413320000, + "y": 7992, + }, + Object { + "x": 1593413321000, + "y": 8368, + }, + Object { + "x": 1593413322000, + "y": 1928, + }, + Object { + "x": 1593413323000, + "y": null, + }, + Object { + "x": 1593413324000, + "y": null, + }, + Object { + "x": 1593413325000, + "y": null, + }, + Object { + "x": 1593413326000, + "y": null, + }, + Object { + "x": 1593413327000, + "y": null, + }, + Object { + "x": 1593413328000, + "y": null, + }, + Object { + "x": 1593413329000, + "y": null, + }, + Object { + "x": 1593413330000, + "y": null, + }, + Object { + "x": 1593413331000, + "y": null, + }, + Object { + "x": 1593413332000, + "y": null, + }, + Object { + "x": 1593413333000, + "y": null, + }, + Object { + "x": 1593413334000, + "y": null, + }, + Object { + "x": 1593413335000, + "y": null, + }, + Object { + "x": 1593413336000, + "y": null, + }, + Object { + "x": 1593413337000, + "y": null, + }, + Object { + "x": 1593413338000, + "y": null, + }, + Object { + "x": 1593413339000, + "y": null, + }, + Object { + "x": 1593413340000, + "y": null, + }, + ], + "p99": Array [ + Object { + "x": 1593413100000, + "y": null, + }, + Object { + "x": 1593413101000, + "y": null, + }, + Object { + "x": 1593413102000, + "y": null, + }, + Object { + "x": 1593413103000, + "y": null, + }, + Object { + "x": 1593413104000, + "y": null, + }, + Object { + "x": 1593413105000, + "y": null, + }, + Object { + "x": 1593413106000, + "y": null, + }, + Object { + "x": 1593413107000, + "y": null, + }, + Object { + "x": 1593413108000, + "y": null, + }, + Object { + "x": 1593413109000, + "y": null, + }, + Object { + "x": 1593413110000, + "y": null, + }, + Object { + "x": 1593413111000, + "y": null, + }, + Object { + "x": 1593413112000, + "y": null, + }, + Object { + "x": 1593413113000, + "y": null, + }, + Object { + "x": 1593413114000, + "y": null, + }, + Object { + "x": 1593413115000, + "y": null, + }, + Object { + "x": 1593413116000, + "y": null, + }, + Object { + "x": 1593413117000, + "y": null, + }, + Object { + "x": 1593413118000, + "y": null, + }, + Object { + "x": 1593413119000, + "y": null, + }, + Object { + "x": 1593413120000, + "y": null, + }, + Object { + "x": 1593413121000, + "y": null, + }, + Object { + "x": 1593413122000, + "y": null, + }, + Object { + "x": 1593413123000, + "y": null, + }, + Object { + "x": 1593413124000, + "y": null, + }, + Object { + "x": 1593413125000, + "y": null, + }, + Object { + "x": 1593413126000, + "y": null, + }, + Object { + "x": 1593413127000, + "y": null, + }, + Object { + "x": 1593413128000, + "y": null, + }, + Object { + "x": 1593413129000, + "y": null, + }, + Object { + "x": 1593413130000, + "y": null, + }, + Object { + "x": 1593413131000, + "y": null, + }, + Object { + "x": 1593413132000, + "y": null, + }, + Object { + "x": 1593413133000, + "y": null, + }, + Object { + "x": 1593413134000, + "y": null, + }, + Object { + "x": 1593413135000, + "y": null, + }, + Object { + "x": 1593413136000, + "y": null, + }, + Object { + "x": 1593413137000, + "y": null, + }, + Object { + "x": 1593413138000, + "y": null, + }, + Object { + "x": 1593413139000, + "y": null, + }, + Object { + "x": 1593413140000, + "y": null, + }, + Object { + "x": 1593413141000, + "y": null, + }, + Object { + "x": 1593413142000, + "y": null, + }, + Object { + "x": 1593413143000, + "y": null, + }, + Object { + "x": 1593413144000, + "y": null, + }, + Object { + "x": 1593413145000, + "y": null, + }, + Object { + "x": 1593413146000, + "y": null, + }, + Object { + "x": 1593413147000, + "y": null, + }, + Object { + "x": 1593413148000, + "y": null, + }, + Object { + "x": 1593413149000, + "y": null, + }, + Object { + "x": 1593413150000, + "y": null, + }, + Object { + "x": 1593413151000, + "y": null, + }, + Object { + "x": 1593413152000, + "y": null, + }, + Object { + "x": 1593413153000, + "y": null, + }, + Object { + "x": 1593413154000, + "y": null, + }, + Object { + "x": 1593413155000, + "y": null, + }, + Object { + "x": 1593413156000, + "y": null, + }, + Object { + "x": 1593413157000, + "y": null, + }, + Object { + "x": 1593413158000, + "y": null, + }, + Object { + "x": 1593413159000, + "y": null, + }, + Object { + "x": 1593413160000, + "y": null, + }, + Object { + "x": 1593413161000, + "y": null, + }, + Object { + "x": 1593413162000, + "y": null, + }, + Object { + "x": 1593413163000, + "y": null, + }, + Object { + "x": 1593413164000, + "y": null, + }, + Object { + "x": 1593413165000, + "y": null, + }, + Object { + "x": 1593413166000, + "y": null, + }, + Object { + "x": 1593413167000, + "y": null, + }, + Object { + "x": 1593413168000, + "y": null, + }, + Object { + "x": 1593413169000, + "y": null, + }, + Object { + "x": 1593413170000, + "y": null, + }, + Object { + "x": 1593413171000, + "y": null, + }, + Object { + "x": 1593413172000, + "y": null, + }, + Object { + "x": 1593413173000, + "y": null, + }, + Object { + "x": 1593413174000, + "y": null, + }, + Object { + "x": 1593413175000, + "y": null, + }, + Object { + "x": 1593413176000, + "y": null, + }, + Object { + "x": 1593413177000, + "y": null, + }, + Object { + "x": 1593413178000, + "y": null, + }, + Object { + "x": 1593413179000, + "y": null, + }, + Object { + "x": 1593413180000, + "y": null, + }, + Object { + "x": 1593413181000, + "y": null, + }, + Object { + "x": 1593413182000, + "y": null, + }, + Object { + "x": 1593413183000, + "y": null, + }, + Object { + "x": 1593413184000, + "y": null, + }, + Object { + "x": 1593413185000, + "y": null, + }, + Object { + "x": 1593413186000, + "y": null, + }, + Object { + "x": 1593413187000, + "y": null, + }, + Object { + "x": 1593413188000, + "y": null, + }, + Object { + "x": 1593413189000, + "y": null, + }, + Object { + "x": 1593413190000, + "y": null, + }, + Object { + "x": 1593413191000, + "y": null, + }, + Object { + "x": 1593413192000, + "y": null, + }, + Object { + "x": 1593413193000, + "y": null, + }, + Object { + "x": 1593413194000, + "y": null, + }, + Object { + "x": 1593413195000, + "y": null, + }, + Object { + "x": 1593413196000, + "y": null, + }, + Object { + "x": 1593413197000, + "y": null, + }, + Object { + "x": 1593413198000, + "y": null, + }, + Object { + "x": 1593413199000, + "y": null, + }, + Object { + "x": 1593413200000, + "y": null, + }, + Object { + "x": 1593413201000, + "y": null, + }, + Object { + "x": 1593413202000, + "y": null, + }, + Object { + "x": 1593413203000, + "y": null, + }, + Object { + "x": 1593413204000, + "y": null, + }, + Object { + "x": 1593413205000, + "y": null, + }, + Object { + "x": 1593413206000, + "y": null, + }, + Object { + "x": 1593413207000, + "y": null, + }, + Object { + "x": 1593413208000, + "y": null, + }, + Object { + "x": 1593413209000, + "y": null, + }, + Object { + "x": 1593413210000, + "y": null, + }, + Object { + "x": 1593413211000, + "y": null, + }, + Object { + "x": 1593413212000, + "y": null, + }, + Object { + "x": 1593413213000, + "y": null, + }, + Object { + "x": 1593413214000, + "y": null, + }, + Object { + "x": 1593413215000, + "y": null, + }, + Object { + "x": 1593413216000, + "y": null, + }, + Object { + "x": 1593413217000, + "y": null, + }, + Object { + "x": 1593413218000, + "y": null, + }, + Object { + "x": 1593413219000, + "y": null, + }, + Object { + "x": 1593413220000, + "y": null, + }, + Object { + "x": 1593413221000, + "y": null, + }, + Object { + "x": 1593413222000, + "y": null, + }, + Object { + "x": 1593413223000, + "y": null, + }, + Object { + "x": 1593413224000, + "y": null, + }, + Object { + "x": 1593413225000, + "y": null, + }, + Object { + "x": 1593413226000, + "y": null, + }, + Object { + "x": 1593413227000, + "y": null, + }, + Object { + "x": 1593413228000, + "y": null, + }, + Object { + "x": 1593413229000, + "y": null, + }, + Object { + "x": 1593413230000, + "y": null, + }, + Object { + "x": 1593413231000, + "y": null, + }, + Object { + "x": 1593413232000, + "y": null, + }, + Object { + "x": 1593413233000, + "y": null, + }, + Object { + "x": 1593413234000, + "y": null, + }, + Object { + "x": 1593413235000, + "y": null, + }, + Object { + "x": 1593413236000, + "y": null, + }, + Object { + "x": 1593413237000, + "y": null, + }, + Object { + "x": 1593413238000, + "y": null, + }, + Object { + "x": 1593413239000, + "y": null, + }, + Object { + "x": 1593413240000, + "y": null, + }, + Object { + "x": 1593413241000, + "y": null, + }, + Object { + "x": 1593413242000, + "y": null, + }, + Object { + "x": 1593413243000, + "y": null, + }, + Object { + "x": 1593413244000, + "y": null, + }, + Object { + "x": 1593413245000, + "y": null, + }, + Object { + "x": 1593413246000, + "y": null, + }, + Object { + "x": 1593413247000, + "y": null, + }, + Object { + "x": 1593413248000, + "y": null, + }, + Object { + "x": 1593413249000, + "y": null, + }, + Object { + "x": 1593413250000, + "y": null, + }, + Object { + "x": 1593413251000, + "y": null, + }, + Object { + "x": 1593413252000, + "y": null, + }, + Object { + "x": 1593413253000, + "y": null, + }, + Object { + "x": 1593413254000, + "y": null, + }, + Object { + "x": 1593413255000, + "y": null, + }, + Object { + "x": 1593413256000, + "y": null, + }, + Object { + "x": 1593413257000, + "y": null, + }, + Object { + "x": 1593413258000, + "y": null, + }, + Object { + "x": 1593413259000, + "y": null, + }, + Object { + "x": 1593413260000, + "y": null, + }, + Object { + "x": 1593413261000, + "y": null, + }, + Object { + "x": 1593413262000, + "y": null, + }, + Object { + "x": 1593413263000, + "y": null, + }, + Object { + "x": 1593413264000, + "y": null, + }, + Object { + "x": 1593413265000, + "y": null, + }, + Object { + "x": 1593413266000, + "y": null, + }, + Object { + "x": 1593413267000, + "y": null, + }, + Object { + "x": 1593413268000, + "y": null, + }, + Object { + "x": 1593413269000, + "y": null, + }, + Object { + "x": 1593413270000, + "y": null, + }, + Object { + "x": 1593413271000, + "y": null, + }, + Object { + "x": 1593413272000, + "y": 45056, + }, + Object { + "x": 1593413273000, + "y": 10080, + }, + Object { + "x": 1593413274000, + "y": null, + }, + Object { + "x": 1593413275000, + "y": null, + }, + Object { + "x": 1593413276000, + "y": null, + }, + Object { + "x": 1593413277000, + "y": 37632, + }, + Object { + "x": 1593413278000, + "y": null, + }, + Object { + "x": 1593413279000, + "y": null, + }, + Object { + "x": 1593413280000, + "y": null, + }, + Object { + "x": 1593413281000, + "y": 33024, + }, + Object { + "x": 1593413282000, + "y": null, + }, + Object { + "x": 1593413283000, + "y": null, + }, + Object { + "x": 1593413284000, + "y": 761728, + }, + Object { + "x": 1593413285000, + "y": 81904, + }, + Object { + "x": 1593413286000, + "y": 358384, + }, + Object { + "x": 1593413287000, + "y": 36088, + }, + Object { + "x": 1593413288000, + "y": 44536, + }, + Object { + "x": 1593413289000, + "y": 11648, + }, + Object { + "x": 1593413290000, + "y": 31984, + }, + Object { + "x": 1593413291000, + "y": 2920, + }, + Object { + "x": 1593413292000, + "y": 9312, + }, + Object { + "x": 1593413293000, + "y": 10912, + }, + Object { + "x": 1593413294000, + "y": 6392, + }, + Object { + "x": 1593413295000, + "y": 11704, + }, + Object { + "x": 1593413296000, + "y": 10816, + }, + Object { + "x": 1593413297000, + "y": 12000, + }, + Object { + "x": 1593413298000, + "y": 15164, + }, + Object { + "x": 1593413299000, + "y": 3216, + }, + Object { + "x": 1593413300000, + "y": 9584, + }, + Object { + "x": 1593413301000, + "y": 21240, + }, + Object { + "x": 1593413302000, + "y": 5624, + }, + Object { + "x": 1593413303000, + "y": 11360, + }, + Object { + "x": 1593413304000, + "y": 12320, + }, + Object { + "x": 1593413305000, + "y": 38640, + }, + Object { + "x": 1593413306000, + "y": 9728, + }, + Object { + "x": 1593413307000, + "y": 17016, + }, + Object { + "x": 1593413308000, + "y": 26848, + }, + Object { + "x": 1593413309000, + "y": 1753072, + }, + Object { + "x": 1593413310000, + "y": 16992, + }, + Object { + "x": 1593413311000, + "y": 26560, + }, + Object { + "x": 1593413312000, + "y": 11232, + }, + Object { + "x": 1593413313000, + "y": 11424, + }, + Object { + "x": 1593413314000, + "y": 16096, + }, + Object { + "x": 1593413315000, + "y": 18800, + }, + Object { + "x": 1593413316000, + "y": 12672, + }, + Object { + "x": 1593413317000, + "y": 24316, + }, + Object { + "x": 1593413318000, + "y": 8944, + }, + Object { + "x": 1593413319000, + "y": 272352, + }, + Object { + "x": 1593413320000, + "y": 7992, + }, + Object { + "x": 1593413321000, + "y": 8368, + }, + Object { + "x": 1593413322000, + "y": 1928, + }, + Object { + "x": 1593413323000, + "y": null, + }, + Object { + "x": 1593413324000, + "y": null, + }, + Object { + "x": 1593413325000, + "y": null, + }, + Object { + "x": 1593413326000, + "y": null, + }, + Object { + "x": 1593413327000, + "y": null, + }, + Object { + "x": 1593413328000, + "y": null, + }, + Object { + "x": 1593413329000, + "y": null, + }, + Object { + "x": 1593413330000, + "y": null, + }, + Object { + "x": 1593413331000, + "y": null, + }, + Object { + "x": 1593413332000, + "y": null, + }, + Object { + "x": 1593413333000, + "y": null, + }, + Object { + "x": 1593413334000, + "y": null, + }, + Object { + "x": 1593413335000, + "y": null, + }, + Object { + "x": 1593413336000, + "y": null, + }, + Object { + "x": 1593413337000, + "y": null, + }, + Object { + "x": 1593413338000, + "y": null, + }, + Object { + "x": 1593413339000, + "y": null, + }, + Object { + "x": 1593413340000, + "y": null, + }, + ], + }, + "tpmBuckets": Array [ + Object { + "avg": 24.75, + "dataPoints": Array [ + Object { + "x": 1593413100000, + "y": 0, + }, + Object { + "x": 1593413101000, + "y": 0, + }, + Object { + "x": 1593413102000, + "y": 0, + }, + Object { + "x": 1593413103000, + "y": 0, + }, + Object { + "x": 1593413104000, + "y": 0, + }, + Object { + "x": 1593413105000, + "y": 0, + }, + Object { + "x": 1593413106000, + "y": 0, + }, + Object { + "x": 1593413107000, + "y": 0, + }, + Object { + "x": 1593413108000, + "y": 0, + }, + Object { + "x": 1593413109000, + "y": 0, + }, + Object { + "x": 1593413110000, + "y": 0, + }, + Object { + "x": 1593413111000, + "y": 0, + }, + Object { + "x": 1593413112000, + "y": 0, + }, + Object { + "x": 1593413113000, + "y": 0, + }, + Object { + "x": 1593413114000, + "y": 0, + }, + Object { + "x": 1593413115000, + "y": 0, + }, + Object { + "x": 1593413116000, + "y": 0, + }, + Object { + "x": 1593413117000, + "y": 0, + }, + Object { + "x": 1593413118000, + "y": 0, + }, + Object { + "x": 1593413119000, + "y": 0, + }, + Object { + "x": 1593413120000, + "y": 0, + }, + Object { + "x": 1593413121000, + "y": 0, + }, + Object { + "x": 1593413122000, + "y": 0, + }, + Object { + "x": 1593413123000, + "y": 0, + }, + Object { + "x": 1593413124000, + "y": 0, + }, + Object { + "x": 1593413125000, + "y": 0, + }, + Object { + "x": 1593413126000, + "y": 0, + }, + Object { + "x": 1593413127000, + "y": 0, + }, + Object { + "x": 1593413128000, + "y": 0, + }, + Object { + "x": 1593413129000, + "y": 0, + }, + Object { + "x": 1593413130000, + "y": 0, + }, + Object { + "x": 1593413131000, + "y": 0, + }, + Object { + "x": 1593413132000, + "y": 0, + }, + Object { + "x": 1593413133000, + "y": 0, + }, + Object { + "x": 1593413134000, + "y": 0, + }, + Object { + "x": 1593413135000, + "y": 0, + }, + Object { + "x": 1593413136000, + "y": 0, + }, + Object { + "x": 1593413137000, + "y": 0, + }, + Object { + "x": 1593413138000, + "y": 0, + }, + Object { + "x": 1593413139000, + "y": 0, + }, + Object { + "x": 1593413140000, + "y": 0, + }, + Object { + "x": 1593413141000, + "y": 0, + }, + Object { + "x": 1593413142000, + "y": 0, + }, + Object { + "x": 1593413143000, + "y": 0, + }, + Object { + "x": 1593413144000, + "y": 0, + }, + Object { + "x": 1593413145000, + "y": 0, + }, + Object { + "x": 1593413146000, + "y": 0, + }, + Object { + "x": 1593413147000, + "y": 0, + }, + Object { + "x": 1593413148000, + "y": 0, + }, + Object { + "x": 1593413149000, + "y": 0, + }, + Object { + "x": 1593413150000, + "y": 0, + }, + Object { + "x": 1593413151000, + "y": 0, + }, + Object { + "x": 1593413152000, + "y": 0, + }, + Object { + "x": 1593413153000, + "y": 0, + }, + Object { + "x": 1593413154000, + "y": 0, + }, + Object { + "x": 1593413155000, + "y": 0, + }, + Object { + "x": 1593413156000, + "y": 0, + }, + Object { + "x": 1593413157000, + "y": 0, + }, + Object { + "x": 1593413158000, + "y": 0, + }, + Object { + "x": 1593413159000, + "y": 0, + }, + Object { + "x": 1593413160000, + "y": 0, + }, + Object { + "x": 1593413161000, + "y": 0, + }, + Object { + "x": 1593413162000, + "y": 0, + }, + Object { + "x": 1593413163000, + "y": 0, + }, + Object { + "x": 1593413164000, + "y": 0, + }, + Object { + "x": 1593413165000, + "y": 0, + }, + Object { + "x": 1593413166000, + "y": 0, + }, + Object { + "x": 1593413167000, + "y": 0, + }, + Object { + "x": 1593413168000, + "y": 0, + }, + Object { + "x": 1593413169000, + "y": 0, + }, + Object { + "x": 1593413170000, + "y": 0, + }, + Object { + "x": 1593413171000, + "y": 0, + }, + Object { + "x": 1593413172000, + "y": 0, + }, + Object { + "x": 1593413173000, + "y": 0, + }, + Object { + "x": 1593413174000, + "y": 0, + }, + Object { + "x": 1593413175000, + "y": 0, + }, + Object { + "x": 1593413176000, + "y": 0, + }, + Object { + "x": 1593413177000, + "y": 0, + }, + Object { + "x": 1593413178000, + "y": 0, + }, + Object { + "x": 1593413179000, + "y": 0, + }, + Object { + "x": 1593413180000, + "y": 0, + }, + Object { + "x": 1593413181000, + "y": 0, + }, + Object { + "x": 1593413182000, + "y": 0, + }, + Object { + "x": 1593413183000, + "y": 0, + }, + Object { + "x": 1593413184000, + "y": 0, + }, + Object { + "x": 1593413185000, + "y": 0, + }, + Object { + "x": 1593413186000, + "y": 0, + }, + Object { + "x": 1593413187000, + "y": 0, + }, + Object { + "x": 1593413188000, + "y": 0, + }, + Object { + "x": 1593413189000, + "y": 0, + }, + Object { + "x": 1593413190000, + "y": 0, + }, + Object { + "x": 1593413191000, + "y": 0, + }, + Object { + "x": 1593413192000, + "y": 0, + }, + Object { + "x": 1593413193000, + "y": 0, + }, + Object { + "x": 1593413194000, + "y": 0, + }, + Object { + "x": 1593413195000, + "y": 0, + }, + Object { + "x": 1593413196000, + "y": 0, + }, + Object { + "x": 1593413197000, + "y": 0, + }, + Object { + "x": 1593413198000, + "y": 0, + }, + Object { + "x": 1593413199000, + "y": 0, + }, + Object { + "x": 1593413200000, + "y": 0, + }, + Object { + "x": 1593413201000, + "y": 0, + }, + Object { + "x": 1593413202000, + "y": 0, + }, + Object { + "x": 1593413203000, + "y": 0, + }, + Object { + "x": 1593413204000, + "y": 0, + }, + Object { + "x": 1593413205000, + "y": 0, + }, + Object { + "x": 1593413206000, + "y": 0, + }, + Object { + "x": 1593413207000, + "y": 0, + }, + Object { + "x": 1593413208000, + "y": 0, + }, + Object { + "x": 1593413209000, + "y": 0, + }, + Object { + "x": 1593413210000, + "y": 0, + }, + Object { + "x": 1593413211000, + "y": 0, + }, + Object { + "x": 1593413212000, + "y": 0, + }, + Object { + "x": 1593413213000, + "y": 0, + }, + Object { + "x": 1593413214000, + "y": 0, + }, + Object { + "x": 1593413215000, + "y": 0, + }, + Object { + "x": 1593413216000, + "y": 0, + }, + Object { + "x": 1593413217000, + "y": 0, + }, + Object { + "x": 1593413218000, + "y": 0, + }, + Object { + "x": 1593413219000, + "y": 0, + }, + Object { + "x": 1593413220000, + "y": 0, + }, + Object { + "x": 1593413221000, + "y": 0, + }, + Object { + "x": 1593413222000, + "y": 0, + }, + Object { + "x": 1593413223000, + "y": 0, + }, + Object { + "x": 1593413224000, + "y": 0, + }, + Object { + "x": 1593413225000, + "y": 0, + }, + Object { + "x": 1593413226000, + "y": 0, + }, + Object { + "x": 1593413227000, + "y": 0, + }, + Object { + "x": 1593413228000, + "y": 0, + }, + Object { + "x": 1593413229000, + "y": 0, + }, + Object { + "x": 1593413230000, + "y": 0, + }, + Object { + "x": 1593413231000, + "y": 0, + }, + Object { + "x": 1593413232000, + "y": 0, + }, + Object { + "x": 1593413233000, + "y": 0, + }, + Object { + "x": 1593413234000, + "y": 0, + }, + Object { + "x": 1593413235000, + "y": 0, + }, + Object { + "x": 1593413236000, + "y": 0, + }, + Object { + "x": 1593413237000, + "y": 0, + }, + Object { + "x": 1593413238000, + "y": 0, + }, + Object { + "x": 1593413239000, + "y": 0, + }, + Object { + "x": 1593413240000, + "y": 0, + }, + Object { + "x": 1593413241000, + "y": 0, + }, + Object { + "x": 1593413242000, + "y": 0, + }, + Object { + "x": 1593413243000, + "y": 0, + }, + Object { + "x": 1593413244000, + "y": 0, + }, + Object { + "x": 1593413245000, + "y": 0, + }, + Object { + "x": 1593413246000, + "y": 0, + }, + Object { + "x": 1593413247000, + "y": 0, + }, + Object { + "x": 1593413248000, + "y": 0, + }, + Object { + "x": 1593413249000, + "y": 0, + }, + Object { + "x": 1593413250000, + "y": 0, + }, + Object { + "x": 1593413251000, + "y": 0, + }, + Object { + "x": 1593413252000, + "y": 0, + }, + Object { + "x": 1593413253000, + "y": 0, + }, + Object { + "x": 1593413254000, + "y": 0, + }, + Object { + "x": 1593413255000, + "y": 0, + }, + Object { + "x": 1593413256000, + "y": 0, + }, + Object { + "x": 1593413257000, + "y": 0, + }, + Object { + "x": 1593413258000, + "y": 0, + }, + Object { + "x": 1593413259000, + "y": 0, + }, + Object { + "x": 1593413260000, + "y": 0, + }, + Object { + "x": 1593413261000, + "y": 0, + }, + Object { + "x": 1593413262000, + "y": 0, + }, + Object { + "x": 1593413263000, + "y": 0, + }, + Object { + "x": 1593413264000, + "y": 0, + }, + Object { + "x": 1593413265000, + "y": 0, + }, + Object { + "x": 1593413266000, + "y": 0, + }, + Object { + "x": 1593413267000, + "y": 0, + }, + Object { + "x": 1593413268000, + "y": 0, + }, + Object { + "x": 1593413269000, + "y": 0, + }, + Object { + "x": 1593413270000, + "y": 0, + }, + Object { + "x": 1593413271000, + "y": 0, + }, + Object { + "x": 1593413272000, + "y": 1, + }, + Object { + "x": 1593413273000, + "y": 2, + }, + Object { + "x": 1593413274000, + "y": 0, + }, + Object { + "x": 1593413275000, + "y": 0, + }, + Object { + "x": 1593413276000, + "y": 0, + }, + Object { + "x": 1593413277000, + "y": 1, + }, + Object { + "x": 1593413278000, + "y": 0, + }, + Object { + "x": 1593413279000, + "y": 0, + }, + Object { + "x": 1593413280000, + "y": 0, + }, + Object { + "x": 1593413281000, + "y": 1, + }, + Object { + "x": 1593413282000, + "y": 0, + }, + Object { + "x": 1593413283000, + "y": 0, + }, + Object { + "x": 1593413284000, + "y": 2, + }, + Object { + "x": 1593413285000, + "y": 2, + }, + Object { + "x": 1593413286000, + "y": 7, + }, + Object { + "x": 1593413287000, + "y": 1, + }, + Object { + "x": 1593413288000, + "y": 2, + }, + Object { + "x": 1593413289000, + "y": 1, + }, + Object { + "x": 1593413290000, + "y": 4, + }, + Object { + "x": 1593413291000, + "y": 2, + }, + Object { + "x": 1593413292000, + "y": 1, + }, + Object { + "x": 1593413293000, + "y": 2, + }, + Object { + "x": 1593413294000, + "y": 3, + }, + Object { + "x": 1593413295000, + "y": 2, + }, + Object { + "x": 1593413296000, + "y": 2, + }, + Object { + "x": 1593413297000, + "y": 2, + }, + Object { + "x": 1593413298000, + "y": 6, + }, + Object { + "x": 1593413299000, + "y": 1, + }, + Object { + "x": 1593413300000, + "y": 2, + }, + Object { + "x": 1593413301000, + "y": 3, + }, + Object { + "x": 1593413302000, + "y": 2, + }, + Object { + "x": 1593413303000, + "y": 2, + }, + Object { + "x": 1593413304000, + "y": 2, + }, + Object { + "x": 1593413305000, + "y": 1, + }, + Object { + "x": 1593413306000, + "y": 2, + }, + Object { + "x": 1593413307000, + "y": 3, + }, + Object { + "x": 1593413308000, + "y": 2, + }, + Object { + "x": 1593413309000, + "y": 2, + }, + Object { + "x": 1593413310000, + "y": 2, + }, + Object { + "x": 1593413311000, + "y": 1, + }, + Object { + "x": 1593413312000, + "y": 3, + }, + Object { + "x": 1593413313000, + "y": 3, + }, + Object { + "x": 1593413314000, + "y": 5, + }, + Object { + "x": 1593413315000, + "y": 2, + }, + Object { + "x": 1593413316000, + "y": 2, + }, + Object { + "x": 1593413317000, + "y": 6, + }, + Object { + "x": 1593413318000, + "y": 2, + }, + Object { + "x": 1593413319000, + "y": 2, + }, + Object { + "x": 1593413320000, + "y": 2, + }, + Object { + "x": 1593413321000, + "y": 2, + }, + Object { + "x": 1593413322000, + "y": 1, + }, + Object { + "x": 1593413323000, + "y": 0, + }, + Object { + "x": 1593413324000, + "y": 0, + }, + Object { + "x": 1593413325000, + "y": 0, + }, + Object { + "x": 1593413326000, + "y": 0, + }, + Object { + "x": 1593413327000, + "y": 0, + }, + Object { + "x": 1593413328000, + "y": 0, + }, + Object { + "x": 1593413329000, + "y": 0, + }, + Object { + "x": 1593413330000, + "y": 0, + }, + Object { + "x": 1593413331000, + "y": 0, + }, + Object { + "x": 1593413332000, + "y": 0, + }, + Object { + "x": 1593413333000, + "y": 0, + }, + Object { + "x": 1593413334000, + "y": 0, + }, + Object { + "x": 1593413335000, + "y": 0, + }, + Object { + "x": 1593413336000, + "y": 0, + }, + Object { + "x": 1593413337000, + "y": 0, + }, + Object { + "x": 1593413338000, + "y": 0, + }, + Object { + "x": 1593413339000, + "y": 0, + }, + Object { + "x": 1593413340000, + "y": 0, + }, + ], + "key": "HTTP 2xx", + }, + Object { + "avg": 1.75, + "dataPoints": Array [ + Object { + "x": 1593413100000, + "y": 0, + }, + Object { + "x": 1593413101000, + "y": 0, + }, + Object { + "x": 1593413102000, + "y": 0, + }, + Object { + "x": 1593413103000, + "y": 0, + }, + Object { + "x": 1593413104000, + "y": 0, + }, + Object { + "x": 1593413105000, + "y": 0, + }, + Object { + "x": 1593413106000, + "y": 0, + }, + Object { + "x": 1593413107000, + "y": 0, + }, + Object { + "x": 1593413108000, + "y": 0, + }, + Object { + "x": 1593413109000, + "y": 0, + }, + Object { + "x": 1593413110000, + "y": 0, + }, + Object { + "x": 1593413111000, + "y": 0, + }, + Object { + "x": 1593413112000, + "y": 0, + }, + Object { + "x": 1593413113000, + "y": 0, + }, + Object { + "x": 1593413114000, + "y": 0, + }, + Object { + "x": 1593413115000, + "y": 0, + }, + Object { + "x": 1593413116000, + "y": 0, + }, + Object { + "x": 1593413117000, + "y": 0, + }, + Object { + "x": 1593413118000, + "y": 0, + }, + Object { + "x": 1593413119000, + "y": 0, + }, + Object { + "x": 1593413120000, + "y": 0, + }, + Object { + "x": 1593413121000, + "y": 0, + }, + Object { + "x": 1593413122000, + "y": 0, + }, + Object { + "x": 1593413123000, + "y": 0, + }, + Object { + "x": 1593413124000, + "y": 0, + }, + Object { + "x": 1593413125000, + "y": 0, + }, + Object { + "x": 1593413126000, + "y": 0, + }, + Object { + "x": 1593413127000, + "y": 0, + }, + Object { + "x": 1593413128000, + "y": 0, + }, + Object { + "x": 1593413129000, + "y": 0, + }, + Object { + "x": 1593413130000, + "y": 0, + }, + Object { + "x": 1593413131000, + "y": 0, + }, + Object { + "x": 1593413132000, + "y": 0, + }, + Object { + "x": 1593413133000, + "y": 0, + }, + Object { + "x": 1593413134000, + "y": 0, + }, + Object { + "x": 1593413135000, + "y": 0, + }, + Object { + "x": 1593413136000, + "y": 0, + }, + Object { + "x": 1593413137000, + "y": 0, + }, + Object { + "x": 1593413138000, + "y": 0, + }, + Object { + "x": 1593413139000, + "y": 0, + }, + Object { + "x": 1593413140000, + "y": 0, + }, + Object { + "x": 1593413141000, + "y": 0, + }, + Object { + "x": 1593413142000, + "y": 0, + }, + Object { + "x": 1593413143000, + "y": 0, + }, + Object { + "x": 1593413144000, + "y": 0, + }, + Object { + "x": 1593413145000, + "y": 0, + }, + Object { + "x": 1593413146000, + "y": 0, + }, + Object { + "x": 1593413147000, + "y": 0, + }, + Object { + "x": 1593413148000, + "y": 0, + }, + Object { + "x": 1593413149000, + "y": 0, + }, + Object { + "x": 1593413150000, + "y": 0, + }, + Object { + "x": 1593413151000, + "y": 0, + }, + Object { + "x": 1593413152000, + "y": 0, + }, + Object { + "x": 1593413153000, + "y": 0, + }, + Object { + "x": 1593413154000, + "y": 0, + }, + Object { + "x": 1593413155000, + "y": 0, + }, + Object { + "x": 1593413156000, + "y": 0, + }, + Object { + "x": 1593413157000, + "y": 0, + }, + Object { + "x": 1593413158000, + "y": 0, + }, + Object { + "x": 1593413159000, + "y": 0, + }, + Object { + "x": 1593413160000, + "y": 0, + }, + Object { + "x": 1593413161000, + "y": 0, + }, + Object { + "x": 1593413162000, + "y": 0, + }, + Object { + "x": 1593413163000, + "y": 0, + }, + Object { + "x": 1593413164000, + "y": 0, + }, + Object { + "x": 1593413165000, + "y": 0, + }, + Object { + "x": 1593413166000, + "y": 0, + }, + Object { + "x": 1593413167000, + "y": 0, + }, + Object { + "x": 1593413168000, + "y": 0, + }, + Object { + "x": 1593413169000, + "y": 0, + }, + Object { + "x": 1593413170000, + "y": 0, + }, + Object { + "x": 1593413171000, + "y": 0, + }, + Object { + "x": 1593413172000, + "y": 0, + }, + Object { + "x": 1593413173000, + "y": 0, + }, + Object { + "x": 1593413174000, + "y": 0, + }, + Object { + "x": 1593413175000, + "y": 0, + }, + Object { + "x": 1593413176000, + "y": 0, + }, + Object { + "x": 1593413177000, + "y": 0, + }, + Object { + "x": 1593413178000, + "y": 0, + }, + Object { + "x": 1593413179000, + "y": 0, + }, + Object { + "x": 1593413180000, + "y": 0, + }, + Object { + "x": 1593413181000, + "y": 0, + }, + Object { + "x": 1593413182000, + "y": 0, + }, + Object { + "x": 1593413183000, + "y": 0, + }, + Object { + "x": 1593413184000, + "y": 0, + }, + Object { + "x": 1593413185000, + "y": 0, + }, + Object { + "x": 1593413186000, + "y": 0, + }, + Object { + "x": 1593413187000, + "y": 0, + }, + Object { + "x": 1593413188000, + "y": 0, + }, + Object { + "x": 1593413189000, + "y": 0, + }, + Object { + "x": 1593413190000, + "y": 0, + }, + Object { + "x": 1593413191000, + "y": 0, + }, + Object { + "x": 1593413192000, + "y": 0, + }, + Object { + "x": 1593413193000, + "y": 0, + }, + Object { + "x": 1593413194000, + "y": 0, + }, + Object { + "x": 1593413195000, + "y": 0, + }, + Object { + "x": 1593413196000, + "y": 0, + }, + Object { + "x": 1593413197000, + "y": 0, + }, + Object { + "x": 1593413198000, + "y": 0, + }, + Object { + "x": 1593413199000, + "y": 0, + }, + Object { + "x": 1593413200000, + "y": 0, + }, + Object { + "x": 1593413201000, + "y": 0, + }, + Object { + "x": 1593413202000, + "y": 0, + }, + Object { + "x": 1593413203000, + "y": 0, + }, + Object { + "x": 1593413204000, + "y": 0, + }, + Object { + "x": 1593413205000, + "y": 0, + }, + Object { + "x": 1593413206000, + "y": 0, + }, + Object { + "x": 1593413207000, + "y": 0, + }, + Object { + "x": 1593413208000, + "y": 0, + }, + Object { + "x": 1593413209000, + "y": 0, + }, + Object { + "x": 1593413210000, + "y": 0, + }, + Object { + "x": 1593413211000, + "y": 0, + }, + Object { + "x": 1593413212000, + "y": 0, + }, + Object { + "x": 1593413213000, + "y": 0, + }, + Object { + "x": 1593413214000, + "y": 0, + }, + Object { + "x": 1593413215000, + "y": 0, + }, + Object { + "x": 1593413216000, + "y": 0, + }, + Object { + "x": 1593413217000, + "y": 0, + }, + Object { + "x": 1593413218000, + "y": 0, + }, + Object { + "x": 1593413219000, + "y": 0, + }, + Object { + "x": 1593413220000, + "y": 0, + }, + Object { + "x": 1593413221000, + "y": 0, + }, + Object { + "x": 1593413222000, + "y": 0, + }, + Object { + "x": 1593413223000, + "y": 0, + }, + Object { + "x": 1593413224000, + "y": 0, + }, + Object { + "x": 1593413225000, + "y": 0, + }, + Object { + "x": 1593413226000, + "y": 0, + }, + Object { + "x": 1593413227000, + "y": 0, + }, + Object { + "x": 1593413228000, + "y": 0, + }, + Object { + "x": 1593413229000, + "y": 0, + }, + Object { + "x": 1593413230000, + "y": 0, + }, + Object { + "x": 1593413231000, + "y": 0, + }, + Object { + "x": 1593413232000, + "y": 0, + }, + Object { + "x": 1593413233000, + "y": 0, + }, + Object { + "x": 1593413234000, + "y": 0, + }, + Object { + "x": 1593413235000, + "y": 0, + }, + Object { + "x": 1593413236000, + "y": 0, + }, + Object { + "x": 1593413237000, + "y": 0, + }, + Object { + "x": 1593413238000, + "y": 0, + }, + Object { + "x": 1593413239000, + "y": 0, + }, + Object { + "x": 1593413240000, + "y": 0, + }, + Object { + "x": 1593413241000, + "y": 0, + }, + Object { + "x": 1593413242000, + "y": 0, + }, + Object { + "x": 1593413243000, + "y": 0, + }, + Object { + "x": 1593413244000, + "y": 0, + }, + Object { + "x": 1593413245000, + "y": 0, + }, + Object { + "x": 1593413246000, + "y": 0, + }, + Object { + "x": 1593413247000, + "y": 0, + }, + Object { + "x": 1593413248000, + "y": 0, + }, + Object { + "x": 1593413249000, + "y": 0, + }, + Object { + "x": 1593413250000, + "y": 0, + }, + Object { + "x": 1593413251000, + "y": 0, + }, + Object { + "x": 1593413252000, + "y": 0, + }, + Object { + "x": 1593413253000, + "y": 0, + }, + Object { + "x": 1593413254000, + "y": 0, + }, + Object { + "x": 1593413255000, + "y": 0, + }, + Object { + "x": 1593413256000, + "y": 0, + }, + Object { + "x": 1593413257000, + "y": 0, + }, + Object { + "x": 1593413258000, + "y": 0, + }, + Object { + "x": 1593413259000, + "y": 0, + }, + Object { + "x": 1593413260000, + "y": 0, + }, + Object { + "x": 1593413261000, + "y": 0, + }, + Object { + "x": 1593413262000, + "y": 0, + }, + Object { + "x": 1593413263000, + "y": 0, + }, + Object { + "x": 1593413264000, + "y": 0, + }, + Object { + "x": 1593413265000, + "y": 0, + }, + Object { + "x": 1593413266000, + "y": 0, + }, + Object { + "x": 1593413267000, + "y": 0, + }, + Object { + "x": 1593413268000, + "y": 0, + }, + Object { + "x": 1593413269000, + "y": 0, + }, + Object { + "x": 1593413270000, + "y": 0, + }, + Object { + "x": 1593413271000, + "y": 0, + }, + Object { + "x": 1593413272000, + "y": 0, + }, + Object { + "x": 1593413273000, + "y": 0, + }, + Object { + "x": 1593413274000, + "y": 0, + }, + Object { + "x": 1593413275000, + "y": 0, + }, + Object { + "x": 1593413276000, + "y": 0, + }, + Object { + "x": 1593413277000, + "y": 0, + }, + Object { + "x": 1593413278000, + "y": 0, + }, + Object { + "x": 1593413279000, + "y": 0, + }, + Object { + "x": 1593413280000, + "y": 0, + }, + Object { + "x": 1593413281000, + "y": 0, + }, + Object { + "x": 1593413282000, + "y": 0, + }, + Object { + "x": 1593413283000, + "y": 0, + }, + Object { + "x": 1593413284000, + "y": 0, + }, + Object { + "x": 1593413285000, + "y": 0, + }, + Object { + "x": 1593413286000, + "y": 0, + }, + Object { + "x": 1593413287000, + "y": 0, + }, + Object { + "x": 1593413288000, + "y": 0, + }, + Object { + "x": 1593413289000, + "y": 0, + }, + Object { + "x": 1593413290000, + "y": 0, + }, + Object { + "x": 1593413291000, + "y": 0, + }, + Object { + "x": 1593413292000, + "y": 0, + }, + Object { + "x": 1593413293000, + "y": 0, + }, + Object { + "x": 1593413294000, + "y": 0, + }, + Object { + "x": 1593413295000, + "y": 0, + }, + Object { + "x": 1593413296000, + "y": 0, + }, + Object { + "x": 1593413297000, + "y": 0, + }, + Object { + "x": 1593413298000, + "y": 2, + }, + Object { + "x": 1593413299000, + "y": 0, + }, + Object { + "x": 1593413300000, + "y": 0, + }, + Object { + "x": 1593413301000, + "y": 3, + }, + Object { + "x": 1593413302000, + "y": 0, + }, + Object { + "x": 1593413303000, + "y": 0, + }, + Object { + "x": 1593413304000, + "y": 0, + }, + Object { + "x": 1593413305000, + "y": 0, + }, + Object { + "x": 1593413306000, + "y": 0, + }, + Object { + "x": 1593413307000, + "y": 0, + }, + Object { + "x": 1593413308000, + "y": 0, + }, + Object { + "x": 1593413309000, + "y": 0, + }, + Object { + "x": 1593413310000, + "y": 0, + }, + Object { + "x": 1593413311000, + "y": 0, + }, + Object { + "x": 1593413312000, + "y": 0, + }, + Object { + "x": 1593413313000, + "y": 0, + }, + Object { + "x": 1593413314000, + "y": 0, + }, + Object { + "x": 1593413315000, + "y": 0, + }, + Object { + "x": 1593413316000, + "y": 0, + }, + Object { + "x": 1593413317000, + "y": 2, + }, + Object { + "x": 1593413318000, + "y": 0, + }, + Object { + "x": 1593413319000, + "y": 0, + }, + Object { + "x": 1593413320000, + "y": 0, + }, + Object { + "x": 1593413321000, + "y": 0, + }, + Object { + "x": 1593413322000, + "y": 0, + }, + Object { + "x": 1593413323000, + "y": 0, + }, + Object { + "x": 1593413324000, + "y": 0, + }, + Object { + "x": 1593413325000, + "y": 0, + }, + Object { + "x": 1593413326000, + "y": 0, + }, + Object { + "x": 1593413327000, + "y": 0, + }, + Object { + "x": 1593413328000, + "y": 0, + }, + Object { + "x": 1593413329000, + "y": 0, + }, + Object { + "x": 1593413330000, + "y": 0, + }, + Object { + "x": 1593413331000, + "y": 0, + }, + Object { + "x": 1593413332000, + "y": 0, + }, + Object { + "x": 1593413333000, + "y": 0, + }, + Object { + "x": 1593413334000, + "y": 0, + }, + Object { + "x": 1593413335000, + "y": 0, + }, + Object { + "x": 1593413336000, + "y": 0, + }, + Object { + "x": 1593413337000, + "y": 0, + }, + Object { + "x": 1593413338000, + "y": 0, + }, + Object { + "x": 1593413339000, + "y": 0, + }, + Object { + "x": 1593413340000, + "y": 0, + }, + ], + "key": "HTTP 3xx", + }, + Object { + "avg": 2, + "dataPoints": Array [ + Object { + "x": 1593413100000, + "y": 0, + }, + Object { + "x": 1593413101000, + "y": 0, + }, + Object { + "x": 1593413102000, + "y": 0, + }, + Object { + "x": 1593413103000, + "y": 0, + }, + Object { + "x": 1593413104000, + "y": 0, + }, + Object { + "x": 1593413105000, + "y": 0, + }, + Object { + "x": 1593413106000, + "y": 0, + }, + Object { + "x": 1593413107000, + "y": 0, + }, + Object { + "x": 1593413108000, + "y": 0, + }, + Object { + "x": 1593413109000, + "y": 0, + }, + Object { + "x": 1593413110000, + "y": 0, + }, + Object { + "x": 1593413111000, + "y": 0, + }, + Object { + "x": 1593413112000, + "y": 0, + }, + Object { + "x": 1593413113000, + "y": 0, + }, + Object { + "x": 1593413114000, + "y": 0, + }, + Object { + "x": 1593413115000, + "y": 0, + }, + Object { + "x": 1593413116000, + "y": 0, + }, + Object { + "x": 1593413117000, + "y": 0, + }, + Object { + "x": 1593413118000, + "y": 0, + }, + Object { + "x": 1593413119000, + "y": 0, + }, + Object { + "x": 1593413120000, + "y": 0, + }, + Object { + "x": 1593413121000, + "y": 0, + }, + Object { + "x": 1593413122000, + "y": 0, + }, + Object { + "x": 1593413123000, + "y": 0, + }, + Object { + "x": 1593413124000, + "y": 0, + }, + Object { + "x": 1593413125000, + "y": 0, + }, + Object { + "x": 1593413126000, + "y": 0, + }, + Object { + "x": 1593413127000, + "y": 0, + }, + Object { + "x": 1593413128000, + "y": 0, + }, + Object { + "x": 1593413129000, + "y": 0, + }, + Object { + "x": 1593413130000, + "y": 0, + }, + Object { + "x": 1593413131000, + "y": 0, + }, + Object { + "x": 1593413132000, + "y": 0, + }, + Object { + "x": 1593413133000, + "y": 0, + }, + Object { + "x": 1593413134000, + "y": 0, + }, + Object { + "x": 1593413135000, + "y": 0, + }, + Object { + "x": 1593413136000, + "y": 0, + }, + Object { + "x": 1593413137000, + "y": 0, + }, + Object { + "x": 1593413138000, + "y": 0, + }, + Object { + "x": 1593413139000, + "y": 0, + }, + Object { + "x": 1593413140000, + "y": 0, + }, + Object { + "x": 1593413141000, + "y": 0, + }, + Object { + "x": 1593413142000, + "y": 0, + }, + Object { + "x": 1593413143000, + "y": 0, + }, + Object { + "x": 1593413144000, + "y": 0, + }, + Object { + "x": 1593413145000, + "y": 0, + }, + Object { + "x": 1593413146000, + "y": 0, + }, + Object { + "x": 1593413147000, + "y": 0, + }, + Object { + "x": 1593413148000, + "y": 0, + }, + Object { + "x": 1593413149000, + "y": 0, + }, + Object { + "x": 1593413150000, + "y": 0, + }, + Object { + "x": 1593413151000, + "y": 0, + }, + Object { + "x": 1593413152000, + "y": 0, + }, + Object { + "x": 1593413153000, + "y": 0, + }, + Object { + "x": 1593413154000, + "y": 0, + }, + Object { + "x": 1593413155000, + "y": 0, + }, + Object { + "x": 1593413156000, + "y": 0, + }, + Object { + "x": 1593413157000, + "y": 0, + }, + Object { + "x": 1593413158000, + "y": 0, + }, + Object { + "x": 1593413159000, + "y": 0, + }, + Object { + "x": 1593413160000, + "y": 0, + }, + Object { + "x": 1593413161000, + "y": 0, + }, + Object { + "x": 1593413162000, + "y": 0, + }, + Object { + "x": 1593413163000, + "y": 0, + }, + Object { + "x": 1593413164000, + "y": 0, + }, + Object { + "x": 1593413165000, + "y": 0, + }, + Object { + "x": 1593413166000, + "y": 0, + }, + Object { + "x": 1593413167000, + "y": 0, + }, + Object { + "x": 1593413168000, + "y": 0, + }, + Object { + "x": 1593413169000, + "y": 0, + }, + Object { + "x": 1593413170000, + "y": 0, + }, + Object { + "x": 1593413171000, + "y": 0, + }, + Object { + "x": 1593413172000, + "y": 0, + }, + Object { + "x": 1593413173000, + "y": 0, + }, + Object { + "x": 1593413174000, + "y": 0, + }, + Object { + "x": 1593413175000, + "y": 0, + }, + Object { + "x": 1593413176000, + "y": 0, + }, + Object { + "x": 1593413177000, + "y": 0, + }, + Object { + "x": 1593413178000, + "y": 0, + }, + Object { + "x": 1593413179000, + "y": 0, + }, + Object { + "x": 1593413180000, + "y": 0, + }, + Object { + "x": 1593413181000, + "y": 0, + }, + Object { + "x": 1593413182000, + "y": 0, + }, + Object { + "x": 1593413183000, + "y": 0, + }, + Object { + "x": 1593413184000, + "y": 0, + }, + Object { + "x": 1593413185000, + "y": 0, + }, + Object { + "x": 1593413186000, + "y": 0, + }, + Object { + "x": 1593413187000, + "y": 0, + }, + Object { + "x": 1593413188000, + "y": 0, + }, + Object { + "x": 1593413189000, + "y": 0, + }, + Object { + "x": 1593413190000, + "y": 0, + }, + Object { + "x": 1593413191000, + "y": 0, + }, + Object { + "x": 1593413192000, + "y": 0, + }, + Object { + "x": 1593413193000, + "y": 0, + }, + Object { + "x": 1593413194000, + "y": 0, + }, + Object { + "x": 1593413195000, + "y": 0, + }, + Object { + "x": 1593413196000, + "y": 0, + }, + Object { + "x": 1593413197000, + "y": 0, + }, + Object { + "x": 1593413198000, + "y": 0, + }, + Object { + "x": 1593413199000, + "y": 0, + }, + Object { + "x": 1593413200000, + "y": 0, + }, + Object { + "x": 1593413201000, + "y": 0, + }, + Object { + "x": 1593413202000, + "y": 0, + }, + Object { + "x": 1593413203000, + "y": 0, + }, + Object { + "x": 1593413204000, + "y": 0, + }, + Object { + "x": 1593413205000, + "y": 0, + }, + Object { + "x": 1593413206000, + "y": 0, + }, + Object { + "x": 1593413207000, + "y": 0, + }, + Object { + "x": 1593413208000, + "y": 0, + }, + Object { + "x": 1593413209000, + "y": 0, + }, + Object { + "x": 1593413210000, + "y": 0, + }, + Object { + "x": 1593413211000, + "y": 0, + }, + Object { + "x": 1593413212000, + "y": 0, + }, + Object { + "x": 1593413213000, + "y": 0, + }, + Object { + "x": 1593413214000, + "y": 0, + }, + Object { + "x": 1593413215000, + "y": 0, + }, + Object { + "x": 1593413216000, + "y": 0, + }, + Object { + "x": 1593413217000, + "y": 0, + }, + Object { + "x": 1593413218000, + "y": 0, + }, + Object { + "x": 1593413219000, + "y": 0, + }, + Object { + "x": 1593413220000, + "y": 0, + }, + Object { + "x": 1593413221000, + "y": 0, + }, + Object { + "x": 1593413222000, + "y": 0, + }, + Object { + "x": 1593413223000, + "y": 0, + }, + Object { + "x": 1593413224000, + "y": 0, + }, + Object { + "x": 1593413225000, + "y": 0, + }, + Object { + "x": 1593413226000, + "y": 0, + }, + Object { + "x": 1593413227000, + "y": 0, + }, + Object { + "x": 1593413228000, + "y": 0, + }, + Object { + "x": 1593413229000, + "y": 0, + }, + Object { + "x": 1593413230000, + "y": 0, + }, + Object { + "x": 1593413231000, + "y": 0, + }, + Object { + "x": 1593413232000, + "y": 0, + }, + Object { + "x": 1593413233000, + "y": 0, + }, + Object { + "x": 1593413234000, + "y": 0, + }, + Object { + "x": 1593413235000, + "y": 0, + }, + Object { + "x": 1593413236000, + "y": 0, + }, + Object { + "x": 1593413237000, + "y": 0, + }, + Object { + "x": 1593413238000, + "y": 0, + }, + Object { + "x": 1593413239000, + "y": 0, + }, + Object { + "x": 1593413240000, + "y": 0, + }, + Object { + "x": 1593413241000, + "y": 0, + }, + Object { + "x": 1593413242000, + "y": 0, + }, + Object { + "x": 1593413243000, + "y": 0, + }, + Object { + "x": 1593413244000, + "y": 0, + }, + Object { + "x": 1593413245000, + "y": 0, + }, + Object { + "x": 1593413246000, + "y": 0, + }, + Object { + "x": 1593413247000, + "y": 0, + }, + Object { + "x": 1593413248000, + "y": 0, + }, + Object { + "x": 1593413249000, + "y": 0, + }, + Object { + "x": 1593413250000, + "y": 0, + }, + Object { + "x": 1593413251000, + "y": 0, + }, + Object { + "x": 1593413252000, + "y": 0, + }, + Object { + "x": 1593413253000, + "y": 0, + }, + Object { + "x": 1593413254000, + "y": 0, + }, + Object { + "x": 1593413255000, + "y": 0, + }, + Object { + "x": 1593413256000, + "y": 0, + }, + Object { + "x": 1593413257000, + "y": 0, + }, + Object { + "x": 1593413258000, + "y": 0, + }, + Object { + "x": 1593413259000, + "y": 0, + }, + Object { + "x": 1593413260000, + "y": 0, + }, + Object { + "x": 1593413261000, + "y": 0, + }, + Object { + "x": 1593413262000, + "y": 0, + }, + Object { + "x": 1593413263000, + "y": 0, + }, + Object { + "x": 1593413264000, + "y": 0, + }, + Object { + "x": 1593413265000, + "y": 0, + }, + Object { + "x": 1593413266000, + "y": 0, + }, + Object { + "x": 1593413267000, + "y": 0, + }, + Object { + "x": 1593413268000, + "y": 0, + }, + Object { + "x": 1593413269000, + "y": 0, + }, + Object { + "x": 1593413270000, + "y": 0, + }, + Object { + "x": 1593413271000, + "y": 0, + }, + Object { + "x": 1593413272000, + "y": 0, + }, + Object { + "x": 1593413273000, + "y": 0, + }, + Object { + "x": 1593413274000, + "y": 0, + }, + Object { + "x": 1593413275000, + "y": 0, + }, + Object { + "x": 1593413276000, + "y": 0, + }, + Object { + "x": 1593413277000, + "y": 0, + }, + Object { + "x": 1593413278000, + "y": 0, + }, + Object { + "x": 1593413279000, + "y": 0, + }, + Object { + "x": 1593413280000, + "y": 0, + }, + Object { + "x": 1593413281000, + "y": 0, + }, + Object { + "x": 1593413282000, + "y": 0, + }, + Object { + "x": 1593413283000, + "y": 0, + }, + Object { + "x": 1593413284000, + "y": 0, + }, + Object { + "x": 1593413285000, + "y": 0, + }, + Object { + "x": 1593413286000, + "y": 0, + }, + Object { + "x": 1593413287000, + "y": 0, + }, + Object { + "x": 1593413288000, + "y": 0, + }, + Object { + "x": 1593413289000, + "y": 1, + }, + Object { + "x": 1593413290000, + "y": 0, + }, + Object { + "x": 1593413291000, + "y": 0, + }, + Object { + "x": 1593413292000, + "y": 1, + }, + Object { + "x": 1593413293000, + "y": 0, + }, + Object { + "x": 1593413294000, + "y": 0, + }, + Object { + "x": 1593413295000, + "y": 0, + }, + Object { + "x": 1593413296000, + "y": 0, + }, + Object { + "x": 1593413297000, + "y": 0, + }, + Object { + "x": 1593413298000, + "y": 0, + }, + Object { + "x": 1593413299000, + "y": 0, + }, + Object { + "x": 1593413300000, + "y": 1, + }, + Object { + "x": 1593413301000, + "y": 0, + }, + Object { + "x": 1593413302000, + "y": 0, + }, + Object { + "x": 1593413303000, + "y": 0, + }, + Object { + "x": 1593413304000, + "y": 0, + }, + Object { + "x": 1593413305000, + "y": 1, + }, + Object { + "x": 1593413306000, + "y": 0, + }, + Object { + "x": 1593413307000, + "y": 0, + }, + Object { + "x": 1593413308000, + "y": 0, + }, + Object { + "x": 1593413309000, + "y": 1, + }, + Object { + "x": 1593413310000, + "y": 1, + }, + Object { + "x": 1593413311000, + "y": 0, + }, + Object { + "x": 1593413312000, + "y": 0, + }, + Object { + "x": 1593413313000, + "y": 0, + }, + Object { + "x": 1593413314000, + "y": 0, + }, + Object { + "x": 1593413315000, + "y": 1, + }, + Object { + "x": 1593413316000, + "y": 0, + }, + Object { + "x": 1593413317000, + "y": 0, + }, + Object { + "x": 1593413318000, + "y": 0, + }, + Object { + "x": 1593413319000, + "y": 0, + }, + Object { + "x": 1593413320000, + "y": 1, + }, + Object { + "x": 1593413321000, + "y": 0, + }, + Object { + "x": 1593413322000, + "y": 0, + }, + Object { + "x": 1593413323000, + "y": 0, + }, + Object { + "x": 1593413324000, + "y": 0, + }, + Object { + "x": 1593413325000, + "y": 0, + }, + Object { + "x": 1593413326000, + "y": 0, + }, + Object { + "x": 1593413327000, + "y": 0, + }, + Object { + "x": 1593413328000, + "y": 0, + }, + Object { + "x": 1593413329000, + "y": 0, + }, + Object { + "x": 1593413330000, + "y": 0, + }, + Object { + "x": 1593413331000, + "y": 0, + }, + Object { + "x": 1593413332000, + "y": 0, + }, + Object { + "x": 1593413333000, + "y": 0, + }, + Object { + "x": 1593413334000, + "y": 0, + }, + Object { + "x": 1593413335000, + "y": 0, + }, + Object { + "x": 1593413336000, + "y": 0, + }, + Object { + "x": 1593413337000, + "y": 0, + }, + Object { + "x": 1593413338000, + "y": 0, + }, + Object { + "x": 1593413339000, + "y": 0, + }, + Object { + "x": 1593413340000, + "y": 0, + }, + ], + "key": "HTTP 4xx", + }, + Object { + "avg": 2.25, + "dataPoints": Array [ + Object { + "x": 1593413100000, + "y": 0, + }, + Object { + "x": 1593413101000, + "y": 0, + }, + Object { + "x": 1593413102000, + "y": 0, + }, + Object { + "x": 1593413103000, + "y": 0, + }, + Object { + "x": 1593413104000, + "y": 0, + }, + Object { + "x": 1593413105000, + "y": 0, + }, + Object { + "x": 1593413106000, + "y": 0, + }, + Object { + "x": 1593413107000, + "y": 0, + }, + Object { + "x": 1593413108000, + "y": 0, + }, + Object { + "x": 1593413109000, + "y": 0, + }, + Object { + "x": 1593413110000, + "y": 0, + }, + Object { + "x": 1593413111000, + "y": 0, + }, + Object { + "x": 1593413112000, + "y": 0, + }, + Object { + "x": 1593413113000, + "y": 0, + }, + Object { + "x": 1593413114000, + "y": 0, + }, + Object { + "x": 1593413115000, + "y": 0, + }, + Object { + "x": 1593413116000, + "y": 0, + }, + Object { + "x": 1593413117000, + "y": 0, + }, + Object { + "x": 1593413118000, + "y": 0, + }, + Object { + "x": 1593413119000, + "y": 0, + }, + Object { + "x": 1593413120000, + "y": 0, + }, + Object { + "x": 1593413121000, + "y": 0, + }, + Object { + "x": 1593413122000, + "y": 0, + }, + Object { + "x": 1593413123000, + "y": 0, + }, + Object { + "x": 1593413124000, + "y": 0, + }, + Object { + "x": 1593413125000, + "y": 0, + }, + Object { + "x": 1593413126000, + "y": 0, + }, + Object { + "x": 1593413127000, + "y": 0, + }, + Object { + "x": 1593413128000, + "y": 0, + }, + Object { + "x": 1593413129000, + "y": 0, + }, + Object { + "x": 1593413130000, + "y": 0, + }, + Object { + "x": 1593413131000, + "y": 0, + }, + Object { + "x": 1593413132000, + "y": 0, + }, + Object { + "x": 1593413133000, + "y": 0, + }, + Object { + "x": 1593413134000, + "y": 0, + }, + Object { + "x": 1593413135000, + "y": 0, + }, + Object { + "x": 1593413136000, + "y": 0, + }, + Object { + "x": 1593413137000, + "y": 0, + }, + Object { + "x": 1593413138000, + "y": 0, + }, + Object { + "x": 1593413139000, + "y": 0, + }, + Object { + "x": 1593413140000, + "y": 0, + }, + Object { + "x": 1593413141000, + "y": 0, + }, + Object { + "x": 1593413142000, + "y": 0, + }, + Object { + "x": 1593413143000, + "y": 0, + }, + Object { + "x": 1593413144000, + "y": 0, + }, + Object { + "x": 1593413145000, + "y": 0, + }, + Object { + "x": 1593413146000, + "y": 0, + }, + Object { + "x": 1593413147000, + "y": 0, + }, + Object { + "x": 1593413148000, + "y": 0, + }, + Object { + "x": 1593413149000, + "y": 0, + }, + Object { + "x": 1593413150000, + "y": 0, + }, + Object { + "x": 1593413151000, + "y": 0, + }, + Object { + "x": 1593413152000, + "y": 0, + }, + Object { + "x": 1593413153000, + "y": 0, + }, + Object { + "x": 1593413154000, + "y": 0, + }, + Object { + "x": 1593413155000, + "y": 0, + }, + Object { + "x": 1593413156000, + "y": 0, + }, + Object { + "x": 1593413157000, + "y": 0, + }, + Object { + "x": 1593413158000, + "y": 0, + }, + Object { + "x": 1593413159000, + "y": 0, + }, + Object { + "x": 1593413160000, + "y": 0, + }, + Object { + "x": 1593413161000, + "y": 0, + }, + Object { + "x": 1593413162000, + "y": 0, + }, + Object { + "x": 1593413163000, + "y": 0, + }, + Object { + "x": 1593413164000, + "y": 0, + }, + Object { + "x": 1593413165000, + "y": 0, + }, + Object { + "x": 1593413166000, + "y": 0, + }, + Object { + "x": 1593413167000, + "y": 0, + }, + Object { + "x": 1593413168000, + "y": 0, + }, + Object { + "x": 1593413169000, + "y": 0, + }, + Object { + "x": 1593413170000, + "y": 0, + }, + Object { + "x": 1593413171000, + "y": 0, + }, + Object { + "x": 1593413172000, + "y": 0, + }, + Object { + "x": 1593413173000, + "y": 0, + }, + Object { + "x": 1593413174000, + "y": 0, + }, + Object { + "x": 1593413175000, + "y": 0, + }, + Object { + "x": 1593413176000, + "y": 0, + }, + Object { + "x": 1593413177000, + "y": 0, + }, + Object { + "x": 1593413178000, + "y": 0, + }, + Object { + "x": 1593413179000, + "y": 0, + }, + Object { + "x": 1593413180000, + "y": 0, + }, + Object { + "x": 1593413181000, + "y": 0, + }, + Object { + "x": 1593413182000, + "y": 0, + }, + Object { + "x": 1593413183000, + "y": 0, + }, + Object { + "x": 1593413184000, + "y": 0, + }, + Object { + "x": 1593413185000, + "y": 0, + }, + Object { + "x": 1593413186000, + "y": 0, + }, + Object { + "x": 1593413187000, + "y": 0, + }, + Object { + "x": 1593413188000, + "y": 0, + }, + Object { + "x": 1593413189000, + "y": 0, + }, + Object { + "x": 1593413190000, + "y": 0, + }, + Object { + "x": 1593413191000, + "y": 0, + }, + Object { + "x": 1593413192000, + "y": 0, + }, + Object { + "x": 1593413193000, + "y": 0, + }, + Object { + "x": 1593413194000, + "y": 0, + }, + Object { + "x": 1593413195000, + "y": 0, + }, + Object { + "x": 1593413196000, + "y": 0, + }, + Object { + "x": 1593413197000, + "y": 0, + }, + Object { + "x": 1593413198000, + "y": 0, + }, + Object { + "x": 1593413199000, + "y": 0, + }, + Object { + "x": 1593413200000, + "y": 0, + }, + Object { + "x": 1593413201000, + "y": 0, + }, + Object { + "x": 1593413202000, + "y": 0, + }, + Object { + "x": 1593413203000, + "y": 0, + }, + Object { + "x": 1593413204000, + "y": 0, + }, + Object { + "x": 1593413205000, + "y": 0, + }, + Object { + "x": 1593413206000, + "y": 0, + }, + Object { + "x": 1593413207000, + "y": 0, + }, + Object { + "x": 1593413208000, + "y": 0, + }, + Object { + "x": 1593413209000, + "y": 0, + }, + Object { + "x": 1593413210000, + "y": 0, + }, + Object { + "x": 1593413211000, + "y": 0, + }, + Object { + "x": 1593413212000, + "y": 0, + }, + Object { + "x": 1593413213000, + "y": 0, + }, + Object { + "x": 1593413214000, + "y": 0, + }, + Object { + "x": 1593413215000, + "y": 0, + }, + Object { + "x": 1593413216000, + "y": 0, + }, + Object { + "x": 1593413217000, + "y": 0, + }, + Object { + "x": 1593413218000, + "y": 0, + }, + Object { + "x": 1593413219000, + "y": 0, + }, + Object { + "x": 1593413220000, + "y": 0, + }, + Object { + "x": 1593413221000, + "y": 0, + }, + Object { + "x": 1593413222000, + "y": 0, + }, + Object { + "x": 1593413223000, + "y": 0, + }, + Object { + "x": 1593413224000, + "y": 0, + }, + Object { + "x": 1593413225000, + "y": 0, + }, + Object { + "x": 1593413226000, + "y": 0, + }, + Object { + "x": 1593413227000, + "y": 0, + }, + Object { + "x": 1593413228000, + "y": 0, + }, + Object { + "x": 1593413229000, + "y": 0, + }, + Object { + "x": 1593413230000, + "y": 0, + }, + Object { + "x": 1593413231000, + "y": 0, + }, + Object { + "x": 1593413232000, + "y": 0, + }, + Object { + "x": 1593413233000, + "y": 0, + }, + Object { + "x": 1593413234000, + "y": 0, + }, + Object { + "x": 1593413235000, + "y": 0, + }, + Object { + "x": 1593413236000, + "y": 0, + }, + Object { + "x": 1593413237000, + "y": 0, + }, + Object { + "x": 1593413238000, + "y": 0, + }, + Object { + "x": 1593413239000, + "y": 0, + }, + Object { + "x": 1593413240000, + "y": 0, + }, + Object { + "x": 1593413241000, + "y": 0, + }, + Object { + "x": 1593413242000, + "y": 0, + }, + Object { + "x": 1593413243000, + "y": 0, + }, + Object { + "x": 1593413244000, + "y": 0, + }, + Object { + "x": 1593413245000, + "y": 0, + }, + Object { + "x": 1593413246000, + "y": 0, + }, + Object { + "x": 1593413247000, + "y": 0, + }, + Object { + "x": 1593413248000, + "y": 0, + }, + Object { + "x": 1593413249000, + "y": 0, + }, + Object { + "x": 1593413250000, + "y": 0, + }, + Object { + "x": 1593413251000, + "y": 0, + }, + Object { + "x": 1593413252000, + "y": 0, + }, + Object { + "x": 1593413253000, + "y": 0, + }, + Object { + "x": 1593413254000, + "y": 0, + }, + Object { + "x": 1593413255000, + "y": 0, + }, + Object { + "x": 1593413256000, + "y": 0, + }, + Object { + "x": 1593413257000, + "y": 0, + }, + Object { + "x": 1593413258000, + "y": 0, + }, + Object { + "x": 1593413259000, + "y": 0, + }, + Object { + "x": 1593413260000, + "y": 0, + }, + Object { + "x": 1593413261000, + "y": 0, + }, + Object { + "x": 1593413262000, + "y": 0, + }, + Object { + "x": 1593413263000, + "y": 0, + }, + Object { + "x": 1593413264000, + "y": 0, + }, + Object { + "x": 1593413265000, + "y": 0, + }, + Object { + "x": 1593413266000, + "y": 0, + }, + Object { + "x": 1593413267000, + "y": 0, + }, + Object { + "x": 1593413268000, + "y": 0, + }, + Object { + "x": 1593413269000, + "y": 0, + }, + Object { + "x": 1593413270000, + "y": 0, + }, + Object { + "x": 1593413271000, + "y": 0, + }, + Object { + "x": 1593413272000, + "y": 0, + }, + Object { + "x": 1593413273000, + "y": 0, + }, + Object { + "x": 1593413274000, + "y": 0, + }, + Object { + "x": 1593413275000, + "y": 0, + }, + Object { + "x": 1593413276000, + "y": 0, + }, + Object { + "x": 1593413277000, + "y": 0, + }, + Object { + "x": 1593413278000, + "y": 0, + }, + Object { + "x": 1593413279000, + "y": 0, + }, + Object { + "x": 1593413280000, + "y": 0, + }, + Object { + "x": 1593413281000, + "y": 0, + }, + Object { + "x": 1593413282000, + "y": 0, + }, + Object { + "x": 1593413283000, + "y": 0, + }, + Object { + "x": 1593413284000, + "y": 0, + }, + Object { + "x": 1593413285000, + "y": 0, + }, + Object { + "x": 1593413286000, + "y": 1, + }, + Object { + "x": 1593413287000, + "y": 1, + }, + Object { + "x": 1593413288000, + "y": 0, + }, + Object { + "x": 1593413289000, + "y": 0, + }, + Object { + "x": 1593413290000, + "y": 0, + }, + Object { + "x": 1593413291000, + "y": 0, + }, + Object { + "x": 1593413292000, + "y": 0, + }, + Object { + "x": 1593413293000, + "y": 0, + }, + Object { + "x": 1593413294000, + "y": 0, + }, + Object { + "x": 1593413295000, + "y": 0, + }, + Object { + "x": 1593413296000, + "y": 0, + }, + Object { + "x": 1593413297000, + "y": 0, + }, + Object { + "x": 1593413298000, + "y": 0, + }, + Object { + "x": 1593413299000, + "y": 1, + }, + Object { + "x": 1593413300000, + "y": 0, + }, + Object { + "x": 1593413301000, + "y": 1, + }, + Object { + "x": 1593413302000, + "y": 0, + }, + Object { + "x": 1593413303000, + "y": 0, + }, + Object { + "x": 1593413304000, + "y": 0, + }, + Object { + "x": 1593413305000, + "y": 1, + }, + Object { + "x": 1593413306000, + "y": 0, + }, + Object { + "x": 1593413307000, + "y": 0, + }, + Object { + "x": 1593413308000, + "y": 1, + }, + Object { + "x": 1593413309000, + "y": 0, + }, + Object { + "x": 1593413310000, + "y": 0, + }, + Object { + "x": 1593413311000, + "y": 1, + }, + Object { + "x": 1593413312000, + "y": 0, + }, + Object { + "x": 1593413313000, + "y": 0, + }, + Object { + "x": 1593413314000, + "y": 0, + }, + Object { + "x": 1593413315000, + "y": 1, + }, + Object { + "x": 1593413316000, + "y": 0, + }, + Object { + "x": 1593413317000, + "y": 0, + }, + Object { + "x": 1593413318000, + "y": 0, + }, + Object { + "x": 1593413319000, + "y": 0, + }, + Object { + "x": 1593413320000, + "y": 0, + }, + Object { + "x": 1593413321000, + "y": 0, + }, + Object { + "x": 1593413322000, + "y": 1, + }, + Object { + "x": 1593413323000, + "y": 0, + }, + Object { + "x": 1593413324000, + "y": 0, + }, + Object { + "x": 1593413325000, + "y": 0, + }, + Object { + "x": 1593413326000, + "y": 0, + }, + Object { + "x": 1593413327000, + "y": 0, + }, + Object { + "x": 1593413328000, + "y": 0, + }, + Object { + "x": 1593413329000, + "y": 0, + }, + Object { + "x": 1593413330000, + "y": 0, + }, + Object { + "x": 1593413331000, + "y": 0, + }, + Object { + "x": 1593413332000, + "y": 0, + }, + Object { + "x": 1593413333000, + "y": 0, + }, + Object { + "x": 1593413334000, + "y": 0, + }, + Object { + "x": 1593413335000, + "y": 0, + }, + Object { + "x": 1593413336000, + "y": 0, + }, + Object { + "x": 1593413337000, + "y": 0, + }, + Object { + "x": 1593413338000, + "y": 0, + }, + Object { + "x": 1593413339000, + "y": 0, + }, + Object { + "x": 1593413340000, + "y": 0, + }, + ], + "key": "HTTP 5xx", + }, + Object { + "avg": 0.25, + "dataPoints": Array [ + Object { + "x": 1593413100000, + "y": 0, + }, + Object { + "x": 1593413101000, + "y": 0, + }, + Object { + "x": 1593413102000, + "y": 0, + }, + Object { + "x": 1593413103000, + "y": 0, + }, + Object { + "x": 1593413104000, + "y": 0, + }, + Object { + "x": 1593413105000, + "y": 0, + }, + Object { + "x": 1593413106000, + "y": 0, + }, + Object { + "x": 1593413107000, + "y": 0, + }, + Object { + "x": 1593413108000, + "y": 0, + }, + Object { + "x": 1593413109000, + "y": 0, + }, + Object { + "x": 1593413110000, + "y": 0, + }, + Object { + "x": 1593413111000, + "y": 0, + }, + Object { + "x": 1593413112000, + "y": 0, + }, + Object { + "x": 1593413113000, + "y": 0, + }, + Object { + "x": 1593413114000, + "y": 0, + }, + Object { + "x": 1593413115000, + "y": 0, + }, + Object { + "x": 1593413116000, + "y": 0, + }, + Object { + "x": 1593413117000, + "y": 0, + }, + Object { + "x": 1593413118000, + "y": 0, + }, + Object { + "x": 1593413119000, + "y": 0, + }, + Object { + "x": 1593413120000, + "y": 0, + }, + Object { + "x": 1593413121000, + "y": 0, + }, + Object { + "x": 1593413122000, + "y": 0, + }, + Object { + "x": 1593413123000, + "y": 0, + }, + Object { + "x": 1593413124000, + "y": 0, + }, + Object { + "x": 1593413125000, + "y": 0, + }, + Object { + "x": 1593413126000, + "y": 0, + }, + Object { + "x": 1593413127000, + "y": 0, + }, + Object { + "x": 1593413128000, + "y": 0, + }, + Object { + "x": 1593413129000, + "y": 0, + }, + Object { + "x": 1593413130000, + "y": 0, + }, + Object { + "x": 1593413131000, + "y": 0, + }, + Object { + "x": 1593413132000, + "y": 0, + }, + Object { + "x": 1593413133000, + "y": 0, + }, + Object { + "x": 1593413134000, + "y": 0, + }, + Object { + "x": 1593413135000, + "y": 0, + }, + Object { + "x": 1593413136000, + "y": 0, + }, + Object { + "x": 1593413137000, + "y": 0, + }, + Object { + "x": 1593413138000, + "y": 0, + }, + Object { + "x": 1593413139000, + "y": 0, + }, + Object { + "x": 1593413140000, + "y": 0, + }, + Object { + "x": 1593413141000, + "y": 0, + }, + Object { + "x": 1593413142000, + "y": 0, + }, + Object { + "x": 1593413143000, + "y": 0, + }, + Object { + "x": 1593413144000, + "y": 0, + }, + Object { + "x": 1593413145000, + "y": 0, + }, + Object { + "x": 1593413146000, + "y": 0, + }, + Object { + "x": 1593413147000, + "y": 0, + }, + Object { + "x": 1593413148000, + "y": 0, + }, + Object { + "x": 1593413149000, + "y": 0, + }, + Object { + "x": 1593413150000, + "y": 0, + }, + Object { + "x": 1593413151000, + "y": 0, + }, + Object { + "x": 1593413152000, + "y": 0, + }, + Object { + "x": 1593413153000, + "y": 0, + }, + Object { + "x": 1593413154000, + "y": 0, + }, + Object { + "x": 1593413155000, + "y": 0, + }, + Object { + "x": 1593413156000, + "y": 0, + }, + Object { + "x": 1593413157000, + "y": 0, + }, + Object { + "x": 1593413158000, + "y": 0, + }, + Object { + "x": 1593413159000, + "y": 0, + }, + Object { + "x": 1593413160000, + "y": 0, + }, + Object { + "x": 1593413161000, + "y": 0, + }, + Object { + "x": 1593413162000, + "y": 0, + }, + Object { + "x": 1593413163000, + "y": 0, + }, + Object { + "x": 1593413164000, + "y": 0, + }, + Object { + "x": 1593413165000, + "y": 0, + }, + Object { + "x": 1593413166000, + "y": 0, + }, + Object { + "x": 1593413167000, + "y": 0, + }, + Object { + "x": 1593413168000, + "y": 0, + }, + Object { + "x": 1593413169000, + "y": 0, + }, + Object { + "x": 1593413170000, + "y": 0, + }, + Object { + "x": 1593413171000, + "y": 0, + }, + Object { + "x": 1593413172000, + "y": 0, + }, + Object { + "x": 1593413173000, + "y": 0, + }, + Object { + "x": 1593413174000, + "y": 0, + }, + Object { + "x": 1593413175000, + "y": 0, + }, + Object { + "x": 1593413176000, + "y": 0, + }, + Object { + "x": 1593413177000, + "y": 0, + }, + Object { + "x": 1593413178000, + "y": 0, + }, + Object { + "x": 1593413179000, + "y": 0, + }, + Object { + "x": 1593413180000, + "y": 0, + }, + Object { + "x": 1593413181000, + "y": 0, + }, + Object { + "x": 1593413182000, + "y": 0, + }, + Object { + "x": 1593413183000, + "y": 0, + }, + Object { + "x": 1593413184000, + "y": 0, + }, + Object { + "x": 1593413185000, + "y": 0, + }, + Object { + "x": 1593413186000, + "y": 0, + }, + Object { + "x": 1593413187000, + "y": 0, + }, + Object { + "x": 1593413188000, + "y": 0, + }, + Object { + "x": 1593413189000, + "y": 0, + }, + Object { + "x": 1593413190000, + "y": 0, + }, + Object { + "x": 1593413191000, + "y": 0, + }, + Object { + "x": 1593413192000, + "y": 0, + }, + Object { + "x": 1593413193000, + "y": 0, + }, + Object { + "x": 1593413194000, + "y": 0, + }, + Object { + "x": 1593413195000, + "y": 0, + }, + Object { + "x": 1593413196000, + "y": 0, + }, + Object { + "x": 1593413197000, + "y": 0, + }, + Object { + "x": 1593413198000, + "y": 0, + }, + Object { + "x": 1593413199000, + "y": 0, + }, + Object { + "x": 1593413200000, + "y": 0, + }, + Object { + "x": 1593413201000, + "y": 0, + }, + Object { + "x": 1593413202000, + "y": 0, + }, + Object { + "x": 1593413203000, + "y": 0, + }, + Object { + "x": 1593413204000, + "y": 0, + }, + Object { + "x": 1593413205000, + "y": 0, + }, + Object { + "x": 1593413206000, + "y": 0, + }, + Object { + "x": 1593413207000, + "y": 0, + }, + Object { + "x": 1593413208000, + "y": 0, + }, + Object { + "x": 1593413209000, + "y": 0, + }, + Object { + "x": 1593413210000, + "y": 0, + }, + Object { + "x": 1593413211000, + "y": 0, + }, + Object { + "x": 1593413212000, + "y": 0, + }, + Object { + "x": 1593413213000, + "y": 0, + }, + Object { + "x": 1593413214000, + "y": 0, + }, + Object { + "x": 1593413215000, + "y": 0, + }, + Object { + "x": 1593413216000, + "y": 0, + }, + Object { + "x": 1593413217000, + "y": 0, + }, + Object { + "x": 1593413218000, + "y": 0, + }, + Object { + "x": 1593413219000, + "y": 0, + }, + Object { + "x": 1593413220000, + "y": 0, + }, + Object { + "x": 1593413221000, + "y": 0, + }, + Object { + "x": 1593413222000, + "y": 0, + }, + Object { + "x": 1593413223000, + "y": 0, + }, + Object { + "x": 1593413224000, + "y": 0, + }, + Object { + "x": 1593413225000, + "y": 0, + }, + Object { + "x": 1593413226000, + "y": 0, + }, + Object { + "x": 1593413227000, + "y": 0, + }, + Object { + "x": 1593413228000, + "y": 0, + }, + Object { + "x": 1593413229000, + "y": 0, + }, + Object { + "x": 1593413230000, + "y": 0, + }, + Object { + "x": 1593413231000, + "y": 0, + }, + Object { + "x": 1593413232000, + "y": 0, + }, + Object { + "x": 1593413233000, + "y": 0, + }, + Object { + "x": 1593413234000, + "y": 0, + }, + Object { + "x": 1593413235000, + "y": 0, + }, + Object { + "x": 1593413236000, + "y": 0, + }, + Object { + "x": 1593413237000, + "y": 0, + }, + Object { + "x": 1593413238000, + "y": 0, + }, + Object { + "x": 1593413239000, + "y": 0, + }, + Object { + "x": 1593413240000, + "y": 0, + }, + Object { + "x": 1593413241000, + "y": 0, + }, + Object { + "x": 1593413242000, + "y": 0, + }, + Object { + "x": 1593413243000, + "y": 0, + }, + Object { + "x": 1593413244000, + "y": 0, + }, + Object { + "x": 1593413245000, + "y": 0, + }, + Object { + "x": 1593413246000, + "y": 0, + }, + Object { + "x": 1593413247000, + "y": 0, + }, + Object { + "x": 1593413248000, + "y": 0, + }, + Object { + "x": 1593413249000, + "y": 0, + }, + Object { + "x": 1593413250000, + "y": 0, + }, + Object { + "x": 1593413251000, + "y": 0, + }, + Object { + "x": 1593413252000, + "y": 0, + }, + Object { + "x": 1593413253000, + "y": 0, + }, + Object { + "x": 1593413254000, + "y": 0, + }, + Object { + "x": 1593413255000, + "y": 0, + }, + Object { + "x": 1593413256000, + "y": 0, + }, + Object { + "x": 1593413257000, + "y": 0, + }, + Object { + "x": 1593413258000, + "y": 0, + }, + Object { + "x": 1593413259000, + "y": 0, + }, + Object { + "x": 1593413260000, + "y": 0, + }, + Object { + "x": 1593413261000, + "y": 0, + }, + Object { + "x": 1593413262000, + "y": 0, + }, + Object { + "x": 1593413263000, + "y": 0, + }, + Object { + "x": 1593413264000, + "y": 0, + }, + Object { + "x": 1593413265000, + "y": 0, + }, + Object { + "x": 1593413266000, + "y": 0, + }, + Object { + "x": 1593413267000, + "y": 0, + }, + Object { + "x": 1593413268000, + "y": 0, + }, + Object { + "x": 1593413269000, + "y": 0, + }, + Object { + "x": 1593413270000, + "y": 0, + }, + Object { + "x": 1593413271000, + "y": 0, + }, + Object { + "x": 1593413272000, + "y": 0, + }, + Object { + "x": 1593413273000, + "y": 0, + }, + Object { + "x": 1593413274000, + "y": 0, + }, + Object { + "x": 1593413275000, + "y": 0, + }, + Object { + "x": 1593413276000, + "y": 0, + }, + Object { + "x": 1593413277000, + "y": 0, + }, + Object { + "x": 1593413278000, + "y": 0, + }, + Object { + "x": 1593413279000, + "y": 0, + }, + Object { + "x": 1593413280000, + "y": 0, + }, + Object { + "x": 1593413281000, + "y": 0, + }, + Object { + "x": 1593413282000, + "y": 0, + }, + Object { + "x": 1593413283000, + "y": 0, + }, + Object { + "x": 1593413284000, + "y": 0, + }, + Object { + "x": 1593413285000, + "y": 0, + }, + Object { + "x": 1593413286000, + "y": 0, + }, + Object { + "x": 1593413287000, + "y": 0, + }, + Object { + "x": 1593413288000, + "y": 0, + }, + Object { + "x": 1593413289000, + "y": 0, + }, + Object { + "x": 1593413290000, + "y": 0, + }, + Object { + "x": 1593413291000, + "y": 0, + }, + Object { + "x": 1593413292000, + "y": 0, + }, + Object { + "x": 1593413293000, + "y": 0, + }, + Object { + "x": 1593413294000, + "y": 0, + }, + Object { + "x": 1593413295000, + "y": 0, + }, + Object { + "x": 1593413296000, + "y": 0, + }, + Object { + "x": 1593413297000, + "y": 0, + }, + Object { + "x": 1593413298000, + "y": 0, + }, + Object { + "x": 1593413299000, + "y": 0, + }, + Object { + "x": 1593413300000, + "y": 0, + }, + Object { + "x": 1593413301000, + "y": 0, + }, + Object { + "x": 1593413302000, + "y": 0, + }, + Object { + "x": 1593413303000, + "y": 0, + }, + Object { + "x": 1593413304000, + "y": 0, + }, + Object { + "x": 1593413305000, + "y": 0, + }, + Object { + "x": 1593413306000, + "y": 0, + }, + Object { + "x": 1593413307000, + "y": 0, + }, + Object { + "x": 1593413308000, + "y": 0, + }, + Object { + "x": 1593413309000, + "y": 1, + }, + Object { + "x": 1593413310000, + "y": 0, + }, + Object { + "x": 1593413311000, + "y": 0, + }, + Object { + "x": 1593413312000, + "y": 0, + }, + Object { + "x": 1593413313000, + "y": 0, + }, + Object { + "x": 1593413314000, + "y": 0, + }, + Object { + "x": 1593413315000, + "y": 0, + }, + Object { + "x": 1593413316000, + "y": 0, + }, + Object { + "x": 1593413317000, + "y": 0, + }, + Object { + "x": 1593413318000, + "y": 0, + }, + Object { + "x": 1593413319000, + "y": 0, + }, + Object { + "x": 1593413320000, + "y": 0, + }, + Object { + "x": 1593413321000, + "y": 0, + }, + Object { + "x": 1593413322000, + "y": 0, + }, + Object { + "x": 1593413323000, + "y": 0, + }, + Object { + "x": 1593413324000, + "y": 0, + }, + Object { + "x": 1593413325000, + "y": 0, + }, + Object { + "x": 1593413326000, + "y": 0, + }, + Object { + "x": 1593413327000, + "y": 0, + }, + Object { + "x": 1593413328000, + "y": 0, + }, + Object { + "x": 1593413329000, + "y": 0, + }, + Object { + "x": 1593413330000, + "y": 0, + }, + Object { + "x": 1593413331000, + "y": 0, + }, + Object { + "x": 1593413332000, + "y": 0, + }, + Object { + "x": 1593413333000, + "y": 0, + }, + Object { + "x": 1593413334000, + "y": 0, + }, + Object { + "x": 1593413335000, + "y": 0, + }, + Object { + "x": 1593413336000, + "y": 0, + }, + Object { + "x": 1593413337000, + "y": 0, + }, + Object { + "x": 1593413338000, + "y": 0, + }, + Object { + "x": 1593413339000, + "y": 0, + }, + Object { + "x": 1593413340000, + "y": 0, + }, + ], + "key": "success", + }, + ], + }, +} +`; diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts index 690935ddc7f6a..21f3aaa04a7b3 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/avg_duration_by_browser.ts @@ -4,9 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import expectedAvgDurationByBrowser from './expectation/avg_duration_by_browser.json'; -import expectedAvgDurationByBrowserWithTransactionName from './expectation/avg_duration_by_browser_transaction_name.json'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -38,7 +37,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.eql(expectedAvgDurationByBrowser); + expectSnapshot(response.body).toMatch(); }); it('returns the average duration by browser filtering by transaction name', async () => { const response = await supertest.get( @@ -46,7 +45,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.eql(expectedAvgDurationByBrowserWithTransactionName); + expectSnapshot(response.body).toMatch(); }); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts index 0b94abaa15890..4e1b1e57fba0f 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/breakdown.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; -import expectedBreakdown from './expectation/breakdown.json'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -38,7 +38,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.eql(expectedBreakdown); + expectSnapshot(response.body).toMatch(); }); it('returns the transaction breakdown for a transaction group', async () => { const response = await supertest.get( @@ -48,22 +48,53 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(response.status).to.be(200); const { timeseries } = response.body; const { title, color, type, data, hideLegend, legendValue } = timeseries[0]; - expect(data).to.eql([ - { x: 1593413100000, y: null }, - { x: 1593413130000, y: null }, - { x: 1593413160000, y: null }, - { x: 1593413190000, y: null }, - { x: 1593413220000, y: null }, - { x: 1593413250000, y: null }, - { x: 1593413280000, y: null }, - { x: 1593413310000, y: 1 }, - { x: 1593413340000, y: null }, - ]); - expect(title).to.be('app'); - expect(color).to.be('#54b399'); - expect(type).to.be('areaStacked'); - expect(hideLegend).to.be(false); - expect(legendValue).to.be('100%'); + + expectSnapshot(data).toMatchInline(` + Array [ + Object { + "x": 1593413100000, + "y": null, + }, + Object { + "x": 1593413130000, + "y": null, + }, + Object { + "x": 1593413160000, + "y": null, + }, + Object { + "x": 1593413190000, + "y": null, + }, + Object { + "x": 1593413220000, + "y": null, + }, + Object { + "x": 1593413250000, + "y": null, + }, + Object { + "x": 1593413280000, + "y": null, + }, + Object { + "x": 1593413310000, + "y": 1, + }, + Object { + "x": 1593413340000, + "y": null, + }, + ] + `); + + expectSnapshot(title).toMatchInline(`"app"`); + expectSnapshot(color).toMatchInline(`"#54b399"`); + expectSnapshot(type).toMatchInline(`"areaStacked"`); + expectSnapshot(hideLegend).toMatchInline(`false`); + expectSnapshot(legendValue).toMatchInline(`"100%"`); }); it('returns the transaction breakdown sorted by name', async () => { const response = await supertest.get( @@ -71,12 +102,15 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body.timeseries.map((serie: { title: string }) => serie.title)).to.eql([ - 'app', - 'http', - 'postgresql', - 'redis', - ]); + expectSnapshot(response.body.timeseries.map((serie: { title: string }) => serie.title)) + .toMatchInline(` + Array [ + "app", + "http", + "postgresql", + "redis", + ] + `); }); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts index 9aa10d2b307b6..cf23883612b7c 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/error_rate.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; import { first, last } from 'lodash'; +import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { @@ -46,24 +47,30 @@ export default function ApiTest({ getService }: FtrProviderContext) { errorRateResponse = response.body; }); - it('has the correct start date', async () => { - expect(first(errorRateResponse.erroneousTransactionsRate)?.x).to.be(1598439600000); + it('has the correct start date', () => { + expectSnapshot( + new Date(first(errorRateResponse.erroneousTransactionsRate)?.x ?? NaN).toISOString() + ).toMatchInline(`"2020-08-26T11:00:00.000Z"`); }); - it('has the correct end date', async () => { - expect(last(errorRateResponse.erroneousTransactionsRate)?.x).to.be(1598441400000); + it('has the correct end date', () => { + expectSnapshot( + new Date(last(errorRateResponse.erroneousTransactionsRate)?.x ?? NaN).toISOString() + ).toMatchInline(`"2020-08-26T11:30:00.000Z"`); }); - it('has the correct number of buckets', async () => { - expect(errorRateResponse.erroneousTransactionsRate.length).to.be(61); + it('has the correct number of buckets', () => { + expectSnapshot(errorRateResponse.erroneousTransactionsRate.length).toMatchInline(`61`); }); - it('has the correct calculation for average', async () => { - expect(errorRateResponse.average).to.be(0.18894993894993897); + it('has the correct calculation for average', () => { + expectSnapshot(errorRateResponse.average).toMatchInline(`0.18894993894993897`); }); - it('has the correct error rate', async () => { - expect(first(errorRateResponse.erroneousTransactionsRate)?.y).to.be(0.5); + it('has the correct error rate', () => { + expectSnapshot(first(errorRateResponse.erroneousTransactionsRate)?.y).toMatchInline( + `0.5` + ); }); }); }); diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser.json deleted file mode 100644 index cd53af3bf7080..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser.json +++ /dev/null @@ -1,735 +0,0 @@ -[ - { - "title":"HeadlessChrome", - "data":[ - { - "x":1593413100000 - }, - { - "x":1593413101000 - }, - { - "x":1593413102000 - }, - { - "x":1593413103000 - }, - { - "x":1593413104000 - }, - { - "x":1593413105000 - }, - { - "x":1593413106000 - }, - { - "x":1593413107000 - }, - { - "x":1593413108000 - }, - { - "x":1593413109000 - }, - { - "x":1593413110000 - }, - { - "x":1593413111000 - }, - { - "x":1593413112000 - }, - { - "x":1593413113000 - }, - { - "x":1593413114000 - }, - { - "x":1593413115000 - }, - { - "x":1593413116000 - }, - { - "x":1593413117000 - }, - { - "x":1593413118000 - }, - { - "x":1593413119000 - }, - { - "x":1593413120000 - }, - { - "x":1593413121000 - }, - { - "x":1593413122000 - }, - { - "x":1593413123000 - }, - { - "x":1593413124000 - }, - { - "x":1593413125000 - }, - { - "x":1593413126000 - }, - { - "x":1593413127000 - }, - { - "x":1593413128000 - }, - { - "x":1593413129000 - }, - { - "x":1593413130000 - }, - { - "x":1593413131000 - }, - { - "x":1593413132000 - }, - { - "x":1593413133000 - }, - { - "x":1593413134000 - }, - { - "x":1593413135000 - }, - { - "x":1593413136000 - }, - { - "x":1593413137000 - }, - { - "x":1593413138000 - }, - { - "x":1593413139000 - }, - { - "x":1593413140000 - }, - { - "x":1593413141000 - }, - { - "x":1593413142000 - }, - { - "x":1593413143000 - }, - { - "x":1593413144000 - }, - { - "x":1593413145000 - }, - { - "x":1593413146000 - }, - { - "x":1593413147000 - }, - { - "x":1593413148000 - }, - { - "x":1593413149000 - }, - { - "x":1593413150000 - }, - { - "x":1593413151000 - }, - { - "x":1593413152000 - }, - { - "x":1593413153000 - }, - { - "x":1593413154000 - }, - { - "x":1593413155000 - }, - { - "x":1593413156000 - }, - { - "x":1593413157000 - }, - { - "x":1593413158000 - }, - { - "x":1593413159000 - }, - { - "x":1593413160000 - }, - { - "x":1593413161000 - }, - { - "x":1593413162000 - }, - { - "x":1593413163000 - }, - { - "x":1593413164000 - }, - { - "x":1593413165000 - }, - { - "x":1593413166000 - }, - { - "x":1593413167000 - }, - { - "x":1593413168000 - }, - { - "x":1593413169000 - }, - { - "x":1593413170000 - }, - { - "x":1593413171000 - }, - { - "x":1593413172000 - }, - { - "x":1593413173000 - }, - { - "x":1593413174000 - }, - { - "x":1593413175000 - }, - { - "x":1593413176000 - }, - { - "x":1593413177000 - }, - { - "x":1593413178000 - }, - { - "x":1593413179000 - }, - { - "x":1593413180000 - }, - { - "x":1593413181000 - }, - { - "x":1593413182000 - }, - { - "x":1593413183000 - }, - { - "x":1593413184000 - }, - { - "x":1593413185000 - }, - { - "x":1593413186000 - }, - { - "x":1593413187000 - }, - { - "x":1593413188000 - }, - { - "x":1593413189000 - }, - { - "x":1593413190000 - }, - { - "x":1593413191000 - }, - { - "x":1593413192000 - }, - { - "x":1593413193000 - }, - { - "x":1593413194000 - }, - { - "x":1593413195000 - }, - { - "x":1593413196000 - }, - { - "x":1593413197000 - }, - { - "x":1593413198000 - }, - { - "x":1593413199000 - }, - { - "x":1593413200000 - }, - { - "x":1593413201000 - }, - { - "x":1593413202000 - }, - { - "x":1593413203000 - }, - { - "x":1593413204000 - }, - { - "x":1593413205000 - }, - { - "x":1593413206000 - }, - { - "x":1593413207000 - }, - { - "x":1593413208000 - }, - { - "x":1593413209000 - }, - { - "x":1593413210000 - }, - { - "x":1593413211000 - }, - { - "x":1593413212000 - }, - { - "x":1593413213000 - }, - { - "x":1593413214000 - }, - { - "x":1593413215000 - }, - { - "x":1593413216000 - }, - { - "x":1593413217000 - }, - { - "x":1593413218000 - }, - { - "x":1593413219000 - }, - { - "x":1593413220000 - }, - { - "x":1593413221000 - }, - { - "x":1593413222000 - }, - { - "x":1593413223000 - }, - { - "x":1593413224000 - }, - { - "x":1593413225000 - }, - { - "x":1593413226000 - }, - { - "x":1593413227000 - }, - { - "x":1593413228000 - }, - { - "x":1593413229000 - }, - { - "x":1593413230000 - }, - { - "x":1593413231000 - }, - { - "x":1593413232000 - }, - { - "x":1593413233000 - }, - { - "x":1593413234000 - }, - { - "x":1593413235000 - }, - { - "x":1593413236000 - }, - { - "x":1593413237000 - }, - { - "x":1593413238000 - }, - { - "x":1593413239000 - }, - { - "x":1593413240000 - }, - { - "x":1593413241000 - }, - { - "x":1593413242000 - }, - { - "x":1593413243000 - }, - { - "x":1593413244000 - }, - { - "x":1593413245000 - }, - { - "x":1593413246000 - }, - { - "x":1593413247000 - }, - { - "x":1593413248000 - }, - { - "x":1593413249000 - }, - { - "x":1593413250000 - }, - { - "x":1593413251000 - }, - { - "x":1593413252000 - }, - { - "x":1593413253000 - }, - { - "x":1593413254000 - }, - { - "x":1593413255000 - }, - { - "x":1593413256000 - }, - { - "x":1593413257000 - }, - { - "x":1593413258000 - }, - { - "x":1593413259000 - }, - { - "x":1593413260000 - }, - { - "x":1593413261000 - }, - { - "x":1593413262000 - }, - { - "x":1593413263000 - }, - { - "x":1593413264000 - }, - { - "x":1593413265000 - }, - { - "x":1593413266000 - }, - { - "x":1593413267000 - }, - { - "x":1593413268000 - }, - { - "x":1593413269000 - }, - { - "x":1593413270000 - }, - { - "x":1593413271000 - }, - { - "x":1593413272000 - }, - { - "x":1593413273000 - }, - { - "x":1593413274000 - }, - { - "x":1593413275000 - }, - { - "x":1593413276000 - }, - { - "x":1593413277000 - }, - { - "x":1593413278000 - }, - { - "x":1593413279000 - }, - { - "x":1593413280000 - }, - { - "x":1593413281000 - }, - { - "x":1593413282000 - }, - { - "x":1593413283000 - }, - { - "x":1593413284000 - }, - { - "x":1593413285000 - }, - { - "x":1593413286000 - }, - { - "x":1593413287000, - "y":342000 - }, - { - "x":1593413288000 - }, - { - "x":1593413289000 - }, - { - "x":1593413290000 - }, - { - "x":1593413291000 - }, - { - "x":1593413292000 - }, - { - "x":1593413293000 - }, - { - "x":1593413294000 - }, - { - "x":1593413295000 - }, - { - "x":1593413296000 - }, - { - "x":1593413297000 - }, - { - "x":1593413298000, - "y":173000 - }, - { - "x":1593413299000 - }, - { - "x":1593413300000 - }, - { - "x":1593413301000, - "y":109000 - }, - { - "x":1593413302000 - }, - { - "x":1593413303000 - }, - { - "x":1593413304000 - }, - { - "x":1593413305000 - }, - { - "x":1593413306000 - }, - { - "x":1593413307000 - }, - { - "x":1593413308000 - }, - { - "x":1593413309000 - }, - { - "x":1593413310000 - }, - { - "x":1593413311000 - }, - { - "x":1593413312000 - }, - { - "x":1593413313000 - }, - { - "x":1593413314000 - }, - { - "x":1593413315000 - }, - { - "x":1593413316000 - }, - { - "x":1593413317000 - }, - { - "x":1593413318000, - "y":140000 - }, - { - "x":1593413319000 - }, - { - "x":1593413320000 - }, - { - "x":1593413321000 - }, - { - "x":1593413322000 - }, - { - "x":1593413323000 - }, - { - "x":1593413324000 - }, - { - "x":1593413325000 - }, - { - "x":1593413326000 - }, - { - "x":1593413327000 - }, - { - "x":1593413328000, - "y":77000 - }, - { - "x":1593413329000 - }, - { - "x":1593413330000 - }, - { - "x":1593413331000 - }, - { - "x":1593413332000 - }, - { - "x":1593413333000 - }, - { - "x":1593413334000 - }, - { - "x":1593413335000 - }, - { - "x":1593413336000 - }, - { - "x":1593413337000 - }, - { - "x":1593413338000 - }, - { - "x":1593413339000 - }, - { - "x":1593413340000 - } - ] - } -] \ No newline at end of file diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser_transaction_name.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser_transaction_name.json deleted file mode 100644 index 107302831d55f..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/avg_duration_by_browser_transaction_name.json +++ /dev/null @@ -1,731 +0,0 @@ -[ - { - "title":"HeadlessChrome", - "data":[ - { - "x":1593413100000 - }, - { - "x":1593413101000 - }, - { - "x":1593413102000 - }, - { - "x":1593413103000 - }, - { - "x":1593413104000 - }, - { - "x":1593413105000 - }, - { - "x":1593413106000 - }, - { - "x":1593413107000 - }, - { - "x":1593413108000 - }, - { - "x":1593413109000 - }, - { - "x":1593413110000 - }, - { - "x":1593413111000 - }, - { - "x":1593413112000 - }, - { - "x":1593413113000 - }, - { - "x":1593413114000 - }, - { - "x":1593413115000 - }, - { - "x":1593413116000 - }, - { - "x":1593413117000 - }, - { - "x":1593413118000 - }, - { - "x":1593413119000 - }, - { - "x":1593413120000 - }, - { - "x":1593413121000 - }, - { - "x":1593413122000 - }, - { - "x":1593413123000 - }, - { - "x":1593413124000 - }, - { - "x":1593413125000 - }, - { - "x":1593413126000 - }, - { - "x":1593413127000 - }, - { - "x":1593413128000 - }, - { - "x":1593413129000 - }, - { - "x":1593413130000 - }, - { - "x":1593413131000 - }, - { - "x":1593413132000 - }, - { - "x":1593413133000 - }, - { - "x":1593413134000 - }, - { - "x":1593413135000 - }, - { - "x":1593413136000 - }, - { - "x":1593413137000 - }, - { - "x":1593413138000 - }, - { - "x":1593413139000 - }, - { - "x":1593413140000 - }, - { - "x":1593413141000 - }, - { - "x":1593413142000 - }, - { - "x":1593413143000 - }, - { - "x":1593413144000 - }, - { - "x":1593413145000 - }, - { - "x":1593413146000 - }, - { - "x":1593413147000 - }, - { - "x":1593413148000 - }, - { - "x":1593413149000 - }, - { - "x":1593413150000 - }, - { - "x":1593413151000 - }, - { - "x":1593413152000 - }, - { - "x":1593413153000 - }, - { - "x":1593413154000 - }, - { - "x":1593413155000 - }, - { - "x":1593413156000 - }, - { - "x":1593413157000 - }, - { - "x":1593413158000 - }, - { - "x":1593413159000 - }, - { - "x":1593413160000 - }, - { - "x":1593413161000 - }, - { - "x":1593413162000 - }, - { - "x":1593413163000 - }, - { - "x":1593413164000 - }, - { - "x":1593413165000 - }, - { - "x":1593413166000 - }, - { - "x":1593413167000 - }, - { - "x":1593413168000 - }, - { - "x":1593413169000 - }, - { - "x":1593413170000 - }, - { - "x":1593413171000 - }, - { - "x":1593413172000 - }, - { - "x":1593413173000 - }, - { - "x":1593413174000 - }, - { - "x":1593413175000 - }, - { - "x":1593413176000 - }, - { - "x":1593413177000 - }, - { - "x":1593413178000 - }, - { - "x":1593413179000 - }, - { - "x":1593413180000 - }, - { - "x":1593413181000 - }, - { - "x":1593413182000 - }, - { - "x":1593413183000 - }, - { - "x":1593413184000 - }, - { - "x":1593413185000 - }, - { - "x":1593413186000 - }, - { - "x":1593413187000 - }, - { - "x":1593413188000 - }, - { - "x":1593413189000 - }, - { - "x":1593413190000 - }, - { - "x":1593413191000 - }, - { - "x":1593413192000 - }, - { - "x":1593413193000 - }, - { - "x":1593413194000 - }, - { - "x":1593413195000 - }, - { - "x":1593413196000 - }, - { - "x":1593413197000 - }, - { - "x":1593413198000 - }, - { - "x":1593413199000 - }, - { - "x":1593413200000 - }, - { - "x":1593413201000 - }, - { - "x":1593413202000 - }, - { - "x":1593413203000 - }, - { - "x":1593413204000 - }, - { - "x":1593413205000 - }, - { - "x":1593413206000 - }, - { - "x":1593413207000 - }, - { - "x":1593413208000 - }, - { - "x":1593413209000 - }, - { - "x":1593413210000 - }, - { - "x":1593413211000 - }, - { - "x":1593413212000 - }, - { - "x":1593413213000 - }, - { - "x":1593413214000 - }, - { - "x":1593413215000 - }, - { - "x":1593413216000 - }, - { - "x":1593413217000 - }, - { - "x":1593413218000 - }, - { - "x":1593413219000 - }, - { - "x":1593413220000 - }, - { - "x":1593413221000 - }, - { - "x":1593413222000 - }, - { - "x":1593413223000 - }, - { - "x":1593413224000 - }, - { - "x":1593413225000 - }, - { - "x":1593413226000 - }, - { - "x":1593413227000 - }, - { - "x":1593413228000 - }, - { - "x":1593413229000 - }, - { - "x":1593413230000 - }, - { - "x":1593413231000 - }, - { - "x":1593413232000 - }, - { - "x":1593413233000 - }, - { - "x":1593413234000 - }, - { - "x":1593413235000 - }, - { - "x":1593413236000 - }, - { - "x":1593413237000 - }, - { - "x":1593413238000 - }, - { - "x":1593413239000 - }, - { - "x":1593413240000 - }, - { - "x":1593413241000 - }, - { - "x":1593413242000 - }, - { - "x":1593413243000 - }, - { - "x":1593413244000 - }, - { - "x":1593413245000 - }, - { - "x":1593413246000 - }, - { - "x":1593413247000 - }, - { - "x":1593413248000 - }, - { - "x":1593413249000 - }, - { - "x":1593413250000 - }, - { - "x":1593413251000 - }, - { - "x":1593413252000 - }, - { - "x":1593413253000 - }, - { - "x":1593413254000 - }, - { - "x":1593413255000 - }, - { - "x":1593413256000 - }, - { - "x":1593413257000 - }, - { - "x":1593413258000 - }, - { - "x":1593413259000 - }, - { - "x":1593413260000 - }, - { - "x":1593413261000 - }, - { - "x":1593413262000 - }, - { - "x":1593413263000 - }, - { - "x":1593413264000 - }, - { - "x":1593413265000 - }, - { - "x":1593413266000 - }, - { - "x":1593413267000 - }, - { - "x":1593413268000 - }, - { - "x":1593413269000 - }, - { - "x":1593413270000 - }, - { - "x":1593413271000 - }, - { - "x":1593413272000 - }, - { - "x":1593413273000 - }, - { - "x":1593413274000 - }, - { - "x":1593413275000 - }, - { - "x":1593413276000 - }, - { - "x":1593413277000 - }, - { - "x":1593413278000 - }, - { - "x":1593413279000 - }, - { - "x":1593413280000 - }, - { - "x":1593413281000 - }, - { - "x":1593413282000 - }, - { - "x":1593413283000 - }, - { - "x":1593413284000 - }, - { - "x":1593413285000 - }, - { - "x":1593413286000 - }, - { - "x":1593413287000 - }, - { - "x":1593413288000 - }, - { - "x":1593413289000 - }, - { - "x":1593413290000 - }, - { - "x":1593413291000 - }, - { - "x":1593413292000 - }, - { - "x":1593413293000 - }, - { - "x":1593413294000 - }, - { - "x":1593413295000 - }, - { - "x":1593413296000 - }, - { - "x":1593413297000 - }, - { - "x":1593413298000 - }, - { - "x":1593413299000 - }, - { - "x":1593413300000 - }, - { - "x":1593413301000 - }, - { - "x":1593413302000 - }, - { - "x":1593413303000 - }, - { - "x":1593413304000 - }, - { - "x":1593413305000 - }, - { - "x":1593413306000 - }, - { - "x":1593413307000 - }, - { - "x":1593413308000 - }, - { - "x":1593413309000 - }, - { - "x":1593413310000 - }, - { - "x":1593413311000 - }, - { - "x":1593413312000 - }, - { - "x":1593413313000 - }, - { - "x":1593413314000 - }, - { - "x":1593413315000 - }, - { - "x":1593413316000 - }, - { - "x":1593413317000 - }, - { - "x":1593413318000 - }, - { - "x":1593413319000 - }, - { - "x":1593413320000 - }, - { - "x":1593413321000 - }, - { - "x":1593413322000 - }, - { - "x":1593413323000 - }, - { - "x":1593413324000 - }, - { - "x":1593413325000 - }, - { - "x":1593413326000 - }, - { - "x":1593413327000 - }, - { - "x":1593413328000, - "y":77000 - }, - { - "x":1593413329000 - }, - { - "x":1593413330000 - }, - { - "x":1593413331000 - }, - { - "x":1593413332000 - }, - { - "x":1593413333000 - }, - { - "x":1593413334000 - }, - { - "x":1593413335000 - }, - { - "x":1593413336000 - }, - { - "x":1593413337000 - }, - { - "x":1593413338000 - }, - { - "x":1593413339000 - }, - { - "x":1593413340000 - } - ] - } -] \ No newline at end of file diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json deleted file mode 100644 index 8ffbba64ec7ab..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/breakdown.json +++ /dev/null @@ -1,184 +0,0 @@ -{ - "timeseries":[ - { - "title":"app", - "color":"#54b399", - "type":"areaStacked", - "data":[ - { - "x":1593413100000, - "y":null - }, - { - "x":1593413130000, - "y":null - }, - { - "x":1593413160000, - "y":null - }, - { - "x":1593413190000, - "y":null - }, - { - "x":1593413220000, - "y":null - }, - { - "x":1593413250000, - "y":null - }, - { - "x":1593413280000, - "y":null - }, - { - "x":1593413310000, - "y":0.16700861715223636 - }, - { - "x":1593413340000, - "y":null - } - ], - "hideLegend":false, - "legendValue": "17%" - }, - { - "title":"http", - "color":"#6092c0", - "type":"areaStacked", - "data":[ - { - "x":1593413100000, - "y":null - }, - { - "x":1593413130000, - "y":null - }, - { - "x":1593413160000, - "y":null - }, - { - "x":1593413190000, - "y":null - }, - { - "x":1593413220000, - "y":null - }, - { - "x":1593413250000, - "y":null - }, - { - "x":1593413280000, - "y":null - }, - { - "x":1593413310000, - "y":0.7702092736971686 - }, - { - "x":1593413340000, - "y":null - } - ], - "hideLegend":false, - "legendValue": "77%" - }, - { - "title":"postgresql", - "color":"#d36086", - "type":"areaStacked", - "data":[ - { - "x":1593413100000, - "y":null - }, - { - "x":1593413130000, - "y":null - }, - { - "x":1593413160000, - "y":null - }, - { - "x":1593413190000, - "y":null - }, - { - "x":1593413220000, - "y":null - }, - { - "x":1593413250000, - "y":null - }, - { - "x":1593413280000, - "y":null - }, - { - "x":1593413310000, - "y":0.0508822322527698 - }, - { - "x":1593413340000, - "y":null - } - ], - "hideLegend":false, - "legendValue": "5.1%" - }, - { - "title":"redis", - "color":"#9170b8", - "type":"areaStacked", - "data":[ - { - "x":1593413100000, - "y":null - }, - { - "x":1593413130000, - "y":null - }, - { - "x":1593413160000, - "y":null - }, - { - "x":1593413190000, - "y":null - }, - { - "x":1593413220000, - "y":null - }, - { - "x":1593413250000, - "y":null - }, - { - "x":1593413280000, - "y":null - }, - { - "x":1593413310000, - "y":0.011899876897825195 - }, - { - "x":1593413340000, - "y":null - } - ], - "hideLegend":false, - "legendValue": "1.2%" - } - ] -} \ No newline at end of file diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/top_transaction_groups.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/top_transaction_groups.json deleted file mode 100644 index 29c55d4ef1b5c..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/top_transaction_groups.json +++ /dev/null @@ -1,3151 +0,0 @@ -{ - "items": [ - { - "key": "GET /api", - "averageResponseTime": 51175.73170731707, - "transactionsPerMinute": 10.25, - "impact": 100, - "p95": 259040, - "sample": { - "@timestamp": "2020-06-29T06:48:06.862Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.8" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:08.305742Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate" - ], - "Connection": [ - "keep-alive" - ], - "Host": [ - "opbeans-node:3000" - ], - "Referer": [ - "http://opbeans-node:3000/dashboard" - ], - "Traceparent": [ - "00-ca86ffcac7753ec8733933bd8fd45d11-5dcb98c9c9021cfc-01" - ], - "User-Agent": [ - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.8" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Type": [ - "application/json;charset=UTF-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:06 GMT" - ], - "Transfer-Encoding": [ - "chunked" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "parent": { - "id": "5dcb98c9c9021cfc" - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.8" - }, - "timestamp": { - "us": 1593413286862021 - }, - "trace": { - "id": "ca86ffcac7753ec8733933bd8fd45d11" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 15738 - }, - "id": "c95371db21c6f407", - "name": "GET /api", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 1 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/products/top", - "original": "/api/products/top", - "path": "/api/products/top", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "HeadlessChrome", - "original": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36", - "os": { - "name": "Linux" - }, - "version": "79.0.3945" - } - } - }, - { - "key": "POST /api/orders", - "averageResponseTime": 270684, - "transactionsPerMinute": 0.25, - "impact": 12.686265169840583, - "p95": 270336, - "sample": { - "@timestamp": "2020-06-29T06:48:39.953Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:43.991549Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "body": { - "original": "[REDACTED]" - }, - "headers": { - "Accept": [ - "application/json" - ], - "Connection": [ - "close" - ], - "Content-Length": [ - "129" - ], - "Content-Type": [ - "application/json" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "post", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "13" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:40 GMT" - ], - "Etag": [ - "W/\"d-eEOWU4Cnr5DZ23ErRUeYu9oOIks\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413319953033 - }, - "trace": { - "id": "52b8fda5f6df745b990740ba18378620" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 270684 - }, - "id": "a3afc2a112e9c893", - "name": "POST /api/orders", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 16 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/orders", - "original": "/api/orders", - "path": "/api/orders", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": "GET /api/customers", - "averageResponseTime": 16896.8, - "transactionsPerMinute": 1.25, - "impact": 3.790160870423129, - "p95": 26432, - "sample": { - "@timestamp": "2020-06-29T06:48:28.444Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:29.982737Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "186769" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:28 GMT" - ], - "Etag": [ - "W/\"2d991-yG3J8W/roH7fSxXTudZrO27Ax9s\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413308444015 - }, - "trace": { - "id": "792fb0b00256164e88b277ec40b65e14" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 26471 - }, - "id": "6c1f848752563d2b", - "name": "GET /api/customers", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 2 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/customers", - "original": "/api/customers", - "path": "/api/customers", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": "GET /log-message", - "averageResponseTime": 32667.5, - "transactionsPerMinute": 0.5, - "impact": 2.875276331059301, - "p95": 38528, - "sample": { - "@timestamp": "2020-06-29T06:48:25.944Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:29.976822Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "24" - ], - "Content-Type": [ - "text/html; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:25 GMT" - ], - "Etag": [ - "W/\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 500 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413305944023 - }, - "trace": { - "id": "cd2ad726ad164d701c5d3103cbab0c81" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 38547 - }, - "id": "9e41667eb64dea55", - "name": "GET /log-message", - "result": "HTTP 5xx", - "sampled": true, - "span_count": { - "started": 0 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/log-message", - "original": "/log-message", - "path": "/log-message", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": "GET /*", - "averageResponseTime": 3262.95, - "transactionsPerMinute": 5, - "impact": 2.8716452680799467, - "p95": 4472, - "sample": { - "@timestamp": "2020-06-29T06:48:25.064Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:27.005197Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "Wget" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "813" - ], - "Content-Type": [ - "text/html" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:25 GMT" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "parent": { - "id": "f673ceaf4583f0f2" - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413305064023 - }, - "trace": { - "id": "30c12f4d8ef77a5be1b4464e5d2235bc" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 3004 - }, - "id": "18a00dfdb919a978", - "name": "GET /*", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 0 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/", - "original": "/", - "path": "/", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Wget", - "original": "Wget" - } - } - }, - { - "key": "GET /api/orders", - "averageResponseTime": 7615.625, - "transactionsPerMinute": 2, - "impact": 2.6645791239678345, - "p95": 11616, - "sample": { - "@timestamp": "2020-06-29T06:48:28.782Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.8" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:29.983252Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate" - ], - "Connection": [ - "keep-alive" - ], - "Host": [ - "opbeans-node:3000" - ], - "Referer": [ - "http://opbeans-node:3000/orders" - ], - "Traceparent": [ - "00-978b56807e0b7a27cbc41a0dfb665f47-3358a24e09e23561-01" - ], - "User-Agent": [ - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.8" - } - }, - "response": { - "headers": { - "Connection": [ - "keep-alive" - ], - "Content-Length": [ - "2" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:28 GMT" - ], - "Etag": [ - "W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "parent": { - "id": "3358a24e09e23561" - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.8" - }, - "timestamp": { - "us": 1593413308782015 - }, - "trace": { - "id": "978b56807e0b7a27cbc41a0dfb665f47" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 7134 - }, - "id": "a6d8f3c5c98903e1", - "name": "GET /api/orders", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 2 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/orders", - "original": "/api/orders", - "path": "/api/orders", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "HeadlessChrome", - "original": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36", - "os": { - "name": "Linux" - }, - "version": "79.0.3945" - } - } - }, - { - "key": "GET /api/products", - "averageResponseTime": 8585, - "transactionsPerMinute": 1.75, - "impact": 2.624924094061731, - "p95": 22112, - "sample": { - "@timestamp": "2020-06-29T06:48:21.475Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:26.996210Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "1023" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:21 GMT" - ], - "Etag": [ - "W/\"3ff-VyOxcDApb+a/lnjkm9FeTOGSDrs\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413301475015 - }, - "trace": { - "id": "389b26b16949c7f783223de4f14b788c" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 6775 - }, - "id": "d2d4088a0b104fb4", - "name": "GET /api/products", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 2 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/products", - "original": "/api/products", - "path": "/api/products", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": "GET /api/products/:id", - "averageResponseTime": 13516.5, - "transactionsPerMinute": 1, - "impact": 2.3368756900811305, - "p95": 37856, - "sample": { - "@timestamp": "2020-06-29T06:47:57.555Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:47:59.085077Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "231" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:47:57 GMT" - ], - "Etag": [ - "W/\"e7-6JlJegaJ+ir0C8I8EmmOjms1dnc\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 87, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413277555176 - }, - "trace": { - "id": "8365e1763f19e4067b88521d4d9247a0" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 37709 - }, - "id": "be2722a418272f10", - "name": "GET /api/products/:id", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 1 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/products/1", - "original": "/api/products/1", - "path": "/api/products/1", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": "GET /api/types", - "averageResponseTime": 26992.5, - "transactionsPerMinute": 0.5, - "impact": 2.3330057413794503, - "p95": 45248, - "sample": { - "@timestamp": "2020-06-29T06:47:52.935Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:47:55.471071Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "112" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:47:52 GMT" - ], - "Etag": [ - "W/\"70-1z6hT7P1WHgBgS/BeUEVeHhOCQU\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 63, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413272935117 - }, - "trace": { - "id": "2946c536a33d163d0c984d00d1f3839a" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 45093 - }, - "id": "103482fda88b9400", - "name": "GET /api/types", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 2 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/types", - "original": "/api/types", - "path": "/api/types", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": "GET static file", - "averageResponseTime": 3492.9285714285716, - "transactionsPerMinute": 3.5, - "impact": 2.0901067389184496, - "p95": 11900, - "sample": { - "@timestamp": "2020-06-29T06:47:53.427Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:47:55.472070Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Accept-Ranges": [ - "bytes" - ], - "Cache-Control": [ - "public, max-age=0" - ], - "Connection": [ - "close" - ], - "Content-Length": [ - "15086" - ], - "Content-Type": [ - "image/x-icon" - ], - "Date": [ - "Mon, 29 Jun 2020 06:47:53 GMT" - ], - "Etag": [ - "W/\"3aee-1725aff14f0\"" - ], - "Last-Modified": [ - "Thu, 28 May 2020 11:16:06 GMT" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 63, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413273427016 - }, - "trace": { - "id": "ec8a804fedf28fcf81d5682d69a16970" - }, - "transaction": { - "duration": { - "us": 4934 - }, - "id": "ab90a62901b770e6", - "name": "GET static file", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 0 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/favicon.ico", - "original": "/favicon.ico", - "path": "/favicon.ico", - "port": 3000, - "scheme": "http" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": "GET /api/products/top", - "averageResponseTime": 22958.5, - "transactionsPerMinute": 0.5, - "impact": 1.9475397398343375, - "p95": 33216, - "sample": { - "@timestamp": "2020-06-29T06:48:01.200Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:02.734903Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "2" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:01 GMT" - ], - "Etag": [ - "W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 115, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413281200133 - }, - "trace": { - "id": "195f32efeb6f91e2f71b6bc8bb74ae3a" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 33097 - }, - "id": "22e72956dfc8967a", - "name": "GET /api/products/top", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 1 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/products/top", - "original": "/api/products/top", - "path": "/api/products/top", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": "GET /api/stats", - "averageResponseTime": 7105.333333333333, - "transactionsPerMinute": 1.5, - "impact": 1.7905918202662048, - "p95": 15136, - "sample": { - "@timestamp": "2020-06-29T06:48:21.150Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.8" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:26.993832Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Accept": [ - "*/*" - ], - "Accept-Encoding": [ - "gzip, deflate" - ], - "Connection": [ - "keep-alive" - ], - "Host": [ - "opbeans-node:3000" - ], - "If-None-Match": [ - "W/\"5c-6I+bqIiLxvkWuwBUnTxhBoK4lBk\"" - ], - "Referer": [ - "http://opbeans-node:3000/dashboard" - ], - "Traceparent": [ - "00-ee0ce8b38b8d5945829fc1c9432538bf-39d52cd5f528d363-01" - ], - "User-Agent": [ - "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.8" - } - }, - "response": { - "headers": { - "Connection": [ - "keep-alive" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:21 GMT" - ], - "Etag": [ - "W/\"5c-6I+bqIiLxvkWuwBUnTxhBoK4lBk\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 304 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "parent": { - "id": "39d52cd5f528d363" - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.8" - }, - "timestamp": { - "us": 1593413301150014 - }, - "trace": { - "id": "ee0ce8b38b8d5945829fc1c9432538bf" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 7273 - }, - "id": "05d5b62182c59a54", - "name": "GET /api/stats", - "result": "HTTP 3xx", - "sampled": true, - "span_count": { - "started": 4 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/stats", - "original": "/api/stats", - "path": "/api/stats", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "HeadlessChrome", - "original": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) HeadlessChrome/79.0.3945.0 Safari/537.36", - "os": { - "name": "Linux" - }, - "version": "79.0.3945" - } - } - }, - { - "key": "GET /log-error", - "averageResponseTime": 35846, - "transactionsPerMinute": 0.25, - "impact": 1.466376117925459, - "p95": 35840, - "sample": { - "@timestamp": "2020-06-29T06:48:07.467Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:18.533253Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "24" - ], - "Content-Type": [ - "text/html; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:07 GMT" - ], - "Etag": [ - "W/\"18-MS3VbhH7auHMzO0fUuNF6v14N/M\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 500 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413287467017 - }, - "trace": { - "id": "d518b2c4d72cd2aaf1e39bad7ebcbdbb" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 35846 - }, - "id": "c7a30c1b076907ec", - "name": "GET /log-error", - "result": "HTTP 5xx", - "sampled": true, - "span_count": { - "started": 0 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/log-error", - "original": "/log-error", - "path": "/log-error", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": "POST /api", - "averageResponseTime": 20011, - "transactionsPerMinute": 0.25, - "impact": 0.7098250353192541, - "p95": 19968, - "sample": { - "@timestamp": "2020-06-29T06:48:25.478Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:27.005671Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "body": { - "original": "[REDACTED]" - }, - "headers": { - "Accept": [ - "application/json" - ], - "Connection": [ - "close" - ], - "Content-Length": [ - "129" - ], - "Content-Type": [ - "application/json" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "post", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Allow": [ - "GET" - ], - "Connection": [ - "close" - ], - "Content-Type": [ - "application/json;charset=UTF-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:25 GMT" - ], - "Transfer-Encoding": [ - "chunked" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 405 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413305478010 - }, - "trace": { - "id": "4bd9027dd1e355ec742970e2d6333124" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 20011 - }, - "id": "94104435cf151478", - "name": "POST /api", - "result": "HTTP 4xx", - "sampled": true, - "span_count": { - "started": 1 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/orders", - "original": "/api/orders", - "path": "/api/orders", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": "GET /api/types/:id", - "averageResponseTime": 8181, - "transactionsPerMinute": 0.5, - "impact": 0.5354862351657939, - "p95": 10080, - "sample": { - "@timestamp": "2020-06-29T06:47:53.928Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:47:55.472718Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "205" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:47:53 GMT" - ], - "Etag": [ - "W/\"cd-pFMi1QOVY6YqWe+nwcbZVviCths\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 63, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413273928016 - }, - "trace": { - "id": "0becaafb422bfeb69e047bf7153aa469" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 10062 - }, - "id": "0cee4574091bda3b", - "name": "GET /api/types/:id", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 2 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/types/2", - "original": "/api/types/2", - "path": "/api/types/2", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": "GET /api/orders/:id", - "averageResponseTime": 4749.666666666667, - "transactionsPerMinute": 0.75, - "impact": 0.43453312891085794, - "p95": 7184, - "sample": { - "@timestamp": "2020-06-29T06:48:35.951Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:39.484133Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "0" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:35 GMT" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 404 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413315951017 - }, - "trace": { - "id": "95979caa80e6622cbbb2d308800c3016" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 3210 - }, - "id": "30344988dace0b43", - "name": "GET /api/orders/:id", - "result": "HTTP 4xx", - "sampled": true, - "span_count": { - "started": 1 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/orders/117", - "original": "/api/orders/117", - "path": "/api/orders/117", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": "GET /api/products/:id/customers", - "averageResponseTime": 4757, - "transactionsPerMinute": 0.5, - "impact": 0.20830834986820673, - "p95": 5616, - "sample": { - "@timestamp": "2020-06-29T06:48:22.977Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:27.000765Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "2" - ], - "Content-Type": [ - "application/json; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:22 GMT" - ], - "Etag": [ - "W/\"2-l9Fw4VUO7kr8CvBlt4zaMCqXZ0w\"" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 200 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413302977008 - }, - "trace": { - "id": "da8f22fe652ccb6680b3029ab6efd284" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 5618 - }, - "id": "bc51c1523afaf57a", - "name": "GET /api/products/:id/customers", - "result": "HTTP 2xx", - "sampled": true, - "span_count": { - "started": 1 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/api/products/3/customers", - "original": "/api/products/3/customers", - "path": "/api/products/3/customers", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - }, - { - "key": "GET /throw-error", - "averageResponseTime": 2577, - "transactionsPerMinute": 0.5, - "impact": 0, - "p95": 3224, - "sample": { - "@timestamp": "2020-06-29T06:48:19.975Z", - "agent": { - "name": "nodejs", - "version": "3.6.1" - }, - "client": { - "ip": "172.18.0.7" - }, - "container": { - "id": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "ecs": { - "version": "1.5.0" - }, - "event": { - "ingested": "2020-06-29T06:48:21.012520Z" - }, - "host": { - "architecture": "x64", - "hostname": "41712ded148f", - "ip": "172.18.0.7", - "name": "41712ded148f", - "os": { - "platform": "linux" - } - }, - "http": { - "request": { - "headers": { - "Connection": [ - "close" - ], - "Host": [ - "opbeans-node:3000" - ], - "User-Agent": [ - "workload/2.4.3" - ] - }, - "method": "get", - "socket": { - "encrypted": false, - "remote_address": "::ffff:172.18.0.7" - } - }, - "response": { - "headers": { - "Connection": [ - "close" - ], - "Content-Length": [ - "148" - ], - "Content-Security-Policy": [ - "default-src 'none'" - ], - "Content-Type": [ - "text/html; charset=utf-8" - ], - "Date": [ - "Mon, 29 Jun 2020 06:48:19 GMT" - ], - "X-Content-Type-Options": [ - "nosniff" - ], - "X-Powered-By": [ - "Express" - ] - }, - "status_code": 500 - }, - "version": "1.1" - }, - "labels": { - "foo": "bar", - "lorem": "ipsum dolor sit amet, consectetur adipiscing elit. Nulla finibus, ipsum id scelerisque consequat, enim leo vulputate massa, vel ultricies ante neque ac risus. Curabitur tincidunt vitae sapien id pulvinar. Mauris eu vestibulum tortor. Integer sit amet lorem fringilla, egestas tellus vitae, vulputate purus. Nulla feugiat blandit nunc et semper. Morbi purus libero, mattis sed mauris non, euismod iaculis lacus. Curabitur eleifend ante eros, non faucibus velit lacinia id. Duis posuere libero augue, at dignissim urna consectetur eget. Praesent eu congue est, iaculis finibus augue.", - "multi-line": "foo\nbar\nbaz", - "this-is-a-very-long-tag-name-without-any-spaces": "test" - }, - "observer": { - "ephemeral_id": "99908b73-9813-4a73-baa6-993db405523a", - "hostname": "aa0bd613aa4c", - "id": "1ccc5210-1e6c-4252-a5c8-1d6571a5fa2e", - "type": "apm-server", - "version": "8.0.0", - "version_major": 8 - }, - "process": { - "args": [ - "/usr/local/bin/node", - "/usr/local/lib/node_modules/pm2/lib/ProcessContainer.js", - "ecosystem-workload.config.js" - ], - "pid": 137, - "ppid": 1, - "title": "node /app/server.js" - }, - "processor": { - "event": "transaction", - "name": "transaction" - }, - "service": { - "environment": "production", - "framework": { - "name": "express", - "version": "4.17.1" - }, - "language": { - "name": "javascript" - }, - "name": "opbeans-node", - "node": { - "name": "41712ded148f30ee09a13421780eec4304bf5049b82a0d8dbc877893be6799e4" - }, - "runtime": { - "name": "node", - "version": "12.18.1" - }, - "version": "1.0.0" - }, - "source": { - "ip": "172.18.0.7" - }, - "timestamp": { - "us": 1593413299975019 - }, - "trace": { - "id": "106f3a55b0b0ea327d1bbe4be66c3bcc" - }, - "transaction": { - "custom": { - "shoppingBasketCount": 42 - }, - "duration": { - "us": 3226 - }, - "id": "247b9141552a9e73", - "name": "GET /throw-error", - "result": "HTTP 5xx", - "sampled": true, - "span_count": { - "started": 0 - }, - "type": "request" - }, - "url": { - "domain": "opbeans-node", - "full": "http://opbeans-node:3000/throw-error", - "original": "/throw-error", - "path": "/throw-error", - "port": 3000, - "scheme": "http" - }, - "user": { - "email": "kimchy@elastic.co", - "id": "42", - "name": "kimchy" - }, - "user_agent": { - "device": { - "name": "Other" - }, - "name": "Other", - "original": "workload/2.4.3" - } - } - } - ], - "isAggregationAccurate": true, - "bucketSize": 1000 -} \ No newline at end of file diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/transaction_charts.json b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/transaction_charts.json deleted file mode 100644 index 0e878969f269f..0000000000000 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/expectation/transaction_charts.json +++ /dev/null @@ -1,1973 +0,0 @@ -{ - "apmTimeseries": { - "responseTimes": { - "avg": [ - { "x": 1593413100000, "y": null }, - { "x": 1593413101000, "y": null }, - { "x": 1593413102000, "y": null }, - { "x": 1593413103000, "y": null }, - { "x": 1593413104000, "y": null }, - { "x": 1593413105000, "y": null }, - { "x": 1593413106000, "y": null }, - { "x": 1593413107000, "y": null }, - { "x": 1593413108000, "y": null }, - { "x": 1593413109000, "y": null }, - { "x": 1593413110000, "y": null }, - { "x": 1593413111000, "y": null }, - { "x": 1593413112000, "y": null }, - { "x": 1593413113000, "y": null }, - { "x": 1593413114000, "y": null }, - { "x": 1593413115000, "y": null }, - { "x": 1593413116000, "y": null }, - { "x": 1593413117000, "y": null }, - { "x": 1593413118000, "y": null }, - { "x": 1593413119000, "y": null }, - { "x": 1593413120000, "y": null }, - { "x": 1593413121000, "y": null }, - { "x": 1593413122000, "y": null }, - { "x": 1593413123000, "y": null }, - { "x": 1593413124000, "y": null }, - { "x": 1593413125000, "y": null }, - { "x": 1593413126000, "y": null }, - { "x": 1593413127000, "y": null }, - { "x": 1593413128000, "y": null }, - { "x": 1593413129000, "y": null }, - { "x": 1593413130000, "y": null }, - { "x": 1593413131000, "y": null }, - { "x": 1593413132000, "y": null }, - { "x": 1593413133000, "y": null }, - { "x": 1593413134000, "y": null }, - { "x": 1593413135000, "y": null }, - { "x": 1593413136000, "y": null }, - { "x": 1593413137000, "y": null }, - { "x": 1593413138000, "y": null }, - { "x": 1593413139000, "y": null }, - { "x": 1593413140000, "y": null }, - { "x": 1593413141000, "y": null }, - { "x": 1593413142000, "y": null }, - { "x": 1593413143000, "y": null }, - { "x": 1593413144000, "y": null }, - { "x": 1593413145000, "y": null }, - { "x": 1593413146000, "y": null }, - { "x": 1593413147000, "y": null }, - { "x": 1593413148000, "y": null }, - { "x": 1593413149000, "y": null }, - { "x": 1593413150000, "y": null }, - { "x": 1593413151000, "y": null }, - { "x": 1593413152000, "y": null }, - { "x": 1593413153000, "y": null }, - { "x": 1593413154000, "y": null }, - { "x": 1593413155000, "y": null }, - { "x": 1593413156000, "y": null }, - { "x": 1593413157000, "y": null }, - { "x": 1593413158000, "y": null }, - { "x": 1593413159000, "y": null }, - { "x": 1593413160000, "y": null }, - { "x": 1593413161000, "y": null }, - { "x": 1593413162000, "y": null }, - { "x": 1593413163000, "y": null }, - { "x": 1593413164000, "y": null }, - { "x": 1593413165000, "y": null }, - { "x": 1593413166000, "y": null }, - { "x": 1593413167000, "y": null }, - { "x": 1593413168000, "y": null }, - { "x": 1593413169000, "y": null }, - { "x": 1593413170000, "y": null }, - { "x": 1593413171000, "y": null }, - { "x": 1593413172000, "y": null }, - { "x": 1593413173000, "y": null }, - { "x": 1593413174000, "y": null }, - { "x": 1593413175000, "y": null }, - { "x": 1593413176000, "y": null }, - { "x": 1593413177000, "y": null }, - { "x": 1593413178000, "y": null }, - { "x": 1593413179000, "y": null }, - { "x": 1593413180000, "y": null }, - { "x": 1593413181000, "y": null }, - { "x": 1593413182000, "y": null }, - { "x": 1593413183000, "y": null }, - { "x": 1593413184000, "y": null }, - { "x": 1593413185000, "y": null }, - { "x": 1593413186000, "y": null }, - { "x": 1593413187000, "y": null }, - { "x": 1593413188000, "y": null }, - { "x": 1593413189000, "y": null }, - { "x": 1593413190000, "y": null }, - { "x": 1593413191000, "y": null }, - { "x": 1593413192000, "y": null }, - { "x": 1593413193000, "y": null }, - { "x": 1593413194000, "y": null }, - { "x": 1593413195000, "y": null }, - { "x": 1593413196000, "y": null }, - { "x": 1593413197000, "y": null }, - { "x": 1593413198000, "y": null }, - { "x": 1593413199000, "y": null }, - { "x": 1593413200000, "y": null }, - { "x": 1593413201000, "y": null }, - { "x": 1593413202000, "y": null }, - { "x": 1593413203000, "y": null }, - { "x": 1593413204000, "y": null }, - { "x": 1593413205000, "y": null }, - { "x": 1593413206000, "y": null }, - { "x": 1593413207000, "y": null }, - { "x": 1593413208000, "y": null }, - { "x": 1593413209000, "y": null }, - { "x": 1593413210000, "y": null }, - { "x": 1593413211000, "y": null }, - { "x": 1593413212000, "y": null }, - { "x": 1593413213000, "y": null }, - { "x": 1593413214000, "y": null }, - { "x": 1593413215000, "y": null }, - { "x": 1593413216000, "y": null }, - { "x": 1593413217000, "y": null }, - { "x": 1593413218000, "y": null }, - { "x": 1593413219000, "y": null }, - { "x": 1593413220000, "y": null }, - { "x": 1593413221000, "y": null }, - { "x": 1593413222000, "y": null }, - { "x": 1593413223000, "y": null }, - { "x": 1593413224000, "y": null }, - { "x": 1593413225000, "y": null }, - { "x": 1593413226000, "y": null }, - { "x": 1593413227000, "y": null }, - { "x": 1593413228000, "y": null }, - { "x": 1593413229000, "y": null }, - { "x": 1593413230000, "y": null }, - { "x": 1593413231000, "y": null }, - { "x": 1593413232000, "y": null }, - { "x": 1593413233000, "y": null }, - { "x": 1593413234000, "y": null }, - { "x": 1593413235000, "y": null }, - { "x": 1593413236000, "y": null }, - { "x": 1593413237000, "y": null }, - { "x": 1593413238000, "y": null }, - { "x": 1593413239000, "y": null }, - { "x": 1593413240000, "y": null }, - { "x": 1593413241000, "y": null }, - { "x": 1593413242000, "y": null }, - { "x": 1593413243000, "y": null }, - { "x": 1593413244000, "y": null }, - { "x": 1593413245000, "y": null }, - { "x": 1593413246000, "y": null }, - { "x": 1593413247000, "y": null }, - { "x": 1593413248000, "y": null }, - { "x": 1593413249000, "y": null }, - { "x": 1593413250000, "y": null }, - { "x": 1593413251000, "y": null }, - { "x": 1593413252000, "y": null }, - { "x": 1593413253000, "y": null }, - { "x": 1593413254000, "y": null }, - { "x": 1593413255000, "y": null }, - { "x": 1593413256000, "y": null }, - { "x": 1593413257000, "y": null }, - { "x": 1593413258000, "y": null }, - { "x": 1593413259000, "y": null }, - { "x": 1593413260000, "y": null }, - { "x": 1593413261000, "y": null }, - { "x": 1593413262000, "y": null }, - { "x": 1593413263000, "y": null }, - { "x": 1593413264000, "y": null }, - { "x": 1593413265000, "y": null }, - { "x": 1593413266000, "y": null }, - { "x": 1593413267000, "y": null }, - { "x": 1593413268000, "y": null }, - { "x": 1593413269000, "y": null }, - { "x": 1593413270000, "y": null }, - { "x": 1593413271000, "y": null }, - { "x": 1593413272000, "y": 45093 }, - { "x": 1593413273000, "y": 7498 }, - { "x": 1593413274000, "y": null }, - { "x": 1593413275000, "y": null }, - { "x": 1593413276000, "y": null }, - { "x": 1593413277000, "y": 37709 }, - { "x": 1593413278000, "y": null }, - { "x": 1593413279000, "y": null }, - { "x": 1593413280000, "y": null }, - { "x": 1593413281000, "y": 33097 }, - { "x": 1593413282000, "y": null }, - { "x": 1593413283000, "y": null }, - { "x": 1593413284000, "y": 388507 }, - { "x": 1593413285000, "y": 42331.5 }, - { "x": 1593413286000, "y": 99104.25 }, - { "x": 1593413287000, "y": 18939.5 }, - { "x": 1593413288000, "y": 23229.5 }, - { "x": 1593413289000, "y": 11318 }, - { "x": 1593413290000, "y": 15651.25 }, - { "x": 1593413291000, "y": 2376 }, - { "x": 1593413292000, "y": 7796 }, - { "x": 1593413293000, "y": 7571 }, - { "x": 1593413294000, "y": 4219.333333333333 }, - { "x": 1593413295000, "y": 6827.5 }, - { "x": 1593413296000, "y": 10415.5 }, - { "x": 1593413297000, "y": 10082 }, - { "x": 1593413298000, "y": 6459.375 }, - { "x": 1593413299000, "y": 3131.5 }, - { "x": 1593413300000, "y": 6713.333333333333 }, - { "x": 1593413301000, "y": 8800 }, - { "x": 1593413302000, "y": 3743.5 }, - { "x": 1593413303000, "y": 9239.5 }, - { "x": 1593413304000, "y": 8402 }, - { "x": 1593413305000, "y": 20520.666666666668 }, - { "x": 1593413306000, "y": 9319.5 }, - { "x": 1593413307000, "y": 7694.333333333333 }, - { "x": 1593413308000, "y": 20131 }, - { "x": 1593413309000, "y": 439937.75 }, - { "x": 1593413310000, "y": 11933 }, - { "x": 1593413311000, "y": 18670.5 }, - { "x": 1593413312000, "y": 9232 }, - { "x": 1593413313000, "y": 7602 }, - { "x": 1593413314000, "y": 10428.8 }, - { "x": 1593413315000, "y": 8405.25 }, - { "x": 1593413316000, "y": 10654.5 }, - { "x": 1593413317000, "y": 10250 }, - { "x": 1593413318000, "y": 5775 }, - { "x": 1593413319000, "y": 137867 }, - { "x": 1593413320000, "y": 5694.333333333333 }, - { "x": 1593413321000, "y": 6115 }, - { "x": 1593413322000, "y": 1832.5 }, - { "x": 1593413323000, "y": null }, - { "x": 1593413324000, "y": null }, - { "x": 1593413325000, "y": null }, - { "x": 1593413326000, "y": null }, - { "x": 1593413327000, "y": null }, - { "x": 1593413328000, "y": null }, - { "x": 1593413329000, "y": null }, - { "x": 1593413330000, "y": null }, - { "x": 1593413331000, "y": null }, - { "x": 1593413332000, "y": null }, - { "x": 1593413333000, "y": null }, - { "x": 1593413334000, "y": null }, - { "x": 1593413335000, "y": null }, - { "x": 1593413336000, "y": null }, - { "x": 1593413337000, "y": null }, - { "x": 1593413338000, "y": null }, - { "x": 1593413339000, "y": null }, - { "x": 1593413340000, "y": null } - ], - "p95": [ - { "x": 1593413100000, "y": null }, - { "x": 1593413101000, "y": null }, - { "x": 1593413102000, "y": null }, - { "x": 1593413103000, "y": null }, - { "x": 1593413104000, "y": null }, - { "x": 1593413105000, "y": null }, - { "x": 1593413106000, "y": null }, - { "x": 1593413107000, "y": null }, - { "x": 1593413108000, "y": null }, - { "x": 1593413109000, "y": null }, - { "x": 1593413110000, "y": null }, - { "x": 1593413111000, "y": null }, - { "x": 1593413112000, "y": null }, - { "x": 1593413113000, "y": null }, - { "x": 1593413114000, "y": null }, - { "x": 1593413115000, "y": null }, - { "x": 1593413116000, "y": null }, - { "x": 1593413117000, "y": null }, - { "x": 1593413118000, "y": null }, - { "x": 1593413119000, "y": null }, - { "x": 1593413120000, "y": null }, - { "x": 1593413121000, "y": null }, - { "x": 1593413122000, "y": null }, - { "x": 1593413123000, "y": null }, - { "x": 1593413124000, "y": null }, - { "x": 1593413125000, "y": null }, - { "x": 1593413126000, "y": null }, - { "x": 1593413127000, "y": null }, - { "x": 1593413128000, "y": null }, - { "x": 1593413129000, "y": null }, - { "x": 1593413130000, "y": null }, - { "x": 1593413131000, "y": null }, - { "x": 1593413132000, "y": null }, - { "x": 1593413133000, "y": null }, - { "x": 1593413134000, "y": null }, - { "x": 1593413135000, "y": null }, - { "x": 1593413136000, "y": null }, - { "x": 1593413137000, "y": null }, - { "x": 1593413138000, "y": null }, - { "x": 1593413139000, "y": null }, - { "x": 1593413140000, "y": null }, - { "x": 1593413141000, "y": null }, - { "x": 1593413142000, "y": null }, - { "x": 1593413143000, "y": null }, - { "x": 1593413144000, "y": null }, - { "x": 1593413145000, "y": null }, - { "x": 1593413146000, "y": null }, - { "x": 1593413147000, "y": null }, - { "x": 1593413148000, "y": null }, - { "x": 1593413149000, "y": null }, - { "x": 1593413150000, "y": null }, - { "x": 1593413151000, "y": null }, - { "x": 1593413152000, "y": null }, - { "x": 1593413153000, "y": null }, - { "x": 1593413154000, "y": null }, - { "x": 1593413155000, "y": null }, - { "x": 1593413156000, "y": null }, - { "x": 1593413157000, "y": null }, - { "x": 1593413158000, "y": null }, - { "x": 1593413159000, "y": null }, - { "x": 1593413160000, "y": null }, - { "x": 1593413161000, "y": null }, - { "x": 1593413162000, "y": null }, - { "x": 1593413163000, "y": null }, - { "x": 1593413164000, "y": null }, - { "x": 1593413165000, "y": null }, - { "x": 1593413166000, "y": null }, - { "x": 1593413167000, "y": null }, - { "x": 1593413168000, "y": null }, - { "x": 1593413169000, "y": null }, - { "x": 1593413170000, "y": null }, - { "x": 1593413171000, "y": null }, - { "x": 1593413172000, "y": null }, - { "x": 1593413173000, "y": null }, - { "x": 1593413174000, "y": null }, - { "x": 1593413175000, "y": null }, - { "x": 1593413176000, "y": null }, - { "x": 1593413177000, "y": null }, - { "x": 1593413178000, "y": null }, - { "x": 1593413179000, "y": null }, - { "x": 1593413180000, "y": null }, - { "x": 1593413181000, "y": null }, - { "x": 1593413182000, "y": null }, - { "x": 1593413183000, "y": null }, - { "x": 1593413184000, "y": null }, - { "x": 1593413185000, "y": null }, - { "x": 1593413186000, "y": null }, - { "x": 1593413187000, "y": null }, - { "x": 1593413188000, "y": null }, - { "x": 1593413189000, "y": null }, - { "x": 1593413190000, "y": null }, - { "x": 1593413191000, "y": null }, - { "x": 1593413192000, "y": null }, - { "x": 1593413193000, "y": null }, - { "x": 1593413194000, "y": null }, - { "x": 1593413195000, "y": null }, - { "x": 1593413196000, "y": null }, - { "x": 1593413197000, "y": null }, - { "x": 1593413198000, "y": null }, - { "x": 1593413199000, "y": null }, - { "x": 1593413200000, "y": null }, - { "x": 1593413201000, "y": null }, - { "x": 1593413202000, "y": null }, - { "x": 1593413203000, "y": null }, - { "x": 1593413204000, "y": null }, - { "x": 1593413205000, "y": null }, - { "x": 1593413206000, "y": null }, - { "x": 1593413207000, "y": null }, - { "x": 1593413208000, "y": null }, - { "x": 1593413209000, "y": null }, - { "x": 1593413210000, "y": null }, - { "x": 1593413211000, "y": null }, - { "x": 1593413212000, "y": null }, - { "x": 1593413213000, "y": null }, - { "x": 1593413214000, "y": null }, - { "x": 1593413215000, "y": null }, - { "x": 1593413216000, "y": null }, - { "x": 1593413217000, "y": null }, - { "x": 1593413218000, "y": null }, - { "x": 1593413219000, "y": null }, - { "x": 1593413220000, "y": null }, - { "x": 1593413221000, "y": null }, - { "x": 1593413222000, "y": null }, - { "x": 1593413223000, "y": null }, - { "x": 1593413224000, "y": null }, - { "x": 1593413225000, "y": null }, - { "x": 1593413226000, "y": null }, - { "x": 1593413227000, "y": null }, - { "x": 1593413228000, "y": null }, - { "x": 1593413229000, "y": null }, - { "x": 1593413230000, "y": null }, - { "x": 1593413231000, "y": null }, - { "x": 1593413232000, "y": null }, - { "x": 1593413233000, "y": null }, - { "x": 1593413234000, "y": null }, - { "x": 1593413235000, "y": null }, - { "x": 1593413236000, "y": null }, - { "x": 1593413237000, "y": null }, - { "x": 1593413238000, "y": null }, - { "x": 1593413239000, "y": null }, - { "x": 1593413240000, "y": null }, - { "x": 1593413241000, "y": null }, - { "x": 1593413242000, "y": null }, - { "x": 1593413243000, "y": null }, - { "x": 1593413244000, "y": null }, - { "x": 1593413245000, "y": null }, - { "x": 1593413246000, "y": null }, - { "x": 1593413247000, "y": null }, - { "x": 1593413248000, "y": null }, - { "x": 1593413249000, "y": null }, - { "x": 1593413250000, "y": null }, - { "x": 1593413251000, "y": null }, - { "x": 1593413252000, "y": null }, - { "x": 1593413253000, "y": null }, - { "x": 1593413254000, "y": null }, - { "x": 1593413255000, "y": null }, - { "x": 1593413256000, "y": null }, - { "x": 1593413257000, "y": null }, - { "x": 1593413258000, "y": null }, - { "x": 1593413259000, "y": null }, - { "x": 1593413260000, "y": null }, - { "x": 1593413261000, "y": null }, - { "x": 1593413262000, "y": null }, - { "x": 1593413263000, "y": null }, - { "x": 1593413264000, "y": null }, - { "x": 1593413265000, "y": null }, - { "x": 1593413266000, "y": null }, - { "x": 1593413267000, "y": null }, - { "x": 1593413268000, "y": null }, - { "x": 1593413269000, "y": null }, - { "x": 1593413270000, "y": null }, - { "x": 1593413271000, "y": null }, - { "x": 1593413272000, "y": 45056 }, - { "x": 1593413273000, "y": 10080 }, - { "x": 1593413274000, "y": null }, - { "x": 1593413275000, "y": null }, - { "x": 1593413276000, "y": null }, - { "x": 1593413277000, "y": 37632 }, - { "x": 1593413278000, "y": null }, - { "x": 1593413279000, "y": null }, - { "x": 1593413280000, "y": null }, - { "x": 1593413281000, "y": 33024 }, - { "x": 1593413282000, "y": null }, - { "x": 1593413283000, "y": null }, - { "x": 1593413284000, "y": 761728 }, - { "x": 1593413285000, "y": 81904 }, - { "x": 1593413286000, "y": 358384 }, - { "x": 1593413287000, "y": 36088 }, - { "x": 1593413288000, "y": 44536 }, - { "x": 1593413289000, "y": 11648 }, - { "x": 1593413290000, "y": 31984 }, - { "x": 1593413291000, "y": 2920 }, - { "x": 1593413292000, "y": 9312 }, - { "x": 1593413293000, "y": 10912 }, - { "x": 1593413294000, "y": 6392 }, - { "x": 1593413295000, "y": 11704 }, - { "x": 1593413296000, "y": 10816 }, - { "x": 1593413297000, "y": 12000 }, - { "x": 1593413298000, "y": 15164 }, - { "x": 1593413299000, "y": 3216 }, - { "x": 1593413300000, "y": 9584 }, - { "x": 1593413301000, "y": 21240 }, - { "x": 1593413302000, "y": 5624 }, - { "x": 1593413303000, "y": 11360 }, - { "x": 1593413304000, "y": 12320 }, - { "x": 1593413305000, "y": 38640 }, - { "x": 1593413306000, "y": 9728 }, - { "x": 1593413307000, "y": 17016 }, - { "x": 1593413308000, "y": 26848 }, - { "x": 1593413309000, "y": 1753072 }, - { "x": 1593413310000, "y": 16992 }, - { "x": 1593413311000, "y": 26560 }, - { "x": 1593413312000, "y": 11232 }, - { "x": 1593413313000, "y": 11424 }, - { "x": 1593413314000, "y": 16096 }, - { "x": 1593413315000, "y": 18800 }, - { "x": 1593413316000, "y": 12672 }, - { "x": 1593413317000, "y": 24316 }, - { "x": 1593413318000, "y": 8944 }, - { "x": 1593413319000, "y": 272352 }, - { "x": 1593413320000, "y": 7992 }, - { "x": 1593413321000, "y": 8368 }, - { "x": 1593413322000, "y": 1928 }, - { "x": 1593413323000, "y": null }, - { "x": 1593413324000, "y": null }, - { "x": 1593413325000, "y": null }, - { "x": 1593413326000, "y": null }, - { "x": 1593413327000, "y": null }, - { "x": 1593413328000, "y": null }, - { "x": 1593413329000, "y": null }, - { "x": 1593413330000, "y": null }, - { "x": 1593413331000, "y": null }, - { "x": 1593413332000, "y": null }, - { "x": 1593413333000, "y": null }, - { "x": 1593413334000, "y": null }, - { "x": 1593413335000, "y": null }, - { "x": 1593413336000, "y": null }, - { "x": 1593413337000, "y": null }, - { "x": 1593413338000, "y": null }, - { "x": 1593413339000, "y": null }, - { "x": 1593413340000, "y": null } - ], - "p99": [ - { "x": 1593413100000, "y": null }, - { "x": 1593413101000, "y": null }, - { "x": 1593413102000, "y": null }, - { "x": 1593413103000, "y": null }, - { "x": 1593413104000, "y": null }, - { "x": 1593413105000, "y": null }, - { "x": 1593413106000, "y": null }, - { "x": 1593413107000, "y": null }, - { "x": 1593413108000, "y": null }, - { "x": 1593413109000, "y": null }, - { "x": 1593413110000, "y": null }, - { "x": 1593413111000, "y": null }, - { "x": 1593413112000, "y": null }, - { "x": 1593413113000, "y": null }, - { "x": 1593413114000, "y": null }, - { "x": 1593413115000, "y": null }, - { "x": 1593413116000, "y": null }, - { "x": 1593413117000, "y": null }, - { "x": 1593413118000, "y": null }, - { "x": 1593413119000, "y": null }, - { "x": 1593413120000, "y": null }, - { "x": 1593413121000, "y": null }, - { "x": 1593413122000, "y": null }, - { "x": 1593413123000, "y": null }, - { "x": 1593413124000, "y": null }, - { "x": 1593413125000, "y": null }, - { "x": 1593413126000, "y": null }, - { "x": 1593413127000, "y": null }, - { "x": 1593413128000, "y": null }, - { "x": 1593413129000, "y": null }, - { "x": 1593413130000, "y": null }, - { "x": 1593413131000, "y": null }, - { "x": 1593413132000, "y": null }, - { "x": 1593413133000, "y": null }, - { "x": 1593413134000, "y": null }, - { "x": 1593413135000, "y": null }, - { "x": 1593413136000, "y": null }, - { "x": 1593413137000, "y": null }, - { "x": 1593413138000, "y": null }, - { "x": 1593413139000, "y": null }, - { "x": 1593413140000, "y": null }, - { "x": 1593413141000, "y": null }, - { "x": 1593413142000, "y": null }, - { "x": 1593413143000, "y": null }, - { "x": 1593413144000, "y": null }, - { "x": 1593413145000, "y": null }, - { "x": 1593413146000, "y": null }, - { "x": 1593413147000, "y": null }, - { "x": 1593413148000, "y": null }, - { "x": 1593413149000, "y": null }, - { "x": 1593413150000, "y": null }, - { "x": 1593413151000, "y": null }, - { "x": 1593413152000, "y": null }, - { "x": 1593413153000, "y": null }, - { "x": 1593413154000, "y": null }, - { "x": 1593413155000, "y": null }, - { "x": 1593413156000, "y": null }, - { "x": 1593413157000, "y": null }, - { "x": 1593413158000, "y": null }, - { "x": 1593413159000, "y": null }, - { "x": 1593413160000, "y": null }, - { "x": 1593413161000, "y": null }, - { "x": 1593413162000, "y": null }, - { "x": 1593413163000, "y": null }, - { "x": 1593413164000, "y": null }, - { "x": 1593413165000, "y": null }, - { "x": 1593413166000, "y": null }, - { "x": 1593413167000, "y": null }, - { "x": 1593413168000, "y": null }, - { "x": 1593413169000, "y": null }, - { "x": 1593413170000, "y": null }, - { "x": 1593413171000, "y": null }, - { "x": 1593413172000, "y": null }, - { "x": 1593413173000, "y": null }, - { "x": 1593413174000, "y": null }, - { "x": 1593413175000, "y": null }, - { "x": 1593413176000, "y": null }, - { "x": 1593413177000, "y": null }, - { "x": 1593413178000, "y": null }, - { "x": 1593413179000, "y": null }, - { "x": 1593413180000, "y": null }, - { "x": 1593413181000, "y": null }, - { "x": 1593413182000, "y": null }, - { "x": 1593413183000, "y": null }, - { "x": 1593413184000, "y": null }, - { "x": 1593413185000, "y": null }, - { "x": 1593413186000, "y": null }, - { "x": 1593413187000, "y": null }, - { "x": 1593413188000, "y": null }, - { "x": 1593413189000, "y": null }, - { "x": 1593413190000, "y": null }, - { "x": 1593413191000, "y": null }, - { "x": 1593413192000, "y": null }, - { "x": 1593413193000, "y": null }, - { "x": 1593413194000, "y": null }, - { "x": 1593413195000, "y": null }, - { "x": 1593413196000, "y": null }, - { "x": 1593413197000, "y": null }, - { "x": 1593413198000, "y": null }, - { "x": 1593413199000, "y": null }, - { "x": 1593413200000, "y": null }, - { "x": 1593413201000, "y": null }, - { "x": 1593413202000, "y": null }, - { "x": 1593413203000, "y": null }, - { "x": 1593413204000, "y": null }, - { "x": 1593413205000, "y": null }, - { "x": 1593413206000, "y": null }, - { "x": 1593413207000, "y": null }, - { "x": 1593413208000, "y": null }, - { "x": 1593413209000, "y": null }, - { "x": 1593413210000, "y": null }, - { "x": 1593413211000, "y": null }, - { "x": 1593413212000, "y": null }, - { "x": 1593413213000, "y": null }, - { "x": 1593413214000, "y": null }, - { "x": 1593413215000, "y": null }, - { "x": 1593413216000, "y": null }, - { "x": 1593413217000, "y": null }, - { "x": 1593413218000, "y": null }, - { "x": 1593413219000, "y": null }, - { "x": 1593413220000, "y": null }, - { "x": 1593413221000, "y": null }, - { "x": 1593413222000, "y": null }, - { "x": 1593413223000, "y": null }, - { "x": 1593413224000, "y": null }, - { "x": 1593413225000, "y": null }, - { "x": 1593413226000, "y": null }, - { "x": 1593413227000, "y": null }, - { "x": 1593413228000, "y": null }, - { "x": 1593413229000, "y": null }, - { "x": 1593413230000, "y": null }, - { "x": 1593413231000, "y": null }, - { "x": 1593413232000, "y": null }, - { "x": 1593413233000, "y": null }, - { "x": 1593413234000, "y": null }, - { "x": 1593413235000, "y": null }, - { "x": 1593413236000, "y": null }, - { "x": 1593413237000, "y": null }, - { "x": 1593413238000, "y": null }, - { "x": 1593413239000, "y": null }, - { "x": 1593413240000, "y": null }, - { "x": 1593413241000, "y": null }, - { "x": 1593413242000, "y": null }, - { "x": 1593413243000, "y": null }, - { "x": 1593413244000, "y": null }, - { "x": 1593413245000, "y": null }, - { "x": 1593413246000, "y": null }, - { "x": 1593413247000, "y": null }, - { "x": 1593413248000, "y": null }, - { "x": 1593413249000, "y": null }, - { "x": 1593413250000, "y": null }, - { "x": 1593413251000, "y": null }, - { "x": 1593413252000, "y": null }, - { "x": 1593413253000, "y": null }, - { "x": 1593413254000, "y": null }, - { "x": 1593413255000, "y": null }, - { "x": 1593413256000, "y": null }, - { "x": 1593413257000, "y": null }, - { "x": 1593413258000, "y": null }, - { "x": 1593413259000, "y": null }, - { "x": 1593413260000, "y": null }, - { "x": 1593413261000, "y": null }, - { "x": 1593413262000, "y": null }, - { "x": 1593413263000, "y": null }, - { "x": 1593413264000, "y": null }, - { "x": 1593413265000, "y": null }, - { "x": 1593413266000, "y": null }, - { "x": 1593413267000, "y": null }, - { "x": 1593413268000, "y": null }, - { "x": 1593413269000, "y": null }, - { "x": 1593413270000, "y": null }, - { "x": 1593413271000, "y": null }, - { "x": 1593413272000, "y": 45056 }, - { "x": 1593413273000, "y": 10080 }, - { "x": 1593413274000, "y": null }, - { "x": 1593413275000, "y": null }, - { "x": 1593413276000, "y": null }, - { "x": 1593413277000, "y": 37632 }, - { "x": 1593413278000, "y": null }, - { "x": 1593413279000, "y": null }, - { "x": 1593413280000, "y": null }, - { "x": 1593413281000, "y": 33024 }, - { "x": 1593413282000, "y": null }, - { "x": 1593413283000, "y": null }, - { "x": 1593413284000, "y": 761728 }, - { "x": 1593413285000, "y": 81904 }, - { "x": 1593413286000, "y": 358384 }, - { "x": 1593413287000, "y": 36088 }, - { "x": 1593413288000, "y": 44536 }, - { "x": 1593413289000, "y": 11648 }, - { "x": 1593413290000, "y": 31984 }, - { "x": 1593413291000, "y": 2920 }, - { "x": 1593413292000, "y": 9312 }, - { "x": 1593413293000, "y": 10912 }, - { "x": 1593413294000, "y": 6392 }, - { "x": 1593413295000, "y": 11704 }, - { "x": 1593413296000, "y": 10816 }, - { "x": 1593413297000, "y": 12000 }, - { "x": 1593413298000, "y": 15164 }, - { "x": 1593413299000, "y": 3216 }, - { "x": 1593413300000, "y": 9584 }, - { "x": 1593413301000, "y": 21240 }, - { "x": 1593413302000, "y": 5624 }, - { "x": 1593413303000, "y": 11360 }, - { "x": 1593413304000, "y": 12320 }, - { "x": 1593413305000, "y": 38640 }, - { "x": 1593413306000, "y": 9728 }, - { "x": 1593413307000, "y": 17016 }, - { "x": 1593413308000, "y": 26848 }, - { "x": 1593413309000, "y": 1753072 }, - { "x": 1593413310000, "y": 16992 }, - { "x": 1593413311000, "y": 26560 }, - { "x": 1593413312000, "y": 11232 }, - { "x": 1593413313000, "y": 11424 }, - { "x": 1593413314000, "y": 16096 }, - { "x": 1593413315000, "y": 18800 }, - { "x": 1593413316000, "y": 12672 }, - { "x": 1593413317000, "y": 24316 }, - { "x": 1593413318000, "y": 8944 }, - { "x": 1593413319000, "y": 272352 }, - { "x": 1593413320000, "y": 7992 }, - { "x": 1593413321000, "y": 8368 }, - { "x": 1593413322000, "y": 1928 }, - { "x": 1593413323000, "y": null }, - { "x": 1593413324000, "y": null }, - { "x": 1593413325000, "y": null }, - { "x": 1593413326000, "y": null }, - { "x": 1593413327000, "y": null }, - { "x": 1593413328000, "y": null }, - { "x": 1593413329000, "y": null }, - { "x": 1593413330000, "y": null }, - { "x": 1593413331000, "y": null }, - { "x": 1593413332000, "y": null }, - { "x": 1593413333000, "y": null }, - { "x": 1593413334000, "y": null }, - { "x": 1593413335000, "y": null }, - { "x": 1593413336000, "y": null }, - { "x": 1593413337000, "y": null }, - { "x": 1593413338000, "y": null }, - { "x": 1593413339000, "y": null }, - { "x": 1593413340000, "y": null } - ] - }, - "tpmBuckets": [ - { - "key": "HTTP 2xx", - "dataPoints": [ - { "x": 1593413100000, "y": 0 }, - { "x": 1593413101000, "y": 0 }, - { "x": 1593413102000, "y": 0 }, - { "x": 1593413103000, "y": 0 }, - { "x": 1593413104000, "y": 0 }, - { "x": 1593413105000, "y": 0 }, - { "x": 1593413106000, "y": 0 }, - { "x": 1593413107000, "y": 0 }, - { "x": 1593413108000, "y": 0 }, - { "x": 1593413109000, "y": 0 }, - { "x": 1593413110000, "y": 0 }, - { "x": 1593413111000, "y": 0 }, - { "x": 1593413112000, "y": 0 }, - { "x": 1593413113000, "y": 0 }, - { "x": 1593413114000, "y": 0 }, - { "x": 1593413115000, "y": 0 }, - { "x": 1593413116000, "y": 0 }, - { "x": 1593413117000, "y": 0 }, - { "x": 1593413118000, "y": 0 }, - { "x": 1593413119000, "y": 0 }, - { "x": 1593413120000, "y": 0 }, - { "x": 1593413121000, "y": 0 }, - { "x": 1593413122000, "y": 0 }, - { "x": 1593413123000, "y": 0 }, - { "x": 1593413124000, "y": 0 }, - { "x": 1593413125000, "y": 0 }, - { "x": 1593413126000, "y": 0 }, - { "x": 1593413127000, "y": 0 }, - { "x": 1593413128000, "y": 0 }, - { "x": 1593413129000, "y": 0 }, - { "x": 1593413130000, "y": 0 }, - { "x": 1593413131000, "y": 0 }, - { "x": 1593413132000, "y": 0 }, - { "x": 1593413133000, "y": 0 }, - { "x": 1593413134000, "y": 0 }, - { "x": 1593413135000, "y": 0 }, - { "x": 1593413136000, "y": 0 }, - { "x": 1593413137000, "y": 0 }, - { "x": 1593413138000, "y": 0 }, - { "x": 1593413139000, "y": 0 }, - { "x": 1593413140000, "y": 0 }, - { "x": 1593413141000, "y": 0 }, - { "x": 1593413142000, "y": 0 }, - { "x": 1593413143000, "y": 0 }, - { "x": 1593413144000, "y": 0 }, - { "x": 1593413145000, "y": 0 }, - { "x": 1593413146000, "y": 0 }, - { "x": 1593413147000, "y": 0 }, - { "x": 1593413148000, "y": 0 }, - { "x": 1593413149000, "y": 0 }, - { "x": 1593413150000, "y": 0 }, - { "x": 1593413151000, "y": 0 }, - { "x": 1593413152000, "y": 0 }, - { "x": 1593413153000, "y": 0 }, - { "x": 1593413154000, "y": 0 }, - { "x": 1593413155000, "y": 0 }, - { "x": 1593413156000, "y": 0 }, - { "x": 1593413157000, "y": 0 }, - { "x": 1593413158000, "y": 0 }, - { "x": 1593413159000, "y": 0 }, - { "x": 1593413160000, "y": 0 }, - { "x": 1593413161000, "y": 0 }, - { "x": 1593413162000, "y": 0 }, - { "x": 1593413163000, "y": 0 }, - { "x": 1593413164000, "y": 0 }, - { "x": 1593413165000, "y": 0 }, - { "x": 1593413166000, "y": 0 }, - { "x": 1593413167000, "y": 0 }, - { "x": 1593413168000, "y": 0 }, - { "x": 1593413169000, "y": 0 }, - { "x": 1593413170000, "y": 0 }, - { "x": 1593413171000, "y": 0 }, - { "x": 1593413172000, "y": 0 }, - { "x": 1593413173000, "y": 0 }, - { "x": 1593413174000, "y": 0 }, - { "x": 1593413175000, "y": 0 }, - { "x": 1593413176000, "y": 0 }, - { "x": 1593413177000, "y": 0 }, - { "x": 1593413178000, "y": 0 }, - { "x": 1593413179000, "y": 0 }, - { "x": 1593413180000, "y": 0 }, - { "x": 1593413181000, "y": 0 }, - { "x": 1593413182000, "y": 0 }, - { "x": 1593413183000, "y": 0 }, - { "x": 1593413184000, "y": 0 }, - { "x": 1593413185000, "y": 0 }, - { "x": 1593413186000, "y": 0 }, - { "x": 1593413187000, "y": 0 }, - { "x": 1593413188000, "y": 0 }, - { "x": 1593413189000, "y": 0 }, - { "x": 1593413190000, "y": 0 }, - { "x": 1593413191000, "y": 0 }, - { "x": 1593413192000, "y": 0 }, - { "x": 1593413193000, "y": 0 }, - { "x": 1593413194000, "y": 0 }, - { "x": 1593413195000, "y": 0 }, - { "x": 1593413196000, "y": 0 }, - { "x": 1593413197000, "y": 0 }, - { "x": 1593413198000, "y": 0 }, - { "x": 1593413199000, "y": 0 }, - { "x": 1593413200000, "y": 0 }, - { "x": 1593413201000, "y": 0 }, - { "x": 1593413202000, "y": 0 }, - { "x": 1593413203000, "y": 0 }, - { "x": 1593413204000, "y": 0 }, - { "x": 1593413205000, "y": 0 }, - { "x": 1593413206000, "y": 0 }, - { "x": 1593413207000, "y": 0 }, - { "x": 1593413208000, "y": 0 }, - { "x": 1593413209000, "y": 0 }, - { "x": 1593413210000, "y": 0 }, - { "x": 1593413211000, "y": 0 }, - { "x": 1593413212000, "y": 0 }, - { "x": 1593413213000, "y": 0 }, - { "x": 1593413214000, "y": 0 }, - { "x": 1593413215000, "y": 0 }, - { "x": 1593413216000, "y": 0 }, - { "x": 1593413217000, "y": 0 }, - { "x": 1593413218000, "y": 0 }, - { "x": 1593413219000, "y": 0 }, - { "x": 1593413220000, "y": 0 }, - { "x": 1593413221000, "y": 0 }, - { "x": 1593413222000, "y": 0 }, - { "x": 1593413223000, "y": 0 }, - { "x": 1593413224000, "y": 0 }, - { "x": 1593413225000, "y": 0 }, - { "x": 1593413226000, "y": 0 }, - { "x": 1593413227000, "y": 0 }, - { "x": 1593413228000, "y": 0 }, - { "x": 1593413229000, "y": 0 }, - { "x": 1593413230000, "y": 0 }, - { "x": 1593413231000, "y": 0 }, - { "x": 1593413232000, "y": 0 }, - { "x": 1593413233000, "y": 0 }, - { "x": 1593413234000, "y": 0 }, - { "x": 1593413235000, "y": 0 }, - { "x": 1593413236000, "y": 0 }, - { "x": 1593413237000, "y": 0 }, - { "x": 1593413238000, "y": 0 }, - { "x": 1593413239000, "y": 0 }, - { "x": 1593413240000, "y": 0 }, - { "x": 1593413241000, "y": 0 }, - { "x": 1593413242000, "y": 0 }, - { "x": 1593413243000, "y": 0 }, - { "x": 1593413244000, "y": 0 }, - { "x": 1593413245000, "y": 0 }, - { "x": 1593413246000, "y": 0 }, - { "x": 1593413247000, "y": 0 }, - { "x": 1593413248000, "y": 0 }, - { "x": 1593413249000, "y": 0 }, - { "x": 1593413250000, "y": 0 }, - { "x": 1593413251000, "y": 0 }, - { "x": 1593413252000, "y": 0 }, - { "x": 1593413253000, "y": 0 }, - { "x": 1593413254000, "y": 0 }, - { "x": 1593413255000, "y": 0 }, - { "x": 1593413256000, "y": 0 }, - { "x": 1593413257000, "y": 0 }, - { "x": 1593413258000, "y": 0 }, - { "x": 1593413259000, "y": 0 }, - { "x": 1593413260000, "y": 0 }, - { "x": 1593413261000, "y": 0 }, - { "x": 1593413262000, "y": 0 }, - { "x": 1593413263000, "y": 0 }, - { "x": 1593413264000, "y": 0 }, - { "x": 1593413265000, "y": 0 }, - { "x": 1593413266000, "y": 0 }, - { "x": 1593413267000, "y": 0 }, - { "x": 1593413268000, "y": 0 }, - { "x": 1593413269000, "y": 0 }, - { "x": 1593413270000, "y": 0 }, - { "x": 1593413271000, "y": 0 }, - { "x": 1593413272000, "y": 1 }, - { "x": 1593413273000, "y": 2 }, - { "x": 1593413274000, "y": 0 }, - { "x": 1593413275000, "y": 0 }, - { "x": 1593413276000, "y": 0 }, - { "x": 1593413277000, "y": 1 }, - { "x": 1593413278000, "y": 0 }, - { "x": 1593413279000, "y": 0 }, - { "x": 1593413280000, "y": 0 }, - { "x": 1593413281000, "y": 1 }, - { "x": 1593413282000, "y": 0 }, - { "x": 1593413283000, "y": 0 }, - { "x": 1593413284000, "y": 2 }, - { "x": 1593413285000, "y": 2 }, - { "x": 1593413286000, "y": 7 }, - { "x": 1593413287000, "y": 1 }, - { "x": 1593413288000, "y": 2 }, - { "x": 1593413289000, "y": 1 }, - { "x": 1593413290000, "y": 4 }, - { "x": 1593413291000, "y": 2 }, - { "x": 1593413292000, "y": 1 }, - { "x": 1593413293000, "y": 2 }, - { "x": 1593413294000, "y": 3 }, - { "x": 1593413295000, "y": 2 }, - { "x": 1593413296000, "y": 2 }, - { "x": 1593413297000, "y": 2 }, - { "x": 1593413298000, "y": 6 }, - { "x": 1593413299000, "y": 1 }, - { "x": 1593413300000, "y": 2 }, - { "x": 1593413301000, "y": 3 }, - { "x": 1593413302000, "y": 2 }, - { "x": 1593413303000, "y": 2 }, - { "x": 1593413304000, "y": 2 }, - { "x": 1593413305000, "y": 1 }, - { "x": 1593413306000, "y": 2 }, - { "x": 1593413307000, "y": 3 }, - { "x": 1593413308000, "y": 2 }, - { "x": 1593413309000, "y": 2 }, - { "x": 1593413310000, "y": 2 }, - { "x": 1593413311000, "y": 1 }, - { "x": 1593413312000, "y": 3 }, - { "x": 1593413313000, "y": 3 }, - { "x": 1593413314000, "y": 5 }, - { "x": 1593413315000, "y": 2 }, - { "x": 1593413316000, "y": 2 }, - { "x": 1593413317000, "y": 6 }, - { "x": 1593413318000, "y": 2 }, - { "x": 1593413319000, "y": 2 }, - { "x": 1593413320000, "y": 2 }, - { "x": 1593413321000, "y": 2 }, - { "x": 1593413322000, "y": 1 }, - { "x": 1593413323000, "y": 0 }, - { "x": 1593413324000, "y": 0 }, - { "x": 1593413325000, "y": 0 }, - { "x": 1593413326000, "y": 0 }, - { "x": 1593413327000, "y": 0 }, - { "x": 1593413328000, "y": 0 }, - { "x": 1593413329000, "y": 0 }, - { "x": 1593413330000, "y": 0 }, - { "x": 1593413331000, "y": 0 }, - { "x": 1593413332000, "y": 0 }, - { "x": 1593413333000, "y": 0 }, - { "x": 1593413334000, "y": 0 }, - { "x": 1593413335000, "y": 0 }, - { "x": 1593413336000, "y": 0 }, - { "x": 1593413337000, "y": 0 }, - { "x": 1593413338000, "y": 0 }, - { "x": 1593413339000, "y": 0 }, - { "x": 1593413340000, "y": 0 } - ], - "avg": 24.75 - }, - { - "key": "HTTP 3xx", - "dataPoints": [ - { "x": 1593413100000, "y": 0 }, - { "x": 1593413101000, "y": 0 }, - { "x": 1593413102000, "y": 0 }, - { "x": 1593413103000, "y": 0 }, - { "x": 1593413104000, "y": 0 }, - { "x": 1593413105000, "y": 0 }, - { "x": 1593413106000, "y": 0 }, - { "x": 1593413107000, "y": 0 }, - { "x": 1593413108000, "y": 0 }, - { "x": 1593413109000, "y": 0 }, - { "x": 1593413110000, "y": 0 }, - { "x": 1593413111000, "y": 0 }, - { "x": 1593413112000, "y": 0 }, - { "x": 1593413113000, "y": 0 }, - { "x": 1593413114000, "y": 0 }, - { "x": 1593413115000, "y": 0 }, - { "x": 1593413116000, "y": 0 }, - { "x": 1593413117000, "y": 0 }, - { "x": 1593413118000, "y": 0 }, - { "x": 1593413119000, "y": 0 }, - { "x": 1593413120000, "y": 0 }, - { "x": 1593413121000, "y": 0 }, - { "x": 1593413122000, "y": 0 }, - { "x": 1593413123000, "y": 0 }, - { "x": 1593413124000, "y": 0 }, - { "x": 1593413125000, "y": 0 }, - { "x": 1593413126000, "y": 0 }, - { "x": 1593413127000, "y": 0 }, - { "x": 1593413128000, "y": 0 }, - { "x": 1593413129000, "y": 0 }, - { "x": 1593413130000, "y": 0 }, - { "x": 1593413131000, "y": 0 }, - { "x": 1593413132000, "y": 0 }, - { "x": 1593413133000, "y": 0 }, - { "x": 1593413134000, "y": 0 }, - { "x": 1593413135000, "y": 0 }, - { "x": 1593413136000, "y": 0 }, - { "x": 1593413137000, "y": 0 }, - { "x": 1593413138000, "y": 0 }, - { "x": 1593413139000, "y": 0 }, - { "x": 1593413140000, "y": 0 }, - { "x": 1593413141000, "y": 0 }, - { "x": 1593413142000, "y": 0 }, - { "x": 1593413143000, "y": 0 }, - { "x": 1593413144000, "y": 0 }, - { "x": 1593413145000, "y": 0 }, - { "x": 1593413146000, "y": 0 }, - { "x": 1593413147000, "y": 0 }, - { "x": 1593413148000, "y": 0 }, - { "x": 1593413149000, "y": 0 }, - { "x": 1593413150000, "y": 0 }, - { "x": 1593413151000, "y": 0 }, - { "x": 1593413152000, "y": 0 }, - { "x": 1593413153000, "y": 0 }, - { "x": 1593413154000, "y": 0 }, - { "x": 1593413155000, "y": 0 }, - { "x": 1593413156000, "y": 0 }, - { "x": 1593413157000, "y": 0 }, - { "x": 1593413158000, "y": 0 }, - { "x": 1593413159000, "y": 0 }, - { "x": 1593413160000, "y": 0 }, - { "x": 1593413161000, "y": 0 }, - { "x": 1593413162000, "y": 0 }, - { "x": 1593413163000, "y": 0 }, - { "x": 1593413164000, "y": 0 }, - { "x": 1593413165000, "y": 0 }, - { "x": 1593413166000, "y": 0 }, - { "x": 1593413167000, "y": 0 }, - { "x": 1593413168000, "y": 0 }, - { "x": 1593413169000, "y": 0 }, - { "x": 1593413170000, "y": 0 }, - { "x": 1593413171000, "y": 0 }, - { "x": 1593413172000, "y": 0 }, - { "x": 1593413173000, "y": 0 }, - { "x": 1593413174000, "y": 0 }, - { "x": 1593413175000, "y": 0 }, - { "x": 1593413176000, "y": 0 }, - { "x": 1593413177000, "y": 0 }, - { "x": 1593413178000, "y": 0 }, - { "x": 1593413179000, "y": 0 }, - { "x": 1593413180000, "y": 0 }, - { "x": 1593413181000, "y": 0 }, - { "x": 1593413182000, "y": 0 }, - { "x": 1593413183000, "y": 0 }, - { "x": 1593413184000, "y": 0 }, - { "x": 1593413185000, "y": 0 }, - { "x": 1593413186000, "y": 0 }, - { "x": 1593413187000, "y": 0 }, - { "x": 1593413188000, "y": 0 }, - { "x": 1593413189000, "y": 0 }, - { "x": 1593413190000, "y": 0 }, - { "x": 1593413191000, "y": 0 }, - { "x": 1593413192000, "y": 0 }, - { "x": 1593413193000, "y": 0 }, - { "x": 1593413194000, "y": 0 }, - { "x": 1593413195000, "y": 0 }, - { "x": 1593413196000, "y": 0 }, - { "x": 1593413197000, "y": 0 }, - { "x": 1593413198000, "y": 0 }, - { "x": 1593413199000, "y": 0 }, - { "x": 1593413200000, "y": 0 }, - { "x": 1593413201000, "y": 0 }, - { "x": 1593413202000, "y": 0 }, - { "x": 1593413203000, "y": 0 }, - { "x": 1593413204000, "y": 0 }, - { "x": 1593413205000, "y": 0 }, - { "x": 1593413206000, "y": 0 }, - { "x": 1593413207000, "y": 0 }, - { "x": 1593413208000, "y": 0 }, - { "x": 1593413209000, "y": 0 }, - { "x": 1593413210000, "y": 0 }, - { "x": 1593413211000, "y": 0 }, - { "x": 1593413212000, "y": 0 }, - { "x": 1593413213000, "y": 0 }, - { "x": 1593413214000, "y": 0 }, - { "x": 1593413215000, "y": 0 }, - { "x": 1593413216000, "y": 0 }, - { "x": 1593413217000, "y": 0 }, - { "x": 1593413218000, "y": 0 }, - { "x": 1593413219000, "y": 0 }, - { "x": 1593413220000, "y": 0 }, - { "x": 1593413221000, "y": 0 }, - { "x": 1593413222000, "y": 0 }, - { "x": 1593413223000, "y": 0 }, - { "x": 1593413224000, "y": 0 }, - { "x": 1593413225000, "y": 0 }, - { "x": 1593413226000, "y": 0 }, - { "x": 1593413227000, "y": 0 }, - { "x": 1593413228000, "y": 0 }, - { "x": 1593413229000, "y": 0 }, - { "x": 1593413230000, "y": 0 }, - { "x": 1593413231000, "y": 0 }, - { "x": 1593413232000, "y": 0 }, - { "x": 1593413233000, "y": 0 }, - { "x": 1593413234000, "y": 0 }, - { "x": 1593413235000, "y": 0 }, - { "x": 1593413236000, "y": 0 }, - { "x": 1593413237000, "y": 0 }, - { "x": 1593413238000, "y": 0 }, - { "x": 1593413239000, "y": 0 }, - { "x": 1593413240000, "y": 0 }, - { "x": 1593413241000, "y": 0 }, - { "x": 1593413242000, "y": 0 }, - { "x": 1593413243000, "y": 0 }, - { "x": 1593413244000, "y": 0 }, - { "x": 1593413245000, "y": 0 }, - { "x": 1593413246000, "y": 0 }, - { "x": 1593413247000, "y": 0 }, - { "x": 1593413248000, "y": 0 }, - { "x": 1593413249000, "y": 0 }, - { "x": 1593413250000, "y": 0 }, - { "x": 1593413251000, "y": 0 }, - { "x": 1593413252000, "y": 0 }, - { "x": 1593413253000, "y": 0 }, - { "x": 1593413254000, "y": 0 }, - { "x": 1593413255000, "y": 0 }, - { "x": 1593413256000, "y": 0 }, - { "x": 1593413257000, "y": 0 }, - { "x": 1593413258000, "y": 0 }, - { "x": 1593413259000, "y": 0 }, - { "x": 1593413260000, "y": 0 }, - { "x": 1593413261000, "y": 0 }, - { "x": 1593413262000, "y": 0 }, - { "x": 1593413263000, "y": 0 }, - { "x": 1593413264000, "y": 0 }, - { "x": 1593413265000, "y": 0 }, - { "x": 1593413266000, "y": 0 }, - { "x": 1593413267000, "y": 0 }, - { "x": 1593413268000, "y": 0 }, - { "x": 1593413269000, "y": 0 }, - { "x": 1593413270000, "y": 0 }, - { "x": 1593413271000, "y": 0 }, - { "x": 1593413272000, "y": 0 }, - { "x": 1593413273000, "y": 0 }, - { "x": 1593413274000, "y": 0 }, - { "x": 1593413275000, "y": 0 }, - { "x": 1593413276000, "y": 0 }, - { "x": 1593413277000, "y": 0 }, - { "x": 1593413278000, "y": 0 }, - { "x": 1593413279000, "y": 0 }, - { "x": 1593413280000, "y": 0 }, - { "x": 1593413281000, "y": 0 }, - { "x": 1593413282000, "y": 0 }, - { "x": 1593413283000, "y": 0 }, - { "x": 1593413284000, "y": 0 }, - { "x": 1593413285000, "y": 0 }, - { "x": 1593413286000, "y": 0 }, - { "x": 1593413287000, "y": 0 }, - { "x": 1593413288000, "y": 0 }, - { "x": 1593413289000, "y": 0 }, - { "x": 1593413290000, "y": 0 }, - { "x": 1593413291000, "y": 0 }, - { "x": 1593413292000, "y": 0 }, - { "x": 1593413293000, "y": 0 }, - { "x": 1593413294000, "y": 0 }, - { "x": 1593413295000, "y": 0 }, - { "x": 1593413296000, "y": 0 }, - { "x": 1593413297000, "y": 0 }, - { "x": 1593413298000, "y": 2 }, - { "x": 1593413299000, "y": 0 }, - { "x": 1593413300000, "y": 0 }, - { "x": 1593413301000, "y": 3 }, - { "x": 1593413302000, "y": 0 }, - { "x": 1593413303000, "y": 0 }, - { "x": 1593413304000, "y": 0 }, - { "x": 1593413305000, "y": 0 }, - { "x": 1593413306000, "y": 0 }, - { "x": 1593413307000, "y": 0 }, - { "x": 1593413308000, "y": 0 }, - { "x": 1593413309000, "y": 0 }, - { "x": 1593413310000, "y": 0 }, - { "x": 1593413311000, "y": 0 }, - { "x": 1593413312000, "y": 0 }, - { "x": 1593413313000, "y": 0 }, - { "x": 1593413314000, "y": 0 }, - { "x": 1593413315000, "y": 0 }, - { "x": 1593413316000, "y": 0 }, - { "x": 1593413317000, "y": 2 }, - { "x": 1593413318000, "y": 0 }, - { "x": 1593413319000, "y": 0 }, - { "x": 1593413320000, "y": 0 }, - { "x": 1593413321000, "y": 0 }, - { "x": 1593413322000, "y": 0 }, - { "x": 1593413323000, "y": 0 }, - { "x": 1593413324000, "y": 0 }, - { "x": 1593413325000, "y": 0 }, - { "x": 1593413326000, "y": 0 }, - { "x": 1593413327000, "y": 0 }, - { "x": 1593413328000, "y": 0 }, - { "x": 1593413329000, "y": 0 }, - { "x": 1593413330000, "y": 0 }, - { "x": 1593413331000, "y": 0 }, - { "x": 1593413332000, "y": 0 }, - { "x": 1593413333000, "y": 0 }, - { "x": 1593413334000, "y": 0 }, - { "x": 1593413335000, "y": 0 }, - { "x": 1593413336000, "y": 0 }, - { "x": 1593413337000, "y": 0 }, - { "x": 1593413338000, "y": 0 }, - { "x": 1593413339000, "y": 0 }, - { "x": 1593413340000, "y": 0 } - ], - "avg": 1.75 - }, - { - "key": "HTTP 4xx", - "dataPoints": [ - { "x": 1593413100000, "y": 0 }, - { "x": 1593413101000, "y": 0 }, - { "x": 1593413102000, "y": 0 }, - { "x": 1593413103000, "y": 0 }, - { "x": 1593413104000, "y": 0 }, - { "x": 1593413105000, "y": 0 }, - { "x": 1593413106000, "y": 0 }, - { "x": 1593413107000, "y": 0 }, - { "x": 1593413108000, "y": 0 }, - { "x": 1593413109000, "y": 0 }, - { "x": 1593413110000, "y": 0 }, - { "x": 1593413111000, "y": 0 }, - { "x": 1593413112000, "y": 0 }, - { "x": 1593413113000, "y": 0 }, - { "x": 1593413114000, "y": 0 }, - { "x": 1593413115000, "y": 0 }, - { "x": 1593413116000, "y": 0 }, - { "x": 1593413117000, "y": 0 }, - { "x": 1593413118000, "y": 0 }, - { "x": 1593413119000, "y": 0 }, - { "x": 1593413120000, "y": 0 }, - { "x": 1593413121000, "y": 0 }, - { "x": 1593413122000, "y": 0 }, - { "x": 1593413123000, "y": 0 }, - { "x": 1593413124000, "y": 0 }, - { "x": 1593413125000, "y": 0 }, - { "x": 1593413126000, "y": 0 }, - { "x": 1593413127000, "y": 0 }, - { "x": 1593413128000, "y": 0 }, - { "x": 1593413129000, "y": 0 }, - { "x": 1593413130000, "y": 0 }, - { "x": 1593413131000, "y": 0 }, - { "x": 1593413132000, "y": 0 }, - { "x": 1593413133000, "y": 0 }, - { "x": 1593413134000, "y": 0 }, - { "x": 1593413135000, "y": 0 }, - { "x": 1593413136000, "y": 0 }, - { "x": 1593413137000, "y": 0 }, - { "x": 1593413138000, "y": 0 }, - { "x": 1593413139000, "y": 0 }, - { "x": 1593413140000, "y": 0 }, - { "x": 1593413141000, "y": 0 }, - { "x": 1593413142000, "y": 0 }, - { "x": 1593413143000, "y": 0 }, - { "x": 1593413144000, "y": 0 }, - { "x": 1593413145000, "y": 0 }, - { "x": 1593413146000, "y": 0 }, - { "x": 1593413147000, "y": 0 }, - { "x": 1593413148000, "y": 0 }, - { "x": 1593413149000, "y": 0 }, - { "x": 1593413150000, "y": 0 }, - { "x": 1593413151000, "y": 0 }, - { "x": 1593413152000, "y": 0 }, - { "x": 1593413153000, "y": 0 }, - { "x": 1593413154000, "y": 0 }, - { "x": 1593413155000, "y": 0 }, - { "x": 1593413156000, "y": 0 }, - { "x": 1593413157000, "y": 0 }, - { "x": 1593413158000, "y": 0 }, - { "x": 1593413159000, "y": 0 }, - { "x": 1593413160000, "y": 0 }, - { "x": 1593413161000, "y": 0 }, - { "x": 1593413162000, "y": 0 }, - { "x": 1593413163000, "y": 0 }, - { "x": 1593413164000, "y": 0 }, - { "x": 1593413165000, "y": 0 }, - { "x": 1593413166000, "y": 0 }, - { "x": 1593413167000, "y": 0 }, - { "x": 1593413168000, "y": 0 }, - { "x": 1593413169000, "y": 0 }, - { "x": 1593413170000, "y": 0 }, - { "x": 1593413171000, "y": 0 }, - { "x": 1593413172000, "y": 0 }, - { "x": 1593413173000, "y": 0 }, - { "x": 1593413174000, "y": 0 }, - { "x": 1593413175000, "y": 0 }, - { "x": 1593413176000, "y": 0 }, - { "x": 1593413177000, "y": 0 }, - { "x": 1593413178000, "y": 0 }, - { "x": 1593413179000, "y": 0 }, - { "x": 1593413180000, "y": 0 }, - { "x": 1593413181000, "y": 0 }, - { "x": 1593413182000, "y": 0 }, - { "x": 1593413183000, "y": 0 }, - { "x": 1593413184000, "y": 0 }, - { "x": 1593413185000, "y": 0 }, - { "x": 1593413186000, "y": 0 }, - { "x": 1593413187000, "y": 0 }, - { "x": 1593413188000, "y": 0 }, - { "x": 1593413189000, "y": 0 }, - { "x": 1593413190000, "y": 0 }, - { "x": 1593413191000, "y": 0 }, - { "x": 1593413192000, "y": 0 }, - { "x": 1593413193000, "y": 0 }, - { "x": 1593413194000, "y": 0 }, - { "x": 1593413195000, "y": 0 }, - { "x": 1593413196000, "y": 0 }, - { "x": 1593413197000, "y": 0 }, - { "x": 1593413198000, "y": 0 }, - { "x": 1593413199000, "y": 0 }, - { "x": 1593413200000, "y": 0 }, - { "x": 1593413201000, "y": 0 }, - { "x": 1593413202000, "y": 0 }, - { "x": 1593413203000, "y": 0 }, - { "x": 1593413204000, "y": 0 }, - { "x": 1593413205000, "y": 0 }, - { "x": 1593413206000, "y": 0 }, - { "x": 1593413207000, "y": 0 }, - { "x": 1593413208000, "y": 0 }, - { "x": 1593413209000, "y": 0 }, - { "x": 1593413210000, "y": 0 }, - { "x": 1593413211000, "y": 0 }, - { "x": 1593413212000, "y": 0 }, - { "x": 1593413213000, "y": 0 }, - { "x": 1593413214000, "y": 0 }, - { "x": 1593413215000, "y": 0 }, - { "x": 1593413216000, "y": 0 }, - { "x": 1593413217000, "y": 0 }, - { "x": 1593413218000, "y": 0 }, - { "x": 1593413219000, "y": 0 }, - { "x": 1593413220000, "y": 0 }, - { "x": 1593413221000, "y": 0 }, - { "x": 1593413222000, "y": 0 }, - { "x": 1593413223000, "y": 0 }, - { "x": 1593413224000, "y": 0 }, - { "x": 1593413225000, "y": 0 }, - { "x": 1593413226000, "y": 0 }, - { "x": 1593413227000, "y": 0 }, - { "x": 1593413228000, "y": 0 }, - { "x": 1593413229000, "y": 0 }, - { "x": 1593413230000, "y": 0 }, - { "x": 1593413231000, "y": 0 }, - { "x": 1593413232000, "y": 0 }, - { "x": 1593413233000, "y": 0 }, - { "x": 1593413234000, "y": 0 }, - { "x": 1593413235000, "y": 0 }, - { "x": 1593413236000, "y": 0 }, - { "x": 1593413237000, "y": 0 }, - { "x": 1593413238000, "y": 0 }, - { "x": 1593413239000, "y": 0 }, - { "x": 1593413240000, "y": 0 }, - { "x": 1593413241000, "y": 0 }, - { "x": 1593413242000, "y": 0 }, - { "x": 1593413243000, "y": 0 }, - { "x": 1593413244000, "y": 0 }, - { "x": 1593413245000, "y": 0 }, - { "x": 1593413246000, "y": 0 }, - { "x": 1593413247000, "y": 0 }, - { "x": 1593413248000, "y": 0 }, - { "x": 1593413249000, "y": 0 }, - { "x": 1593413250000, "y": 0 }, - { "x": 1593413251000, "y": 0 }, - { "x": 1593413252000, "y": 0 }, - { "x": 1593413253000, "y": 0 }, - { "x": 1593413254000, "y": 0 }, - { "x": 1593413255000, "y": 0 }, - { "x": 1593413256000, "y": 0 }, - { "x": 1593413257000, "y": 0 }, - { "x": 1593413258000, "y": 0 }, - { "x": 1593413259000, "y": 0 }, - { "x": 1593413260000, "y": 0 }, - { "x": 1593413261000, "y": 0 }, - { "x": 1593413262000, "y": 0 }, - { "x": 1593413263000, "y": 0 }, - { "x": 1593413264000, "y": 0 }, - { "x": 1593413265000, "y": 0 }, - { "x": 1593413266000, "y": 0 }, - { "x": 1593413267000, "y": 0 }, - { "x": 1593413268000, "y": 0 }, - { "x": 1593413269000, "y": 0 }, - { "x": 1593413270000, "y": 0 }, - { "x": 1593413271000, "y": 0 }, - { "x": 1593413272000, "y": 0 }, - { "x": 1593413273000, "y": 0 }, - { "x": 1593413274000, "y": 0 }, - { "x": 1593413275000, "y": 0 }, - { "x": 1593413276000, "y": 0 }, - { "x": 1593413277000, "y": 0 }, - { "x": 1593413278000, "y": 0 }, - { "x": 1593413279000, "y": 0 }, - { "x": 1593413280000, "y": 0 }, - { "x": 1593413281000, "y": 0 }, - { "x": 1593413282000, "y": 0 }, - { "x": 1593413283000, "y": 0 }, - { "x": 1593413284000, "y": 0 }, - { "x": 1593413285000, "y": 0 }, - { "x": 1593413286000, "y": 0 }, - { "x": 1593413287000, "y": 0 }, - { "x": 1593413288000, "y": 0 }, - { "x": 1593413289000, "y": 1 }, - { "x": 1593413290000, "y": 0 }, - { "x": 1593413291000, "y": 0 }, - { "x": 1593413292000, "y": 1 }, - { "x": 1593413293000, "y": 0 }, - { "x": 1593413294000, "y": 0 }, - { "x": 1593413295000, "y": 0 }, - { "x": 1593413296000, "y": 0 }, - { "x": 1593413297000, "y": 0 }, - { "x": 1593413298000, "y": 0 }, - { "x": 1593413299000, "y": 0 }, - { "x": 1593413300000, "y": 1 }, - { "x": 1593413301000, "y": 0 }, - { "x": 1593413302000, "y": 0 }, - { "x": 1593413303000, "y": 0 }, - { "x": 1593413304000, "y": 0 }, - { "x": 1593413305000, "y": 1 }, - { "x": 1593413306000, "y": 0 }, - { "x": 1593413307000, "y": 0 }, - { "x": 1593413308000, "y": 0 }, - { "x": 1593413309000, "y": 1 }, - { "x": 1593413310000, "y": 1 }, - { "x": 1593413311000, "y": 0 }, - { "x": 1593413312000, "y": 0 }, - { "x": 1593413313000, "y": 0 }, - { "x": 1593413314000, "y": 0 }, - { "x": 1593413315000, "y": 1 }, - { "x": 1593413316000, "y": 0 }, - { "x": 1593413317000, "y": 0 }, - { "x": 1593413318000, "y": 0 }, - { "x": 1593413319000, "y": 0 }, - { "x": 1593413320000, "y": 1 }, - { "x": 1593413321000, "y": 0 }, - { "x": 1593413322000, "y": 0 }, - { "x": 1593413323000, "y": 0 }, - { "x": 1593413324000, "y": 0 }, - { "x": 1593413325000, "y": 0 }, - { "x": 1593413326000, "y": 0 }, - { "x": 1593413327000, "y": 0 }, - { "x": 1593413328000, "y": 0 }, - { "x": 1593413329000, "y": 0 }, - { "x": 1593413330000, "y": 0 }, - { "x": 1593413331000, "y": 0 }, - { "x": 1593413332000, "y": 0 }, - { "x": 1593413333000, "y": 0 }, - { "x": 1593413334000, "y": 0 }, - { "x": 1593413335000, "y": 0 }, - { "x": 1593413336000, "y": 0 }, - { "x": 1593413337000, "y": 0 }, - { "x": 1593413338000, "y": 0 }, - { "x": 1593413339000, "y": 0 }, - { "x": 1593413340000, "y": 0 } - ], - "avg": 2 - }, - { - "key": "HTTP 5xx", - "dataPoints": [ - { "x": 1593413100000, "y": 0 }, - { "x": 1593413101000, "y": 0 }, - { "x": 1593413102000, "y": 0 }, - { "x": 1593413103000, "y": 0 }, - { "x": 1593413104000, "y": 0 }, - { "x": 1593413105000, "y": 0 }, - { "x": 1593413106000, "y": 0 }, - { "x": 1593413107000, "y": 0 }, - { "x": 1593413108000, "y": 0 }, - { "x": 1593413109000, "y": 0 }, - { "x": 1593413110000, "y": 0 }, - { "x": 1593413111000, "y": 0 }, - { "x": 1593413112000, "y": 0 }, - { "x": 1593413113000, "y": 0 }, - { "x": 1593413114000, "y": 0 }, - { "x": 1593413115000, "y": 0 }, - { "x": 1593413116000, "y": 0 }, - { "x": 1593413117000, "y": 0 }, - { "x": 1593413118000, "y": 0 }, - { "x": 1593413119000, "y": 0 }, - { "x": 1593413120000, "y": 0 }, - { "x": 1593413121000, "y": 0 }, - { "x": 1593413122000, "y": 0 }, - { "x": 1593413123000, "y": 0 }, - { "x": 1593413124000, "y": 0 }, - { "x": 1593413125000, "y": 0 }, - { "x": 1593413126000, "y": 0 }, - { "x": 1593413127000, "y": 0 }, - { "x": 1593413128000, "y": 0 }, - { "x": 1593413129000, "y": 0 }, - { "x": 1593413130000, "y": 0 }, - { "x": 1593413131000, "y": 0 }, - { "x": 1593413132000, "y": 0 }, - { "x": 1593413133000, "y": 0 }, - { "x": 1593413134000, "y": 0 }, - { "x": 1593413135000, "y": 0 }, - { "x": 1593413136000, "y": 0 }, - { "x": 1593413137000, "y": 0 }, - { "x": 1593413138000, "y": 0 }, - { "x": 1593413139000, "y": 0 }, - { "x": 1593413140000, "y": 0 }, - { "x": 1593413141000, "y": 0 }, - { "x": 1593413142000, "y": 0 }, - { "x": 1593413143000, "y": 0 }, - { "x": 1593413144000, "y": 0 }, - { "x": 1593413145000, "y": 0 }, - { "x": 1593413146000, "y": 0 }, - { "x": 1593413147000, "y": 0 }, - { "x": 1593413148000, "y": 0 }, - { "x": 1593413149000, "y": 0 }, - { "x": 1593413150000, "y": 0 }, - { "x": 1593413151000, "y": 0 }, - { "x": 1593413152000, "y": 0 }, - { "x": 1593413153000, "y": 0 }, - { "x": 1593413154000, "y": 0 }, - { "x": 1593413155000, "y": 0 }, - { "x": 1593413156000, "y": 0 }, - { "x": 1593413157000, "y": 0 }, - { "x": 1593413158000, "y": 0 }, - { "x": 1593413159000, "y": 0 }, - { "x": 1593413160000, "y": 0 }, - { "x": 1593413161000, "y": 0 }, - { "x": 1593413162000, "y": 0 }, - { "x": 1593413163000, "y": 0 }, - { "x": 1593413164000, "y": 0 }, - { "x": 1593413165000, "y": 0 }, - { "x": 1593413166000, "y": 0 }, - { "x": 1593413167000, "y": 0 }, - { "x": 1593413168000, "y": 0 }, - { "x": 1593413169000, "y": 0 }, - { "x": 1593413170000, "y": 0 }, - { "x": 1593413171000, "y": 0 }, - { "x": 1593413172000, "y": 0 }, - { "x": 1593413173000, "y": 0 }, - { "x": 1593413174000, "y": 0 }, - { "x": 1593413175000, "y": 0 }, - { "x": 1593413176000, "y": 0 }, - { "x": 1593413177000, "y": 0 }, - { "x": 1593413178000, "y": 0 }, - { "x": 1593413179000, "y": 0 }, - { "x": 1593413180000, "y": 0 }, - { "x": 1593413181000, "y": 0 }, - { "x": 1593413182000, "y": 0 }, - { "x": 1593413183000, "y": 0 }, - { "x": 1593413184000, "y": 0 }, - { "x": 1593413185000, "y": 0 }, - { "x": 1593413186000, "y": 0 }, - { "x": 1593413187000, "y": 0 }, - { "x": 1593413188000, "y": 0 }, - { "x": 1593413189000, "y": 0 }, - { "x": 1593413190000, "y": 0 }, - { "x": 1593413191000, "y": 0 }, - { "x": 1593413192000, "y": 0 }, - { "x": 1593413193000, "y": 0 }, - { "x": 1593413194000, "y": 0 }, - { "x": 1593413195000, "y": 0 }, - { "x": 1593413196000, "y": 0 }, - { "x": 1593413197000, "y": 0 }, - { "x": 1593413198000, "y": 0 }, - { "x": 1593413199000, "y": 0 }, - { "x": 1593413200000, "y": 0 }, - { "x": 1593413201000, "y": 0 }, - { "x": 1593413202000, "y": 0 }, - { "x": 1593413203000, "y": 0 }, - { "x": 1593413204000, "y": 0 }, - { "x": 1593413205000, "y": 0 }, - { "x": 1593413206000, "y": 0 }, - { "x": 1593413207000, "y": 0 }, - { "x": 1593413208000, "y": 0 }, - { "x": 1593413209000, "y": 0 }, - { "x": 1593413210000, "y": 0 }, - { "x": 1593413211000, "y": 0 }, - { "x": 1593413212000, "y": 0 }, - { "x": 1593413213000, "y": 0 }, - { "x": 1593413214000, "y": 0 }, - { "x": 1593413215000, "y": 0 }, - { "x": 1593413216000, "y": 0 }, - { "x": 1593413217000, "y": 0 }, - { "x": 1593413218000, "y": 0 }, - { "x": 1593413219000, "y": 0 }, - { "x": 1593413220000, "y": 0 }, - { "x": 1593413221000, "y": 0 }, - { "x": 1593413222000, "y": 0 }, - { "x": 1593413223000, "y": 0 }, - { "x": 1593413224000, "y": 0 }, - { "x": 1593413225000, "y": 0 }, - { "x": 1593413226000, "y": 0 }, - { "x": 1593413227000, "y": 0 }, - { "x": 1593413228000, "y": 0 }, - { "x": 1593413229000, "y": 0 }, - { "x": 1593413230000, "y": 0 }, - { "x": 1593413231000, "y": 0 }, - { "x": 1593413232000, "y": 0 }, - { "x": 1593413233000, "y": 0 }, - { "x": 1593413234000, "y": 0 }, - { "x": 1593413235000, "y": 0 }, - { "x": 1593413236000, "y": 0 }, - { "x": 1593413237000, "y": 0 }, - { "x": 1593413238000, "y": 0 }, - { "x": 1593413239000, "y": 0 }, - { "x": 1593413240000, "y": 0 }, - { "x": 1593413241000, "y": 0 }, - { "x": 1593413242000, "y": 0 }, - { "x": 1593413243000, "y": 0 }, - { "x": 1593413244000, "y": 0 }, - { "x": 1593413245000, "y": 0 }, - { "x": 1593413246000, "y": 0 }, - { "x": 1593413247000, "y": 0 }, - { "x": 1593413248000, "y": 0 }, - { "x": 1593413249000, "y": 0 }, - { "x": 1593413250000, "y": 0 }, - { "x": 1593413251000, "y": 0 }, - { "x": 1593413252000, "y": 0 }, - { "x": 1593413253000, "y": 0 }, - { "x": 1593413254000, "y": 0 }, - { "x": 1593413255000, "y": 0 }, - { "x": 1593413256000, "y": 0 }, - { "x": 1593413257000, "y": 0 }, - { "x": 1593413258000, "y": 0 }, - { "x": 1593413259000, "y": 0 }, - { "x": 1593413260000, "y": 0 }, - { "x": 1593413261000, "y": 0 }, - { "x": 1593413262000, "y": 0 }, - { "x": 1593413263000, "y": 0 }, - { "x": 1593413264000, "y": 0 }, - { "x": 1593413265000, "y": 0 }, - { "x": 1593413266000, "y": 0 }, - { "x": 1593413267000, "y": 0 }, - { "x": 1593413268000, "y": 0 }, - { "x": 1593413269000, "y": 0 }, - { "x": 1593413270000, "y": 0 }, - { "x": 1593413271000, "y": 0 }, - { "x": 1593413272000, "y": 0 }, - { "x": 1593413273000, "y": 0 }, - { "x": 1593413274000, "y": 0 }, - { "x": 1593413275000, "y": 0 }, - { "x": 1593413276000, "y": 0 }, - { "x": 1593413277000, "y": 0 }, - { "x": 1593413278000, "y": 0 }, - { "x": 1593413279000, "y": 0 }, - { "x": 1593413280000, "y": 0 }, - { "x": 1593413281000, "y": 0 }, - { "x": 1593413282000, "y": 0 }, - { "x": 1593413283000, "y": 0 }, - { "x": 1593413284000, "y": 0 }, - { "x": 1593413285000, "y": 0 }, - { "x": 1593413286000, "y": 1 }, - { "x": 1593413287000, "y": 1 }, - { "x": 1593413288000, "y": 0 }, - { "x": 1593413289000, "y": 0 }, - { "x": 1593413290000, "y": 0 }, - { "x": 1593413291000, "y": 0 }, - { "x": 1593413292000, "y": 0 }, - { "x": 1593413293000, "y": 0 }, - { "x": 1593413294000, "y": 0 }, - { "x": 1593413295000, "y": 0 }, - { "x": 1593413296000, "y": 0 }, - { "x": 1593413297000, "y": 0 }, - { "x": 1593413298000, "y": 0 }, - { "x": 1593413299000, "y": 1 }, - { "x": 1593413300000, "y": 0 }, - { "x": 1593413301000, "y": 1 }, - { "x": 1593413302000, "y": 0 }, - { "x": 1593413303000, "y": 0 }, - { "x": 1593413304000, "y": 0 }, - { "x": 1593413305000, "y": 1 }, - { "x": 1593413306000, "y": 0 }, - { "x": 1593413307000, "y": 0 }, - { "x": 1593413308000, "y": 1 }, - { "x": 1593413309000, "y": 0 }, - { "x": 1593413310000, "y": 0 }, - { "x": 1593413311000, "y": 1 }, - { "x": 1593413312000, "y": 0 }, - { "x": 1593413313000, "y": 0 }, - { "x": 1593413314000, "y": 0 }, - { "x": 1593413315000, "y": 1 }, - { "x": 1593413316000, "y": 0 }, - { "x": 1593413317000, "y": 0 }, - { "x": 1593413318000, "y": 0 }, - { "x": 1593413319000, "y": 0 }, - { "x": 1593413320000, "y": 0 }, - { "x": 1593413321000, "y": 0 }, - { "x": 1593413322000, "y": 1 }, - { "x": 1593413323000, "y": 0 }, - { "x": 1593413324000, "y": 0 }, - { "x": 1593413325000, "y": 0 }, - { "x": 1593413326000, "y": 0 }, - { "x": 1593413327000, "y": 0 }, - { "x": 1593413328000, "y": 0 }, - { "x": 1593413329000, "y": 0 }, - { "x": 1593413330000, "y": 0 }, - { "x": 1593413331000, "y": 0 }, - { "x": 1593413332000, "y": 0 }, - { "x": 1593413333000, "y": 0 }, - { "x": 1593413334000, "y": 0 }, - { "x": 1593413335000, "y": 0 }, - { "x": 1593413336000, "y": 0 }, - { "x": 1593413337000, "y": 0 }, - { "x": 1593413338000, "y": 0 }, - { "x": 1593413339000, "y": 0 }, - { "x": 1593413340000, "y": 0 } - ], - "avg": 2.25 - }, - { - "key": "success", - "dataPoints": [ - { "x": 1593413100000, "y": 0 }, - { "x": 1593413101000, "y": 0 }, - { "x": 1593413102000, "y": 0 }, - { "x": 1593413103000, "y": 0 }, - { "x": 1593413104000, "y": 0 }, - { "x": 1593413105000, "y": 0 }, - { "x": 1593413106000, "y": 0 }, - { "x": 1593413107000, "y": 0 }, - { "x": 1593413108000, "y": 0 }, - { "x": 1593413109000, "y": 0 }, - { "x": 1593413110000, "y": 0 }, - { "x": 1593413111000, "y": 0 }, - { "x": 1593413112000, "y": 0 }, - { "x": 1593413113000, "y": 0 }, - { "x": 1593413114000, "y": 0 }, - { "x": 1593413115000, "y": 0 }, - { "x": 1593413116000, "y": 0 }, - { "x": 1593413117000, "y": 0 }, - { "x": 1593413118000, "y": 0 }, - { "x": 1593413119000, "y": 0 }, - { "x": 1593413120000, "y": 0 }, - { "x": 1593413121000, "y": 0 }, - { "x": 1593413122000, "y": 0 }, - { "x": 1593413123000, "y": 0 }, - { "x": 1593413124000, "y": 0 }, - { "x": 1593413125000, "y": 0 }, - { "x": 1593413126000, "y": 0 }, - { "x": 1593413127000, "y": 0 }, - { "x": 1593413128000, "y": 0 }, - { "x": 1593413129000, "y": 0 }, - { "x": 1593413130000, "y": 0 }, - { "x": 1593413131000, "y": 0 }, - { "x": 1593413132000, "y": 0 }, - { "x": 1593413133000, "y": 0 }, - { "x": 1593413134000, "y": 0 }, - { "x": 1593413135000, "y": 0 }, - { "x": 1593413136000, "y": 0 }, - { "x": 1593413137000, "y": 0 }, - { "x": 1593413138000, "y": 0 }, - { "x": 1593413139000, "y": 0 }, - { "x": 1593413140000, "y": 0 }, - { "x": 1593413141000, "y": 0 }, - { "x": 1593413142000, "y": 0 }, - { "x": 1593413143000, "y": 0 }, - { "x": 1593413144000, "y": 0 }, - { "x": 1593413145000, "y": 0 }, - { "x": 1593413146000, "y": 0 }, - { "x": 1593413147000, "y": 0 }, - { "x": 1593413148000, "y": 0 }, - { "x": 1593413149000, "y": 0 }, - { "x": 1593413150000, "y": 0 }, - { "x": 1593413151000, "y": 0 }, - { "x": 1593413152000, "y": 0 }, - { "x": 1593413153000, "y": 0 }, - { "x": 1593413154000, "y": 0 }, - { "x": 1593413155000, "y": 0 }, - { "x": 1593413156000, "y": 0 }, - { "x": 1593413157000, "y": 0 }, - { "x": 1593413158000, "y": 0 }, - { "x": 1593413159000, "y": 0 }, - { "x": 1593413160000, "y": 0 }, - { "x": 1593413161000, "y": 0 }, - { "x": 1593413162000, "y": 0 }, - { "x": 1593413163000, "y": 0 }, - { "x": 1593413164000, "y": 0 }, - { "x": 1593413165000, "y": 0 }, - { "x": 1593413166000, "y": 0 }, - { "x": 1593413167000, "y": 0 }, - { "x": 1593413168000, "y": 0 }, - { "x": 1593413169000, "y": 0 }, - { "x": 1593413170000, "y": 0 }, - { "x": 1593413171000, "y": 0 }, - { "x": 1593413172000, "y": 0 }, - { "x": 1593413173000, "y": 0 }, - { "x": 1593413174000, "y": 0 }, - { "x": 1593413175000, "y": 0 }, - { "x": 1593413176000, "y": 0 }, - { "x": 1593413177000, "y": 0 }, - { "x": 1593413178000, "y": 0 }, - { "x": 1593413179000, "y": 0 }, - { "x": 1593413180000, "y": 0 }, - { "x": 1593413181000, "y": 0 }, - { "x": 1593413182000, "y": 0 }, - { "x": 1593413183000, "y": 0 }, - { "x": 1593413184000, "y": 0 }, - { "x": 1593413185000, "y": 0 }, - { "x": 1593413186000, "y": 0 }, - { "x": 1593413187000, "y": 0 }, - { "x": 1593413188000, "y": 0 }, - { "x": 1593413189000, "y": 0 }, - { "x": 1593413190000, "y": 0 }, - { "x": 1593413191000, "y": 0 }, - { "x": 1593413192000, "y": 0 }, - { "x": 1593413193000, "y": 0 }, - { "x": 1593413194000, "y": 0 }, - { "x": 1593413195000, "y": 0 }, - { "x": 1593413196000, "y": 0 }, - { "x": 1593413197000, "y": 0 }, - { "x": 1593413198000, "y": 0 }, - { "x": 1593413199000, "y": 0 }, - { "x": 1593413200000, "y": 0 }, - { "x": 1593413201000, "y": 0 }, - { "x": 1593413202000, "y": 0 }, - { "x": 1593413203000, "y": 0 }, - { "x": 1593413204000, "y": 0 }, - { "x": 1593413205000, "y": 0 }, - { "x": 1593413206000, "y": 0 }, - { "x": 1593413207000, "y": 0 }, - { "x": 1593413208000, "y": 0 }, - { "x": 1593413209000, "y": 0 }, - { "x": 1593413210000, "y": 0 }, - { "x": 1593413211000, "y": 0 }, - { "x": 1593413212000, "y": 0 }, - { "x": 1593413213000, "y": 0 }, - { "x": 1593413214000, "y": 0 }, - { "x": 1593413215000, "y": 0 }, - { "x": 1593413216000, "y": 0 }, - { "x": 1593413217000, "y": 0 }, - { "x": 1593413218000, "y": 0 }, - { "x": 1593413219000, "y": 0 }, - { "x": 1593413220000, "y": 0 }, - { "x": 1593413221000, "y": 0 }, - { "x": 1593413222000, "y": 0 }, - { "x": 1593413223000, "y": 0 }, - { "x": 1593413224000, "y": 0 }, - { "x": 1593413225000, "y": 0 }, - { "x": 1593413226000, "y": 0 }, - { "x": 1593413227000, "y": 0 }, - { "x": 1593413228000, "y": 0 }, - { "x": 1593413229000, "y": 0 }, - { "x": 1593413230000, "y": 0 }, - { "x": 1593413231000, "y": 0 }, - { "x": 1593413232000, "y": 0 }, - { "x": 1593413233000, "y": 0 }, - { "x": 1593413234000, "y": 0 }, - { "x": 1593413235000, "y": 0 }, - { "x": 1593413236000, "y": 0 }, - { "x": 1593413237000, "y": 0 }, - { "x": 1593413238000, "y": 0 }, - { "x": 1593413239000, "y": 0 }, - { "x": 1593413240000, "y": 0 }, - { "x": 1593413241000, "y": 0 }, - { "x": 1593413242000, "y": 0 }, - { "x": 1593413243000, "y": 0 }, - { "x": 1593413244000, "y": 0 }, - { "x": 1593413245000, "y": 0 }, - { "x": 1593413246000, "y": 0 }, - { "x": 1593413247000, "y": 0 }, - { "x": 1593413248000, "y": 0 }, - { "x": 1593413249000, "y": 0 }, - { "x": 1593413250000, "y": 0 }, - { "x": 1593413251000, "y": 0 }, - { "x": 1593413252000, "y": 0 }, - { "x": 1593413253000, "y": 0 }, - { "x": 1593413254000, "y": 0 }, - { "x": 1593413255000, "y": 0 }, - { "x": 1593413256000, "y": 0 }, - { "x": 1593413257000, "y": 0 }, - { "x": 1593413258000, "y": 0 }, - { "x": 1593413259000, "y": 0 }, - { "x": 1593413260000, "y": 0 }, - { "x": 1593413261000, "y": 0 }, - { "x": 1593413262000, "y": 0 }, - { "x": 1593413263000, "y": 0 }, - { "x": 1593413264000, "y": 0 }, - { "x": 1593413265000, "y": 0 }, - { "x": 1593413266000, "y": 0 }, - { "x": 1593413267000, "y": 0 }, - { "x": 1593413268000, "y": 0 }, - { "x": 1593413269000, "y": 0 }, - { "x": 1593413270000, "y": 0 }, - { "x": 1593413271000, "y": 0 }, - { "x": 1593413272000, "y": 0 }, - { "x": 1593413273000, "y": 0 }, - { "x": 1593413274000, "y": 0 }, - { "x": 1593413275000, "y": 0 }, - { "x": 1593413276000, "y": 0 }, - { "x": 1593413277000, "y": 0 }, - { "x": 1593413278000, "y": 0 }, - { "x": 1593413279000, "y": 0 }, - { "x": 1593413280000, "y": 0 }, - { "x": 1593413281000, "y": 0 }, - { "x": 1593413282000, "y": 0 }, - { "x": 1593413283000, "y": 0 }, - { "x": 1593413284000, "y": 0 }, - { "x": 1593413285000, "y": 0 }, - { "x": 1593413286000, "y": 0 }, - { "x": 1593413287000, "y": 0 }, - { "x": 1593413288000, "y": 0 }, - { "x": 1593413289000, "y": 0 }, - { "x": 1593413290000, "y": 0 }, - { "x": 1593413291000, "y": 0 }, - { "x": 1593413292000, "y": 0 }, - { "x": 1593413293000, "y": 0 }, - { "x": 1593413294000, "y": 0 }, - { "x": 1593413295000, "y": 0 }, - { "x": 1593413296000, "y": 0 }, - { "x": 1593413297000, "y": 0 }, - { "x": 1593413298000, "y": 0 }, - { "x": 1593413299000, "y": 0 }, - { "x": 1593413300000, "y": 0 }, - { "x": 1593413301000, "y": 0 }, - { "x": 1593413302000, "y": 0 }, - { "x": 1593413303000, "y": 0 }, - { "x": 1593413304000, "y": 0 }, - { "x": 1593413305000, "y": 0 }, - { "x": 1593413306000, "y": 0 }, - { "x": 1593413307000, "y": 0 }, - { "x": 1593413308000, "y": 0 }, - { "x": 1593413309000, "y": 1 }, - { "x": 1593413310000, "y": 0 }, - { "x": 1593413311000, "y": 0 }, - { "x": 1593413312000, "y": 0 }, - { "x": 1593413313000, "y": 0 }, - { "x": 1593413314000, "y": 0 }, - { "x": 1593413315000, "y": 0 }, - { "x": 1593413316000, "y": 0 }, - { "x": 1593413317000, "y": 0 }, - { "x": 1593413318000, "y": 0 }, - { "x": 1593413319000, "y": 0 }, - { "x": 1593413320000, "y": 0 }, - { "x": 1593413321000, "y": 0 }, - { "x": 1593413322000, "y": 0 }, - { "x": 1593413323000, "y": 0 }, - { "x": 1593413324000, "y": 0 }, - { "x": 1593413325000, "y": 0 }, - { "x": 1593413326000, "y": 0 }, - { "x": 1593413327000, "y": 0 }, - { "x": 1593413328000, "y": 0 }, - { "x": 1593413329000, "y": 0 }, - { "x": 1593413330000, "y": 0 }, - { "x": 1593413331000, "y": 0 }, - { "x": 1593413332000, "y": 0 }, - { "x": 1593413333000, "y": 0 }, - { "x": 1593413334000, "y": 0 }, - { "x": 1593413335000, "y": 0 }, - { "x": 1593413336000, "y": 0 }, - { "x": 1593413337000, "y": 0 }, - { "x": 1593413338000, "y": 0 }, - { "x": 1593413339000, "y": 0 }, - { "x": 1593413340000, "y": 0 } - ], - "avg": 0.25 - } - ], - "overallAvgDuration": 38682.52419354839 - } -} diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts index 94559a3e4aa54..cebf27ecdff2b 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/top_transaction_groups.ts @@ -5,8 +5,8 @@ */ import expect from '@kbn/expect'; import { sortBy } from 'lodash'; +import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import expectedTransactionGroups from './expectation/top_transaction_groups.json'; function sortTransactionGroups(items: any[]) { return sortBy(items, 'impact'); @@ -34,7 +34,13 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.eql({ items: [], isAggregationAccurate: true, bucketSize: 1000 }); + expectSnapshot(response.body).toMatchInline(` + Object { + "bucketSize": 1000, + "isAggregationAccurate": true, + "items": Array [], + } + `); }); }); @@ -53,13 +59,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); it('returns the correct number of buckets', async () => { - expect(response.body.items.length).to.be(18); + expectSnapshot(response.body.items.length).toMatchInline(`18`); }); it('returns the correct buckets (when ignoring samples)', async () => { - expect(omitSampleFromTransactionGroups(response.body.items)).to.eql( - omitSampleFromTransactionGroups(expectedTransactionGroups.items) - ); + expectSnapshot(omitSampleFromTransactionGroups(response.body.items)).toMatch(); }); it('returns the correct buckets and samples', async () => { diff --git a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts index 68a7499a2389c..a8418fe2860a3 100644 --- a/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts +++ b/x-pack/test/apm_api_integration/basic/tests/transaction_groups/transaction_charts.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; -import expectedTransactionCharts from './expectation/transaction_charts.json'; export default function ApiTest({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -24,17 +24,19 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.eql({ - apmTimeseries: { - overallAvgDuration: null, - responseTimes: { - avg: [], - p95: [], - p99: [], + expectSnapshot(response.body).toMatchInline(` + Object { + "apmTimeseries": Object { + "overallAvgDuration": null, + "responseTimes": Object { + "avg": Array [], + "p95": Array [], + "p99": Array [], + }, + "tpmBuckets": Array [], }, - tpmBuckets: [], - }, - }); + } + `); }); }); @@ -48,7 +50,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { ); expect(response.status).to.be(200); - expect(response.body).to.eql(expectedTransactionCharts); + expectSnapshot(response.body).toMatch(); }); }); }); diff --git a/x-pack/test/apm_api_integration/common/archives_metadata.ts b/x-pack/test/apm_api_integration/common/archives_metadata.ts new file mode 100644 index 0000000000000..ab9e3a191e24d --- /dev/null +++ b/x-pack/test/apm_api_integration/common/archives_metadata.ts @@ -0,0 +1,12 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export default { + 'apm_8.0.0': { + start: '2020-09-10T06:00:00.000Z', + end: '2020-09-10T07:00:00.000Z', + }, +}; diff --git a/x-pack/test/apm_api_integration/common/config.ts b/x-pack/test/apm_api_integration/common/config.ts index 110f42115397e..5edf1bf23e594 100644 --- a/x-pack/test/apm_api_integration/common/config.ts +++ b/x-pack/test/apm_api_integration/common/config.ts @@ -7,6 +7,7 @@ import { FtrConfigProviderContext } from '@kbn/test/types/ftr'; import supertestAsPromised from 'supertest-as-promised'; import { format, UrlObject } from 'url'; +import path from 'path'; import { InheritedFtrProviderContext, InheritedServices } from './ftr_provider_context'; import { PromiseReturnType } from '../../../plugins/apm/typings/common'; import { createApmUser, APM_TEST_PASSWORD, ApmUser } from './authentication'; @@ -49,6 +50,9 @@ export function createTestConfig(settings: Settings) { return { testFiles, servers, + esArchiver: { + directory: path.resolve(__dirname, './fixtures/es_archiver'), + }, services: { ...services, supertest: supertestAsApmReadUser, diff --git a/x-pack/test/apm_api_integration/basic/fixtures/es_archiver/8.0.0/data.json.gz b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0/data.json.gz similarity index 100% rename from x-pack/test/apm_api_integration/basic/fixtures/es_archiver/8.0.0/data.json.gz rename to x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0/data.json.gz diff --git a/x-pack/test/apm_api_integration/basic/fixtures/es_archiver/8.0.0/mappings.json b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0/mappings.json similarity index 100% rename from x-pack/test/apm_api_integration/basic/fixtures/es_archiver/8.0.0/mappings.json rename to x-pack/test/apm_api_integration/common/fixtures/es_archiver/8.0.0/mappings.json diff --git a/x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0/data.json.gz b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0/data.json.gz new file mode 100644 index 0000000000000..fa40458dedaff Binary files /dev/null and b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0/data.json.gz differ diff --git a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/ml_8.0.0/mappings.json b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0/mappings.json similarity index 99% rename from x-pack/test/apm_api_integration/trial/fixtures/es_archiver/ml_8.0.0/mappings.json rename to x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0/mappings.json index 5a5d48464b8b1..5171ea03fb49f 100644 --- a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/ml_8.0.0/mappings.json +++ b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/apm_8.0.0/mappings.json @@ -1,84 +1,3 @@ -{ - "type": "index", - "value": { - "aliases": { - ".ml-annotations-read": { - "is_hidden": true - }, - ".ml-annotations-write": { - "is_hidden": true - } - }, - "index": ".ml-annotations-6", - "mappings": { - "_meta": { - "version": "8.0.0" - }, - "properties": { - "annotation": { - "type": "text" - }, - "by_field_name": { - "type": "keyword" - }, - "by_field_value": { - "type": "keyword" - }, - "create_time": { - "type": "date" - }, - "create_username": { - "type": "keyword" - }, - "detector_index": { - "type": "integer" - }, - "end_timestamp": { - "type": "date" - }, - "event": { - "type": "keyword" - }, - "job_id": { - "type": "keyword" - }, - "modified_time": { - "type": "date" - }, - "modified_username": { - "type": "keyword" - }, - "over_field_name": { - "type": "keyword" - }, - "over_field_value": { - "type": "keyword" - }, - "partition_field_name": { - "type": "keyword" - }, - "partition_field_value": { - "type": "keyword" - }, - "timestamp": { - "type": "date" - }, - "type": { - "type": "keyword" - } - } - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "hidden": "true", - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} - { "type": "index", "value": { @@ -98,6 +17,9 @@ ".ml-anomalies-.write-auto_http_0x73c4bc9426fb6908_high_latency_by_geo": { "is_hidden": true }, + ".ml-anomalies-.write-auto_http_0xa1e2426c5b01459d_high_latency_by_geo": { + "is_hidden": true + }, ".ml-anomalies-.write-kibana-logs-ui-default-default-log-entry-categories-count": { "is_hidden": true }, @@ -159,6 +81,17 @@ }, "is_hidden": true }, + ".ml-anomalies-auto_http_0xa1e2426c5b01459d_high_latency_by_geo": { + "filter": { + "term": { + "job_id": { + "boost": 1, + "value": "auto_http_0xa1e2426c5b01459d_high_latency_by_geo" + } + } + }, + "is_hidden": true + }, ".ml-anomalies-kibana-logs-ui-default-default-log-entry-categories-count": { "filter": { "term": { @@ -1201,36 +1134,6 @@ } } -{ - "type": "index", - "value": { - "aliases": { - ".ml-state-write": { - "is_hidden": true - } - }, - "index": ".ml-state-000001", - "mappings": { - "_meta": { - "version": "8000099" - }, - "enabled": false - }, - "settings": { - "index": { - "auto_expand_replicas": "0-1", - "hidden": "true", - "lifecycle": { - "name": "ml-size-based-ilm-policy", - "rollover_alias": ".ml-state-write" - }, - "number_of_replicas": "1", - "number_of_shards": "1" - } - } - } -} - { "type": "index", "value": { @@ -63034,6 +62937,14 @@ "scaling_factor": 1000000, "type": "scaled_float" }, + "firstContentfulPaint": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, + "largestContentfulPaint": { + "scaling_factor": 1000000, + "type": "scaled_float" + }, "timeToFirstByte": { "scaling_factor": 1000000, "type": "scaled_float" diff --git a/x-pack/test/apm_api_integration/basic/fixtures/es_archiver/observability_overview/data.json.gz b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/observability_overview/data.json.gz similarity index 100% rename from x-pack/test/apm_api_integration/basic/fixtures/es_archiver/observability_overview/data.json.gz rename to x-pack/test/apm_api_integration/common/fixtures/es_archiver/observability_overview/data.json.gz diff --git a/x-pack/test/apm_api_integration/basic/fixtures/es_archiver/observability_overview/mappings.json b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/observability_overview/mappings.json similarity index 100% rename from x-pack/test/apm_api_integration/basic/fixtures/es_archiver/observability_overview/mappings.json rename to x-pack/test/apm_api_integration/common/fixtures/es_archiver/observability_overview/mappings.json diff --git a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/data.json.gz b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_8.0.0/data.json.gz similarity index 100% rename from x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/data.json.gz rename to x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_8.0.0/data.json.gz diff --git a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/mappings.json b/x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_8.0.0/mappings.json similarity index 100% rename from x-pack/test/apm_api_integration/trial/fixtures/es_archiver/rum_8.0.0/mappings.json rename to x-pack/test/apm_api_integration/common/fixtures/es_archiver/rum_8.0.0/mappings.json diff --git a/x-pack/test/apm_api_integration/common/match_snapshot.ts b/x-pack/test/apm_api_integration/common/match_snapshot.ts new file mode 100644 index 0000000000000..a8cb0418583af --- /dev/null +++ b/x-pack/test/apm_api_integration/common/match_snapshot.ts @@ -0,0 +1,205 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { SnapshotState, toMatchSnapshot, toMatchInlineSnapshot } from 'jest-snapshot'; +import path from 'path'; +import expect from '@kbn/expect'; +// @ts-expect-error +import prettier from 'prettier'; +// @ts-expect-error +import babelTraverse from '@babel/traverse'; +import { Suite, Test } from 'mocha'; + +type ISnapshotState = InstanceType; + +interface SnapshotContext { + snapshotState: ISnapshotState; + currentTestName: string; +} + +let testContext: { + file: string; + snapshotTitle: string; + snapshotContext: SnapshotContext; +} | null = null; + +let registered: boolean = false; + +function getSnapshotMeta(currentTest: Test) { + // Make sure snapshot title is unique per-file, rather than entire + // suite. This allows reuse of tests, for instance to compare + // results for different configurations. + + const titles = [currentTest.title]; + const file = currentTest.file; + + let test: Suite | undefined = currentTest?.parent; + + while (test && test.file === file) { + titles.push(test.title); + test = test.parent; + } + + const snapshotTitle = titles.reverse().join(' '); + + if (!file || !snapshotTitle) { + throw new Error(`file or snapshotTitle not available in Mocha test context`); + } + + return { + file, + snapshotTitle, + }; +} + +export function registerMochaHooksForSnapshots() { + let snapshotStatesByFilePath: Record< + string, + { snapshotState: ISnapshotState; testsInFile: Test[] } + > = {}; + + registered = true; + + beforeEach(function () { + const currentTest = this.currentTest!; + + const { file, snapshotTitle } = getSnapshotMeta(currentTest); + + if (!snapshotStatesByFilePath[file]) { + snapshotStatesByFilePath[file] = getSnapshotState(file, currentTest); + } + + testContext = { + file, + snapshotTitle, + snapshotContext: { + snapshotState: snapshotStatesByFilePath[file].snapshotState, + currentTestName: snapshotTitle, + }, + }; + }); + + afterEach(function () { + testContext = null; + }); + + after(function () { + // save snapshot after tests complete + + const unused: string[] = []; + + const isUpdatingSnapshots = process.env.UPDATE_SNAPSHOTS; + + Object.keys(snapshotStatesByFilePath).forEach((file) => { + const { snapshotState, testsInFile } = snapshotStatesByFilePath[file]; + + testsInFile.forEach((test) => { + const snapshotMeta = getSnapshotMeta(test); + // If test is failed or skipped, mark snapshots as used. Otherwise, + // running a test in isolation will generate false positives. + if (!test.isPassed()) { + snapshotState.markSnapshotsAsCheckedForTest(snapshotMeta.snapshotTitle); + } + }); + + if (!isUpdatingSnapshots) { + unused.push(...snapshotState.getUncheckedKeys()); + } else { + snapshotState.removeUncheckedKeys(); + } + + snapshotState.save(); + }); + + if (unused.length) { + throw new Error( + `${unused.length} obsolete snapshot(s) found:\n${unused.join( + '\n\t' + )}.\n\nRun tests again with \`UPDATE_SNAPSHOTS=1\` to remove them.` + ); + } + + snapshotStatesByFilePath = {}; + + registered = false; + }); +} + +const originalPrepareStackTrace = Error.prepareStackTrace; + +// jest-snapshot uses a stack trace to determine which file/line/column +// an inline snapshot should be written to. We filter out match_snapshot +// from the stack trace to prevent it from wanting to write to this file. + +Error.prepareStackTrace = (error, structuredStackTrace) => { + const filteredStrackTrace = structuredStackTrace.filter((callSite) => { + return !callSite.getFileName()?.endsWith('match_snapshot.ts'); + }); + if (originalPrepareStackTrace) { + return originalPrepareStackTrace(error, filteredStrackTrace); + } +}; + +function getSnapshotState(file: string, test: Test) { + const dirname = path.dirname(file); + const filename = path.basename(file); + + let parent = test.parent; + const testsInFile: Test[] = []; + + while (parent) { + testsInFile.push(...parent.tests); + parent = parent.parent; + } + + const snapshotState = new SnapshotState( + path.join(dirname + `/__snapshots__/` + filename.replace(path.extname(filename), '.snap')), + { + updateSnapshot: process.env.UPDATE_SNAPSHOTS ? 'all' : 'new', + getPrettier: () => prettier, + getBabelTraverse: () => babelTraverse, + } + ); + + return { snapshotState, testsInFile }; +} + +export function expectSnapshot(received: any) { + if (!registered) { + throw new Error( + 'Mocha hooks were not registered before expectSnapshot was used. Call `registerMochaHooksForSnapshots` in your top-level describe().' + ); + } + + if (!testContext) { + throw new Error('A current Mocha context is needed to match snapshots'); + } + + return { + toMatch: expectToMatchSnapshot.bind(null, testContext.snapshotContext, received), + // use bind to support optional 3rd argument (actual) + toMatchInline: expectToMatchInlineSnapshot.bind(null, testContext.snapshotContext, received), + }; +} + +function expectToMatchSnapshot(snapshotContext: SnapshotContext, received: any) { + const matcher = toMatchSnapshot.bind(snapshotContext as any); + const result = matcher(received); + + expect(result.pass).to.eql(true, result.message()); +} + +function expectToMatchInlineSnapshot( + snapshotContext: SnapshotContext, + received: any, + _actual?: any +) { + const matcher = toMatchInlineSnapshot.bind(snapshotContext as any); + + const result = arguments.length === 2 ? matcher(received) : matcher(received, _actual); + + expect(result.pass).to.eql(true, result.message()); +} diff --git a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/8.0.0/data.json.gz b/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/8.0.0/data.json.gz deleted file mode 100644 index e9360878b7bb7..0000000000000 Binary files a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/8.0.0/data.json.gz and /dev/null differ diff --git a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/8.0.0/mappings.json b/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/8.0.0/mappings.json deleted file mode 100644 index 5e9f9f52be8d3..0000000000000 --- a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/8.0.0/mappings.json +++ /dev/null @@ -1,25698 +0,0 @@ -{ - "type": "index", - "value": { - "aliases": { - "apm-8.0.0-error": { - "is_write_index": true - } - }, - "index": "apm-8.0.0-error-000001", - "mappings": { - "_meta": { - "beat": "apm", - "version": "8.0.0" - }, - "date_detection": false, - "dynamic_templates": [ - { - "labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" - } - }, - { - "container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "container.labels.*" - } - }, - { - "dns.answers": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "dns.answers.*" - } - }, - { - "log.syslog": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "log.syslog.*" - } - }, - { - "network.inner": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "network.inner.*" - } - }, - { - "observer.egress": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "observer.egress.*" - } - }, - { - "observer.ingress": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "observer.ingress.*" - } - }, - { - "fields": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "fields.*" - } - }, - { - "docker.container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "docker.container.labels.*" - } - }, - { - "kubernetes.labels.*": { - "mapping": { - "type": "keyword" - }, - "path_match": "kubernetes.labels.*" - } - }, - { - "kubernetes.annotations.*": { - "mapping": { - "type": "keyword" - }, - "path_match": "kubernetes.annotations.*" - } - }, - { - "labels_string": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" - } - }, - { - "labels_boolean": { - "mapping": { - "type": "boolean" - }, - "match_mapping_type": "boolean", - "path_match": "labels.*" - } - }, - { - "labels_*": { - "mapping": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "path_match": "labels.*" - } - }, - { - "transaction.marks": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "transaction.marks.*" - } - }, - { - "transaction.marks.*.*": { - "mapping": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "path_match": "transaction.marks.*.*" - } - }, - { - "strings_as_keyword": { - "mapping": { - "ignore_above": 1024, - "type": "keyword" - }, - "match_mapping_type": "string" - } - } - ], - "properties": { - "@timestamp": { - "type": "date" - }, - "agent": { - "dynamic": "false", - "properties": { - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "hostname": { - "path": "agent.name", - "type": "alias" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "child": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "client": { - "dynamic": "false", - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "account": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "availability_zone": { - "ignore_above": 1024, - "type": "keyword" - }, - "image": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "instance": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "machine": { - "dynamic": "false", - "properties": { - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "project": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "region": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "container": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "image": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "tag": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "type": "object" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "runtime": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "destination": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "dll": { - "properties": { - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "dns": { - "properties": { - "answers": { - "properties": { - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "data": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "ttl": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "header_flags": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "op_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "question": { - "properties": { - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "subdomain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "resolved_ip": { - "type": "ip" - }, - "response_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "docker": { - "properties": { - "container": { - "properties": { - "labels": { - "type": "object" - } - } - } - } - }, - "ecs": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "error": { - "dynamic": "false", - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "culprit": { - "ignore_above": 1024, - "type": "keyword" - }, - "exception": { - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "handled": { - "type": "boolean" - }, - "message": { - "norms": false, - "type": "text" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "grouping_key": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "log": { - "properties": { - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "logger_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "message": { - "norms": false, - "type": "text" - }, - "param_message": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "message": { - "norms": false, - "type": "text" - }, - "stack_trace": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "event": { - "properties": { - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "created": { - "type": "date" - }, - "dataset": { - "ignore_above": 1024, - "type": "keyword" - }, - "duration": { - "type": "long" - }, - "end": { - "type": "date" - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ingested": { - "type": "date" - }, - "kind": { - "ignore_above": 1024, - "type": "keyword" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "outcome": { - "ignore_above": 1024, - "type": "keyword" - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "risk_score": { - "type": "float" - }, - "risk_score_norm": { - "type": "float" - }, - "sequence": { - "type": "long" - }, - "severity": { - "type": "long" - }, - "start": { - "type": "date" - }, - "timezone": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "url": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "experimental": { - "dynamic": "true", - "type": "object" - }, - "fields": { - "type": "object" - }, - "file": { - "properties": { - "accessed": { - "type": "date" - }, - "attributes": { - "ignore_above": 1024, - "type": "keyword" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "created": { - "type": "date" - }, - "ctime": { - "type": "date" - }, - "device": { - "ignore_above": 1024, - "type": "keyword" - }, - "directory": { - "ignore_above": 1024, - "type": "keyword" - }, - "drive_letter": { - "ignore_above": 1, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "inode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mime_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "mode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mtime": { - "type": "date" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "owner": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "size": { - "type": "long" - }, - "target_path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "host": { - "dynamic": "false", - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "containerized": { - "type": "boolean" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "ignore_above": 1024, - "type": "keyword" - }, - "codename": { - "ignore_above": 1024, - "type": "keyword" - }, - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "http": { - "dynamic": "false", - "properties": { - "request": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "headers": { - "enabled": false, - "type": "object" - }, - "method": { - "ignore_above": 1024, - "type": "keyword" - }, - "referrer": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "response": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "finished": { - "type": "boolean" - }, - "headers": { - "enabled": false, - "type": "object" - }, - "status_code": { - "type": "long" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "kubernetes": { - "dynamic": "false", - "properties": { - "annotations": { - "properties": { - "*": { - "type": "object" - } - } - }, - "container": { - "properties": { - "image": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "deployment": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "properties": { - "*": { - "type": "object" - } - } - }, - "namespace": { - "ignore_above": 1024, - "type": "keyword" - }, - "node": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pod": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "replicaset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "statefulset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "labels": { - "dynamic": "true", - "properties": { - "foo": { - "type": "keyword" - }, - "lorem": { - "type": "keyword" - }, - "multi-line": { - "type": "keyword" - }, - "this-is-a-very-long-tag-name-without-any-spaces": { - "type": "keyword" - } - } - }, - "log": { - "properties": { - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "logger": { - "ignore_above": 1024, - "type": "keyword" - }, - "origin": { - "properties": { - "file": { - "properties": { - "line": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "syslog": { - "properties": { - "facility": { - "properties": { - "code": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "priority": { - "type": "long" - }, - "severity": { - "properties": { - "code": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "message": { - "norms": false, - "type": "text" - }, - "network": { - "properties": { - "application": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "community_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "direction": { - "ignore_above": 1024, - "type": "keyword" - }, - "forwarded_ip": { - "type": "ip" - }, - "iana_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "inner": { - "properties": { - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "transport": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "observer": { - "dynamic": "false", - "properties": { - "egress": { - "properties": { - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "zone": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "ingress": { - "properties": { - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "zone": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "listening": { - "ignore_above": 1024, - "type": "keyword" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "vendor": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - }, - "version_major": { - "type": "byte" - } - } - }, - "organization": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "package": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "build_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "checksum": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "install_scope": { - "ignore_above": 1024, - "type": "keyword" - }, - "installed": { - "type": "date" - }, - "license": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "parent": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "process": { - "dynamic": "false", - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "parent": { - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "title": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "title": { - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "processor": { - "properties": { - "event": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "profile": { - "dynamic": "false", - "properties": { - "alloc_objects": { - "properties": { - "count": { - "type": "long" - } - } - }, - "alloc_space": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "cpu": { - "properties": { - "ns": { - "type": "long" - } - } - }, - "duration": { - "type": "long" - }, - "inuse_objects": { - "properties": { - "count": { - "type": "long" - } - } - }, - "inuse_space": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "samples": { - "properties": { - "count": { - "type": "long" - } - } - }, - "stack": { - "dynamic": "false", - "properties": { - "filename": { - "ignore_above": 1024, - "type": "keyword" - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "line": { - "type": "long" - } - } - }, - "top": { - "dynamic": "false", - "properties": { - "filename": { - "ignore_above": 1024, - "type": "keyword" - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "line": { - "type": "long" - } - } - } - } - }, - "registry": { - "properties": { - "data": { - "properties": { - "bytes": { - "ignore_above": 1024, - "type": "keyword" - }, - "strings": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hive": { - "ignore_above": 1024, - "type": "keyword" - }, - "key": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "value": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "related": { - "properties": { - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "rule": { - "properties": { - "author": { - "ignore_above": 1024, - "type": "keyword" - }, - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "license": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "ruleset": { - "ignore_above": 1024, - "type": "keyword" - }, - "uuid": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "server": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "service": { - "dynamic": "false", - "properties": { - "environment": { - "ignore_above": 1024, - "type": "keyword" - }, - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "framework": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "language": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "node": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "state": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "source": { - "dynamic": "false", - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "sourcemap": { - "dynamic": "false", - "properties": { - "bundle_filepath": { - "ignore_above": 1024, - "type": "keyword" - }, - "service": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "span": { - "dynamic": "false", - "properties": { - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "db": { - "dynamic": "false", - "properties": { - "link": { - "ignore_above": 1024, - "type": "keyword" - }, - "rows_affected": { - "type": "long" - } - } - }, - "destination": { - "dynamic": "false", - "properties": { - "service": { - "dynamic": "false", - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "resource": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "duration": { - "properties": { - "us": { - "type": "long" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "message": { - "dynamic": "false", - "properties": { - "age": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "self_time": { - "properties": { - "count": { - "type": "long" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - } - } - }, - "start": { - "properties": { - "us": { - "type": "long" - } - } - }, - "subtype": { - "ignore_above": 1024, - "type": "keyword" - }, - "sync": { - "type": "boolean" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "system": { - "properties": { - "cpu": { - "properties": { - "total": { - "properties": { - "norm": { - "properties": { - "pct": { - "scaling_factor": 1000, - "type": "scaled_float" - } - } - } - } - } - } - }, - "memory": { - "properties": { - "actual": { - "properties": { - "free": { - "type": "long" - } - } - }, - "total": { - "type": "long" - } - } - }, - "process": { - "properties": { - "cpu": { - "properties": { - "total": { - "properties": { - "norm": { - "properties": { - "pct": { - "scaling_factor": 1000, - "type": "scaled_float" - } - } - } - } - } - } - }, - "memory": { - "properties": { - "rss": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "size": { - "type": "long" - } - } - } - } - } - } - }, - "tags": { - "ignore_above": 1024, - "type": "keyword" - }, - "threat": { - "properties": { - "framework": { - "ignore_above": 1024, - "type": "keyword" - }, - "tactic": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "technique": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "timeseries": { - "properties": { - "instance": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "timestamp": { - "properties": { - "us": { - "type": "long" - } - } - }, - "tls": { - "properties": { - "cipher": { - "ignore_above": 1024, - "type": "keyword" - }, - "client": { - "properties": { - "certificate": { - "ignore_above": 1024, - "type": "keyword" - }, - "certificate_chain": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "issuer": { - "ignore_above": 1024, - "type": "keyword" - }, - "ja3": { - "ignore_above": 1024, - "type": "keyword" - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "server_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject": { - "ignore_above": 1024, - "type": "keyword" - }, - "supported_ciphers": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "curve": { - "ignore_above": 1024, - "type": "keyword" - }, - "established": { - "type": "boolean" - }, - "next_protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "resumed": { - "type": "boolean" - }, - "server": { - "properties": { - "certificate": { - "ignore_above": 1024, - "type": "keyword" - }, - "certificate_chain": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "issuer": { - "ignore_above": 1024, - "type": "keyword" - }, - "ja3s": { - "ignore_above": 1024, - "type": "keyword" - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "subject": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - }, - "version_protocol": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "trace": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "tracing": { - "properties": { - "trace": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "transaction": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "transaction": { - "dynamic": "false", - "properties": { - "breakdown": { - "properties": { - "count": { - "type": "long" - } - } - }, - "duration": { - "properties": { - "count": { - "type": "long" - }, - "histogram": { - "type": "histogram" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - }, - "us": { - "type": "long" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "marks": { - "dynamic": "true", - "properties": { - "*": { - "properties": { - "*": { - "dynamic": "true", - "type": "object" - } - } - } - } - }, - "message": { - "dynamic": "false", - "properties": { - "age": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "result": { - "ignore_above": 1024, - "type": "keyword" - }, - "root": { - "type": "boolean" - }, - "sampled": { - "type": "boolean" - }, - "self_time": { - "properties": { - "count": { - "type": "long" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - } - } - }, - "span_count": { - "properties": { - "dropped": { - "type": "long" - } - } - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "url": { - "dynamic": "false", - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "fragment": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "password": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "port": { - "type": "long" - }, - "query": { - "ignore_above": 1024, - "type": "keyword" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "scheme": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "username": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user": { - "dynamic": "false", - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user_agent": { - "dynamic": "false", - "properties": { - "device": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "view spans": { - "ignore_above": 1024, - "type": "keyword" - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vulnerability": { - "properties": { - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "classification": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "enumeration": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "report_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "scanner": { - "properties": { - "vendor": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "score": { - "properties": { - "base": { - "type": "float" - }, - "environmental": { - "type": "float" - }, - "temporal": { - "type": "float" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "severity": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "settings": { - "index": { - "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-error" - }, - "mapping": { - "total_fields": { - "limit": "2000" - } - }, - "number_of_replicas": "0", - "number_of_shards": "1", - "priority": "100", - "query": { - "default_field": [ - "message", - "tags", - "agent.ephemeral_id", - "agent.id", - "agent.name", - "agent.type", - "agent.version", - "as.organization.name", - "client.address", - "client.as.organization.name", - "client.domain", - "client.geo.city_name", - "client.geo.continent_name", - "client.geo.country_iso_code", - "client.geo.country_name", - "client.geo.name", - "client.geo.region_iso_code", - "client.geo.region_name", - "client.mac", - "client.registered_domain", - "client.top_level_domain", - "client.user.domain", - "client.user.email", - "client.user.full_name", - "client.user.group.domain", - "client.user.group.id", - "client.user.group.name", - "client.user.hash", - "client.user.id", - "client.user.name", - "cloud.account.id", - "cloud.availability_zone", - "cloud.instance.id", - "cloud.instance.name", - "cloud.machine.type", - "cloud.provider", - "cloud.region", - "container.id", - "container.image.name", - "container.image.tag", - "container.name", - "container.runtime", - "destination.address", - "destination.as.organization.name", - "destination.domain", - "destination.geo.city_name", - "destination.geo.continent_name", - "destination.geo.country_iso_code", - "destination.geo.country_name", - "destination.geo.name", - "destination.geo.region_iso_code", - "destination.geo.region_name", - "destination.mac", - "destination.registered_domain", - "destination.top_level_domain", - "destination.user.domain", - "destination.user.email", - "destination.user.full_name", - "destination.user.group.domain", - "destination.user.group.id", - "destination.user.group.name", - "destination.user.hash", - "destination.user.id", - "destination.user.name", - "dns.answers.class", - "dns.answers.data", - "dns.answers.name", - "dns.answers.type", - "dns.header_flags", - "dns.id", - "dns.op_code", - "dns.question.class", - "dns.question.name", - "dns.question.registered_domain", - "dns.question.subdomain", - "dns.question.top_level_domain", - "dns.question.type", - "dns.response_code", - "dns.type", - "ecs.version", - "error.code", - "error.id", - "error.message", - "error.stack_trace", - "error.type", - "event.action", - "event.category", - "event.code", - "event.dataset", - "event.hash", - "event.id", - "event.kind", - "event.module", - "event.original", - "event.outcome", - "event.provider", - "event.timezone", - "event.type", - "file.device", - "file.directory", - "file.extension", - "file.gid", - "file.group", - "file.hash.md5", - "file.hash.sha1", - "file.hash.sha256", - "file.hash.sha512", - "file.inode", - "file.mode", - "file.name", - "file.owner", - "file.path", - "file.target_path", - "file.type", - "file.uid", - "geo.city_name", - "geo.continent_name", - "geo.country_iso_code", - "geo.country_name", - "geo.name", - "geo.region_iso_code", - "geo.region_name", - "group.domain", - "group.id", - "group.name", - "hash.md5", - "hash.sha1", - "hash.sha256", - "hash.sha512", - "host.architecture", - "host.geo.city_name", - "host.geo.continent_name", - "host.geo.country_iso_code", - "host.geo.country_name", - "host.geo.name", - "host.geo.region_iso_code", - "host.geo.region_name", - "host.hostname", - "host.id", - "host.mac", - "host.name", - "host.os.family", - "host.os.full", - "host.os.kernel", - "host.os.name", - "host.os.platform", - "host.os.version", - "host.type", - "host.user.domain", - "host.user.email", - "host.user.full_name", - "host.user.group.domain", - "host.user.group.id", - "host.user.group.name", - "host.user.hash", - "host.user.id", - "host.user.name", - "http.request.body.content", - "http.request.method", - "http.request.referrer", - "http.response.body.content", - "http.version", - "log.level", - "log.logger", - "log.origin.file.name", - "log.origin.function", - "log.original", - "log.syslog.facility.name", - "log.syslog.severity.name", - "network.application", - "network.community_id", - "network.direction", - "network.iana_number", - "network.name", - "network.protocol", - "network.transport", - "network.type", - "observer.geo.city_name", - "observer.geo.continent_name", - "observer.geo.country_iso_code", - "observer.geo.country_name", - "observer.geo.name", - "observer.geo.region_iso_code", - "observer.geo.region_name", - "observer.hostname", - "observer.mac", - "observer.name", - "observer.os.family", - "observer.os.full", - "observer.os.kernel", - "observer.os.name", - "observer.os.platform", - "observer.os.version", - "observer.product", - "observer.serial_number", - "observer.type", - "observer.vendor", - "observer.version", - "organization.id", - "organization.name", - "os.family", - "os.full", - "os.kernel", - "os.name", - "os.platform", - "os.version", - "package.architecture", - "package.checksum", - "package.description", - "package.install_scope", - "package.license", - "package.name", - "package.path", - "package.version", - "process.args", - "text", - "process.executable", - "process.hash.md5", - "process.hash.sha1", - "process.hash.sha256", - "process.hash.sha512", - "process.name", - "text", - "text", - "text", - "text", - "text", - "process.thread.name", - "process.title", - "process.working_directory", - "server.address", - "server.as.organization.name", - "server.domain", - "server.geo.city_name", - "server.geo.continent_name", - "server.geo.country_iso_code", - "server.geo.country_name", - "server.geo.name", - "server.geo.region_iso_code", - "server.geo.region_name", - "server.mac", - "server.registered_domain", - "server.top_level_domain", - "server.user.domain", - "server.user.email", - "server.user.full_name", - "server.user.group.domain", - "server.user.group.id", - "server.user.group.name", - "server.user.hash", - "server.user.id", - "server.user.name", - "service.ephemeral_id", - "service.id", - "service.name", - "service.node.name", - "service.state", - "service.type", - "service.version", - "source.address", - "source.as.organization.name", - "source.domain", - "source.geo.city_name", - "source.geo.continent_name", - "source.geo.country_iso_code", - "source.geo.country_name", - "source.geo.name", - "source.geo.region_iso_code", - "source.geo.region_name", - "source.mac", - "source.registered_domain", - "source.top_level_domain", - "source.user.domain", - "source.user.email", - "source.user.full_name", - "source.user.group.domain", - "source.user.group.id", - "source.user.group.name", - "source.user.hash", - "source.user.id", - "source.user.name", - "threat.framework", - "threat.tactic.id", - "threat.tactic.name", - "threat.tactic.reference", - "threat.technique.id", - "threat.technique.name", - "threat.technique.reference", - "tracing.trace.id", - "tracing.transaction.id", - "url.domain", - "url.extension", - "url.fragment", - "url.full", - "url.original", - "url.password", - "url.path", - "url.query", - "url.registered_domain", - "url.scheme", - "url.top_level_domain", - "url.username", - "user.domain", - "user.email", - "user.full_name", - "user.group.domain", - "user.group.id", - "user.group.name", - "user.hash", - "user.id", - "user.name", - "user_agent.device.name", - "user_agent.name", - "text", - "user_agent.original", - "user_agent.os.family", - "user_agent.os.full", - "user_agent.os.kernel", - "user_agent.os.name", - "user_agent.os.platform", - "user_agent.os.version", - "user_agent.version", - "text", - "timeseries.instance", - "cloud.project.id", - "cloud.image.id", - "host.os.build", - "host.os.codename", - "kubernetes.pod.name", - "kubernetes.pod.uid", - "kubernetes.namespace", - "kubernetes.node.name", - "kubernetes.replicaset.name", - "kubernetes.deployment.name", - "kubernetes.statefulset.name", - "kubernetes.container.name", - "kubernetes.container.image", - "processor.name", - "processor.event", - "url.scheme", - "url.full", - "url.domain", - "url.path", - "url.query", - "url.fragment", - "http.version", - "http.request.method", - "http.request.referrer", - "service.name", - "service.version", - "service.environment", - "service.node.name", - "service.language.name", - "service.language.version", - "service.runtime.name", - "service.runtime.version", - "service.framework.name", - "service.framework.version", - "transaction.id", - "transaction.type", - "text", - "transaction.name", - "span.type", - "span.subtype", - "trace.id", - "parent.id", - "agent.name", - "agent.version", - "agent.ephemeral_id", - "container.id", - "kubernetes.namespace", - "kubernetes.node.name", - "kubernetes.pod.name", - "kubernetes.pod.uid", - "host.architecture", - "host.hostname", - "host.name", - "host.os.platform", - "process.args", - "process.title", - "observer.listening", - "observer.hostname", - "observer.version", - "observer.type", - "user.name", - "user.id", - "user.email", - "destination.address", - "text", - "user_agent.original", - "user_agent.name", - "user_agent.version", - "user_agent.device.name", - "user_agent.os.platform", - "user_agent.os.name", - "user_agent.os.full", - "user_agent.os.family", - "user_agent.os.version", - "user_agent.os.kernel", - "cloud.account.id", - "cloud.account.name", - "cloud.availability_zone", - "cloud.instance.id", - "cloud.instance.name", - "cloud.machine.type", - "cloud.project.id", - "cloud.project.name", - "cloud.provider", - "cloud.region", - "error.id", - "error.culprit", - "error.grouping_key", - "error.exception.code", - "error.exception.message", - "error.exception.module", - "error.exception.type", - "error.log.level", - "error.log.logger_name", - "error.log.message", - "error.log.param_message", - "profile.top.id", - "profile.top.function", - "profile.top.filename", - "profile.stack.id", - "profile.stack.function", - "profile.stack.filename", - "sourcemap.service.name", - "sourcemap.service.version", - "sourcemap.bundle_filepath", - "view spans", - "child.id", - "span.id", - "span.name", - "span.action", - "span.db.link", - "span.destination.service.type", - "span.destination.service.name", - "span.destination.service.resource", - "span.message.queue.name", - "transaction.result", - "transaction.message.queue.name", - "fields.*" - ] - }, - "refresh_interval": "1ms" - } - } - } -} - -{ - "type": "index", - "value": { - "aliases": { - "apm-8.0.0-metric": { - "is_write_index": true - } - }, - "index": "apm-8.0.0-metric-000001", - "mappings": { - "_meta": { - "beat": "apm", - "version": "8.0.0" - }, - "date_detection": false, - "dynamic_templates": [ - { - "labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" - } - }, - { - "container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "container.labels.*" - } - }, - { - "dns.answers": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "dns.answers.*" - } - }, - { - "log.syslog": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "log.syslog.*" - } - }, - { - "network.inner": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "network.inner.*" - } - }, - { - "observer.egress": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "observer.egress.*" - } - }, - { - "observer.ingress": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "observer.ingress.*" - } - }, - { - "fields": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "fields.*" - } - }, - { - "docker.container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "docker.container.labels.*" - } - }, - { - "kubernetes.labels.*": { - "mapping": { - "type": "keyword" - }, - "path_match": "kubernetes.labels.*" - } - }, - { - "kubernetes.annotations.*": { - "mapping": { - "type": "keyword" - }, - "path_match": "kubernetes.annotations.*" - } - }, - { - "labels_string": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" - } - }, - { - "labels_boolean": { - "mapping": { - "type": "boolean" - }, - "match_mapping_type": "boolean", - "path_match": "labels.*" - } - }, - { - "labels_*": { - "mapping": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "path_match": "labels.*" - } - }, - { - "transaction.marks": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "transaction.marks.*" - } - }, - { - "transaction.marks.*.*": { - "mapping": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "path_match": "transaction.marks.*.*" - } - }, - { - "strings_as_keyword": { - "mapping": { - "ignore_above": 1024, - "type": "keyword" - }, - "match_mapping_type": "string" - } - } - ], - "properties": { - "@timestamp": { - "type": "date" - }, - "agent": { - "dynamic": "false", - "properties": { - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "hostname": { - "path": "agent.name", - "type": "alias" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "child": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "client": { - "dynamic": "false", - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "account": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "availability_zone": { - "ignore_above": 1024, - "type": "keyword" - }, - "image": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "instance": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "machine": { - "dynamic": "false", - "properties": { - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "project": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "region": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "container": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "image": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "tag": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "type": "object" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "runtime": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "destination": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "dll": { - "properties": { - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "dns": { - "properties": { - "answers": { - "properties": { - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "data": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "ttl": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "header_flags": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "op_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "question": { - "properties": { - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "subdomain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "resolved_ip": { - "type": "ip" - }, - "response_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "docker": { - "properties": { - "container": { - "properties": { - "labels": { - "type": "object" - } - } - } - } - }, - "ecs": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "error": { - "dynamic": "false", - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "culprit": { - "ignore_above": 1024, - "type": "keyword" - }, - "exception": { - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "handled": { - "type": "boolean" - }, - "message": { - "norms": false, - "type": "text" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "grouping_key": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "log": { - "properties": { - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "logger_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "message": { - "norms": false, - "type": "text" - }, - "param_message": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "message": { - "norms": false, - "type": "text" - }, - "stack_trace": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "event": { - "properties": { - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "created": { - "type": "date" - }, - "dataset": { - "ignore_above": 1024, - "type": "keyword" - }, - "duration": { - "type": "long" - }, - "end": { - "type": "date" - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ingested": { - "type": "date" - }, - "kind": { - "ignore_above": 1024, - "type": "keyword" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "outcome": { - "ignore_above": 1024, - "type": "keyword" - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "risk_score": { - "type": "float" - }, - "risk_score_norm": { - "type": "float" - }, - "sequence": { - "type": "long" - }, - "severity": { - "type": "long" - }, - "start": { - "type": "date" - }, - "timezone": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "url": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "experimental": { - "dynamic": "true", - "type": "object" - }, - "fields": { - "type": "object" - }, - "file": { - "properties": { - "accessed": { - "type": "date" - }, - "attributes": { - "ignore_above": 1024, - "type": "keyword" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "created": { - "type": "date" - }, - "ctime": { - "type": "date" - }, - "device": { - "ignore_above": 1024, - "type": "keyword" - }, - "directory": { - "ignore_above": 1024, - "type": "keyword" - }, - "drive_letter": { - "ignore_above": 1, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "inode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mime_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "mode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mtime": { - "type": "date" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "owner": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "size": { - "type": "long" - }, - "target_path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "host": { - "dynamic": "false", - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "containerized": { - "type": "boolean" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "ignore_above": 1024, - "type": "keyword" - }, - "codename": { - "ignore_above": 1024, - "type": "keyword" - }, - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "http": { - "dynamic": "false", - "properties": { - "request": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "headers": { - "enabled": false, - "type": "object" - }, - "method": { - "ignore_above": 1024, - "type": "keyword" - }, - "referrer": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "response": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "finished": { - "type": "boolean" - }, - "headers": { - "enabled": false, - "type": "object" - }, - "status_code": { - "type": "long" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "jvm": { - "properties": { - "gc": { - "properties": { - "alloc": { - "type": "float" - }, - "count": { - "type": "long" - }, - "time": { - "type": "long" - } - } - }, - "memory": { - "properties": { - "heap": { - "properties": { - "committed": { - "type": "float" - }, - "max": { - "type": "float" - }, - "used": { - "type": "float" - } - } - }, - "non_heap": { - "properties": { - "committed": { - "type": "float" - }, - "max": { - "type": "long" - }, - "used": { - "type": "float" - } - } - } - } - }, - "thread": { - "properties": { - "count": { - "type": "long" - } - } - } - } - }, - "kubernetes": { - "dynamic": "false", - "properties": { - "annotations": { - "properties": { - "*": { - "type": "object" - } - } - }, - "container": { - "properties": { - "image": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "deployment": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "properties": { - "*": { - "type": "object" - } - } - }, - "namespace": { - "ignore_above": 1024, - "type": "keyword" - }, - "node": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pod": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "replicaset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "statefulset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "labels": { - "dynamic": "true", - "properties": { - "env": { - "type": "keyword" - }, - "hostname": { - "type": "keyword" - }, - "name": { - "type": "keyword" - } - } - }, - "log": { - "properties": { - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "logger": { - "ignore_above": 1024, - "type": "keyword" - }, - "origin": { - "properties": { - "file": { - "properties": { - "line": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "syslog": { - "properties": { - "facility": { - "properties": { - "code": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "priority": { - "type": "long" - }, - "severity": { - "properties": { - "code": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "message": { - "norms": false, - "type": "text" - }, - "network": { - "properties": { - "application": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "community_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "direction": { - "ignore_above": 1024, - "type": "keyword" - }, - "forwarded_ip": { - "type": "ip" - }, - "iana_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "inner": { - "properties": { - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "transport": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "nodejs": { - "properties": { - "eventloop": { - "properties": { - "delay": { - "properties": { - "avg": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "ns": { - "type": "long" - } - } - } - } - }, - "handles": { - "properties": { - "active": { - "type": "long" - } - } - }, - "memory": { - "properties": { - "arrayBuffers": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "external": { - "properties": { - "bytes": { - "type": "float" - } - } - }, - "heap": { - "properties": { - "allocated": { - "properties": { - "bytes": { - "type": "float" - } - } - }, - "used": { - "properties": { - "bytes": { - "type": "float" - } - } - } - } - } - } - }, - "requests": { - "properties": { - "active": { - "type": "long" - } - } - } - } - }, - "observer": { - "dynamic": "false", - "properties": { - "egress": { - "properties": { - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "zone": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "ingress": { - "properties": { - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "zone": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "listening": { - "ignore_above": 1024, - "type": "keyword" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "vendor": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - }, - "version_major": { - "type": "byte" - } - } - }, - "organization": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "package": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "build_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "checksum": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "install_scope": { - "ignore_above": 1024, - "type": "keyword" - }, - "installed": { - "type": "date" - }, - "license": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "parent": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "process": { - "dynamic": "false", - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "parent": { - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "title": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "title": { - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "processor": { - "properties": { - "event": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "profile": { - "dynamic": "false", - "properties": { - "alloc_objects": { - "properties": { - "count": { - "type": "long" - } - } - }, - "alloc_space": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "cpu": { - "properties": { - "ns": { - "type": "long" - } - } - }, - "duration": { - "type": "long" - }, - "inuse_objects": { - "properties": { - "count": { - "type": "long" - } - } - }, - "inuse_space": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "samples": { - "properties": { - "count": { - "type": "long" - } - } - }, - "stack": { - "dynamic": "false", - "properties": { - "filename": { - "ignore_above": 1024, - "type": "keyword" - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "line": { - "type": "long" - } - } - }, - "top": { - "dynamic": "false", - "properties": { - "filename": { - "ignore_above": 1024, - "type": "keyword" - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "line": { - "type": "long" - } - } - } - } - }, - "registry": { - "properties": { - "data": { - "properties": { - "bytes": { - "ignore_above": 1024, - "type": "keyword" - }, - "strings": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hive": { - "ignore_above": 1024, - "type": "keyword" - }, - "key": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "value": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "related": { - "properties": { - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "rule": { - "properties": { - "author": { - "ignore_above": 1024, - "type": "keyword" - }, - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "license": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "ruleset": { - "ignore_above": 1024, - "type": "keyword" - }, - "uuid": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "server": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "service": { - "dynamic": "false", - "properties": { - "environment": { - "ignore_above": 1024, - "type": "keyword" - }, - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "framework": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "language": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "node": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "state": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "source": { - "dynamic": "false", - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "sourcemap": { - "dynamic": "false", - "properties": { - "bundle_filepath": { - "ignore_above": 1024, - "type": "keyword" - }, - "service": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "span": { - "dynamic": "false", - "properties": { - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "db": { - "dynamic": "false", - "properties": { - "link": { - "ignore_above": 1024, - "type": "keyword" - }, - "rows_affected": { - "type": "long" - } - } - }, - "destination": { - "dynamic": "false", - "properties": { - "service": { - "dynamic": "false", - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "resource": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "duration": { - "properties": { - "us": { - "type": "long" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "message": { - "dynamic": "false", - "properties": { - "age": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "self_time": { - "properties": { - "count": { - "type": "long" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - } - } - }, - "start": { - "properties": { - "us": { - "type": "long" - } - } - }, - "subtype": { - "ignore_above": 1024, - "type": "keyword" - }, - "sync": { - "type": "boolean" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "system": { - "properties": { - "cpu": { - "properties": { - "total": { - "properties": { - "norm": { - "properties": { - "pct": { - "scaling_factor": 1000, - "type": "scaled_float" - } - } - } - } - } - } - }, - "memory": { - "properties": { - "actual": { - "properties": { - "free": { - "type": "long" - } - } - }, - "total": { - "type": "long" - } - } - }, - "process": { - "properties": { - "cpu": { - "properties": { - "system": { - "properties": { - "norm": { - "properties": { - "pct": { - "type": "float" - } - } - } - } - }, - "total": { - "properties": { - "norm": { - "properties": { - "pct": { - "scaling_factor": 1000, - "type": "scaled_float" - } - } - } - } - }, - "user": { - "properties": { - "norm": { - "properties": { - "pct": { - "type": "float" - } - } - } - } - } - } - }, - "memory": { - "properties": { - "rss": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "size": { - "type": "long" - } - } - } - } - } - } - }, - "tags": { - "ignore_above": 1024, - "type": "keyword" - }, - "threat": { - "properties": { - "framework": { - "ignore_above": 1024, - "type": "keyword" - }, - "tactic": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "technique": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "timeseries": { - "properties": { - "instance": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "timestamp": { - "properties": { - "us": { - "type": "long" - } - } - }, - "tls": { - "properties": { - "cipher": { - "ignore_above": 1024, - "type": "keyword" - }, - "client": { - "properties": { - "certificate": { - "ignore_above": 1024, - "type": "keyword" - }, - "certificate_chain": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "issuer": { - "ignore_above": 1024, - "type": "keyword" - }, - "ja3": { - "ignore_above": 1024, - "type": "keyword" - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "server_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject": { - "ignore_above": 1024, - "type": "keyword" - }, - "supported_ciphers": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "curve": { - "ignore_above": 1024, - "type": "keyword" - }, - "established": { - "type": "boolean" - }, - "next_protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "resumed": { - "type": "boolean" - }, - "server": { - "properties": { - "certificate": { - "ignore_above": 1024, - "type": "keyword" - }, - "certificate_chain": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "issuer": { - "ignore_above": 1024, - "type": "keyword" - }, - "ja3s": { - "ignore_above": 1024, - "type": "keyword" - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "subject": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - }, - "version_protocol": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "trace": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "tracing": { - "properties": { - "trace": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "transaction": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "transaction": { - "dynamic": "false", - "properties": { - "breakdown": { - "properties": { - "count": { - "type": "long" - } - } - }, - "duration": { - "properties": { - "count": { - "type": "long" - }, - "histogram": { - "type": "histogram" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - }, - "us": { - "type": "long" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "marks": { - "dynamic": "true", - "properties": { - "*": { - "properties": { - "*": { - "dynamic": "true", - "type": "object" - } - } - } - } - }, - "message": { - "dynamic": "false", - "properties": { - "age": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "result": { - "ignore_above": 1024, - "type": "keyword" - }, - "root": { - "type": "boolean" - }, - "sampled": { - "type": "boolean" - }, - "self_time": { - "properties": { - "count": { - "type": "long" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - } - } - }, - "span_count": { - "properties": { - "dropped": { - "type": "long" - } - } - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "url": { - "dynamic": "false", - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "fragment": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "password": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "port": { - "type": "long" - }, - "query": { - "ignore_above": 1024, - "type": "keyword" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "scheme": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "username": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user": { - "dynamic": "false", - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user_agent": { - "dynamic": "false", - "properties": { - "device": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "view spans": { - "ignore_above": 1024, - "type": "keyword" - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vulnerability": { - "properties": { - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "classification": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "enumeration": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "report_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "scanner": { - "properties": { - "vendor": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "score": { - "properties": { - "base": { - "type": "float" - }, - "environmental": { - "type": "float" - }, - "temporal": { - "type": "float" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "severity": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "settings": { - "index": { - "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-metric" - }, - "mapping": { - "total_fields": { - "limit": "2000" - } - }, - "number_of_replicas": "0", - "number_of_shards": "1", - "priority": "100", - "query": { - "default_field": [ - "message", - "tags", - "agent.ephemeral_id", - "agent.id", - "agent.name", - "agent.type", - "agent.version", - "as.organization.name", - "client.address", - "client.as.organization.name", - "client.domain", - "client.geo.city_name", - "client.geo.continent_name", - "client.geo.country_iso_code", - "client.geo.country_name", - "client.geo.name", - "client.geo.region_iso_code", - "client.geo.region_name", - "client.mac", - "client.registered_domain", - "client.top_level_domain", - "client.user.domain", - "client.user.email", - "client.user.full_name", - "client.user.group.domain", - "client.user.group.id", - "client.user.group.name", - "client.user.hash", - "client.user.id", - "client.user.name", - "cloud.account.id", - "cloud.availability_zone", - "cloud.instance.id", - "cloud.instance.name", - "cloud.machine.type", - "cloud.provider", - "cloud.region", - "container.id", - "container.image.name", - "container.image.tag", - "container.name", - "container.runtime", - "destination.address", - "destination.as.organization.name", - "destination.domain", - "destination.geo.city_name", - "destination.geo.continent_name", - "destination.geo.country_iso_code", - "destination.geo.country_name", - "destination.geo.name", - "destination.geo.region_iso_code", - "destination.geo.region_name", - "destination.mac", - "destination.registered_domain", - "destination.top_level_domain", - "destination.user.domain", - "destination.user.email", - "destination.user.full_name", - "destination.user.group.domain", - "destination.user.group.id", - "destination.user.group.name", - "destination.user.hash", - "destination.user.id", - "destination.user.name", - "dns.answers.class", - "dns.answers.data", - "dns.answers.name", - "dns.answers.type", - "dns.header_flags", - "dns.id", - "dns.op_code", - "dns.question.class", - "dns.question.name", - "dns.question.registered_domain", - "dns.question.subdomain", - "dns.question.top_level_domain", - "dns.question.type", - "dns.response_code", - "dns.type", - "ecs.version", - "error.code", - "error.id", - "error.message", - "error.stack_trace", - "error.type", - "event.action", - "event.category", - "event.code", - "event.dataset", - "event.hash", - "event.id", - "event.kind", - "event.module", - "event.original", - "event.outcome", - "event.provider", - "event.timezone", - "event.type", - "file.device", - "file.directory", - "file.extension", - "file.gid", - "file.group", - "file.hash.md5", - "file.hash.sha1", - "file.hash.sha256", - "file.hash.sha512", - "file.inode", - "file.mode", - "file.name", - "file.owner", - "file.path", - "file.target_path", - "file.type", - "file.uid", - "geo.city_name", - "geo.continent_name", - "geo.country_iso_code", - "geo.country_name", - "geo.name", - "geo.region_iso_code", - "geo.region_name", - "group.domain", - "group.id", - "group.name", - "hash.md5", - "hash.sha1", - "hash.sha256", - "hash.sha512", - "host.architecture", - "host.geo.city_name", - "host.geo.continent_name", - "host.geo.country_iso_code", - "host.geo.country_name", - "host.geo.name", - "host.geo.region_iso_code", - "host.geo.region_name", - "host.hostname", - "host.id", - "host.mac", - "host.name", - "host.os.family", - "host.os.full", - "host.os.kernel", - "host.os.name", - "host.os.platform", - "host.os.version", - "host.type", - "host.user.domain", - "host.user.email", - "host.user.full_name", - "host.user.group.domain", - "host.user.group.id", - "host.user.group.name", - "host.user.hash", - "host.user.id", - "host.user.name", - "http.request.body.content", - "http.request.method", - "http.request.referrer", - "http.response.body.content", - "http.version", - "log.level", - "log.logger", - "log.origin.file.name", - "log.origin.function", - "log.original", - "log.syslog.facility.name", - "log.syslog.severity.name", - "network.application", - "network.community_id", - "network.direction", - "network.iana_number", - "network.name", - "network.protocol", - "network.transport", - "network.type", - "observer.geo.city_name", - "observer.geo.continent_name", - "observer.geo.country_iso_code", - "observer.geo.country_name", - "observer.geo.name", - "observer.geo.region_iso_code", - "observer.geo.region_name", - "observer.hostname", - "observer.mac", - "observer.name", - "observer.os.family", - "observer.os.full", - "observer.os.kernel", - "observer.os.name", - "observer.os.platform", - "observer.os.version", - "observer.product", - "observer.serial_number", - "observer.type", - "observer.vendor", - "observer.version", - "organization.id", - "organization.name", - "os.family", - "os.full", - "os.kernel", - "os.name", - "os.platform", - "os.version", - "package.architecture", - "package.checksum", - "package.description", - "package.install_scope", - "package.license", - "package.name", - "package.path", - "package.version", - "process.args", - "text", - "process.executable", - "process.hash.md5", - "process.hash.sha1", - "process.hash.sha256", - "process.hash.sha512", - "process.name", - "text", - "text", - "text", - "text", - "text", - "process.thread.name", - "process.title", - "process.working_directory", - "server.address", - "server.as.organization.name", - "server.domain", - "server.geo.city_name", - "server.geo.continent_name", - "server.geo.country_iso_code", - "server.geo.country_name", - "server.geo.name", - "server.geo.region_iso_code", - "server.geo.region_name", - "server.mac", - "server.registered_domain", - "server.top_level_domain", - "server.user.domain", - "server.user.email", - "server.user.full_name", - "server.user.group.domain", - "server.user.group.id", - "server.user.group.name", - "server.user.hash", - "server.user.id", - "server.user.name", - "service.ephemeral_id", - "service.id", - "service.name", - "service.node.name", - "service.state", - "service.type", - "service.version", - "source.address", - "source.as.organization.name", - "source.domain", - "source.geo.city_name", - "source.geo.continent_name", - "source.geo.country_iso_code", - "source.geo.country_name", - "source.geo.name", - "source.geo.region_iso_code", - "source.geo.region_name", - "source.mac", - "source.registered_domain", - "source.top_level_domain", - "source.user.domain", - "source.user.email", - "source.user.full_name", - "source.user.group.domain", - "source.user.group.id", - "source.user.group.name", - "source.user.hash", - "source.user.id", - "source.user.name", - "threat.framework", - "threat.tactic.id", - "threat.tactic.name", - "threat.tactic.reference", - "threat.technique.id", - "threat.technique.name", - "threat.technique.reference", - "tracing.trace.id", - "tracing.transaction.id", - "url.domain", - "url.extension", - "url.fragment", - "url.full", - "url.original", - "url.password", - "url.path", - "url.query", - "url.registered_domain", - "url.scheme", - "url.top_level_domain", - "url.username", - "user.domain", - "user.email", - "user.full_name", - "user.group.domain", - "user.group.id", - "user.group.name", - "user.hash", - "user.id", - "user.name", - "user_agent.device.name", - "user_agent.name", - "text", - "user_agent.original", - "user_agent.os.family", - "user_agent.os.full", - "user_agent.os.kernel", - "user_agent.os.name", - "user_agent.os.platform", - "user_agent.os.version", - "user_agent.version", - "text", - "timeseries.instance", - "cloud.project.id", - "cloud.image.id", - "host.os.build", - "host.os.codename", - "kubernetes.pod.name", - "kubernetes.pod.uid", - "kubernetes.namespace", - "kubernetes.node.name", - "kubernetes.replicaset.name", - "kubernetes.deployment.name", - "kubernetes.statefulset.name", - "kubernetes.container.name", - "kubernetes.container.image", - "processor.name", - "processor.event", - "url.scheme", - "url.full", - "url.domain", - "url.path", - "url.query", - "url.fragment", - "http.version", - "http.request.method", - "http.request.referrer", - "service.name", - "service.version", - "service.environment", - "service.node.name", - "service.language.name", - "service.language.version", - "service.runtime.name", - "service.runtime.version", - "service.framework.name", - "service.framework.version", - "transaction.id", - "transaction.type", - "text", - "transaction.name", - "span.type", - "span.subtype", - "trace.id", - "parent.id", - "agent.name", - "agent.version", - "agent.ephemeral_id", - "container.id", - "kubernetes.namespace", - "kubernetes.node.name", - "kubernetes.pod.name", - "kubernetes.pod.uid", - "host.architecture", - "host.hostname", - "host.name", - "host.os.platform", - "process.args", - "process.title", - "observer.listening", - "observer.hostname", - "observer.version", - "observer.type", - "user.name", - "user.id", - "user.email", - "destination.address", - "text", - "user_agent.original", - "user_agent.name", - "user_agent.version", - "user_agent.device.name", - "user_agent.os.platform", - "user_agent.os.name", - "user_agent.os.full", - "user_agent.os.family", - "user_agent.os.version", - "user_agent.os.kernel", - "cloud.account.id", - "cloud.account.name", - "cloud.availability_zone", - "cloud.instance.id", - "cloud.instance.name", - "cloud.machine.type", - "cloud.project.id", - "cloud.project.name", - "cloud.provider", - "cloud.region", - "error.id", - "error.culprit", - "error.grouping_key", - "error.exception.code", - "error.exception.message", - "error.exception.module", - "error.exception.type", - "error.log.level", - "error.log.logger_name", - "error.log.message", - "error.log.param_message", - "profile.top.id", - "profile.top.function", - "profile.top.filename", - "profile.stack.id", - "profile.stack.function", - "profile.stack.filename", - "sourcemap.service.name", - "sourcemap.service.version", - "sourcemap.bundle_filepath", - "view spans", - "child.id", - "span.id", - "span.name", - "span.action", - "span.db.link", - "span.destination.service.type", - "span.destination.service.name", - "span.destination.service.resource", - "span.message.queue.name", - "transaction.result", - "transaction.message.queue.name", - "fields.*" - ] - }, - "refresh_interval": "1ms" - } - } - } -} - -{ - "type": "index", - "value": { - "aliases": { - }, - "index": "apm-8.0.0-onboarding-2020.06.29", - "mappings": { - "_meta": { - "beat": "apm", - "version": "8.0.0" - }, - "date_detection": false, - "dynamic_templates": [ - { - "labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" - } - }, - { - "container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "container.labels.*" - } - }, - { - "dns.answers": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "dns.answers.*" - } - }, - { - "log.syslog": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "log.syslog.*" - } - }, - { - "network.inner": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "network.inner.*" - } - }, - { - "observer.egress": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "observer.egress.*" - } - }, - { - "observer.ingress": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "observer.ingress.*" - } - }, - { - "fields": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "fields.*" - } - }, - { - "docker.container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "docker.container.labels.*" - } - }, - { - "kubernetes.labels.*": { - "mapping": { - "type": "keyword" - }, - "path_match": "kubernetes.labels.*" - } - }, - { - "kubernetes.annotations.*": { - "mapping": { - "type": "keyword" - }, - "path_match": "kubernetes.annotations.*" - } - }, - { - "labels_string": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" - } - }, - { - "labels_boolean": { - "mapping": { - "type": "boolean" - }, - "match_mapping_type": "boolean", - "path_match": "labels.*" - } - }, - { - "labels_*": { - "mapping": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "path_match": "labels.*" - } - }, - { - "transaction.marks": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "transaction.marks.*" - } - }, - { - "transaction.marks.*.*": { - "mapping": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "path_match": "transaction.marks.*.*" - } - }, - { - "strings_as_keyword": { - "mapping": { - "ignore_above": 1024, - "type": "keyword" - }, - "match_mapping_type": "string" - } - } - ], - "properties": { - "@timestamp": { - "type": "date" - }, - "agent": { - "dynamic": "false", - "properties": { - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "hostname": { - "path": "agent.name", - "type": "alias" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "child": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "client": { - "dynamic": "false", - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "account": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "availability_zone": { - "ignore_above": 1024, - "type": "keyword" - }, - "image": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "instance": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "machine": { - "dynamic": "false", - "properties": { - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "project": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "region": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "container": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "image": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "tag": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "type": "object" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "runtime": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "destination": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "dll": { - "properties": { - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "dns": { - "properties": { - "answers": { - "properties": { - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "data": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "ttl": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "header_flags": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "op_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "question": { - "properties": { - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "subdomain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "resolved_ip": { - "type": "ip" - }, - "response_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "docker": { - "properties": { - "container": { - "properties": { - "labels": { - "type": "object" - } - } - } - } - }, - "ecs": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "error": { - "dynamic": "false", - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "culprit": { - "ignore_above": 1024, - "type": "keyword" - }, - "exception": { - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "handled": { - "type": "boolean" - }, - "message": { - "norms": false, - "type": "text" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "grouping_key": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "log": { - "properties": { - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "logger_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "message": { - "norms": false, - "type": "text" - }, - "param_message": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "message": { - "norms": false, - "type": "text" - }, - "stack_trace": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "event": { - "properties": { - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "created": { - "type": "date" - }, - "dataset": { - "ignore_above": 1024, - "type": "keyword" - }, - "duration": { - "type": "long" - }, - "end": { - "type": "date" - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ingested": { - "type": "date" - }, - "kind": { - "ignore_above": 1024, - "type": "keyword" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "outcome": { - "ignore_above": 1024, - "type": "keyword" - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "risk_score": { - "type": "float" - }, - "risk_score_norm": { - "type": "float" - }, - "sequence": { - "type": "long" - }, - "severity": { - "type": "long" - }, - "start": { - "type": "date" - }, - "timezone": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "url": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "experimental": { - "dynamic": "true", - "type": "object" - }, - "fields": { - "type": "object" - }, - "file": { - "properties": { - "accessed": { - "type": "date" - }, - "attributes": { - "ignore_above": 1024, - "type": "keyword" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "created": { - "type": "date" - }, - "ctime": { - "type": "date" - }, - "device": { - "ignore_above": 1024, - "type": "keyword" - }, - "directory": { - "ignore_above": 1024, - "type": "keyword" - }, - "drive_letter": { - "ignore_above": 1, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "inode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mime_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "mode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mtime": { - "type": "date" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "owner": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "size": { - "type": "long" - }, - "target_path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "host": { - "dynamic": "false", - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "containerized": { - "type": "boolean" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "ignore_above": 1024, - "type": "keyword" - }, - "codename": { - "ignore_above": 1024, - "type": "keyword" - }, - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "http": { - "dynamic": "false", - "properties": { - "request": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "headers": { - "enabled": false, - "type": "object" - }, - "method": { - "ignore_above": 1024, - "type": "keyword" - }, - "referrer": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "response": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "finished": { - "type": "boolean" - }, - "headers": { - "enabled": false, - "type": "object" - }, - "status_code": { - "type": "long" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "kubernetes": { - "dynamic": "false", - "properties": { - "annotations": { - "properties": { - "*": { - "type": "object" - } - } - }, - "container": { - "properties": { - "image": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "deployment": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "properties": { - "*": { - "type": "object" - } - } - }, - "namespace": { - "ignore_above": 1024, - "type": "keyword" - }, - "node": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pod": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "replicaset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "statefulset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "labels": { - "dynamic": "true", - "type": "object" - }, - "log": { - "properties": { - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "logger": { - "ignore_above": 1024, - "type": "keyword" - }, - "origin": { - "properties": { - "file": { - "properties": { - "line": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "syslog": { - "properties": { - "facility": { - "properties": { - "code": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "priority": { - "type": "long" - }, - "severity": { - "properties": { - "code": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "message": { - "norms": false, - "type": "text" - }, - "network": { - "properties": { - "application": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "community_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "direction": { - "ignore_above": 1024, - "type": "keyword" - }, - "forwarded_ip": { - "type": "ip" - }, - "iana_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "inner": { - "properties": { - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "transport": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "observer": { - "dynamic": "false", - "properties": { - "egress": { - "properties": { - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "zone": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "ingress": { - "properties": { - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "zone": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "listening": { - "ignore_above": 1024, - "type": "keyword" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "vendor": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - }, - "version_major": { - "type": "byte" - } - } - }, - "organization": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "package": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "build_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "checksum": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "install_scope": { - "ignore_above": 1024, - "type": "keyword" - }, - "installed": { - "type": "date" - }, - "license": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "parent": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "process": { - "dynamic": "false", - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "parent": { - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "title": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "title": { - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "processor": { - "properties": { - "event": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "profile": { - "dynamic": "false", - "properties": { - "alloc_objects": { - "properties": { - "count": { - "type": "long" - } - } - }, - "alloc_space": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "cpu": { - "properties": { - "ns": { - "type": "long" - } - } - }, - "duration": { - "type": "long" - }, - "inuse_objects": { - "properties": { - "count": { - "type": "long" - } - } - }, - "inuse_space": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "samples": { - "properties": { - "count": { - "type": "long" - } - } - }, - "stack": { - "dynamic": "false", - "properties": { - "filename": { - "ignore_above": 1024, - "type": "keyword" - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "line": { - "type": "long" - } - } - }, - "top": { - "dynamic": "false", - "properties": { - "filename": { - "ignore_above": 1024, - "type": "keyword" - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "line": { - "type": "long" - } - } - } - } - }, - "registry": { - "properties": { - "data": { - "properties": { - "bytes": { - "ignore_above": 1024, - "type": "keyword" - }, - "strings": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hive": { - "ignore_above": 1024, - "type": "keyword" - }, - "key": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "value": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "related": { - "properties": { - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "rule": { - "properties": { - "author": { - "ignore_above": 1024, - "type": "keyword" - }, - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "license": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "ruleset": { - "ignore_above": 1024, - "type": "keyword" - }, - "uuid": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "server": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "service": { - "dynamic": "false", - "properties": { - "environment": { - "ignore_above": 1024, - "type": "keyword" - }, - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "framework": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "language": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "node": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "state": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "source": { - "dynamic": "false", - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "sourcemap": { - "dynamic": "false", - "properties": { - "bundle_filepath": { - "ignore_above": 1024, - "type": "keyword" - }, - "service": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "span": { - "dynamic": "false", - "properties": { - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "db": { - "dynamic": "false", - "properties": { - "link": { - "ignore_above": 1024, - "type": "keyword" - }, - "rows_affected": { - "type": "long" - } - } - }, - "destination": { - "dynamic": "false", - "properties": { - "service": { - "dynamic": "false", - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "resource": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "duration": { - "properties": { - "us": { - "type": "long" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "message": { - "dynamic": "false", - "properties": { - "age": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "self_time": { - "properties": { - "count": { - "type": "long" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - } - } - }, - "start": { - "properties": { - "us": { - "type": "long" - } - } - }, - "subtype": { - "ignore_above": 1024, - "type": "keyword" - }, - "sync": { - "type": "boolean" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "system": { - "properties": { - "cpu": { - "properties": { - "total": { - "properties": { - "norm": { - "properties": { - "pct": { - "scaling_factor": 1000, - "type": "scaled_float" - } - } - } - } - } - } - }, - "memory": { - "properties": { - "actual": { - "properties": { - "free": { - "type": "long" - } - } - }, - "total": { - "type": "long" - } - } - }, - "process": { - "properties": { - "cpu": { - "properties": { - "total": { - "properties": { - "norm": { - "properties": { - "pct": { - "scaling_factor": 1000, - "type": "scaled_float" - } - } - } - } - } - } - }, - "memory": { - "properties": { - "rss": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "size": { - "type": "long" - } - } - } - } - } - } - }, - "tags": { - "ignore_above": 1024, - "type": "keyword" - }, - "threat": { - "properties": { - "framework": { - "ignore_above": 1024, - "type": "keyword" - }, - "tactic": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "technique": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "timeseries": { - "properties": { - "instance": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "timestamp": { - "properties": { - "us": { - "type": "long" - } - } - }, - "tls": { - "properties": { - "cipher": { - "ignore_above": 1024, - "type": "keyword" - }, - "client": { - "properties": { - "certificate": { - "ignore_above": 1024, - "type": "keyword" - }, - "certificate_chain": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "issuer": { - "ignore_above": 1024, - "type": "keyword" - }, - "ja3": { - "ignore_above": 1024, - "type": "keyword" - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "server_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject": { - "ignore_above": 1024, - "type": "keyword" - }, - "supported_ciphers": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "curve": { - "ignore_above": 1024, - "type": "keyword" - }, - "established": { - "type": "boolean" - }, - "next_protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "resumed": { - "type": "boolean" - }, - "server": { - "properties": { - "certificate": { - "ignore_above": 1024, - "type": "keyword" - }, - "certificate_chain": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "issuer": { - "ignore_above": 1024, - "type": "keyword" - }, - "ja3s": { - "ignore_above": 1024, - "type": "keyword" - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "subject": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - }, - "version_protocol": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "trace": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "tracing": { - "properties": { - "trace": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "transaction": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "transaction": { - "dynamic": "false", - "properties": { - "breakdown": { - "properties": { - "count": { - "type": "long" - } - } - }, - "duration": { - "properties": { - "count": { - "type": "long" - }, - "histogram": { - "type": "histogram" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - }, - "us": { - "type": "long" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "marks": { - "dynamic": "true", - "properties": { - "*": { - "properties": { - "*": { - "dynamic": "true", - "type": "object" - } - } - } - } - }, - "message": { - "dynamic": "false", - "properties": { - "age": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "result": { - "ignore_above": 1024, - "type": "keyword" - }, - "root": { - "type": "boolean" - }, - "sampled": { - "type": "boolean" - }, - "self_time": { - "properties": { - "count": { - "type": "long" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - } - } - }, - "span_count": { - "properties": { - "dropped": { - "type": "long" - } - } - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "url": { - "dynamic": "false", - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "fragment": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "password": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "port": { - "type": "long" - }, - "query": { - "ignore_above": 1024, - "type": "keyword" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "scheme": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "username": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user": { - "dynamic": "false", - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user_agent": { - "dynamic": "false", - "properties": { - "device": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "view spans": { - "ignore_above": 1024, - "type": "keyword" - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vulnerability": { - "properties": { - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "classification": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "enumeration": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "report_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "scanner": { - "properties": { - "vendor": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "score": { - "properties": { - "base": { - "type": "float" - }, - "environmental": { - "type": "float" - }, - "temporal": { - "type": "float" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "severity": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "settings": { - "index": { - "codec": "best_compression", - "mapping": { - "total_fields": { - "limit": "2000" - } - }, - "number_of_replicas": "0", - "number_of_shards": "1", - "query": { - "default_field": [ - "message", - "tags", - "agent.ephemeral_id", - "agent.id", - "agent.name", - "agent.type", - "agent.version", - "as.organization.name", - "client.address", - "client.as.organization.name", - "client.domain", - "client.geo.city_name", - "client.geo.continent_name", - "client.geo.country_iso_code", - "client.geo.country_name", - "client.geo.name", - "client.geo.region_iso_code", - "client.geo.region_name", - "client.mac", - "client.registered_domain", - "client.top_level_domain", - "client.user.domain", - "client.user.email", - "client.user.full_name", - "client.user.group.domain", - "client.user.group.id", - "client.user.group.name", - "client.user.hash", - "client.user.id", - "client.user.name", - "cloud.account.id", - "cloud.availability_zone", - "cloud.instance.id", - "cloud.instance.name", - "cloud.machine.type", - "cloud.provider", - "cloud.region", - "container.id", - "container.image.name", - "container.image.tag", - "container.name", - "container.runtime", - "destination.address", - "destination.as.organization.name", - "destination.domain", - "destination.geo.city_name", - "destination.geo.continent_name", - "destination.geo.country_iso_code", - "destination.geo.country_name", - "destination.geo.name", - "destination.geo.region_iso_code", - "destination.geo.region_name", - "destination.mac", - "destination.registered_domain", - "destination.top_level_domain", - "destination.user.domain", - "destination.user.email", - "destination.user.full_name", - "destination.user.group.domain", - "destination.user.group.id", - "destination.user.group.name", - "destination.user.hash", - "destination.user.id", - "destination.user.name", - "dns.answers.class", - "dns.answers.data", - "dns.answers.name", - "dns.answers.type", - "dns.header_flags", - "dns.id", - "dns.op_code", - "dns.question.class", - "dns.question.name", - "dns.question.registered_domain", - "dns.question.subdomain", - "dns.question.top_level_domain", - "dns.question.type", - "dns.response_code", - "dns.type", - "ecs.version", - "error.code", - "error.id", - "error.message", - "error.stack_trace", - "error.type", - "event.action", - "event.category", - "event.code", - "event.dataset", - "event.hash", - "event.id", - "event.kind", - "event.module", - "event.original", - "event.outcome", - "event.provider", - "event.timezone", - "event.type", - "file.device", - "file.directory", - "file.extension", - "file.gid", - "file.group", - "file.hash.md5", - "file.hash.sha1", - "file.hash.sha256", - "file.hash.sha512", - "file.inode", - "file.mode", - "file.name", - "file.owner", - "file.path", - "file.target_path", - "file.type", - "file.uid", - "geo.city_name", - "geo.continent_name", - "geo.country_iso_code", - "geo.country_name", - "geo.name", - "geo.region_iso_code", - "geo.region_name", - "group.domain", - "group.id", - "group.name", - "hash.md5", - "hash.sha1", - "hash.sha256", - "hash.sha512", - "host.architecture", - "host.geo.city_name", - "host.geo.continent_name", - "host.geo.country_iso_code", - "host.geo.country_name", - "host.geo.name", - "host.geo.region_iso_code", - "host.geo.region_name", - "host.hostname", - "host.id", - "host.mac", - "host.name", - "host.os.family", - "host.os.full", - "host.os.kernel", - "host.os.name", - "host.os.platform", - "host.os.version", - "host.type", - "host.user.domain", - "host.user.email", - "host.user.full_name", - "host.user.group.domain", - "host.user.group.id", - "host.user.group.name", - "host.user.hash", - "host.user.id", - "host.user.name", - "http.request.body.content", - "http.request.method", - "http.request.referrer", - "http.response.body.content", - "http.version", - "log.level", - "log.logger", - "log.origin.file.name", - "log.origin.function", - "log.original", - "log.syslog.facility.name", - "log.syslog.severity.name", - "network.application", - "network.community_id", - "network.direction", - "network.iana_number", - "network.name", - "network.protocol", - "network.transport", - "network.type", - "observer.geo.city_name", - "observer.geo.continent_name", - "observer.geo.country_iso_code", - "observer.geo.country_name", - "observer.geo.name", - "observer.geo.region_iso_code", - "observer.geo.region_name", - "observer.hostname", - "observer.mac", - "observer.name", - "observer.os.family", - "observer.os.full", - "observer.os.kernel", - "observer.os.name", - "observer.os.platform", - "observer.os.version", - "observer.product", - "observer.serial_number", - "observer.type", - "observer.vendor", - "observer.version", - "organization.id", - "organization.name", - "os.family", - "os.full", - "os.kernel", - "os.name", - "os.platform", - "os.version", - "package.architecture", - "package.checksum", - "package.description", - "package.install_scope", - "package.license", - "package.name", - "package.path", - "package.version", - "process.args", - "text", - "process.executable", - "process.hash.md5", - "process.hash.sha1", - "process.hash.sha256", - "process.hash.sha512", - "process.name", - "text", - "text", - "text", - "text", - "text", - "process.thread.name", - "process.title", - "process.working_directory", - "server.address", - "server.as.organization.name", - "server.domain", - "server.geo.city_name", - "server.geo.continent_name", - "server.geo.country_iso_code", - "server.geo.country_name", - "server.geo.name", - "server.geo.region_iso_code", - "server.geo.region_name", - "server.mac", - "server.registered_domain", - "server.top_level_domain", - "server.user.domain", - "server.user.email", - "server.user.full_name", - "server.user.group.domain", - "server.user.group.id", - "server.user.group.name", - "server.user.hash", - "server.user.id", - "server.user.name", - "service.ephemeral_id", - "service.id", - "service.name", - "service.node.name", - "service.state", - "service.type", - "service.version", - "source.address", - "source.as.organization.name", - "source.domain", - "source.geo.city_name", - "source.geo.continent_name", - "source.geo.country_iso_code", - "source.geo.country_name", - "source.geo.name", - "source.geo.region_iso_code", - "source.geo.region_name", - "source.mac", - "source.registered_domain", - "source.top_level_domain", - "source.user.domain", - "source.user.email", - "source.user.full_name", - "source.user.group.domain", - "source.user.group.id", - "source.user.group.name", - "source.user.hash", - "source.user.id", - "source.user.name", - "threat.framework", - "threat.tactic.id", - "threat.tactic.name", - "threat.tactic.reference", - "threat.technique.id", - "threat.technique.name", - "threat.technique.reference", - "tracing.trace.id", - "tracing.transaction.id", - "url.domain", - "url.extension", - "url.fragment", - "url.full", - "url.original", - "url.password", - "url.path", - "url.query", - "url.registered_domain", - "url.scheme", - "url.top_level_domain", - "url.username", - "user.domain", - "user.email", - "user.full_name", - "user.group.domain", - "user.group.id", - "user.group.name", - "user.hash", - "user.id", - "user.name", - "user_agent.device.name", - "user_agent.name", - "text", - "user_agent.original", - "user_agent.os.family", - "user_agent.os.full", - "user_agent.os.kernel", - "user_agent.os.name", - "user_agent.os.platform", - "user_agent.os.version", - "user_agent.version", - "text", - "timeseries.instance", - "cloud.project.id", - "cloud.image.id", - "host.os.build", - "host.os.codename", - "kubernetes.pod.name", - "kubernetes.pod.uid", - "kubernetes.namespace", - "kubernetes.node.name", - "kubernetes.replicaset.name", - "kubernetes.deployment.name", - "kubernetes.statefulset.name", - "kubernetes.container.name", - "kubernetes.container.image", - "processor.name", - "processor.event", - "url.scheme", - "url.full", - "url.domain", - "url.path", - "url.query", - "url.fragment", - "http.version", - "http.request.method", - "http.request.referrer", - "service.name", - "service.version", - "service.environment", - "service.node.name", - "service.language.name", - "service.language.version", - "service.runtime.name", - "service.runtime.version", - "service.framework.name", - "service.framework.version", - "transaction.id", - "transaction.type", - "text", - "transaction.name", - "span.type", - "span.subtype", - "trace.id", - "parent.id", - "agent.name", - "agent.version", - "agent.ephemeral_id", - "container.id", - "kubernetes.namespace", - "kubernetes.node.name", - "kubernetes.pod.name", - "kubernetes.pod.uid", - "host.architecture", - "host.hostname", - "host.name", - "host.os.platform", - "process.args", - "process.title", - "observer.listening", - "observer.hostname", - "observer.version", - "observer.type", - "user.name", - "user.id", - "user.email", - "destination.address", - "text", - "user_agent.original", - "user_agent.name", - "user_agent.version", - "user_agent.device.name", - "user_agent.os.platform", - "user_agent.os.name", - "user_agent.os.full", - "user_agent.os.family", - "user_agent.os.version", - "user_agent.os.kernel", - "cloud.account.id", - "cloud.account.name", - "cloud.availability_zone", - "cloud.instance.id", - "cloud.instance.name", - "cloud.machine.type", - "cloud.project.id", - "cloud.project.name", - "cloud.provider", - "cloud.region", - "error.id", - "error.culprit", - "error.grouping_key", - "error.exception.code", - "error.exception.message", - "error.exception.module", - "error.exception.type", - "error.log.level", - "error.log.logger_name", - "error.log.message", - "error.log.param_message", - "profile.top.id", - "profile.top.function", - "profile.top.filename", - "profile.stack.id", - "profile.stack.function", - "profile.stack.filename", - "sourcemap.service.name", - "sourcemap.service.version", - "sourcemap.bundle_filepath", - "view spans", - "child.id", - "span.id", - "span.name", - "span.action", - "span.db.link", - "span.destination.service.type", - "span.destination.service.name", - "span.destination.service.resource", - "span.message.queue.name", - "transaction.result", - "transaction.message.queue.name", - "fields.*" - ] - }, - "refresh_interval": "1ms" - } - } - } -} - -{ - "type": "index", - "value": { - "aliases": { - "apm-8.0.0-profile": { - "is_write_index": true - } - }, - "index": "apm-8.0.0-profile-000001", - "mappings": { - "_meta": { - "beat": "apm", - "version": "8.0.0" - }, - "date_detection": false, - "dynamic_templates": [ - { - "labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" - } - }, - { - "container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "container.labels.*" - } - }, - { - "dns.answers": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "dns.answers.*" - } - }, - { - "log.syslog": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "log.syslog.*" - } - }, - { - "network.inner": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "network.inner.*" - } - }, - { - "observer.egress": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "observer.egress.*" - } - }, - { - "observer.ingress": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "observer.ingress.*" - } - }, - { - "fields": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "fields.*" - } - }, - { - "docker.container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "docker.container.labels.*" - } - }, - { - "kubernetes.labels.*": { - "mapping": { - "type": "keyword" - }, - "path_match": "kubernetes.labels.*" - } - }, - { - "kubernetes.annotations.*": { - "mapping": { - "type": "keyword" - }, - "path_match": "kubernetes.annotations.*" - } - }, - { - "labels_string": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" - } - }, - { - "labels_boolean": { - "mapping": { - "type": "boolean" - }, - "match_mapping_type": "boolean", - "path_match": "labels.*" - } - }, - { - "labels_*": { - "mapping": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "path_match": "labels.*" - } - }, - { - "transaction.marks": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "transaction.marks.*" - } - }, - { - "transaction.marks.*.*": { - "mapping": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "path_match": "transaction.marks.*.*" - } - }, - { - "strings_as_keyword": { - "mapping": { - "ignore_above": 1024, - "type": "keyword" - }, - "match_mapping_type": "string" - } - } - ], - "properties": { - "@timestamp": { - "type": "date" - }, - "agent": { - "dynamic": "false", - "properties": { - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "hostname": { - "path": "agent.name", - "type": "alias" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "child": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "client": { - "dynamic": "false", - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "account": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "availability_zone": { - "ignore_above": 1024, - "type": "keyword" - }, - "image": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "instance": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "machine": { - "dynamic": "false", - "properties": { - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "project": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "region": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "container": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "image": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "tag": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "type": "object" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "runtime": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "destination": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "dll": { - "properties": { - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "dns": { - "properties": { - "answers": { - "properties": { - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "data": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "ttl": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "header_flags": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "op_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "question": { - "properties": { - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "subdomain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "resolved_ip": { - "type": "ip" - }, - "response_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "docker": { - "properties": { - "container": { - "properties": { - "labels": { - "type": "object" - } - } - } - } - }, - "ecs": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "error": { - "dynamic": "false", - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "culprit": { - "ignore_above": 1024, - "type": "keyword" - }, - "exception": { - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "handled": { - "type": "boolean" - }, - "message": { - "norms": false, - "type": "text" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "grouping_key": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "log": { - "properties": { - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "logger_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "message": { - "norms": false, - "type": "text" - }, - "param_message": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "message": { - "norms": false, - "type": "text" - }, - "stack_trace": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "event": { - "properties": { - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "created": { - "type": "date" - }, - "dataset": { - "ignore_above": 1024, - "type": "keyword" - }, - "duration": { - "type": "long" - }, - "end": { - "type": "date" - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ingested": { - "type": "date" - }, - "kind": { - "ignore_above": 1024, - "type": "keyword" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "outcome": { - "ignore_above": 1024, - "type": "keyword" - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "risk_score": { - "type": "float" - }, - "risk_score_norm": { - "type": "float" - }, - "sequence": { - "type": "long" - }, - "severity": { - "type": "long" - }, - "start": { - "type": "date" - }, - "timezone": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "url": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "experimental": { - "dynamic": "true", - "type": "object" - }, - "fields": { - "type": "object" - }, - "file": { - "properties": { - "accessed": { - "type": "date" - }, - "attributes": { - "ignore_above": 1024, - "type": "keyword" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "created": { - "type": "date" - }, - "ctime": { - "type": "date" - }, - "device": { - "ignore_above": 1024, - "type": "keyword" - }, - "directory": { - "ignore_above": 1024, - "type": "keyword" - }, - "drive_letter": { - "ignore_above": 1, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "inode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mime_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "mode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mtime": { - "type": "date" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "owner": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "size": { - "type": "long" - }, - "target_path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "host": { - "dynamic": "false", - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "containerized": { - "type": "boolean" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "ignore_above": 1024, - "type": "keyword" - }, - "codename": { - "ignore_above": 1024, - "type": "keyword" - }, - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "http": { - "dynamic": "false", - "properties": { - "request": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "headers": { - "enabled": false, - "type": "object" - }, - "method": { - "ignore_above": 1024, - "type": "keyword" - }, - "referrer": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "response": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "finished": { - "type": "boolean" - }, - "headers": { - "enabled": false, - "type": "object" - }, - "status_code": { - "type": "long" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "kubernetes": { - "dynamic": "false", - "properties": { - "annotations": { - "properties": { - "*": { - "type": "object" - } - } - }, - "container": { - "properties": { - "image": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "deployment": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "properties": { - "*": { - "type": "object" - } - } - }, - "namespace": { - "ignore_above": 1024, - "type": "keyword" - }, - "node": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pod": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "replicaset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "statefulset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "labels": { - "dynamic": "true", - "type": "object" - }, - "log": { - "properties": { - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "logger": { - "ignore_above": 1024, - "type": "keyword" - }, - "origin": { - "properties": { - "file": { - "properties": { - "line": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "syslog": { - "properties": { - "facility": { - "properties": { - "code": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "priority": { - "type": "long" - }, - "severity": { - "properties": { - "code": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "message": { - "norms": false, - "type": "text" - }, - "network": { - "properties": { - "application": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "community_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "direction": { - "ignore_above": 1024, - "type": "keyword" - }, - "forwarded_ip": { - "type": "ip" - }, - "iana_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "inner": { - "properties": { - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "transport": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "observer": { - "dynamic": "false", - "properties": { - "egress": { - "properties": { - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "zone": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "ingress": { - "properties": { - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "zone": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "listening": { - "ignore_above": 1024, - "type": "keyword" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "vendor": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - }, - "version_major": { - "type": "byte" - } - } - }, - "organization": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "package": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "build_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "checksum": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "install_scope": { - "ignore_above": 1024, - "type": "keyword" - }, - "installed": { - "type": "date" - }, - "license": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "parent": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "process": { - "dynamic": "false", - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "parent": { - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "title": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "title": { - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "processor": { - "properties": { - "event": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "profile": { - "dynamic": "false", - "properties": { - "alloc_objects": { - "properties": { - "count": { - "type": "long" - } - } - }, - "alloc_space": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "cpu": { - "properties": { - "ns": { - "type": "long" - } - } - }, - "duration": { - "type": "long" - }, - "inuse_objects": { - "properties": { - "count": { - "type": "long" - } - } - }, - "inuse_space": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "samples": { - "properties": { - "count": { - "type": "long" - } - } - }, - "stack": { - "dynamic": "false", - "properties": { - "filename": { - "ignore_above": 1024, - "type": "keyword" - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "line": { - "type": "long" - } - } - }, - "top": { - "dynamic": "false", - "properties": { - "filename": { - "ignore_above": 1024, - "type": "keyword" - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "line": { - "type": "long" - } - } - } - } - }, - "registry": { - "properties": { - "data": { - "properties": { - "bytes": { - "ignore_above": 1024, - "type": "keyword" - }, - "strings": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hive": { - "ignore_above": 1024, - "type": "keyword" - }, - "key": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "value": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "related": { - "properties": { - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "rule": { - "properties": { - "author": { - "ignore_above": 1024, - "type": "keyword" - }, - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "license": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "ruleset": { - "ignore_above": 1024, - "type": "keyword" - }, - "uuid": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "server": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "service": { - "dynamic": "false", - "properties": { - "environment": { - "ignore_above": 1024, - "type": "keyword" - }, - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "framework": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "language": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "node": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "state": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "source": { - "dynamic": "false", - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "sourcemap": { - "dynamic": "false", - "properties": { - "bundle_filepath": { - "ignore_above": 1024, - "type": "keyword" - }, - "service": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "span": { - "dynamic": "false", - "properties": { - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "db": { - "dynamic": "false", - "properties": { - "link": { - "ignore_above": 1024, - "type": "keyword" - }, - "rows_affected": { - "type": "long" - } - } - }, - "destination": { - "dynamic": "false", - "properties": { - "service": { - "dynamic": "false", - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "resource": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "duration": { - "properties": { - "us": { - "type": "long" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "message": { - "dynamic": "false", - "properties": { - "age": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "self_time": { - "properties": { - "count": { - "type": "long" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - } - } - }, - "start": { - "properties": { - "us": { - "type": "long" - } - } - }, - "subtype": { - "ignore_above": 1024, - "type": "keyword" - }, - "sync": { - "type": "boolean" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "system": { - "properties": { - "cpu": { - "properties": { - "total": { - "properties": { - "norm": { - "properties": { - "pct": { - "scaling_factor": 1000, - "type": "scaled_float" - } - } - } - } - } - } - }, - "memory": { - "properties": { - "actual": { - "properties": { - "free": { - "type": "long" - } - } - }, - "total": { - "type": "long" - } - } - }, - "process": { - "properties": { - "cpu": { - "properties": { - "total": { - "properties": { - "norm": { - "properties": { - "pct": { - "scaling_factor": 1000, - "type": "scaled_float" - } - } - } - } - } - } - }, - "memory": { - "properties": { - "rss": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "size": { - "type": "long" - } - } - } - } - } - } - }, - "tags": { - "ignore_above": 1024, - "type": "keyword" - }, - "threat": { - "properties": { - "framework": { - "ignore_above": 1024, - "type": "keyword" - }, - "tactic": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "technique": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "timeseries": { - "properties": { - "instance": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "timestamp": { - "properties": { - "us": { - "type": "long" - } - } - }, - "tls": { - "properties": { - "cipher": { - "ignore_above": 1024, - "type": "keyword" - }, - "client": { - "properties": { - "certificate": { - "ignore_above": 1024, - "type": "keyword" - }, - "certificate_chain": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "issuer": { - "ignore_above": 1024, - "type": "keyword" - }, - "ja3": { - "ignore_above": 1024, - "type": "keyword" - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "server_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject": { - "ignore_above": 1024, - "type": "keyword" - }, - "supported_ciphers": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "curve": { - "ignore_above": 1024, - "type": "keyword" - }, - "established": { - "type": "boolean" - }, - "next_protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "resumed": { - "type": "boolean" - }, - "server": { - "properties": { - "certificate": { - "ignore_above": 1024, - "type": "keyword" - }, - "certificate_chain": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "issuer": { - "ignore_above": 1024, - "type": "keyword" - }, - "ja3s": { - "ignore_above": 1024, - "type": "keyword" - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "subject": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - }, - "version_protocol": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "trace": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "tracing": { - "properties": { - "trace": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "transaction": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "transaction": { - "dynamic": "false", - "properties": { - "breakdown": { - "properties": { - "count": { - "type": "long" - } - } - }, - "duration": { - "properties": { - "count": { - "type": "long" - }, - "histogram": { - "type": "histogram" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - }, - "us": { - "type": "long" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "marks": { - "dynamic": "true", - "properties": { - "*": { - "properties": { - "*": { - "dynamic": "true", - "type": "object" - } - } - } - } - }, - "message": { - "dynamic": "false", - "properties": { - "age": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "result": { - "ignore_above": 1024, - "type": "keyword" - }, - "root": { - "type": "boolean" - }, - "sampled": { - "type": "boolean" - }, - "self_time": { - "properties": { - "count": { - "type": "long" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - } - } - }, - "span_count": { - "properties": { - "dropped": { - "type": "long" - } - } - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "url": { - "dynamic": "false", - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "fragment": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "password": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "port": { - "type": "long" - }, - "query": { - "ignore_above": 1024, - "type": "keyword" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "scheme": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "username": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user": { - "dynamic": "false", - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user_agent": { - "dynamic": "false", - "properties": { - "device": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "view spans": { - "ignore_above": 1024, - "type": "keyword" - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vulnerability": { - "properties": { - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "classification": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "enumeration": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "report_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "scanner": { - "properties": { - "vendor": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "score": { - "properties": { - "base": { - "type": "float" - }, - "environmental": { - "type": "float" - }, - "temporal": { - "type": "float" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "severity": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "settings": { - "index": { - "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-profile" - }, - "mapping": { - "total_fields": { - "limit": "2000" - } - }, - "number_of_replicas": "0", - "number_of_shards": "1", - "priority": "100", - "query": { - "default_field": [ - "message", - "tags", - "agent.ephemeral_id", - "agent.id", - "agent.name", - "agent.type", - "agent.version", - "as.organization.name", - "client.address", - "client.as.organization.name", - "client.domain", - "client.geo.city_name", - "client.geo.continent_name", - "client.geo.country_iso_code", - "client.geo.country_name", - "client.geo.name", - "client.geo.region_iso_code", - "client.geo.region_name", - "client.mac", - "client.registered_domain", - "client.top_level_domain", - "client.user.domain", - "client.user.email", - "client.user.full_name", - "client.user.group.domain", - "client.user.group.id", - "client.user.group.name", - "client.user.hash", - "client.user.id", - "client.user.name", - "cloud.account.id", - "cloud.availability_zone", - "cloud.instance.id", - "cloud.instance.name", - "cloud.machine.type", - "cloud.provider", - "cloud.region", - "container.id", - "container.image.name", - "container.image.tag", - "container.name", - "container.runtime", - "destination.address", - "destination.as.organization.name", - "destination.domain", - "destination.geo.city_name", - "destination.geo.continent_name", - "destination.geo.country_iso_code", - "destination.geo.country_name", - "destination.geo.name", - "destination.geo.region_iso_code", - "destination.geo.region_name", - "destination.mac", - "destination.registered_domain", - "destination.top_level_domain", - "destination.user.domain", - "destination.user.email", - "destination.user.full_name", - "destination.user.group.domain", - "destination.user.group.id", - "destination.user.group.name", - "destination.user.hash", - "destination.user.id", - "destination.user.name", - "dns.answers.class", - "dns.answers.data", - "dns.answers.name", - "dns.answers.type", - "dns.header_flags", - "dns.id", - "dns.op_code", - "dns.question.class", - "dns.question.name", - "dns.question.registered_domain", - "dns.question.subdomain", - "dns.question.top_level_domain", - "dns.question.type", - "dns.response_code", - "dns.type", - "ecs.version", - "error.code", - "error.id", - "error.message", - "error.stack_trace", - "error.type", - "event.action", - "event.category", - "event.code", - "event.dataset", - "event.hash", - "event.id", - "event.kind", - "event.module", - "event.original", - "event.outcome", - "event.provider", - "event.timezone", - "event.type", - "file.device", - "file.directory", - "file.extension", - "file.gid", - "file.group", - "file.hash.md5", - "file.hash.sha1", - "file.hash.sha256", - "file.hash.sha512", - "file.inode", - "file.mode", - "file.name", - "file.owner", - "file.path", - "file.target_path", - "file.type", - "file.uid", - "geo.city_name", - "geo.continent_name", - "geo.country_iso_code", - "geo.country_name", - "geo.name", - "geo.region_iso_code", - "geo.region_name", - "group.domain", - "group.id", - "group.name", - "hash.md5", - "hash.sha1", - "hash.sha256", - "hash.sha512", - "host.architecture", - "host.geo.city_name", - "host.geo.continent_name", - "host.geo.country_iso_code", - "host.geo.country_name", - "host.geo.name", - "host.geo.region_iso_code", - "host.geo.region_name", - "host.hostname", - "host.id", - "host.mac", - "host.name", - "host.os.family", - "host.os.full", - "host.os.kernel", - "host.os.name", - "host.os.platform", - "host.os.version", - "host.type", - "host.user.domain", - "host.user.email", - "host.user.full_name", - "host.user.group.domain", - "host.user.group.id", - "host.user.group.name", - "host.user.hash", - "host.user.id", - "host.user.name", - "http.request.body.content", - "http.request.method", - "http.request.referrer", - "http.response.body.content", - "http.version", - "log.level", - "log.logger", - "log.origin.file.name", - "log.origin.function", - "log.original", - "log.syslog.facility.name", - "log.syslog.severity.name", - "network.application", - "network.community_id", - "network.direction", - "network.iana_number", - "network.name", - "network.protocol", - "network.transport", - "network.type", - "observer.geo.city_name", - "observer.geo.continent_name", - "observer.geo.country_iso_code", - "observer.geo.country_name", - "observer.geo.name", - "observer.geo.region_iso_code", - "observer.geo.region_name", - "observer.hostname", - "observer.mac", - "observer.name", - "observer.os.family", - "observer.os.full", - "observer.os.kernel", - "observer.os.name", - "observer.os.platform", - "observer.os.version", - "observer.product", - "observer.serial_number", - "observer.type", - "observer.vendor", - "observer.version", - "organization.id", - "organization.name", - "os.family", - "os.full", - "os.kernel", - "os.name", - "os.platform", - "os.version", - "package.architecture", - "package.checksum", - "package.description", - "package.install_scope", - "package.license", - "package.name", - "package.path", - "package.version", - "process.args", - "text", - "process.executable", - "process.hash.md5", - "process.hash.sha1", - "process.hash.sha256", - "process.hash.sha512", - "process.name", - "text", - "text", - "text", - "text", - "text", - "process.thread.name", - "process.title", - "process.working_directory", - "server.address", - "server.as.organization.name", - "server.domain", - "server.geo.city_name", - "server.geo.continent_name", - "server.geo.country_iso_code", - "server.geo.country_name", - "server.geo.name", - "server.geo.region_iso_code", - "server.geo.region_name", - "server.mac", - "server.registered_domain", - "server.top_level_domain", - "server.user.domain", - "server.user.email", - "server.user.full_name", - "server.user.group.domain", - "server.user.group.id", - "server.user.group.name", - "server.user.hash", - "server.user.id", - "server.user.name", - "service.ephemeral_id", - "service.id", - "service.name", - "service.node.name", - "service.state", - "service.type", - "service.version", - "source.address", - "source.as.organization.name", - "source.domain", - "source.geo.city_name", - "source.geo.continent_name", - "source.geo.country_iso_code", - "source.geo.country_name", - "source.geo.name", - "source.geo.region_iso_code", - "source.geo.region_name", - "source.mac", - "source.registered_domain", - "source.top_level_domain", - "source.user.domain", - "source.user.email", - "source.user.full_name", - "source.user.group.domain", - "source.user.group.id", - "source.user.group.name", - "source.user.hash", - "source.user.id", - "source.user.name", - "threat.framework", - "threat.tactic.id", - "threat.tactic.name", - "threat.tactic.reference", - "threat.technique.id", - "threat.technique.name", - "threat.technique.reference", - "tracing.trace.id", - "tracing.transaction.id", - "url.domain", - "url.extension", - "url.fragment", - "url.full", - "url.original", - "url.password", - "url.path", - "url.query", - "url.registered_domain", - "url.scheme", - "url.top_level_domain", - "url.username", - "user.domain", - "user.email", - "user.full_name", - "user.group.domain", - "user.group.id", - "user.group.name", - "user.hash", - "user.id", - "user.name", - "user_agent.device.name", - "user_agent.name", - "text", - "user_agent.original", - "user_agent.os.family", - "user_agent.os.full", - "user_agent.os.kernel", - "user_agent.os.name", - "user_agent.os.platform", - "user_agent.os.version", - "user_agent.version", - "text", - "timeseries.instance", - "cloud.project.id", - "cloud.image.id", - "host.os.build", - "host.os.codename", - "kubernetes.pod.name", - "kubernetes.pod.uid", - "kubernetes.namespace", - "kubernetes.node.name", - "kubernetes.replicaset.name", - "kubernetes.deployment.name", - "kubernetes.statefulset.name", - "kubernetes.container.name", - "kubernetes.container.image", - "processor.name", - "processor.event", - "url.scheme", - "url.full", - "url.domain", - "url.path", - "url.query", - "url.fragment", - "http.version", - "http.request.method", - "http.request.referrer", - "service.name", - "service.version", - "service.environment", - "service.node.name", - "service.language.name", - "service.language.version", - "service.runtime.name", - "service.runtime.version", - "service.framework.name", - "service.framework.version", - "transaction.id", - "transaction.type", - "text", - "transaction.name", - "span.type", - "span.subtype", - "trace.id", - "parent.id", - "agent.name", - "agent.version", - "agent.ephemeral_id", - "container.id", - "kubernetes.namespace", - "kubernetes.node.name", - "kubernetes.pod.name", - "kubernetes.pod.uid", - "host.architecture", - "host.hostname", - "host.name", - "host.os.platform", - "process.args", - "process.title", - "observer.listening", - "observer.hostname", - "observer.version", - "observer.type", - "user.name", - "user.id", - "user.email", - "destination.address", - "text", - "user_agent.original", - "user_agent.name", - "user_agent.version", - "user_agent.device.name", - "user_agent.os.platform", - "user_agent.os.name", - "user_agent.os.full", - "user_agent.os.family", - "user_agent.os.version", - "user_agent.os.kernel", - "cloud.account.id", - "cloud.account.name", - "cloud.availability_zone", - "cloud.instance.id", - "cloud.instance.name", - "cloud.machine.type", - "cloud.project.id", - "cloud.project.name", - "cloud.provider", - "cloud.region", - "error.id", - "error.culprit", - "error.grouping_key", - "error.exception.code", - "error.exception.message", - "error.exception.module", - "error.exception.type", - "error.log.level", - "error.log.logger_name", - "error.log.message", - "error.log.param_message", - "profile.top.id", - "profile.top.function", - "profile.top.filename", - "profile.stack.id", - "profile.stack.function", - "profile.stack.filename", - "sourcemap.service.name", - "sourcemap.service.version", - "sourcemap.bundle_filepath", - "view spans", - "child.id", - "span.id", - "span.name", - "span.action", - "span.db.link", - "span.destination.service.type", - "span.destination.service.name", - "span.destination.service.resource", - "span.message.queue.name", - "transaction.result", - "transaction.message.queue.name", - "fields.*" - ] - }, - "refresh_interval": "1ms" - } - } - } -} - -{ - "type": "index", - "value": { - "aliases": { - "apm-8.0.0-span": { - "is_write_index": true - } - }, - "index": "apm-8.0.0-span-000001", - "mappings": { - "_meta": { - "beat": "apm", - "version": "8.0.0" - }, - "date_detection": false, - "dynamic_templates": [ - { - "labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" - } - }, - { - "container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "container.labels.*" - } - }, - { - "dns.answers": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "dns.answers.*" - } - }, - { - "log.syslog": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "log.syslog.*" - } - }, - { - "network.inner": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "network.inner.*" - } - }, - { - "observer.egress": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "observer.egress.*" - } - }, - { - "observer.ingress": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "observer.ingress.*" - } - }, - { - "fields": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "fields.*" - } - }, - { - "docker.container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "docker.container.labels.*" - } - }, - { - "kubernetes.labels.*": { - "mapping": { - "type": "keyword" - }, - "path_match": "kubernetes.labels.*" - } - }, - { - "kubernetes.annotations.*": { - "mapping": { - "type": "keyword" - }, - "path_match": "kubernetes.annotations.*" - } - }, - { - "labels_string": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" - } - }, - { - "labels_boolean": { - "mapping": { - "type": "boolean" - }, - "match_mapping_type": "boolean", - "path_match": "labels.*" - } - }, - { - "labels_*": { - "mapping": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "path_match": "labels.*" - } - }, - { - "transaction.marks": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "transaction.marks.*" - } - }, - { - "transaction.marks.*.*": { - "mapping": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "path_match": "transaction.marks.*.*" - } - }, - { - "strings_as_keyword": { - "mapping": { - "ignore_above": 1024, - "type": "keyword" - }, - "match_mapping_type": "string" - } - } - ], - "properties": { - "@timestamp": { - "type": "date" - }, - "agent": { - "dynamic": "false", - "properties": { - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "hostname": { - "path": "agent.name", - "type": "alias" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "child": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "client": { - "dynamic": "false", - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "account": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "availability_zone": { - "ignore_above": 1024, - "type": "keyword" - }, - "image": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "instance": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "machine": { - "dynamic": "false", - "properties": { - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "project": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "region": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "container": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "image": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "tag": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "type": "object" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "runtime": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "destination": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "dll": { - "properties": { - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "dns": { - "properties": { - "answers": { - "properties": { - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "data": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "ttl": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "header_flags": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "op_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "question": { - "properties": { - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "subdomain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "resolved_ip": { - "type": "ip" - }, - "response_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "docker": { - "properties": { - "container": { - "properties": { - "labels": { - "type": "object" - } - } - } - } - }, - "ecs": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "error": { - "dynamic": "false", - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "culprit": { - "ignore_above": 1024, - "type": "keyword" - }, - "exception": { - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "handled": { - "type": "boolean" - }, - "message": { - "norms": false, - "type": "text" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "grouping_key": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "log": { - "properties": { - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "logger_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "message": { - "norms": false, - "type": "text" - }, - "param_message": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "message": { - "norms": false, - "type": "text" - }, - "stack_trace": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "event": { - "properties": { - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "created": { - "type": "date" - }, - "dataset": { - "ignore_above": 1024, - "type": "keyword" - }, - "duration": { - "type": "long" - }, - "end": { - "type": "date" - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ingested": { - "type": "date" - }, - "kind": { - "ignore_above": 1024, - "type": "keyword" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "outcome": { - "ignore_above": 1024, - "type": "keyword" - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "risk_score": { - "type": "float" - }, - "risk_score_norm": { - "type": "float" - }, - "sequence": { - "type": "long" - }, - "severity": { - "type": "long" - }, - "start": { - "type": "date" - }, - "timezone": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "url": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "experimental": { - "dynamic": "true", - "type": "object" - }, - "fields": { - "type": "object" - }, - "file": { - "properties": { - "accessed": { - "type": "date" - }, - "attributes": { - "ignore_above": 1024, - "type": "keyword" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "created": { - "type": "date" - }, - "ctime": { - "type": "date" - }, - "device": { - "ignore_above": 1024, - "type": "keyword" - }, - "directory": { - "ignore_above": 1024, - "type": "keyword" - }, - "drive_letter": { - "ignore_above": 1, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "inode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mime_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "mode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mtime": { - "type": "date" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "owner": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "size": { - "type": "long" - }, - "target_path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "host": { - "dynamic": "false", - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "containerized": { - "type": "boolean" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "ignore_above": 1024, - "type": "keyword" - }, - "codename": { - "ignore_above": 1024, - "type": "keyword" - }, - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "http": { - "dynamic": "false", - "properties": { - "request": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "headers": { - "enabled": false, - "type": "object" - }, - "method": { - "ignore_above": 1024, - "type": "keyword" - }, - "referrer": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "response": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "finished": { - "type": "boolean" - }, - "headers": { - "enabled": false, - "type": "object" - }, - "status_code": { - "type": "long" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "kubernetes": { - "dynamic": "false", - "properties": { - "annotations": { - "properties": { - "*": { - "type": "object" - } - } - }, - "container": { - "properties": { - "image": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "deployment": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "properties": { - "*": { - "type": "object" - } - } - }, - "namespace": { - "ignore_above": 1024, - "type": "keyword" - }, - "node": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pod": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "replicaset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "statefulset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "labels": { - "dynamic": "true", - "properties": { - "foo": { - "type": "keyword" - }, - "productId": { - "type": "keyword" - } - } - }, - "log": { - "properties": { - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "logger": { - "ignore_above": 1024, - "type": "keyword" - }, - "origin": { - "properties": { - "file": { - "properties": { - "line": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "syslog": { - "properties": { - "facility": { - "properties": { - "code": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "priority": { - "type": "long" - }, - "severity": { - "properties": { - "code": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "message": { - "norms": false, - "type": "text" - }, - "network": { - "properties": { - "application": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "community_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "direction": { - "ignore_above": 1024, - "type": "keyword" - }, - "forwarded_ip": { - "type": "ip" - }, - "iana_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "inner": { - "properties": { - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "transport": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "observer": { - "dynamic": "false", - "properties": { - "egress": { - "properties": { - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "zone": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "ingress": { - "properties": { - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "zone": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "listening": { - "ignore_above": 1024, - "type": "keyword" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "vendor": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - }, - "version_major": { - "type": "byte" - } - } - }, - "organization": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "package": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "build_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "checksum": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "install_scope": { - "ignore_above": 1024, - "type": "keyword" - }, - "installed": { - "type": "date" - }, - "license": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "parent": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "process": { - "dynamic": "false", - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "parent": { - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "title": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "title": { - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "processor": { - "properties": { - "event": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "profile": { - "dynamic": "false", - "properties": { - "alloc_objects": { - "properties": { - "count": { - "type": "long" - } - } - }, - "alloc_space": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "cpu": { - "properties": { - "ns": { - "type": "long" - } - } - }, - "duration": { - "type": "long" - }, - "inuse_objects": { - "properties": { - "count": { - "type": "long" - } - } - }, - "inuse_space": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "samples": { - "properties": { - "count": { - "type": "long" - } - } - }, - "stack": { - "dynamic": "false", - "properties": { - "filename": { - "ignore_above": 1024, - "type": "keyword" - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "line": { - "type": "long" - } - } - }, - "top": { - "dynamic": "false", - "properties": { - "filename": { - "ignore_above": 1024, - "type": "keyword" - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "line": { - "type": "long" - } - } - } - } - }, - "registry": { - "properties": { - "data": { - "properties": { - "bytes": { - "ignore_above": 1024, - "type": "keyword" - }, - "strings": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hive": { - "ignore_above": 1024, - "type": "keyword" - }, - "key": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "value": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "related": { - "properties": { - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "rule": { - "properties": { - "author": { - "ignore_above": 1024, - "type": "keyword" - }, - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "license": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "ruleset": { - "ignore_above": 1024, - "type": "keyword" - }, - "uuid": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "server": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "service": { - "dynamic": "false", - "properties": { - "environment": { - "ignore_above": 1024, - "type": "keyword" - }, - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "framework": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "language": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "node": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "state": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "source": { - "dynamic": "false", - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "sourcemap": { - "dynamic": "false", - "properties": { - "bundle_filepath": { - "ignore_above": 1024, - "type": "keyword" - }, - "service": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "span": { - "dynamic": "false", - "properties": { - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "db": { - "dynamic": "false", - "properties": { - "link": { - "ignore_above": 1024, - "type": "keyword" - }, - "rows_affected": { - "type": "long" - } - } - }, - "destination": { - "dynamic": "false", - "properties": { - "service": { - "dynamic": "false", - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "resource": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "duration": { - "properties": { - "us": { - "type": "long" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "message": { - "dynamic": "false", - "properties": { - "age": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "self_time": { - "properties": { - "count": { - "type": "long" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - } - } - }, - "start": { - "properties": { - "us": { - "type": "long" - } - } - }, - "subtype": { - "ignore_above": 1024, - "type": "keyword" - }, - "sync": { - "type": "boolean" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "system": { - "properties": { - "cpu": { - "properties": { - "total": { - "properties": { - "norm": { - "properties": { - "pct": { - "scaling_factor": 1000, - "type": "scaled_float" - } - } - } - } - } - } - }, - "memory": { - "properties": { - "actual": { - "properties": { - "free": { - "type": "long" - } - } - }, - "total": { - "type": "long" - } - } - }, - "process": { - "properties": { - "cpu": { - "properties": { - "total": { - "properties": { - "norm": { - "properties": { - "pct": { - "scaling_factor": 1000, - "type": "scaled_float" - } - } - } - } - } - } - }, - "memory": { - "properties": { - "rss": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "size": { - "type": "long" - } - } - } - } - } - } - }, - "tags": { - "ignore_above": 1024, - "type": "keyword" - }, - "threat": { - "properties": { - "framework": { - "ignore_above": 1024, - "type": "keyword" - }, - "tactic": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "technique": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "timeseries": { - "properties": { - "instance": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "timestamp": { - "properties": { - "us": { - "type": "long" - } - } - }, - "tls": { - "properties": { - "cipher": { - "ignore_above": 1024, - "type": "keyword" - }, - "client": { - "properties": { - "certificate": { - "ignore_above": 1024, - "type": "keyword" - }, - "certificate_chain": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "issuer": { - "ignore_above": 1024, - "type": "keyword" - }, - "ja3": { - "ignore_above": 1024, - "type": "keyword" - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "server_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject": { - "ignore_above": 1024, - "type": "keyword" - }, - "supported_ciphers": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "curve": { - "ignore_above": 1024, - "type": "keyword" - }, - "established": { - "type": "boolean" - }, - "next_protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "resumed": { - "type": "boolean" - }, - "server": { - "properties": { - "certificate": { - "ignore_above": 1024, - "type": "keyword" - }, - "certificate_chain": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "issuer": { - "ignore_above": 1024, - "type": "keyword" - }, - "ja3s": { - "ignore_above": 1024, - "type": "keyword" - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "subject": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - }, - "version_protocol": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "trace": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "tracing": { - "properties": { - "trace": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "transaction": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "transaction": { - "dynamic": "false", - "properties": { - "breakdown": { - "properties": { - "count": { - "type": "long" - } - } - }, - "duration": { - "properties": { - "count": { - "type": "long" - }, - "histogram": { - "type": "histogram" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - }, - "us": { - "type": "long" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "marks": { - "dynamic": "true", - "properties": { - "*": { - "properties": { - "*": { - "dynamic": "true", - "type": "object" - } - } - } - } - }, - "message": { - "dynamic": "false", - "properties": { - "age": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "result": { - "ignore_above": 1024, - "type": "keyword" - }, - "root": { - "type": "boolean" - }, - "sampled": { - "type": "boolean" - }, - "self_time": { - "properties": { - "count": { - "type": "long" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - } - } - }, - "span_count": { - "properties": { - "dropped": { - "type": "long" - } - } - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "url": { - "dynamic": "false", - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "fragment": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "password": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "port": { - "type": "long" - }, - "query": { - "ignore_above": 1024, - "type": "keyword" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "scheme": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "username": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user": { - "dynamic": "false", - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user_agent": { - "dynamic": "false", - "properties": { - "device": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "view spans": { - "ignore_above": 1024, - "type": "keyword" - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vulnerability": { - "properties": { - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "classification": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "enumeration": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "report_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "scanner": { - "properties": { - "vendor": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "score": { - "properties": { - "base": { - "type": "float" - }, - "environmental": { - "type": "float" - }, - "temporal": { - "type": "float" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "severity": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "settings": { - "index": { - "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-span" - }, - "mapping": { - "total_fields": { - "limit": "2000" - } - }, - "number_of_replicas": "0", - "number_of_shards": "1", - "priority": "100", - "query": { - "default_field": [ - "message", - "tags", - "agent.ephemeral_id", - "agent.id", - "agent.name", - "agent.type", - "agent.version", - "as.organization.name", - "client.address", - "client.as.organization.name", - "client.domain", - "client.geo.city_name", - "client.geo.continent_name", - "client.geo.country_iso_code", - "client.geo.country_name", - "client.geo.name", - "client.geo.region_iso_code", - "client.geo.region_name", - "client.mac", - "client.registered_domain", - "client.top_level_domain", - "client.user.domain", - "client.user.email", - "client.user.full_name", - "client.user.group.domain", - "client.user.group.id", - "client.user.group.name", - "client.user.hash", - "client.user.id", - "client.user.name", - "cloud.account.id", - "cloud.availability_zone", - "cloud.instance.id", - "cloud.instance.name", - "cloud.machine.type", - "cloud.provider", - "cloud.region", - "container.id", - "container.image.name", - "container.image.tag", - "container.name", - "container.runtime", - "destination.address", - "destination.as.organization.name", - "destination.domain", - "destination.geo.city_name", - "destination.geo.continent_name", - "destination.geo.country_iso_code", - "destination.geo.country_name", - "destination.geo.name", - "destination.geo.region_iso_code", - "destination.geo.region_name", - "destination.mac", - "destination.registered_domain", - "destination.top_level_domain", - "destination.user.domain", - "destination.user.email", - "destination.user.full_name", - "destination.user.group.domain", - "destination.user.group.id", - "destination.user.group.name", - "destination.user.hash", - "destination.user.id", - "destination.user.name", - "dns.answers.class", - "dns.answers.data", - "dns.answers.name", - "dns.answers.type", - "dns.header_flags", - "dns.id", - "dns.op_code", - "dns.question.class", - "dns.question.name", - "dns.question.registered_domain", - "dns.question.subdomain", - "dns.question.top_level_domain", - "dns.question.type", - "dns.response_code", - "dns.type", - "ecs.version", - "error.code", - "error.id", - "error.message", - "error.stack_trace", - "error.type", - "event.action", - "event.category", - "event.code", - "event.dataset", - "event.hash", - "event.id", - "event.kind", - "event.module", - "event.original", - "event.outcome", - "event.provider", - "event.timezone", - "event.type", - "file.device", - "file.directory", - "file.extension", - "file.gid", - "file.group", - "file.hash.md5", - "file.hash.sha1", - "file.hash.sha256", - "file.hash.sha512", - "file.inode", - "file.mode", - "file.name", - "file.owner", - "file.path", - "file.target_path", - "file.type", - "file.uid", - "geo.city_name", - "geo.continent_name", - "geo.country_iso_code", - "geo.country_name", - "geo.name", - "geo.region_iso_code", - "geo.region_name", - "group.domain", - "group.id", - "group.name", - "hash.md5", - "hash.sha1", - "hash.sha256", - "hash.sha512", - "host.architecture", - "host.geo.city_name", - "host.geo.continent_name", - "host.geo.country_iso_code", - "host.geo.country_name", - "host.geo.name", - "host.geo.region_iso_code", - "host.geo.region_name", - "host.hostname", - "host.id", - "host.mac", - "host.name", - "host.os.family", - "host.os.full", - "host.os.kernel", - "host.os.name", - "host.os.platform", - "host.os.version", - "host.type", - "host.user.domain", - "host.user.email", - "host.user.full_name", - "host.user.group.domain", - "host.user.group.id", - "host.user.group.name", - "host.user.hash", - "host.user.id", - "host.user.name", - "http.request.body.content", - "http.request.method", - "http.request.referrer", - "http.response.body.content", - "http.version", - "log.level", - "log.logger", - "log.origin.file.name", - "log.origin.function", - "log.original", - "log.syslog.facility.name", - "log.syslog.severity.name", - "network.application", - "network.community_id", - "network.direction", - "network.iana_number", - "network.name", - "network.protocol", - "network.transport", - "network.type", - "observer.geo.city_name", - "observer.geo.continent_name", - "observer.geo.country_iso_code", - "observer.geo.country_name", - "observer.geo.name", - "observer.geo.region_iso_code", - "observer.geo.region_name", - "observer.hostname", - "observer.mac", - "observer.name", - "observer.os.family", - "observer.os.full", - "observer.os.kernel", - "observer.os.name", - "observer.os.platform", - "observer.os.version", - "observer.product", - "observer.serial_number", - "observer.type", - "observer.vendor", - "observer.version", - "organization.id", - "organization.name", - "os.family", - "os.full", - "os.kernel", - "os.name", - "os.platform", - "os.version", - "package.architecture", - "package.checksum", - "package.description", - "package.install_scope", - "package.license", - "package.name", - "package.path", - "package.version", - "process.args", - "text", - "process.executable", - "process.hash.md5", - "process.hash.sha1", - "process.hash.sha256", - "process.hash.sha512", - "process.name", - "text", - "text", - "text", - "text", - "text", - "process.thread.name", - "process.title", - "process.working_directory", - "server.address", - "server.as.organization.name", - "server.domain", - "server.geo.city_name", - "server.geo.continent_name", - "server.geo.country_iso_code", - "server.geo.country_name", - "server.geo.name", - "server.geo.region_iso_code", - "server.geo.region_name", - "server.mac", - "server.registered_domain", - "server.top_level_domain", - "server.user.domain", - "server.user.email", - "server.user.full_name", - "server.user.group.domain", - "server.user.group.id", - "server.user.group.name", - "server.user.hash", - "server.user.id", - "server.user.name", - "service.ephemeral_id", - "service.id", - "service.name", - "service.node.name", - "service.state", - "service.type", - "service.version", - "source.address", - "source.as.organization.name", - "source.domain", - "source.geo.city_name", - "source.geo.continent_name", - "source.geo.country_iso_code", - "source.geo.country_name", - "source.geo.name", - "source.geo.region_iso_code", - "source.geo.region_name", - "source.mac", - "source.registered_domain", - "source.top_level_domain", - "source.user.domain", - "source.user.email", - "source.user.full_name", - "source.user.group.domain", - "source.user.group.id", - "source.user.group.name", - "source.user.hash", - "source.user.id", - "source.user.name", - "threat.framework", - "threat.tactic.id", - "threat.tactic.name", - "threat.tactic.reference", - "threat.technique.id", - "threat.technique.name", - "threat.technique.reference", - "tracing.trace.id", - "tracing.transaction.id", - "url.domain", - "url.extension", - "url.fragment", - "url.full", - "url.original", - "url.password", - "url.path", - "url.query", - "url.registered_domain", - "url.scheme", - "url.top_level_domain", - "url.username", - "user.domain", - "user.email", - "user.full_name", - "user.group.domain", - "user.group.id", - "user.group.name", - "user.hash", - "user.id", - "user.name", - "user_agent.device.name", - "user_agent.name", - "text", - "user_agent.original", - "user_agent.os.family", - "user_agent.os.full", - "user_agent.os.kernel", - "user_agent.os.name", - "user_agent.os.platform", - "user_agent.os.version", - "user_agent.version", - "text", - "timeseries.instance", - "cloud.project.id", - "cloud.image.id", - "host.os.build", - "host.os.codename", - "kubernetes.pod.name", - "kubernetes.pod.uid", - "kubernetes.namespace", - "kubernetes.node.name", - "kubernetes.replicaset.name", - "kubernetes.deployment.name", - "kubernetes.statefulset.name", - "kubernetes.container.name", - "kubernetes.container.image", - "processor.name", - "processor.event", - "url.scheme", - "url.full", - "url.domain", - "url.path", - "url.query", - "url.fragment", - "http.version", - "http.request.method", - "http.request.referrer", - "service.name", - "service.version", - "service.environment", - "service.node.name", - "service.language.name", - "service.language.version", - "service.runtime.name", - "service.runtime.version", - "service.framework.name", - "service.framework.version", - "transaction.id", - "transaction.type", - "text", - "transaction.name", - "span.type", - "span.subtype", - "trace.id", - "parent.id", - "agent.name", - "agent.version", - "agent.ephemeral_id", - "container.id", - "kubernetes.namespace", - "kubernetes.node.name", - "kubernetes.pod.name", - "kubernetes.pod.uid", - "host.architecture", - "host.hostname", - "host.name", - "host.os.platform", - "process.args", - "process.title", - "observer.listening", - "observer.hostname", - "observer.version", - "observer.type", - "user.name", - "user.id", - "user.email", - "destination.address", - "text", - "user_agent.original", - "user_agent.name", - "user_agent.version", - "user_agent.device.name", - "user_agent.os.platform", - "user_agent.os.name", - "user_agent.os.full", - "user_agent.os.family", - "user_agent.os.version", - "user_agent.os.kernel", - "cloud.account.id", - "cloud.account.name", - "cloud.availability_zone", - "cloud.instance.id", - "cloud.instance.name", - "cloud.machine.type", - "cloud.project.id", - "cloud.project.name", - "cloud.provider", - "cloud.region", - "error.id", - "error.culprit", - "error.grouping_key", - "error.exception.code", - "error.exception.message", - "error.exception.module", - "error.exception.type", - "error.log.level", - "error.log.logger_name", - "error.log.message", - "error.log.param_message", - "profile.top.id", - "profile.top.function", - "profile.top.filename", - "profile.stack.id", - "profile.stack.function", - "profile.stack.filename", - "sourcemap.service.name", - "sourcemap.service.version", - "sourcemap.bundle_filepath", - "view spans", - "child.id", - "span.id", - "span.name", - "span.action", - "span.db.link", - "span.destination.service.type", - "span.destination.service.name", - "span.destination.service.resource", - "span.message.queue.name", - "transaction.result", - "transaction.message.queue.name", - "fields.*" - ] - }, - "refresh_interval": "1ms" - } - } - } -} - -{ - "type": "index", - "value": { - "aliases": { - "apm-8.0.0-transaction": { - "is_write_index": true - } - }, - "index": "apm-8.0.0-transaction-000001", - "mappings": { - "_meta": { - "beat": "apm", - "version": "8.0.0" - }, - "date_detection": false, - "dynamic_templates": [ - { - "labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" - } - }, - { - "container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "container.labels.*" - } - }, - { - "dns.answers": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "dns.answers.*" - } - }, - { - "log.syslog": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "log.syslog.*" - } - }, - { - "network.inner": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "network.inner.*" - } - }, - { - "observer.egress": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "observer.egress.*" - } - }, - { - "observer.ingress": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "observer.ingress.*" - } - }, - { - "fields": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "fields.*" - } - }, - { - "docker.container.labels": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "docker.container.labels.*" - } - }, - { - "kubernetes.labels.*": { - "mapping": { - "type": "keyword" - }, - "path_match": "kubernetes.labels.*" - } - }, - { - "kubernetes.annotations.*": { - "mapping": { - "type": "keyword" - }, - "path_match": "kubernetes.annotations.*" - } - }, - { - "labels_string": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "labels.*" - } - }, - { - "labels_boolean": { - "mapping": { - "type": "boolean" - }, - "match_mapping_type": "boolean", - "path_match": "labels.*" - } - }, - { - "labels_*": { - "mapping": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "path_match": "labels.*" - } - }, - { - "transaction.marks": { - "mapping": { - "type": "keyword" - }, - "match_mapping_type": "string", - "path_match": "transaction.marks.*" - } - }, - { - "transaction.marks.*.*": { - "mapping": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "path_match": "transaction.marks.*.*" - } - }, - { - "strings_as_keyword": { - "mapping": { - "ignore_above": 1024, - "type": "keyword" - }, - "match_mapping_type": "string" - } - } - ], - "properties": { - "@timestamp": { - "type": "date" - }, - "agent": { - "dynamic": "false", - "properties": { - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "hostname": { - "path": "agent.name", - "type": "alias" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "child": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "client": { - "dynamic": "false", - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "cloud": { - "properties": { - "account": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "availability_zone": { - "ignore_above": 1024, - "type": "keyword" - }, - "image": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "instance": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "machine": { - "dynamic": "false", - "properties": { - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "project": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "region": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "container": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "image": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "tag": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "type": "object" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "runtime": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "destination": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "dll": { - "properties": { - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "dns": { - "properties": { - "answers": { - "properties": { - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "data": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "ttl": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "header_flags": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "op_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "question": { - "properties": { - "class": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "subdomain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "resolved_ip": { - "type": "ip" - }, - "response_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "docker": { - "properties": { - "container": { - "properties": { - "labels": { - "type": "object" - } - } - } - } - }, - "ecs": { - "properties": { - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "error": { - "dynamic": "false", - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "culprit": { - "ignore_above": 1024, - "type": "keyword" - }, - "exception": { - "properties": { - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "handled": { - "type": "boolean" - }, - "message": { - "norms": false, - "type": "text" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "grouping_key": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "log": { - "properties": { - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "logger_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "message": { - "norms": false, - "type": "text" - }, - "param_message": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "message": { - "norms": false, - "type": "text" - }, - "stack_trace": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "event": { - "properties": { - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "code": { - "ignore_above": 1024, - "type": "keyword" - }, - "created": { - "type": "date" - }, - "dataset": { - "ignore_above": 1024, - "type": "keyword" - }, - "duration": { - "type": "long" - }, - "end": { - "type": "date" - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ingested": { - "type": "date" - }, - "kind": { - "ignore_above": 1024, - "type": "keyword" - }, - "module": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "outcome": { - "ignore_above": 1024, - "type": "keyword" - }, - "provider": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "risk_score": { - "type": "float" - }, - "risk_score_norm": { - "type": "float" - }, - "sequence": { - "type": "long" - }, - "severity": { - "type": "long" - }, - "start": { - "type": "date" - }, - "timezone": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "url": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "experimental": { - "dynamic": "true", - "type": "object" - }, - "fields": { - "type": "object" - }, - "file": { - "properties": { - "accessed": { - "type": "date" - }, - "attributes": { - "ignore_above": 1024, - "type": "keyword" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "created": { - "type": "date" - }, - "ctime": { - "type": "date" - }, - "device": { - "ignore_above": 1024, - "type": "keyword" - }, - "directory": { - "ignore_above": 1024, - "type": "keyword" - }, - "drive_letter": { - "ignore_above": 1, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "gid": { - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "inode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mime_type": { - "ignore_above": 1024, - "type": "keyword" - }, - "mode": { - "ignore_above": 1024, - "type": "keyword" - }, - "mtime": { - "type": "date" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "owner": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "size": { - "type": "long" - }, - "target_path": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "host": { - "dynamic": "false", - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "containerized": { - "type": "boolean" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "build": { - "ignore_above": 1024, - "type": "keyword" - }, - "codename": { - "ignore_above": 1024, - "type": "keyword" - }, - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "http": { - "dynamic": "false", - "properties": { - "request": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "headers": { - "enabled": false, - "type": "object" - }, - "method": { - "ignore_above": 1024, - "type": "keyword" - }, - "referrer": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "response": { - "properties": { - "body": { - "properties": { - "bytes": { - "type": "long" - }, - "content": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "bytes": { - "type": "long" - }, - "finished": { - "type": "boolean" - }, - "headers": { - "enabled": false, - "type": "object" - }, - "status_code": { - "type": "long" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "kubernetes": { - "dynamic": "false", - "properties": { - "annotations": { - "properties": { - "*": { - "type": "object" - } - } - }, - "container": { - "properties": { - "image": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "deployment": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "labels": { - "properties": { - "*": { - "type": "object" - } - } - }, - "namespace": { - "ignore_above": 1024, - "type": "keyword" - }, - "node": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pod": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "uid": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "replicaset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "statefulset": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "labels": { - "dynamic": "true", - "properties": { - "foo": { - "type": "keyword" - }, - "lorem": { - "type": "keyword" - }, - "multi-line": { - "type": "keyword" - }, - "this-is-a-very-long-tag-name-without-any-spaces": { - "type": "keyword" - } - } - }, - "log": { - "properties": { - "level": { - "ignore_above": 1024, - "type": "keyword" - }, - "logger": { - "ignore_above": 1024, - "type": "keyword" - }, - "origin": { - "properties": { - "file": { - "properties": { - "line": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "original": { - "ignore_above": 1024, - "type": "keyword" - }, - "syslog": { - "properties": { - "facility": { - "properties": { - "code": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "priority": { - "type": "long" - }, - "severity": { - "properties": { - "code": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - } - } - }, - "message": { - "norms": false, - "type": "text" - }, - "network": { - "properties": { - "application": { - "ignore_above": 1024, - "type": "keyword" - }, - "bytes": { - "type": "long" - }, - "community_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "direction": { - "ignore_above": 1024, - "type": "keyword" - }, - "forwarded_ip": { - "type": "ip" - }, - "iana_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "inner": { - "properties": { - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "packets": { - "type": "long" - }, - "protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "transport": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "observer": { - "dynamic": "false", - "properties": { - "egress": { - "properties": { - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "zone": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hostname": { - "ignore_above": 1024, - "type": "keyword" - }, - "ingress": { - "properties": { - "interface": { - "properties": { - "alias": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "zone": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "listening": { - "ignore_above": 1024, - "type": "keyword" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - }, - "serial_number": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "vendor": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - }, - "version_major": { - "type": "byte" - } - } - }, - "organization": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "package": { - "properties": { - "architecture": { - "ignore_above": 1024, - "type": "keyword" - }, - "build_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "checksum": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "install_scope": { - "ignore_above": 1024, - "type": "keyword" - }, - "installed": { - "type": "date" - }, - "license": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "size": { - "type": "long" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "parent": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "process": { - "dynamic": "false", - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "parent": { - "properties": { - "args": { - "ignore_above": 1024, - "type": "keyword" - }, - "args_count": { - "type": "long" - }, - "code_signature": { - "properties": { - "exists": { - "type": "boolean" - }, - "status": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "trusted": { - "type": "boolean" - }, - "valid": { - "type": "boolean" - } - } - }, - "command_line": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "entity_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "executable": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "exit_code": { - "type": "long" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha512": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "title": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pe": { - "properties": { - "company": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "file_version": { - "ignore_above": 1024, - "type": "keyword" - }, - "original_file_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "product": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "pgid": { - "type": "long" - }, - "pid": { - "type": "long" - }, - "ppid": { - "type": "long" - }, - "start": { - "type": "date" - }, - "thread": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "title": { - "ignore_above": 1024, - "type": "keyword" - }, - "uptime": { - "type": "long" - }, - "working_directory": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "processor": { - "properties": { - "event": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "profile": { - "dynamic": "false", - "properties": { - "alloc_objects": { - "properties": { - "count": { - "type": "long" - } - } - }, - "alloc_space": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "cpu": { - "properties": { - "ns": { - "type": "long" - } - } - }, - "duration": { - "type": "long" - }, - "inuse_objects": { - "properties": { - "count": { - "type": "long" - } - } - }, - "inuse_space": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "samples": { - "properties": { - "count": { - "type": "long" - } - } - }, - "stack": { - "dynamic": "false", - "properties": { - "filename": { - "ignore_above": 1024, - "type": "keyword" - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "line": { - "type": "long" - } - } - }, - "top": { - "dynamic": "false", - "properties": { - "filename": { - "ignore_above": 1024, - "type": "keyword" - }, - "function": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "line": { - "type": "long" - } - } - } - } - }, - "registry": { - "properties": { - "data": { - "properties": { - "bytes": { - "ignore_above": 1024, - "type": "keyword" - }, - "strings": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hive": { - "ignore_above": 1024, - "type": "keyword" - }, - "key": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "value": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "related": { - "properties": { - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "ip": { - "type": "ip" - }, - "user": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "rule": { - "properties": { - "author": { - "ignore_above": 1024, - "type": "keyword" - }, - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "license": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "ruleset": { - "ignore_above": 1024, - "type": "keyword" - }, - "uuid": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "server": { - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "service": { - "dynamic": "false", - "properties": { - "environment": { - "ignore_above": 1024, - "type": "keyword" - }, - "ephemeral_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "framework": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "language": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "node": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "runtime": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "state": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "source": { - "dynamic": "false", - "properties": { - "address": { - "ignore_above": 1024, - "type": "keyword" - }, - "as": { - "properties": { - "number": { - "type": "long" - }, - "organization": { - "properties": { - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "bytes": { - "type": "long" - }, - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "geo": { - "properties": { - "city_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "continent_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "country_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "location": { - "type": "geo_point" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_iso_code": { - "ignore_above": 1024, - "type": "keyword" - }, - "region_name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "ip": { - "type": "ip" - }, - "mac": { - "ignore_above": 1024, - "type": "keyword" - }, - "nat": { - "properties": { - "ip": { - "type": "ip" - }, - "port": { - "type": "long" - } - } - }, - "packets": { - "type": "long" - }, - "port": { - "type": "long" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "user": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "sourcemap": { - "dynamic": "false", - "properties": { - "bundle_filepath": { - "ignore_above": 1024, - "type": "keyword" - }, - "service": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "span": { - "dynamic": "false", - "properties": { - "action": { - "ignore_above": 1024, - "type": "keyword" - }, - "db": { - "dynamic": "false", - "properties": { - "link": { - "ignore_above": 1024, - "type": "keyword" - }, - "rows_affected": { - "type": "long" - } - } - }, - "destination": { - "dynamic": "false", - "properties": { - "service": { - "dynamic": "false", - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "resource": { - "ignore_above": 1024, - "type": "keyword" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "duration": { - "properties": { - "us": { - "type": "long" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "message": { - "dynamic": "false", - "properties": { - "age": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "self_time": { - "properties": { - "count": { - "type": "long" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - } - } - }, - "start": { - "properties": { - "us": { - "type": "long" - } - } - }, - "subtype": { - "ignore_above": 1024, - "type": "keyword" - }, - "sync": { - "type": "boolean" - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "system": { - "properties": { - "cpu": { - "properties": { - "total": { - "properties": { - "norm": { - "properties": { - "pct": { - "scaling_factor": 1000, - "type": "scaled_float" - } - } - } - } - } - } - }, - "memory": { - "properties": { - "actual": { - "properties": { - "free": { - "type": "long" - } - } - }, - "total": { - "type": "long" - } - } - }, - "process": { - "properties": { - "cpu": { - "properties": { - "total": { - "properties": { - "norm": { - "properties": { - "pct": { - "scaling_factor": 1000, - "type": "scaled_float" - } - } - } - } - } - } - }, - "memory": { - "properties": { - "rss": { - "properties": { - "bytes": { - "type": "long" - } - } - }, - "size": { - "type": "long" - } - } - } - } - } - } - }, - "tags": { - "ignore_above": 1024, - "type": "keyword" - }, - "threat": { - "properties": { - "framework": { - "ignore_above": 1024, - "type": "keyword" - }, - "tactic": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "technique": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "timeseries": { - "properties": { - "instance": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "timestamp": { - "properties": { - "us": { - "type": "long" - } - } - }, - "tls": { - "properties": { - "cipher": { - "ignore_above": 1024, - "type": "keyword" - }, - "client": { - "properties": { - "certificate": { - "ignore_above": 1024, - "type": "keyword" - }, - "certificate_chain": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "issuer": { - "ignore_above": 1024, - "type": "keyword" - }, - "ja3": { - "ignore_above": 1024, - "type": "keyword" - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "server_name": { - "ignore_above": 1024, - "type": "keyword" - }, - "subject": { - "ignore_above": 1024, - "type": "keyword" - }, - "supported_ciphers": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "curve": { - "ignore_above": 1024, - "type": "keyword" - }, - "established": { - "type": "boolean" - }, - "next_protocol": { - "ignore_above": 1024, - "type": "keyword" - }, - "resumed": { - "type": "boolean" - }, - "server": { - "properties": { - "certificate": { - "ignore_above": 1024, - "type": "keyword" - }, - "certificate_chain": { - "ignore_above": 1024, - "type": "keyword" - }, - "hash": { - "properties": { - "md5": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha1": { - "ignore_above": 1024, - "type": "keyword" - }, - "sha256": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "issuer": { - "ignore_above": 1024, - "type": "keyword" - }, - "ja3s": { - "ignore_above": 1024, - "type": "keyword" - }, - "not_after": { - "type": "date" - }, - "not_before": { - "type": "date" - }, - "subject": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - }, - "version_protocol": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "trace": { - "dynamic": "false", - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "tracing": { - "properties": { - "trace": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "transaction": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "transaction": { - "dynamic": "false", - "properties": { - "breakdown": { - "properties": { - "count": { - "type": "long" - } - } - }, - "duration": { - "properties": { - "count": { - "type": "long" - }, - "histogram": { - "type": "histogram" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - }, - "us": { - "type": "long" - } - } - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "marks": { - "dynamic": "true", - "properties": { - "*": { - "properties": { - "*": { - "dynamic": "true", - "type": "object" - } - } - }, - "agent": { - "properties": { - "domComplete": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "domInteractive": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "firstContentfulPaint": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "largestContentfulPaint": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "timeToFirstByte": { - "scaling_factor": 1000000, - "type": "scaled_float" - } - } - }, - "navigationTiming": { - "properties": { - "connectEnd": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "connectStart": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "domComplete": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "domContentLoadedEventEnd": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "domContentLoadedEventStart": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "domInteractive": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "domLoading": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "domainLookupEnd": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "domainLookupStart": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "fetchStart": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "loadEventEnd": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "loadEventStart": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "requestStart": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "responseEnd": { - "scaling_factor": 1000000, - "type": "scaled_float" - }, - "responseStart": { - "scaling_factor": 1000000, - "type": "scaled_float" - } - } - } - } - }, - "message": { - "dynamic": "false", - "properties": { - "age": { - "properties": { - "ms": { - "type": "long" - } - } - }, - "queue": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "result": { - "ignore_above": 1024, - "type": "keyword" - }, - "root": { - "type": "boolean" - }, - "sampled": { - "type": "boolean" - }, - "self_time": { - "properties": { - "count": { - "type": "long" - }, - "sum": { - "properties": { - "us": { - "type": "long" - } - } - } - } - }, - "span_count": { - "properties": { - "dropped": { - "type": "long" - } - } - }, - "type": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "url": { - "dynamic": "false", - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "extension": { - "ignore_above": 1024, - "type": "keyword" - }, - "fragment": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "password": { - "ignore_above": 1024, - "type": "keyword" - }, - "path": { - "ignore_above": 1024, - "type": "keyword" - }, - "port": { - "type": "long" - }, - "query": { - "ignore_above": 1024, - "type": "keyword" - }, - "registered_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "scheme": { - "ignore_above": 1024, - "type": "keyword" - }, - "top_level_domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "username": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user": { - "dynamic": "false", - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "email": { - "ignore_above": 1024, - "type": "keyword" - }, - "full_name": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "group": { - "properties": { - "domain": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "hash": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "user_agent": { - "dynamic": "false", - "properties": { - "device": { - "properties": { - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "original": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "os": { - "properties": { - "family": { - "ignore_above": 1024, - "type": "keyword" - }, - "full": { - "ignore_above": 1024, - "type": "keyword" - }, - "kernel": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - }, - "platform": { - "ignore_above": 1024, - "type": "keyword" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "view spans": { - "ignore_above": 1024, - "type": "keyword" - }, - "vlan": { - "properties": { - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "name": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "vulnerability": { - "properties": { - "category": { - "ignore_above": 1024, - "type": "keyword" - }, - "classification": { - "ignore_above": 1024, - "type": "keyword" - }, - "description": { - "fields": { - "text": { - "norms": false, - "type": "text" - } - }, - "ignore_above": 1024, - "type": "keyword" - }, - "enumeration": { - "ignore_above": 1024, - "type": "keyword" - }, - "id": { - "ignore_above": 1024, - "type": "keyword" - }, - "reference": { - "ignore_above": 1024, - "type": "keyword" - }, - "report_id": { - "ignore_above": 1024, - "type": "keyword" - }, - "scanner": { - "properties": { - "vendor": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "score": { - "properties": { - "base": { - "type": "float" - }, - "environmental": { - "type": "float" - }, - "temporal": { - "type": "float" - }, - "version": { - "ignore_above": 1024, - "type": "keyword" - } - } - }, - "severity": { - "ignore_above": 1024, - "type": "keyword" - } - } - } - } - }, - "settings": { - "index": { - "codec": "best_compression", - "lifecycle": { - "name": "apm-rollover-30-days", - "rollover_alias": "apm-8.0.0-transaction" - }, - "mapping": { - "total_fields": { - "limit": "2000" - } - }, - "number_of_replicas": "0", - "number_of_shards": "1", - "priority": "100", - "query": { - "default_field": [ - "message", - "tags", - "agent.ephemeral_id", - "agent.id", - "agent.name", - "agent.type", - "agent.version", - "as.organization.name", - "client.address", - "client.as.organization.name", - "client.domain", - "client.geo.city_name", - "client.geo.continent_name", - "client.geo.country_iso_code", - "client.geo.country_name", - "client.geo.name", - "client.geo.region_iso_code", - "client.geo.region_name", - "client.mac", - "client.registered_domain", - "client.top_level_domain", - "client.user.domain", - "client.user.email", - "client.user.full_name", - "client.user.group.domain", - "client.user.group.id", - "client.user.group.name", - "client.user.hash", - "client.user.id", - "client.user.name", - "cloud.account.id", - "cloud.availability_zone", - "cloud.instance.id", - "cloud.instance.name", - "cloud.machine.type", - "cloud.provider", - "cloud.region", - "container.id", - "container.image.name", - "container.image.tag", - "container.name", - "container.runtime", - "destination.address", - "destination.as.organization.name", - "destination.domain", - "destination.geo.city_name", - "destination.geo.continent_name", - "destination.geo.country_iso_code", - "destination.geo.country_name", - "destination.geo.name", - "destination.geo.region_iso_code", - "destination.geo.region_name", - "destination.mac", - "destination.registered_domain", - "destination.top_level_domain", - "destination.user.domain", - "destination.user.email", - "destination.user.full_name", - "destination.user.group.domain", - "destination.user.group.id", - "destination.user.group.name", - "destination.user.hash", - "destination.user.id", - "destination.user.name", - "dns.answers.class", - "dns.answers.data", - "dns.answers.name", - "dns.answers.type", - "dns.header_flags", - "dns.id", - "dns.op_code", - "dns.question.class", - "dns.question.name", - "dns.question.registered_domain", - "dns.question.subdomain", - "dns.question.top_level_domain", - "dns.question.type", - "dns.response_code", - "dns.type", - "ecs.version", - "error.code", - "error.id", - "error.message", - "error.stack_trace", - "error.type", - "event.action", - "event.category", - "event.code", - "event.dataset", - "event.hash", - "event.id", - "event.kind", - "event.module", - "event.original", - "event.outcome", - "event.provider", - "event.timezone", - "event.type", - "file.device", - "file.directory", - "file.extension", - "file.gid", - "file.group", - "file.hash.md5", - "file.hash.sha1", - "file.hash.sha256", - "file.hash.sha512", - "file.inode", - "file.mode", - "file.name", - "file.owner", - "file.path", - "file.target_path", - "file.type", - "file.uid", - "geo.city_name", - "geo.continent_name", - "geo.country_iso_code", - "geo.country_name", - "geo.name", - "geo.region_iso_code", - "geo.region_name", - "group.domain", - "group.id", - "group.name", - "hash.md5", - "hash.sha1", - "hash.sha256", - "hash.sha512", - "host.architecture", - "host.geo.city_name", - "host.geo.continent_name", - "host.geo.country_iso_code", - "host.geo.country_name", - "host.geo.name", - "host.geo.region_iso_code", - "host.geo.region_name", - "host.hostname", - "host.id", - "host.mac", - "host.name", - "host.os.family", - "host.os.full", - "host.os.kernel", - "host.os.name", - "host.os.platform", - "host.os.version", - "host.type", - "host.user.domain", - "host.user.email", - "host.user.full_name", - "host.user.group.domain", - "host.user.group.id", - "host.user.group.name", - "host.user.hash", - "host.user.id", - "host.user.name", - "http.request.body.content", - "http.request.method", - "http.request.referrer", - "http.response.body.content", - "http.version", - "log.level", - "log.logger", - "log.origin.file.name", - "log.origin.function", - "log.original", - "log.syslog.facility.name", - "log.syslog.severity.name", - "network.application", - "network.community_id", - "network.direction", - "network.iana_number", - "network.name", - "network.protocol", - "network.transport", - "network.type", - "observer.geo.city_name", - "observer.geo.continent_name", - "observer.geo.country_iso_code", - "observer.geo.country_name", - "observer.geo.name", - "observer.geo.region_iso_code", - "observer.geo.region_name", - "observer.hostname", - "observer.mac", - "observer.name", - "observer.os.family", - "observer.os.full", - "observer.os.kernel", - "observer.os.name", - "observer.os.platform", - "observer.os.version", - "observer.product", - "observer.serial_number", - "observer.type", - "observer.vendor", - "observer.version", - "organization.id", - "organization.name", - "os.family", - "os.full", - "os.kernel", - "os.name", - "os.platform", - "os.version", - "package.architecture", - "package.checksum", - "package.description", - "package.install_scope", - "package.license", - "package.name", - "package.path", - "package.version", - "process.args", - "text", - "process.executable", - "process.hash.md5", - "process.hash.sha1", - "process.hash.sha256", - "process.hash.sha512", - "process.name", - "text", - "text", - "text", - "text", - "text", - "process.thread.name", - "process.title", - "process.working_directory", - "server.address", - "server.as.organization.name", - "server.domain", - "server.geo.city_name", - "server.geo.continent_name", - "server.geo.country_iso_code", - "server.geo.country_name", - "server.geo.name", - "server.geo.region_iso_code", - "server.geo.region_name", - "server.mac", - "server.registered_domain", - "server.top_level_domain", - "server.user.domain", - "server.user.email", - "server.user.full_name", - "server.user.group.domain", - "server.user.group.id", - "server.user.group.name", - "server.user.hash", - "server.user.id", - "server.user.name", - "service.ephemeral_id", - "service.id", - "service.name", - "service.node.name", - "service.state", - "service.type", - "service.version", - "source.address", - "source.as.organization.name", - "source.domain", - "source.geo.city_name", - "source.geo.continent_name", - "source.geo.country_iso_code", - "source.geo.country_name", - "source.geo.name", - "source.geo.region_iso_code", - "source.geo.region_name", - "source.mac", - "source.registered_domain", - "source.top_level_domain", - "source.user.domain", - "source.user.email", - "source.user.full_name", - "source.user.group.domain", - "source.user.group.id", - "source.user.group.name", - "source.user.hash", - "source.user.id", - "source.user.name", - "threat.framework", - "threat.tactic.id", - "threat.tactic.name", - "threat.tactic.reference", - "threat.technique.id", - "threat.technique.name", - "threat.technique.reference", - "tracing.trace.id", - "tracing.transaction.id", - "url.domain", - "url.extension", - "url.fragment", - "url.full", - "url.original", - "url.password", - "url.path", - "url.query", - "url.registered_domain", - "url.scheme", - "url.top_level_domain", - "url.username", - "user.domain", - "user.email", - "user.full_name", - "user.group.domain", - "user.group.id", - "user.group.name", - "user.hash", - "user.id", - "user.name", - "user_agent.device.name", - "user_agent.name", - "text", - "user_agent.original", - "user_agent.os.family", - "user_agent.os.full", - "user_agent.os.kernel", - "user_agent.os.name", - "user_agent.os.platform", - "user_agent.os.version", - "user_agent.version", - "text", - "timeseries.instance", - "cloud.project.id", - "cloud.image.id", - "host.os.build", - "host.os.codename", - "kubernetes.pod.name", - "kubernetes.pod.uid", - "kubernetes.namespace", - "kubernetes.node.name", - "kubernetes.replicaset.name", - "kubernetes.deployment.name", - "kubernetes.statefulset.name", - "kubernetes.container.name", - "kubernetes.container.image", - "processor.name", - "processor.event", - "url.scheme", - "url.full", - "url.domain", - "url.path", - "url.query", - "url.fragment", - "http.version", - "http.request.method", - "http.request.referrer", - "service.name", - "service.version", - "service.environment", - "service.node.name", - "service.language.name", - "service.language.version", - "service.runtime.name", - "service.runtime.version", - "service.framework.name", - "service.framework.version", - "transaction.id", - "transaction.type", - "text", - "transaction.name", - "span.type", - "span.subtype", - "trace.id", - "parent.id", - "agent.name", - "agent.version", - "agent.ephemeral_id", - "container.id", - "kubernetes.namespace", - "kubernetes.node.name", - "kubernetes.pod.name", - "kubernetes.pod.uid", - "host.architecture", - "host.hostname", - "host.name", - "host.os.platform", - "process.args", - "process.title", - "observer.listening", - "observer.hostname", - "observer.version", - "observer.type", - "user.name", - "user.id", - "user.email", - "destination.address", - "text", - "user_agent.original", - "user_agent.name", - "user_agent.version", - "user_agent.device.name", - "user_agent.os.platform", - "user_agent.os.name", - "user_agent.os.full", - "user_agent.os.family", - "user_agent.os.version", - "user_agent.os.kernel", - "cloud.account.id", - "cloud.account.name", - "cloud.availability_zone", - "cloud.instance.id", - "cloud.instance.name", - "cloud.machine.type", - "cloud.project.id", - "cloud.project.name", - "cloud.provider", - "cloud.region", - "error.id", - "error.culprit", - "error.grouping_key", - "error.exception.code", - "error.exception.message", - "error.exception.module", - "error.exception.type", - "error.log.level", - "error.log.logger_name", - "error.log.message", - "error.log.param_message", - "profile.top.id", - "profile.top.function", - "profile.top.filename", - "profile.stack.id", - "profile.stack.function", - "profile.stack.filename", - "sourcemap.service.name", - "sourcemap.service.version", - "sourcemap.bundle_filepath", - "view spans", - "child.id", - "span.id", - "span.name", - "span.action", - "span.db.link", - "span.destination.service.type", - "span.destination.service.name", - "span.destination.service.resource", - "span.message.queue.name", - "transaction.result", - "transaction.message.queue.name", - "fields.*" - ] - }, - "refresh_interval": "1ms" - } - } - } -} \ No newline at end of file diff --git a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/ml_8.0.0/data.json.gz b/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/ml_8.0.0/data.json.gz deleted file mode 100644 index d23b9e51d734d..0000000000000 Binary files a/x-pack/test/apm_api_integration/trial/fixtures/es_archiver/ml_8.0.0/data.json.gz and /dev/null differ diff --git a/x-pack/test/apm_api_integration/trial/tests/index.ts b/x-pack/test/apm_api_integration/trial/tests/index.ts index 1b3b5602445ed..c5ca086b5f370 100644 --- a/x-pack/test/apm_api_integration/trial/tests/index.ts +++ b/x-pack/test/apm_api_integration/trial/tests/index.ts @@ -5,14 +5,18 @@ */ import { FtrProviderContext } from '../../../api_integration/ftr_provider_context'; +import { registerMochaHooksForSnapshots } from '../../common/match_snapshot'; export default function observabilityApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('APM specs (trial)', function () { this.tags('ciGroup1'); + registerMochaHooksForSnapshots(); + describe('Services', function () { loadTestFile(require.resolve('./services/annotations')); loadTestFile(require.resolve('./services/rum_services.ts')); + loadTestFile(require.resolve('./services/top_services.ts')); }); describe('Settings', function () { diff --git a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts index aadff2c31a17b..f799d80f6ef13 100644 --- a/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts +++ b/x-pack/test/apm_api_integration/trial/tests/service_maps/service_maps.ts @@ -6,6 +6,8 @@ import querystring from 'querystring'; import expect from '@kbn/expect'; +import { isEmpty } from 'lodash'; +import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function serviceMapsApiTests({ getService }: FtrProviderContext) { @@ -21,7 +23,7 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) ); expect(response.status).to.be(200); - expect(response.body).to.eql({ elements: [] }); + expect(response.body.elements.length).to.be(0); }); }); @@ -36,227 +38,229 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) expect(response.status).to.be(200); - expect(response.body).to.eql({ - elements: [ - { - data: { - source: 'client', - target: 'opbeans-node', - id: 'client~opbeans-node', - sourceData: { - id: 'client', - 'service.name': 'client', - 'agent.name': 'rum-js', - }, - targetData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', + expectSnapshot(response.body).toMatchInline(` + Object { + "elements": Array [ + Object { + "data": Object { + "id": "client~opbeans-node", + "source": "client", + "sourceData": Object { + "agent.name": "rum-js", + "id": "client", + "service.name": "client", + }, + "target": "opbeans-node", + "targetData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "production", + "service.name": "opbeans-node", + }, }, }, - }, - { - data: { - source: 'opbeans-java', - target: '>opbeans-java:3000', - id: 'opbeans-java~>opbeans-java:3000', - sourceData: { - id: 'opbeans-java', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'agent.name': 'java', - }, - targetData: { - 'span.subtype': 'http', - 'span.destination.service.resource': 'opbeans-java:3000', - 'span.type': 'external', - id: '>opbeans-java:3000', - label: 'opbeans-java:3000', + Object { + "data": Object { + "id": "opbeans-java~>opbeans-java:3000", + "source": "opbeans-java", + "sourceData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + }, + "target": ">opbeans-java:3000", + "targetData": Object { + "id": ">opbeans-java:3000", + "label": "opbeans-java:3000", + "span.destination.service.resource": "opbeans-java:3000", + "span.subtype": "http", + "span.type": "external", + }, }, }, - }, - { - data: { - source: 'opbeans-java', - target: '>postgresql', - id: 'opbeans-java~>postgresql', - sourceData: { - id: 'opbeans-java', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'agent.name': 'java', - }, - targetData: { - 'span.subtype': 'postgresql', - 'span.destination.service.resource': 'postgresql', - 'span.type': 'db', - id: '>postgresql', - label: 'postgresql', + Object { + "data": Object { + "id": "opbeans-java~>postgresql", + "source": "opbeans-java", + "sourceData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + }, + "target": ">postgresql", + "targetData": Object { + "id": ">postgresql", + "label": "postgresql", + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, }, }, - }, - { - data: { - source: 'opbeans-java', - target: 'opbeans-node', - id: 'opbeans-java~opbeans-node', - sourceData: { - id: 'opbeans-java', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'agent.name': 'java', - }, - targetData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', + Object { + "data": Object { + "bidirectional": true, + "id": "opbeans-java~opbeans-node", + "source": "opbeans-java", + "sourceData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + }, + "target": "opbeans-node", + "targetData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "production", + "service.name": "opbeans-node", + }, }, - bidirectional: true, }, - }, - { - data: { - source: 'opbeans-node', - target: '>93.184.216.34:80', - id: 'opbeans-node~>93.184.216.34:80', - sourceData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', - }, - targetData: { - 'span.subtype': 'http', - 'span.destination.service.resource': '93.184.216.34:80', - 'span.type': 'external', - id: '>93.184.216.34:80', - label: '93.184.216.34:80', + Object { + "data": Object { + "id": "opbeans-node~>93.184.216.34:80", + "source": "opbeans-node", + "sourceData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "production", + "service.name": "opbeans-node", + }, + "target": ">93.184.216.34:80", + "targetData": Object { + "id": ">93.184.216.34:80", + "label": "93.184.216.34:80", + "span.destination.service.resource": "93.184.216.34:80", + "span.subtype": "http", + "span.type": "external", + }, }, }, - }, - { - data: { - source: 'opbeans-node', - target: '>postgresql', - id: 'opbeans-node~>postgresql', - sourceData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', - }, - targetData: { - 'span.subtype': 'postgresql', - 'span.destination.service.resource': 'postgresql', - 'span.type': 'db', - id: '>postgresql', - label: 'postgresql', + Object { + "data": Object { + "id": "opbeans-node~>postgresql", + "source": "opbeans-node", + "sourceData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "production", + "service.name": "opbeans-node", + }, + "target": ">postgresql", + "targetData": Object { + "id": ">postgresql", + "label": "postgresql", + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, }, }, - }, - { - data: { - source: 'opbeans-node', - target: '>redis', - id: 'opbeans-node~>redis', - sourceData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', - }, - targetData: { - 'span.subtype': 'redis', - 'span.destination.service.resource': 'redis', - 'span.type': 'cache', - id: '>redis', - label: 'redis', + Object { + "data": Object { + "id": "opbeans-node~>redis", + "source": "opbeans-node", + "sourceData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "production", + "service.name": "opbeans-node", + }, + "target": ">redis", + "targetData": Object { + "id": ">redis", + "label": "redis", + "span.destination.service.resource": "redis", + "span.subtype": "redis", + "span.type": "cache", + }, }, }, - }, - { - data: { - source: 'opbeans-node', - target: 'opbeans-java', - id: 'opbeans-node~opbeans-java', - sourceData: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', - }, - targetData: { - id: 'opbeans-java', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'agent.name': 'java', + Object { + "data": Object { + "id": "opbeans-node~opbeans-java", + "isInverseEdge": true, + "source": "opbeans-node", + "sourceData": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "production", + "service.name": "opbeans-node", + }, + "target": "opbeans-java", + "targetData": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + }, }, - isInverseEdge: true, }, - }, - { - data: { - id: 'opbeans-java', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'agent.name': 'java', + Object { + "data": Object { + "agent.name": "java", + "id": "opbeans-java", + "service.environment": "production", + "service.name": "opbeans-java", + }, }, - }, - { - data: { - id: 'opbeans-node', - 'service.environment': 'production', - 'service.name': 'opbeans-node', - 'agent.name': 'nodejs', + Object { + "data": Object { + "agent.name": "nodejs", + "id": "opbeans-node", + "service.environment": "production", + "service.name": "opbeans-node", + }, }, - }, - { - data: { - 'span.subtype': 'http', - 'span.destination.service.resource': 'opbeans-java:3000', - 'span.type': 'external', - id: '>opbeans-java:3000', - label: 'opbeans-java:3000', + Object { + "data": Object { + "id": ">opbeans-java:3000", + "label": "opbeans-java:3000", + "span.destination.service.resource": "opbeans-java:3000", + "span.subtype": "http", + "span.type": "external", + }, }, - }, - { - data: { - id: 'client', - 'service.name': 'client', - 'agent.name': 'rum-js', + Object { + "data": Object { + "agent.name": "rum-js", + "id": "client", + "service.name": "client", + }, }, - }, - { - data: { - 'span.subtype': 'redis', - 'span.destination.service.resource': 'redis', - 'span.type': 'cache', - id: '>redis', - label: 'redis', + Object { + "data": Object { + "id": ">redis", + "label": "redis", + "span.destination.service.resource": "redis", + "span.subtype": "redis", + "span.type": "cache", + }, }, - }, - { - data: { - 'span.subtype': 'postgresql', - 'span.destination.service.resource': 'postgresql', - 'span.type': 'db', - id: '>postgresql', - label: 'postgresql', + Object { + "data": Object { + "id": ">postgresql", + "label": "postgresql", + "span.destination.service.resource": "postgresql", + "span.subtype": "postgresql", + "span.type": "db", + }, }, - }, - { - data: { - 'span.subtype': 'http', - 'span.destination.service.resource': '93.184.216.34:80', - 'span.type': 'external', - id: '>93.184.216.34:80', - label: '93.184.216.34:80', + Object { + "data": Object { + "id": ">93.184.216.34:80", + "label": "93.184.216.34:80", + "span.destination.service.resource": "93.184.216.34:80", + "span.subtype": "http", + "span.type": "external", + }, }, - }, - ], - }); + ], + } + `); }); }); }); @@ -287,34 +291,25 @@ export default function serviceMapsApiTests({ getService }: FtrProviderContext) }); describe('when there is data with anomalies', () => { - before(() => esArchiver.load('ml_8.0.0')); - after(() => esArchiver.unload('ml_8.0.0')); + before(() => esArchiver.load('apm_8.0.0')); + after(() => esArchiver.unload('apm_8.0.0')); it('returns service map elements', async () => { - const response = await supertest.get( - '/api/apm/service-map?start=2020-08-26T11%3A00%3A00.000Z&end=2020-08-26T11%3A30%3A00.000Z' - ); + const start = encodeURIComponent('2020-09-10T06:00:00.000Z'); + const end = encodeURIComponent('2020-09-10T07:00:00.000Z'); + + const response = await supertest.get(`/api/apm/service-map?start=${start}&end=${end}`); expect(response.status).to.be(200); - const opbeansJavaWithAnomaly = response.body.elements.filter( - (el: { data: { id: string } }) => el.data.id === 'opbeans-java' + const dataWithAnomalies = response.body.elements.filter( + (el: { data: { serviceAnomalyStats?: {} } }) => !isEmpty(el.data.serviceAnomalyStats) ); - expect(opbeansJavaWithAnomaly).to.eql([ - { - data: { - id: 'opbeans-java', - 'service.environment': 'production', - 'service.name': 'opbeans-java', - 'agent.name': 'java', - serviceAnomalyStats: { - transactionType: 'request', - anomalyScore: 0.21359169006333134, - actualValue: 1526662.1320754716, - jobId: 'apm-production-229a-high_mean_transaction_duration', - }, - }, - }, - ]); + expect(dataWithAnomalies).to.not.empty(); + dataWithAnomalies.forEach(({ data }: any) => { + expect( + Object.values(data.serviceAnomalyStats).filter((value) => isEmpty(value)) + ).to.not.empty(); + }); }); }); }); diff --git a/x-pack/test/apm_api_integration/trial/tests/services/rum_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/rum_services.ts index 78171a65a11fd..088488bc143fd 100644 --- a/x-pack/test/apm_api_integration/trial/tests/services/rum_services.ts +++ b/x-pack/test/apm_api_integration/trial/tests/services/rum_services.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; import { FtrProviderContext } from '../../../common/ftr_provider_context'; export default function rumServicesApiTests({ getService }: FtrProviderContext) { @@ -40,7 +41,12 @@ export default function rumServicesApiTests({ getService }: FtrProviderContext) expect(response.status).to.be(200); - expect(response.body).to.eql(['client', 'opbean-client-rum']); + expectSnapshot(response.body).toMatchInline(` + Array [ + "client", + "opbean-client-rum", + ] + `); }); }); }); diff --git a/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts new file mode 100644 index 0000000000000..76af02ec1606e --- /dev/null +++ b/x-pack/test/apm_api_integration/trial/tests/services/top_services.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import expect from '@kbn/expect'; +import { expectSnapshot } from '../../../common/match_snapshot'; +import { PromiseReturnType } from '../../../../../plugins/apm/typings/common'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import archives_metadata from '../../../common/archives_metadata'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const esArchiver = getService('esArchiver'); + + const archiveName = 'apm_8.0.0'; + + const range = archives_metadata[archiveName]; + + // url parameters + const start = encodeURIComponent(range.start); + const end = encodeURIComponent(range.end); + + const uiFilters = encodeURIComponent(JSON.stringify({})); + + describe('APM Services Overview', () => { + describe('when data is loaded', () => { + before(() => esArchiver.load(archiveName)); + after(() => esArchiver.unload(archiveName)); + + describe('and fetching a list of services', () => { + let response: PromiseReturnType; + before(async () => { + response = await supertest.get( + `/api/apm/services?start=${start}&end=${end}&uiFilters=${uiFilters}` + ); + }); + + it('the response is successful', () => { + expect(response.status).to.eql(200); + }); + + it('there is at least one service', () => { + expect(response.body.items.length).to.be.greaterThan(0); + }); + + it('some items have severity set', () => { + // Under the assumption that the loaded archive has + // at least one APM ML job, and the time range is longer + // than 15m, at least one items should have severity set. + // Note that we currently have a bug where healthy services + // report as unknown (so without any severity status): + // https://github.com/elastic/kibana/issues/77083 + + const severityScores = response.body.items.map((item: any) => item.severity); + + expect(severityScores.filter(Boolean).length).to.be.greaterThan(0); + + expectSnapshot(severityScores).toMatchInline(` + Array [ + undefined, + undefined, + undefined, + undefined, + undefined, + "warning", + undefined, + ] + `); + }); + }); + }); + }); +} diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts index 39cd578917ba2..8c3ed246adba0 100644 --- a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts +++ b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/no_access_user.ts @@ -25,14 +25,16 @@ export default function apiTest({ getService }: FtrProviderContext) { describe('when calling the endpoint for listing jobs', () => { it('returns an error because the user does not have access', async () => { const { body } = await getJobs(); - expect(body).to.eql({ statusCode: 404, error: 'Not Found', message: 'Not Found' }); + expect(body.statusCode).to.be(404); + expect(body.error).to.be('Not Found'); }); }); describe('when calling create endpoint', () => { it('returns an error because the user does not have access', async () => { const { body } = await createJobs(['production', 'staging']); - expect(body).to.eql({ statusCode: 404, error: 'Not Found', message: 'Not Found' }); + expect(body.statusCode).to.be(404); + expect(body.error).to.be('Not Found'); }); }); }); diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts index 6ea0124e5ee8e..d158ed847fbb7 100644 --- a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts +++ b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/read_user.ts @@ -25,17 +25,18 @@ export default function apiTest({ getService }: FtrProviderContext) { describe('when calling the endpoint for listing jobs', () => { it('returns a list of jobs', async () => { const { body } = await getJobs(); - expect(body).to.eql({ - jobs: [], - hasLegacyJobs: false, - }); + + expect(body.jobs.length).to.be(0); + expect(body.hasLegacyJobs).to.be(false); }); }); describe('when calling create endpoint', () => { it('returns an error because the user does not have access', async () => { const { body } = await createJobs(['production', 'staging']); - expect(body).to.eql({ statusCode: 404, error: 'Not Found', message: 'Not Found' }); + + expect(body.statusCode).to.be(404); + expect(body.error).to.be('Not Found'); }); }); }); diff --git a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts index 56a2d5dc0f662..d257fe1dd0b00 100644 --- a/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts +++ b/x-pack/test/apm_api_integration/trial/tests/settings/anomaly_detection/write_user.ts @@ -35,7 +35,8 @@ export default function apiTest({ getService }: FtrProviderContext) { describe('when calling the endpoint for listing jobs', () => { it('returns a list of jobs', async () => { const { body } = await getJobs(); - expect(body).to.eql({ jobs: [], hasLegacyJobs: false }); + expect(body.jobs.length).to.be(0); + expect(body.hasLegacyJobs).to.be(false); }); }); diff --git a/x-pack/test/case_api_integration/common/lib/utils.ts b/x-pack/test/case_api_integration/common/lib/utils.ts index fb6f4fce3c29a..41f92d022f06c 100644 --- a/x-pack/test/case_api_integration/common/lib/utils.ts +++ b/x-pack/test/case_api_integration/common/lib/utils.ts @@ -66,7 +66,7 @@ export const getJiraConnector = () => ({ config: { apiUrl: 'http://some.non.existent.com', projectKey: 'pkey', - casesConfiguration: { + incidentConfiguration: { mapping: [ { source: 'title', @@ -85,6 +85,7 @@ export const getJiraConnector = () => ({ }, ], }, + isCaseOwned: true, }, }); @@ -98,7 +99,7 @@ export const getResilientConnector = () => ({ config: { apiUrl: 'http://some.non.existent.com', orgId: 'pkey', - casesConfiguration: { + incidentConfiguration: { mapping: [ { source: 'title', @@ -117,6 +118,7 @@ export const getResilientConnector = () => ({ }, ], }, + isCaseOwned: true, }, }); diff --git a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts index 5b0d28bf09508..ac4a1298e28b9 100644 --- a/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts +++ b/x-pack/test/functional/apps/advanced_settings/feature_controls/advanced_settings_security.ts @@ -10,7 +10,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const security = getService('security'); - const config = getService('config'); const PageObjects = getPageObjects(['common', 'settings', 'security', 'spaceSelector']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); @@ -174,20 +173,18 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await security.user.delete('no_advanced_settings_privileges_user'); }); - it('shows Management navlink', async () => { + it('does not show Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover']); }); - it(`does not allow navigation to advanced settings; redirects to management home`, async () => { + it(`does not allow navigation to advanced settings; shows "not found" error`, async () => { await PageObjects.common.navigateToUrl('management', 'kibana/settings', { ensureCurrentUrl: false, shouldLoginIfPrompted: false, shouldUseHashForSubUrl: false, }); - await testSubjects.existOrFail('managementHome', { - timeout: config.get('timeouts.waitFor'), - }); + await testSubjects.existOrFail('appNotFoundPageContent'); }); }); }); diff --git a/x-pack/test/functional/apps/api_keys/feature_controls/api_keys_security.ts b/x-pack/test/functional/apps/api_keys/feature_controls/api_keys_security.ts new file mode 100644 index 0000000000000..d3d2846082854 --- /dev/null +++ b/x-pack/test/functional/apps/api_keys/feature_controls/api_keys_security.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Security" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with manage_security', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'manage_security'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Security" section with API Keys', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(1); + expect(sections[0]).to.eql({ + sectionId: 'security', + sectionLinks: ['users', 'roles', 'api_keys', 'role_mappings'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/api_keys/feature_controls/index.ts b/x-pack/test/functional/apps/api_keys/feature_controls/index.ts new file mode 100644 index 0000000000000..169b5c7fb0a73 --- /dev/null +++ b/x-pack/test/functional/apps/api_keys/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./api_keys_security')); + }); +} diff --git a/x-pack/test/functional/apps/api_keys/home_page.ts b/x-pack/test/functional/apps/api_keys/home_page.ts index 0c4097a1d5c4e..39d8449218ffa 100644 --- a/x-pack/test/functional/apps/api_keys/home_page.ts +++ b/x-pack/test/functional/apps/api_keys/home_page.ts @@ -24,10 +24,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); // https://www.elastic.co/guide/en/kibana/7.6/api-keys.html#api-keys-security-privileges - it('Shows required privileges ', async () => { - log.debug('Checking for required privileges method section header'); - const message = await pageObjects.apiKeys.apiKeysPermissionDeniedMessage(); - expect(message).to.be('You need permission to manage API keys'); + it('Hides management link if user is not authorized', async () => { + await testSubjects.missingOrFail('apiKeys'); }); it('Loads the app', async () => { diff --git a/x-pack/test/functional/apps/api_keys/index.ts b/x-pack/test/functional/apps/api_keys/index.ts index 703aae04140f2..7a17430dc8f6c 100644 --- a/x-pack/test/functional/apps/api_keys/index.ts +++ b/x-pack/test/functional/apps/api_keys/index.ts @@ -10,5 +10,6 @@ export default ({ loadTestFile }: FtrProviderContext) => { describe('API Keys app', function () { this.tags(['ciGroup7']); loadTestFile(require.resolve('./home_page')); + loadTestFile(require.resolve('./feature_controls')); }); }; diff --git a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts index e9fa4ccf8e48b..5a8fb207d5062 100644 --- a/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts +++ b/x-pack/test/functional/apps/canvas/feature_controls/canvas_security.ts @@ -66,7 +66,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows canvas navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Canvas', 'Stack Management']); + expect(navLinks).to.eql(['Canvas']); }); it(`landing page shows "Create new workpad" button`, async () => { @@ -142,7 +142,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows canvas navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Canvas', 'Stack Management']); + expect(navLinks).to.eql(['Canvas']); }); it(`landing page shows disabled "Create new workpad" button`, async () => { diff --git a/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/ccr_security.ts b/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/ccr_security.ts new file mode 100644 index 0000000000000..6b4b9c61151ba --- /dev/null +++ b/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/ccr_security.ts @@ -0,0 +1,77 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Data" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with ccr_user', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'ccr_user'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Data" section with CCR', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(3); + expect(sections[1]).to.eql({ + sectionId: 'data', + sectionLinks: [ + 'index_management', + 'index_lifecycle_management', + 'snapshot_restore', + 'rollup_jobs', + 'transform', + 'cross_cluster_replication', + 'remote_clusters', + ], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/index.ts b/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/index.ts new file mode 100644 index 0000000000000..e7be2cb48ce3e --- /dev/null +++ b/x-pack/test/functional/apps/cross_cluster_replication/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./ccr_security')); + }); +} diff --git a/x-pack/test/functional/apps/cross_cluster_replication/index.ts b/x-pack/test/functional/apps/cross_cluster_replication/index.ts index 5db6103307af9..0e54c0d1c0d15 100644 --- a/x-pack/test/functional/apps/cross_cluster_replication/index.ts +++ b/x-pack/test/functional/apps/cross_cluster_replication/index.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Cross Cluster Replication app', function () { this.tags(['ciGroup4', 'skipCloud']); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts index 505e35907bd80..46dc0316a5d6b 100644 --- a/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts +++ b/x-pack/test/functional/apps/dashboard/feature_controls/dashboard_security.ts @@ -81,9 +81,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await security.user.delete('global_dashboard_all_user'); }); - it('shows dashboard navlink', async () => { + it('only shows the dashboard navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map((link) => link.text)).to.contain('Dashboard'); + expect(navLinks.map((link) => link.text)).to.eql(['Dashboard']); }); it(`landing page shows "Create new Dashboard" button`, async () => { @@ -287,7 +287,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.contain('Dashboard'); + expect(navLinks).to.eql(['Dashboard']); }); it(`landing page doesn't show "Create new Dashboard" button`, async () => { @@ -415,7 +415,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows dashboard navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.contain('Dashboard'); + expect(navLinks).to.eql(['Dashboard']); }); it(`landing page doesn't show "Create new Dashboard" button`, async () => { diff --git a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts index 803ff6399a035..807ba6ded88a2 100644 --- a/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts +++ b/x-pack/test/functional/apps/dev_tools/feature_controls/dev_tools_security.ts @@ -63,7 +63,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows Dev Tools navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map((link) => link.text)).to.eql(['Dev Tools', 'Stack Management']); + expect(navLinks.map((link) => link.text)).to.eql(['Dev Tools']); }); describe('console', () => { @@ -144,7 +144,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it(`shows 'Dev Tools' navlink`, async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Dev Tools', 'Stack Management']); + expect(navLinks).to.eql(['Dev Tools']); }); describe('console', () => { diff --git a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts index 8be4349762808..d94451d023ec0 100644 --- a/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts +++ b/x-pack/test/functional/apps/discover/feature_controls/discover_security.ts @@ -82,7 +82,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows discover navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map((link) => link.text)).to.eql(['Discover', 'Stack Management']); + expect(navLinks.map((link) => link.text)).to.eql(['Discover']); }); it('shows save button', async () => { @@ -184,7 +184,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows discover navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover']); }); it(`doesn't show save button`, async () => { @@ -275,7 +275,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows discover navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover']); }); it(`doesn't show save button`, async () => { diff --git a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts index 9121028c14404..3b4a1fbdbe0d8 100644 --- a/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts +++ b/x-pack/test/functional/apps/graph/feature_controls/graph_security.ts @@ -64,7 +64,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows graph navlink', async () => { const navLinks = await appsMenu.readLinks(); - expect(navLinks.map((link) => link.text)).to.eql(['Graph', 'Stack Management']); + expect(navLinks.map((link) => link.text)).to.eql(['Graph']); }); it('landing page shows "Create new graph" button', async () => { @@ -127,7 +127,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows graph navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Graph', 'Stack Management']); + expect(navLinks).to.eql(['Graph']); }); it('does not show a "Create new Workspace" button', async () => { diff --git a/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/ilm_security.ts b/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/ilm_security.ts new file mode 100644 index 0000000000000..4cb0d3077aaa4 --- /dev/null +++ b/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/ilm_security.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Data" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with manage_ilm', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'manage_ilm'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Data" section with ILM', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(1); + expect(sections[0]).to.eql({ + sectionId: 'data', + sectionLinks: ['index_lifecycle_management'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/index.ts b/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/index.ts new file mode 100644 index 0000000000000..0bb6476f36687 --- /dev/null +++ b/x-pack/test/functional/apps/index_lifecycle_management/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./ilm_security')); + }); +} diff --git a/x-pack/test/functional/apps/index_lifecycle_management/index.ts b/x-pack/test/functional/apps/index_lifecycle_management/index.ts index f535710814ab2..157fb62b7a84d 100644 --- a/x-pack/test/functional/apps/index_lifecycle_management/index.ts +++ b/x-pack/test/functional/apps/index_lifecycle_management/index.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Index Lifecycle Management app', function () { this.tags('ciGroup7'); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/index_management/feature_controls/index.ts b/x-pack/test/functional/apps/index_management/feature_controls/index.ts new file mode 100644 index 0000000000000..85398a73eceff --- /dev/null +++ b/x-pack/test/functional/apps/index_management/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./index_management_security')); + }); +} diff --git a/x-pack/test/functional/apps/index_management/feature_controls/index_management_security.ts b/x-pack/test/functional/apps/index_management/feature_controls/index_management_security.ts new file mode 100644 index 0000000000000..2019751d9101c --- /dev/null +++ b/x-pack/test/functional/apps/index_management/feature_controls/index_management_security.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Data" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with index_management_user', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'index_management_user'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Data" section with index management', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(1); + expect(sections[0]).to.eql({ + sectionId: 'data', + sectionLinks: ['index_management', 'transform'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/index_management/index.ts b/x-pack/test/functional/apps/index_management/index.ts index a9bb44d002334..97b23cbf82c31 100644 --- a/x-pack/test/functional/apps/index_management/index.ts +++ b/x-pack/test/functional/apps/index_management/index.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Index Management app', function () { this.tags('ciGroup3'); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts index cedd96f147c2b..4873a11d75eaa 100644 --- a/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts +++ b/x-pack/test/functional/apps/index_patterns/feature_controls/index_patterns_security.ts @@ -10,7 +10,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const kibanaServer = getService('kibanaServer'); const security = getService('security'); - const config = getService('config'); const PageObjects = getPageObjects(['common', 'settings', 'security']); const appsMenu = getService('appsMenu'); const testSubjects = getService('testSubjects'); @@ -175,28 +174,17 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { await security.user.delete('no_index_patterns_privileges_user'); }); - it('shows Management navlink', async () => { + it('does not show Management navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Discover', 'Stack Management']); + expect(navLinks).to.eql(['Discover']); }); it(`doesn't show Index Patterns in management side-nav`, async () => { - await PageObjects.settings.navigateTo(); - await testSubjects.existOrFail('managementHome', { - timeout: config.get('timeouts.waitFor'), - }); - await testSubjects.missingOrFail('indexPatterns'); - }); - - it(`does not allow navigation to Index Patterns; redirects to management home`, async () => { - await PageObjects.common.navigateToUrl('management', 'kibana/indexPatterns', { + await PageObjects.common.navigateToActualUrl('management', '', { ensureCurrentUrl: false, shouldLoginIfPrompted: false, - shouldUseHashForSubUrl: false, - }); - await testSubjects.existOrFail('managementHome', { - timeout: config.get('timeouts.waitFor'), }); + await testSubjects.existOrFail('~appNotFoundPageContent'); }); }); }); diff --git a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts index 64154ff6cf3f7..552e948f56a9b 100644 --- a/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts +++ b/x-pack/test/functional/apps/infra/feature_controls/logs_security.ts @@ -58,7 +58,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Logs', 'Stack Management']); + expect(navLinks).to.eql(['Overview', 'Logs']); }); describe('logs landing page without data', () => { @@ -121,7 +121,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows logs navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Overview', 'Logs', 'Stack Management']); + expect(navLinks).to.eql(['Overview', 'Logs']); }); describe('logs landing page without data', () => { diff --git a/x-pack/test/functional/apps/ingest_pipelines/feature_controls/index.ts b/x-pack/test/functional/apps/ingest_pipelines/feature_controls/index.ts new file mode 100644 index 0000000000000..fbaf7648646b8 --- /dev/null +++ b/x-pack/test/functional/apps/ingest_pipelines/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./ingest_pipelines_security')); + }); +} diff --git a/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts b/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts new file mode 100644 index 0000000000000..bf703a8f60dc2 --- /dev/null +++ b/x-pack/test/functional/apps/ingest_pipelines/feature_controls/ingest_pipelines_security.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Ingest" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with ingest_pipelines_user', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'ingest_pipelines_user'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Ingest" section with ingest pipelines', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(1); + expect(sections[0]).to.eql({ + sectionId: 'ingest', + sectionLinks: ['ingest_pipelines'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/ingest_pipelines/index.ts b/x-pack/test/functional/apps/ingest_pipelines/index.ts index 8d2b9ee1dcb69..2a4781c5e216d 100644 --- a/x-pack/test/functional/apps/ingest_pipelines/index.ts +++ b/x-pack/test/functional/apps/ingest_pipelines/index.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Ingest pipelines app', function () { this.tags('ciGroup3'); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./ingest_pipelines')); }); }; diff --git a/x-pack/test/functional/apps/license_management/feature_controls/index.ts b/x-pack/test/functional/apps/license_management/feature_controls/index.ts new file mode 100644 index 0000000000000..5c7c04d4ccde1 --- /dev/null +++ b/x-pack/test/functional/apps/license_management/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./license_management_security')); + }); +} diff --git a/x-pack/test/functional/apps/license_management/feature_controls/license_management_security.ts b/x-pack/test/functional/apps/license_management/feature_controls/license_management_security.ts new file mode 100644 index 0000000000000..59fc287c6cf2e --- /dev/null +++ b/x-pack/test/functional/apps/license_management/feature_controls/license_management_security.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Stack" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with license_management_user', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'license_management_user'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Stack" section with License Management', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(3); + expect(sections[2]).to.eql({ + sectionId: 'stack', + sectionLinks: ['license_management', 'upgrade_assistant'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/license_management/index.ts b/x-pack/test/functional/apps/license_management/index.ts index 6d01b1bb098f0..0b090223c18fe 100644 --- a/x-pack/test/functional/apps/license_management/index.ts +++ b/x-pack/test/functional/apps/license_management/index.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('License app', function () { this.tags('ciGroup7'); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/logstash/feature_controls/index.ts b/x-pack/test/functional/apps/logstash/feature_controls/index.ts new file mode 100644 index 0000000000000..d3cc7fae94d98 --- /dev/null +++ b/x-pack/test/functional/apps/logstash/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./logstash_security')); + }); +} diff --git a/x-pack/test/functional/apps/logstash/feature_controls/logstash_security.ts b/x-pack/test/functional/apps/logstash/feature_controls/logstash_security.ts new file mode 100644 index 0000000000000..8e2609e3b7e85 --- /dev/null +++ b/x-pack/test/functional/apps/logstash/feature_controls/logstash_security.ts @@ -0,0 +1,69 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Ingest" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with logstash_read_user', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'logstash_read_user'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Ingest" section with Logstash Pipelines', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(1); + expect(sections[0]).to.eql({ + sectionId: 'ingest', + sectionLinks: ['pipelines'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/logstash/index.js b/x-pack/test/functional/apps/logstash/index.js index 515674577fb52..3258d948cedfc 100644 --- a/x-pack/test/functional/apps/logstash/index.js +++ b/x-pack/test/functional/apps/logstash/index.js @@ -8,6 +8,7 @@ export default function ({ loadTestFile }) { describe('logstash', function () { this.tags(['ciGroup2']); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./pipeline_list')); loadTestFile(require.resolve('./pipeline_create')); }); diff --git a/x-pack/test/functional/apps/management/feature_controls/index.ts b/x-pack/test/functional/apps/management/feature_controls/index.ts new file mode 100644 index 0000000000000..8b8226da7dc3c --- /dev/null +++ b/x-pack/test/functional/apps/management/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./management_security')); + }); +} diff --git a/x-pack/test/functional/apps/management/feature_controls/management_security.ts b/x-pack/test/functional/apps/management/feature_controls/management_security.ts new file mode 100644 index 0000000000000..cf1a83ca49686 --- /dev/null +++ b/x-pack/test/functional/apps/management/feature_controls/management_security.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + const testSubjects = getService('testSubjects'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('no management privileges', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should not show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.eql(['Dashboard']); + }); + + it('should render the "application not found" view when navigating to management directly', async () => { + await PageObjects.common.navigateToApp('management'); + expect(await testSubjects.exists('appNotFoundPageContent')).to.eql(true); + }); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should only render management entries controllable via Kibana privileges', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(2); + expect(sections[0]).to.eql({ + sectionId: 'insightsAndAlerting', + sectionLinks: ['triggersActions'], + }); + expect(sections[1]).to.eql({ + sectionId: 'kibana', + sectionLinks: ['indexPatterns', 'objects', 'spaces', 'settings'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/management/index.js b/x-pack/test/functional/apps/management/index.ts similarity index 67% rename from x-pack/test/functional/apps/management/index.js rename to x-pack/test/functional/apps/management/index.ts index 19c68a2da9d9b..7a461c9963be9 100644 --- a/x-pack/test/functional/apps/management/index.js +++ b/x-pack/test/functional/apps/management/index.ts @@ -4,10 +4,13 @@ * you may not use this file except in compliance with the Elastic License. */ -export default function ({ loadTestFile }) { +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { describe('management', function () { this.tags(['ciGroup2']); loadTestFile(require.resolve('./create_index_pattern_wizard')); + loadTestFile(require.resolve('./feature_controls')); }); } diff --git a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts index ae9b0f095fc44..e32f14200ad80 100644 --- a/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts +++ b/x-pack/test/functional/apps/maps/feature_controls/maps_security.ts @@ -67,7 +67,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Maps', 'Stack Management']); + expect(navLinks).to.eql(['Maps']); }); it(`allows a map to be created`, async () => { @@ -170,7 +170,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows Maps navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Maps', 'Stack Management']); + expect(navLinks).to.eql(['Maps']); }); it(`does not show create new button`, async () => { diff --git a/x-pack/test/functional/apps/maps/index.js b/x-pack/test/functional/apps/maps/index.js index ef8b4ad4c0f19..03b75601ec2a8 100644 --- a/x-pack/test/functional/apps/maps/index.js +++ b/x-pack/test/functional/apps/maps/index.js @@ -28,7 +28,7 @@ export default function ({ loadTestFile, getService }) { }); describe('', function () { - this.tags('ciGroup7'); + this.tags('ciGroup9'); loadTestFile(require.resolve('./documents_source')); loadTestFile(require.resolve('./blended_vector_layer')); loadTestFile(require.resolve('./vector_styling')); diff --git a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts index 6fd78458a6ce5..ab67e567e67ac 100644 --- a/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/no_ml_access.ts @@ -55,16 +55,9 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('should not allow to access the Stack Management ML page', async () => { await ml.testExecution.logTestStep( - 'should load the stack management with the ML menu item being present' + 'should load the stack management with the ML menu item being absent' ); - await ml.navigation.navigateToStackManagement(); - - await ml.testExecution.logTestStep( - 'should display the access denied page in stack management' - ); - await ml.navigation.navigateToStackManagementJobsListPage({ - expectAccessDenied: true, - }); + await ml.navigation.navigateToStackManagement({ expectMlLink: false }); }); }); } diff --git a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts index a358e57f792c7..cb964995511ef 100644 --- a/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts +++ b/x-pack/test/functional/apps/ml/permissions/read_ml_access.ts @@ -408,16 +408,9 @@ export default function ({ getService }: FtrProviderContext) { it('should display elements on Stack Management ML page correctly', async () => { await ml.testExecution.logTestStep( - 'should load the stack management with the ML menu item being present' + 'should load the stack management with the ML menu item being absent' ); - await ml.navigation.navigateToStackManagement(); - - await ml.testExecution.logTestStep( - 'should display the access denied page in stack management' - ); - await ml.navigation.navigateToStackManagementJobsListPage({ - expectAccessDenied: true, - }); + await ml.navigation.navigateToStackManagement({ expectMlLink: false }); }); }); } diff --git a/x-pack/test/functional/apps/remote_clusters/feature_controls/index.ts b/x-pack/test/functional/apps/remote_clusters/feature_controls/index.ts new file mode 100644 index 0000000000000..bfcaef629dc42 --- /dev/null +++ b/x-pack/test/functional/apps/remote_clusters/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./remote_clusters_security')); + }); +} diff --git a/x-pack/test/functional/apps/remote_clusters/feature_controls/remote_clusters_security.ts b/x-pack/test/functional/apps/remote_clusters/feature_controls/remote_clusters_security.ts new file mode 100644 index 0000000000000..b1edc74607161 --- /dev/null +++ b/x-pack/test/functional/apps/remote_clusters/feature_controls/remote_clusters_security.ts @@ -0,0 +1,76 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Stack" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with license_management_user', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'license_management_user'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Data" section with Remote Clusters', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(3); + expect(sections[1]).to.eql({ + sectionId: 'data', + sectionLinks: [ + 'index_management', + 'index_lifecycle_management', + 'snapshot_restore', + 'rollup_jobs', + 'transform', + 'remote_clusters', + ], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/remote_clusters/index.ts b/x-pack/test/functional/apps/remote_clusters/index.ts index d91d413e2b7af..0839c2f22af47 100644 --- a/x-pack/test/functional/apps/remote_clusters/index.ts +++ b/x-pack/test/functional/apps/remote_clusters/index.ts @@ -9,6 +9,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default ({ loadTestFile }: FtrProviderContext) => { describe('Remote Clusters app', function () { this.tags(['ciGroup4', 'skipCloud']); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./home_page')); }); }; diff --git a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts index 28b8153ea4c2b..02b2ec4d4c681 100644 --- a/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts +++ b/x-pack/test/functional/apps/saved_objects_management/feature_controls/saved_objects_management_security.ts @@ -10,14 +10,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const security = getService('security'); const testSubjects = getService('testSubjects'); - const PageObjects = getPageObjects([ - 'common', - 'settings', - 'security', - 'error', - 'header', - 'savedObjects', - ]); + const PageObjects = getPageObjects(['common', 'settings', 'security', 'error', 'savedObjects']); let version: string = ''; describe('feature controls saved objects management', () => { @@ -310,12 +303,6 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { }); describe('listing', () => { - it(`doesn't display management section`, async () => { - await PageObjects.settings.navigateTo(); - await testSubjects.existOrFail('managementHome'); // this ensures we've gotten to the management page - await testSubjects.missingOrFail('objects'); - }); - it(`can't navigate to listing page`, async () => { await PageObjects.common.navigateToUrl('management', 'kibana/objects', { ensureCurrentUrl: false, @@ -323,7 +310,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { shouldUseHashForSubUrl: false, }); - await testSubjects.existOrFail('managementHome'); + await testSubjects.existOrFail('appNotFoundPageContent'); }); }); @@ -338,8 +325,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { shouldUseHashForSubUrl: false, } ); - await PageObjects.header.waitUntilLoadingHasFinished(); - await testSubjects.existOrFail('managementHome'); + await testSubjects.existOrFail('appNotFoundPageContent'); }); }); }); diff --git a/x-pack/test/functional/apps/security/secure_roles_perm.js b/x-pack/test/functional/apps/security/secure_roles_perm.js index 2054a7b0b0038..c547657bf880a 100644 --- a/x-pack/test/functional/apps/security/secure_roles_perm.js +++ b/x-pack/test/functional/apps/security/secure_roles_perm.js @@ -21,7 +21,6 @@ export default function ({ getService, getPageObjects }) { const browser = getService('browser'); const kibanaServer = getService('kibanaServer'); const testSubjects = getService('testSubjects'); - const retry = getService('retry'); describe('secure roles and permissions', function () { before(async () => { @@ -74,12 +73,9 @@ export default function ({ getService, getPageObjects }) { await PageObjects.security.login('Rashmi', 'changeme'); }); - it('Kibana User navigating to Management gets permission denied', async function () { + it('Kibana User does not have link to user management', async function () { await PageObjects.settings.navigateTo(); - await PageObjects.security.clickElasticsearchUsers(); - await retry.tryForTime(2000, async () => { - await testSubjects.find('permissionDeniedMessage'); - }); + await testSubjects.missingOrFail('users'); }); it('Kibana User navigating to Discover and trying to generate CSV gets - Authorization Error ', async function () { diff --git a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts index a3ade23f5c178..d705140954de4 100644 --- a/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts +++ b/x-pack/test/functional/apps/timelion/feature_controls/timelion_security.ts @@ -60,7 +60,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows timelion navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Timelion', 'Stack Management']); + expect(navLinks).to.eql(['Timelion']); }); it(`allows a timelion sheet to be created`, async () => { @@ -112,7 +112,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows timelion navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Timelion', 'Stack Management']); + expect(navLinks).to.eql(['Timelion']); }); it(`does not allow a timelion sheet to be created`, async () => { diff --git a/x-pack/test/functional/apps/transform/cloning.ts b/x-pack/test/functional/apps/transform/cloning.ts index b6ccd68bb2096..a147b56d56251 100644 --- a/x-pack/test/functional/apps/transform/cloning.ts +++ b/x-pack/test/functional/apps/transform/cloning.ts @@ -5,12 +5,12 @@ */ import { FtrProviderContext } from '../../ftr_provider_context'; -import { TransformPivotConfig } from '../../../../plugins/transform/public/app/common'; +import { TransformPivotConfig } from '../../../../plugins/transform/common/types/transform'; function getTransformConfig(): TransformPivotConfig { const date = Date.now(); return { - id: `ec_2_${date}`, + id: `ec_cloning_${date}`, source: { index: ['ft_ecommerce'] }, pivot: { group_by: { category: { terms: { field: 'category.keyword' } } }, @@ -32,7 +32,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { await esArchiver.loadIfNeeded('ml/ecommerce'); await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); - await transform.api.createAndRunTransform(transformConfig); + await transform.api.createAndRunTransform(transformConfig.id, transformConfig); await transform.testResources.setKibanaTimeZoneToUTC(); await transform.securityUI.loginAsTransformPowerUser(); diff --git a/x-pack/test/functional/apps/transform/creation_index_pattern.ts b/x-pack/test/functional/apps/transform/creation_index_pattern.ts index 4e2b832838b7d..13213679a6117 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + import { FtrProviderContext } from '../../ftr_provider_context'; interface GroupByEntry { @@ -141,7 +143,7 @@ export default function ({ getService }: FtrProviderContext) { values: [`Men's Accessories`], }, row: { - status: 'stopped', + status: TRANSFORM_STATE.STOPPED, mode: 'batch', progress: '100', }, @@ -239,7 +241,7 @@ export default function ({ getService }: FtrProviderContext) { values: ['AE', 'CO', 'EG', 'FR', 'GB'], }, row: { - status: 'stopped', + status: TRANSFORM_STATE.STOPPED, mode: 'batch', progress: '100', }, diff --git a/x-pack/test/functional/apps/transform/creation_saved_search.ts b/x-pack/test/functional/apps/transform/creation_saved_search.ts index 229ff97782362..20d276c2e017b 100644 --- a/x-pack/test/functional/apps/transform/creation_saved_search.ts +++ b/x-pack/test/functional/apps/transform/creation_saved_search.ts @@ -4,6 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + import { FtrProviderContext } from '../../ftr_provider_context'; interface GroupByEntry { @@ -58,7 +60,7 @@ export default function ({ getService }: FtrProviderContext) { values: ['ASA'], }, row: { - status: 'stopped', + status: TRANSFORM_STATE.STOPPED, mode: 'batch', progress: '100', }, diff --git a/x-pack/test/functional/apps/transform/editing.ts b/x-pack/test/functional/apps/transform/editing.ts index 460e7c5b24a98..ac955bde4ad5d 100644 --- a/x-pack/test/functional/apps/transform/editing.ts +++ b/x-pack/test/functional/apps/transform/editing.ts @@ -4,13 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ +import { TransformPivotConfig } from '../../../../plugins/transform/common/types/transform'; +import { TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; + import { FtrProviderContext } from '../../ftr_provider_context'; -import { TransformPivotConfig } from '../../../../plugins/transform/public/app/common'; function getTransformConfig(): TransformPivotConfig { const date = Date.now(); return { - id: `ec_2_${date}`, + id: `ec_editing_${date}`, source: { index: ['ft_ecommerce'] }, pivot: { group_by: { category: { terms: { field: 'category.keyword' } } }, @@ -32,7 +34,7 @@ export default function ({ getService }: FtrProviderContext) { before(async () => { await esArchiver.loadIfNeeded('ml/ecommerce'); await transform.testResources.createIndexPatternIfNeeded('ft_ecommerce', 'order_date'); - await transform.api.createAndRunTransform(transformConfig); + await transform.api.createAndRunTransform(transformConfig.id, transformConfig); await transform.testResources.setKibanaTimeZoneToUTC(); await transform.securityUI.loginAsTransformPowerUser(); @@ -52,7 +54,7 @@ export default function ({ getService }: FtrProviderContext) { expected: { messageText: 'updated transform.', row: { - status: 'stopped', + status: TRANSFORM_STATE.STOPPED, mode: 'batch', progress: '100', }, diff --git a/x-pack/test/functional/apps/transform/feature_controls/index.ts b/x-pack/test/functional/apps/transform/feature_controls/index.ts new file mode 100644 index 0000000000000..794e6f516d982 --- /dev/null +++ b/x-pack/test/functional/apps/transform/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./transform_security')); + }); +} diff --git a/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts b/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts new file mode 100644 index 0000000000000..5d7d8ec3c307e --- /dev/null +++ b/x-pack/test/functional/apps/transform/feature_controls/transform_security.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.security.forceLogout(); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Stack" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with transform_user', () => { + before(async () => { + await security.testUser.setRoles(['global_dashboard_all', 'transform_user'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Data" section with Transform', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(1); + expect(sections[0]).to.eql({ + sectionId: 'data', + sectionLinks: ['transform'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/transform/index.ts b/x-pack/test/functional/apps/transform/index.ts index a01f3fa5d53a5..2837ddb7333e6 100644 --- a/x-pack/test/functional/apps/transform/index.ts +++ b/x-pack/test/functional/apps/transform/index.ts @@ -37,5 +37,6 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./creation_saved_search')); loadTestFile(require.resolve('./cloning')); loadTestFile(require.resolve('./editing')); + loadTestFile(require.resolve('./feature_controls')); }); } diff --git a/x-pack/test/functional/apps/upgrade_assistant/feature_controls/index.ts b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/index.ts new file mode 100644 index 0000000000000..f1c73e39fbc3e --- /dev/null +++ b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ loadTestFile }: FtrProviderContext) { + describe('feature controls', function () { + this.tags(['ciGroup2']); + + loadTestFile(require.resolve('./upgrade_assistant_security')); + }); +} diff --git a/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts new file mode 100644 index 0000000000000..1f541dbe03537 --- /dev/null +++ b/x-pack/test/functional/apps/upgrade_assistant/feature_controls/upgrade_assistant_security.ts @@ -0,0 +1,72 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getPageObjects, getService }: FtrProviderContext) { + const esArchiver = getService('esArchiver'); + const security = getService('security'); + const PageObjects = getPageObjects(['common', 'settings', 'security']); + const appsMenu = getService('appsMenu'); + const managementMenu = getService('managementMenu'); + + describe('security', () => { + before(async () => { + await esArchiver.load('empty_kibana'); + await PageObjects.common.navigateToApp('home'); + }); + + after(async () => { + await esArchiver.unload('empty_kibana'); + }); + + describe('global all privileges (aka kibana_admin)', () => { + before(async () => { + await security.testUser.setRoles(['kibana_admin'], true); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should not render the "Stack" section', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = (await managementMenu.getSections()).map((section) => section.sectionId); + expect(sections).to.eql(['insightsAndAlerting', 'kibana']); + }); + }); + + describe('global dashboard all with global_upgrade_assistant_role', () => { + before(async () => { + await security.testUser.setRoles( + ['global_dashboard_all', 'global_upgrade_assistant_role'], + true + ); + }); + after(async () => { + await security.testUser.restoreDefaults(); + }); + it('should show the Stack Management nav link', async () => { + const links = await appsMenu.readLinks(); + expect(links.map((link) => link.text)).to.contain('Stack Management'); + }); + + it('should render the "Stack" section with Upgrde Assistant', async () => { + await PageObjects.common.navigateToApp('management'); + const sections = await managementMenu.getSections(); + expect(sections).to.have.length(3); + expect(sections[2]).to.eql({ + sectionId: 'stack', + sectionLinks: ['license_management', 'upgrade_assistant'], + }); + }); + }); + }); +} diff --git a/x-pack/test/functional/apps/upgrade_assistant/index.ts b/x-pack/test/functional/apps/upgrade_assistant/index.ts index 0e6c52f0812ee..131cb6a249c78 100644 --- a/x-pack/test/functional/apps/upgrade_assistant/index.ts +++ b/x-pack/test/functional/apps/upgrade_assistant/index.ts @@ -9,6 +9,7 @@ export default function upgradeCheckup({ loadTestFile }: FtrProviderContext) { describe('Upgrade checkup ', function upgradeAssistantTestSuite() { this.tags('ciGroup4'); + loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./upgrade_assistant')); }); } diff --git a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts index 49435df4f1c2a..ca84a8e561164 100644 --- a/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts +++ b/x-pack/test/functional/apps/visualize/feature_controls/visualize_security.ts @@ -79,7 +79,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Visualize', 'Stack Management']); + expect(navLinks).to.eql(['Visualize']); }); it(`landing page shows "Create new Visualization" button`, async () => { @@ -210,7 +210,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Visualize', 'Stack Management']); + expect(navLinks).to.eql(['Visualize']); }); it(`landing page shows "Create new Visualization" button`, async () => { @@ -325,7 +325,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { it('shows visualize navlink', async () => { const navLinks = (await appsMenu.readLinks()).map((link) => link.text); - expect(navLinks).to.eql(['Visualize', 'Stack Management']); + expect(navLinks).to.eql(['Visualize']); }); it(`landing page shows "Create new Visualization" button`, async () => { diff --git a/x-pack/test/functional/config.js b/x-pack/test/functional/config.js index 16e2cd1559fce..d1164c8c85362 100644 --- a/x-pack/test/functional/config.js +++ b/x-pack/test/functional/config.js @@ -266,6 +266,16 @@ export default async function ({ readConfigFile }) { }, ], }, + global_dashboard_all: { + kibana: [ + { + feature: { + dashboard: ['all'], + }, + spaces: ['*'], + }, + ], + }, global_maps_all: { kibana: [ { @@ -352,6 +362,65 @@ export default async function ({ readConfigFile }) { }, ], }, + + manage_security: { + elasticsearch: { + cluster: ['manage_security'], + }, + }, + + ccr_user: { + elasticsearch: { + cluster: ['manage', 'manage_ccr'], + }, + }, + + manage_ilm: { + elasticsearch: { + cluster: ['manage_ilm'], + }, + }, + + index_management_user: { + elasticsearch: { + cluster: ['monitor', 'manage_index_templates'], + indices: [ + { + names: ['geo_shapes*'], + privileges: ['all'], + }, + ], + }, + }, + + ingest_pipelines_user: { + elasticsearch: { + cluster: ['manage_pipeline', 'cluster:monitor/nodes/info'], + }, + }, + + license_management_user: { + elasticsearch: { + cluster: ['manage'], + }, + }, + + logstash_read_user: { + elasticsearch: { + indices: [ + { + names: ['.logstash*'], + privileges: ['read'], + }, + ], + }, + }, + + remote_clusters_user: { + elasticsearch: { + cluster: ['manage'], + }, + }, }, defaultRoles: ['superuser'], }, diff --git a/x-pack/test/functional/es_archives/endpoint/metadata/destination_index/data.json b/x-pack/test/functional/es_archives/endpoint/metadata/destination_index/data.json new file mode 100644 index 0000000000000..b19e5e2cbf1d7 --- /dev/null +++ b/x-pack/test/functional/es_archives/endpoint/metadata/destination_index/data.json @@ -0,0 +1,223 @@ +{ + "type": "doc", + "value": { + "id": "M92ScEJT9M9QusfIi3hpEb0AAAAAAAAA", + "index": "metrics-endpoint.metadata_current-default", + "source": { + "HostDetails": { + "@timestamp": 1579881969541, + "Endpoint": { + "policy": { + "applied": { + "id": "00000000-0000-0000-0000-000000000000", + "name": "Default", + "status": "failure" + } + }, + "status": "enrolled" + }, + "agent": { + "id": "3838df35-a095-4af4-8fce-0b6d78793f2e", + "name": "Elastic Endpoint", + "version": "6.8.0" + }, + "elastic": { + "agent": { + "id": "023fa40c-411d-4188-a941-4147bfadd095" + } + }, + "event": { + "action": "endpoint_metadata", + "category": [ + "host" + ], + "created": 1579881969541, + "dataset": "endpoint.metadata", + "id": "32f5fda2-48e4-4fae-b89e-a18038294d16", + "ingested": "2020-09-09T18:25:15.853783Z", + "kind": "metric", + "module": "endpoint", + "type": [ + "info" + ] + }, + "host": { + "hostname": "rezzani-7.example.com", + "id": "fc0ff548-feba-41b6-8367-65e8790d0eaf", + "ip": [ + "10.101.149.26", + "2606:a000:ffc0:39:11ef:37b9:3371:578c" + ], + "mac": [ + "e2-6d-f9-0-46-2e" + ], + "name": "rezzani-7.example.com", + "os": { + "Ext": { + "variant": "Windows Pro" + }, + "family": "Windows", + "full": "Windows 10", + "name": "windows 10.0", + "platform": "Windows", + "version": "10.0" + } + } + }, + "agent": { + "id": "3838df35-a095-4af4-8fce-0b6d78793f2e" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "OU3RgCJaNnR90byeDEHutp8AAAAAAAAA", + "index": "metrics-endpoint.metadata_current-default", + "source": { + "HostDetails": { + "@timestamp": 1579881969541, + "Endpoint": { + "policy": { + "applied": { + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A", + "name": "Default", + "status": "failure" + } + }, + "status": "enrolled" + }, + "agent": { + "id": "963b081e-60d1-482c-befd-a5815fa8290f", + "name": "Elastic Endpoint", + "version": "6.6.1" + }, + "elastic": { + "agent": { + "id": "11488bae-880b-4e7b-8d28-aac2aa9de816" + } + }, + "event": { + "action": "endpoint_metadata", + "category": [ + "host" + ], + "created": 1579881969541, + "dataset": "endpoint.metadata", + "id": "32f5fda2-48e4-4fae-b89e-a18038294d14", + "ingested": "2020-09-09T18:25:14.919526Z", + "kind": "metric", + "module": "endpoint", + "type": [ + "info" + ] + }, + "host": { + "architecture": "x86", + "hostname": "cadmann-4.example.com", + "id": "1fb3e58f-6ab0-4406-9d2a-91911207a712", + "ip": [ + "10.192.213.130", + "10.70.28.129" + ], + "mac": [ + "a9-71-6a-cc-93-85", + "f7-31-84-d3-21-68", + "2-95-12-39-ca-71" + ], + "name": "cadmann-4.example.com", + "os": { + "Ext": { + "variant": "Windows Pro" + }, + "family": "Windows", + "full": "Windows 10", + "name": "windows 10.0", + "platform": "Windows", + "version": "10.0" + } + } + }, + "agent": { + "id": "963b081e-60d1-482c-befd-a5815fa8290f" + } + } + } +} + +{ + "type": "doc", + "value": { + "id": "YjqDCEuI6JmLeLOSyZx_NhMAAAAAAAAA", + "index": "metrics-endpoint.metadata_current-default", + "source": { + "HostDetails": { + "@timestamp": 1579881969541, + "Endpoint": { + "policy": { + "applied": { + "id": "C2A9093E-E289-4C0A-AA44-8C32A414FA7A", + "name": "Default", + "status": "success" + } + }, + "status": "enrolled" + }, + "agent": { + "id": "b3412d6f-b022-4448-8fee-21cc936ea86b", + "name": "Elastic Endpoint", + "version": "6.0.0" + }, + "elastic": { + "agent": { + "id": "92ac1ce0-e1f7-409e-8af6-f17e97b1fc71" + } + }, + "event": { + "action": "endpoint_metadata", + "category": [ + "host" + ], + "created": 1579881969541, + "dataset": "endpoint.metadata", + "id": "32f5fda2-48e4-4fae-b89e-a18038294d15", + "ingested": "2020-09-09T18:25:15.853404Z", + "kind": "metric", + "module": "endpoint", + "type": [ + "info" + ] + }, + "host": { + "architecture": "x86_64", + "hostname": "thurlow-9.example.com", + "id": "2f735e3d-be14-483b-9822-bad06e9045ca", + "ip": [ + "10.46.229.234" + ], + "mac": [ + "30-8c-45-55-69-b8", + "e5-36-7e-8f-a3-84", + "39-a1-37-20-18-74" + ], + "name": "thurlow-9.example.com", + "os": { + "Ext": { + "variant": "Windows Server" + }, + "family": "Windows", + "full": "Windows Server 2016", + "name": "windows 10.0", + "platform": "Windows", + "version": "10.0" + } + } + }, + "agent": { + "id": "b3412d6f-b022-4448-8fee-21cc936ea86b" + } + } + } +} diff --git a/x-pack/test/functional/page_objects/space_selector_page.ts b/x-pack/test/functional/page_objects/space_selector_page.ts index 90196c89170dd..b272175ff2b21 100644 --- a/x-pack/test/functional/page_objects/space_selector_page.ts +++ b/x-pack/test/functional/page_objects/space_selector_page.ts @@ -50,6 +50,126 @@ export function SpaceSelectorPageProvider({ getService, getPageObjects }: FtrPro return await testSubjects.click('spacesNavSelector'); } + async clickManageSpaces() { + await testSubjects.click('manageSpaces'); + } + + async clickCreateSpace() { + await testSubjects.click('createSpace'); + } + + async clickEnterSpaceName() { + await testSubjects.click('addSpaceName'); + } + + async addSpaceName(spaceName: string) { + await testSubjects.setValue('addSpaceName', spaceName); + } + + async clickSpaceAcustomAvatar() { + await testSubjects.click('space-avatar-space_a'); + } + + async clickSpaceInitials() { + await testSubjects.click('spaceLetterInitial'); + } + + async addSpaceInitials(spaceInitials: string) { + await testSubjects.setValue('spaceLetterInitial', spaceInitials); + } + + async clickColorPicker() { + await testSubjects.click('colorPickerAnchor'); + } + + async setColorinPicker(hexValue: string) { + await testSubjects.setValue('colorPickerAnchor', hexValue); + } + + async clickShowFeatures() { + await testSubjects.click('show-hide-section-link'); + } + + async clickChangeAllPriv() { + await testSubjects.click('changeAllPrivilegesButton'); + } + + async clickSaveSpaceCreation() { + await testSubjects.click('save-space-button'); + } + + async clickSpaceEditButton(spaceName: string) { + await testSubjects.click(`${spaceName}-editSpace`); + } + + async clickGoToRolesPage() { + await testSubjects.click('rolesManagementPage'); + } + + async clickCancelSpaceCreation() { + await testSubjects.click('cancel-space-button'); + } + + async clickOnCustomizeURL() { + await testSubjects.click('CustomizeOrReset'); + } + + async clickOnSpaceURLDisplay() { + await testSubjects.click('spaceURLDisplay'); + } + + async setSpaceURL(spaceURL: string) { + await testSubjects.setValue('spaceURLDisplay', spaceURL); + } + + async clickFeaturesVisibilityButton() { + await testSubjects.click('changeAllFeatureVisibilityPopover'); + } + + async clickHideAllFeatures() { + await testSubjects.click('spc-toggle-all-features-hide'); + } + + async clickShowAllFeatures() { + await testSubjects.click('spc-toggle-all-features-show'); + } + + async toggleFeatureVisibility(featureName: string) { + await testSubjects.click(`feature-${featureName}-toggle`); + } + + async clickOnDescriptionOfSpace() { + await testSubjects.click('descriptionSpaceText'); + } + + async setOnDescriptionOfSpace(descriptionSpace: string) { + await testSubjects.setValue('descriptionSpaceText', descriptionSpace); + } + + async clickOnDeleteSpaceButton(spaceName: string) { + await testSubjects.click(`${spaceName}-deleteSpace`); + } + + async setSpaceNameTobeDeleted(spaceName: string) { + await testSubjects.setValue('deleteSpaceInput', spaceName); + } + + async cancelDeletingSpace() { + await testSubjects.click('confirmModalCancelButton'); + } + + async confirmDeletingSpace() { + await testSubjects.click('confirmModalConfirmButton'); + } + + async clickOnSpaceb() { + await testSubjects.click('space-avatar-space_b'); + } + + async goToSpecificSpace(spaceName: string) { + await testSubjects.click(`${spaceName}-gotoSpace`); + } + async clickSpaceAvatar(spaceId: string) { return await retry.try(async () => { log.info(`SpaceSelectorPage:clickSpaceAvatar(${spaceId})`); diff --git a/x-pack/test/functional/services/ml/api.ts b/x-pack/test/functional/services/ml/api.ts index 5c9718539f47b..35d0439f69740 100644 --- a/x-pack/test/functional/services/ml/api.ts +++ b/x-pack/test/functional/services/ml/api.ts @@ -268,7 +268,7 @@ export function MachineLearningAPIProvider({ getService }: FtrProviderContext) { async waitForDFAJobTrainingRecordCountToBePositive(analyticsId: string) { await retry.waitForWithTimeout( `'${analyticsId}' to have training_docs_count > 0`, - 10 * 1000, + 60 * 1000, async () => { const trainingRecordCount = await this.getDFAJobTrainingRecordCount(analyticsId); if (trainingRecordCount > 0) { diff --git a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts index ffa1d9fd46c75..e01e065867ac7 100644 --- a/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts +++ b/x-pack/test/functional/services/ml/data_frame_analytics_creation.ts @@ -10,25 +10,9 @@ import { FtrProviderContext } from '../../ftr_provider_context'; import { MlCommonUI } from './common_ui'; import { MlApi } from './api'; import { - ClassificationAnalysis, - RegressionAnalysis, -} from '../../../../plugins/ml/common/types/data_frame_analytics'; - -enum ANALYSIS_CONFIG_TYPE { - OUTLIER_DETECTION = 'outlier_detection', - REGRESSION = 'regression', - CLASSIFICATION = 'classification', -} - -const isRegressionAnalysis = (arg: any): arg is RegressionAnalysis => { - const keys = Object.keys(arg); - return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.REGRESSION; -}; - -const isClassificationAnalysis = (arg: any): arg is ClassificationAnalysis => { - const keys = Object.keys(arg); - return keys.length === 1 && keys[0] === ANALYSIS_CONFIG_TYPE.CLASSIFICATION; -}; + isRegressionAnalysis, + isClassificationAnalysis, +} from '../../../../plugins/ml/common/util/analytics_utils'; export function MachineLearningDataFrameAnalyticsCreationProvider( { getService }: FtrProviderContext, diff --git a/x-pack/test/functional/services/ml/navigation.ts b/x-pack/test/functional/services/ml/navigation.ts index 9b53e5ce2f7e7..e564c03f62d58 100644 --- a/x-pack/test/functional/services/ml/navigation.ts +++ b/x-pack/test/functional/services/ml/navigation.ts @@ -23,10 +23,14 @@ export function MachineLearningNavigationProvider({ }); }, - async navigateToStackManagement() { + async navigateToStackManagement({ expectMlLink = true }: { expectMlLink?: boolean } = {}) { await retry.tryForTime(60 * 1000, async () => { await PageObjects.common.navigateToApp('management'); - await testSubjects.existOrFail('jobsListLink', { timeout: 2000 }); + if (expectMlLink) { + await testSubjects.existOrFail('jobsListLink', { timeout: 2000 }); + } else { + await testSubjects.missingOrFail('jobsListLink', { timeout: 2000 }); + } }); }, @@ -84,22 +88,14 @@ export function MachineLearningNavigationProvider({ await this.navigateToArea('~mlMainTab & ~settings', 'mlPageSettings'); }, - async navigateToStackManagementJobsListPage({ - expectAccessDenied = false, - }: { - expectAccessDenied?: boolean; - } = {}) { + async navigateToStackManagementJobsListPage() { // clicks the jobsListLink and loads the jobs list page await testSubjects.click('jobsListLink'); await retry.tryForTime(60 * 1000, async () => { - if (expectAccessDenied === true) { - await testSubjects.existOrFail('mlPageAccessDenied'); - } else { - // verify that the overall page is present - await testSubjects.existOrFail('mlPageStackManagementJobsList'); - // verify that the default tab with the anomaly detection jobs list got loaded - await testSubjects.existOrFail('ml-jobs-list'); - } + // verify that the overall page is present + await testSubjects.existOrFail('mlPageStackManagementJobsList'); + // verify that the default tab with the anomaly detection jobs list got loaded + await testSubjects.existOrFail('ml-jobs-list'); }); }, diff --git a/x-pack/test/functional/services/transform/api.ts b/x-pack/test/functional/services/transform/api.ts index 697020fafb196..d97db93c31b3b 100644 --- a/x-pack/test/functional/services/transform/api.ts +++ b/x-pack/test/functional/services/transform/api.ts @@ -5,13 +5,17 @@ */ import expect from '@kbn/expect'; +import type { PutTransformsRequestSchema } from '../../../../plugins/transform/common/api_schemas/transforms'; +import { TransformState, TRANSFORM_STATE } from '../../../../plugins/transform/common/constants'; +import type { TransformStats } from '../../../../plugins/transform/common/types/transform_stats'; + import { FtrProviderContext } from '../../ftr_provider_context'; -import { TRANSFORM_STATE } from '../../../../plugins/transform/common'; -import { - TransformPivotConfig, - TransformStats, -} from '../../../../plugins/transform/public/app/common'; +export async function asyncForEach(array: any[], callback: Function) { + for (let index = 0; index < array.length; index++) { + await callback(array[index], index, array); + } +} export function TransformAPIProvider({ getService }: FtrProviderContext) { const es = getService('legacyEs'); @@ -35,7 +39,7 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { await this.waitForIndicesToExist(indices, `expected ${indices} to be created`); }, - async deleteIndices(indices: string) { + async deleteIndices(indices: string, skipWaitForIndicesNotToExist?: boolean) { log.debug(`Deleting indices: '${indices}'...`); if ((await es.indices.exists({ index: indices, allowNoIndices: false })) === false) { log.debug(`Indices '${indices}' don't exist. Nothing to delete.`); @@ -49,7 +53,13 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { .to.have.property('acknowledged') .eql(true, 'Response for delete request should be acknowledged'); - await this.waitForIndicesNotToExist(indices, `expected indices '${indices}' to be deleted`); + // Check for the option to skip the check if the indices are deleted. + // For example, we might want to clear the .transform-* indices but they + // will be automatically regenerated making tests flaky without the option + // to skip this check. + if (!skipWaitForIndicesNotToExist) { + await this.waitForIndicesNotToExist(indices, `expected indices '${indices}' to be deleted`); + } }, async waitForIndicesToExist(indices: string, errorMsg?: string) { @@ -73,7 +83,26 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { }, async cleanTransformIndices() { - await this.deleteIndices('.transform-*'); + // Delete all transforms using the API since we mustn't just delete + // all `.transform-*` indices since this might result in orphaned ES tasks. + const { + body: { transforms }, + } = await esSupertest.get(`/_transform/`).expect(200); + const transformIds = transforms.map((t: { id: string }) => t.id); + + await asyncForEach(transformIds, async (transformId: string) => { + await esSupertest + .post(`/_transform/${transformId}/_stop?force=true&wait_for_completion`) + .expect(200); + await this.waitForTransformState(transformId, TRANSFORM_STATE.STOPPED); + + await esSupertest.delete(`/_transform/${transformId}`).expect(200); + await this.waitForTransformNotToExist(transformId); + }); + + // Delete all transform related notifications to clear messages tabs + // in the transforms list expanded rows. + await this.deleteIndices('.transform-notifications-*'); }, async getTransformStats(transformId: string): Promise { @@ -90,12 +119,12 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { return statsResponse.transforms[0]; }, - async getTransformState(transformId: string): Promise { + async getTransformState(transformId: string): Promise { const stats = await this.getTransformStats(transformId); return stats.state; }, - async waitForTransformState(transformId: string, expectedState: TRANSFORM_STATE) { + async waitForTransformState(transformId: string, expectedState: TransformState) { await retry.waitForWithTimeout( `transform state to be ${expectedState}`, 2 * 60 * 1000, @@ -110,6 +139,23 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { ); }, + async waitForTransformStateNotToBe(transformId: string, notExpectedState: TransformState) { + await retry.waitForWithTimeout( + `transform state not to be ${notExpectedState}`, + 2 * 60 * 1000, + async () => { + const state = await this.getTransformState(transformId); + if (state !== notExpectedState) { + return true; + } else { + throw new Error( + `expected transform state to not be ${notExpectedState} but got ${state}` + ); + } + } + ); + }, + async waitForBatchTransformToComplete(transformId: string) { await retry.waitForWithTimeout(`batch transform to complete`, 2 * 60 * 1000, async () => { const stats = await this.getTransformStats(transformId); @@ -127,8 +173,7 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { return await esSupertest.get(`/_transform/${transformId}`).expect(expectedCode); }, - async createTransform(transformConfig: TransformPivotConfig) { - const transformId = transformConfig.id; + async createTransform(transformId: string, transformConfig: PutTransformsRequestSchema) { log.debug(`Creating transform with id '${transformId}'...`); await esSupertest.put(`/_transform/${transformId}`).send(transformConfig).expect(200); @@ -147,6 +192,7 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { } }); }, + async waitForTransformNotToExist(transformId: string, errorMsg?: string) { await retry.waitForWithTimeout(`'${transformId}' to exist`, 5 * 1000, async () => { if (await this.getTransform(transformId, 404)) { @@ -162,15 +208,15 @@ export function TransformAPIProvider({ getService }: FtrProviderContext) { await esSupertest.post(`/_transform/${transformId}/_start`).expect(200); }, - async createAndRunTransform(transformConfig: TransformPivotConfig) { - await this.createTransform(transformConfig); - await this.startTransform(transformConfig.id); + async createAndRunTransform(transformId: string, transformConfig: PutTransformsRequestSchema) { + await this.createTransform(transformId, transformConfig); + await this.startTransform(transformId); if (transformConfig.sync === undefined) { // batch mode - await this.waitForBatchTransformToComplete(transformConfig.id); + await this.waitForBatchTransformToComplete(transformId); } else { // continuous mode - await this.waitForTransformState(transformConfig.id, TRANSFORM_STATE.STARTED); + await this.waitForTransformStateNotToBe(transformId, TRANSFORM_STATE.STOPPED); } }, }; diff --git a/x-pack/test/functional/services/transform/transform_table.ts b/x-pack/test/functional/services/transform/transform_table.ts index 77e52b642261b..cc360379f32c3 100644 --- a/x-pack/test/functional/services/transform/transform_table.ts +++ b/x-pack/test/functional/services/transform/transform_table.ts @@ -174,7 +174,7 @@ export function TransformTableProvider({ getService }: FtrProviderContext) { await testSubjects.existOrFail('transformMessagesTab'); await testSubjects.click('transformMessagesTab'); await testSubjects.existOrFail('~transformMessagesTabContent'); - await retry.tryForTime(5000, async () => { + await retry.tryForTime(30 * 1000, async () => { const actualText = await testSubjects.getVisibleText('~transformMessagesTabContent'); expect(actualText.includes(expectedText)).to.eql( true, diff --git a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts index dd81c860e9fa8..5c42c1978a0b5 100644 --- a/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts +++ b/x-pack/test/functional_with_es_ssl/fixtures/plugins/alerts/server/plugin.ts @@ -21,7 +21,7 @@ export class AlertingFixturePlugin implements Plugin { diff --git a/x-pack/test/ingest_manager_api_integration/config.ts b/x-pack/test/ingest_manager_api_integration/config.ts index 08d5da148b51e..94fbee0593d3e 100644 --- a/x-pack/test/ingest_manager_api_integration/config.ts +++ b/x-pack/test/ingest_manager_api_integration/config.ts @@ -12,7 +12,7 @@ import { defineDockerServersConfig } from '@kbn/test'; // Docker image to use for Ingest Manager API integration tests. // This hash comes from the commit hash here: https://github.com/elastic/package-storage/commit export const dockerImage = - 'docker.elastic.co/package-registry/distribution:f6b01daec8cfe355101e366de9941d35a4c3763e'; + 'docker.elastic.co/package-registry/distribution:5e0e12ce1bc2cb0c2f67f2e07d11b9a6043bcf25'; export default async function ({ readConfigFile }: FtrConfigProviderContext) { const xPackAPITestsConfig = await readConfigFile(require.resolve('../api_integration/config.ts')); diff --git a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts index 1f4428e198539..459dc4739897c 100644 --- a/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts +++ b/x-pack/test/kerberos_api_integration/apis/security/kerberos_login.ts @@ -79,9 +79,9 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); expect(user.username).to.eql(username); - expect(user.authentication_realm).to.eql({ name: 'reserved', type: 'reserved' }); expect(user.authentication_provider).to.eql('basic'); expect(user.authentication_type).to.eql('realm'); + // Do not assert on the `authentication_realm`, as the value differes for on-prem vs cloud }); describe('initiating SPNEGO', () => { diff --git a/x-pack/test/login_selector_api_integration/apis/login_selector.ts b/x-pack/test/login_selector_api_integration/apis/login_selector.ts index 7eb1f07d67506..44582355cf890 100644 --- a/x-pack/test/login_selector_api_integration/apis/login_selector.ts +++ b/x-pack/test/login_selector_api_integration/apis/login_selector.ts @@ -36,7 +36,7 @@ export default function ({ getService }: FtrProviderContext) { sessionCookie: Cookie, username: string, providerName: string, - authenticationRealm: { name: string; type: string }, + authenticationRealm: { name: string; type: string } | null, authenticationType: string ) { expect(sessionCookie.key).to.be('sid'); @@ -67,7 +67,9 @@ export default function ({ getService }: FtrProviderContext) { expect(apiResponse.body.username).to.be(username); expect(apiResponse.body.authentication_provider).to.be(providerName); - expect(apiResponse.body.authentication_realm).to.eql(authenticationRealm); + if (authenticationRealm) { + expect(apiResponse.body.authentication_realm).to.eql(authenticationRealm); + } expect(apiResponse.body.authentication_type).to.be(authenticationType); } @@ -228,16 +230,9 @@ export default function ({ getService }: FtrProviderContext) { const basicSessionCookie = request.cookie( basicAuthenticationResponse.headers['set-cookie'][0] )!; - await checkSessionCookie( - basicSessionCookie, - 'elastic', - 'basic1', - { - name: 'reserved', - type: 'reserved', - }, - 'realm' - ); + // Skip auth provider check since this comes from the reserved realm, + // which is not available when running on ESS + await checkSessionCookie(basicSessionCookie, 'elastic', 'basic1', null, 'realm'); const authenticationResponse = await supertest .post('/api/security/saml/callback') diff --git a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.ts b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.ts index 0a230ac84d991..c2335cf04504f 100644 --- a/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.ts +++ b/x-pack/test/oidc_api_integration/apis/authorization_code_flow/oidc_auth.ts @@ -43,9 +43,9 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); expect(user.username).to.eql(username); - expect(user.authentication_realm).to.eql({ name: 'reserved', type: 'reserved' }); expect(user.authentication_provider).to.eql('basic'); expect(user.authentication_type).to.be('realm'); + // Do not assert on the `authentication_realm`, as the value differes for on-prem vs cloud }); describe('initiating handshake', () => { diff --git a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts index 2f6b088ab7190..0559e9e96fe3f 100644 --- a/x-pack/test/pki_api_integration/apis/security/pki_auth.ts +++ b/x-pack/test/pki_api_integration/apis/security/pki_auth.ts @@ -93,8 +93,8 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); expect(user.username).to.eql(username); - expect(user.authentication_realm).to.eql({ name: 'reserved', type: 'reserved' }); expect(user.authentication_provider).to.eql('basic'); + // Do not assert on the `authentication_realm`, as the value differes for on-prem vs cloud }); it('should properly set cookie and authenticate user', async () => { diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts index 6a68bd530cf63..9eafd0c318383 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/spaces.ts @@ -27,8 +27,7 @@ export default function ({ getService }: FtrProviderContext) { ); }; - // FLAKY: https://github.com/elastic/kibana/issues/76551 - describe.skip('Exports from Non-default Space', () => { + describe('Exports from Non-default Space', () => { before(async () => { await esArchiver.load('reporting/ecommerce'); await esArchiver.load('reporting/ecommerce_kibana_spaces'); // dashboard in non default space @@ -54,7 +53,8 @@ export default function ({ getService }: FtrProviderContext) { expect(reportCompleted).to.match(/^"order_date",/); }); - it('should complete a job of PNG export of a dashboard in non-default space', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/76551 + it.skip('should complete a job of PNG export of a dashboard in non-default space', async () => { const downloadPath = await reportingAPI.postJob( `/s/non_default_space/api/reporting/generate/png?jobParams=%28browserTimezone%3AUTC%2Clayout%3A%28dimensions%3A%28height%3A512%2Cwidth%3A2402%29%2Cid%3Apng%29%2CobjectType%3Adashboard%2CrelativeUrl%3A%27%2Fs%2Fnon_default_space%2Fapp%2Fdashboards%23%2Fview%2F3c9ee360-e7ee-11ea-a730-d58e9ea7581b%3F_g%3D%28filters%3A%21%21%28%29%2CrefreshInterval%3A%28pause%3A%21%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3A%21%272019-06-10T03%3A17%3A28.800Z%21%27%2Cto%3A%21%272019-07-14T19%3A25%3A06.385Z%21%27%29%29%26_a%3D%28description%3A%21%27%21%27%2Cfilters%3A%21%21%28%29%2CfullScreenMode%3A%21%21f%2Coptions%3A%28hidePanelTitles%3A%21%21f%2CuseMargins%3A%21%21t%29%2Cquery%3A%28language%3Akuery%2Cquery%3A%21%27%21%27%29%2CtimeRestore%3A%21%21t%2Ctitle%3A%21%27Ecom%2520Dashboard%2520Non%2520Default%2520Space%21%27%2CviewMode%3Aview%29%27%2Ctitle%3A%27Ecom%20Dashboard%20Non%20Default%20Space%27%29` ); @@ -64,7 +64,8 @@ export default function ({ getService }: FtrProviderContext) { expect(reportCompleted).to.not.be(null); }); - it('should complete a job of PDF export of a dashboard in non-default space', async () => { + // FLAKY: https://github.com/elastic/kibana/issues/76551 + it.skip('should complete a job of PDF export of a dashboard in non-default space', async () => { const downloadPath = await reportingAPI.postJob( `/s/non_default_space/api/reporting/generate/printablePdf?jobParams=%28browserTimezone%3AUTC%2Clayout%3A%28dimensions%3A%28height%3A512%2Cwidth%3A2402%29%2Cid%3Apreserve_layout%29%2CobjectType%3Adashboard%2CrelativeUrls%3A%21%28%27%2Fs%2Fnon_default_space%2Fapp%2Fdashboards%23%2Fview%2F3c9ee360-e7ee-11ea-a730-d58e9ea7581b%3F_g%3D%28filters%3A%21%21%28%29%2CrefreshInterval%3A%28pause%3A%21%21t%2Cvalue%3A0%29%2Ctime%3A%28from%3A%21%272019-06-10T03%3A17%3A28.800Z%21%27%2Cto%3A%21%272019-07-14T19%3A25%3A06.385Z%21%27%29%29%26_a%3D%28description%3A%21%27%21%27%2Cfilters%3A%21%21%28%29%2CfullScreenMode%3A%21%21f%2Coptions%3A%28hidePanelTitles%3A%21%21f%2CuseMargins%3A%21%21t%29%2Cquery%3A%28language%3Akuery%2Cquery%3A%21%27%21%27%29%2CtimeRestore%3A%21%21t%2Ctitle%3A%21%27Ecom%2520Dashboard%2520Non%2520Default%2520Space%21%27%2CviewMode%3Aview%29%27%29%2Ctitle%3A%27Ecom%20Dashboard%20Non%20Default%20Space%27%29` ); diff --git a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts index 49db8696c1134..aaf4dd3926411 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security/usage.ts @@ -21,8 +21,7 @@ export default function ({ getService }: FtrProviderContext) { const reportingAPI = getService('reportingAPI'); const usageAPI = getService('usageAPI'); - // FAILING: https://github.com/elastic/kibana/issues/76581 - describe.skip('Usage', () => { + describe('Usage', () => { before(async () => { await esArchiver.load(OSS_KIBANA_ARCHIVE_PATH); await esArchiver.load(OSS_DATA_ARCHIVE_PATH); @@ -116,7 +115,8 @@ export default function ({ getService }: FtrProviderContext) { }); }); - describe('from new jobs posted', () => { + // FAILING: https://github.com/elastic/kibana/issues/76581 + describe.skip('from new jobs posted', () => { it('should handle csv', async () => { await reportingAPI.expectAllJobsToFinishSuccessfully( await Promise.all([ diff --git a/x-pack/test/saml_api_integration/apis/security/saml_login.ts b/x-pack/test/saml_api_integration/apis/security/saml_login.ts index 501e1e5f2c203..2da7c92cd07b6 100644 --- a/x-pack/test/saml_api_integration/apis/security/saml_login.ts +++ b/x-pack/test/saml_api_integration/apis/security/saml_login.ts @@ -93,9 +93,9 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); expect(user.username).to.eql(username); - expect(user.authentication_realm).to.eql({ name: 'reserved', type: 'reserved' }); expect(user.authentication_provider).to.eql('basic'); expect(user.authentication_type).to.be('realm'); + // Do not assert on the `authentication_realm`, as the value differes for on-prem vs cloud }); describe('initiating handshake', () => { diff --git a/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts b/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts index 0b5656004492a..2e3c55f029d29 100644 --- a/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/common/suites/bulk_update.ts @@ -8,12 +8,7 @@ import expect from '@kbn/expect'; import { SuperTest } from 'supertest'; import { SAVED_OBJECT_TEST_CASES as CASES } from '../lib/saved_object_test_cases'; import { SPACES } from '../lib/spaces'; -import { - createRequest, - expectResponses, - getUrlPrefix, - getTestTitle, -} from '../lib/saved_object_test_utils'; +import { expectResponses, getUrlPrefix, getTestTitle } from '../lib/saved_object_test_utils'; import { ExpectResponseBody, TestCase, TestDefinition, TestSuite } from '../lib/types'; export interface BulkUpdateTestDefinition extends TestDefinition { @@ -21,6 +16,7 @@ export interface BulkUpdateTestDefinition extends TestDefinition { } export type BulkUpdateTestSuite = TestSuite; export interface BulkUpdateTestCase extends TestCase { + namespace?: string; // used to define individual "object namespace" strings, e.g., bulkUpdate across multiple namespaces failure?: 404; // only used for permitted response case } @@ -30,6 +26,12 @@ const NEW_ATTRIBUTE_VAL = `Updated attribute value ${Date.now()}`; const DOES_NOT_EXIST = Object.freeze({ type: 'dashboard', id: 'does-not-exist' }); export const TEST_CASES = Object.freeze({ ...CASES, DOES_NOT_EXIST }); +const createRequest = ({ type, id, namespace }: BulkUpdateTestCase) => ({ + type, + id, + ...(namespace && { namespace }), // individual "object namespace" string +}); + export function bulkUpdateTestSuiteFactory(esArchiver: any, supertest: SuperTest) { const expectForbidden = expectResponses.forbiddenTypes('bulk_update'); const expectResponseBody = ( diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts index 90f72e0b34449..1e11d1fc61110 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/bulk_update.ts @@ -39,7 +39,18 @@ const createTestCases = (spaceId: string) => { ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; const allTypes = normalTypes.concat(hiddenType); - return { normalTypes, hiddenType, allTypes }; + // an "object namespace" string can be specified for individual objects (to bulkUpdate across namespaces) + const withObjectNamespaces = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, namespace: DEFAULT_SPACE_ID }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, namespace: SPACE_1_ID }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, namespace: SPACE_1_ID, ...fail404() }, // intentional 404 test case + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, namespace: DEFAULT_SPACE_ID }, // SPACE_1_ID would also work + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, namespace: SPACE_2_ID, ...fail404() }, // intentional 404 test case + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, namespace: SPACE_2_ID }, + CASES.NAMESPACE_AGNOSTIC, // any namespace would work and would make no difference + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + return { normalTypes, hiddenType, allTypes, withObjectNamespaces }; }; export default function ({ getService }: FtrProviderContext) { @@ -51,26 +62,42 @@ export default function ({ getService }: FtrProviderContext) { supertest ); const createTests = (spaceId: string) => { - const { normalTypes, hiddenType, allTypes } = createTestCases(spaceId); + const { normalTypes, hiddenType, allTypes, withObjectNamespaces } = createTestCases(spaceId); // use singleRequest to reduce execution time and/or test combined cases + const authorizedCommon = [ + createTestDefinitions(normalTypes, false, { singleRequest: true }), + createTestDefinitions(hiddenType, true), + createTestDefinitions(allTypes, true, { + singleRequest: true, + responseBodyOverride: expectForbidden(['hiddentype']), + }), + ].flat(); return { - unauthorized: createTestDefinitions(allTypes, true), - authorized: [ - createTestDefinitions(normalTypes, false, { singleRequest: true }), - createTestDefinitions(hiddenType, true), - createTestDefinitions(allTypes, true, { - singleRequest: true, - responseBodyOverride: expectForbidden(['hiddentype']), - }), + unauthorized: [ + createTestDefinitions(allTypes, true), + createTestDefinitions(withObjectNamespaces, true, { singleRequest: true }), + ].flat(), + authorizedAtSpace: [ + authorizedCommon, + createTestDefinitions(withObjectNamespaces, true, { singleRequest: true }), + ].flat(), + authorizedAllSpaces: [ + authorizedCommon, + createTestDefinitions(withObjectNamespaces, false, { singleRequest: true }), + ].flat(), + superuser: [ + createTestDefinitions(allTypes, false, { singleRequest: true }), + createTestDefinitions(withObjectNamespaces, false, { singleRequest: true }), ].flat(), - superuser: createTestDefinitions(allTypes, false, { singleRequest: true }), }; }; describe('_bulk_update', () => { getTestScenarios().securityAndSpaces.forEach(({ spaceId, users }) => { const suffix = ` within the ${spaceId} space`; - const { unauthorized, authorized, superuser } = createTests(spaceId); + const { unauthorized, authorizedAtSpace, authorizedAllSpaces, superuser } = createTests( + spaceId + ); const _addTests = (user: TestUser, tests: BulkUpdateTestDefinition[]) => { addTests(`${user.description}${suffix}`, { user, spaceId, tests }); }; @@ -85,8 +112,11 @@ export default function ({ getService }: FtrProviderContext) { ].forEach((user) => { _addTests(user, unauthorized); }); - [users.dualAll, users.allGlobally, users.allAtSpace].forEach((user) => { - _addTests(user, authorized); + [users.allAtSpace].forEach((user) => { + _addTests(user, authorizedAtSpace); + }); + [users.dualAll, users.allGlobally].forEach((user) => { + _addTests(user, authorizedAllSpaces); }); _addTests(users.superuser, superuser); }); diff --git a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts index 81ffc5eea9220..ed501b235a457 100644 --- a/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts +++ b/x-pack/test/saved_object_api_integration/security_and_spaces/apis/index.ts @@ -12,7 +12,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { const supertest = getService('supertest'); describe('saved objects security and spaces enabled', function () { - this.tags('ciGroup5'); + this.tags('ciGroup8'); before(async () => { await createUsersAndRoles(es, supertest); diff --git a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts index d42eb25b81cf5..39ceb5a70d1b2 100644 --- a/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/security_only/apis/bulk_update.ts @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { SPACES } from '../../common/lib/spaces'; import { testCaseFailures, getTestScenarios } from '../../common/lib/saved_object_test_utils'; import { TestUser } from '../../common/lib/types'; import { FtrProviderContext } from '../../common/ftr_provider_context'; @@ -13,6 +14,11 @@ import { BulkUpdateTestDefinition, } from '../../common/suites/bulk_update'; +const { + DEFAULT: { spaceId: DEFAULT_SPACE_ID }, + SPACE_1: { spaceId: SPACE_1_ID }, + SPACE_2: { spaceId: SPACE_2_ID }, +} = SPACES; const { fail404 } = testCaseFailures; const createTestCases = () => { @@ -30,7 +36,19 @@ const createTestCases = () => { ]; const hiddenType = [{ ...CASES.HIDDEN, ...fail404() }]; const allTypes = normalTypes.concat(hiddenType); - return { normalTypes, hiddenType, allTypes }; + // an "object namespace" string can be specified for individual objects (to bulkUpdate across namespaces) + // even if the Spaces plugin is disabled, this should work, as `namespace` is handled by the Core API + const withObjectNamespaces = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, namespace: DEFAULT_SPACE_ID }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, namespace: SPACE_1_ID }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, namespace: SPACE_1_ID, ...fail404() }, // intentional 404 test case + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, namespace: DEFAULT_SPACE_ID }, // SPACE_1_ID would also work + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, namespace: SPACE_2_ID, ...fail404() }, // intentional 404 test case + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, namespace: SPACE_2_ID }, + CASES.NAMESPACE_AGNOSTIC, // any namespace would work and would make no difference + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + return { normalTypes, hiddenType, allTypes, withObjectNamespaces }; }; export default function ({ getService }: FtrProviderContext) { @@ -42,10 +60,13 @@ export default function ({ getService }: FtrProviderContext) { supertest ); const createTests = () => { - const { normalTypes, hiddenType, allTypes } = createTestCases(); + const { normalTypes, hiddenType, allTypes, withObjectNamespaces } = createTestCases(); // use singleRequest to reduce execution time and/or test combined cases return { - unauthorized: createTestDefinitions(allTypes, true), + unauthorized: [ + createTestDefinitions(allTypes, true), + createTestDefinitions(withObjectNamespaces, true, { singleRequest: true }), + ].flat(), authorized: [ createTestDefinitions(normalTypes, false, { singleRequest: true }), createTestDefinitions(hiddenType, true), @@ -53,8 +74,12 @@ export default function ({ getService }: FtrProviderContext) { singleRequest: true, responseBodyOverride: expectForbidden(['hiddentype']), }), + createTestDefinitions(withObjectNamespaces, false, { singleRequest: true }), + ].flat(), + superuser: [ + createTestDefinitions(allTypes, false, { singleRequest: true }), + createTestDefinitions(withObjectNamespaces, false, { singleRequest: true }), ].flat(), - superuser: createTestDefinitions(allTypes, false, { singleRequest: true }), }; }; diff --git a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts index 93e44e357918a..b51ec303fadf3 100644 --- a/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts +++ b/x-pack/test/saved_object_api_integration/spaces_only/apis/bulk_update.ts @@ -16,22 +16,37 @@ const { } = SPACES; const { fail404 } = testCaseFailures; -const createTestCases = (spaceId: string) => [ +const createTestCases = (spaceId: string) => { // for each outcome, if failure !== undefined then we expect to receive // an error; otherwise, we expect to receive a success result - { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, - { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, - { - ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, - ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), - }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, - { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, - CASES.NAMESPACE_AGNOSTIC, - { ...CASES.HIDDEN, ...fail404() }, - { ...CASES.DOES_NOT_EXIST, ...fail404() }, -]; + const normal = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, ...fail404(spaceId !== DEFAULT_SPACE_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + { + ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, + ...fail404(spaceId !== DEFAULT_SPACE_ID && spaceId !== SPACE_1_ID), + }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, ...fail404(spaceId !== SPACE_1_ID) }, + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, ...fail404(spaceId !== SPACE_2_ID) }, + CASES.NAMESPACE_AGNOSTIC, + { ...CASES.HIDDEN, ...fail404() }, + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + + // an "object namespace" string can be specified for individual objects (to bulkUpdate across namespaces) + const withObjectNamespaces = [ + { ...CASES.SINGLE_NAMESPACE_DEFAULT_SPACE, namespace: DEFAULT_SPACE_ID }, + { ...CASES.SINGLE_NAMESPACE_SPACE_1, namespace: SPACE_1_ID }, + { ...CASES.SINGLE_NAMESPACE_SPACE_2, namespace: SPACE_1_ID, ...fail404() }, // intentional 404 test case + { ...CASES.MULTI_NAMESPACE_DEFAULT_AND_SPACE_1, namespace: DEFAULT_SPACE_ID }, // SPACE_1_ID would also work + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_1, namespace: SPACE_2_ID, ...fail404() }, // intentional 404 test case + { ...CASES.MULTI_NAMESPACE_ONLY_SPACE_2, namespace: SPACE_2_ID }, + CASES.NAMESPACE_AGNOSTIC, // any namespace would work and would make no difference + { ...CASES.DOES_NOT_EXIST, ...fail404() }, + ]; + return { normal, withObjectNamespaces }; +}; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -39,8 +54,11 @@ export default function ({ getService }: FtrProviderContext) { const { addTests, createTestDefinitions } = bulkUpdateTestSuiteFactory(esArchiver, supertest); const createTests = (spaceId: string) => { - const testCases = createTestCases(spaceId); - return createTestDefinitions(testCases, false, { singleRequest: true }); + const { normal, withObjectNamespaces } = createTestCases(spaceId); + return [ + createTestDefinitions(normal, false, { singleRequest: true }), + createTestDefinitions(withObjectNamespaces, false, { singleRequest: true }), + ].flat(); }; describe('_bulk_update', () => { diff --git a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts index c9385bf9cebf2..00b4b82f9d602 100644 --- a/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts +++ b/x-pack/test/security_solution_endpoint/apps/endpoint/endpoint_list.ts @@ -6,7 +6,13 @@ import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; -import { deleteMetadataStream } from '../../../security_solution_endpoint_api_int/apis/data_stream_helper'; + +import { + deleteMetadataCurrentStream, + deleteMetadataStream, + deleteAllDocsFromMetadataCurrentIndex, +} from '../../../security_solution_endpoint_api_int/apis/data_stream_helper'; + export default ({ getPageObjects, getService }: FtrProviderContext) => { const pageObjects = getPageObjects(['common', 'endpoint', 'header', 'endpointPageUtils']); const esArchiver = getService('esArchiver'); @@ -23,6 +29,16 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { 'Version', 'Last Active', ], + [ + 'rezzani-7.example.com', + 'Error', + 'Default', + 'Failure', + 'windows 10.0', + '10.101.149.26, 2606:a000:ffc0:39:11ef:37b9:3371:578c', + '6.8.0', + 'Jan 24, 2020 @ 16:06:09.541', + ], [ 'cadmann-4.example.com', 'Error', @@ -43,16 +59,6 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { '6.0.0', 'Jan 24, 2020 @ 16:06:09.541', ], - [ - 'rezzani-7.example.com', - 'Error', - 'Default', - 'Failure', - 'windows 10.0', - '10.101.149.26, 2606:a000:ffc0:39:11ef:37b9:3371:578c', - '6.8.0', - 'Jan 24, 2020 @ 16:06:09.541', - ], ]; describe('endpoint list', function () { @@ -61,10 +67,15 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('when initially navigating to page', () => { before(async () => { + await deleteMetadataStream(getService); + await deleteMetadataCurrentStream(getService); + await deleteAllDocsFromMetadataCurrentIndex(getService); await pageObjects.endpoint.navigateToEndpointList(); }); after(async () => { await deleteMetadataStream(getService); + await deleteMetadataCurrentStream(getService); + await deleteAllDocsFromMetadataCurrentIndex(getService); }); it('finds no data in list and prompts onboarding to add policy', async () => { @@ -72,8 +83,8 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); it('finds data after load and polling', async () => { - await esArchiver.load('endpoint/metadata/api_feature', { useCreate: true }); - await pageObjects.endpoint.waitForTableToHaveData('endpointListTable', 10000); + await esArchiver.load('endpoint/metadata/destination_index', { useCreate: true }); + await pageObjects.endpoint.waitForTableToHaveData('endpointListTable', 1100); const tableData = await pageObjects.endpointPageUtils.tableData('endpointListTable'); expect(tableData).to.eql(expectedData); }); @@ -81,11 +92,13 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { describe('when there is data,', () => { before(async () => { - await esArchiver.load('endpoint/metadata/api_feature', { useCreate: true }); + await esArchiver.load('endpoint/metadata/destination_index', { useCreate: true }); await pageObjects.endpoint.navigateToEndpointList(); }); after(async () => { await deleteMetadataStream(getService); + await deleteMetadataCurrentStream(getService); + await deleteAllDocsFromMetadataCurrentIndex(getService); }); it('finds page title', async () => { @@ -202,10 +215,96 @@ export default ({ getPageObjects, getService }: FtrProviderContext) => { }); }); - describe('when there is no data,', () => { + describe('displays the correct table data for the kql queries', () => { + before(async () => { + await esArchiver.load('endpoint/metadata/destination_index', { useCreate: true }); + await pageObjects.endpoint.navigateToEndpointList(); + }); + after(async () => { + await deleteMetadataStream(getService); + await deleteMetadataCurrentStream(getService); + await deleteAllDocsFromMetadataCurrentIndex(getService); + }); + it('for the kql query: na, table shows an empty list', async () => { + await testSubjects.setValue('adminSearchBar', 'na'); + await (await testSubjects.find('querySubmitButton')).click(); + const expectedDataFromQuery = [ + [ + 'Hostname', + 'Agent Status', + 'Integration', + 'Configuration Status', + 'Operating System', + 'IP Address', + 'Version', + 'Last Active', + ], + ['No items found'], + ]; + + await pageObjects.endpoint.waitForTableToNotHaveData('endpointListTable'); + const tableData = await pageObjects.endpointPageUtils.tableData('endpointListTable'); + expect(tableData).to.eql(expectedDataFromQuery); + }); + + it('for the kql query: HostDetails.Endpoint.policy.applied.id : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A", table shows 2 items', async () => { + await testSubjects.setValue('adminSearchBar', ' '); + await (await testSubjects.find('querySubmitButton')).click(); + + const endpointListTableTotal = await testSubjects.getVisibleText('endpointListTableTotal'); + + await testSubjects.setValue( + 'adminSearchBar', + 'HostDetails.Endpoint.policy.applied.id : "C2A9093E-E289-4C0A-AA44-8C32A414FA7A" ' + ); + await (await testSubjects.find('querySubmitButton')).click(); + const expectedDataFromQuery = [ + [ + 'Hostname', + 'Agent Status', + 'Integration', + 'Configuration Status', + 'Operating System', + 'IP Address', + 'Version', + 'Last Active', + ], + [ + 'cadmann-4.example.com', + 'Error', + 'Default', + 'Failure', + 'windows 10.0', + '10.192.213.130, 10.70.28.129', + '6.6.1', + 'Jan 24, 2020 @ 16:06:09.541', + ], + [ + 'thurlow-9.example.com', + 'Error', + 'Default', + 'Success', + 'windows 10.0', + '10.46.229.234', + '6.0.0', + 'Jan 24, 2020 @ 16:06:09.541', + ], + ]; + + await pageObjects.endpoint.waitForVisibleTextToChange( + 'endpointListTableTotal', + endpointListTableTotal + ); + const tableData = await pageObjects.endpointPageUtils.tableData('endpointListTable'); + expect(tableData).to.eql(expectedDataFromQuery); + }); + }); + + describe.skip('when there is no data,', () => { before(async () => { // clear out the data and reload the page await deleteMetadataStream(getService); + await deleteMetadataCurrentStream(getService); await pageObjects.endpoint.navigateToEndpointList(); }); it('displays empty Policy Table page.', async () => { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/data_stream_helper.ts b/x-pack/test/security_solution_endpoint_api_int/apis/data_stream_helper.ts index b16da16b3137f..be25f26532d9c 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/data_stream_helper.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/data_stream_helper.ts @@ -10,6 +10,7 @@ import { eventsIndexPattern, alertsIndexPattern, policyIndexPattern, + metadataCurrentIndexPattern, } from '../../../plugins/security_solution/common/endpoint/constants'; export async function deleteDataStream(getService: (serviceName: 'es') => Client, index: string) { @@ -25,10 +26,44 @@ export async function deleteDataStream(getService: (serviceName: 'es') => Client ); } +export async function deleteAllDocsFromIndex( + getService: (serviceName: 'es') => Client, + index: string +) { + const client = getService('es'); + await client.deleteByQuery( + { + body: { + query: { + match_all: {}, + }, + }, + index: `${index}`, + }, + { + ignore: [404], + } + ); +} + export async function deleteMetadataStream(getService: (serviceName: 'es') => Client) { await deleteDataStream(getService, metadataIndexPattern); } +export async function deleteMetadataCurrentStream(getService: (serviceName: 'es') => Client) { + await deleteDataStream(getService, metadataCurrentIndexPattern); +} + +export async function deleteAllDocsFromMetadataIndex(getService: (serviceName: 'es') => Client) { + await deleteAllDocsFromIndex(getService, metadataIndexPattern); +} + +export async function deleteAllDocsFromMetadataCurrentIndex( + getService: (serviceName: 'es') => Client +) { + await deleteAllDocsFromIndex(getService, metadataCurrentIndexPattern); +} + export async function deleteEventsStream(getService: (serviceName: 'es') => Client) { await deleteDataStream(getService, eventsIndexPattern); } diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts index 3afa9f397a2ea..2286320ed7a88 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/metadata.ts @@ -5,7 +5,12 @@ */ import expect from '@kbn/expect/expect.js'; import { FtrProviderContext } from '../ftr_provider_context'; -import { deleteMetadataStream } from './data_stream_helper'; +import { + deleteAllDocsFromMetadataCurrentIndex, + deleteMetadataCurrentStream, + deleteAllDocsFromMetadataIndex, + deleteMetadataStream, +} from './data_stream_helper'; /** * The number of host documents in the es archive. @@ -15,12 +20,14 @@ const numberOfHostsInFixture = 3; export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const supertest = getService('supertest'); + describe('test metadata api', () => { describe('POST /api/endpoint/metadata when index is empty', () => { it('metadata api should return empty result when index is empty', async () => { - // the endpoint uses data streams and es archiver does not support deleting them at the moment so we need - // to do it manually await deleteMetadataStream(getService); + await deleteAllDocsFromMetadataIndex(getService); + await deleteMetadataCurrentStream(getService); + await deleteAllDocsFromMetadataCurrentIndex(getService); const { body } = await supertest .post('/api/endpoint/metadata') .set('kbn-xsrf', 'xxx') @@ -34,12 +41,19 @@ export default function ({ getService }: FtrProviderContext) { }); describe('POST /api/endpoint/metadata when index is not empty', () => { - before( - async () => await esArchiver.load('endpoint/metadata/api_feature', { useCreate: true }) - ); + before(async () => { + await esArchiver.load('endpoint/metadata/api_feature', { useCreate: true }); + // wait for transform + await new Promise((r) => setTimeout(r, 120000)); + }); // the endpoint uses data streams and es archiver does not support deleting them at the moment so we need // to do it manually - after(async () => await deleteMetadataStream(getService)); + after(async () => { + await deleteMetadataStream(getService); + await deleteAllDocsFromMetadataIndex(getService); + await deleteMetadataCurrentStream(getService); + await deleteAllDocsFromMetadataCurrentIndex(getService); + }); it('metadata api should return one entry for each host with default paging', async () => { const { body } = await supertest .post('/api/endpoint/metadata') @@ -121,7 +135,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ filters: { - kql: 'not host.ip:10.46.229.234', + kql: 'not HostDetails.host.ip:10.46.229.234', }, }) .expect(200); @@ -146,7 +160,7 @@ export default function ({ getService }: FtrProviderContext) { }, ], filters: { - kql: `not host.ip:${notIncludedIp}`, + kql: `not HostDetails.host.ip:${notIncludedIp}`, }, }) .expect(200); @@ -154,12 +168,14 @@ export default function ({ getService }: FtrProviderContext) { const resultIps: string[] = [].concat( ...body.hosts.map((hostInfo: Record) => hostInfo.metadata.host.ip) ); - expect(resultIps).to.eql([ - '10.192.213.130', - '10.70.28.129', - '10.101.149.26', - '2606:a000:ffc0:39:11ef:37b9:3371:578c', - ]); + expect(resultIps.sort()).to.eql( + [ + '10.192.213.130', + '10.70.28.129', + '10.101.149.26', + '2606:a000:ffc0:39:11ef:37b9:3371:578c', + ].sort() + ); expect(resultIps).not.include.eql(notIncludedIp); expect(body.hosts.length).to.eql(2); expect(body.request_page_size).to.eql(10); @@ -173,7 +189,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ filters: { - kql: `host.os.Ext.variant:${variantValue}`, + kql: `HostDetails.host.os.Ext.variant:${variantValue}`, }, }) .expect(200); @@ -194,7 +210,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ filters: { - kql: `host.ip:${targetEndpointIp}`, + kql: `HostDetails.host.ip:${targetEndpointIp}`, }, }) .expect(200); @@ -215,7 +231,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ filters: { - kql: `not Endpoint.policy.applied.status:success`, + kql: `not HostDetails.Endpoint.policy.applied.status:success`, }, }) .expect(200); @@ -236,7 +252,7 @@ export default function ({ getService }: FtrProviderContext) { .set('kbn-xsrf', 'xxx') .send({ filters: { - kql: `elastic.agent.id:${targetElasticAgentId}`, + kql: `HostDetails.elastic.agent.id:${targetElasticAgentId}`, }, }) .expect(200); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts index 82d844aae8016..bf7ed711b75a5 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/alerts.ts @@ -4,7 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { eventId } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { + eventIDSafeVersion, + timestampSafeVersion, +} from '../../../../plugins/security_solution/common/endpoint/models/event'; import { ResolverRelatedAlerts } from '../../../../plugins/security_solution/common/endpoint/types'; import { FtrProviderContext } from '../../ftr_provider_context'; import { @@ -69,7 +72,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should allow alerts to be filtered', async () => { - const filter = `not event.id:"${tree.origin.relatedAlerts[0].event.id}"`; + const filter = `not event.id:"${tree.origin.relatedAlerts[0].event?.id}"`; const { body }: { body: ResolverRelatedAlerts } = await supertest .post(`/api/endpoint/resolver/${tree.origin.id}/alerts`) .set('kbn-xsrf', 'xxx') @@ -84,7 +87,7 @@ export default function ({ getService }: FtrProviderContext) { // should not find the alert that we excluded in the filter expect( body.alerts.find((bodyAlert) => { - return eventId(bodyAlert) === tree.origin.relatedAlerts[0].event.id; + return eventIDSafeVersion(bodyAlert) === tree.origin.relatedAlerts[0].event?.id; }) ).to.not.be.ok(); }); @@ -135,14 +138,16 @@ export default function ({ getService }: FtrProviderContext) { .expect(200); const sortedAsc = [...tree.origin.relatedAlerts].sort((event1, event2) => { // this sorts the events by timestamp in ascending order - const diff = event1['@timestamp'] - event2['@timestamp']; + const diff = (timestampSafeVersion(event1) ?? 0) - (timestampSafeVersion(event2) ?? 0); + const event1ID = eventIDSafeVersion(event1) ?? 0; + const event2ID = eventIDSafeVersion(event2) ?? 0; // if the timestamps are the same, fallback to the event.id sorted in // ascending order if (diff === 0) { - if (event1.event.id < event2.event.id) { + if (event1ID < event2ID) { return -1; } - if (event1.event.id > event2.event.id) { + if (event1ID > event2ID) { return 1; } return 0; @@ -152,7 +157,7 @@ export default function ({ getService }: FtrProviderContext) { expect(body.alerts.length).to.eql(4); for (let i = 0; i < body.alerts.length; i++) { - expect(eventId(body.alerts[i])).to.equal(sortedAsc[i].event.id); + expect(eventIDSafeVersion(body.alerts[i])).to.equal(sortedAsc[i].event?.id); } }); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts index 2dec3c755a93b..49e24ff67fa77 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/children.ts @@ -5,14 +5,17 @@ */ import expect from '@kbn/expect'; import { SearchResponse } from 'elasticsearch'; -import { entityId } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { + entityIDSafeVersion, + timestampSafeVersion, +} from '../../../../plugins/security_solution/common/endpoint/models/event'; import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; import { ChildrenPaginationBuilder } from '../../../../plugins/security_solution/server/endpoint/routes/resolver/utils/children_pagination'; import { ChildrenQuery } from '../../../../plugins/security_solution/server/endpoint/routes/resolver/queries/children'; import { - ResolverTree, - ResolverEvent, - ResolverChildren, + SafeResolverTree, + SafeResolverEvent, + SafeResolverChildren, } from '../../../../plugins/security_solution/common/endpoint/types'; import { FtrProviderContext } from '../../ftr_provider_context'; import { @@ -20,6 +23,7 @@ import { EndpointDocGenerator, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; import { InsertedEvents } from '../../services/resolver'; +import { createAncestryArray } from './common'; export default function resolverAPIIntegrationTests({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -40,20 +44,20 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC // Origin -> infoEvent -> startEvent -> execEvent origin = generator.generateEvent(); infoEvent = generator.generateEvent({ - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), eventType: ['info'], }); startEvent = generator.generateEvent({ - parentEntityID: infoEvent.process.entity_id, - ancestry: [infoEvent.process.entity_id, origin.process.entity_id], + parentEntityID: entityIDSafeVersion(infoEvent), + ancestry: createAncestryArray([infoEvent, origin]), eventType: ['start'], }); execEvent = generator.generateEvent({ - parentEntityID: startEvent.process.entity_id, - ancestry: [startEvent.process.entity_id, infoEvent.process.entity_id], + parentEntityID: entityIDSafeVersion(startEvent), + ancestry: createAncestryArray([startEvent, infoEvent]), eventType: ['change'], }); genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); @@ -64,13 +68,13 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC }); it('finds all the children of the origin', async () => { - const { body }: { body: ResolverTree } = await supertest - .get(`/api/endpoint/resolver/${origin.process.entity_id}?children=100`) + const { body }: { body: SafeResolverTree } = await supertest + .get(`/api/endpoint/resolver/${origin.process?.entity_id}?children=100`) .expect(200); expect(body.children.childNodes.length).to.be(3); - expect(body.children.childNodes[0].entityID).to.be(infoEvent.process.entity_id); - expect(body.children.childNodes[1].entityID).to.be(startEvent.process.entity_id); - expect(body.children.childNodes[2].entityID).to.be(execEvent.process.entity_id); + expect(body.children.childNodes[0].entityID).to.be(infoEvent.process?.entity_id); + expect(body.children.childNodes[1].entityID).to.be(startEvent.process?.entity_id); + expect(body.children.childNodes[2].entityID).to.be(execEvent.process?.entity_id); }); }); @@ -86,23 +90,23 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC // Origin -> (infoEvent, startEvent, execEvent are all for the same node) origin = generator.generateEvent(); startEvent = generator.generateEvent({ - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), eventType: ['start'], }); infoEvent = generator.generateEvent({ - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], - entityID: startEvent.process.entity_id, + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), + entityID: entityIDSafeVersion(startEvent), eventType: ['info'], }); execEvent = generator.generateEvent({ - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), eventType: ['change'], - entityID: startEvent.process.entity_id, + entityID: entityIDSafeVersion(startEvent), }); genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); }); @@ -117,12 +121,12 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC eventsIndexPattern ); // [1] here gets the body portion of the array - const [, query] = childrenQuery.buildMSearch(origin.process.entity_id); - const { body } = await es.search>({ body: query }); + const [, query] = childrenQuery.buildMSearch(entityIDSafeVersion(origin) ?? ''); + const { body } = await es.search>({ body: query }); expect(body.hits.hits.length).to.be(1); const event = body.hits.hits[0]._source; - expect(entityId(event)).to.be(startEvent.process.entity_id); + expect(entityIDSafeVersion(event)).to.be(startEvent.process?.entity_id); expect(event.event?.type).to.eql(['start']); }); }); @@ -139,25 +143,25 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC // Origin -> (infoEvent, startEvent, execEvent are all for the same node) origin = generator.generateEvent(); startEvent = generator.generateEvent({ - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), eventType: ['start'], }); infoEvent = generator.generateEvent({ - timestamp: startEvent['@timestamp'] + 100, - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], - entityID: startEvent.process.entity_id, + timestamp: (timestampSafeVersion(startEvent) ?? 0) + 100, + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), + entityID: entityIDSafeVersion(startEvent), eventType: ['info'], }); execEvent = generator.generateEvent({ - timestamp: infoEvent['@timestamp'] + 100, - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], + timestamp: (timestampSafeVersion(infoEvent) ?? 0) + 100, + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), eventType: ['change'], - entityID: startEvent.process.entity_id, + entityID: entityIDSafeVersion(startEvent), }); genData = await resolver.insertEvents([origin, infoEvent, startEvent, execEvent]); }); @@ -167,37 +171,37 @@ export default function resolverAPIIntegrationTests({ getService }: FtrProviderC }); it('retrieves the same node three times', async () => { - let { body }: { body: ResolverChildren } = await supertest - .get(`/api/endpoint/resolver/${origin.process.entity_id}/children?children=1`) + let { body }: { body: SafeResolverChildren } = await supertest + .get(`/api/endpoint/resolver/${origin.process?.entity_id}/children?children=1`) .expect(200); expect(body.childNodes.length).to.be(1); expect(body.nextChild).to.not.be(null); - expect(body.childNodes[0].entityID).to.be(startEvent.process.entity_id); - expect(body.childNodes[0].lifecycle[0].event?.type).to.eql(startEvent.event.type); + expect(body.childNodes[0].entityID).to.be(startEvent.process?.entity_id); + expect(body.childNodes[0].lifecycle[0].event?.type).to.eql(startEvent.event?.type); ({ body } = await supertest .get( - `/api/endpoint/resolver/${origin.process.entity_id}/children?children=1&afterChild=${body.nextChild}` + `/api/endpoint/resolver/${origin.process?.entity_id}/children?children=1&afterChild=${body.nextChild}` ) .expect(200)); expect(body.childNodes.length).to.be(1); expect(body.nextChild).to.not.be(null); - expect(body.childNodes[0].entityID).to.be(infoEvent.process.entity_id); - expect(body.childNodes[0].lifecycle[1].event?.type).to.eql(infoEvent.event.type); + expect(body.childNodes[0].entityID).to.be(infoEvent.process?.entity_id); + expect(body.childNodes[0].lifecycle[1].event?.type).to.eql(infoEvent.event?.type); ({ body } = await supertest .get( - `/api/endpoint/resolver/${origin.process.entity_id}/children?children=1&afterChild=${body.nextChild}` + `/api/endpoint/resolver/${origin.process?.entity_id}/children?children=1&afterChild=${body.nextChild}` ) .expect(200)); expect(body.childNodes.length).to.be(1); expect(body.nextChild).to.not.be(null); - expect(body.childNodes[0].entityID).to.be(infoEvent.process.entity_id); - expect(body.childNodes[0].lifecycle[2].event?.type).to.eql(execEvent.event.type); + expect(body.childNodes[0].entityID).to.be(infoEvent.process?.entity_id); + expect(body.childNodes[0].lifecycle[2].event?.type).to.eql(execEvent.event?.type); ({ body } = await supertest .get( - `/api/endpoint/resolver/${origin.process.entity_id}/children?children=1&afterChild=${body.nextChild}` + `/api/endpoint/resolver/${origin.process?.entity_id}/children?children=1&afterChild=${body.nextChild}` ) .expect(200)); expect(body.childNodes.length).to.be(0); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts index 92d14fb94a2d8..2c59863099ae7 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/common.ts @@ -6,14 +6,15 @@ import _ from 'lodash'; import expect from '@kbn/expect'; import { - ResolverChildNode, - ResolverLifecycleNode, - ResolverEvent, + SafeResolverChildNode, + SafeResolverLifecycleNode, + SafeResolverEvent, ResolverNodeStats, } from '../../../../plugins/security_solution/common/endpoint/types'; import { - parentEntityId, - eventId, + parentEntityIDSafeVersion, + entityIDSafeVersion, + eventIDSafeVersion, } from '../../../../plugins/security_solution/common/endpoint/models/event'; import { Event, @@ -23,13 +24,33 @@ import { categoryMapping, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; +/** + * Creates the ancestry array based on an array of events. The order of the ancestry array will match the order + * of the events passed in. + * + * @param events an array of generated events + */ +export const createAncestryArray = (events: Event[]) => { + const ancestry: string[] = []; + for (const event of events) { + const entityID = entityIDSafeVersion(event); + if (entityID) { + ancestry.push(entityID); + } + } + return ancestry; +}; + /** * Check that the given lifecycle is in the resolver tree's corresponding map * * @param node a lifecycle node containing the start and end events for a node * @param nodeMap a map of entity_ids to nodes to look for the passed in `node` */ -const expectLifecycleNodeInMap = (node: ResolverLifecycleNode, nodeMap: Map) => { +const expectLifecycleNodeInMap = ( + node: SafeResolverLifecycleNode, + nodeMap: Map +) => { const genNode = nodeMap.get(node.entityID); expect(genNode).to.be.ok(); compareArrays(genNode!.lifecycle, node.lifecycle, true); @@ -44,7 +65,7 @@ const expectLifecycleNodeInMap = (node: ResolverLifecycleNode, nodeMap: Map { @@ -52,7 +73,7 @@ export const verifyAncestry = ( const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); // group by parent entity_id const groupedAncestorsParent = _.groupBy(ancestors, (ancestor) => - parentEntityId(ancestor.lifecycle[0]) + parentEntityIDSafeVersion(ancestor.lifecycle[0]) ); // make sure there aren't any nodes with the same entity_id expect(Object.keys(groupedAncestors).length).to.eql(ancestors.length); @@ -69,7 +90,7 @@ export const verifyAncestry = ( let foundParents = 0; let node = ancestors[0]; for (let i = 0; i < ancestors.length; i++) { - const parentID = parentEntityId(node.lifecycle[0]); + const parentID = parentEntityIDSafeVersion(node.lifecycle[0]); if (parentID !== undefined) { const nextNode = groupedAncestors[parentID]; if (!nextNode) { @@ -95,12 +116,12 @@ export const verifyAncestry = ( * * @param ancestors an array of ancestor nodes */ -export const retrieveDistantAncestor = (ancestors: ResolverLifecycleNode[]) => { +export const retrieveDistantAncestor = (ancestors: SafeResolverLifecycleNode[]) => { // group the ancestors by their entity_id mapped to a lifecycle node const groupedAncestors = _.groupBy(ancestors, (ancestor) => ancestor.entityID); let node = ancestors[0]; for (let i = 0; i < ancestors.length; i++) { - const parentID = parentEntityId(node.lifecycle[0]); + const parentID = parentEntityIDSafeVersion(node.lifecycle[0]); if (parentID !== undefined) { const nextNode = groupedAncestors[parentID]; if (nextNode) { @@ -122,7 +143,7 @@ export const retrieveDistantAncestor = (ancestors: ResolverLifecycleNode[]) => { * @param childrenPerParent an optional number to compare that there are a certain number of children for each parent */ export const verifyChildren = ( - children: ResolverChildNode[], + children: SafeResolverChildNode[], tree: Tree, numberOfParents?: number, childrenPerParent?: number @@ -132,7 +153,9 @@ export const verifyChildren = ( // make sure each child is unique expect(Object.keys(groupedChildren).length).to.eql(children.length); if (numberOfParents !== undefined) { - const groupParent = _.groupBy(children, (child) => parentEntityId(child.lifecycle[0])); + const groupParent = _.groupBy(children, (child) => + parentEntityIDSafeVersion(child.lifecycle[0]) + ); expect(Object.keys(groupParent).length).to.eql(numberOfParents); if (childrenPerParent !== undefined) { Object.values(groupParent).forEach((childNodes) => @@ -155,7 +178,7 @@ export const verifyChildren = ( */ export const compareArrays = ( expected: Event[], - toTest: ResolverEvent[], + toTest: SafeResolverEvent[], lengthCheck: boolean = false ) => { if (lengthCheck) { @@ -168,7 +191,7 @@ export const compareArrays = ( // we're only checking that the event ids are the same here. The reason we can't check the entire document // is because ingest pipelines are used to add fields to the document when it is received by elasticsearch, // therefore it will not be the same as the document created by the generator - return eventId(toTestEvent) === eventId(arrEvent); + return eventIDSafeVersion(toTestEvent) === eventIDSafeVersion(arrEvent); }) ).to.be.ok(); }); @@ -212,7 +235,7 @@ export const verifyStats = ( * @param categories the related event info used when generating the resolver tree */ export const verifyLifecycleStats = ( - nodes: ResolverLifecycleNode[], + nodes: SafeResolverLifecycleNode[], categories: RelatedEventInfo[], relatedAlerts: number ) => { diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts index cb6c49e17c712..e6d5e8fccd00d 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/entity_id.ts @@ -4,9 +4,10 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; +import { entityIDSafeVersion } from '../../../../plugins/security_solution/common/endpoint/models/event'; import { eventsIndexPattern } from '../../../../plugins/security_solution/common/endpoint/constants'; import { - ResolverTree, + SafeResolverTree, ResolverEntityIndex, } from '../../../../plugins/security_solution/common/endpoint/types'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -15,19 +16,26 @@ import { Event, } from '../../../../plugins/security_solution/common/endpoint/generate_data'; import { InsertedEvents } from '../../services/resolver'; +import { createAncestryArray } from './common'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); const resolver = getService('resolverGenerator'); const generator = new EndpointDocGenerator('resolver'); + const setEntityIDEmptyString = (event: Event) => { + if (event.process?.entity_id) { + event.process.entity_id = ''; + } + }; + describe('Resolver handling of entity ids', () => { describe('entity api', () => { let origin: Event; let genData: InsertedEvents; before(async () => { origin = generator.generateEvent({ parentEntityID: 'a' }); - origin.process.entity_id = ''; + setEntityIDEmptyString(origin); genData = await resolver.insertEvents([origin]); }); @@ -57,16 +65,16 @@ export default function ({ getService }: FtrProviderContext) { // should not be returned by the backend. origin = generator.generateEvent({ entityID: 'a' }); childNoEntityID = generator.generateEvent({ - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), }); // force it to be empty - childNoEntityID.process.entity_id = ''; + setEntityIDEmptyString(childNoEntityID); childWithEntityID = generator.generateEvent({ entityID: 'b', - parentEntityID: origin.process.entity_id, - ancestry: [origin.process.entity_id], + parentEntityID: entityIDSafeVersion(origin), + ancestry: createAncestryArray([origin]), }); events = [origin, childNoEntityID, childWithEntityID]; genData = await resolver.insertEvents(events); @@ -77,11 +85,11 @@ export default function ({ getService }: FtrProviderContext) { }); it('does not find children without a process entity_id', async () => { - const { body }: { body: ResolverTree } = await supertest - .get(`/api/endpoint/resolver/${origin.process.entity_id}`) + const { body }: { body: SafeResolverTree } = await supertest + .get(`/api/endpoint/resolver/${origin.process?.entity_id}`) .expect(200); expect(body.children.childNodes.length).to.be(1); - expect(body.children.childNodes[0].entityID).to.be(childWithEntityID.process.entity_id); + expect(body.children.childNodes[0].entityID).to.be(childWithEntityID.process?.entity_id); }); }); @@ -101,21 +109,21 @@ export default function ({ getService }: FtrProviderContext) { }); ancestor1 = generator.generateEvent({ entityID: '1', - parentEntityID: ancestor2.process.entity_id, - ancestry: [ancestor2.process.entity_id], + parentEntityID: entityIDSafeVersion(ancestor2), + ancestry: createAncestryArray([ancestor2]), }); // we'll insert an event that doesn't have an entity id so if the backend does search for it, it should be // returned and our test should fail ancestorNoEntityID = generator.generateEvent({ - ancestry: [ancestor2.process.entity_id], + ancestry: createAncestryArray([ancestor2]), }); - ancestorNoEntityID.process.entity_id = ''; + setEntityIDEmptyString(ancestorNoEntityID); origin = generator.generateEvent({ entityID: 'a', - parentEntityID: ancestor1.process.entity_id, - ancestry: ['', ancestor2.process.entity_id], + parentEntityID: entityIDSafeVersion(ancestor1), + ancestry: ['', ...createAncestryArray([ancestor2])], }); events = [origin, ancestor1, ancestor2, ancestorNoEntityID]; @@ -127,11 +135,11 @@ export default function ({ getService }: FtrProviderContext) { }); it('does not query for ancestors that have an empty string for the entity_id', async () => { - const { body }: { body: ResolverTree } = await supertest - .get(`/api/endpoint/resolver/${origin.process.entity_id}`) + const { body }: { body: SafeResolverTree } = await supertest + .get(`/api/endpoint/resolver/${origin.process?.entity_id}`) .expect(200); expect(body.ancestry.ancestors.length).to.be(1); - expect(body.ancestry.ancestors[0].entityID).to.be(ancestor2.process.entity_id); + expect(body.ancestry.ancestors[0].entityID).to.be(ancestor2.process?.entity_id); }); }); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts index c0e4e466c7b62..4e248f52ec297 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/events.ts @@ -4,8 +4,8 @@ * you may not use this file except in compliance with the Elastic License. */ import expect from '@kbn/expect'; -import { eventId } from '../../../../plugins/security_solution/common/endpoint/models/event'; -import { ResolverRelatedEvents } from '../../../../plugins/security_solution/common/endpoint/types'; +import { eventIDSafeVersion } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { SafeResolverRelatedEvents } from '../../../../plugins/security_solution/common/endpoint/types'; import { FtrProviderContext } from '../../ftr_provider_context'; import { Tree, @@ -59,7 +59,7 @@ export default function ({ getService }: FtrProviderContext) { const cursor = 'eyJ0aW1lc3RhbXAiOjE1ODE0NTYyNTUwMDAsImV2ZW50SUQiOiI5NDA0MyJ9'; it('should return details for the root node', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}`) .set('kbn-xsrf', 'xxx') .expect(200); @@ -69,7 +69,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('returns no values when there is no more data', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest // after is set to the document id of the last event so there shouldn't be any more after it .post( `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=${cursor}` @@ -82,7 +82,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post( `/api/endpoint/resolver/${entityID}/events?legacyEndpointID=${endpointID}&afterEvent=blah` ) @@ -93,7 +93,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return no results for an invalid endpoint ID', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/${entityID}/events?legacyEndpointID=foo`) .set('kbn-xsrf', 'xxx') .expect(200); @@ -120,7 +120,7 @@ export default function ({ getService }: FtrProviderContext) { describe('endpoint events', () => { it('should not find any events', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/5555/events`) .set('kbn-xsrf', 'xxx') .expect(200); @@ -129,7 +129,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return details for the root node', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/${tree.origin.id}/events`) .set('kbn-xsrf', 'xxx') .expect(200); @@ -140,7 +140,7 @@ export default function ({ getService }: FtrProviderContext) { it('should allow for the events to be filtered', async () => { const filter = `event.category:"${RelatedEventCategory.Driver}"`; - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/${tree.origin.id}/events`) .set('kbn-xsrf', 'xxx') .send({ @@ -156,7 +156,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return paginated results for the root node', async () => { - let { body }: { body: ResolverRelatedEvents } = await supertest + let { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/${tree.origin.id}/events?events=2`) .set('kbn-xsrf', 'xxx') .expect(200); @@ -185,7 +185,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should return the first page of information when the cursor is invalid', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/${tree.origin.id}/events?afterEvent=blah`) .set('kbn-xsrf', 'xxx') .expect(200); @@ -195,7 +195,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should sort the events in descending order', async () => { - const { body }: { body: ResolverRelatedEvents } = await supertest + const { body }: { body: SafeResolverRelatedEvents } = await supertest .post(`/api/endpoint/resolver/${tree.origin.id}/events`) .set('kbn-xsrf', 'xxx') .expect(200); @@ -204,8 +204,8 @@ export default function ({ getService }: FtrProviderContext) { // the last element in the array so let's reverse it const relatedEvents = tree.origin.relatedEvents.reverse(); for (let i = 0; i < body.events.length; i++) { - expect(body.events[i].event?.category).to.equal(relatedEvents[i].event.category); - expect(eventId(body.events[i])).to.equal(relatedEvents[i].event.id); + expect(body.events[i].event?.category).to.equal(relatedEvents[i].event?.category); + expect(eventIDSafeVersion(body.events[i])).to.equal(relatedEvents[i].event?.id); } }); }); diff --git a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts index 957d559087f5e..837af6a940f5c 100644 --- a/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts +++ b/x-pack/test/security_solution_endpoint_api_int/apis/resolver/tree.ts @@ -5,12 +5,12 @@ */ import expect from '@kbn/expect'; import { - ResolverAncestry, - ResolverChildren, - ResolverTree, - LegacyEndpointEvent, + SafeResolverAncestry, + SafeResolverChildren, + SafeResolverTree, + SafeLegacyEndpointEvent, } from '../../../../plugins/security_solution/common/endpoint/types'; -import { parentEntityId } from '../../../../plugins/security_solution/common/endpoint/models/event'; +import { parentEntityIDSafeVersion } from '../../../../plugins/security_solution/common/endpoint/models/event'; import { FtrProviderContext } from '../../ftr_provider_context'; import { Tree, @@ -71,7 +71,7 @@ export default function ({ getService }: FtrProviderContext) { const entityID = '94042'; it('should return details for the root node', async () => { - const { body }: { body: ResolverAncestry } = await supertest + const { body }: { body: SafeResolverAncestry } = await supertest .get( `/api/endpoint/resolver/${entityID}/ancestry?legacyEndpointID=${endpointID}&ancestors=5` ) @@ -82,7 +82,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should have a populated next parameter', async () => { - const { body }: { body: ResolverAncestry } = await supertest + const { body }: { body: SafeResolverAncestry } = await supertest .get( `/api/endpoint/resolver/${entityID}/ancestry?legacyEndpointID=${endpointID}&ancestors=0` ) @@ -91,7 +91,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should handle an ancestors param request', async () => { - let { body }: { body: ResolverAncestry } = await supertest + let { body }: { body: SafeResolverAncestry } = await supertest .get( `/api/endpoint/resolver/${entityID}/ancestry?legacyEndpointID=${endpointID}&ancestors=0` ) @@ -110,14 +110,14 @@ export default function ({ getService }: FtrProviderContext) { describe('endpoint events', () => { it('should return the origin node at the front of the array', async () => { - const { body }: { body: ResolverAncestry } = await supertest + const { body }: { body: SafeResolverAncestry } = await supertest .get(`/api/endpoint/resolver/${tree.origin.id}/ancestry?ancestors=9`) .expect(200); expect(body.ancestors[0].entityID).to.eql(tree.origin.id); }); it('should return details for the root node', async () => { - const { body }: { body: ResolverAncestry } = await supertest + const { body }: { body: SafeResolverAncestry } = await supertest .get(`/api/endpoint/resolver/${tree.origin.id}/ancestry?ancestors=9`) .expect(200); // the tree we generated had 5 ancestors + 1 origin node @@ -128,7 +128,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('should handle an invalid id', async () => { - const { body }: { body: ResolverAncestry } = await supertest + const { body }: { body: SafeResolverAncestry } = await supertest .get(`/api/endpoint/resolver/alskdjflasj/ancestry`) .expect(200); expect(body.ancestors).to.be.empty(); @@ -136,18 +136,20 @@ export default function ({ getService }: FtrProviderContext) { }); it('should have a populated next parameter', async () => { - const { body }: { body: ResolverAncestry } = await supertest + const { body }: { body: SafeResolverAncestry } = await supertest .get(`/api/endpoint/resolver/${tree.origin.id}/ancestry?ancestors=2`) .expect(200); // it should have 2 ancestors + 1 origin expect(body.ancestors.length).to.eql(3); verifyAncestry(body.ancestors, tree, false); const distantGrandparent = retrieveDistantAncestor(body.ancestors); - expect(body.nextAncestor).to.eql(parentEntityId(distantGrandparent.lifecycle[0])); + expect(body.nextAncestor).to.eql( + parentEntityIDSafeVersion(distantGrandparent.lifecycle[0]) + ); }); it('should handle multiple ancestor requests', async () => { - let { body }: { body: ResolverAncestry } = await supertest + let { body }: { body: SafeResolverAncestry } = await supertest .get(`/api/endpoint/resolver/${tree.origin.id}/ancestry?ancestors=3`) .expect(200); expect(body.ancestors.length).to.eql(4); @@ -171,7 +173,7 @@ export default function ({ getService }: FtrProviderContext) { const entityID = '94041'; it('returns child process lifecycle events', async () => { - const { body }: { body: ResolverChildren } = await supertest + const { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}`) .expect(200); expect(body.childNodes.length).to.eql(1); @@ -179,12 +181,12 @@ export default function ({ getService }: FtrProviderContext) { expect( // for some reason the ts server doesn't think `endgame` exists even though we're using ResolverEvent // here, so to avoid it complaining we'll just force it - (body.childNodes[0].lifecycle[0] as LegacyEndpointEvent).endgame.unique_pid + (body.childNodes[0].lifecycle[0] as SafeLegacyEndpointEvent).endgame.unique_pid ).to.eql(94042); }); it('returns multiple levels of child process lifecycle events', async () => { - const { body }: { body: ResolverChildren } = await supertest + const { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/93802/children?legacyEndpointID=${endpointID}&children=10`) .expect(200); expect(body.childNodes.length).to.eql(10); @@ -193,12 +195,12 @@ export default function ({ getService }: FtrProviderContext) { expect( // for some reason the ts server doesn't think `endgame` exists even though we're using ResolverEvent // here, so to avoid it complaining we'll just force it - (body.childNodes[0].lifecycle[0] as LegacyEndpointEvent).endgame.unique_pid + (body.childNodes[0].lifecycle[0] as SafeLegacyEndpointEvent).endgame.unique_pid ).to.eql(93932); }); it('returns no values when there is no more data', async () => { - let { body }: { body: ResolverChildren } = await supertest + let { body }: { body: SafeResolverChildren } = await supertest .get( // there should only be a single child for this node `/api/endpoint/resolver/94041/children?legacyEndpointID=${endpointID}&children=1` @@ -216,7 +218,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('returns the first page of information when the cursor is invalid', async () => { - const { body }: { body: ResolverChildren } = await supertest + const { body }: { body: SafeResolverChildren } = await supertest .get( `/api/endpoint/resolver/${entityID}/children?legacyEndpointID=${endpointID}&afterChild=blah` ) @@ -236,7 +238,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('returns empty events without a matching entity id', async () => { - const { body }: { body: ResolverChildren } = await supertest + const { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/5555/children`) .expect(200); expect(body.nextChild).to.eql(null); @@ -244,7 +246,7 @@ export default function ({ getService }: FtrProviderContext) { }); it('returns empty events with an invalid endpoint id', async () => { - const { body }: { body: ResolverChildren } = await supertest + const { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/${entityID}/children?legacyEndpointID=foo`) .expect(200); expect(body.nextChild).to.eql(null); @@ -254,7 +256,7 @@ export default function ({ getService }: FtrProviderContext) { describe('endpoint events', () => { it('returns all children for the origin', async () => { - const { body }: { body: ResolverChildren } = await supertest + const { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/${tree.origin.id}/children?children=100`) .expect(200); // there are 2 levels in the children part of the tree and 3 nodes for each = @@ -269,7 +271,7 @@ export default function ({ getService }: FtrProviderContext) { // this gets a node should have 3 children which were created in succession so that the timestamps // are ordered correctly to be retrieved in a single call const distantChildEntityID = Array.from(tree.childrenLevels[0].values())[0].id; - const { body }: { body: ResolverChildren } = await supertest + const { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/${distantChildEntityID}/children?children=3`) .expect(200); expect(body.childNodes.length).to.eql(3); @@ -281,7 +283,7 @@ export default function ({ getService }: FtrProviderContext) { // this gets a node should have 3 children which were created in succession so that the timestamps // are ordered correctly to be retrieved in a single call const distantChildEntityID = Array.from(tree.childrenLevels[0].values())[0].id; - let { body }: { body: ResolverChildren } = await supertest + let { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/${distantChildEntityID}/children?children=1`) .expect(200); expect(body.childNodes.length).to.eql(1); @@ -308,7 +310,7 @@ export default function ({ getService }: FtrProviderContext) { it('gets all children in two queries', async () => { // should get all the children of the origin - let { body }: { body: ResolverChildren } = await supertest + let { body }: { body: SafeResolverChildren } = await supertest .get(`/api/endpoint/resolver/${tree.origin.id}/children?children=3`) .expect(200); expect(body.childNodes.length).to.eql(3); @@ -334,7 +336,7 @@ export default function ({ getService }: FtrProviderContext) { const endpointID = '5a0c957f-b8e7-4538-965e-57e8bb86ad3a'; it('returns ancestors, events, children, and current process lifecycle', async () => { - const { body }: { body: ResolverTree } = await supertest + const { body }: { body: SafeResolverTree } = await supertest .get(`/api/endpoint/resolver/93933?legacyEndpointID=${endpointID}`) .expect(200); expect(body.ancestry.nextAncestor).to.equal(null); @@ -348,7 +350,7 @@ export default function ({ getService }: FtrProviderContext) { describe('endpoint events', () => { it('returns a tree', async () => { - const { body }: { body: ResolverTree } = await supertest + const { body }: { body: SafeResolverTree } = await supertest .get( `/api/endpoint/resolver/${tree.origin.id}?children=100&ancestors=5&events=5&alerts=5` ) diff --git a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts index 7e4d4177affac..c5855281f55c9 100644 --- a/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts +++ b/x-pack/test/security_solution_endpoint_api_int/services/resolver.ts @@ -9,6 +9,7 @@ import { EndpointDocGenerator, Event, } from '../../../plugins/security_solution/common/endpoint/generate_data'; +import { firstNonNullValue } from '../../../plugins/security_solution/common/endpoint/models/ecs_safety_helpers'; import { FtrProviderContext } from '../ftr_provider_context'; export const processEventsIndex = 'logs-endpoint.events.process-default'; @@ -87,7 +88,7 @@ export function ResolverGeneratorProvider({ getService }: FtrProviderContext) { const tree = generator.generateTree(options); const body = tree.allEvents.reduce((array: Array, doc) => { let index = eventsIndex; - if (doc.event.kind === 'alert') { + if (firstNonNullValue(doc.event?.kind) === 'alert') { index = alertsIndex; } /** diff --git a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts index 5c80b4283a69b..a950b4fc3d70a 100644 --- a/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts +++ b/x-pack/test/ui_capabilities/common/fixtures/plugins/foo_plugin/server/index.ts @@ -14,7 +14,7 @@ interface SetupDeps { class FooPlugin implements Plugin { setup(core: CoreSetup, plugins: SetupDeps) { - plugins.features.registerFeature({ + plugins.features.registerKibanaFeature({ id: 'foo', name: 'Foo', icon: 'upArrow', diff --git a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts index d9c27d67ae329..dde99e7409dee 100644 --- a/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_and_spaces/tests/catalogue.ts @@ -13,6 +13,8 @@ import { UserAtSpaceScenarios } from '../scenarios'; export default function catalogueTests({ getService }: FtrProviderContext) { const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + const esFeatureExceptions = ['security', 'rollup_jobs', 'reporting', 'transform', 'watcher']; + describe('catalogue', () => { UserAtSpaceScenarios.forEach((scenario) => { it(`${scenario.id}`, async () => { @@ -35,13 +37,14 @@ export default function catalogueTests({ getService }: FtrProviderContext) { case 'dual_privileges_all at everything_space': { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); - // everything except ml and monitoring is enabled + // everything except ml, monitoring, and ES features are enabled const expected = mapValues( uiCapabilities.value!.catalogue, (enabled, catalogueId) => catalogueId !== 'ml' && catalogueId !== 'ml_file_data_visualizer' && - catalogueId !== 'monitoring' + catalogueId !== 'monitoring' && + !esFeatureExceptions.includes(catalogueId) ); expect(uiCapabilities.value!.catalogue).to.eql(expected); break; @@ -52,7 +55,8 @@ export default function catalogueTests({ getService }: FtrProviderContext) { case 'everything_space_read at everything_space': { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); - // everything except ml and monitoring and enterprise search is enabled + // everything except spaces, ml, monitoring, the enterprise search suite, and ES features are enabled + // (easier to say: all "proper" Kibana features are enabled) const exceptions = [ 'ml', 'ml_file_data_visualizer', @@ -60,6 +64,8 @@ export default function catalogueTests({ getService }: FtrProviderContext) { 'enterpriseSearch', 'appSearch', 'workplaceSearch', + 'spaces', + ...esFeatureExceptions, ]; const expected = mapValues( uiCapabilities.value!.catalogue, @@ -68,10 +74,36 @@ export default function catalogueTests({ getService }: FtrProviderContext) { expect(uiCapabilities.value!.catalogue).to.eql(expected); break; } - // the nothing_space has no features enabled, so even if we have - // privileges to perform these actions, we won't be able to - case 'superuser at nothing_space': + // the nothing_space has no Kibana features enabled, so even if we have + // privileges to perform these actions, we won't be able to. + // Note that ES features may still be enabled if the user has privileges, since + // they cannot be disabled at the space level at this time. + case 'superuser at nothing_space': { + expect(uiCapabilities.success).to.be(true); + expect(uiCapabilities.value).to.have.property('catalogue'); + // everything is disabled except for the es feature exceptions and spaces management + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => + esFeatureExceptions.includes(catalogueId) || catalogueId === 'spaces' + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + // the nothing_space has no Kibana features enabled, so even if we have + // privileges to perform these actions, we won't be able to. case 'global_all at nothing_space': + case 'dual_privileges_all at nothing_space': { + // everything is disabled except for spaces management + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => catalogueId === 'spaces' + ); + expect(uiCapabilities.value!.catalogue).to.eql(expected); + break; + } + // the nothing_space has no Kibana features enabled, so even if we have + // privileges to perform these actions, we won't be able to. case 'global_read at nothing_space': case 'dual_privileges_all at nothing_space': case 'dual_privileges_read at nothing_space': @@ -88,7 +120,10 @@ export default function catalogueTests({ getService }: FtrProviderContext) { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); // everything is disabled - const expected = mapValues(uiCapabilities.value!.catalogue, () => false); + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => false + ); expect(uiCapabilities.value!.catalogue).to.eql(expected); break; } diff --git a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts index 7852167fcc1cb..1f19228b2d958 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/catalogue.ts @@ -13,6 +13,8 @@ import { UserScenarios } from '../scenarios'; export default function catalogueTests({ getService }: FtrProviderContext) { const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + const esFeatureExceptions = ['security', 'rollup_jobs', 'reporting', 'transform', 'watcher']; + describe('catalogue', () => { UserScenarios.forEach((scenario) => { it(`${scenario.fullName}`, async () => { @@ -35,13 +37,14 @@ export default function catalogueTests({ getService }: FtrProviderContext) { case 'dual_privileges_all': { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); - // everything except ml and monitoring is enabled + // everything except ml, monitoring, and ES features are enabled const expected = mapValues( uiCapabilities.value!.catalogue, (enabled, catalogueId) => catalogueId !== 'ml' && + catalogueId !== 'monitoring' && catalogueId !== 'ml_file_data_visualizer' && - catalogueId !== 'monitoring' + !esFeatureExceptions.includes(catalogueId) ); expect(uiCapabilities.value!.catalogue).to.eql(expected); break; @@ -58,6 +61,7 @@ export default function catalogueTests({ getService }: FtrProviderContext) { 'enterpriseSearch', 'appSearch', 'workplaceSearch', + ...esFeatureExceptions, ]; const expected = mapValues( uiCapabilities.value!.catalogue, diff --git a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts index d7a0dfa1cf80a..091bbccd6f87a 100644 --- a/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts +++ b/x-pack/test/ui_capabilities/security_only/tests/nav_links.ts @@ -49,7 +49,13 @@ export default function navLinksTests({ getService }: FtrProviderContext) { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('navLinks'); expect(uiCapabilities.value!.navLinks).to.eql( - navLinksBuilder.except('ml', 'monitoring', 'appSearch', 'workplaceSearch') + navLinksBuilder.except( + 'ml', + 'monitoring', + 'enterpriseSearch', + 'appSearch', + 'workplaceSearch' + ) ); break; case 'foo_all': diff --git a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts index 2ef5108403427..baae3286ddb5d 100644 --- a/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts +++ b/x-pack/test/ui_capabilities/spaces_only/tests/catalogue.ts @@ -13,6 +13,8 @@ import { SpaceScenarios } from '../scenarios'; export default function catalogueTests({ getService }: FtrProviderContext) { const uiCapabilitiesService: UICapabilitiesService = getService('uiCapabilities'); + const esFeatureExceptions = ['security', 'rollup_jobs', 'reporting', 'transform', 'watcher']; + describe('catalogue', () => { SpaceScenarios.forEach((scenario) => { it(`${scenario.name}`, async () => { @@ -29,8 +31,12 @@ export default function catalogueTests({ getService }: FtrProviderContext) { case 'nothing_space': { expect(uiCapabilities.success).to.be(true); expect(uiCapabilities.value).to.have.property('catalogue'); - // everything is disabled - const expected = mapValues(uiCapabilities.value!.catalogue, () => false); + // everything is disabled except for ES features and spaces management + const expected = mapValues( + uiCapabilities.value!.catalogue, + (enabled, catalogueId) => + esFeatureExceptions.includes(catalogueId) || catalogueId === 'spaces' + ); expect(uiCapabilities.value!.catalogue).to.eql(expected); break; } diff --git a/yarn.lock b/yarn.lock index 95066c9fa8cda..ddecaf17f7bcd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1144,10 +1144,10 @@ dependencies: "@elastic/apm-rum-core" "^5.6.0" -"@elastic/charts@21.0.1": - version "21.0.1" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-21.0.1.tgz#1e8be5303d0c7d53d7ccfcfa121ef68164ae200f" - integrity sha512-KMJE2JNwoy021Rvhgu1wiga0FVCa0u4NlFVUSR+h+G010KLarc+c9yUKBTG8v/nZ6ijBtuOLCjjU9OCWXYfxvA== +"@elastic/charts@21.1.2": + version "21.1.2" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-21.1.2.tgz#da7e9c1025bf730a738b6ac6d7024d97dd2b5aa2" + integrity sha512-Uri+Xolgii7/mRSarfXTfA6X2JC76ILIxTPO8RlYdI44gzprJfUO7Aw5s8vVQke3x6Cu39a+9B0s6TY4GAaApQ== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -1168,9 +1168,6 @@ ts-debounce "^1.0.0" utility-types "^3.10.0" uuid "^3.3.2" - optionalDependencies: - redux-immutable-state-invariant "^2.1.0" - redux-logger "^3.0.6" "@elastic/elasticsearch@7.9.0-rc.1": version "7.9.0-rc.1" @@ -4126,11 +4123,6 @@ "@types/node" "*" "@types/webpack" "*" -"@types/lodash@^3.10.1": - version "3.10.3" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-3.10.3.tgz#aaddec6a3c93bf03b402db3acf5d4c77bce8bdff" - integrity sha512-b9zScBKmB/RJqETbxu3YRya61vJOik89/lR+NdxjZAFMDcMSjwX6IhQoP4terJkhsa9TE1C+l6XwxCkhhsaZXg== - "@types/lodash@^4.14.116", "@types/lodash@^4.14.159": version "4.14.159" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.159.tgz#61089719dc6fdd9c5cb46efc827f2571d1517065" @@ -5709,11 +5701,6 @@ ansi-escapes@^1.0.0, ansi-escapes@^1.1.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" integrity sha1-06ioOzGapneTZisT52HHkRQiMG4= -ansi-escapes@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-2.0.0.tgz#5bae52be424878dd9783e8910e3fc2922e83c81b" - integrity sha1-W65SvkJIeN2Xg+iRDj/Cki6DyBs= - ansi-escapes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" @@ -8044,15 +8031,6 @@ camelcase-keys@^2.0.0: camelcase "^2.0.0" map-obj "^1.0.0" -camelcase-keys@^4.0.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77" - integrity sha1-oqpfsa9oh1glnDLBQUJteJI7m3c= - dependencies: - camelcase "^4.1.0" - map-obj "^2.0.0" - quick-lru "^1.0.0" - camelcase-keys@^6.2.2: version "6.2.2" resolved "https://registry.yarnpkg.com/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" @@ -8077,7 +8055,7 @@ camelcase@^3.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= -camelcase@^4.0.0, camelcase@^4.1.0: +camelcase@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= @@ -8742,15 +8720,6 @@ cliui@^3.0.3, cliui@^3.2.0: strip-ansi "^3.0.1" wrap-ansi "^2.0.0" -cliui@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" - integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== - dependencies: - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi "^2.0.0" - cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -10350,7 +10319,7 @@ debuglog@^1.0.1: resolved "https://registry.yarnpkg.com/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= -decamelize-keys@^1.0.0, decamelize-keys@^1.1.0: +decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= @@ -10394,11 +10363,6 @@ dedent@^0.7.0: resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" integrity sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw= -deep-diff@^0.3.5: - version "0.3.8" - resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84" - integrity sha1-wB3mPvsO7JeYgB1Ax+Da4ltYLIQ= - deep-eql@^0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/deep-eql/-/deep-eql-0.1.3.tgz#ef558acab8de25206cd713906d74e56930eb69f2" @@ -10556,13 +10520,13 @@ defined@^1.0.0, defined@~1.0.0: resolved "https://registry.yarnpkg.com/defined/-/defined-1.0.0.tgz#c98d9bcef75674188e110969151199e39b1fa693" integrity sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM= -del-cli@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/del-cli/-/del-cli-3.0.0.tgz#327a15d4c18d6b7e5c849a53ef0d17901bc28197" - integrity sha512-J4HDC2mpcN5aopya4VdkyiFXZaqAoo7ua9VpKbciX3DDUSbtJbPMc3ivggJsAAgS6EqonmbenIiMhBGtJPW9FA== +del-cli@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/del-cli/-/del-cli-3.0.1.tgz#2d27ff260204b5104cadeda86f78f180a4ebe89a" + integrity sha512-BLHItGr82rUbHhjMu41d+vw9Md49i81jmZSV00HdTq4t+RTHywmEht/23mNFpUl2YeLYJZJyGz4rdlMAyOxNeg== dependencies: del "^5.1.0" - meow "^5.0.0" + meow "^6.1.1" del@^2.0.2: version "2.2.2" @@ -11868,17 +11832,6 @@ eslint-config-prettier@^6.11.0: dependencies: get-stdin "^6.0.0" -eslint-formatter-pretty@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/eslint-formatter-pretty/-/eslint-formatter-pretty-1.3.0.tgz#985d9e41c1f8475f4a090c5dbd2dfcf2821d607e" - integrity sha512-5DY64Y1rYCm7cfFDHEGUn54bvCnK+wSUVF07N8oXeqUJFSd+gnYOTXbzelQ1HurESluY6gnEQPmXOIkB4Wa+gA== - dependencies: - ansi-escapes "^2.0.0" - chalk "^2.1.0" - log-symbols "^2.0.0" - plur "^2.1.2" - string-width "^2.0.0" - eslint-formatter-pretty@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/eslint-formatter-pretty/-/eslint-formatter-pretty-4.0.0.tgz#dc15f3bf4fb51b7ba5fbedb77f57ba8841140ce2" @@ -12295,15 +12248,10 @@ eventemitter2@~0.4.13: resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-0.4.14.tgz#8f61b75cde012b2e9eb284d4545583b5643b61ab" integrity sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas= -eventemitter3@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-3.1.0.tgz#090b4d6cdbd645ed10bf750d4b5407942d7ba163" - integrity sha512-ivIvhpq/Y0uSjcHDcOIccjmYjGLcP09MFGE7ysAwkAvkXfpZlC985pH2/ui64DKazbTW/4kN3yqozUxlXzI6cA== - eventemitter3@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" - integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== + version "4.0.7" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== events@^1.0.2: version "1.1.1" @@ -12344,19 +12292,6 @@ execa@^0.1.1: object-assign "^4.0.1" strip-eof "^1.0.0" -execa@^0.10.0: - version "0.10.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.10.0.tgz#ff456a8f53f90f8eccc71a96d11bdfc7f082cb50" - integrity sha512-7XOMnz8Ynx1gGo/3hyV9loYNPWM94jG3+3T3Y8tsfSstFmETmENCMU/A/zj8Lyaj1lkgEepKepvd6240tBRvlw== - dependencies: - cross-spawn "^6.0.0" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.4.0.tgz#4eb6467a36a095fabb2970ff9d5e3fb7bce6ebc3" @@ -14203,7 +14138,7 @@ globby@^6.1.0: pify "^2.0.0" pinkie-promise "^2.0.0" -globby@^9.1.0, globby@^9.2.0: +globby@^9.2.0: version "9.2.0" resolved "https://registry.yarnpkg.com/globby/-/globby-9.2.0.tgz#fd029a706c703d29bdd170f4b6db3a3f7a7cb63d" integrity sha512-ollPHROa5mcxDEkwg6bPt3QbEf4pDQSNtd6JPL1YvOvAo/7/0VAm9TccUeoTmarjPw4pfUthSCqcyfNB1I3ZSg== @@ -15524,11 +15459,11 @@ http-proxy-middleware@0.19.1: micromatch "^3.1.10" http-proxy@^1.17.0: - version "1.17.0" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.17.0.tgz#7ad38494658f84605e2f6db4436df410f4e5be9a" - integrity sha512-Taqn+3nNvYRfJ3bGvKfBSRwy1v6eePlm3oc/aWVxZp57DQr5Eq3xhKJi7Z4hZpS8PC3H4qI+Yly5EmFacGuA/g== + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== dependencies: - eventemitter3 "^3.0.0" + eventemitter3 "^4.0.0" follow-redirects "^1.0.0" requires-port "^1.0.0" @@ -16215,11 +16150,6 @@ iron@5.x.x: cryptiles "4.x.x" hoek "5.x.x" -irregular-plurals@^1.0.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-1.4.0.tgz#2ca9b033651111855412f16be5d77c62a458a766" - integrity sha1-LKmwM2UREYVUEvFr5dd8YqRYp2Y= - irregular-plurals@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/irregular-plurals/-/irregular-plurals-3.2.0.tgz#b19c490a0723798db51b235d7e39add44dab0822" @@ -18207,10 +18137,10 @@ kdbush@^3.0.0: resolved "https://registry.yarnpkg.com/kdbush/-/kdbush-3.0.0.tgz#f8484794d47004cc2d85ed3a79353dbe0abc2bf0" integrity sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew== -kea@2.2.0-rc.4: - version "2.2.0-rc.4" - resolved "https://registry.yarnpkg.com/kea/-/kea-2.2.0-rc.4.tgz#cc0376950530a6751f73387c4b25a39efa1faa77" - integrity sha512-pYuwaCiJkBvHZShi8kqhk8dC4DjeELdK51Lw7Pn0tNdJgZJDF6COhsUiF/yrh9d7woNYDxKfuxH+QWZFfo8PkA== +kea@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/kea/-/kea-2.2.0.tgz#1ba4a174a53880cca8002a67cf62b19b30d09702" + integrity sha512-IzgTC6SC89wTLfiBMPlduG4r4YanxONYK4werz8RMZxPvcYw4XEEK8xQJguwVYtLCEGm4x5YiLCubGqGfRcbEw== kew@~0.1.7: version "0.1.7" @@ -18766,7 +18696,7 @@ lodash._reinterpolate@^3.0.0: resolved "https://registry.yarnpkg.com/lodash._reinterpolate/-/lodash._reinterpolate-3.0.0.tgz#0ccf2d89166af03b3663c796538b75ac6e114d9d" integrity sha1-DM8tiRZq8Ds2Y8eWU4t1rG4RTZ0= -lodash.assign@^4.0.3, lodash.assign@^4.0.6, lodash.assign@^4.2.0: +lodash.assign@^4.2.0: version "4.2.0" resolved "https://registry.yarnpkg.com/lodash.assign/-/lodash.assign-4.2.0.tgz#0d99f3ccd7a6d261d19bdaeb9245005d285808e7" integrity sha1-DZnzzNem0mHRm9rrkkUAXShYCOc= @@ -19036,11 +18966,6 @@ lodash@^3.10.1: resolved "https://registry.yarnpkg.com/lodash/-/lodash-3.10.1.tgz#5bf45e8e49ba4189e17d482789dfd15bd140b7b6" integrity sha1-W/Rejkm6QYnhfUgnid/RW9FAt7Y= -"lodash@npm:@elastic/lodash@3.10.1-kibana4": - version "3.10.1-kibana4" - resolved "https://registry.yarnpkg.com/@elastic/lodash/-/lodash-3.10.1-kibana4.tgz#d491228fd659b4a1b0dfa08ba9c67a4979b9746d" - integrity sha512-geQqXd9ZedRCL+kq5cpeahYWYaYRV0BMXhCwzq4DpnGCVs430FTMS3Wcot3XChZZhCvkwHm15bpNjB312vPxaA== - log-ok@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/log-ok/-/log-ok-0.1.1.tgz#bea3dd36acd0b8a7240d78736b5b97c65444a334" @@ -19049,7 +18974,7 @@ log-ok@^0.1.1: ansi-green "^0.1.1" success-symbol "^0.1.0" -log-symbols@2.2.0, log-symbols@^2.0.0, log-symbols@^2.1.0, log-symbols@^2.2.0: +log-symbols@2.2.0, log-symbols@^2.1.0, log-symbols@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" integrity sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg== @@ -19351,11 +19276,6 @@ map-obj@^1.0.0, map-obj@^1.0.1: resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= -map-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" - integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk= - map-obj@^4.0.0: version "4.1.0" resolved "https://registry.yarnpkg.com/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5" @@ -19659,20 +19579,22 @@ meow@^3.0.0, meow@^3.3.0, meow@^3.7.0: redent "^1.0.0" trim-newlines "^1.0.0" -meow@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4" - integrity sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig== +meow@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467" + integrity sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg== dependencies: - camelcase-keys "^4.0.0" - decamelize-keys "^1.0.0" - loud-rejection "^1.0.0" - minimist-options "^3.0.1" - normalize-package-data "^2.3.4" - read-pkg-up "^3.0.0" - redent "^2.0.0" - trim-newlines "^2.0.0" - yargs-parser "^10.0.0" + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "^4.0.2" + normalize-package-data "^2.5.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.13.1" + yargs-parser "^18.1.3" meow@^7.0.1: version "7.0.1" @@ -19920,14 +19842,6 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: dependencies: brace-expansion "^1.1.7" -minimist-options@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" - integrity sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ== - dependencies: - arrify "^1.0.1" - is-plain-obj "^1.1.0" - minimist-options@^4.0.2: version "4.1.0" resolved "https://registry.yarnpkg.com/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" @@ -20122,15 +20036,15 @@ mocha@^7.1.1: yargs-parser "13.1.2" yargs-unparser "1.6.0" -mochawesome-merge@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mochawesome-merge/-/mochawesome-merge-2.0.1.tgz#c690433acc78fd769effe4db1a107508351e2dc5" - integrity sha512-QRYok/9y9MJ4zlWGajC/OV6BxjUGyv1AYX3DBOPSbpzk09p2dFBWV1QYSN/dHu7bo/q44ZGmOBHO8ZnAyI+Yug== +mochawesome-merge@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/mochawesome-merge/-/mochawesome-merge-4.1.0.tgz#25a514460c6e106e2c8399daaec2d085b6e89b56" + integrity sha512-cDMzSmYu1dRKcr+ZrjjUEuXSiirU8LTG6R8hrAPlZ7zy1EeL7LLpi+a156obxzqh8quTWmYxKtUbTF2PQt0l7A== dependencies: fs-extra "^7.0.1" - minimatch "^3.0.4" + glob "^7.1.6" uuid "^3.3.2" - yargs "^12.0.5" + yargs "^15.3.1" mochawesome-report-generator@^4.0.0: version "4.0.1" @@ -21569,15 +21483,6 @@ os-locale@^1.4.0: dependencies: lcid "^1.0.0" -os-locale@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.0.1.tgz#3b014fbf01d87f60a1e5348d80fe870dc82c4620" - integrity sha512-7g5e7dmXPtzcP4bgsZ8ixDVqA7oWYuEz4lOSujeWyliPai4gfVDiFIcwBg3aGCPnmSGfzOKTK3ccPn0CKv3DBw== - dependencies: - execa "^0.10.0" - lcid "^2.0.0" - mem "^4.0.0" - os-locale@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" @@ -22417,13 +22322,6 @@ plugin-error@^1.0.1: arr-union "^3.1.0" extend-shallow "^3.0.2" -plur@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/plur/-/plur-2.1.2.tgz#7482452c1a0f508e3e344eaec312c91c29dc655a" - integrity sha1-dIJFLBoPUI4+NE6uwxLJHCncZVo= - dependencies: - irregular-plurals "^1.0.0" - plur@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/plur/-/plur-4.0.0.tgz#729aedb08f452645fe8c58ef115bf16b0a73ef84" @@ -23136,11 +23034,6 @@ queue@6.0.1: dependencies: inherits "~2.0.3" -quick-lru@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" - integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= - quick-lru@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" @@ -24162,14 +24055,6 @@ read-pkg-up@^2.0.0: find-up "^2.0.0" read-pkg "^2.0.0" -read-pkg-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" - integrity sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc= - dependencies: - find-up "^2.0.0" - read-pkg "^3.0.0" - read-pkg-up@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-4.0.0.tgz#1b221c6088ba7799601c808f91161c66e58f8978" @@ -24399,14 +24284,6 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" -redent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" - integrity sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo= - dependencies: - indent-string "^3.0.0" - strip-indent "^2.0.0" - redent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -24455,21 +24332,6 @@ redux-devtools-extension@^2.13.8: resolved "https://registry.yarnpkg.com/redux-devtools-extension/-/redux-devtools-extension-2.13.8.tgz#37b982688626e5e4993ff87220c9bbb7cd2d96e1" integrity sha512-8qlpooP2QqPtZHQZRhx3x3OP5skEV1py/zUdMY28WNAocbafxdG2tRD1MWE7sp8obGMNYuLWanhhQ7EQvT1FBg== -redux-immutable-state-invariant@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/redux-immutable-state-invariant/-/redux-immutable-state-invariant-2.1.0.tgz#308fd3cc7415a0e7f11f51ec997b6379c7055ce1" - integrity sha512-3czbDKs35FwiBRsx/3KabUk5zSOoTXC+cgVofGkpBNv3jQcqIe5JrHcF5AmVt7B/4hyJ8MijBIpCJ8cife6yJg== - dependencies: - invariant "^2.1.0" - json-stringify-safe "^5.0.1" - -redux-logger@^3.0.6: - version "3.0.6" - resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf" - integrity sha1-91VZZvMJjzyIYExEnPC69XeCdL8= - dependencies: - deep-diff "^0.3.5" - redux-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/redux-observable/-/redux-observable-1.2.0.tgz#ff51b6c6be2598e9b5e89fc36639186bb0e669c7" @@ -27051,11 +26913,6 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" -strip-indent@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" - integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= - strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -28150,11 +28007,6 @@ trim-newlines@^1.0.0: resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= -trim-newlines@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" - integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= - trim-newlines@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" @@ -28261,19 +28113,6 @@ tsd@^0.13.1: read-pkg-up "^7.0.0" update-notifier "^4.1.0" -tsd@^0.7.4: - version "0.7.4" - resolved "https://registry.yarnpkg.com/tsd/-/tsd-0.7.4.tgz#d9aba567f1394641821a6800dcee60746c87bd03" - integrity sha512-cqr1s2GHtVkU3L/4BXDaeJOjFEuZ7iOVC+hwmyx4G7Eo26mSXCFNnwFm4EasK/MW2HdY3AQWux+AjYzDYLzZow== - dependencies: - eslint-formatter-pretty "^1.3.0" - globby "^9.1.0" - meow "^5.0.0" - path-exists "^3.0.0" - read-pkg-up "^4.0.0" - typescript "^3.0.1" - update-notifier "^2.5.0" - tslib@^1, tslib@^1.0.0, tslib@^1.10.0, tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.13.0" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" @@ -28440,7 +28279,7 @@ typescript-tuple@^2.2.1: dependencies: typescript-compare "^0.0.2" -typescript@4.0.2, typescript@^3.0.1, typescript@^3.0.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.7.2: +typescript@4.0.2, typescript@^3.0.3, typescript@^3.2.2, typescript@^3.3.3333, typescript@^3.4.5, typescript@~3.7.2: version "4.0.2" resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.0.2.tgz#7ea7c88777c723c681e33bf7988be5d008d05ac2" integrity sha512-e4ERvRV2wb+rRZ/IQeb3jm2VxBsirQLpQhdxplZ2MEzGvDkkMmPglecnNDfSUBivMjP93vRbngYYDQqQ/78bcQ== @@ -30268,11 +30107,6 @@ window-size@^0.1.4: resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.4.tgz#f8e1aa1ee5a53ec5bf151ffa09742a6ad7697876" integrity sha1-+OGqHuWlPsW/FR/6CXQqatdpeHY= -window-size@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.2.0.tgz#b4315bb4214a3d7058ebeee892e13fa24d98b075" - integrity sha1-tDFbtCFKPXBY6+7okuE/ok2YsHU= - windows-release@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/windows-release/-/windows-release-3.2.0.tgz#8122dad5afc303d833422380680a79cdfa91785f" @@ -30642,7 +30476,7 @@ y18n@^3.2.0, y18n@^3.2.1: resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= -"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: +y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== @@ -30693,21 +30527,6 @@ yargs-parser@5.0.0-security.0: camelcase "^3.0.0" object.assign "^4.1.0" -yargs-parser@^10.0.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== - dependencies: - camelcase "^4.1.0" - -yargs-parser@^11.1.1: - version "11.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" - integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs-parser@^18.1.1, yargs-parser@^18.1.2, yargs-parser@^18.1.3: version "18.1.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" @@ -30716,14 +30535,6 @@ yargs-parser@^18.1.1, yargs-parser@^18.1.2, yargs-parser@^18.1.3: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^2.4.1: - version "2.4.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-2.4.1.tgz#85568de3cf150ff49fa51825f03a8c880ddcc5c4" - integrity sha1-hVaN488VD/SfpRgl8DqMiA3cxcQ= - dependencies: - camelcase "^3.0.0" - lodash.assign "^4.0.6" - yargs-unparser@1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/yargs-unparser/-/yargs-unparser-1.6.0.tgz#ef25c2c769ff6bd09e4b0f9d7c605fb27846ea9f" @@ -30766,45 +30577,7 @@ yargs@13.3.2, yargs@^13.2.2, yargs@^13.3.0, yargs@^13.3.2: y18n "^4.0.0" yargs-parser "^13.1.2" -yargs@4.8.1: - version "4.8.1" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-4.8.1.tgz#c0c42924ca4aaa6b0e6da1739dfb216439f9ddc0" - integrity sha1-wMQpJMpKqmsObaFznfshZDn53cA= - dependencies: - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - lodash.assign "^4.0.3" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.1" - which-module "^1.0.0" - window-size "^0.2.0" - y18n "^3.2.1" - yargs-parser "^2.4.1" - -yargs@^12.0.5: - version "12.0.5" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" - integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== - dependencies: - cliui "^4.0.0" - decamelize "^1.2.0" - find-up "^3.0.0" - get-caller-file "^1.0.1" - os-locale "^3.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1 || ^4.0.0" - yargs-parser "^11.1.1" - -yargs@^15.0.2, yargs@^15.1.0, yargs@^15.3.1, yargs@^15.4.0: +yargs@^15.0.2, yargs@^15.1.0, yargs@^15.3.1, yargs@^15.4.0, yargs@^15.4.1: version "15.4.1" resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8" integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==