diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a64ab63494b35..61369a37ec3c2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -177,6 +177,8 @@ /x-pack/test/functional/services/ml/ @elastic/ml-ui /x-pack/test/functional_basic/apps/ml/ @elastic/ml-ui /x-pack/test/functional_with_es_ssl/apps/ml/ @elastic/ml-ui +/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/ @elastic/ml-ui +/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/transform_rule_types/ @elastic/ml-ui # ML team owns and maintains the transform plugin despite it living in the Data management section. /x-pack/plugins/transform/ @elastic/ml-ui diff --git a/docs/developer/plugin-list.asciidoc b/docs/developer/plugin-list.asciidoc index e997c0bc68cde..3d9de2d35b500 100644 --- a/docs/developer/plugin-list.asciidoc +++ b/docs/developer/plugin-list.asciidoc @@ -540,6 +540,11 @@ Elastic. |Add tagging capability to saved objects +|{kib-repo}blob/{branch}/x-pack/plugins/screenshotting/README.md[screenshotting] +|This plugin provides functionality to take screenshots of the Kibana pages. +It uses Chromium and Puppeteer underneath to run the browser in headless mode. + + |{kib-repo}blob/{branch}/x-pack/plugins/searchprofiler/README.md[searchprofiler] |The search profiler consumes the Profile API by sending a search API with profile: true enabled in the request body. The response contains 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 403d8594999a7..8f6f1f6c98ab2 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 @@ -88,6 +88,7 @@ readonly links: { readonly usersAccess: string; }; readonly workplaceSearch: { + readonly apiKeys: string; readonly box: string; readonly confluenceCloud: string; readonly confluenceServer: 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 131d4452c980c..a9828f04672e9 100644 --- a/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md +++ b/docs/development/core/public/kibana-plugin-core-public.doclinksstart.md @@ -17,5 +17,5 @@ export interface DocLinksStart | --- | --- | --- | | [DOC\_LINK\_VERSION](./kibana-plugin-core-public.doclinksstart.doc_link_version.md) | string | | | [ELASTIC\_WEBSITE\_URL](./kibana-plugin-core-public.doclinksstart.elastic_website_url.md) | string | | -| [links](./kibana-plugin-core-public.doclinksstart.links.md) | { readonly settings: string; readonly elasticStackGetStarted: string; readonly upgrade: { readonly upgradingElasticStack: string; }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; readonly customLinks: string; readonly droppedTransactionSpans: string; readonly upgrading: string; readonly metaData: string; }; readonly canvas: { readonly guide: string; }; readonly cloud: { readonly indexManagement: string; }; readonly dashboard: { readonly guide: string; readonly drilldowns: string; readonly drilldownsTriggerPicker: string; readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; readonly discover: Record<string, string>; readonly filebeat: { readonly base: string; readonly installation: string; readonly configuration: string; readonly elasticsearchOutput: string; readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; readonly suricataModule: string; readonly zeekModule: string; }; readonly auditbeat: { readonly base: string; readonly auditdModule: string; readonly systemModule: string; }; readonly metricbeat: { readonly base: string; readonly configure: string; readonly httpEndpoint: string; readonly install: string; readonly start: string; }; readonly appSearch: { readonly apiRef: string; readonly apiClients: string; readonly apiKeys: string; readonly authentication: string; readonly crawlRules: string; readonly curations: string; readonly duplicateDocuments: string; readonly entryPoints: string; readonly guide: string; readonly indexingDocuments: string; readonly indexingDocumentsSchema: string; readonly logSettings: string; readonly metaEngines: string; readonly precisionTuning: string; readonly relevanceTuning: string; readonly resultSettings: string; readonly searchUI: string; readonly security: string; readonly synonyms: string; readonly webCrawler: string; readonly webCrawlerEventLogs: string; }; readonly enterpriseSearch: { readonly configuration: string; readonly licenseManagement: string; readonly mailService: string; readonly usersAccess: string; }; readonly workplaceSearch: { readonly box: string; readonly confluenceCloud: string; readonly confluenceServer: string; readonly customSources: string; readonly customSourcePermissions: string; readonly documentPermissions: string; readonly dropbox: string; readonly externalIdentities: string; readonly gitHub: string; readonly gettingStarted: string; readonly gmail: string; readonly googleDrive: string; readonly indexingSchedule: string; readonly jiraCloud: string; readonly jiraServer: string; readonly oneDrive: string; readonly permissions: string; readonly salesforce: string; readonly security: string; readonly serviceNow: string; readonly sharePoint: string; readonly slack: string; readonly synch: string; readonly zendesk: string; }; readonly heartbeat: { readonly base: string; }; readonly libbeat: { readonly getStarted: string; }; readonly logstash: { readonly base: string; }; readonly functionbeat: { readonly base: string; }; readonly winlogbeat: { readonly base: string; }; readonly aggs: { readonly composite: string; readonly composite\_missing\_bucket: string; readonly date\_histogram: string; readonly date\_range: string; readonly date\_format\_pattern: string; readonly filter: string; readonly filters: string; readonly geohash\_grid: string; readonly histogram: string; readonly ip\_range: string; readonly range: string; readonly significant\_terms: string; readonly terms: string; readonly terms\_doc\_count\_error: string; readonly avg: string; readonly avg\_bucket: string; readonly max\_bucket: string; readonly min\_bucket: string; readonly sum\_bucket: string; readonly cardinality: string; readonly count: string; readonly cumulative\_sum: string; readonly derivative: string; readonly geo\_bounds: string; readonly geo\_centroid: string; readonly max: string; readonly median: string; readonly min: string; readonly moving\_avg: string; readonly percentile\_ranks: string; readonly serial\_diff: string; readonly std\_dev: string; readonly sum: string; readonly top\_hits: string; }; readonly runtimeFields: { readonly overview: string; readonly mapping: string; }; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; readonly painless: string; readonly painlessApi: string; readonly painlessLangSpec: string; readonly painlessSyntax: string; readonly painlessWalkthrough: string; readonly luceneExpressions: string; }; readonly search: { readonly sessions: string; readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; readonly fieldFormattersNumber: string; readonly fieldFormattersString: string; readonly runtimeFields: string; }; readonly addData: string; readonly kibana: string; readonly upgradeAssistant: { readonly overview: string; readonly batchReindex: string; readonly remoteReindex: string; }; readonly rollupJobs: string; readonly elasticsearch: Record<string, string>; readonly siem: { readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; readonly ruleChangeLog: string; readonly detectionsReq: string; readonly networkMap: string; readonly troubleshootGaps: string; }; readonly securitySolution: { readonly trustedApps: string; }; readonly query: { readonly eql: string; readonly kueryQuerySyntax: string; readonly luceneQuerySyntax: string; readonly percolate: string; readonly queryDsl: string; }; readonly date: { readonly dateMath: string; readonly dateMathIndexNames: string; }; readonly management: Record<string, string>; readonly ml: Record<string, string>; readonly transforms: Record<string, string>; readonly visualize: Record<string, string>; readonly apis: Readonly<{ bulkIndexAlias: string; byteSizeUnits: string; createAutoFollowPattern: string; createFollower: string; createIndex: string; createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; putComponentTemplate: string; painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putIndexTemplateV1: string; putWatch: string; simulatePipeline: string; timeUnits: string; updateTransform: string; }>; readonly observability: Readonly<{ guide: string; infrastructureThreshold: string; logsThreshold: string; metricsThreshold: string; monitorStatus: string; monitorUptime: string; tlsCertificate: string; uptimeDurationAnomaly: string; }>; readonly alerting: Record<string, string>; readonly maps: Readonly<{ guide: string; importGeospatialPrivileges: string; gdalTutorial: string; }>; readonly monitoring: Record<string, string>; readonly security: Readonly<{ apiKeyServiceSettings: string; clusterPrivileges: string; elasticsearchSettings: string; elasticsearchEnableSecurity: string; elasticsearchEnableApiKeys: string; indicesPrivileges: string; kibanaTLS: string; kibanaPrivileges: string; mappingRoles: string; mappingRolesFieldRules: string; runAsPrivilege: string; }>; readonly spaces: Readonly<{ kibanaLegacyUrlAliases: string; kibanaDisableLegacyUrlAliasesApi: string; }>; readonly watcher: Record<string, string>; readonly ccs: Record<string, string>; readonly plugins: Record<string, string>; readonly snapshotRestore: Record<string, string>; readonly ingest: Record<string, string>; readonly fleet: Readonly<{ beatsAgentComparison: string; guide: string; fleetServer: string; fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; datastreamsNamingScheme: string; installElasticAgent: string; installElasticAgentStandalone: string; upgradeElasticAgent: string; upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; onPremRegistry: string; }>; readonly ecs: { readonly guide: string; }; readonly clients: { readonly guide: string; readonly goOverview: string; readonly javaIndex: string; readonly jsIntro: string; readonly netGuide: string; readonly perlGuide: string; readonly phpGuide: string; readonly pythonGuide: string; readonly rubyOverview: string; readonly rustGuide: string; }; readonly endpoints: { readonly troubleshooting: string; }; } | | +| [links](./kibana-plugin-core-public.doclinksstart.links.md) | { readonly settings: string; readonly elasticStackGetStarted: string; readonly upgrade: { readonly upgradingElasticStack: string; }; readonly apm: { readonly kibanaSettings: string; readonly supportedServiceMaps: string; readonly customLinks: string; readonly droppedTransactionSpans: string; readonly upgrading: string; readonly metaData: string; }; readonly canvas: { readonly guide: string; }; readonly cloud: { readonly indexManagement: string; }; readonly dashboard: { readonly guide: string; readonly drilldowns: string; readonly drilldownsTriggerPicker: string; readonly urlDrilldownTemplateSyntax: string; readonly urlDrilldownVariables: string; }; readonly discover: Record<string, string>; readonly filebeat: { readonly base: string; readonly installation: string; readonly configuration: string; readonly elasticsearchOutput: string; readonly elasticsearchModule: string; readonly startup: string; readonly exportedFields: string; readonly suricataModule: string; readonly zeekModule: string; }; readonly auditbeat: { readonly base: string; readonly auditdModule: string; readonly systemModule: string; }; readonly metricbeat: { readonly base: string; readonly configure: string; readonly httpEndpoint: string; readonly install: string; readonly start: string; }; readonly appSearch: { readonly apiRef: string; readonly apiClients: string; readonly apiKeys: string; readonly authentication: string; readonly crawlRules: string; readonly curations: string; readonly duplicateDocuments: string; readonly entryPoints: string; readonly guide: string; readonly indexingDocuments: string; readonly indexingDocumentsSchema: string; readonly logSettings: string; readonly metaEngines: string; readonly recisionTuning: string; readonly relevanceTuning: string; readonly resultSettings: string; readonly searchUI: string; readonly security: string; readonly synonyms: string; readonly webCrawler: string; readonly webCrawlerEventLogs: string; }; readonly enterpriseSearch: { readonly configuration: string; readonly licenseManagement: string; readonly mailService: string; readonly usersAccess: string; }; readonly workplaceSearch: { readonly apiKeys: string; readonly box: string; readonly confluenceCloud: string; readonly confluenceServer: string; readonly customSources: string; readonly customSourcePermissions: string; readonly documentPermissions: string; readonly dropbox: string; readonly externalIdentities: string; readonly gitHub: string; readonly gettingStarted: string; readonly gmail: string; readonly googleDrive: string; readonly indexingSchedule: string; readonly jiraCloud: string; readonly jiraServer: string; readonly oneDrive: string; readonly permissions: string; readonly salesforce: string; readonly security: string; readonly serviceNow: string; readonly sharePoint: string; readonly slack: string; readonly synch: string; readonly zendesk: string; }; readonly heartbeat: { readonly base: string; }; readonly libbeat: { readonly getStarted: string; }; readonly logstash: { readonly base: string; }; readonly functionbeat: { readonly base: string; }; readonly winlogbeat: { readonly base: string; }; readonly aggs: { readonly composite: string; readonly composite\_missing\_bucket: string; readonly date\_histogram: string; readonly date\_range: string; readonly date\_format\_pattern: string; readonly filter: string; readonly filters: string; readonly geohash\_grid: string; readonly histogram: string; readonly ip\_range: string; readonly range: string; readonly significant\_terms: string; readonly terms: string; readonly terms\_doc\_count\_error: string; readonly avg: string; readonly avg\_bucket: string; readonly max\_bucket: string; readonly min\_bucket: string; readonly sum\_bucket: string; readonly cardinality: string; readonly count: string; readonly cumulative\_sum: string; readonly derivative: string; readonly geo\_bounds: string; readonly geo\_centroid: string; readonly max: string; readonly median: string; readonly min: string; readonly moving\_avg: string; readonly percentile\_ranks: string; readonly serial\_diff: string; readonly std\_dev: string; readonly sum: string; readonly top\_hits: string; }; readonly runtimeFields: { readonly overview: string; readonly mapping: string; }; readonly scriptedFields: { readonly scriptFields: string; readonly scriptAggs: string; readonly painless: string; readonly painlessApi: string; readonly painlessLangSpec: string; readonly painlessSyntax: string; readonly painlessWalkthrough: string; readonly luceneExpressions: string; }; readonly search: { readonly sessions: string; readonly sessionLimits: string; }; readonly indexPatterns: { readonly introduction: string; readonly fieldFormattersNumber: string; readonly fieldFormattersString: string; readonly runtimeFields: string; }; readonly addData: string; readonly kibana: string; readonly upgradeAssistant: { readonly overview: string; readonly batchReindex: string; readonly remoteReindex: string; }; readonly rollupJobs: string; readonly elasticsearch: Record<string, string>; readonly siem: { readonly privileges: string; readonly guide: string; readonly gettingStarted: string; readonly ml: string; readonly ruleChangeLog: string; readonly detectionsReq: string; readonly networkMap: string; readonly troubleshootGaps: string; }; readonly securitySolution: { readonly trustedApps: string; }; readonly query: { readonly eql: string; readonly kueryQuerySyntax: string; readonly luceneQuerySyntax: string; readonly percolate: string; readonly queryDsl: string; }; readonly date: { readonly dateMath: string; readonly dateMathIndexNames: string; }; readonly management: Record<string, string>; readonly ml: Record<string, string>; readonly transforms: Record<string, string>; readonly visualize: Record<string, string>; readonly apis: Readonly<{ bulkIndexAlias: string; byteSizeUnits: string; createAutoFollowPattern: string; createFollower: string; createIndex: string; createSnapshotLifecyclePolicy: string; createRoleMapping: string; createRoleMappingTemplates: string; createRollupJobsRequest: string; createApiKey: string; createPipeline: string; createTransformRequest: string; cronExpressions: string; executeWatchActionModes: string; indexExists: string; openIndex: string; putComponentTemplate: string; painlessExecute: string; painlessExecuteAPIContexts: string; putComponentTemplateMetadata: string; putSnapshotLifecyclePolicy: string; putIndexTemplateV1: string; putWatch: string; simulatePipeline: string; timeUnits: string; updateTransform: string; }>; readonly observability: Readonly<{ guide: string; infrastructureThreshold: string; logsThreshold: string; metricsThreshold: string; monitorStatus: string; monitorUptime: string; tlsCertificate: string; uptimeDurationAnomaly: string; }>; readonly alerting: Record<string, string>; readonly maps: Readonly<{ guide: string; importGeospatialPrivileges: string; gdalTutorial: string; }>; readonly monitoring: Record<string, string>; readonly security: Readonly<{ apiKeyServiceSettings: string; clusterPrivileges: string; elasticsearchSettings: string; elasticsearchEnableSecurity: string; elasticsearchEnableApiKeys: string; indicesPrivileges: string; kibanaTLS: string; kibanaPrivileges: string; mappingRoles: string; mappingRolesFieldRules: string; runAsPrivilege: string; }>; readonly spaces: Readonly<{ kibanaLegacyUrlAliases: string; kibanaDisableLegacyUrlAliasesApi: string; }>; readonly watcher: Record<string, string>; readonly ccs: Record<string, string>; readonly plugins: Record<string, string>; readonly snapshotRestore: Record<string, string>; readonly ingest: Record<string, string>; readonly fleet: Readonly<{ beatsAgentComparison: string; guide: string; fleetServer: string; fleetServerAddFleetServer: string; settings: string; settingsFleetServerHostSettings: string; settingsFleetServerProxySettings: string; troubleshooting: string; elasticAgent: string; datastreams: string; datastreamsNamingScheme: string; installElasticAgent: string; installElasticAgentStandalone: string; upgradeElasticAgent: string; upgradeElasticAgent712lower: string; learnMoreBlog: string; apiKeysLearnMore: string; onPremRegistry: string; }>; readonly ecs: { readonly guide: string; }; readonly clients: { readonly guide: string; readonly goOverview: string; readonly javaIndex: string; readonly jsIntro: string; readonly netGuide: string; readonly perlGuide: string; readonly phpGuide: string; readonly pythonGuide: string; readonly rubyOverview: string; readonly rustGuide: string; }; readonly endpoints: { readonly troubleshooting: string; }; } | | diff --git a/docs/osquery/osquery.asciidoc b/docs/osquery/osquery.asciidoc index 396135d8d1751..500dc6959fc00 100644 --- a/docs/osquery/osquery.asciidoc +++ b/docs/osquery/osquery.asciidoc @@ -288,13 +288,21 @@ This is useful for teams who need in-depth and detailed control. [float] === Customize Osquery configuration -By default, all Osquery Manager integrations share the same osquery configuration. However, you can customize how Osquery is configured by editing the Osquery Manager integration for each agent policy +experimental[] By default, all Osquery Manager integrations share the same osquery configuration. However, you can customize how Osquery is configured by editing the Osquery Manager integration for each agent policy you want to adjust. The custom configuration is then applied to all agents in the policy. This powerful feature allows you to configure https://osquery.readthedocs.io/en/stable/deployment/file-integrity-monitoring[File Integrity Monitoring], https://osquery.readthedocs.io/en/stable/deployment/process-auditing[Process auditing], and https://osquery.readthedocs.io/en/stable/deployment/configuration/#configuration-specification[others]. -IMPORTANT: Take caution when editing this configuration. The changes you make are distributed to all agents in the policy. +[IMPORTANT] +========================= + +* Take caution when editing this configuration. The changes you make are distributed to all agents in the policy. + +* Take caution when editing `packs` using the Advanced *Osquery config* field. +Any changes you make to `packs` from this field are not reflected in the UI on the Osquery *Packs* page in {kib}, however, these changes are deployed to agents in the policy. +While this allows you to use advanced Osquery functionality like pack discovery queries, you do lose the ability to manage packs defined this way from the Osquery *Packs* page. +========================= . From the {kib} main menu, click *Fleet*, then the *Agent policies* tab. @@ -315,6 +323,16 @@ IMPORTANT: Take caution when editing this configuration. The changes you make ar * (Optional) To load a full configuration file, drag and drop an Osquery `.conf` file into the area at the bottom of the page. . Click *Save integration* to apply the custom configuration to all agents in the policy. ++ +As an example, the following configuration disables two tables. ++ +```ts +{ + "options":{ + "disable_tables":"curl,process_envs" + } +} +``` [float] === Upgrade Osquery versions diff --git a/docs/settings/monitoring-settings.asciidoc b/docs/settings/monitoring-settings.asciidoc index d8bc26b7b3987..8bc98a028b8f6 100644 --- a/docs/settings/monitoring-settings.asciidoc +++ b/docs/settings/monitoring-settings.asciidoc @@ -72,6 +72,9 @@ For more information, see | `monitoring.ui.elasticsearch.ssl` | Shares the same configuration as <>. These settings configure encrypted communication between {kib} and the monitoring cluster. +| `monitoring.cluster_alerts.allowedSpaces` {ess-icon} + | Specifies the spaces where cluster Stack Monitoring alerts can be created. You must specify all spaces where you want to generate alerts, including the default space. Defaults to `[ "default" ]`. + |=== [float] diff --git a/docs/settings/spaces-settings.asciidoc b/docs/settings/spaces-settings.asciidoc index dd37943101145..3eb91a0d884ef 100644 --- a/docs/settings/spaces-settings.asciidoc +++ b/docs/settings/spaces-settings.asciidoc @@ -12,11 +12,3 @@ The maximum number of spaces that you can use with the {kib} instance. Some {kib return all spaces using a single `_search` from {es}, so you must configure this setting lower than the `index.max_result_window` in {es}. The default is `1000`. - -`monitoring.cluster_alerts.allowedSpaces` {ess-icon}:: -Specifies the spaces where cluster alerts are automatically generated. -You must specify all spaces where you want to generate alerts, including the default space. -When the default space is unspecified, {kib} is unable to generate an alert for the default space. -{es} clusters that run on {es} services are all containers. To send monitoring data -from your self-managed {es} installation to {es} services, set to `false`. -The default is `true`. diff --git a/docs/settings/url-drilldown-settings.asciidoc b/docs/settings/url-drilldown-settings.asciidoc index 702829ec34dcc..36dbabbe7fe1e 100644 --- a/docs/settings/url-drilldown-settings.asciidoc +++ b/docs/settings/url-drilldown-settings.asciidoc @@ -6,16 +6,13 @@ Configure the URL drilldown settings in your `kibana.yml` configuration file. -[cols="2*<"] -|=== -| [[external-URL-policy]] `externalUrl.policy` - | Configures the external URL policies. URL drilldowns respect the global *External URL* service, which you can use to deny or allow external URLs. +[[external-URL-policy]] `externalUrl.policy`:: +Configures the external URL policies. URL drilldowns respect the global *External URL* service, which you can use to deny or allow external URLs. By default all external URLs are allowed. -|=== - -For example, to allow external URLs only to the `example.com` domain with the `https` scheme, except for the `danger.example.com` sub-domain, ++ +For example, to allow only external URLs to the `example.com` domain with the `https` scheme, except for the `danger.example.com` sub-domain, which is denied even when `https` scheme is used: - ++ ["source","yml"] ----------- externalUrl.policy: @@ -25,4 +22,3 @@ externalUrl.policy: host: example.com protocol: https ----------- - diff --git a/docs/setup/docker.asciidoc b/docs/setup/docker.asciidoc index 3acaf2ddd2c12..0aa6c680a7761 100644 --- a/docs/setup/docker.asciidoc +++ b/docs/setup/docker.asciidoc @@ -14,10 +14,13 @@ https://github.com/elastic/dockerfiles/tree/{branch}/kibana[GitHub]. These images contain both free and subscription features. <> to try out all of the features. -[float] +[discrete] [[run-kibana-on-docker-for-dev]] === Run {kib} on Docker for development +. Start an {es} container for development or testing: ++ +-- ifeval::["{release-state}"=="unreleased"] NOTE: No Docker images are currently available for {kib} {version}. @@ -26,14 +29,16 @@ endif::[] ifeval::["{release-state}"!="unreleased"] -. Start an {es} container for development or testing: -+ [source,sh,subs="attributes"] ---- docker network create elastic docker pull {es-docker-image} docker run --name es-node01 --net elastic -p 9200:9200 -p 9300:9300 -t {es-docker-image} ---- + +endif::[] + +-- + When you start {es} for the first time, the following security configuration occurs automatically: @@ -51,30 +56,26 @@ and enrollment token. . Copy the generated password and enrollment token and save them in a secure location. These values are shown only when you start {es} for the first time. You'll use these to enroll {kib} with your {es} cluster and log in. + +. In a new terminal session, start {kib} and connect it to your {es} container: + -[NOTE] -==== -If you need to reset the password for the `elastic` user or other -built-in users, run the {ref}/reset-password.html[`elasticsearch-reset-password`] -tool. To generate new enrollment tokens for {kib} or {es} nodes, run the -{ref}/create-enrollment-token.html[`elasticsearch-create-enrollment-token`] tool. -These tools are available in the {es} `bin` directory of the Docker container. +-- +ifeval::["{release-state}"=="unreleased"] -For example: +NOTE: No Docker images are currently available for {kib} {version}. -[source,sh] ----- -docker exec -it es-node01 /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic ----- -==== +endif::[] + +ifeval::["{release-state}"!="unreleased"] -. In a new terminal session, start {kib} and connect it to your {es} container: -+ [source,sh,subs="attributes"] ---- docker pull {docker-image} docker run --name kib-01 --net elastic -p 5601:5601 {docker-image} ---- + +endif::[] +-- + When you start {kib}, a unique link is output to your terminal. @@ -86,7 +87,32 @@ When you start {kib}, a unique link is output to your terminal. .. Log in to {kib} as the `elastic` user with the password that was generated when you started {es}. -[float] +[[docker-generate]] +[discrete] +=== Generate passwords and enrollment tokens +If you need to reset the password for the `elastic` user or other +built-in users, run the {ref}/reset-password.html[`elasticsearch-reset-password`] +tool. This tool is available in the {es} `bin` directory of the Docker container. + +For example, to reset the password for the `elastic` user: + +[source,sh] +---- +docker exec -it es-node01 /usr/share/elasticsearch/bin/elasticsearch-reset-password -u elastic +---- + +If you need to generate new enrollment tokens for {kib} or {es} nodes, run the +{ref}/create-enrollment-token.html[`elasticsearch-create-enrollment-token`] tool. +This tool is available in the {es} `bin` directory of the Docker container. + +For example, to generate a new enrollment token for {kib}: + +[source,sh] +---- +docker exec -it es-node01 /usr/share/elasticsearch/bin/elasticsearch-create-enrollment-token -s kibana +---- + +[discrete] === Remove Docker containers To remove the containers and their network, run: @@ -98,8 +124,7 @@ docker rm es-node01 docker rm kib-01 ---- -endif::[] -[float] +[discrete] [[configuring-kibana-docker]] === Configure Kibana on Docker @@ -108,7 +133,7 @@ conventional approach is to provide a `kibana.yml` file as described in {kibana-ref}/settings.html[Configuring Kibana], but it's also possible to use environment variables to define settings. -[float] +[discrete] [[bind-mount-config]] ==== Bind-mounted configuration @@ -127,7 +152,7 @@ services: ==== Persist the {kib} keystore -By default, {kib] auto-generates a keystore file for secure settings at startup. To persist your {kibana-ref}/secure-settings.html[secure settings], use the `kibana-keystore` utility to bind-mount the parent directory of the keystore to the container. For example: +By default, {kib} auto-generates a keystore file for secure settings at startup. To persist your {kibana-ref}/secure-settings.html[secure settings], use the `kibana-keystore` utility to bind-mount the parent directory of the keystore to the container. For example: ["source","sh",subs="attributes"] ---- @@ -135,7 +160,7 @@ docker run -it --rm -v full_path_to/config:/usr/share/kibana/config -v full_path docker run -it --rm -v full_path_to/config:/usr/share/kibana/config -v full_path_to/data:/usr/share/kibana/data {docker-image} bin/kibana-keystore add test_keystore_setting ---- -[float] +[discrete] [[environment-variable-config]] ==== Environment variable configuration @@ -179,7 +204,7 @@ services: Since environment variables are translated to CLI arguments, they take precedence over settings configured in `kibana.yml`. -[float] +[discrete] [[docker-defaults]] ==== Docker defaults The following settings have different default values when using the Docker diff --git a/docs/setup/install/deb.asciidoc b/docs/setup/install/deb.asciidoc index 3f600d7c2bdbc..8e8c43ff8a15d 100644 --- a/docs/setup/install/deb.asciidoc +++ b/docs/setup/install/deb.asciidoc @@ -188,9 +188,9 @@ locations for a Debian-based system: | path.data | logs - | Logs files location - | /var/log/kibana - | path.logs + | Logs files location + | /var/log/kibana + | path.logs | plugins | Plugin files location. Each plugin will be contained in a subdirectory. diff --git a/docs/setup/install/rpm.asciidoc b/docs/setup/install/rpm.asciidoc index 329af9af0ccf7..0ef714c73b9ba 100644 --- a/docs/setup/install/rpm.asciidoc +++ b/docs/setup/install/rpm.asciidoc @@ -174,7 +174,6 @@ locations for an RPM-based system: | Configuration files including `kibana.yml` | /etc/kibana | <> - d| | data | The location of the data files written to disk by Kibana and its plugins @@ -182,9 +181,9 @@ locations for an RPM-based system: | path.data | logs - | Logs files location - | /var/log/kibana - | path.logs + | Logs files location + | /var/log/kibana + | path.logs | plugins | Plugin files location. Each plugin will be contained in a subdirectory. diff --git a/docs/setup/install/targz.asciidoc b/docs/setup/install/targz.asciidoc index d9849811a7455..1d8c61a6e9a07 100644 --- a/docs/setup/install/targz.asciidoc +++ b/docs/setup/install/targz.asciidoc @@ -125,7 +125,7 @@ important data later on. | home | Kibana home directory or `$KIBANA_HOME` d| Directory created by unpacking the archive - d| + | | bin | Binary scripts including `kibana` to start the Kibana server @@ -137,7 +137,6 @@ important data later on. | Configuration files including `kibana.yml` | $KIBANA_HOME\config | <> - d| | data | The location of the data files written to disk by Kibana and its plugins diff --git a/package.json b/package.json index 374ccee71ec6a..6b7d6662eb70b 100644 --- a/package.json +++ b/package.json @@ -103,7 +103,7 @@ "@elastic/apm-rum": "^5.9.1", "@elastic/apm-rum-react": "^1.3.1", "@elastic/apm-synthtrace": "link:bazel-bin/packages/elastic-apm-synthtrace", - "@elastic/charts": "40.0.0", + "@elastic/charts": "40.1.0", "@elastic/datemath": "link:bazel-bin/packages/elastic-datemath", "@elastic/elasticsearch": "npm:@elastic/elasticsearch-canary@^8.0.0-canary.35", "@elastic/ems-client": "8.0.0", @@ -197,7 +197,7 @@ "axios": "^0.21.1", "base64-js": "^1.3.1", "brace": "0.11.1", - "broadcast-channel": "^4.5.0", + "broadcast-channel": "^4.7.0", "chalk": "^4.1.0", "cheerio": "^1.0.0-rc.10", "chokidar": "^3.4.3", @@ -531,7 +531,6 @@ "@types/file-saver": "^2.0.0", "@types/flot": "^0.0.31", "@types/geojson": "7946.0.7", - "@types/getopts": "^2.0.1", "@types/getos": "^3.0.0", "@types/glob": "^7.1.2", "@types/gulp": "^4.0.6", @@ -568,6 +567,7 @@ "@types/kbn__config": "link:bazel-bin/packages/kbn-config/npm_module_types", "@types/kbn__config-schema": "link:bazel-bin/packages/kbn-config-schema/npm_module_types", "@types/kbn__crypto": "link:bazel-bin/packages/kbn-crypto/npm_module_types", + "@types/kbn__docs-utils": "link:bazel-bin/packages/kbn-docs-utils/npm_module_types", "@types/kbn__i18n": "link:bazel-bin/packages/kbn-i18n/npm_module_types", "@types/kbn__i18n-react": "link:bazel-bin/packages/kbn-i18n-react/npm_module_types", "@types/license-checker": "15.0.0", @@ -677,7 +677,7 @@ "babel-plugin-add-module-exports": "^1.0.4", "babel-plugin-istanbul": "^6.1.1", "babel-plugin-require-context-hook": "^1.0.0", - "babel-plugin-styled-components": "^1.13.3", + "babel-plugin-styled-components": "^2.0.2", "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "backport": "^5.6.6", "callsites": "^3.1.0", diff --git a/packages/BUILD.bazel b/packages/BUILD.bazel index 96b1846147689..aa90c3c122171 100644 --- a/packages/BUILD.bazel +++ b/packages/BUILD.bazel @@ -86,6 +86,7 @@ filegroup( "//packages/kbn-config:build_types", "//packages/kbn-config-schema:build_types", "//packages/kbn-crypto:build_types", + "//packages/kbn-docs-utils:build_types", "//packages/kbn-i18n:build_types", "//packages/kbn-i18n-react:build_types", ], diff --git a/packages/kbn-apm-config-loader/src/init_apm.test.ts b/packages/kbn-apm-config-loader/src/init_apm.test.ts index 95f0a15a448c8..cabab421519bd 100644 --- a/packages/kbn-apm-config-loader/src/init_apm.test.ts +++ b/packages/kbn-apm-config-loader/src/init_apm.test.ts @@ -12,13 +12,13 @@ import { initApm } from './init_apm'; import apm from 'elastic-apm-node'; describe('initApm', () => { - let apmAddFilterSpy: jest.SpyInstance; - let apmStartSpy: jest.SpyInstance; + let apmAddFilterMock: jest.Mock; + let apmStartMock: jest.Mock; let getConfig: jest.Mock; beforeEach(() => { - apmAddFilterSpy = jest.spyOn(apm, 'addFilter').mockImplementation(() => undefined); - apmStartSpy = jest.spyOn(apm, 'start').mockImplementation(() => undefined as any); + apmAddFilterMock = apm.addFilter as jest.Mock; + apmStartMock = apm.start as jest.Mock; getConfig = jest.fn(); mockLoadConfiguration.mockImplementation(() => ({ @@ -27,7 +27,8 @@ describe('initApm', () => { }); afterEach(() => { - jest.restoreAllMocks(); + apmAddFilterMock.mockReset(); + apmStartMock.mockReset(); mockLoadConfiguration.mockReset(); }); @@ -48,8 +49,8 @@ describe('initApm', () => { it('registers a filter using `addFilter`', () => { initApm(['foo', 'bar'], 'rootDir', true, 'service-name'); - expect(apmAddFilterSpy).toHaveBeenCalledTimes(1); - expect(apmAddFilterSpy).toHaveBeenCalledWith(expect.any(Function)); + expect(apmAddFilterMock).toHaveBeenCalledTimes(1); + expect(apmAddFilterMock).toHaveBeenCalledWith(expect.any(Function)); }); it('starts apm with the config returned from `getConfig`', () => { @@ -60,7 +61,7 @@ describe('initApm', () => { initApm(['foo', 'bar'], 'rootDir', true, 'service-name'); - expect(apmStartSpy).toHaveBeenCalledTimes(1); - expect(apmStartSpy).toHaveBeenCalledWith(config); + expect(apmStartMock).toHaveBeenCalledTimes(1); + expect(apmStartMock).toHaveBeenCalledWith(config); }); }); diff --git a/packages/kbn-cli-dev-mode/BUILD.bazel b/packages/kbn-cli-dev-mode/BUILD.bazel index 66e00706e9e58..dfb441dffc6ef 100644 --- a/packages/kbn-cli-dev-mode/BUILD.bazel +++ b/packages/kbn-cli-dev-mode/BUILD.bazel @@ -60,12 +60,12 @@ TYPES_DEPS = [ "@npm//chokidar", "@npm//elastic-apm-node", "@npm//execa", + "@npm//getopts", "@npm//moment", "@npm//rxjs", "@npm//supertest", "@npm//@types/hapi__h2o2", "@npm//@types/hapi__hapi", - "@npm//@types/getopts", "@npm//@types/jest", "@npm//@types/lodash", "@npm//@types/node", diff --git a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts index 9fa13b013f195..06ded8d8bf526 100644 --- a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts +++ b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.test.ts @@ -65,7 +65,7 @@ it('produces the right watch and ignore list', () => { /x-pack/test/plugin_functional/plugins/resolver_test/target/**, /x-pack/test/plugin_functional/plugins/resolver_test/scripts/**, /x-pack/test/plugin_functional/plugins/resolver_test/docs/**, - /x-pack/plugins/reporting/chromium, + /x-pack/plugins/screenshotting/chromium, /x-pack/plugins/security_solution/cypress, /x-pack/plugins/apm/scripts, /x-pack/plugins/apm/ftr_e2e, diff --git a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts index e1bd431d280a4..f075dc806b6ec 100644 --- a/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts +++ b/packages/kbn-cli-dev-mode/src/get_server_watch_paths.ts @@ -56,7 +56,7 @@ export function getServerWatchPaths({ pluginPaths, pluginScanDirs }: Options) { /\.(md|sh|txt)$/, /debug\.log$/, ...pluginInternalDirsIgnore, - fromRoot('x-pack/plugins/reporting/chromium'), + fromRoot('x-pack/plugins/screenshotting/chromium'), fromRoot('x-pack/plugins/security_solution/cypress'), fromRoot('x-pack/plugins/apm/scripts'), fromRoot('x-pack/plugins/apm/ftr_e2e'), // prevents restarts for APM cypress tests diff --git a/packages/kbn-docs-utils/BUILD.bazel b/packages/kbn-docs-utils/BUILD.bazel index 6bb37b3500152..37e5bb06377cc 100644 --- a/packages/kbn-docs-utils/BUILD.bazel +++ b/packages/kbn-docs-utils/BUILD.bazel @@ -1,9 +1,10 @@ -load("@npm//@bazel/typescript:index.bzl", "ts_config", "ts_project") -load("@build_bazel_rules_nodejs//:index.bzl", "js_library", "pkg_npm") -load("//src/dev/bazel:index.bzl", "jsts_transpiler") +load("@npm//@bazel/typescript:index.bzl", "ts_config") +load("@build_bazel_rules_nodejs//:index.bzl", "js_library") +load("//src/dev/bazel:index.bzl", "jsts_transpiler", "pkg_npm", "pkg_npm_types", "ts_project") PKG_BASE_NAME = "kbn-docs-utils" PKG_REQUIRE_NAME = "@kbn/docs-utils" +TYPES_PKG_REQUIRE_NAME = "@types/kbn__docs-utils" SOURCE_FILES = glob( [ @@ -77,7 +78,7 @@ ts_project( js_library( name = PKG_BASE_NAME, srcs = NPM_MODULE_EXTRA_FILES, - deps = RUNTIME_DEPS + [":target_node", ":tsc_types"], + deps = RUNTIME_DEPS + [":target_node"], package_name = PKG_REQUIRE_NAME, visibility = ["//visibility:public"], ) @@ -96,3 +97,20 @@ filegroup( ], visibility = ["//visibility:public"], ) + +pkg_npm_types( + name = "npm_module_types", + srcs = SRCS, + deps = [":tsc_types"], + package_name = TYPES_PKG_REQUIRE_NAME, + tsconfig = ":tsconfig", + visibility = ["//visibility:public"], +) + +filegroup( + name = "build_types", + srcs = [ + ":npm_module_types", + ], + visibility = ["//visibility:public"], +) diff --git a/packages/kbn-docs-utils/package.json b/packages/kbn-docs-utils/package.json index dcff832583f59..84fc3ccb0cded 100644 --- a/packages/kbn-docs-utils/package.json +++ b/packages/kbn-docs-utils/package.json @@ -4,7 +4,6 @@ "license": "SSPL-1.0 OR Elastic License 2.0", "private": "true", "main": "target_node/index.js", - "types": "target_types/index.d.ts", "kibana": { "devOnly": true } diff --git a/packages/kbn-optimizer/limits.yml b/packages/kbn-optimizer/limits.yml index 41c4d3bdd1b35..1de3a8a1b3976 100644 --- a/packages/kbn-optimizer/limits.yml +++ b/packages/kbn-optimizer/limits.yml @@ -117,3 +117,4 @@ pageLoadAssetSize: dataViewManagement: 5000 reporting: 57003 visTypeHeatmap: 25340 + screenshotting: 17017 diff --git a/packages/kbn-storybook/src/lib/theme_switcher.tsx b/packages/kbn-storybook/src/lib/theme_switcher.tsx index 3d6f7999545a0..8cc805ee2e494 100644 --- a/packages/kbn-storybook/src/lib/theme_switcher.tsx +++ b/packages/kbn-storybook/src/lib/theme_switcher.tsx @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import { Icons, IconButton, TooltipLinkList, WithTooltip } from '@storybook/components'; import { useGlobals } from '@storybook/api'; @@ -17,14 +17,52 @@ type Link = ArrayItem['links']>; const defaultTheme = 'v8.light'; export function ThemeSwitcher() { - const [globals, updateGlobals] = useGlobals(); - const selectedTheme = globals.euiTheme; + const [{ euiTheme: selectedTheme }, updateGlobals] = useGlobals(); - if (!selectedTheme) { - updateGlobals({ euiTheme: defaultTheme }); - } + const selectTheme = useCallback( + (themeId: string) => { + updateGlobals({ euiTheme: themeId }); + }, + [updateGlobals] + ); - function Menu({ onHide }: { onHide: () => void }) { + useEffect(() => { + if (!selectedTheme) { + selectTheme(defaultTheme); + } + }, [selectTheme, selectedTheme]); + + return ( + ( + + )} + > + {/* @ts-ignore Remove when @storybook has moved to @emotion v11 */} + + + + + ); +} + +const ThemeSwitcherTooltip = React.memo( + ({ + onHide, + onChangeSelectedTheme, + selectedTheme, + }: { + onHide: () => void; + onChangeSelectedTheme: (themeId: string) => void; + selectedTheme: string; + }) => { const links = [ { id: 'v8.light', @@ -38,8 +76,8 @@ export function ThemeSwitcher() { (link): Link => ({ ...link, onClick: (_event, item) => { - if (item.id !== selectedTheme) { - updateGlobals({ euiTheme: item.id }); + if (item.id != null && item.id !== selectedTheme) { + onChangeSelectedTheme(item.id); } onHide(); }, @@ -49,18 +87,4 @@ export function ThemeSwitcher() { return ; } - - return ( - } - > - {/* @ts-ignore Remove when @storybook has moved to @emotion v11 */} - - - - - ); -} +); diff --git a/packages/kbn-test/BUILD.bazel b/packages/kbn-test/BUILD.bazel index c42c33483703e..1d1d95d639861 100644 --- a/packages/kbn-test/BUILD.bazel +++ b/packages/kbn-test/BUILD.bazel @@ -76,6 +76,7 @@ TYPES_DEPS = [ "//packages/kbn-i18n-react:npm_module_types", "//packages/kbn-utils", "@npm//@elastic/elasticsearch", + "@npm//elastic-apm-node", "@npm//del", "@npm//form-data", "@npm//jest", diff --git a/packages/kbn-test/jest-preset.js b/packages/kbn-test/jest-preset.js index db64f070b37d9..e2607100babc5 100644 --- a/packages/kbn-test/jest-preset.js +++ b/packages/kbn-test/jest-preset.js @@ -28,6 +28,7 @@ module.exports = { moduleNameMapper: { '@elastic/eui/lib/(.*)?': '/node_modules/@elastic/eui/test-env/$1', '@elastic/eui$': '/node_modules/@elastic/eui/test-env', + 'elastic-apm-node': '/node_modules/@kbn/test/target_node/jest/mocks/apm_agent_mock.js', '\\.module.(css|scss)$': '/node_modules/@kbn/test/target_node/jest/mocks/css_module_mock.js', '\\.(css|less|scss)$': '/node_modules/@kbn/test/target_node/jest/mocks/style_mock.js', diff --git a/packages/kbn-test/src/jest/mocks/apm_agent_mock.ts b/packages/kbn-test/src/jest/mocks/apm_agent_mock.ts new file mode 100644 index 0000000000000..1615f710504ad --- /dev/null +++ b/packages/kbn-test/src/jest/mocks/apm_agent_mock.ts @@ -0,0 +1,63 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import type { Agent } from 'elastic-apm-node'; + +/** + * `elastic-apm-node` patches the runtime at import time + * causing memory leak with jest module sandbox, so it + * needs to be mocked for tests + */ +const agent: jest.Mocked = { + start: jest.fn().mockImplementation(() => agent), + isStarted: jest.fn().mockReturnValue(false), + getServiceName: jest.fn().mockReturnValue('mock-service'), + setFramework: jest.fn(), + addPatch: jest.fn(), + removePatch: jest.fn(), + clearPatches: jest.fn(), + lambda: jest.fn(), + handleUncaughtExceptions: jest.fn(), + captureError: jest.fn(), + currentTraceparent: null, + currentTraceIds: {}, + startTransaction: jest.fn().mockReturnValue(null), + setTransactionName: jest.fn(), + endTransaction: jest.fn(), + currentTransaction: null, + startSpan: jest.fn(), + currentSpan: null, + setLabel: jest.fn().mockReturnValue(false), + addLabels: jest.fn().mockReturnValue(false), + setUserContext: jest.fn(), + setCustomContext: jest.fn(), + addFilter: jest.fn(), + addErrorFilter: jest.fn(), + addSpanFilter: jest.fn(), + addTransactionFilter: jest.fn(), + addMetadataFilter: jest.fn(), + flush: jest.fn(), + destroy: jest.fn(), + registerMetric: jest.fn(), + setTransactionOutcome: jest.fn(), + setSpanOutcome: jest.fn(), + middleware: { + connect: jest.fn().mockReturnValue(jest.fn()), + }, + logger: { + fatal: jest.fn(), + error: jest.fn(), + warn: jest.fn(), + info: jest.fn(), + debug: jest.fn(), + trace: jest.fn(), + }, +}; + +// eslint-disable-next-line import/no-default-export +export default agent; diff --git a/packages/kbn-test/src/kbn/users.ts b/packages/kbn-test/src/kbn/users.ts index 230354089dcac..88480fde74ddc 100644 --- a/packages/kbn-test/src/kbn/users.ts +++ b/packages/kbn-test/src/kbn/users.ts @@ -14,7 +14,7 @@ export const kibanaTestUser = { }; export const kibanaServerTestUser = { - username: env.TEST_KIBANA_SERVER_USER || 'kibana', + username: env.TEST_KIBANA_SERVER_USER || 'kibana_system', password: env.TEST_KIBANA_SERVER_PASS || 'changeme', }; diff --git a/packages/kbn-typed-react-router-config/BUILD.bazel b/packages/kbn-typed-react-router-config/BUILD.bazel index b347915ae3310..d759948a6c576 100644 --- a/packages/kbn-typed-react-router-config/BUILD.bazel +++ b/packages/kbn-typed-react-router-config/BUILD.bazel @@ -41,10 +41,10 @@ TYPES_DEPS = [ "@npm//query-string", "@npm//utility-types", "@npm//@types/jest", - "@npm//@types/history", "@npm//@types/node", "@npm//@types/react-router-config", "@npm//@types/react-router-dom", + "@npm//@types/history", ] jsts_transpiler( diff --git a/packages/kbn-typed-react-router-config/src/create_router.test.tsx b/packages/kbn-typed-react-router-config/src/create_router.test.tsx index e82fcf791804e..ac337f8bb5b87 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.test.tsx +++ b/packages/kbn-typed-react-router-config/src/create_router.test.tsx @@ -267,7 +267,6 @@ describe('createRouter', () => { const matches = router.matchRoutes('/', history.location); - // @ts-expect-error 4.3.5 upgrade - router doesn't seem able to merge properly when two routes match expect(matches[1]?.match.params).toEqual({ query: { rangeFrom: 'now-30m', @@ -286,7 +285,6 @@ describe('createRouter', () => { expect(matchedRoutes.length).toEqual(4); - // @ts-expect-error 4.3.5 upgrade - router doesn't seem able to merge properly when two routes match expect(matchedRoutes[matchedRoutes.length - 1].match).toEqual({ isExact: true, params: { diff --git a/packages/kbn-typed-react-router-config/src/create_router.ts b/packages/kbn-typed-react-router-config/src/create_router.ts index 186f949d9c8e8..89ff4fc6b0c6c 100644 --- a/packages/kbn-typed-react-router-config/src/create_router.ts +++ b/packages/kbn-typed-react-router-config/src/create_router.ts @@ -23,7 +23,7 @@ function toReactRouterPath(path: string) { return path.replace(/(?:{([^\/]+)})/g, ':$1'); } -export function createRouter(routes: TRoute[]): Router { +export function createRouter(routes: TRoutes): Router { const routesByReactRouterConfig = new Map(); const reactRouterConfigsByRoute = new Map(); @@ -181,8 +181,10 @@ export function createRouter(routes: TRoute[]): Router { + return link(path, ...args); + }, getParams: (...args: any[]) => { const matches = matchRoutes(...args); return matches.length @@ -195,13 +197,11 @@ export function createRouter(routes: TRoute[]): Router { return matchRoutes(...args) as any; }, - getRoutePath: (route: Route) => { + getRoutePath: (route) => { return reactRouterConfigsByRoute.get(route)!.path as string; }, getRoutesToMatch: (path: string) => { - return getRoutesToMatch(path) as unknown as FlattenRoutesOf; + return getRoutesToMatch(path) as unknown as FlattenRoutesOf; }, }; - - return router; } diff --git a/packages/kbn-typed-react-router-config/src/types/index.ts b/packages/kbn-typed-react-router-config/src/types/index.ts index 3c09b60054a0c..c1ae5afd816ee 100644 --- a/packages/kbn-typed-react-router-config/src/types/index.ts +++ b/packages/kbn-typed-react-router-config/src/types/index.ts @@ -115,7 +115,7 @@ export interface RouteMatch { params: t.Type; } ? t.TypeOf - : AnyObj; + : {}; }; } @@ -160,11 +160,10 @@ interface ReadonlyPlainRoute { } export type Route = PlainRoute | ReadonlyPlainRoute; -type AnyObj = Record; interface DefaultOutput { - path: AnyObj; - query: AnyObj; + path: {}; + query: {}; } type OutputOfRouteMatch = TRouteMatch extends { @@ -191,21 +190,20 @@ type TypeOfRouteMatch = TRouteMatch extends { route: { params: t.Type }; } ? t.TypeOf - : AnyObj; + : {}; type TypeOfMatches = TRouteMatches extends [RouteMatch] ? TypeOfRouteMatch : TRouteMatches extends [RouteMatch, ...infer TNextRouteMatches] ? TypeOfRouteMatch & - (TNextRouteMatches extends RouteMatch[] ? TypeOfMatches : AnyObj) - : AnyObj; + (TNextRouteMatches extends RouteMatch[] ? TypeOfMatches : {}) + : {}; export type TypeOf< TRoutes extends Route[], TPath extends PathsOf, TWithDefaultOutput extends boolean = true -> = TypeOfMatches> & - (TWithDefaultOutput extends true ? DefaultOutput : AnyObj); +> = TypeOfMatches> & (TWithDefaultOutput extends true ? DefaultOutput : {}); export type TypeAsArgs = keyof TObject extends never ? [] @@ -278,7 +276,7 @@ type MapRoute = MaybeUnion< >; } > - : AnyObj + : {} >; type MapRoutes = TRoutes extends [Route] @@ -343,7 +341,7 @@ type MapRoutes = TRoutes extends [Route] MapRoute & MapRoute & MapRoute - : AnyObj; + : {}; // const element = null as any; diff --git a/src/core/public/doc_links/doc_links_service.ts b/src/core/public/doc_links/doc_links_service.ts index 24c085ef64de3..692367cd0f580 100644 --- a/src/core/public/doc_links/doc_links_service.ts +++ b/src/core/public/doc_links/doc_links_service.ts @@ -113,6 +113,7 @@ export class DocLinksService { usersAccess: `${ENTERPRISE_SEARCH_DOCS}users-access.html`, }, workplaceSearch: { + apiKeys: `${WORKPLACE_SEARCH_DOCS}workplace-search-api-authentication.html`, box: `${WORKPLACE_SEARCH_DOCS}workplace-search-box-connector.html`, confluenceCloud: `${WORKPLACE_SEARCH_DOCS}workplace-search-confluence-cloud-connector.html`, confluenceServer: `${WORKPLACE_SEARCH_DOCS}workplace-search-confluence-server-connector.html`, @@ -671,6 +672,7 @@ export interface DocLinksStart { readonly usersAccess: string; }; readonly workplaceSearch: { + readonly apiKeys: string; readonly box: string; readonly confluenceCloud: string; readonly confluenceServer: string; diff --git a/src/core/public/public.api.md b/src/core/public/public.api.md index 30225acb3dd8d..08d41ab1301b0 100644 --- a/src/core/public/public.api.md +++ b/src/core/public/public.api.md @@ -571,6 +571,7 @@ export interface DocLinksStart { readonly usersAccess: string; }; readonly workplaceSearch: { + readonly apiKeys: string; readonly box: string; readonly confluenceCloud: string; readonly confluenceServer: string; diff --git a/src/dev/build/tasks/install_chromium.js b/src/dev/build/tasks/install_chromium.js index ad60019ea81a4..2bcceb33fad00 100644 --- a/src/dev/build/tasks/install_chromium.js +++ b/src/dev/build/tasks/install_chromium.js @@ -6,10 +6,8 @@ * Side Public License, v 1. */ -import { first } from 'rxjs/operators'; - // eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { installBrowser } from '../../../../x-pack/plugins/reporting/server/browsers/install'; +import { install } from '../../../../x-pack/plugins/screenshotting/server/utils'; export const InstallChromium = { description: 'Installing Chromium', @@ -22,13 +20,23 @@ export const InstallChromium = { // revert after https://github.com/elastic/kibana/issues/109949 if (target === 'darwin-arm64') continue; - const { binaryPath$ } = installBrowser( - log, - build.resolvePathForPlatform(platform, 'x-pack/plugins/reporting/chromium'), + const logger = { + get: log.withType.bind(log), + debug: log.debug.bind(log), + info: log.info.bind(log), + warn: log.warning.bind(log), + trace: log.verbose.bind(log), + error: log.error.bind(log), + fatal: log.error.bind(log), + log: log.write.bind(log), + }; + + await install( + logger, + build.resolvePathForPlatform(platform, 'x-pack/plugins/screenshotting/chromium'), platform.getName(), platform.getArchitecture() ); - await binaryPath$.pipe(first()).toPromise(); } }, }; diff --git a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js index 05af7c2a154a4..57467d84f1f61 100644 --- a/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js +++ b/src/dev/code_coverage/ingest_coverage/__tests__/enumerate_patterns.test.js @@ -15,16 +15,17 @@ const log = new ToolingLog({ }); describe(`enumeratePatterns`, () => { - it(`should resolve x-pack/plugins/reporting/server/browsers/extract/unzip.ts to kibana-reporting`, () => { + it(`should resolve x-pack/plugins/screenshotting/server/browsers/extract/unzip.ts to kibana-screenshotting`, () => { const actual = enumeratePatterns(REPO_ROOT)(log)( - new Map([['x-pack/plugins/reporting', ['kibana-reporting']]]) + new Map([['x-pack/plugins/screenshotting', ['kibana-screenshotting']]]) ); - expect( - actual[0].includes( - 'x-pack/plugins/reporting/server/browsers/extract/unzip.ts kibana-reporting' - ) - ).toBe(true); + expect(actual).toHaveProperty( + '0', + expect.arrayContaining([ + 'x-pack/plugins/screenshotting/server/browsers/extract/unzip.ts kibana-screenshotting', + ]) + ); }); it(`should resolve src/plugins/charts/common/static/color_maps/color_maps.ts to kibana-app`, () => { const actual = enumeratePatterns(REPO_ROOT)(log)( diff --git a/src/plugins/console/public/lib/mappings/mappings.js b/src/plugins/console/public/lib/mappings/mappings.js index d4996f9fd8862..2a4ee6b2e346b 100644 --- a/src/plugins/console/public/lib/mappings/mappings.js +++ b/src/plugins/console/public/lib/mappings/mappings.js @@ -250,7 +250,10 @@ function retrieveSettings(settingsKey, settingsToRetrieve) { // Fetch autocomplete info if setting is set to true, and if user has made changes. if (settingsToRetrieve[settingsKey] === true) { - return es.send('GET', settingKeyToPathMap[settingsKey], null, true); + // Use pretty=false in these request in order to compress the response by removing whitespace + const path = `${settingKeyToPathMap[settingsKey]}?pretty=false`; + + return es.send('GET', path, null, true); } else { const settingsPromise = new $.Deferred(); if (settingsToRetrieve[settingsKey] === false) { diff --git a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx index 40f6f872535f9..e6404d6327ec3 100644 --- a/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/add_to_library_action.test.tsx @@ -65,6 +65,7 @@ beforeEach(async () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, + theme: coreStart.theme, presentationUtil: getStubPluginServices(), screenshotMode: screenshotModePluginMock.createSetupContract(), }; diff --git a/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx b/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx index fc4c6b299284b..3c3872226ffb0 100644 --- a/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/clone_panel_action.test.tsx @@ -56,6 +56,7 @@ beforeEach(async () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, + theme: coreStart.theme, presentationUtil: getStubPluginServices(), screenshotMode: screenshotModePluginMock.createSetupContract(), }; diff --git a/src/plugins/dashboard/public/application/actions/copy_to_dashboard_action.tsx b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_action.tsx index f16486dd65e3c..3c9c1cbbba83e 100644 --- a/src/plugins/dashboard/public/application/actions/copy_to_dashboard_action.tsx +++ b/src/plugins/dashboard/public/application/actions/copy_to_dashboard_action.tsx @@ -7,7 +7,7 @@ */ import React from 'react'; -import { OverlayStart } from '../../../../../core/public'; +import { CoreStart, OverlayStart } from '../../../../../core/public'; import { dashboardCopyToDashboardAction } from '../../dashboard_strings'; import { EmbeddableStateTransfer, IEmbeddable } from '../../services/embeddable'; import { toMountPoint } from '../../services/kibana_react'; @@ -37,6 +37,7 @@ export class CopyToDashboardAction implements Action + />, + { theme$: this.theme.theme$ } ), { maxWidth: 400, diff --git a/src/plugins/dashboard/public/application/actions/expand_panel_action.test.tsx b/src/plugins/dashboard/public/application/actions/expand_panel_action.test.tsx index b20a96c79aed6..f99b539ecb26c 100644 --- a/src/plugins/dashboard/public/application/actions/expand_panel_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/expand_panel_action.test.tsx @@ -48,6 +48,7 @@ beforeEach(async () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreMock.createStart().http, + theme: coreMock.createStart().theme, presentationUtil: getStubPluginServices(), screenshotMode: screenshotModePluginMock.createSetupContract(), }; diff --git a/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx b/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx index 797765eda232d..c08a8d4af68dd 100644 --- a/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/export_csv_action.test.tsx @@ -61,6 +61,7 @@ describe('Export CSV action', () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, + theme: coreStart.theme, presentationUtil: getStubPluginServices(), screenshotMode: screenshotModePluginMock.createSetupContract(), }; diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx index ab442bf839e37..92042f581fad4 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.test.tsx @@ -62,6 +62,7 @@ beforeEach(async () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, + theme: coreStart.theme, presentationUtil: getStubPluginServices(), screenshotMode: screenshotModePluginMock.createSetupContract(), }; @@ -90,7 +91,7 @@ beforeEach(async () => { }); test('Notification is incompatible with Error Embeddables', async () => { - const action = new LibraryNotificationAction(unlinkAction); + const action = new LibraryNotificationAction(coreStart.theme, unlinkAction); const errorEmbeddable = new ErrorEmbeddable( 'Wow what an awful error', { id: ' 404' }, @@ -100,19 +101,19 @@ test('Notification is incompatible with Error Embeddables', async () => { }); test('Notification is shown when embeddable on dashboard has reference type input', async () => { - const action = new LibraryNotificationAction(unlinkAction); + const action = new LibraryNotificationAction(coreStart.theme, unlinkAction); embeddable.updateInput(await embeddable.getInputAsRefType()); expect(await action.isCompatible({ embeddable })).toBe(true); }); test('Notification is not shown when embeddable input is by value', async () => { - const action = new LibraryNotificationAction(unlinkAction); + const action = new LibraryNotificationAction(coreStart.theme, unlinkAction); embeddable.updateInput(await embeddable.getInputAsValueType()); expect(await action.isCompatible({ embeddable })).toBe(false); }); test('Notification is not shown when view mode is set to view', async () => { - const action = new LibraryNotificationAction(unlinkAction); + const action = new LibraryNotificationAction(coreStart.theme, unlinkAction); embeddable.updateInput(await embeddable.getInputAsRefType()); embeddable.updateInput({ viewMode: ViewMode.VIEW }); expect(await action.isCompatible({ embeddable })).toBe(false); diff --git a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx index 4211dcf1443ed..b867f83985a6e 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_action.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_action.tsx @@ -9,7 +9,8 @@ import React from 'react'; import { Action, IncompatibleActionError } from '../../services/ui_actions'; -import { reactToUiComponent } from '../../services/kibana_react'; +import { CoreStart } from '../../../../../core/public'; +import { KibanaThemeProvider, reactToUiComponent } from '../../services/kibana_react'; import { IEmbeddable, ViewMode, @@ -32,7 +33,7 @@ export class LibraryNotificationAction implements Action { const { embeddable } = context; return ( - + + + ); }; diff --git a/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx b/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx index de1a475fdbd18..a2a55404072eb 100644 --- a/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx +++ b/src/plugins/dashboard/public/application/actions/library_notification_popover.test.tsx @@ -58,6 +58,7 @@ describe('LibraryNotificationPopover', () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, + theme: coreStart.theme, presentationUtil: getStubPluginServices(), screenshotMode: screenshotModePluginMock.createSetupContract(), }; 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 cca72d10fac15..fa79b02d20dd5 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 @@ -47,7 +47,8 @@ export async function openReplacePanelFlyout(options: { savedObjectsFinder={savedObjectFinder} notifications={notifications} getEmbeddableFactories={getEmbeddableFactories} - /> + />, + { theme$: core.theme.theme$ } ), { 'data-test-subj': 'dashboardReplacePanel', diff --git a/src/plugins/dashboard/public/application/actions/replace_panel_action.test.tsx b/src/plugins/dashboard/public/application/actions/replace_panel_action.test.tsx index fe39f6112a7f3..9781736606607 100644 --- a/src/plugins/dashboard/public/application/actions/replace_panel_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/replace_panel_action.test.tsx @@ -48,6 +48,7 @@ beforeEach(async () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, + theme: coreStart.theme, presentationUtil: getStubPluginServices(), screenshotMode: screenshotModePluginMock.createSetupContract(), }; diff --git a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx index 4f10f833f643c..f82b8d1bc7a87 100644 --- a/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx +++ b/src/plugins/dashboard/public/application/actions/unlink_from_library_action.test.tsx @@ -57,6 +57,7 @@ beforeEach(async () => { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreStart.http, + theme: coreStart.theme, presentationUtil: getStubPluginServices(), screenshotMode: screenshotModePluginMock.createSetupContract(), }; diff --git a/src/plugins/dashboard/public/application/dashboard_router.tsx b/src/plugins/dashboard/public/application/dashboard_router.tsx index c74ac506e4809..ae16527b64440 100644 --- a/src/plugins/dashboard/public/application/dashboard_router.tsx +++ b/src/plugins/dashboard/public/application/dashboard_router.tsx @@ -20,7 +20,7 @@ import { DashboardListing } from './listing'; import { dashboardStateStore } from './state'; import { DashboardApp } from './dashboard_app'; import { DashboardNoMatch } from './listing/dashboard_no_match'; -import { KibanaContextProvider } from '../services/kibana_react'; +import { KibanaContextProvider, KibanaThemeProvider } from '../services/kibana_react'; import { addHelpMenuToAppChrome, DashboardSessionStorage } from './lib'; import { createDashboardListingFilterUrl } from '../dashboard_constants'; import { createDashboardEditUrl, DashboardConstants } from '../dashboard_constants'; @@ -226,26 +226,28 @@ export async function mountApp({ - - - - - - - - - - + + + + + + + + + + + + diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx index 744d63c1ba04a..d5eef0c05129d 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.test.tsx @@ -55,6 +55,7 @@ const options: DashboardContainerServices = { uiActions: {} as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreMock.createStart().http, + theme: coreMock.createStart().theme, presentationUtil, }; diff --git a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx index d7081bf020d85..3e259d4e26179 100644 --- a/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx +++ b/src/plugins/dashboard/public/application/embeddable/dashboard_container.tsx @@ -36,6 +36,7 @@ import { KibanaContextProvider, KibanaReactContext, KibanaReactContextValue, + KibanaThemeProvider, } from '../../services/kibana_react'; import { PLACEHOLDER_EMBEDDABLE } from './placeholder'; import { DashboardAppCapabilities, DashboardContainerInput } from '../../types'; @@ -60,6 +61,7 @@ export interface DashboardContainerServices { uiSettings: IUiSettingsClient; embeddable: EmbeddableStart; uiActions: UiActionsStart; + theme: CoreStart['theme']; http: CoreStart['http']; } @@ -259,9 +261,11 @@ export class DashboardContainer extends Container - - - + + + + + , dom diff --git a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx index 7518a36433d35..59f346caf4b0d 100644 --- a/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/grid/dashboard_grid.test.tsx @@ -71,6 +71,7 @@ function prepare(props?: Partial) { } as any, uiSettings: uiSettingsServiceMock.createStartContract(), http: coreMock.createStart().http, + theme: coreMock.createStart().theme, presentationUtil, screenshotMode: screenshotModePluginMock.createSetupContract(), }; diff --git a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable.tsx b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable.tsx index 97ca4a1332f24..598d6fc5eabb5 100644 --- a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable.tsx +++ b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable.tsx @@ -10,15 +10,25 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { EuiLoadingChart } from '@elastic/eui'; import classNames from 'classnames'; +import { CoreStart } from 'src/core/public'; import { Embeddable, EmbeddableInput, IContainer } from '../../../services/embeddable'; +import { KibanaThemeProvider } from '../../../services/kibana_react'; export const PLACEHOLDER_EMBEDDABLE = 'placeholder'; +export interface PlaceholderEmbeddableServices { + theme: CoreStart['theme']; +} + export class PlaceholderEmbeddable extends Embeddable { public readonly type = PLACEHOLDER_EMBEDDABLE; private node?: HTMLElement; - constructor(initialInput: EmbeddableInput, parent?: IContainer) { + constructor( + initialInput: EmbeddableInput, + private readonly services: PlaceholderEmbeddableServices, + parent?: IContainer + ) { super(initialInput, {}, parent); this.input = initialInput; } @@ -30,9 +40,11 @@ export class PlaceholderEmbeddable extends Embeddable { const classes = classNames('embPanel', 'embPanel-isLoading'); ReactDOM.render( -
- -
, + +
+ +
+
, node ); } diff --git a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts index 50cf85998913b..b0dce72ad77e3 100644 --- a/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts +++ b/src/plugins/dashboard/public/application/embeddable/placeholder/placeholder_embeddable_factory.ts @@ -13,11 +13,17 @@ import { EmbeddableInput, IContainer, } from '../../../services/embeddable'; -import { PlaceholderEmbeddable, PLACEHOLDER_EMBEDDABLE } from './placeholder_embeddable'; +import { + PlaceholderEmbeddable, + PlaceholderEmbeddableServices, + PLACEHOLDER_EMBEDDABLE, +} from './placeholder_embeddable'; export class PlaceholderEmbeddableFactory implements EmbeddableFactoryDefinition { public readonly type = PLACEHOLDER_EMBEDDABLE; + constructor(private readonly getStartServices: () => Promise) {} + public async isEditable() { return false; } @@ -27,7 +33,8 @@ export class PlaceholderEmbeddableFactory implements EmbeddableFactoryDefinition } public async create(initialInput: EmbeddableInput, parent?: IContainer) { - return new PlaceholderEmbeddable(initialInput, parent); + const services = await this.getStartServices(); + return new PlaceholderEmbeddable(initialInput, services, parent); } public getDisplayName() { diff --git a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx index f0333cefd612f..d9de67ee9455d 100644 --- a/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx +++ b/src/plugins/dashboard/public/application/embeddable/viewport/dashboard_viewport.test.tsx @@ -49,6 +49,7 @@ function getProps(props?: Partial): { application: applicationServiceMock.createStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), http: coreMock.createStart().http, + theme: coreMock.createStart().theme, embeddable: { getTriggerCompatibleActions: (() => []) as any, getEmbeddablePanel: jest.fn(), diff --git a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx b/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx index e3f7b32ef8223..f2792790f2f5d 100644 --- a/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx +++ b/src/plugins/dashboard/public/application/listing/confirm_overlays.tsx @@ -20,7 +20,7 @@ import { } from '@elastic/eui'; import React from 'react'; -import { OverlayStart } from '../../../../../core/public'; +import { CoreStart, OverlayStart } from '../../../../../core/public'; import { toMountPoint } from '../../services/kibana_react'; import { createConfirmStrings, discardConfirmStrings } from '../../dashboard_strings'; @@ -43,6 +43,7 @@ export const confirmDiscardUnsavedChanges = (overlays: OverlayStart, discardCall export const confirmCreateWithUnsaved = ( overlays: OverlayStart, + theme: CoreStart['theme'], startBlankCallback: () => void, contineCallback: () => void ) => { @@ -105,7 +106,8 @@ export const confirmCreateWithUnsaved = ( - + , + { theme$: theme.theme$ } ), { 'data-test-subj': 'dashboardCreateConfirmModal', diff --git a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx index 605e5ec88565f..deb8671edb97d 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_listing.tsx @@ -119,6 +119,7 @@ export const DashboardListing = ({ } else { confirmCreateWithUnsaved( core.overlays, + core.theme, () => { dashboardSessionStorage.clearState(); redirectTo({ destination: 'dashboard' }); @@ -126,7 +127,7 @@ export const DashboardListing = ({ () => redirectTo({ destination: 'dashboard' }) ); } - }, [dashboardSessionStorage, redirectTo, core.overlays]); + }, [dashboardSessionStorage, redirectTo, core.overlays, core.theme]); const emptyPrompt = useMemo(() => { if (!showWriteControls) { diff --git a/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx b/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx index 228a6994dcbb7..df7e9bc21e46d 100644 --- a/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx +++ b/src/plugins/dashboard/public/application/listing/dashboard_no_match.tsx @@ -45,7 +45,8 @@ export const DashboardNoMatch = ({ history }: { history: RouteComponentProps['hi }} />

- + , + { theme$: services.core.theme.theme$ } ) ); diff --git a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx index 8e24e9ea595dc..bc5bb3aa4a566 100644 --- a/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx +++ b/src/plugins/dashboard/public/application/top_nav/dashboard_top_nav.tsx @@ -110,7 +110,9 @@ export function DashboardTopNav({ } = useKibana().services; const { version: kibanaVersion } = initializerContext.env.packageInfo; const timefilter = data.query.timefilter.timefilter; - const toasts = core.notifications.toasts; + const { notifications, theme } = core; + const { toasts } = notifications; + const { theme$ } = theme; const dispatchDashboardStateChange = useDashboardDispatch(); const dashboardState = useDashboardSelector((state) => state.dashboardStateReducer); @@ -367,7 +369,7 @@ export function DashboardTopNav({ }); return saveResult.id ? { id: saveResult.id } : { error: saveResult.error }; }; - showCloneModal(onClone, currentState.title); + showCloneModal({ onClone, title: currentState.title, theme$ }); }, [ dashboardSessionStorage, savedObjectsTagging, @@ -375,6 +377,7 @@ export function DashboardTopNav({ kibanaVersion, redirectTo, timefilter, + theme$, toasts, ]); @@ -395,9 +398,10 @@ export function DashboardTopNav({ onHidePanelTitlesChange: (isChecked: boolean) => { dispatchDashboardStateChange(setHidePanelTitles(isChecked)); }, + theme$, }); }, - [dashboardAppState, dispatchDashboardStateChange] + [dashboardAppState, dispatchDashboardStateChange, theme$] ); const showShare = useCallback( diff --git a/src/plugins/dashboard/public/application/top_nav/show_clone_modal.tsx b/src/plugins/dashboard/public/application/top_nav/show_clone_modal.tsx index 66803d0d7741e..5c7ec042bf1d9 100644 --- a/src/plugins/dashboard/public/application/top_nav/show_clone_modal.tsx +++ b/src/plugins/dashboard/public/application/top_nav/show_clone_modal.tsx @@ -10,16 +10,21 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { i18n } from '@kbn/i18n'; import { I18nProvider } from '@kbn/i18n-react'; +import { CoreStart } from 'src/core/public'; import { DashboardCloneModal } from './clone_modal'; +import { KibanaThemeProvider } from '../../services/kibana_react'; -export function showCloneModal( +export interface ShowCloneModalProps { onClone: ( newTitle: string, isTitleDuplicateConfirmed: boolean, onTitleDuplicate: () => void - ) => Promise<{ id?: string } | { error: Error }>, - title: string -) { + ) => Promise<{ id?: string } | { error: Error }>; + title: string; + theme$: CoreStart['theme']['theme$']; +} + +export function showCloneModal({ onClone, title, theme$ }: ShowCloneModalProps) { const container = document.createElement('div'); const closeModal = () => { ReactDOM.unmountComponentAtNode(container); @@ -44,14 +49,16 @@ export function showCloneModal( document.body.appendChild(container); const element = ( - + + + ); ReactDOM.render(element, container); diff --git a/src/plugins/dashboard/public/application/top_nav/show_options_popover.tsx b/src/plugins/dashboard/public/application/top_nav/show_options_popover.tsx index c9e10f83ff7ef..c53103075dcfb 100644 --- a/src/plugins/dashboard/public/application/top_nav/show_options_popover.tsx +++ b/src/plugins/dashboard/public/application/top_nav/show_options_popover.tsx @@ -10,8 +10,9 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { I18nProvider } from '@kbn/i18n-react'; import { EuiWrappingPopover } from '@elastic/eui'; - +import { CoreStart } from 'src/core/public'; import { OptionsMenu } from './options'; +import { KibanaThemeProvider } from '../../services/kibana_react'; let isOpen = false; @@ -22,6 +23,17 @@ const onClose = () => { isOpen = false; }; +export interface ShowOptionsPopoverProps { + anchorElement: HTMLElement; + useMargins: boolean; + onUseMarginsChange: (useMargins: boolean) => void; + syncColors: boolean; + onSyncColorsChange: (syncColors: boolean) => void; + hidePanelTitles: boolean; + onHidePanelTitlesChange: (hideTitles: boolean) => void; + theme$: CoreStart['theme']['theme$']; +} + export function showOptionsPopover({ anchorElement, useMargins, @@ -30,15 +42,8 @@ export function showOptionsPopover({ onHidePanelTitlesChange, syncColors, onSyncColorsChange, -}: { - anchorElement: HTMLElement; - useMargins: boolean; - onUseMarginsChange: (useMargins: boolean) => void; - syncColors: boolean; - onSyncColorsChange: (syncColors: boolean) => void; - hidePanelTitles: boolean; - onHidePanelTitlesChange: (hideTitles: boolean) => void; -}) { + theme$, +}: ShowOptionsPopoverProps) { if (isOpen) { onClose(); return; @@ -49,16 +54,23 @@ export function showOptionsPopover({ document.body.appendChild(container); const element = ( - - - + + + + + ); ReactDOM.render(element, container); diff --git a/src/plugins/dashboard/public/plugin.tsx b/src/plugins/dashboard/public/plugin.tsx index 9912aef943144..7f784d43c0cb7 100644 --- a/src/plugins/dashboard/public/plugin.tsx +++ b/src/plugins/dashboard/public/plugin.tsx @@ -193,6 +193,11 @@ export class DashboardPlugin ); } + const getPlaceholderEmbeddableStartServices = async () => { + const [coreStart] = await core.getStartServices(); + return { theme: coreStart.theme }; + }; + const getStartServices = async () => { const [coreStart, deps] = await core.getStartServices(); @@ -203,13 +208,14 @@ export class DashboardPlugin SavedObjectFinder: getSavedObjectFinder(coreStart.savedObjects, coreStart.uiSettings), showWriteControls: Boolean(coreStart.application.capabilities.dashboard.showWriteControls), notifications: coreStart.notifications, + screenshotMode: deps.screenshotMode, application: coreStart.application, uiSettings: coreStart.uiSettings, overlays: coreStart.overlays, embeddable: deps.embeddable, uiActions: deps.uiActions, inspector: deps.inspector, - screenshotMode: deps.screenshotMode, + theme: coreStart.theme, http: coreStart.http, ExitFullScreenButton, presentationUtil: deps.presentationUtil, @@ -279,10 +285,12 @@ export class DashboardPlugin dashboardContainerFactory.type, dashboardContainerFactory ); - }); - const placeholderFactory = new PlaceholderEmbeddableFactory(); - embeddable.registerEmbeddableFactory(placeholderFactory.type, placeholderFactory); + const placeholderFactory = new PlaceholderEmbeddableFactory( + getPlaceholderEmbeddableStartServices + ); + embeddable.registerEmbeddableFactory(placeholderFactory.type, placeholderFactory); + }); this.stopUrlTracking = () => { stopUrlTracker(); @@ -364,7 +372,7 @@ export class DashboardPlugin } public start(core: CoreStart, plugins: DashboardStartDependencies): DashboardStart { - const { notifications, overlays, application } = core; + const { notifications, overlays, application, theme } = core; const { uiActions, data, share, presentationUtil, embeddable } = plugins; const dashboardCapabilities: Readonly = application.capabilities @@ -406,11 +414,15 @@ export class DashboardPlugin uiActions.registerAction(unlinkFromLibraryAction); uiActions.attachAction(CONTEXT_MENU_TRIGGER, unlinkFromLibraryAction.id); - const libraryNotificationAction = new LibraryNotificationAction(unlinkFromLibraryAction); + const libraryNotificationAction = new LibraryNotificationAction( + theme, + unlinkFromLibraryAction + ); uiActions.registerAction(libraryNotificationAction); uiActions.attachAction(PANEL_NOTIFICATION_TRIGGER, libraryNotificationAction.id); const copyToDashboardAction = new CopyToDashboardAction( + theme, overlays, embeddable.getStateTransfer(), { diff --git a/src/plugins/dashboard/public/services/kibana_react.ts b/src/plugins/dashboard/public/services/kibana_react.ts index 4d5a3a5b57657..8cab64065824d 100644 --- a/src/plugins/dashboard/public/services/kibana_react.ts +++ b/src/plugins/dashboard/public/services/kibana_react.ts @@ -20,4 +20,5 @@ export { reactToUiComponent, ExitFullScreenButton, KibanaContextProvider, + KibanaThemeProvider, } from '../../../kibana_react/public'; diff --git a/src/plugins/expression_image/kibana.json b/src/plugins/expression_image/kibana.json index 4f4b736d82d1a..7391b17bce779 100755 --- a/src/plugins/expression_image/kibana.json +++ b/src/plugins/expression_image/kibana.json @@ -10,5 +10,6 @@ "server": true, "ui": true, "requiredPlugins": ["expressions", "presentationUtil"], - "optionalPlugins": [] + "optionalPlugins": [], + "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/expression_image/public/expression_renderers/__stories__/image_renderer.stories.tsx b/src/plugins/expression_image/public/expression_renderers/__stories__/image_renderer.stories.tsx index d75aa1a4263eb..dc54194d5d83f 100644 --- a/src/plugins/expression_image/public/expression_renderers/__stories__/image_renderer.stories.tsx +++ b/src/plugins/expression_image/public/expression_renderers/__stories__/image_renderer.stories.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { Render, waitFor } from '../../../../presentation_util/public/__stories__'; -import { imageRenderer } from '../image_renderer'; +import { getImageRenderer } from '../image_renderer'; import { getElasticLogo } from '../../../../../../src/plugins/presentation_util/common/lib'; import { ImageMode } from '../../../common'; @@ -19,7 +19,7 @@ const Renderer = ({ elasticLogo }: { elasticLogo: string }) => { mode: ImageMode.COVER, }; - return ; + return ; }; storiesOf('renderers/image', module).add( diff --git a/src/plugins/expression_image/public/expression_renderers/image_renderer.tsx b/src/plugins/expression_image/public/expression_renderers/image_renderer.tsx index 3d542a9978a83..a38649f13fb32 100644 --- a/src/plugins/expression_image/public/expression_renderers/image_renderer.tsx +++ b/src/plugins/expression_image/public/expression_renderers/image_renderer.tsx @@ -9,7 +9,11 @@ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; import { ExpressionRenderDefinition, IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { i18n } from '@kbn/i18n'; -import { getElasticLogo, isValidUrl } from '../../../presentation_util/public'; +import { Observable } from 'rxjs'; +import { CoreTheme } from 'kibana/public'; +import { CoreSetup } from '../../../../core/public'; +import { KibanaThemeProvider } from '../../../kibana_react/public'; +import { getElasticLogo, isValidUrl, defaultTheme$ } from '../../../presentation_util/public'; import { ImageRendererConfig } from '../../common/types'; const strings = { @@ -23,31 +27,41 @@ const strings = { }), }; -export const imageRenderer = (): ExpressionRenderDefinition => ({ - name: 'image', - displayName: strings.getDisplayName(), - help: strings.getHelpDescription(), - reuseDomNode: true, - render: async ( - domNode: HTMLElement, - config: ImageRendererConfig, - handlers: IInterpreterRenderHandlers - ) => { - const { elasticLogo } = await getElasticLogo(); - const dataurl = isValidUrl(config.dataurl ?? '') ? config.dataurl : elasticLogo; +export const getImageRenderer = + (theme$: Observable = defaultTheme$) => + (): ExpressionRenderDefinition => ({ + name: 'image', + displayName: strings.getDisplayName(), + help: strings.getHelpDescription(), + reuseDomNode: true, + render: async ( + domNode: HTMLElement, + config: ImageRendererConfig, + handlers: IInterpreterRenderHandlers + ) => { + const { elasticLogo } = await getElasticLogo(); + const dataurl = isValidUrl(config.dataurl ?? '') ? config.dataurl : elasticLogo; - const style = { - height: '100%', - backgroundImage: `url(${dataurl})`, - backgroundRepeat: 'no-repeat', - backgroundPosition: 'center center', - backgroundSize: config.mode as string, - }; + const style = { + height: '100%', + backgroundImage: `url(${dataurl})`, + backgroundRepeat: 'no-repeat', + backgroundPosition: 'center center', + backgroundSize: config.mode as string, + }; - handlers.onDestroy(() => { - unmountComponentAtNode(domNode); - }); + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); - render(
, domNode, () => handlers.done()); - }, -}); + render( + +
+ , + domNode, + () => handlers.done() + ); + }, + }); + +export const imageRendererFactory = (core: CoreSetup) => getImageRenderer(core.theme.theme$); diff --git a/src/plugins/expression_image/public/expression_renderers/index.ts b/src/plugins/expression_image/public/expression_renderers/index.ts index 96c274f05a7a9..6b4c4b03f7922 100644 --- a/src/plugins/expression_image/public/expression_renderers/index.ts +++ b/src/plugins/expression_image/public/expression_renderers/index.ts @@ -6,8 +6,4 @@ * Side Public License, v 1. */ -import { imageRenderer } from './image_renderer'; - -export const renderers = [imageRenderer]; - -export { imageRenderer }; +export { imageRendererFactory, getImageRenderer } from './image_renderer'; diff --git a/src/plugins/expression_image/public/index.ts b/src/plugins/expression_image/public/index.ts index 661a12e7cf028..c379dd05dc221 100755 --- a/src/plugins/expression_image/public/index.ts +++ b/src/plugins/expression_image/public/index.ts @@ -6,9 +6,6 @@ * Side Public License, v 1. */ -// TODO: https://github.com/elastic/kibana/issues/110893 -/* eslint-disable @kbn/eslint/no_export_all */ - import { ExpressionImagePlugin } from './plugin'; export type { ExpressionImagePluginSetup, ExpressionImagePluginStart } from './plugin'; @@ -17,4 +14,4 @@ export function plugin() { return new ExpressionImagePlugin(); } -export * from './expression_renderers'; +export { imageRendererFactory, getImageRenderer } from './expression_renderers'; diff --git a/src/plugins/expression_image/public/plugin.ts b/src/plugins/expression_image/public/plugin.ts index 6e6c02248642f..ba7e2baded8d8 100755 --- a/src/plugins/expression_image/public/plugin.ts +++ b/src/plugins/expression_image/public/plugin.ts @@ -8,7 +8,7 @@ import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { ExpressionsStart, ExpressionsSetup } from '../../expressions/public'; -import { imageRenderer } from './expression_renderers'; +import { imageRendererFactory } from './expression_renderers'; import { imageFunction } from '../common/expression_functions'; interface SetupDeps { @@ -27,7 +27,7 @@ export class ExpressionImagePlugin { public setup(core: CoreSetup, { expressions }: SetupDeps): ExpressionImagePluginSetup { expressions.registerFunction(imageFunction); - expressions.registerRenderer(imageRenderer); + expressions.registerRenderer(imageRendererFactory(core)); } public start(core: CoreStart): ExpressionImagePluginStart {} diff --git a/src/plugins/expression_shape/common/index.ts b/src/plugins/expression_shape/common/index.ts index 6019cda7a51bd..2a889e6de1bb3 100755 --- a/src/plugins/expression_shape/common/index.ts +++ b/src/plugins/expression_shape/common/index.ts @@ -6,10 +6,31 @@ * Side Public License, v 1. */ -// TODO: https://github.com/elastic/kibana/issues/110893 -/* eslint-disable @kbn/eslint/no_export_all */ +export { + PLUGIN_ID, + PLUGIN_NAME, + SVG, + CSS, + FONT_FAMILY, + FONT_WEIGHT, + BOOLEAN_TRUE, + BOOLEAN_FALSE, +} from './constants'; -export * from './constants'; -export * from './types'; +export type { + Output, + ExpressionShapeFunction, + ProgressArguments, + ProgressOutput, + ExpressionProgressFunction, + OriginString, + ShapeRendererConfig, + NodeDimensions, + ParentNodeParams, + ViewBoxParams, + ProgressRendererConfig, +} from './types'; + +export { Progress, Shape } from './types'; export { getAvailableShapes, getAvailableProgressShapes } from './lib/available_shapes'; diff --git a/src/plugins/expression_shape/common/types/index.ts b/src/plugins/expression_shape/common/types/index.ts index ec934e7affe88..ef45082ac2d96 100644 --- a/src/plugins/expression_shape/common/types/index.ts +++ b/src/plugins/expression_shape/common/types/index.ts @@ -5,5 +5,21 @@ * in compliance with, at your election, the Elastic License 2.0 or the Server * Side Public License, v 1. */ -export * from './expression_functions'; -export * from './expression_renderers'; + +export type { + Output, + ExpressionShapeFunction, + ProgressArguments, + ProgressOutput, + ExpressionProgressFunction, +} from './expression_functions'; +export { Progress, Shape } from './expression_functions'; + +export type { + OriginString, + ShapeRendererConfig, + NodeDimensions, + ParentNodeParams, + ViewBoxParams, + ProgressRendererConfig, +} from './expression_renderers'; diff --git a/src/plugins/expression_shape/kibana.json b/src/plugins/expression_shape/kibana.json index adf95689e271b..5d831f8e98f60 100755 --- a/src/plugins/expression_shape/kibana.json +++ b/src/plugins/expression_shape/kibana.json @@ -12,5 +12,5 @@ "extraPublicDirs": ["common"], "requiredPlugins": ["expressions", "presentationUtil"], "optionalPlugins": [], - "requiredBundles": [] + "requiredBundles": ["kibanaReact"] } diff --git a/src/plugins/expression_shape/public/expression_renderers/__stories__/progress_renderer.stories.tsx b/src/plugins/expression_shape/public/expression_renderers/__stories__/progress_renderer.stories.tsx index dcf2daaafcfc1..862718f775c5e 100644 --- a/src/plugins/expression_shape/public/expression_renderers/__stories__/progress_renderer.stories.tsx +++ b/src/plugins/expression_shape/public/expression_renderers/__stories__/progress_renderer.stories.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { Render } from '../../../../presentation_util/public/__stories__'; -import { progressRenderer } from '../progress_renderer'; +import { getProgressRenderer } from '../progress_renderer'; import { Progress } from '../../../common'; storiesOf('renderers/progress', module).add('default', () => { @@ -29,5 +29,5 @@ storiesOf('renderers/progress', module).add('default', () => { valueWeight: 15, }; - return ; + return ; }); diff --git a/src/plugins/expression_shape/public/expression_renderers/__stories__/shape_renderer.stories.tsx b/src/plugins/expression_shape/public/expression_renderers/__stories__/shape_renderer.stories.tsx index 10ac3df88e81c..d7098e8378c60 100644 --- a/src/plugins/expression_shape/public/expression_renderers/__stories__/shape_renderer.stories.tsx +++ b/src/plugins/expression_shape/public/expression_renderers/__stories__/shape_renderer.stories.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; -import { shapeRenderer as shape } from '../'; +import { getShapeRenderer } from '../'; import { Render } from '../../../../presentation_util/public/__stories__'; import { Shape } from '../../../common/types'; @@ -22,5 +22,5 @@ storiesOf('renderers/shape', module).add('default', () => { maintainAspect: true, }; - return ; + return ; }); diff --git a/src/plugins/expression_shape/public/expression_renderers/index.ts b/src/plugins/expression_shape/public/expression_renderers/index.ts index fc031c4a03c8a..59d98e7bd6f8f 100644 --- a/src/plugins/expression_shape/public/expression_renderers/index.ts +++ b/src/plugins/expression_shape/public/expression_renderers/index.ts @@ -6,9 +6,5 @@ * Side Public License, v 1. */ -import { shapeRenderer } from './shape_renderer'; -import { progressRenderer } from './progress_renderer'; - -export const renderers = [shapeRenderer, progressRenderer]; - -export { shapeRenderer, progressRenderer }; +export { getShapeRenderer, shapeRendererFactory } from './shape_renderer'; +export { getProgressRenderer, progressRendererFactory } from './progress_renderer'; diff --git a/src/plugins/expression_shape/public/expression_renderers/progress_renderer.tsx b/src/plugins/expression_shape/public/expression_renderers/progress_renderer.tsx index 5f81ffcffd3d9..b618d24d26fb0 100644 --- a/src/plugins/expression_shape/public/expression_renderers/progress_renderer.tsx +++ b/src/plugins/expression_shape/public/expression_renderers/progress_renderer.tsx @@ -7,11 +7,16 @@ */ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { Observable } from 'rxjs'; +import { CoreTheme } from 'kibana/public'; import { ExpressionRenderDefinition, IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { i18n } from '@kbn/i18n'; +import { I18nProvider } from '@kbn/i18n-react'; +import { KibanaThemeProvider } from '../../../kibana_react/public'; +import { CoreSetup } from '../../../../core/public'; import { ProgressRendererConfig } from '../../common/types'; import { LazyProgressComponent } from '../components/progress'; -import { withSuspense } from '../../../presentation_util/public'; +import { withSuspense, defaultTheme$ } from '../../../presentation_util/public'; const ProgressComponent = withSuspense(LazyProgressComponent); @@ -26,23 +31,31 @@ const strings = { }), }; -export const progressRenderer = (): ExpressionRenderDefinition => ({ - name: 'progress', - displayName: strings.getDisplayName(), - help: strings.getHelpDescription(), - reuseDomNode: true, - render: async ( - domNode: HTMLElement, - config: ProgressRendererConfig, - handlers: IInterpreterRenderHandlers - ) => { - handlers.onDestroy(() => { - unmountComponentAtNode(domNode); - }); +export const getProgressRenderer = + (theme$: Observable = defaultTheme$) => + (): ExpressionRenderDefinition => ({ + name: 'progress', + displayName: strings.getDisplayName(), + help: strings.getHelpDescription(), + reuseDomNode: true, + render: async ( + domNode: HTMLElement, + config: ProgressRendererConfig, + handlers: IInterpreterRenderHandlers + ) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); - render( - , - domNode - ); - }, -}); + render( + + + + + , + domNode + ); + }, + }); + +export const progressRendererFactory = (core: CoreSetup) => getProgressRenderer(core.theme.theme$); diff --git a/src/plugins/expression_shape/public/expression_renderers/shape_renderer.tsx b/src/plugins/expression_shape/public/expression_renderers/shape_renderer.tsx index d6fc7c4d27107..fb2a32884d03b 100644 --- a/src/plugins/expression_shape/public/expression_renderers/shape_renderer.tsx +++ b/src/plugins/expression_shape/public/expression_renderers/shape_renderer.tsx @@ -7,10 +7,14 @@ */ import React from 'react'; import { render, unmountComponentAtNode } from 'react-dom'; +import { Observable } from 'rxjs'; +import { CoreTheme } from 'kibana/public'; import { I18nProvider } from '@kbn/i18n-react'; import { ExpressionRenderDefinition, IInterpreterRenderHandlers } from 'src/plugins/expressions'; import { i18n } from '@kbn/i18n'; -import { withSuspense } from '../../../presentation_util/public'; +import { CoreSetup } from '../../../../core/public'; +import { KibanaThemeProvider } from '../../../kibana_react/public'; +import { withSuspense, defaultTheme$ } from '../../../presentation_util/public'; import { ShapeRendererConfig } from '../../common/types'; import { LazyShapeComponent } from '../components/shape'; @@ -27,25 +31,31 @@ const strings = { const ShapeComponent = withSuspense(LazyShapeComponent); -export const shapeRenderer = (): ExpressionRenderDefinition => ({ - name: 'shape', - displayName: strings.getDisplayName(), - help: strings.getHelpDescription(), - reuseDomNode: true, - render: async ( - domNode: HTMLElement, - config: ShapeRendererConfig, - handlers: IInterpreterRenderHandlers - ) => { - handlers.onDestroy(() => { - unmountComponentAtNode(domNode); - }); +export const getShapeRenderer = + (theme$: Observable = defaultTheme$) => + (): ExpressionRenderDefinition => ({ + name: 'shape', + displayName: strings.getDisplayName(), + help: strings.getHelpDescription(), + reuseDomNode: true, + render: async ( + domNode: HTMLElement, + config: ShapeRendererConfig, + handlers: IInterpreterRenderHandlers + ) => { + handlers.onDestroy(() => { + unmountComponentAtNode(domNode); + }); - render( - - - , - domNode - ); - }, -}); + render( + + + + + , + domNode + ); + }, + }); + +export const shapeRendererFactory = (core: CoreSetup) => getShapeRenderer(core.theme.theme$); diff --git a/src/plugins/expression_shape/public/index.ts b/src/plugins/expression_shape/public/index.ts index 21276d3fb4df9..be260c4c8c80b 100755 --- a/src/plugins/expression_shape/public/index.ts +++ b/src/plugins/expression_shape/public/index.ts @@ -6,9 +6,6 @@ * Side Public License, v 1. */ -// TODO: https://github.com/elastic/kibana/issues/110893 -/* eslint-disable @kbn/eslint/no_export_all */ - import { ExpressionShapePlugin } from './plugin'; export type { ExpressionShapePluginSetup, ExpressionShapePluginStart } from './plugin'; @@ -17,10 +14,50 @@ export function plugin() { return new ExpressionShapePlugin(); } -export * from './expression_renderers'; +export { + getShapeRenderer, + shapeRendererFactory, + getProgressRenderer, + progressRendererFactory, +} from './expression_renderers'; + export { LazyShapeDrawer } from './components/shape'; export { LazyProgressDrawer } from './components/progress'; export { getDefaultShapeData } from './components/reusable'; -export * from './components/shape/types'; -export * from './components/reusable/types'; -export * from '../common/types'; + +export type { + ShapeProps, + ShapeAttributes, + ShapeContentAttributes, + SvgConfig, + SvgTextAttributes, + CircleParams, + RectParams, + PathParams, + PolygonParams, + SpecificShapeContentAttributes, + ShapeDrawerProps, + ShapeDrawerComponentProps, + ShapeRef, + ShapeType, +} from './components/reusable/types'; + +export { SvgElementTypes } from './components/reusable/types'; + +export type { + Output, + ExpressionShapeFunction, + ProgressArguments, + ProgressOutput, + ExpressionProgressFunction, + OriginString, + ShapeRendererConfig, + NodeDimensions, + ParentNodeParams, + ViewBoxParams, + ProgressRendererConfig, +} from '../common/types'; + +export { Progress, Shape } from '../common/types'; + +export type { ShapeComponentProps, Dimensions } from './components/shape/types'; diff --git a/src/plugins/expression_shape/public/plugin.ts b/src/plugins/expression_shape/public/plugin.ts index 9403bce0af728..5728b92e97f94 100755 --- a/src/plugins/expression_shape/public/plugin.ts +++ b/src/plugins/expression_shape/public/plugin.ts @@ -8,7 +8,7 @@ import { CoreSetup, CoreStart, Plugin } from '../../../core/public'; import { ExpressionsStart, ExpressionsSetup } from '../../expressions/public'; -import { shapeRenderer, progressRenderer } from './expression_renderers'; +import { shapeRendererFactory, progressRendererFactory } from './expression_renderers'; import { shapeFunction, progressFunction } from '../common/expression_functions'; interface SetupDeps { @@ -28,8 +28,8 @@ export class ExpressionShapePlugin public setup(core: CoreSetup, { expressions }: SetupDeps): ExpressionShapePluginSetup { expressions.registerFunction(shapeFunction); expressions.registerFunction(progressFunction); - expressions.registerRenderer(shapeRenderer); - expressions.registerRenderer(progressRenderer); + expressions.registerRenderer(shapeRendererFactory(core)); + expressions.registerRenderer(progressRendererFactory(core)); } public start(core: CoreStart): ExpressionShapePluginStart {} diff --git a/src/plugins/presentation_util/public/components/controls/control_group/component/control_frame_component.tsx b/src/plugins/presentation_util/public/components/controls/control_group/component/control_frame_component.tsx index f94d2f8fee0dc..bdc3b2978f888 100644 --- a/src/plugins/presentation_util/public/components/controls/control_group/component/control_frame_component.tsx +++ b/src/plugins/presentation_util/public/components/controls/control_group/component/control_frame_component.tsx @@ -70,6 +70,7 @@ export const ControlFrame = ({ customPrepend, enableActions, embeddableId }: Con openConfirm(ControlGroupStrings.management.deleteControls.getSubtitle(), { @@ -131,6 +132,7 @@ export const ControlFrame = ({ customPrepend, enableActions, embeddableId }: Con <> {embeddable && enableActions && floatingActions} { direction="row" responsive={false} alignItems="center" + data-test-subj="controls-group" + data-shared-items-count={idsInOrder.length} > { aria-label={ControlGroupStrings.management.getManageButtonTitle()} iconType="gear" color="text" - data-test-subj="inputControlsSortingButton" + data-test-subj="controls-sorting-button" onClick={() => { const flyoutInstance = openFlyout( forwardAllContext( @@ -198,7 +200,7 @@ export const ControlGroup = () => { ) : ( <> - +

{ControlGroupStrings.emptyState.getCallToAction()}

diff --git a/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_sortable_item.tsx b/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_sortable_item.tsx index 0ef9c4b7f115a..f4c28e840556a 100644 --- a/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_sortable_item.tsx +++ b/src/plugins/presentation_util/public/components/controls/control_group/component/control_group_sortable_item.tsx @@ -76,6 +76,9 @@ const SortableControlInner = forwardRef< return ( - + {ControlTypeEditor && ( @@ -105,6 +105,7 @@ export const ControlEditor = ({ )} { @@ -147,6 +148,7 @@ export const ControlEditor = ({ { onCancel(); @@ -158,6 +160,7 @@ export const ControlEditor = ({ { if (getControlTypes().length > 1) { setIsControlTypePopoverOpen(!isControlTypePopoverOpen); @@ -132,15 +125,17 @@ export const CreateControlButton = ({ isIconButton }: { isIconButton: boolean }) createNewControl(getControlTypes()[0]); }; + const commonButtonProps = { + onClick: onCreateButtonClick, + color: 'primary' as EuiButtonIconColor, + 'data-test-subj': 'controls-create-button', + 'aria-label': ControlGroupStrings.management.getManageButtonTitle(), + }; + const createControlButton = isIconButton ? ( - + ) : ( - + {ControlGroupStrings.emptyState.getAddControlButtonTitle()} ); @@ -153,6 +148,7 @@ export const CreateControlButton = ({ isIconButton }: { isIconButton: boolean }) { setIsControlTypePopoverOpen(false); createNewControl(type); @@ -169,6 +165,7 @@ export const CreateControlButton = ({ isIconButton }: { isIconButton: boolean }) isOpen={isControlTypePopoverOpen} panelPaddingSize="none" anchorPosition="downLeft" + data-test-subj="control-type-picker" closePopover={() => setIsControlTypePopoverOpen(false)} > diff --git a/src/plugins/presentation_util/public/components/controls/control_group/editor/edit_control.tsx b/src/plugins/presentation_util/public/components/controls/control_group/editor/edit_control.tsx index 549d3c51b6e34..eb628049f7c93 100644 --- a/src/plugins/presentation_util/public/components/controls/control_group/editor/edit_control.tsx +++ b/src/plugins/presentation_util/public/components/controls/control_group/editor/edit_control.tsx @@ -132,6 +132,7 @@ export const EditControlButton = ({ embeddableId }: { embeddableId: string }) => return ( editControl()} diff --git a/src/plugins/presentation_util/public/components/controls/control_group/editor/editor_constants.ts b/src/plugins/presentation_util/public/components/controls/control_group/editor/editor_constants.ts index 812f794efc8c3..814e2a08cd931 100644 --- a/src/plugins/presentation_util/public/components/controls/control_group/editor/editor_constants.ts +++ b/src/plugins/presentation_util/public/components/controls/control_group/editor/editor_constants.ts @@ -14,18 +14,22 @@ export const DEFAULT_CONTROL_WIDTH: ControlWidth = 'auto'; export const CONTROL_WIDTH_OPTIONS = [ { id: `auto`, + 'data-test-subj': 'control-editor-width-auto', label: ControlGroupStrings.management.controlWidth.getAutoWidthTitle(), }, { id: `small`, + 'data-test-subj': 'control-editor-width-small', label: ControlGroupStrings.management.controlWidth.getSmallWidthTitle(), }, { id: `medium`, + 'data-test-subj': 'control-editor-width-medium', label: ControlGroupStrings.management.controlWidth.getMediumWidthTitle(), }, { id: `large`, + 'data-test-subj': 'control-editor-width-large', label: ControlGroupStrings.management.controlWidth.getLargeWidthTitle(), }, ]; diff --git a/src/plugins/presentation_util/public/components/controls/control_types/index.ts b/src/plugins/presentation_util/public/components/controls/control_types/index.ts new file mode 100644 index 0000000000000..141e9f9b4d55f --- /dev/null +++ b/src/plugins/presentation_util/public/components/controls/control_types/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +export * from './options_list'; diff --git a/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_component.tsx b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_component.tsx index 43026a67eb946..1c79d1ce3e9b0 100644 --- a/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_component.tsx +++ b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_component.tsx @@ -44,7 +44,9 @@ export const OptionsListComponent = ({ actions: { replaceSelection }, } = useReduxEmbeddableContext(); const dispatch = useEmbeddableDispatch(); - const { controlStyle, selectedOptions, singleSelect } = useEmbeddableSelector((state) => state); + const { controlStyle, selectedOptions, singleSelect, id } = useEmbeddableSelector( + (state) => state + ); // useStateObservable to get component state from Embeddable const { availableOptions, loading } = useStateObservable( @@ -90,6 +92,7 @@ export const OptionsListComponent = ({ 'optionsList--filterBtnSingle': controlStyle !== 'twoLine', 'optionsList--filterBtnPlaceholder': !selectedOptionsCount, })} + data-test-subj={`optionsList-control-${id}`} onClick={() => setIsPopoverOpen((openState) => !openState)} isSelected={isPopoverOpen} numActiveFilters={selectedOptionsCount} diff --git a/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_popover_component.tsx b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_popover_component.tsx index a84d0460e9299..4aae049a5d446 100644 --- a/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_popover_component.tsx +++ b/src/plugins/presentation_util/public/components/controls/control_types/options_list/options_list_popover_component.tsx @@ -63,6 +63,7 @@ export const OptionsListPopover = ({ disabled={showOnlySelected} onChange={(event) => updateSearchString(event.target.value)} value={searchString} + data-test-subj="optionsList-control-search-input" /> @@ -74,6 +75,7 @@ export const OptionsListPopover = ({ size="s" color="danger" iconType="eraser" + data-test-subj="optionsList-control-clear-all-selections" aria-label={OptionsListStrings.popover.getClearAllSelectionsButtonTitle()} onClick={() => dispatch(clearSelections({}))} /> @@ -102,11 +104,16 @@ export const OptionsListPopover = ({
-
+
{!showOnlySelected && ( <> {availableOptions?.map((availableOption, index) => ( { diff --git a/src/plugins/presentation_util/public/components/controls/index.ts b/src/plugins/presentation_util/public/components/controls/index.ts index dbea24336699d..c110bc348498d 100644 --- a/src/plugins/presentation_util/public/components/controls/index.ts +++ b/src/plugins/presentation_util/public/components/controls/index.ts @@ -7,4 +7,5 @@ */ export * from './control_group'; +export * from './control_types'; export * from './types'; diff --git a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx index 7b285944840c8..2911ae7a1e687 100644 --- a/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx +++ b/src/plugins/presentation_util/public/components/data_view_picker/data_view_picker.tsx @@ -47,6 +47,7 @@ export function DataViewPicker({ return ( setPopoverIsOpen(!isPopoverOpen)} fullWidth {...colorProp} @@ -68,7 +69,7 @@ export function DataViewPicker({ ownFocus >
- + {i18n.translate('presentationUtil.dataViewPicker.changeDataViewTitle', { defaultMessage: 'Data view', })} @@ -86,6 +87,7 @@ export function DataViewPicker({ key: id, label: title, value: id, + 'data-test-subj': `data-view-picker-${title}`, checked: id === selectedDataViewId ? 'on' : undefined, }))} onChange={(choices) => { diff --git a/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx index fd83eeb0c8895..ebfbb24e7c390 100644 --- a/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx +++ b/src/plugins/presentation_util/public/components/field_picker/field_picker.tsx @@ -88,6 +88,7 @@ export const FieldPicker = ({ return ( onSearchChange(event.currentTarget.value)} placeholder={searchPlaceholder} diff --git a/src/plugins/vis_types/xy/public/config/get_config.ts b/src/plugins/vis_types/xy/public/config/get_config.ts index bd79b915be917..76fe1b21a74d6 100644 --- a/src/plugins/vis_types/xy/public/config/get_config.ts +++ b/src/plugins/vis_types/xy/public/config/get_config.ts @@ -134,8 +134,6 @@ const shouldEnableHistogramMode = ( } return bars.every(({ valueAxis: groupId, mode }) => { - const yAxisScale = yAxes.find(({ groupId: axisGroupId }) => axisGroupId === groupId)?.scale; - - return mode === 'stacked' || yAxisScale?.mode === 'percentage'; + return mode === 'stacked'; }); }; diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap index 59a7cf966df91..56f35ae021173 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/__snapshots__/chart_options.test.tsx.snap @@ -54,7 +54,6 @@ exports[`ChartOptions component should init with the default set of props 1`] = { expect(setParamByIndex).toBeCalledWith('seriesParams', 0, paramName, ChartMode.Normal); }); - - it('should set "stacked" mode and disabled control if the referenced axis is "percentage"', () => { - defaultProps.valueAxes[0].scale.mode = AxisMode.Percentage; - defaultProps.chart.mode = ChartMode.Normal; - const paramName = 'mode'; - const comp = mount(); - - expect(setParamByIndex).toBeCalledWith('seriesParams', 0, paramName, ChartMode.Stacked); - expect(comp.find({ paramName }).prop('disabled')).toBeTruthy(); - }); }); diff --git a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/chart_options.tsx b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/chart_options.tsx index 04013969fb4fa..f1643746cd84e 100644 --- a/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/chart_options.tsx +++ b/src/plugins/vis_types/xy/public/editor/components/options/metrics_axes/chart_options.tsx @@ -6,14 +6,14 @@ * Side Public License, v 1. */ -import React, { useMemo, useCallback, useEffect, useState } from 'react'; +import React, { useMemo, useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import { SelectOption } from '../../../../../../../vis_default_editor/public'; -import { SeriesParam, ValueAxis, ChartMode, AxisMode } from '../../../../types'; +import { SeriesParam, ValueAxis } from '../../../../types'; import { LineOptions } from './line_options'; import { PointOptions } from './point_options'; import { SetParamByIndex, ChangeValueAxis } from '.'; @@ -39,7 +39,6 @@ function ChartOptions({ changeValueAxis, setParamByIndex, }: ChartOptionsParams) { - const [disabledMode, setDisabledMode] = useState(false); const setChart: SetChart = useCallback( (paramName, value) => { setParamByIndex('seriesParams', index, paramName, value); @@ -70,20 +69,6 @@ function ChartOptions({ [valueAxes] ); - useEffect(() => { - const valueAxisToMetric = valueAxes.find((valueAxis) => valueAxis.id === chart.valueAxis); - if (valueAxisToMetric) { - if (valueAxisToMetric.scale.mode === AxisMode.Percentage) { - setDisabledMode(true); - if (chart.mode !== ChartMode.Stacked) { - setChart('mode', ChartMode.Stacked); - } - } else if (disabledMode) { - setDisabledMode(false); - } - } - }, [valueAxes, chart, disabledMode, setChart, setDisabledMode]); - return ( <> diff --git a/src/plugins/vis_types/xy/public/utils/render_all_series.test.mocks.ts b/src/plugins/vis_types/xy/public/utils/render_all_series.test.mocks.ts index c14e313b1e7a4..4c51d8cad64e4 100644 --- a/src/plugins/vis_types/xy/public/utils/render_all_series.test.mocks.ts +++ b/src/plugins/vis_types/xy/public/utils/render_all_series.test.mocks.ts @@ -109,7 +109,7 @@ export const getVisConfig = (): VisConfig => { show: false, }, scale: { - mode: AxisMode.Normal, + mode: AxisMode.Percentage, type: 'linear', }, domain: { diff --git a/src/plugins/vis_types/xy/public/utils/render_all_series.test.tsx b/src/plugins/vis_types/xy/public/utils/render_all_series.test.tsx index 47b103003b3ed..6f56eff3c2a92 100644 --- a/src/plugins/vis_types/xy/public/utils/render_all_series.test.tsx +++ b/src/plugins/vis_types/xy/public/utils/render_all_series.test.tsx @@ -95,6 +95,43 @@ describe('renderAllSeries', function () { expect(wrapper.find(BarSeries).length).toBe(1); }); + it('renders percentage data for percentage mode', () => { + const barSeriesParams = [{ ...defaultSeriesParams[0], type: 'histogram', mode: 'percentage' }]; + const config = getVisConfig(); + + const renderBarSeries = renderAllSeries( + config, + barSeriesParams as SeriesParam[], + defaultData, + jest.fn(), + jest.fn(), + 'Europe/Athens', + 'col-0-2', + [] + ); + const wrapper = shallow(
{renderBarSeries}
); + expect(wrapper.find(BarSeries).length).toBe(1); + expect(wrapper.find(BarSeries).prop('stackMode')).toEqual('percentage'); + expect(wrapper.find(BarSeries).prop('data')).toEqual([ + { + 'col-0-2': 1610960220000, + 'col-1-3': 1, + }, + { + 'col-0-2': 1610961300000, + 'col-1-3': 1, + }, + { + 'col-0-2': 1610961900000, + 'col-1-3': 1, + }, + { + 'col-0-2': 1610962980000, + 'col-1-3': 1, + }, + ]); + }); + it('renders the correct yAccessors for not percentile aggs', () => { const renderSeries = getAllSeries(getVisConfig(), defaultSeriesParams, defaultData); const wrapper = shallow(
{renderSeries}
); diff --git a/src/plugins/vis_types/xy/public/utils/render_all_series.tsx b/src/plugins/vis_types/xy/public/utils/render_all_series.tsx index c248b3b86e42a..4d71cf454cfd6 100644 --- a/src/plugins/vis_types/xy/public/utils/render_all_series.tsx +++ b/src/plugins/vis_types/xy/public/utils/render_all_series.tsx @@ -19,6 +19,7 @@ import { AccessorFn, ColorVariant, LabelOverflowConstraint, + computeRatioByGroups, } from '@elastic/charts'; import { DatatableRow } from '../../../../expressions/public'; @@ -90,7 +91,24 @@ export const renderAllSeries = ( const id = `${type}-${yAccessors[0]}`; const yAxisScale = yAxes.find(({ groupId: axisGroupId }) => axisGroupId === groupId)?.scale; - const isStacked = mode === 'stacked' || yAxisScale?.mode === 'percentage'; + // compute percentage mode data + const splitChartAccessor = aspects.splitColumn?.accessor || aspects.splitRow?.accessor; + const groupAccessors = [String(xAccessor)]; + if (splitChartAccessor) { + groupAccessors.push(splitChartAccessor); + } + let computedData = data; + if (yAxisScale?.mode === 'percentage') { + yAccessors.forEach((accessor) => { + computedData = computeRatioByGroups( + computedData, + groupAccessors, + (d) => d[accessor], + accessor + ); + }); + } + const isStacked = mode === 'stacked'; const stackMode = yAxisScale?.mode === 'normal' ? undefined : yAxisScale?.mode; // needed to seperate stacked and non-stacked bars into unique pseudo groups const pseudoGroupId = isStacked ? `__pseudo_stacked_group-${groupId}__` : groupId; @@ -113,7 +131,7 @@ export const renderAllSeries = ( xAccessor={xAccessor} yAccessors={yAccessors} splitSeriesAccessors={splitSeriesAccessors} - data={data} + data={computedData} timeZone={timeZone} stackAccessors={isStacked ? ['__any_value__'] : undefined} enableHistogramMode={enableHistogramMode} @@ -153,7 +171,7 @@ export const renderAllSeries = ( markSizeAccessor={markSizeAccessor} markFormat={aspects.z?.formatter} splitSeriesAccessors={splitSeriesAccessors} - data={data} + data={computedData} stackAccessors={isStacked ? ['__any_value__'] : undefined} displayValueSettings={{ showValueLabel, diff --git a/test/functional/apps/dashboard/dashboard_controls_integration.ts b/test/functional/apps/dashboard/dashboard_controls_integration.ts new file mode 100644 index 0000000000000..789d66fab6c86 --- /dev/null +++ b/test/functional/apps/dashboard/dashboard_controls_integration.ts @@ -0,0 +1,216 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import expect from '@kbn/expect'; + +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const security = getService('security'); + const queryBar = getService('queryBar'); + const pieChart = getService('pieChart'); + const filterBar = getService('filterBar'); + const esArchiver = getService('esArchiver'); + const testSubjects = getService('testSubjects'); + const kibanaServer = getService('kibanaServer'); + const dashboardAddPanel = getService('dashboardAddPanel'); + const { dashboardControls, timePicker, common, dashboard, header } = getPageObjects([ + 'dashboardControls', + 'timePicker', + 'dashboard', + 'common', + 'header', + ]); + + describe('Dashboard controls integration', () => { + before(async () => { + await esArchiver.load('test/functional/fixtures/es_archiver/dashboard/current/kibana'); + await security.testUser.setRoles(['kibana_admin', 'test_logstash_reader', 'animals']); + await kibanaServer.uiSettings.replace({ + defaultIndex: '0bf35f60-3dc9-11e8-8660-4d65aa086b3c', + }); + await common.navigateToApp('dashboard'); + await dashboardControls.enableControlsLab(); + await common.navigateToApp('dashboard'); + await dashboard.preserveCrossAppState(); + await dashboard.gotoDashboardLandingPage(); + await dashboard.clickNewDashboard(); + await timePicker.setDefaultDataRange(); + }); + + it('shows the empty control callout on a new dashboard', async () => { + await testSubjects.existOrFail('controls-empty'); + }); + + describe('Options List Control creation and editing experience', async () => { + it('can add a new options list control from a blank state', async () => { + await dashboardControls.createOptionsListControl({ fieldName: 'machine.os.raw' }); + expect(await dashboardControls.getControlsCount()).to.be(1); + }); + + it('can add a second options list control with a non-default data view', async () => { + await dashboardControls.createOptionsListControl({ + dataViewTitle: 'animals-*', + fieldName: 'sound.keyword', + }); + expect(await dashboardControls.getControlsCount()).to.be(2); + + // data views should be properly propagated from the control group to the dashboard + expect(await filterBar.getIndexPatterns()).to.be('logstash-*,animals-*'); + }); + + it('renames an existing control', async () => { + const secondId = (await dashboardControls.getAllControlIds())[1]; + + const newTitle = 'wow! Animal sounds?'; + await dashboardControls.editExistingControl(secondId); + await dashboardControls.controlEditorSetTitle(newTitle); + await dashboardControls.controlEditorSave(); + expect(await dashboardControls.doesControlTitleExist(newTitle)).to.be(true); + }); + + it('can change the data view and field of an existing options list', async () => { + const firstId = (await dashboardControls.getAllControlIds())[0]; + await dashboardControls.editExistingControl(firstId); + + await dashboardControls.optionsListEditorSetDataView('animals-*'); + await dashboardControls.optionsListEditorSetfield('animal.keyword'); + await dashboardControls.controlEditorSave(); + + // when creating a new filter, the ability to select a data view should be removed, because the dashboard now only has one data view + await testSubjects.click('addFilter'); + await testSubjects.missingOrFail('filterIndexPatternsSelect'); + await filterBar.ensureFieldEditorModalIsClosed(); + }); + + it('deletes an existing control', async () => { + const firstId = (await dashboardControls.getAllControlIds())[0]; + + await dashboardControls.removeExistingControl(firstId); + expect(await dashboardControls.getControlsCount()).to.be(1); + }); + + after(async () => { + const controlIds = await dashboardControls.getAllControlIds(); + for (const controlId of controlIds) { + await dashboardControls.removeExistingControl(controlId); + } + }); + }); + + describe('Interact with options list on dashboard', async () => { + before(async () => { + await dashboardAddPanel.addVisualization('Rendering-Test:-animal-sounds-pie'); + + await dashboardControls.createOptionsListControl({ + dataViewTitle: 'animals-*', + fieldName: 'sound.keyword', + title: 'Animal Sounds', + }); + }); + + it('Shows available options in options list', async () => { + const controlIds = await dashboardControls.getAllControlIds(); + await dashboardControls.optionsListOpenPopover(controlIds[0]); + await retry.try(async () => { + expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(8); + }); + await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]); + }); + + it('Can search options list for available options', async () => { + const controlIds = await dashboardControls.getAllControlIds(); + await dashboardControls.optionsListOpenPopover(controlIds[0]); + await dashboardControls.optionsListPopoverSearchForOption('meo'); + await retry.try(async () => { + expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(1); + expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql(['meow']); + }); + await dashboardControls.optionsListPopoverClearSearch(); + await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]); + }); + + it('Applies dashboard query to options list control', async () => { + await queryBar.setQuery('isDog : true '); + await queryBar.submitQuery(); + await dashboard.waitForRenderComplete(); + await header.waitUntilLoadingHasFinished(); + + const controlIds = await dashboardControls.getAllControlIds(); + await dashboardControls.optionsListOpenPopover(controlIds[0]); + await retry.try(async () => { + expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(5); + expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([ + 'ruff', + 'bark', + 'grrr', + 'bow ow ow', + 'grr', + ]); + }); + + await queryBar.setQuery(''); + await queryBar.submitQuery(); + }); + + it('Applies dashboard filters to options list control', async () => { + await filterBar.addFilter('sound.keyword', 'is one of', ['bark', 'bow ow ow', 'ruff']); + await dashboard.waitForRenderComplete(); + await header.waitUntilLoadingHasFinished(); + + const controlIds = await dashboardControls.getAllControlIds(); + await dashboardControls.optionsListOpenPopover(controlIds[0]); + await retry.try(async () => { + expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(3); + expect(await dashboardControls.optionsListPopoverGetAvailableOptions()).to.eql([ + 'ruff', + 'bark', + 'bow ow ow', + ]); + }); + + await filterBar.removeAllFilters(); + }); + + it('Can select multiple available options', async () => { + const controlIds = await dashboardControls.getAllControlIds(); + await dashboardControls.optionsListOpenPopover(controlIds[0]); + await dashboardControls.optionsListPopoverSelectOption('hiss'); + await dashboardControls.optionsListPopoverSelectOption('grr'); + await dashboardControls.optionsListEnsurePopoverIsClosed(controlIds[0]); + }); + + it('Selected options appear in control', async () => { + const controlIds = await dashboardControls.getAllControlIds(); + const selectionString = await dashboardControls.optionsListGetSelectionsString( + controlIds[0] + ); + expect(selectionString).to.be('hiss, grr'); + }); + + it('Applies options list control options to dashboard', async () => { + expect(await pieChart.getPieSliceCount()).to.be(2); + }); + + it('Applies options list control options to dashboard by default on open', async () => { + await dashboard.gotoDashboardLandingPage(); + await header.waitUntilLoadingHasFinished(); + await dashboard.clickUnsavedChangesContinueEditing('New Dashboard'); + await header.waitUntilLoadingHasFinished(); + expect(await pieChart.getPieSliceCount()).to.be(2); + + const controlIds = await dashboardControls.getAllControlIds(); + const selectionString = await dashboardControls.optionsListGetSelectionsString( + controlIds[0] + ); + expect(selectionString).to.be('hiss, grr'); + }); + }); + }); +} diff --git a/test/functional/apps/dashboard/dashboard_filtering.ts b/test/functional/apps/dashboard/dashboard_filtering.ts index 73a53281df16d..796e8e35f0d49 100644 --- a/test/functional/apps/dashboard/dashboard_filtering.ts +++ b/test/functional/apps/dashboard/dashboard_filtering.ts @@ -67,7 +67,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { await security.testUser.restoreDefaults(); }); - describe('adding a filter that excludes all data', () => { + // FLAKY: https://github.com/elastic/kibana/issues/120195 + describe.skip('adding a filter that excludes all data', () => { before(async () => { await populateDashboard(); await addFilterAndRefresh(); diff --git a/test/functional/apps/dashboard/index.ts b/test/functional/apps/dashboard/index.ts index c9a62447f223a..73a8754982e4f 100644 --- a/test/functional/apps/dashboard/index.ts +++ b/test/functional/apps/dashboard/index.ts @@ -72,6 +72,7 @@ export default function ({ getService, loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./full_screen_mode')); loadTestFile(require.resolve('./dashboard_filter_bar')); loadTestFile(require.resolve('./dashboard_filtering')); + loadTestFile(require.resolve('./dashboard_controls_integration')); loadTestFile(require.resolve('./panel_expand_toggle')); loadTestFile(require.resolve('./dashboard_grid')); loadTestFile(require.resolve('./view_edit')); diff --git a/test/functional/page_objects/dashboard_page_controls.ts b/test/functional/page_objects/dashboard_page_controls.ts new file mode 100644 index 0000000000000..2603608eebee9 --- /dev/null +++ b/test/functional/page_objects/dashboard_page_controls.ts @@ -0,0 +1,254 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0 and the Server Side Public License, v 1; you may not use this file except + * in compliance with, at your election, the Elastic License 2.0 or the Server + * Side Public License, v 1. + */ + +import { WebElementWrapper } from 'test/functional/services/lib/web_element_wrapper'; +import { OPTIONS_LIST_CONTROL } from '../../../src/plugins/presentation_util/common/controls/'; +import { ControlWidth } from '../../../src/plugins/presentation_util/public/components/controls'; + +import { FtrService } from '../ftr_provider_context'; + +export class DashboardPageControls extends FtrService { + private readonly log = this.ctx.getService('log'); + private readonly find = this.ctx.getService('find'); + private readonly retry = this.ctx.getService('retry'); + private readonly common = this.ctx.getPageObject('common'); + private readonly header = this.ctx.getPageObject('header'); + private readonly settings = this.ctx.getPageObject('settings'); + private readonly testSubjects = this.ctx.getService('testSubjects'); + + /* ----------------------------------------------------------- + General controls functions + ----------------------------------------------------------- */ + + public async enableControlsLab() { + await this.header.clickStackManagement(); + await this.settings.clickKibanaSettings(); + await this.settings.toggleAdvancedSettingCheckbox('labs:dashboard:dashboardControls'); + } + + public async expectControlsEmpty() { + await this.testSubjects.existOrFail('controls-empty'); + } + + public async getAllControlIds() { + const controlFrames = await this.testSubjects.findAll('control-frame'); + const ids = await Promise.all( + controlFrames.map(async (controlFrame) => await controlFrame.getAttribute('data-control-id')) + ); + this.log.debug('Got all control ids:', ids); + return ids; + } + + public async getAllControlTitles() { + const titleObjects = await this.testSubjects.findAll('control-frame-title'); + const titles = await Promise.all( + titleObjects.map(async (title) => (await title.getVisibleText()).split('\n')[0]) + ); + this.log.debug('Got all control titles:', titles); + return titles; + } + + public async doesControlTitleExist(title: string) { + const titles = await this.getAllControlTitles(); + return Boolean(titles.find((currentTitle) => currentTitle.indexOf(title))); + } + + public async getControlsCount() { + const allTitles = await this.getAllControlTitles(); + return allTitles.length; + } + + public async openCreateControlFlyout(type: string) { + this.log.debug(`Opening flyout for ${type} control`); + await this.testSubjects.click('controls-create-button'); + if (await this.testSubjects.exists('control-type-picker')) { + await this.testSubjects.click(`create-${type}-control`); + } + await this.retry.try(async () => { + await this.testSubjects.existOrFail('control-editor-flyout'); + }); + } + + /* ----------------------------------------------------------- + Individual controls functions + ----------------------------------------------------------- */ + + // Control Frame functions + public async getControlElementById(controlId: string): Promise { + const errorText = `Control frame ${controlId} could not be found`; + let controlElement: WebElementWrapper | undefined; + await this.retry.try(async () => { + const controlFrames = await this.testSubjects.findAll('control-frame'); + const framesWithIds = await Promise.all( + controlFrames.map(async (frame) => { + const id = await frame.getAttribute('data-control-id'); + return { id, element: frame }; + }) + ); + const foundControlFrame = framesWithIds.find(({ id }) => id === controlId); + if (!foundControlFrame) throw new Error(errorText); + controlElement = foundControlFrame.element; + }); + if (!controlElement) throw new Error(errorText); + return controlElement; + } + + public async hoverOverExistingControl(controlId: string) { + const elementToHover = await this.getControlElementById(controlId); + await this.retry.try(async () => { + await elementToHover.moveMouseTo(); + await this.testSubjects.existOrFail(`control-action-${controlId}-edit`); + }); + } + + public async editExistingControl(controlId: string) { + this.log.debug(`Opening control editor for control: ${controlId}`); + await this.hoverOverExistingControl(controlId); + await this.testSubjects.click(`control-action-${controlId}-edit`); + } + + public async removeExistingControl(controlId: string) { + this.log.debug(`Removing control: ${controlId}`); + await this.hoverOverExistingControl(controlId); + await this.testSubjects.click(`control-action-${controlId}-delete`); + await this.common.clickConfirmOnModal(); + } + + // Options list functions + public async optionsListGetSelectionsString(controlId: string) { + this.log.debug(`Getting selections string for Options List: ${controlId}`); + const controlElement = await this.getControlElementById(controlId); + return (await controlElement.getVisibleText()).split('\n')[1]; + } + + public async optionsListOpenPopover(controlId: string) { + this.log.debug(`Opening popover for Options List: ${controlId}`); + await this.testSubjects.click(`optionsList-control-${controlId}`); + await this.retry.try(async () => { + await this.testSubjects.existOrFail(`optionsList-control-available-options`); + }); + } + + public async optionsListEnsurePopoverIsClosed(controlId: string) { + this.log.debug(`Opening popover for Options List: ${controlId}`); + await this.testSubjects.click(`optionsList-control-${controlId}`); + await this.testSubjects.waitForDeleted(`optionsList-control-available-options`); + } + + public async optionsListPopoverAssertOpen() { + await this.retry.try(async () => { + if (!(await this.testSubjects.exists(`optionsList-control-available-options`))) { + throw new Error('options list popover must be open before calling selectOption'); + } + }); + } + + public async optionsListPopoverGetAvailableOptionsCount() { + this.log.debug(`getting available options count from options list`); + const availableOptions = await this.testSubjects.find(`optionsList-control-available-options`); + return +(await availableOptions.getAttribute('data-option-count')); + } + + public async optionsListPopoverGetAvailableOptions() { + this.log.debug(`getting available options count from options list`); + const availableOptions = await this.testSubjects.find(`optionsList-control-available-options`); + return (await availableOptions.getVisibleText()).split('\n'); + } + + public async optionsListPopoverSearchForOption(search: string) { + this.log.debug(`searching for ${search} in options list`); + await this.optionsListPopoverAssertOpen(); + await this.testSubjects.setValue(`optionsList-control-search-input`, search); + } + + public async optionsListPopoverClearSearch() { + this.log.debug(`clearing search from options list`); + await this.optionsListPopoverAssertOpen(); + await this.find.clickByCssSelector('.euiFormControlLayoutClearButton'); + } + + public async optionsListPopoverSelectOption(availableOption: string) { + this.log.debug(`selecting ${availableOption} from options list`); + await this.optionsListPopoverAssertOpen(); + await this.testSubjects.click(`optionsList-control-selection-${availableOption}`); + } + + public async optionsListPopoverClearSelections() { + this.log.debug(`clearing all selections from options list`); + await this.optionsListPopoverAssertOpen(); + await this.testSubjects.click(`optionsList-control-clear-all-selections`); + } + + /* ----------------------------------------------------------- + Control editor flyout + ----------------------------------------------------------- */ + + // Generic control editor functions + public async controlEditorSetTitle(title: string) { + this.log.debug(`Setting control title to ${title}`); + await this.testSubjects.setValue('control-editor-title-input', title); + } + + public async controlEditorSetWidth(width: ControlWidth) { + this.log.debug(`Setting control width to ${width}`); + await this.testSubjects.click(`control-editor-width-${width}`); + } + + public async controlEditorSave() { + this.log.debug(`Saving changes in control editor`); + await this.testSubjects.click(`control-editor-save`); + } + + public async controlEditorCancel() { + this.log.debug(`Canceling changes in control editor`); + await this.testSubjects.click(`control-editor-cancel`); + } + + // Options List editor functions + public async createOptionsListControl({ + dataViewTitle, + fieldName, + width, + title, + }: { + title?: string; + fieldName: string; + width?: ControlWidth; + dataViewTitle?: string; + }) { + this.log.debug(`Creating options list control ${title ?? fieldName}`); + await this.openCreateControlFlyout(OPTIONS_LIST_CONTROL); + + if (dataViewTitle) await this.optionsListEditorSetDataView(dataViewTitle); + if (fieldName) await this.optionsListEditorSetfield(fieldName); + if (title) await this.controlEditorSetTitle(title); + if (width) await this.controlEditorSetWidth(width); + + await this.controlEditorSave(); + } + + public async optionsListEditorSetDataView(dataViewTitle: string) { + this.log.debug(`Setting options list data view to ${dataViewTitle}`); + await this.testSubjects.click('open-data-view-picker'); + await this.retry.try(async () => { + await this.testSubjects.existOrFail('data-view-picker-title'); + }); + await this.testSubjects.click(`data-view-picker-${dataViewTitle}`); + } + + public async optionsListEditorSetfield(fieldName: string, shouldSearch: boolean = false) { + this.log.debug(`Setting options list field to ${fieldName}`); + if (shouldSearch) { + await this.testSubjects.setValue('field-search-input', fieldName); + } + await this.retry.try(async () => { + await this.testSubjects.existOrFail(`field-picker-select-${fieldName}`); + }); + await this.testSubjects.click(`field-picker-select-${fieldName}`); + } +} diff --git a/test/functional/page_objects/index.ts b/test/functional/page_objects/index.ts index cda2c7de44d3b..826c4b78d1d0f 100644 --- a/test/functional/page_objects/index.ts +++ b/test/functional/page_objects/index.ts @@ -30,12 +30,14 @@ import { VegaChartPageObject } from './vega_chart_page'; import { SavedObjectsPageObject } from './management/saved_objects_page'; import { LegacyDataTableVisPageObject } from './legacy/data_table_vis'; import { IndexPatternFieldEditorPageObject } from './management/indexpattern_field_editor_page'; +import { DashboardPageControls } from './dashboard_page_controls'; export const pageObjects = { common: CommonPageObject, console: ConsolePageObject, context: ContextPageObject, dashboard: DashboardPageObject, + dashboardControls: DashboardPageControls, discover: DiscoverPageObject, error: ErrorPageObject, header: HeaderPageObject, diff --git a/x-pack/.gitignore b/x-pack/.gitignore index 9a02a9e552b40..0e0e9aba84467 100644 --- a/x-pack/.gitignore +++ b/x-pack/.gitignore @@ -6,7 +6,7 @@ /test/functional/apps/**/reports/session /test/reporting/configs/failure_debug/ /plugins/reporting/.chromium/ -/plugins/reporting/chromium/ +/plugins/screenshotting/chromium/ /plugins/reporting/.phantom/ /.aws-config.json /.env diff --git a/x-pack/.i18nrc.json b/x-pack/.i18nrc.json index b51363f1b7006..aac29086fe53d 100644 --- a/x-pack/.i18nrc.json +++ b/x-pack/.i18nrc.json @@ -46,6 +46,7 @@ "xpack.reporting": ["plugins/reporting"], "xpack.rollupJobs": ["plugins/rollup"], "xpack.runtimeFields": "plugins/runtime_fields", + "xpack.screenshotting": "plugins/screenshotting", "xpack.searchProfiler": "plugins/searchprofiler", "xpack.security": "plugins/security", "xpack.server": "legacy/server", diff --git a/x-pack/examples/reporting_example/kibana.json b/x-pack/examples/reporting_example/kibana.json index 716c6ea29c2a0..94780f1df0b36 100644 --- a/x-pack/examples/reporting_example/kibana.json +++ b/x-pack/examples/reporting_example/kibana.json @@ -10,5 +10,6 @@ }, "description": "Example integration code for applications to feature reports.", "optionalPlugins": [], - "requiredPlugins": ["reporting", "developerExamples", "navigation", "screenshotMode", "share"] + "requiredPlugins": ["reporting", "developerExamples", "navigation", "screenshotMode", "share"], + "requiredBundles": ["screenshotting"] } diff --git a/x-pack/examples/reporting_example/public/containers/main.tsx b/x-pack/examples/reporting_example/public/containers/main.tsx index c6723c9839197..5f6cd816e9db3 100644 --- a/x-pack/examples/reporting_example/public/containers/main.tsx +++ b/x-pack/examples/reporting_example/public/containers/main.tsx @@ -39,7 +39,8 @@ import type { JobParamsPDFV2, JobParamsPNGV2, } from '../../../../plugins/reporting/public'; -import { constants, ReportingStart } from '../../../../plugins/reporting/public'; +import { LayoutTypes } from '../../../../plugins/screenshotting/public'; +import { ReportingStart } from '../../../../plugins/reporting/public'; import { REPORTING_EXAMPLE_LOCATOR_ID } from '../../common'; @@ -87,7 +88,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp const getPDFJobParamsDefault = (): JobAppParamsPDF => { return { layout: { - id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT, + id: LayoutTypes.PRESERVE_LAYOUT, }, relativeUrls: ['/app/reportingExample#/intended-visualization'], objectType: 'develeloperExample', @@ -99,7 +100,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp return { version: '8.0.0', layout: { - id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT, + id: LayoutTypes.PRESERVE_LAYOUT, }, locatorParams: [ { id: REPORTING_EXAMPLE_LOCATOR_ID, version: '0.5.0', params: { myTestState: {} } }, @@ -114,7 +115,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp return { version: '8.0.0', layout: { - id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT, + id: LayoutTypes.PRESERVE_LAYOUT, }, locatorParams: { id: REPORTING_EXAMPLE_LOCATOR_ID, @@ -131,7 +132,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp return { version: '8.0.0', layout: { - id: constants.LAYOUT_TYPES.PRESERVE_LAYOUT, + id: LayoutTypes.PRESERVE_LAYOUT, }, locatorParams: { id: REPORTING_EXAMPLE_LOCATOR_ID, @@ -148,7 +149,7 @@ export const Main = ({ basename, reporting, screenshotMode }: ReportingExampleAp return { version: '8.0.0', layout: { - id: print ? constants.LAYOUT_TYPES.PRINT : constants.LAYOUT_TYPES.PRESERVE_LAYOUT, + id: print ? LayoutTypes.PRINT : LayoutTypes.PRESERVE_LAYOUT, dimensions: { // Magic numbers based on height of components not rendered on this screen :( height: 2400, diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts index 649c5c1526377..481edb07cedb9 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.test.ts @@ -2055,6 +2055,62 @@ describe('successful migrations', () => { undefined ); }); + + describe('Metrics Inventory Threshold rule', () => { + test('Migrates incorrect action group spelling', () => { + const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + + const actions = [ + { + group: 'metrics.invenotry_threshold.fired', + params: { + level: 'info', + message: + '""{{alertName}} - {{context.group}} is in a state of {{context.alertState}} Reason: {{context.reason}}""', + }, + actionRef: 'action_0', + actionTypeId: '.server-log', + }, + ]; + + const alert = getMockData({ alertTypeId: 'metrics.alert.inventory.threshold', actions }); + + expect(migration800(alert, migrationContext)).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + actions: [{ ...actions[0], group: 'metrics.inventory_threshold.fired' }], + }, + }); + }); + + test('Works with the correct action group spelling', () => { + const migration800 = getMigrations(encryptedSavedObjectsSetup, isPreconfigured)['8.0.0']; + + const actions = [ + { + group: 'metrics.inventory_threshold.fired', + params: { + level: 'info', + message: + '""{{alertName}} - {{context.group}} is in a state of {{context.alertState}} Reason: {{context.reason}}""', + }, + actionRef: 'action_0', + actionTypeId: '.server-log', + }, + ]; + + const alert = getMockData({ alertTypeId: 'metrics.alert.inventory.threshold', actions }); + + expect(migration800(alert, migrationContext)).toMatchObject({ + ...alert, + attributes: { + ...alert.attributes, + actions: [{ ...actions[0], group: 'metrics.inventory_threshold.fired' }], + }, + }); + }); + }); }); }); diff --git a/x-pack/plugins/alerting/server/saved_objects/migrations.ts b/x-pack/plugins/alerting/server/saved_objects/migrations.ts index 9dd3bac7f37a2..201c78ed2340d 100644 --- a/x-pack/plugins/alerting/server/saved_objects/migrations.ts +++ b/x-pack/plugins/alerting/server/saved_objects/migrations.ts @@ -129,7 +129,11 @@ export function getMigrations( const migrationRules800 = createEsoMigration( encryptedSavedObjects, (doc: SavedObjectUnsanitizedDoc): doc is SavedObjectUnsanitizedDoc => true, - pipeMigrations(addThreatIndicatorPathToThreatMatchRules, addRACRuleTypes) + pipeMigrations( + addThreatIndicatorPathToThreatMatchRules, + addRACRuleTypes, + fixInventoryThresholdGroupId + ) ); return { @@ -751,6 +755,42 @@ function removePreconfiguredConnectorsFromReferences( return doc; } +// This fixes an issue whereby metrics.alert.inventory.threshold rules had the +// group for actions incorrectly spelt as metrics.invenotry_threshold.fired vs metrics.inventory_threshold.fired +function fixInventoryThresholdGroupId( + doc: SavedObjectUnsanitizedDoc +): SavedObjectUnsanitizedDoc { + if (doc.attributes.alertTypeId === 'metrics.alert.inventory.threshold') { + const { + attributes: { actions }, + } = doc; + + const updatedActions = actions + ? actions.map((action) => { + // Wrong spelling + if (action.group === 'metrics.invenotry_threshold.fired') { + return { + ...action, + group: 'metrics.inventory_threshold.fired', + }; + } else { + return action; + } + }) + : []; + + return { + ...doc, + attributes: { + ...doc.attributes, + actions: updatedActions, + }, + }; + } else { + return doc; + } +} + function getCorrespondingAction( actions: SavedObjectAttribute, connectorRef: string diff --git a/x-pack/plugins/apm/common/anomaly_detection/apm_ml_job.ts b/x-pack/plugins/apm/common/anomaly_detection/apm_ml_job.ts new file mode 100644 index 0000000000000..ab630decb70c8 --- /dev/null +++ b/x-pack/plugins/apm/common/anomaly_detection/apm_ml_job.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { DATAFEED_STATE, JOB_STATE } from '../../../ml/common'; +import { Environment } from '../environment_rt'; + +export interface ApmMlJob { + environment: Environment; + version: number; + jobId: string; + jobState?: JOB_STATE; + datafeedId?: string; + datafeedState?: DATAFEED_STATE; +} diff --git a/x-pack/plugins/apm/common/anomaly_detection/get_anomaly_detection_setup_state.ts b/x-pack/plugins/apm/common/anomaly_detection/get_anomaly_detection_setup_state.ts new file mode 100644 index 0000000000000..9ca8ddbe437fe --- /dev/null +++ b/x-pack/plugins/apm/common/anomaly_detection/get_anomaly_detection_setup_state.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { FETCH_STATUS } from '../../public/hooks/use_fetcher'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import type { APIReturnType } from '../../public/services/rest/createCallApmApi'; +import { ENVIRONMENT_ALL } from '../environment_filter_values'; + +export enum AnomalyDetectionSetupState { + Loading = 'pending', + Failure = 'failure', + Unknown = 'unknown', + NoJobs = 'noJobs', + NoJobsForEnvironment = 'noJobsForEnvironment', + LegacyJobs = 'legacyJobs', + UpgradeableJobs = 'upgradeableJobs', + UpToDate = 'upToDate', +} + +export function getAnomalyDetectionSetupState({ + environment, + jobs, + fetchStatus, + isAuthorized, +}: { + environment: string; + jobs: APIReturnType<'GET /internal/apm/settings/anomaly-detection/jobs'>['jobs']; + fetchStatus: FETCH_STATUS; + isAuthorized: boolean; +}): AnomalyDetectionSetupState { + if (!isAuthorized) { + return AnomalyDetectionSetupState.Unknown; + } + + if (fetchStatus === FETCH_STATUS.LOADING) { + return AnomalyDetectionSetupState.Loading; + } + + if (fetchStatus === FETCH_STATUS.FAILURE) { + return AnomalyDetectionSetupState.Failure; + } + + if (fetchStatus !== FETCH_STATUS.SUCCESS) { + return AnomalyDetectionSetupState.Unknown; + } + + const jobsForEnvironment = + environment === ENVIRONMENT_ALL.value + ? jobs + : jobs.filter((job) => job.environment === environment); + + const hasV1Jobs = jobs.some((job) => job.version === 1); + const hasV2Jobs = jobsForEnvironment.some((job) => job.version === 2); + const hasV3Jobs = jobsForEnvironment.some((job) => job.version === 3); + const hasAnyJobs = jobs.length > 0; + + if (hasV3Jobs) { + return AnomalyDetectionSetupState.UpToDate; + } + + if (hasV2Jobs) { + return AnomalyDetectionSetupState.UpgradeableJobs; + } + + if (hasV1Jobs) { + return AnomalyDetectionSetupState.LegacyJobs; + } + + if (hasAnyJobs) { + return AnomalyDetectionSetupState.NoJobsForEnvironment; + } + + return AnomalyDetectionSetupState.NoJobs; +} diff --git a/x-pack/plugins/apm/common/environment_rt.ts b/x-pack/plugins/apm/common/environment_rt.ts index 4598ffa6f6681..67d1a6ce6fa64 100644 --- a/x-pack/plugins/apm/common/environment_rt.ts +++ b/x-pack/plugins/apm/common/environment_rt.ts @@ -11,12 +11,14 @@ import { ENVIRONMENT_NOT_DEFINED, } from './environment_filter_values'; +export const environmentStringRt = t.union([ + t.literal(ENVIRONMENT_NOT_DEFINED.value), + t.literal(ENVIRONMENT_ALL.value), + nonEmptyStringRt, +]); + export const environmentRt = t.type({ - environment: t.union([ - t.literal(ENVIRONMENT_NOT_DEFINED.value), - t.literal(ENVIRONMENT_ALL.value), - nonEmptyStringRt, - ]), + environment: environmentStringRt, }); export type Environment = t.TypeOf['environment']; diff --git a/x-pack/plugins/apm/dev_docs/testing.md b/x-pack/plugins/apm/dev_docs/testing.md index 2a7533402ecca..f6a8298ef9d0c 100644 --- a/x-pack/plugins/apm/dev_docs/testing.md +++ b/x-pack/plugins/apm/dev_docs/testing.md @@ -23,17 +23,44 @@ API tests are separated in two suites: - a basic license test suite [default] - a trial license test suite (the equivalent of gold+) +### Run tests with [--trial] license + ``` node scripts/test/api [--trial] [--help] ``` +The above command will initiate an Elasticsearch instance on http://localhost:9220 and a kibana instance on http://localhost:5620 and will run the api test against these environments. +Once the tests finish, the instances will be terminated. + +### Start test server + +``` +node scripts/test/api --server +``` +Start Elasticsearch and Kibana instances. + +### Run all tests + +``` +node scripts/test/api --runner +``` +Run all tests. The test server needs to be running, see [Start Test Server](#start-test-server). + +### Update snapshots (from Kibana root) + +To update snapshots append `--updateSnapshots` to the `functional_test_runner` command + +``` +node scripts/functional_test_runner --config x-pack/test/apm_api_integration/[basic | trial]/config.ts --quiet --updateSnapshots +``` +The test server needs to be running, see [Start Test Server](#start-test-server). + The API tests are located in [`x-pack/test/apm_api_integration/`](/x-pack/test/apm_api_integration/). **API Test tips** - For data generation in API tests have a look at the [elastic-apm-synthtrace](../../../../packages/elastic-apm-synthtrace/README.md) package - For debugging access Elasticsearch on http://localhost:9220 and Kibana on http://localhost:5620 (`elastic` / `changeme`) -- To update snapshots append `--updateSnapshots` to the functional_test_runner command --- diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx index 8e1064a71647f..7fd40cc4a1663 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/index.tsx @@ -11,19 +11,14 @@ import { ML_ERRORS } from '../../../../../common/anomaly_detection'; import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { JobsList } from './jobs_list'; import { AddEnvironments } from './add_environments'; -import { useFetcher } from '../../../../hooks/use_fetcher'; import { LicensePrompt } from '../../../shared/license_prompt'; import { useLicenseContext } from '../../../../context/license/use_license_context'; import { APIReturnType } from '../../../../services/rest/createCallApmApi'; +import { useAnomalyDetectionJobsContext } from '../../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context'; export type AnomalyDetectionApiResponse = APIReturnType<'GET /internal/apm/settings/anomaly-detection/jobs'>; -const DEFAULT_VALUE: AnomalyDetectionApiResponse = { - jobs: [], - hasLegacyJobs: false, -}; - export function AnomalyDetection() { const plugin = useApmPluginContext(); const canGetJobs = !!plugin.core.application.capabilities.ml?.canGetJobs; @@ -33,20 +28,14 @@ export function AnomalyDetection() { const [viewAddEnvironments, setViewAddEnvironments] = useState(false); const { - refetch, - data = DEFAULT_VALUE, - status, - } = useFetcher( - (callApmApi) => { - if (canGetJobs) { - return callApmApi({ - endpoint: `GET /internal/apm/settings/anomaly-detection/jobs`, - }); - } - }, - [canGetJobs], - { preservePreviousData: false, showToastOnError: false } - ); + anomalyDetectionJobsStatus, + anomalyDetectionJobsRefetch, + anomalyDetectionJobsData = { + jobs: [], + hasLegacyJobs: false, + } as AnomalyDetectionApiResponse, + anomalyDetectionSetupState, + } = useAnomalyDetectionJobsContext(); if (!hasValidLicense) { return ( @@ -71,9 +60,11 @@ export function AnomalyDetection() { <> {viewAddEnvironments ? ( environment)} + currentEnvironments={anomalyDetectionJobsData.jobs.map( + ({ environment }) => environment + )} onCreateJobSuccess={() => { - refetch(); + anomalyDetectionJobsRefetch(); setViewAddEnvironments(false); }} onCancel={() => { @@ -82,11 +73,15 @@ export function AnomalyDetection() { /> ) : ( { setViewAddEnvironments(true); }} + onUpdateComplete={() => { + anomalyDetectionJobsRefetch(); + }} /> )} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx index 2e199d1d726fb..1faab4092361d 100644 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list.tsx @@ -5,27 +5,35 @@ * 2.0. */ +import { EuiSwitch } from '@elastic/eui'; import { EuiButton, EuiFlexGroup, EuiFlexItem, + EuiIcon, EuiSpacer, EuiText, EuiTitle, + EuiToolTip, RIGHT_ALIGNMENT, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; -import React from 'react'; +import React, { useState } from 'react'; +import { MLJobsAwaitingNodeWarning } from '../../../../../../ml/public'; +import { AnomalyDetectionSetupState } from '../../../../../common/anomaly_detection/get_anomaly_detection_setup_state'; import { getEnvironmentLabel } from '../../../../../common/environment_filter_values'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; import { FETCH_STATUS } from '../../../../hooks/use_fetcher'; +import { useMlManageJobsHref } from '../../../../hooks/use_ml_manage_jobs_href'; +import { callApmApi } from '../../../../services/rest/createCallApmApi'; import { MLExplorerLink } from '../../../shared/Links/MachineLearningLinks/MLExplorerLink'; import { MLManageJobsLink } from '../../../shared/Links/MachineLearningLinks/MLManageJobsLink'; import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; import { ITableColumn, ManagedTable } from '../../../shared/managed_table'; +import { MLCallout, shouldDisplayMlCallout } from '../../../shared/ml_callout'; import { AnomalyDetectionApiResponse } from './index'; -import { LegacyJobsCallout } from './legacy_jobs_callout'; -import { MLJobsAwaitingNodeWarning } from '../../../../../../ml/public'; +import { JobsListStatus } from './jobs_list_status'; type Jobs = AnomalyDetectionApiResponse['jobs']; @@ -36,7 +44,24 @@ const columns: Array> = [ 'xpack.apm.settings.anomalyDetection.jobList.environmentColumnLabel', { defaultMessage: 'Environment' } ), - render: getEnvironmentLabel, + width: '100%', + render: (_, { environment, jobId, jobState, datafeedState, version }) => { + return ( + + + {getEnvironmentLabel(environment)} + + + + + + ); + }, }, { field: 'job_id', @@ -45,30 +70,79 @@ const columns: Array> = [ 'xpack.apm.settings.anomalyDetection.jobList.actionColumnLabel', { defaultMessage: 'Action' } ), - render: (_, { job_id: jobId }) => ( - - {i18n.translate( - 'xpack.apm.settings.anomalyDetection.jobList.mlJobLinkText', - { - defaultMessage: 'View job in ML', - } - )} - - ), + render: (_, { jobId }) => { + return ( + + + + {/* setting the key to remount the element as a workaround for https://github.com/elastic/kibana/issues/119951*/} + + + + + + + + + + + + + + ); + }, }, ]; interface Props { data: AnomalyDetectionApiResponse; + setupState: AnomalyDetectionSetupState; status: FETCH_STATUS; onAddEnvironments: () => void; + onUpdateComplete: () => void; } -export function JobsList({ data, status, onAddEnvironments }: Props) { - const { jobs, hasLegacyJobs } = data; + +export function JobsList({ + data, + status, + onAddEnvironments, + setupState, + onUpdateComplete, +}: Props) { + const { core } = useApmPluginContext(); + + const { jobs } = data; + + // default to showing legacy jobs if not up to date + const [showLegacyJobs, setShowLegacyJobs] = useState( + setupState !== AnomalyDetectionSetupState.UpToDate + ); + + const mlManageJobsHref = useMlManageJobsHref(); + + const displayMlCallout = shouldDisplayMlCallout(setupState); + + const filteredJobs = showLegacyJobs + ? jobs + : jobs.filter((job) => job.version >= 3); return ( <> - j.job_id)} /> + j.jobId)} /> + {displayMlCallout && ( + <> + { + onAddEnvironments(); + }} + onUpgradeClick={() => { + if (setupState === AnomalyDetectionSetupState.UpgradeableJobs) { + return callApmApi({ + endpoint: + 'POST /internal/apm/settings/anomaly-detection/update_to_v3', + signal: null, + }).then(() => { + core.notifications.toasts.addSuccess({ + title: i18n.translate( + 'xpack.apm.jobsList.updateCompletedToastTitle', + { + defaultMessage: 'Anomaly detection jobs created!', + } + ), + text: i18n.translate( + 'xpack.apm.jobsList.updateCompletedToastText', + { + defaultMessage: + 'Your new anomaly detection jobs have been created successfully. You will start to see anomaly detection results in the app within minutes. The old jobs have been closed but the results are still available within Machine Learning.', + } + ), + }); + onUpdateComplete(); + }); + } + }} + anomalyDetectionSetupState={setupState} + /> + + + )} - +

@@ -103,12 +215,36 @@ export function JobsList({ data, status, onAddEnvironments }: Props) {

+ + { + setShowLegacyJobs(e.target.checked); + }} + label={i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.showLegacyJobsCheckboxText', + { + defaultMessage: 'Show legacy jobs', + } + )} + /> + + + + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.manageMlJobsButtonText', + { + defaultMessage: 'Manage jobs', + } + )} + + {i18n.translate( 'xpack.apm.settings.anomalyDetection.jobList.addEnvironments', { - defaultMessage: 'Create ML Job', + defaultMessage: 'Create job', } )} @@ -120,11 +256,10 @@ export function JobsList({ data, status, onAddEnvironments }: Props) { - - {hasLegacyJobs && } ); } diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list_status.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list_status.tsx new file mode 100644 index 0000000000000..6145e9f9ca7da --- /dev/null +++ b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/jobs_list_status.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { DATAFEED_STATE, JOB_STATE } from '../../../../../../ml/common'; +import { MLManageJobsLink } from '../../../shared/Links/MachineLearningLinks/MLManageJobsLink'; + +export function JobsListStatus({ + jobId, + jobState, + datafeedState, + version, +}: { + jobId: string; + jobState?: JOB_STATE; + datafeedState?: DATAFEED_STATE; + version: number; +}) { + const jobIsOk = + jobState === JOB_STATE.OPENED || jobState === JOB_STATE.OPENING; + + const datafeedIsOk = + datafeedState === DATAFEED_STATE.STARTED || + datafeedState === DATAFEED_STATE.STARTING; + + const isClosed = + jobState === JOB_STATE.CLOSED || jobState === JOB_STATE.CLOSING; + + const isLegacy = version < 3; + + const statuses: React.ReactElement[] = []; + + if (jobIsOk && datafeedIsOk) { + statuses.push( + + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.okStatusLabel', + { defaultMessage: 'OK' } + )} + + ); + } else if (!isClosed) { + statuses.push( + + + + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.warningStatusBadgeLabel', + { defaultMessage: 'Warning' } + )} + + + + ); + } + + if (isClosed) { + statuses.push( + + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.closedStatusLabel', + { defaultMessage: 'Closed' } + )} + + ); + } + + if (isLegacy) { + statuses.push( + + {' '} + {i18n.translate( + 'xpack.apm.settings.anomalyDetection.jobList.legacyStatusLabel', + { defaultMessage: 'Legacy' } + )} + + ); + } + + return ( + + {statuses.map((status, idx) => ( + + {status} + + ))} + + ); +} diff --git a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx b/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx deleted file mode 100644 index 0d3da5c9f97ad..0000000000000 --- a/x-pack/plugins/apm/public/components/app/Settings/anomaly_detection/legacy_jobs_callout.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiCallOut, EuiButton } from '@elastic/eui'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; -import { useMlHref } from '../../../../../../ml/public'; - -export function LegacyJobsCallout() { - const { - core, - plugins: { ml }, - } = useApmPluginContext(); - const mlADLink = useMlHref(ml, core.http.basePath.get(), { - page: 'jobs', - pageState: { - jobId: 'high_mean_response_time', - }, - }); - - return ( - -

- {i18n.translate( - 'xpack.apm.settings.anomaly_detection.legacy_jobs.body', - { - defaultMessage: - 'We have discovered legacy Machine Learning jobs from our previous integration which are no longer being used in the APM app', - } - )} -

- - {i18n.translate( - 'xpack.apm.settings.anomaly_detection.legacy_jobs.button', - { defaultMessage: 'Review jobs' } - )} - -
- ); -} diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx index 6ca632eac4f2e..1994d3641ee53 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/index.tsx @@ -26,7 +26,7 @@ import { useUpgradeAssistantHref } from '../../shared/Links/kibana'; import { SearchBar } from '../../shared/search_bar'; import { getTimeRangeComparison } from '../../shared/time_comparison/get_time_range_comparison'; import { ServiceList } from './service_list'; -import { MLCallout } from './service_list/MLCallout'; +import { MLCallout, shouldDisplayMlCallout } from '../../shared/ml_callout'; const initialData = { requestId: '', @@ -159,26 +159,19 @@ function useServicesFetcher() { } export function ServiceInventory() { - const { core } = useApmPluginContext(); - const { mainStatisticsData, mainStatisticsStatus, comparisonData } = useServicesFetcher(); - const { anomalyDetectionJobsData, anomalyDetectionJobsStatus } = - useAnomalyDetectionJobsContext(); + const { anomalyDetectionSetupState } = useAnomalyDetectionJobsContext(); const [userHasDismissedCallout, setUserHasDismissedCallout] = useLocalStorage( - 'apm.userHasDismissedServiceInventoryMlCallout', + `apm.userHasDismissedServiceInventoryMlCallout.${anomalyDetectionSetupState}`, false ); - const canCreateJob = !!core.application.capabilities.ml?.canCreateJob; - const displayMlCallout = - anomalyDetectionJobsStatus === FETCH_STATUS.SUCCESS && - !anomalyDetectionJobsData?.jobs.length && - canCreateJob && - !userHasDismissedCallout; + !userHasDismissedCallout && + shouldDisplayMlCallout(anomalyDetectionSetupState); const isLoading = mainStatisticsStatus === FETCH_STATUS.LOADING; const isFailure = mainStatisticsStatus === FETCH_STATUS.FAILURE; @@ -198,10 +191,14 @@ export function ServiceInventory() { return ( <> - + {displayMlCallout && ( - setUserHasDismissedCallout(true)} /> + setUserHasDismissedCallout(true)} + /> )} diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx index 0a4adc07e1a98..bececfb545ba9 100644 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/service_inventory/service_inventory.stories.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { CoreStart } from '../../../../../../../src/core/public'; import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; +import { AnomalyDetectionSetupState } from '../../../../common/anomaly_detection/get_anomaly_detection_setup_state'; import { TimeRangeComparisonEnum } from '../../../../common/runtime_types/comparison_type_rt'; import { AnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/anomaly_detection_jobs_context'; import { ApmPluginContextValue } from '../../../context/apm_plugin/apm_plugin_context'; @@ -45,6 +46,7 @@ const stories: Meta<{}> = { anomalyDetectionJobsData: { jobs: [], hasLegacyJobs: false }, anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS, anomalyDetectionJobsRefetch: () => {}, + anomalyDetectionSetupState: AnomalyDetectionSetupState.NoJobs, }; return ( diff --git a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/MLCallout.tsx b/x-pack/plugins/apm/public/components/app/service_inventory/service_list/MLCallout.tsx deleted file mode 100644 index 91625af7062cc..0000000000000 --- a/x-pack/plugins/apm/public/components/app/service_inventory/service_list/MLCallout.tsx +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React 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: `Pinpoint anomalous transactions and see the health of upstream and downstream services with APM's anomaly detection integration. Get started in just a few minutes.`, - })} -

- - - - - {i18n.translate( - 'xpack.apm.serviceOverview.mlNudgeMessage.learnMoreButton', - { - defaultMessage: `Get started`, - } - )} - - - - - onDismiss()}> - {i18n.translate( - 'xpack.apm.serviceOverview.mlNudgeMessage.dismissButton', - { - defaultMessage: `Dismiss`, - } - )} - - - -
- ); -} diff --git a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLManageJobsLink.tsx b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLManageJobsLink.tsx index eb7b531121753..4e2a7f477b666 100644 --- a/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLManageJobsLink.tsx +++ b/x-pack/plugins/apm/public/components/shared/Links/MachineLearningLinks/MLManageJobsLink.tsx @@ -7,47 +7,17 @@ import { EuiLink } from '@elastic/eui'; import React from 'react'; -import { UI_SETTINGS } from '../../../../../../../../src/plugins/data/common'; -import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; -import { useMlHref, ML_PAGES } from '../../../../../../ml/public'; -import { useLegacyUrlParams } from '../../../../context/url_params_context/use_url_params'; -import { TimePickerRefreshInterval } from '../../DatePicker/typings'; +import { useMlManageJobsHref } from '../../../../hooks/use_ml_manage_jobs_href'; interface Props { children?: React.ReactNode; external?: boolean; + jobId?: string; } -export function MLManageJobsLink({ children, external }: Props) { - const { - core, - plugins: { ml }, - } = useApmPluginContext(); - - const { urlParams } = useLegacyUrlParams(); - - const timePickerRefreshIntervalDefaults = - core.uiSettings.get( - UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS - ); - - const { - // hardcoding a custom default of 1 hour since the default kibana timerange of 15 minutes is shorter than the ML interval - rangeFrom = 'now-1h', - rangeTo = 'now', - refreshInterval = timePickerRefreshIntervalDefaults.value, - refreshPaused = timePickerRefreshIntervalDefaults.pause, - } = urlParams; - - const mlADLink = useMlHref(ml, core.http.basePath.get(), { - page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, - pageState: { - groupIds: ['apm'], - globalState: { - time: { from: rangeFrom, to: rangeTo }, - refreshInterval: { pause: refreshPaused, value: refreshInterval }, - }, - }, +export function MLManageJobsLink({ children, external, jobId }: Props) { + const mlADLink = useMlManageJobsHref({ + jobId, }); return ( diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.test.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.test.tsx index 0520cfa39a743..e47c4853827de 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.test.tsx @@ -5,28 +5,55 @@ * 2.0. */ +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { createMemoryHistory } from 'history'; import React from 'react'; -import { render, fireEvent, waitFor } from '@testing-library/react'; -import { MissingJobsAlert } from './anomaly_detection_setup_link'; +import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common'; +import { ApmMlJob } from '../../../../common/anomaly_detection/apm_ml_job'; +import { getAnomalyDetectionSetupState } from '../../../../common/anomaly_detection/get_anomaly_detection_setup_state'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import * as hooks from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context'; +import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { AnomalyDetectionSetupLink } from './anomaly_detection_setup_link'; async function renderTooltipAnchor({ jobs, environment, }: { - jobs: Array<{ job_id: string; environment: string }>; + jobs: ApmMlJob[]; environment?: string; }) { // mock api response jest.spyOn(hooks, 'useAnomalyDetectionJobsContext').mockReturnValue({ - anomalyDetectionJobsData: { jobs, hasLegacyJobs: false }, + anomalyDetectionJobsData: { + jobs, + hasLegacyJobs: jobs.some((job) => job.version <= 2), + }, anomalyDetectionJobsStatus: FETCH_STATUS.SUCCESS, anomalyDetectionJobsRefetch: () => {}, + anomalyDetectionSetupState: getAnomalyDetectionSetupState({ + environment: environment ?? ENVIRONMENT_ALL.value, + fetchStatus: FETCH_STATUS.SUCCESS, + isAuthorized: true, + jobs, + }), + }); + + const history = createMemoryHistory({ + initialEntries: [ + `/services?environment=${ + environment || ENVIRONMENT_ALL.value + }&rangeFrom=now-15m&rangeTo=now`, + ], }); const { baseElement, container } = render( - + + + + + ); // hover tooltip anchor if it exists @@ -65,7 +92,13 @@ describe('MissingJobsAlert', () => { describe('when no jobs exists for the selected environment', () => { it('shows a warning', async () => { const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ - jobs: [{ environment: 'production', job_id: 'my_job_id' }], + jobs: [ + { + environment: 'production', + jobId: 'my_job_id', + version: 3, + } as ApmMlJob, + ], environment: 'staging', }); @@ -79,7 +112,13 @@ describe('MissingJobsAlert', () => { describe('when a job exists for the selected environment', () => { it('does not show a warning', async () => { const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ - jobs: [{ environment: 'production', job_id: 'my_job_id' }], + jobs: [ + { + environment: 'production', + jobId: 'my_job_id', + version: 3, + } as ApmMlJob, + ], environment: 'production', }); @@ -91,7 +130,13 @@ describe('MissingJobsAlert', () => { describe('when at least one job exists and no environment is selected', () => { it('does not show a warning', async () => { const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ - jobs: [{ environment: 'production', job_id: 'my_job_id' }], + jobs: [ + { + environment: 'production', + jobId: 'my_job_id', + version: 3, + } as ApmMlJob, + ], }); expect(toolTipAnchor).not.toBeInTheDocument(); @@ -102,7 +147,54 @@ describe('MissingJobsAlert', () => { describe('when at least one job exists and all environments are selected', () => { it('does not show a warning', async () => { const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ - jobs: [{ environment: 'ENVIRONMENT_ALL', job_id: 'my_job_id' }], + jobs: [ + { + environment: 'ENVIRONMENT_ALL', + jobId: 'my_job_id', + version: 3, + } as ApmMlJob, + ], + }); + + expect(toolTipAnchor).not.toBeInTheDocument(); + expect(toolTipText).toBe(undefined); + }); + }); + + describe('when at least one legacy job exists', () => { + it('displays a nudge to upgrade', async () => { + const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ + jobs: [ + { + environment: 'ENVIRONMENT_ALL', + jobId: 'my_job_id', + version: 2, + } as ApmMlJob, + ], + }); + + expect(toolTipAnchor).toBeInTheDocument(); + expect(toolTipText).toBe( + 'Updates available for existing anomaly detection jobs.' + ); + }); + }); + + describe('when both legacy and modern jobs exist', () => { + it('does not show a tooltip', async () => { + const { toolTipAnchor, toolTipText } = await renderTooltipAnchor({ + jobs: [ + { + environment: 'ENVIRONMENT_ALL', + jobId: 'my_job_id', + version: 2, + } as ApmMlJob, + { + environment: 'ENVIRONMENT_ALL', + jobId: 'my_job_id_2', + version: 3, + } as ApmMlJob, + ], }); expect(toolTipAnchor).not.toBeInTheDocument(); diff --git a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx index 4891ca896076a..e1bda5475acc4 100644 --- a/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx +++ b/x-pack/plugins/apm/public/components/shared/apm_header_action_menu/anomaly_detection_setup_link.tsx @@ -5,32 +5,22 @@ * 2.0. */ -import { - EuiHeaderLink, - EuiIcon, - EuiLoadingSpinner, - EuiToolTip, -} from '@elastic/eui'; +import { EuiLoadingSpinner } from '@elastic/eui'; +import { IconType } from '@elastic/eui'; +import { EuiHeaderLink, EuiIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import React from 'react'; +import { AnomalyDetectionSetupState } from '../../../../common/anomaly_detection/get_anomaly_detection_setup_state'; import { ENVIRONMENT_ALL, getEnvironmentLabel, } from '../../../../common/environment_filter_values'; import { useAnomalyDetectionJobsContext } from '../../../context/anomaly_detection_jobs/use_anomaly_detection_jobs_context'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; -import { useLicenseContext } from '../../../context/license/use_license_context'; import { useApmParams } from '../../../hooks/use_apm_params'; -import { FETCH_STATUS } from '../../../hooks/use_fetcher'; import { useTheme } from '../../../hooks/use_theme'; -import { APIReturnType } from '../../../services/rest/createCallApmApi'; import { getLegacyApmHref } from '../Links/apm/APMLink'; -export type AnomalyDetectionApiResponse = - APIReturnType<'GET /internal/apm/settings/anomaly-detection/jobs'>; - -const DEFAULT_DATA = { jobs: [], hasLegacyJobs: false }; - export function AnomalyDetectionSetupLink() { const { query } = useApmParams('/*'); @@ -38,71 +28,86 @@ export function AnomalyDetectionSetupLink() { ('environment' in query && query.environment) || ENVIRONMENT_ALL.value; const { core } = useApmPluginContext(); - const canGetJobs = !!core.application.capabilities.ml?.canGetJobs; - const license = useLicenseContext(); - const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); + const { basePath } = core.http; const theme = useTheme(); - return ( + const { anomalyDetectionSetupState } = useAnomalyDetectionJobsContext(); + + let tooltipText: string = ''; + let color: 'warning' | 'text' | 'success' | 'danger' = 'text'; + let icon: IconType | undefined; + + if (anomalyDetectionSetupState === AnomalyDetectionSetupState.Failure) { + color = 'warning'; + tooltipText = i18n.translate( + 'xpack.apm.anomalyDetectionSetup.jobFetchFailureText', + { + defaultMessage: 'Could not determine state of anomaly detection setup.', + } + ); + icon = 'alert'; + } else if ( + anomalyDetectionSetupState === AnomalyDetectionSetupState.NoJobs || + anomalyDetectionSetupState === + AnomalyDetectionSetupState.NoJobsForEnvironment + ) { + color = 'warning'; + tooltipText = getNoJobsMessage(anomalyDetectionSetupState, environment); + icon = 'alert'; + } else if ( + anomalyDetectionSetupState === AnomalyDetectionSetupState.UpgradeableJobs + ) { + color = 'success'; + tooltipText = i18n.translate( + 'xpack.apm.anomalyDetectionSetup.upgradeableJobsText', + { + defaultMessage: + 'Updates available for existing anomaly detection jobs.', + } + ); + icon = 'wrench'; + } + + let pre: React.ReactElement | null = null; + + if (anomalyDetectionSetupState === AnomalyDetectionSetupState.Loading) { + pre = ; + } else if (icon) { + pre = ; + } + + const element = ( - {canGetJobs && hasValidLicense ? ( - - ) : ( - - )} + {pre} {ANOMALY_DETECTION_LINK_LABEL} ); -} - -export function MissingJobsAlert({ environment }: { environment?: string }) { - const { - anomalyDetectionJobsData = DEFAULT_DATA, - anomalyDetectionJobsStatus, - } = useAnomalyDetectionJobsContext(); - const defaultIcon = ; - - if (anomalyDetectionJobsStatus === FETCH_STATUS.LOADING) { - return ; - } - - if (anomalyDetectionJobsStatus !== FETCH_STATUS.SUCCESS) { - return defaultIcon; - } - - const isEnvironmentSelected = - environment && environment !== ENVIRONMENT_ALL.value; - - // there are jobs for at least one environment - if (!isEnvironmentSelected && anomalyDetectionJobsData.jobs.length > 0) { - return defaultIcon; - } - - // there are jobs for the selected environment - if ( - isEnvironmentSelected && - anomalyDetectionJobsData.jobs.some((job) => environment === job.environment) - ) { - return defaultIcon; - } - - return ( - - + const wrappedElement = tooltipText ? ( + + {element} + ) : ( + element ); + + return wrappedElement; } -function getTooltipText(environment?: string) { - if (!environment || environment === ENVIRONMENT_ALL.value) { +function getNoJobsMessage( + state: + | AnomalyDetectionSetupState.NoJobs + | AnomalyDetectionSetupState.NoJobsForEnvironment, + environment: string +) { + if (state === AnomalyDetectionSetupState.NoJobs) { return i18n.translate('xpack.apm.anomalyDetectionSetup.notEnabledText', { defaultMessage: `Anomaly detection is not yet enabled. Click to continue setup.`, }); diff --git a/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx b/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx index 30ca3e79f6d7b..03ae13c06c613 100644 --- a/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/managed_table/index.tsx @@ -44,6 +44,7 @@ interface Props { pagination?: boolean; isLoading?: boolean; error?: boolean; + tableLayout?: 'auto' | 'fixed'; } function defaultSortFn( @@ -70,6 +71,7 @@ function UnoptimizedManagedTable(props: Props) { pagination = true, isLoading = false, error = false, + tableLayout, } = props; const { @@ -141,6 +143,7 @@ function UnoptimizedManagedTable(props: Props) { // @ts-expect-error TS thinks pagination should be non-nullable, but it's not void; + onUpgradeClick?: () => any; + onCreateJobClick?: () => void; + isOnSettingsPage: boolean; + append?: React.ReactElement; +}) { + const [loading, setLoading] = useState(false); + + const mlManageJobsHref = useMlManageJobsHref(); + + let properties: + | { + primaryAction: React.ReactNode | undefined; + color: 'primary' | 'success' | 'danger' | 'warning'; + title: string; + icon: string; + text: string; + } + | undefined; + + const getLearnMoreLink = (color: 'primary' | 'success') => ( + + + {i18n.translate('xpack.apm.mlCallout.learnMoreButton', { + defaultMessage: `Learn more`, + })} + + + ); + + switch (anomalyDetectionSetupState) { + case AnomalyDetectionSetupState.NoJobs: + properties = { + title: i18n.translate('xpack.apm.mlCallout.noJobsCalloutTitle', { + defaultMessage: + 'Enable anomaly detection to add health status indicators to your services', + }), + text: i18n.translate('xpack.apm.mlCallout.noJobsCalloutText', { + defaultMessage: `Pinpoint anomalous transactions and see the health of upstream and downstream services with APM's anomaly detection integration. Get started in just a few minutes.`, + }), + icon: 'iInCircle', + color: 'primary', + primaryAction: isOnSettingsPage ? ( + { + onCreateJobClick?.(); + }} + > + {i18n.translate('xpack.apm.mlCallout.noJobsCalloutButtonText', { + defaultMessage: 'Create ML Job', + })} + + ) : ( + getLearnMoreLink('primary') + ), + }; + break; + + case AnomalyDetectionSetupState.UpgradeableJobs: + properties = { + title: i18n.translate( + 'xpack.apm.mlCallout.updateAvailableCalloutTitle', + { defaultMessage: 'Updates available' } + ), + text: i18n.translate('xpack.apm.mlCallout.updateAvailableCalloutText', { + defaultMessage: + 'We have updated the anomaly detection jobs that provide insights into degraded performance and added detectors for throughput and failed transaction rate. If you choose to upgrade, we will create the new jobs and close the existing legacy jobs. The data shown in the APM app will automatically switch to the new.', + }), + color: 'success', + icon: 'wrench', + primaryAction: isOnSettingsPage ? ( + { + setLoading(true); + Promise.resolve(onUpgradeClick?.()).finally(() => { + setLoading(false); + }); + }} + > + {i18n.translate( + 'xpack.apm.mlCallout.updateAvailableCalloutButtonText', + { + defaultMessage: 'Update jobs', + } + )} + + ) : ( + getLearnMoreLink('success') + ), + }; + break; + + case AnomalyDetectionSetupState.LegacyJobs: + properties = { + title: i18n.translate('xpack.apm.mlCallout.legacyJobsCalloutTitle', { + defaultMessage: 'Legacy ML jobs are no longer used in APM app', + }), + text: i18n.translate('xpack.apm.mlCallout.legacyJobsCalloutText', { + defaultMessage: + 'We have discovered legacy Machine Learning jobs from our previous integration which are no longer being used in the APM app', + }), + icon: 'iInCircle', + color: 'primary', + primaryAction: ( + + {i18n.translate( + 'xpack.apm.settings.anomaly_detection.legacy_jobs.button', + { defaultMessage: 'Review jobs' } + )} + + ), + }; + break; + } + + if (!properties) { + return null; + } + + const dismissable = !isOnSettingsPage; + + const hasAnyActions = properties.primaryAction || dismissable; + + const actions = hasAnyActions ? ( + + {properties.primaryAction && ( + {properties.primaryAction} + )} + {dismissable && ( + + + {i18n.translate('xpack.apm.mlCallout.dismissButton', { + defaultMessage: `Dismiss`, + })} + + + )} + + ) : null; + + return ( + +

{properties.text}

+ {actions} +
+ ); +} diff --git a/x-pack/plugins/apm/public/context/anomaly_detection_jobs/anomaly_detection_jobs_context.tsx b/x-pack/plugins/apm/public/context/anomaly_detection_jobs/anomaly_detection_jobs_context.tsx index bf9f2941fa2fb..3b9cea7b88998 100644 --- a/x-pack/plugins/apm/public/context/anomaly_detection_jobs/anomaly_detection_jobs_context.tsx +++ b/x-pack/plugins/apm/public/context/anomaly_detection_jobs/anomaly_detection_jobs_context.tsx @@ -5,14 +5,23 @@ * 2.0. */ -import React, { createContext, ReactChild, useState } from 'react'; +import React, { createContext, ReactChild } from 'react'; +import { + AnomalyDetectionSetupState, + getAnomalyDetectionSetupState, +} from '../../../common/anomaly_detection/get_anomaly_detection_setup_state'; +import { ENVIRONMENT_ALL } from '../../../common/environment_filter_values'; +import { useApmParams } from '../../hooks/use_apm_params'; import { FETCH_STATUS, useFetcher } from '../../hooks/use_fetcher'; import { APIReturnType } from '../../services/rest/createCallApmApi'; +import { useApmPluginContext } from '../apm_plugin/use_apm_plugin_context'; +import { useLicenseContext } from '../license/use_license_context'; export interface AnomalyDetectionJobsContextValue { anomalyDetectionJobsData?: APIReturnType<'GET /internal/apm/settings/anomaly-detection/jobs'>; anomalyDetectionJobsStatus: FETCH_STATUS; anomalyDetectionJobsRefetch: () => void; + anomalyDetectionSetupState: AnomalyDetectionSetupState; } export const AnomalyDetectionJobsContext = createContext( @@ -24,24 +33,45 @@ export function AnomalyDetectionJobsContextProvider({ }: { children: ReactChild; }) { - const [fetchId, setFetchId] = useState(0); - const refetch = () => setFetchId((id) => id + 1); + const { core } = useApmPluginContext(); + const canGetJobs = !!core.application.capabilities.ml?.canGetJobs; + const license = useLicenseContext(); + const hasValidLicense = license?.isActive && license?.hasAtLeast('platinum'); - const { data, status } = useFetcher( - (callApmApi) => - callApmApi({ + const isAuthorized = !!(canGetJobs && hasValidLicense); + + const { data, status, refetch } = useFetcher( + (callApmApi) => { + if (!isAuthorized) { + return; + } + return callApmApi({ endpoint: `GET /internal/apm/settings/anomaly-detection/jobs`, - }), - [fetchId], // eslint-disable-line react-hooks/exhaustive-deps + }); + }, + [isAuthorized], { showToastOnError: false } ); + const { query } = useApmParams('/*'); + + const environment = + ('environment' in query && query.environment) || ENVIRONMENT_ALL.value; + + const anomalyDetectionSetupState = getAnomalyDetectionSetupState({ + environment, + fetchStatus: status, + jobs: data?.jobs ?? [], + isAuthorized, + }); + return ( {children} diff --git a/x-pack/plugins/apm/public/hooks/use_ml_manage_jobs_href.ts b/x-pack/plugins/apm/public/hooks/use_ml_manage_jobs_href.ts new file mode 100644 index 0000000000000..cc187c6cf619a --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_ml_manage_jobs_href.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { UI_SETTINGS } from '../../../../../src/plugins/data/public'; +import { ML_PAGES, useMlHref } from '../../../ml/public'; +import { TimePickerRefreshInterval } from '../components/shared/DatePicker/typings'; +import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; +import { useLegacyUrlParams } from '../context/url_params_context/use_url_params'; + +export function useMlManageJobsHref({ jobId }: { jobId?: string } = {}) { + const { + core, + plugins: { ml }, + } = useApmPluginContext(); + + const { urlParams } = useLegacyUrlParams(); + + const timePickerRefreshIntervalDefaults = + core.uiSettings.get( + UI_SETTINGS.TIMEPICKER_REFRESH_INTERVAL_DEFAULTS + ); + + const { + // hardcoding a custom default of 1 hour since the default kibana timerange of 15 minutes is shorter than the ML interval + rangeFrom = 'now-1h', + rangeTo = 'now', + refreshInterval = timePickerRefreshIntervalDefaults.value, + refreshPaused = timePickerRefreshIntervalDefaults.pause, + } = urlParams; + + const mlADLink = useMlHref(ml, core.http.basePath.get(), { + page: ML_PAGES.ANOMALY_DETECTION_JOBS_MANAGE, + pageState: { + groupIds: ['apm'], + jobId, + globalState: { + time: { from: rangeFrom, to: rangeTo }, + refreshInterval: { pause: refreshPaused, value: refreshInterval }, + }, + }, + }); + + return mlADLink; +} diff --git a/x-pack/plugins/apm/server/deprecations/index.ts b/x-pack/plugins/apm/server/deprecations/index.ts index 06d04eb037d73..6c6567440f267 100644 --- a/x-pack/plugins/apm/server/deprecations/index.ts +++ b/x-pack/plugins/apm/server/deprecations/index.ts @@ -51,7 +51,7 @@ export function getDeprecations({ }), message: i18n.translate('xpack.apm.deprecations.message', { defaultMessage: - 'Running the APM Server binary directly is considered a legacy option and is deprecated since 7.16. Switch to APM Server managed by an Elastic Agent instead. Read our documentation to learn more.', + 'Running the APM Server binary directly is considered a legacy option and will be deprecated and removed in the future.', }), documentationUrl: `https://www.elastic.co/guide/en/apm/server/${docBranch}/apm-integration.html`, level: 'warning', @@ -68,7 +68,7 @@ export function getDeprecations({ }), i18n.translate('xpack.apm.deprecations.steps.switch', { defaultMessage: - 'Click "Switch to data streams". You will be guided through the process', + 'Click "Switch to Elastic Agent". You will be guided through the process', }), ], }, diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/apm_ml_jobs_query.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/apm_ml_jobs_query.ts index 4eec3b39f3739..2720dbdecfe1c 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/apm_ml_jobs_query.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/apm_ml_jobs_query.ts @@ -4,12 +4,10 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { - MlJob, - QueryDslQueryContainer, -} from '@elastic/elasticsearch/lib/api/types'; +import { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types'; +import { ApmMlJob } from '../../../common/anomaly_detection/apm_ml_job'; -export function apmMlJobsQuery(jobs: MlJob[]) { +export function apmMlJobsQuery(jobs: ApmMlJob[]) { if (!jobs.length) { throw new Error('At least one ML job should be given'); } @@ -17,7 +15,7 @@ export function apmMlJobsQuery(jobs: MlJob[]) { return [ { terms: { - job_id: jobs.map((job) => job.job_id), + job_id: jobs.map((job) => job.jobId), }, }, ] as QueryDslQueryContainer[]; diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts index 7277a12c2bf14..d855adee4a9ba 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/create_anomaly_detection_jobs.ts @@ -15,6 +15,7 @@ import { METRICSET_NAME, PROCESSOR_EVENT, } from '../../../common/elasticsearch_fieldnames'; +import { Environment } from '../../../common/environment_rt'; import { ProcessorEvent } from '../../../common/processor_event'; import { environmentQuery } from '../../../common/utils/environment_query'; import { withApmSpan } from '../../utils/with_apm_span'; @@ -24,7 +25,7 @@ import { getAnomalyDetectionJobs } from './get_anomaly_detection_jobs'; export async function createAnomalyDetectionJobs( setup: Setup, - environments: string[], + environments: Environment[], logger: Logger ) { const { ml, indices } = setup; @@ -33,13 +34,6 @@ export async function createAnomalyDetectionJobs( throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); } - const mlCapabilities = await withApmSpan('get_ml_capabilities', () => - ml.mlSystem.mlCapabilities() - ); - if (!mlCapabilities.mlFeatureEnabledInSpace) { - throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); - } - const uniqueMlJobEnvs = await getUniqueMlJobEnvs(setup, environments, logger); if (uniqueMlJobEnvs.length === 0) { return []; @@ -56,6 +50,7 @@ export async function createAnomalyDetectionJobs( createAnomalyDetectionJob({ ml, environment, dataViewName }) ) ); + const jobResponses = responses.flatMap((response) => response.jobs); const failedJobs = jobResponses.filter(({ success }) => !success); @@ -116,12 +111,15 @@ async function createAnomalyDetectionJob({ async function getUniqueMlJobEnvs( setup: Setup, - environments: string[], + environments: Environment[], logger: Logger ) { // skip creation of duplicate ML jobs - const jobs = await getAnomalyDetectionJobs(setup, logger); - const existingMlJobEnvs = jobs.map(({ environment }) => environment); + const jobs = await getAnomalyDetectionJobs(setup); + const existingMlJobEnvs = jobs + .filter((job) => job.version === 3) + .map(({ environment }) => environment); + const requestedExistingMlJobEnvs = environments.filter((env) => existingMlJobEnvs.includes(env) ); diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts index 75b2e8289c7a8..9047ae9ed90d0 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_detection_jobs.ts @@ -4,41 +4,17 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - -import { Logger } from 'kibana/server'; import Boom from '@hapi/boom'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { Setup } from '../helpers/setup_request'; import { getMlJobsWithAPMGroup } from './get_ml_jobs_with_apm_group'; -import { withApmSpan } from '../../utils/with_apm_span'; -export function getAnomalyDetectionJobs(setup: Setup, logger: Logger) { +export function getAnomalyDetectionJobs(setup: Setup) { const { ml } = setup; if (!ml) { throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); } - return withApmSpan('get_anomaly_detection_jobs', async () => { - const mlCapabilities = await withApmSpan('get_ml_capabilities', () => - ml.mlSystem.mlCapabilities() - ); - - if (!mlCapabilities.mlFeatureEnabledInSpace) { - throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); - } - - const response = await getMlJobsWithAPMGroup(ml.anomalyDetectors); - return response.jobs - .filter( - (job) => (job.custom_settings?.job_tags?.apm_ml_version ?? 0) >= 2 - ) - .map((job) => { - const environment = job.custom_settings?.job_tags?.environment ?? ''; - return { - job_id: job.job_id, - environment, - }; - }); - }); + return getMlJobsWithAPMGroup(ml.anomalyDetectors); } diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_timeseries.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_timeseries.ts index 77ffef9801a86..37279d3320585 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_timeseries.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_anomaly_timeseries.ts @@ -46,9 +46,7 @@ export async function getAnomalyTimeseries({ end, }); - const { jobs: mlJobs } = await getMlJobsWithAPMGroup( - mlSetup.anomalyDetectors - ); + const mlJobs = await getMlJobsWithAPMGroup(mlSetup.anomalyDetectors); if (!mlJobs.length) { return []; @@ -148,7 +146,7 @@ export async function getAnomalyTimeseries({ } ); - const jobsById = keyBy(mlJobs, (job) => job.job_id); + const jobsById = keyBy(mlJobs, (job) => job.jobId); function divide(value: number | null, divider: number) { if (value === null) { @@ -176,9 +174,9 @@ export async function getAnomalyTimeseries({ jobId, type, serviceName: bucket.key.serviceName as string, - environment: job.custom_settings!.job_tags!.environment as string, + environment: job.environment, transactionType: bucket.key.transactionType as string, - version: Number(job.custom_settings!.job_tags!.apm_ml_version), + version: job.version, anomalies: bucket.timeseries.buckets.map((dateBucket) => ({ x: dateBucket.key as number, y: diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_with_apm_group.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_with_apm_group.ts index bcea8f1ed6b26..1f989ba17fe7c 100644 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_with_apm_group.ts +++ b/x-pack/plugins/apm/server/lib/anomaly_detection/get_ml_jobs_with_apm_group.ts @@ -6,23 +6,63 @@ */ import { MlPluginSetup } from '../../../../ml/server'; +import { ApmMlJob } from '../../../common/anomaly_detection/apm_ml_job'; +import { Environment } from '../../../common/environment_rt'; import { withApmSpan } from '../../utils/with_apm_span'; import { APM_ML_JOB_GROUP } from './constants'; // returns ml jobs containing "apm" group // workaround: the ML api returns 404 when no jobs are found. This is handled so instead of throwing an empty response is returned + +function catch404(e: any) { + if (e.statusCode === 404) { + return []; + } + + throw e; +} + export function getMlJobsWithAPMGroup( anomalyDetectors: ReturnType -) { +): Promise { return withApmSpan('get_ml_jobs_with_apm_group', async () => { try { - return await anomalyDetectors.jobs(APM_ML_JOB_GROUP); - } catch (e) { - if (e.statusCode === 404) { - return { count: 0, jobs: [] }; - } + const [jobs, allJobStats, allDatafeedStats] = await Promise.all([ + anomalyDetectors + .jobs(APM_ML_JOB_GROUP) + .then((response) => response.jobs), + anomalyDetectors + .jobStats(APM_ML_JOB_GROUP) + .then((response) => response.jobs) + .catch(catch404), + anomalyDetectors + .datafeedStats(`datafeed-${APM_ML_JOB_GROUP}*`) + .then((response) => response.datafeeds) + .catch(catch404), + ]); + + return jobs.map((job): ApmMlJob => { + const jobStats = allJobStats.find( + (stats) => stats.job_id === job.job_id + ); - throw e; + const datafeedStats = allDatafeedStats.find( + (stats) => stats.datafeed_id === job.datafeed_config?.datafeed_id + ); + + return { + environment: String( + job.custom_settings?.job_tags?.environment + ) as Environment, + jobId: job.job_id, + jobState: jobStats?.state as ApmMlJob['jobState'], + version: Number(job.custom_settings?.job_tags?.apm_ml_version ?? 1), + datafeedId: datafeedStats?.datafeed_id, + datafeedState: datafeedStats?.state as ApmMlJob['datafeedState'], + }; + }); + } catch (e) { + return catch404(e) as ApmMlJob[]; } }); } diff --git a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts b/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts deleted file mode 100644 index c189d24efc23a..0000000000000 --- a/x-pack/plugins/apm/server/lib/anomaly_detection/has_legacy_jobs.ts +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import Boom from '@hapi/boom'; -import { ML_ERRORS } from '../../../common/anomaly_detection'; -import { withApmSpan } from '../../utils/with_apm_span'; -import { Setup } from '../helpers/setup_request'; -import { getMlJobsWithAPMGroup } from './get_ml_jobs_with_apm_group'; - -// Determine whether there are any legacy ml jobs. -// A legacy ML job has a job id that ends with "high_mean_response_time" and created_by=ml-module-apm-transaction -export function hasLegacyJobs(setup: Setup) { - const { ml } = setup; - - if (!ml) { - throw Boom.notImplemented(ML_ERRORS.ML_NOT_AVAILABLE); - } - - return withApmSpan('has_legacy_jobs', async () => { - const mlCapabilities = await withApmSpan('get_ml_capabilities', () => - ml.mlSystem.mlCapabilities() - ); - if (!mlCapabilities.mlFeatureEnabledInSpace) { - throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE_IN_SPACE); - } - - const response = await getMlJobsWithAPMGroup(ml.anomalyDetectors); - return response.jobs.some( - (job) => - job.job_id.endsWith('high_mean_response_time') && - job.custom_settings?.created_by === 'ml-module-apm-transaction' - ); - }); -} diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts index fa4125c54126d..889fe3c16596e 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.test.ts @@ -6,9 +6,10 @@ */ import { registerTransactionDurationAnomalyAlertType } from './register_transaction_duration_anomaly_alert_type'; import { ANOMALY_SEVERITY } from '../../../common/ml_constants'; -import { Job, MlPluginSetup } from '../../../../ml/server'; +import { MlPluginSetup } from '../../../../ml/server'; import * as GetServiceAnomalies from '../service_map/get_service_anomalies'; import { createRuleTypeMocks } from './test_utils'; +import { ApmMlJob } from '../../../common/anomaly_detection/apm_ml_job'; describe('Transaction duration anomaly alert', () => { afterEach(() => { @@ -65,14 +66,14 @@ describe('Transaction duration anomaly alert', () => { jest.spyOn(GetServiceAnomalies, 'getMLJobs').mockReturnValue( Promise.resolve([ { - job_id: '1', - custom_settings: { job_tags: { environment: 'development' } }, + jobId: '1', + environment: 'development', }, { - job_id: '2', - custom_settings: { job_tags: { environment: 'production' } }, + jobId: '2', + environment: 'production', }, - ] as unknown as Job[]) + ] as unknown as ApmMlJob[]) ); const { services, dependencies, executor } = createRuleTypeMocks(); @@ -118,14 +119,14 @@ describe('Transaction duration anomaly alert', () => { jest.spyOn(GetServiceAnomalies, 'getMLJobs').mockReturnValue( Promise.resolve([ { - job_id: '1', - custom_settings: { job_tags: { environment: 'development' } }, + jobId: '1', + environment: 'development', }, { - job_id: '2', - custom_settings: { job_tags: { environment: 'production' } }, + jobId: '2', + environment: 'production', }, - ] as unknown as Job[]) + ] as unknown as ApmMlJob[]) ); const { services, dependencies, executor, scheduleActions } = diff --git a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.ts b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.ts index dead149cd7761..5216d485bc31e 100644 --- a/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.ts +++ b/x-pack/plugins/apm/server/routes/alerts/register_transaction_duration_anomaly_alert_type.ts @@ -126,7 +126,7 @@ export function registerTransactionDurationAnomalyAlertType({ return {}; } - const jobIds = mlJobs.map((job) => job.job_id); + const jobIds = mlJobs.map((job) => job.jobId); const anomalySearchParams = { body: { size: 0, @@ -190,7 +190,7 @@ export function registerTransactionDurationAnomalyAlertType({ .map((bucket) => { const latest = bucket.latest_score.top[0].metrics; - const job = mlJobs.find((j) => j.job_id === latest.job_id); + const job = mlJobs.find((j) => j.jobId === latest.job_id); if (!job) { logger.warn( @@ -202,7 +202,7 @@ export function registerTransactionDurationAnomalyAlertType({ return { serviceName: latest.partition_field_value as string, transactionType: latest.by_field_value as string, - environment: job.custom_settings!.job_tags!.environment, + environment: job.environment, score: latest.record_score as number, }; }) diff --git a/x-pack/plugins/apm/server/routes/service_map/get_service_anomalies.ts b/x-pack/plugins/apm/server/routes/service_map/get_service_anomalies.ts index 0a0a92760decd..792bc0463aa15 100644 --- a/x-pack/plugins/apm/server/routes/service_map/get_service_anomalies.ts +++ b/x-pack/plugins/apm/server/routes/service_map/get_service_anomalies.ts @@ -162,17 +162,13 @@ export async function getMLJobs( anomalyDetectors: ReturnType, environment: string ) { - const response = await getMlJobsWithAPMGroup(anomalyDetectors); + const jobs = await getMlJobsWithAPMGroup(anomalyDetectors); // to filter out legacy jobs we are filtering by the existence of `apm_ml_version` in `custom_settings` // and checking that it is compatable. - const mlJobs = response.jobs.filter( - (job) => (job.custom_settings?.job_tags?.apm_ml_version ?? 0) >= 2 - ); + const mlJobs = jobs.filter((job) => job.version >= 2); if (environment !== ENVIRONMENT_ALL.value) { - const matchingMLJob = mlJobs.find( - (job) => job.custom_settings?.job_tags?.environment === environment - ); + const matchingMLJob = mlJobs.find((job) => job.environment === environment); if (!matchingMLJob) { return []; } @@ -186,5 +182,5 @@ export async function getMLJobIds( environment: string ) { const mlJobs = await getMLJobs(anomalyDetectors, environment); - return mlJobs.map((job) => job.job_id); + return mlJobs.map((job) => job.jobId); } diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection/route.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection/route.ts index a924a9214977d..35089acf38688 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection/route.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection/route.ts @@ -11,15 +11,15 @@ import { maxSuggestions } from '../../../../../observability/common'; import { isActivePlatinumLicense } from '../../../../common/license_check'; import { ML_ERRORS } from '../../../../common/anomaly_detection'; import { createApmServerRoute } from '../../apm_routes/create_apm_server_route'; -import { getAnomalyDetectionJobs } from '../../../lib/anomaly_detection/get_anomaly_detection_jobs'; import { createAnomalyDetectionJobs } from '../../../lib/anomaly_detection/create_anomaly_detection_jobs'; import { setupRequest } from '../../../lib/helpers/setup_request'; import { getAllEnvironments } from '../../environments/get_all_environments'; -import { hasLegacyJobs } from '../../../lib/anomaly_detection/has_legacy_jobs'; import { getSearchAggregatedTransactions } from '../../../lib/helpers/transactions'; import { notifyFeatureUsage } from '../../../feature'; -import { withApmSpan } from '../../../utils/with_apm_span'; import { createApmServerRouteRepository } from '../../apm_routes/create_apm_server_route_repository'; +import { updateToV3 } from './update_to_v3'; +import { environmentStringRt } from '../../../../common/environment_rt'; +import { getMlJobsWithAPMGroup } from '../../../lib/anomaly_detection/get_ml_jobs_with_apm_group'; // get ML anomaly detection jobs for each environment const anomalyDetectionJobsRoute = createApmServerRoute({ @@ -29,22 +29,21 @@ const anomalyDetectionJobsRoute = createApmServerRoute({ }, handler: async (resources) => { const setup = await setupRequest(resources); - const { context, logger } = resources; + const { context } = resources; if (!isActivePlatinumLicense(context.licensing.license)) { throw Boom.forbidden(ML_ERRORS.INVALID_LICENSE); } - const [jobs, legacyJobs] = await withApmSpan('get_available_ml_jobs', () => - Promise.all([ - getAnomalyDetectionJobs(setup, logger), - hasLegacyJobs(setup), - ]) - ); + if (!setup.ml) { + throw Boom.forbidden(ML_ERRORS.ML_NOT_AVAILABLE); + } + + const jobs = await getMlJobsWithAPMGroup(setup.ml?.anomalyDetectors); return { jobs, - hasLegacyJobs: legacyJobs, + hasLegacyJobs: jobs.some((job) => job.version === 1), }; }, }); @@ -57,7 +56,7 @@ const createAnomalyDetectionJobsRoute = createApmServerRoute({ }, params: t.type({ body: t.type({ - environments: t.array(t.string), + environments: t.array(environmentStringRt), }), }), handler: async (resources) => { @@ -107,7 +106,35 @@ const anomalyDetectionEnvironmentsRoute = createApmServerRoute({ }, }); +const anomalyDetectionUpdateToV3Route = createApmServerRoute({ + endpoint: 'POST /internal/apm/settings/anomaly-detection/update_to_v3', + options: { + tags: [ + 'access:apm', + 'access:apm_write', + 'access:ml:canCreateJob', + 'access:ml:canGetJobs', + 'access:ml:canCloseJob', + ], + }, + handler: async (resources) => { + const [setup, esClient] = await Promise.all([ + setupRequest(resources), + resources.core + .start() + .then((start) => start.elasticsearch.client.asInternalUser), + ]); + + const { logger } = resources; + + return { + update: await updateToV3({ setup, logger, esClient }), + }; + }, +}); + export const anomalyDetectionRouteRepository = createApmServerRouteRepository() .add(anomalyDetectionJobsRoute) .add(createAnomalyDetectionJobsRoute) - .add(anomalyDetectionEnvironmentsRoute); + .add(anomalyDetectionEnvironmentsRoute) + .add(anomalyDetectionUpdateToV3Route); diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection/update_to_v3.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection/update_to_v3.ts new file mode 100644 index 0000000000000..b23a28648482e --- /dev/null +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection/update_to_v3.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Logger } from '@kbn/logging'; +import { uniq } from 'lodash'; +import pLimit from 'p-limit'; +import { ElasticsearchClient } from '../../../../../../../src/core/server'; +import { JOB_STATE } from '../../../../../ml/common'; +import { createAnomalyDetectionJobs } from '../../../lib/anomaly_detection/create_anomaly_detection_jobs'; +import { getAnomalyDetectionJobs } from '../../../lib/anomaly_detection/get_anomaly_detection_jobs'; +import { Setup } from '../../../lib/helpers/setup_request'; +import { withApmSpan } from '../../../utils/with_apm_span'; + +export async function updateToV3({ + logger, + setup, + esClient, +}: { + logger: Logger; + setup: Setup; + esClient: ElasticsearchClient; +}) { + const allJobs = await getAnomalyDetectionJobs(setup); + + const v2Jobs = allJobs.filter((job) => job.version === 2); + + const activeV2Jobs = v2Jobs.filter( + (job) => + job.jobState === JOB_STATE.OPENED || job.jobState === JOB_STATE.OPENING + ); + + const environments = uniq(v2Jobs.map((job) => job.environment)); + + const limiter = pLimit(3); + + if (!v2Jobs.length) { + return true; + } + + if (activeV2Jobs.length) { + await withApmSpan('anomaly_detection_stop_v2_jobs', () => + Promise.all( + activeV2Jobs.map((job) => + limiter(() => { + return esClient.ml.closeJob({ + job_id: job.jobId, + }); + }) + ) + ) + ); + } + + await createAnomalyDetectionJobs(setup, environments, logger); + + return true; +} diff --git a/x-pack/plugins/apm/server/routes/typings.ts b/x-pack/plugins/apm/server/routes/typings.ts index a089c7bf3968a..6ec196b2a9b8c 100644 --- a/x-pack/plugins/apm/server/routes/typings.ts +++ b/x-pack/plugins/apm/server/routes/typings.ts @@ -37,6 +37,7 @@ export interface APMRouteCreateOptions { | 'access:apm_write' | 'access:ml:canGetJobs' | 'access:ml:canCreateJob' + | 'access:ml:canCloseJob' >; body?: { accepts: Array<'application/json' | 'multipart/form-data'> }; disableTelemetry?: boolean; diff --git a/x-pack/plugins/canvas/canvas_plugin_src/renderers/external.ts b/x-pack/plugins/canvas/canvas_plugin_src/renderers/external.ts index 569669032cb0b..9b79c50a92098 100644 --- a/x-pack/plugins/canvas/canvas_plugin_src/renderers/external.ts +++ b/x-pack/plugins/canvas/canvas_plugin_src/renderers/external.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { imageRenderer } from '../../../../../src/plugins/expression_image/public'; +import { imageRendererFactory } from '../../../../../src/plugins/expression_image/public'; import { metricRendererFactory } from '../../../../../src/plugins/expression_metric/public'; import { errorRendererFactory, @@ -14,15 +14,18 @@ import { import { revealImageRendererFactory } from '../../../../../src/plugins/expression_reveal_image/public'; import { repeatImageRendererFactory } from '../../../../../src/plugins/expression_repeat_image/public'; import { - shapeRenderer, - progressRenderer, + shapeRendererFactory, + progressRendererFactory, } from '../../../../../src/plugins/expression_shape/public'; -export const renderFunctions = [imageRenderer, shapeRenderer, progressRenderer]; +export const renderFunctions = []; export const renderFunctionFactories = [ debugRendererFactory, errorRendererFactory, + imageRendererFactory, + shapeRendererFactory, + progressRendererFactory, revealImageRendererFactory, repeatImageRendererFactory, metricRendererFactory, diff --git a/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js b/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js index 01b8cc98ba5ec..f8aebc04efe5c 100644 --- a/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js +++ b/x-pack/plugins/canvas/shareable_runtime/supported_renderers.js @@ -10,7 +10,7 @@ import { pie } from '../canvas_plugin_src/renderers/pie'; import { plot } from '../canvas_plugin_src/renderers/plot'; import { getTableRenderer } from '../canvas_plugin_src/renderers/table'; import { getTextRenderer } from '../canvas_plugin_src/renderers/text'; -import { imageRenderer as image } from '../../../../src/plugins/expression_image/public'; +import { getImageRenderer } from '../../../../src/plugins/expression_image/public'; import { getErrorRenderer, getDebugRenderer, @@ -18,8 +18,8 @@ import { import { getRevealImageRenderer } from '../../../../src/plugins/expression_reveal_image/public'; import { getRepeatImageRenderer } from '../../../../src/plugins/expression_repeat_image/public'; import { - shapeRenderer as shape, - progressRenderer as progress, + getShapeRenderer, + getProgressRenderer, } from '../../../../src/plugins/expression_shape/public'; import { getMetricRenderer } from '../../../../src/plugins/expression_metric/public'; @@ -31,6 +31,9 @@ const renderFunctionsFactories = [ getTableRenderer, getErrorRenderer, getDebugRenderer, + getImageRenderer, + getShapeRenderer, + getProgressRenderer, getRevealImageRenderer, getRepeatImageRenderer, getMetricRenderer, @@ -41,13 +44,6 @@ const renderFunctionsFactories = [ * a renderer is not listed here, but is used by the Shared Workpad, it will * not render. This includes any plugins. */ -export const renderFunctions = [ - image, - pie, - plot, - progress, - shape, - ...renderFunctionsFactories.map(unboxFactory), -]; +export const renderFunctions = [pie, plot, ...renderFunctionsFactories.map(unboxFactory)]; export const renderFunctionNames = [...renderFunctions.map((fn) => fn().name)]; diff --git a/x-pack/plugins/cases/common/index.ts b/x-pack/plugins/cases/common/index.ts index d38b1a779981c..41bba2ee2194d 100644 --- a/x-pack/plugins/cases/common/index.ts +++ b/x-pack/plugins/cases/common/index.ts @@ -5,11 +5,28 @@ * 2.0. */ -// TODO: https://github.com/elastic/kibana/issues/110896 -/* eslint-disable @kbn/eslint/no_export_all */ - -export * from './constants'; -export * from './api'; -export * from './ui/types'; -export * from './utils/connectors_api'; -export * from './utils/user_actions'; +// Careful of exporting anything from this file as any file(s) you export here will cause your page bundle size to increase. +// If you're using functions/types/etc... internally or within integration tests it's best to import directly from their paths +// than expose the functions/types/etc... here. You should _only_ expose functions/types/etc... that need to be shared with other plugins here. + +// When you do have to add things here you might want to consider creating a package such as kbn-cases-constants to share with +// other plugins instead as packages are easier to break down and you do not have to carry the cost of extra plugin weight on +// first download since the other plugins/areas of your code can directly pull from the package in their async imports. +// For example, constants below could eventually be in a "kbn-cases-constants" instead. +// See: https://docs.elastic.dev/kibana-dev-docs/key-concepts/platform-intro#public-plugin-api + +export { CASES_URL, SECURITY_SOLUTION_OWNER, ENABLE_CASE_CONNECTOR } from './constants'; + +export { CommentType, CaseStatuses, getCasesFromAlertsUrl, throwErrors } from './api'; + +export type { + SubCase, + Case, + Ecs, + CasesContextValue, + CaseViewRefreshPropInterface, +} from './ui/types'; + +export { StatusAll } from './ui/types'; + +export { getCreateConnectorUrl, getAllConnectorsUrl } from './utils/connectors_api'; diff --git a/x-pack/plugins/cases/common/utils/connectors_api.ts b/x-pack/plugins/cases/common/utils/connectors_api.ts index f9f85bbfb0127..3ab8f856d925e 100644 --- a/x-pack/plugins/cases/common/utils/connectors_api.ts +++ b/x-pack/plugins/cases/common/utils/connectors_api.ts @@ -9,7 +9,7 @@ * Actions and connectors API endpoint helpers */ -import { ACTION_URL, ACTION_TYPES_URL, CONNECTORS_URL } from '../../common'; +import { ACTION_URL, ACTION_TYPES_URL, CONNECTORS_URL } from '../../common/constants'; /** * diff --git a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts index 49d03d44a3a4f..08eb2ebf3df7a 100644 --- a/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts +++ b/x-pack/plugins/cases/public/common/lib/kibana/hooks.ts @@ -10,7 +10,7 @@ import moment from 'moment-timezone'; import { useCallback, useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; -import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../common'; +import { DEFAULT_DATE_FORMAT, DEFAULT_DATE_FORMAT_TZ } from '../../../../common/constants'; import { AuthenticatedUser } from '../../../../../security/common/model'; import { convertToCamelCase } from '../../../containers/utils'; import { StartServices } from '../../../types'; diff --git a/x-pack/plugins/cases/public/common/mock/register_connectors.ts b/x-pack/plugins/cases/public/common/mock/register_connectors.ts index 42e7cd4a85e40..b86968e4bf801 100644 --- a/x-pack/plugins/cases/public/common/mock/register_connectors.ts +++ b/x-pack/plugins/cases/public/common/mock/register_connectors.ts @@ -8,7 +8,7 @@ import { TriggersAndActionsUIPublicPluginStart } from '../../../../triggers_actions_ui/public'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { actionTypeRegistryMock } from '../../../../triggers_actions_ui/public/application/action_type_registry.mock'; -import { CaseActionConnector } from '../../../common'; +import { CaseActionConnector } from '../../../common/ui/types'; const getUniqueActionTypeIds = (connectors: CaseActionConnector[]) => new Set(connectors.map((connector) => connector.actionTypeId)); diff --git a/x-pack/plugins/cases/public/common/mock/test_providers.tsx b/x-pack/plugins/cases/public/common/mock/test_providers.tsx index d016dce48a24e..c076ca28c9318 100644 --- a/x-pack/plugins/cases/public/common/mock/test_providers.tsx +++ b/x-pack/plugins/cases/public/common/mock/test_providers.tsx @@ -10,7 +10,8 @@ import { merge } from 'lodash'; import { euiDarkVars } from '@kbn/ui-shared-deps-src/theme'; import { I18nProvider } from '@kbn/i18n-react'; import { ThemeProvider } from 'styled-components'; -import { CasesContextValue, DEFAULT_FEATURES, SECURITY_SOLUTION_OWNER } from '../../../common'; +import { DEFAULT_FEATURES, SECURITY_SOLUTION_OWNER } from '../../../common/constants'; +import { CasesContextValue } from '../../../common/ui/types'; import { CasesProvider } from '../../components/cases_context'; import { createKibanaContextProviderMock } from '../lib/kibana/kibana_react.mock'; import { FieldHook } from '../shared_imports'; diff --git a/x-pack/plugins/cases/public/common/user_actions/parsers.test.ts b/x-pack/plugins/cases/public/common/user_actions/parsers.test.ts index c6d13cc41686c..e2d24bf19f3d4 100644 --- a/x-pack/plugins/cases/public/common/user_actions/parsers.test.ts +++ b/x-pack/plugins/cases/public/common/user_actions/parsers.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConnectorTypes, noneConnectorId } from '../../../common'; +import { ConnectorTypes, noneConnectorId } from '../../../common/api'; import { parseStringAsConnector, parseStringAsExternalService } from './parsers'; describe('user actions utility functions', () => { diff --git a/x-pack/plugins/cases/public/common/user_actions/parsers.ts b/x-pack/plugins/cases/public/common/user_actions/parsers.ts index dfea22443aa51..0384a97124c54 100644 --- a/x-pack/plugins/cases/public/common/user_actions/parsers.ts +++ b/x-pack/plugins/cases/public/common/user_actions/parsers.ts @@ -12,7 +12,7 @@ import { noneConnectorId, CaseFullExternalService, CaseUserActionExternalServiceRt, -} from '../../../common'; +} from '../../../common/api'; export const parseStringAsConnector = ( id: string | null, diff --git a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx index c15722a3ec354..f1167504628c4 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.test.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.test.tsx @@ -12,7 +12,8 @@ import { noop } from 'lodash/fp'; import { TestProviders } from '../../common/mock'; -import { CommentRequest, CommentType, SECURITY_SOLUTION_OWNER } from '../../../common'; +import { CommentRequest, CommentType } from '../../../common/api'; +import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { usePostComment } from '../../containers/use_post_comment'; import { AddComment, AddCommentProps, AddCommentRefObject } from '.'; import { CasesTimelineIntegrationProvider } from '../timeline_context'; diff --git a/x-pack/plugins/cases/public/components/add_comment/index.tsx b/x-pack/plugins/cases/public/components/add_comment/index.tsx index 37f21e0949288..83bd187e7863a 100644 --- a/x-pack/plugins/cases/public/components/add_comment/index.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/index.tsx @@ -17,7 +17,7 @@ import { EuiButton, EuiFlexItem, EuiFlexGroup, EuiLoadingSpinner } from '@elasti import styled from 'styled-components'; import { isEmpty } from 'lodash'; -import { CommentType } from '../../../common'; +import { CommentType } from '../../../common/api'; import { usePostComment } from '../../containers/use_post_comment'; import { Case } from '../../containers/types'; import { EuiMarkdownEditorRef, MarkdownEditorForm } from '../markdown_editor'; diff --git a/x-pack/plugins/cases/public/components/add_comment/schema.tsx b/x-pack/plugins/cases/public/components/add_comment/schema.tsx index 9693219dd5196..3e32c8a938b68 100644 --- a/x-pack/plugins/cases/public/components/add_comment/schema.tsx +++ b/x-pack/plugins/cases/public/components/add_comment/schema.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { CommentRequestUserType } from '../../../common'; +import { CommentRequestUserType } from '../../../common/api'; import { FIELD_TYPES, fieldValidators, FormSchema } from '../../common/shared_imports'; import * as i18n from './translations'; diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx index bf02202ff83b2..85e33402ebe45 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.test.tsx @@ -20,7 +20,9 @@ import { connectorsMock, } from '../../containers/mock'; -import { CaseStatuses, CaseType, SECURITY_SOLUTION_OWNER, StatusAll } from '../../../common'; +import { StatusAll } from '../../../common/ui/types'; +import { CaseStatuses, CaseType } from '../../../common/api'; +import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { getEmptyTagValue } from '../empty_value'; import { useDeleteCases } from '../../containers/use_delete_cases'; import { useGetCases } from '../../containers/use_get_cases'; diff --git a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx index 58c17695d0dfe..b3631155f1b6e 100644 --- a/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/all_cases_list.tsx @@ -13,15 +13,12 @@ import classnames from 'classnames'; import { Case, - CaseStatuses, - CaseType, - CommentRequestAlertType, CaseStatusWithAllStatus, FilterOptions, SortFieldCase, SubCase, - caseStatuses, -} from '../../../common'; +} from '../../../common/ui/types'; +import { CaseStatuses, CaseType, CommentRequestAlertType, caseStatuses } from '../../../common/api'; import { SELECTABLE_MESSAGE_COLLECTIONS } from '../../common/translations'; import { useGetCases } from '../../containers/use_get_cases'; import { usePostComment } from '../../containers/use_post_comment'; diff --git a/x-pack/plugins/cases/public/components/all_cases/columns.tsx b/x-pack/plugins/cases/public/components/all_cases/columns.tsx index c30ddd199fc49..684b9644a7879 100644 --- a/x-pack/plugins/cases/public/components/all_cases/columns.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/columns.tsx @@ -22,16 +22,14 @@ import { import { RIGHT_ALIGNMENT } from '@elastic/eui/lib/services'; import styled from 'styled-components'; +import { Case, DeleteCase, SubCase } from '../../../common/ui/types'; import { CaseStatuses, CaseType, CommentType, CommentRequestAlertType, - DeleteCase, - Case, - SubCase, ActionConnector, -} from '../../../common'; +} from '../../../common/api'; import { getEmptyTagValue } from '../empty_value'; import { FormattedRelativePreferenceDate } from '../formatted_date'; import { CaseDetailsLink } from '../links'; diff --git a/x-pack/plugins/cases/public/components/all_cases/count.tsx b/x-pack/plugins/cases/public/components/all_cases/count.tsx index eb33cf1069a9b..1f6e71c377ee6 100644 --- a/x-pack/plugins/cases/public/components/all_cases/count.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/count.tsx @@ -7,7 +7,7 @@ import React, { FunctionComponent, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { CaseStatuses } from '../../../common'; +import { CaseStatuses } from '../../../common/api'; import { Stats } from '../status'; import { useGetCasesStatus } from '../../containers/use_get_cases_status'; diff --git a/x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx b/x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx index 2b43fbf63095e..4719c2ce3db82 100644 --- a/x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/expanded_row.tsx @@ -10,7 +10,7 @@ import { EuiBasicTable } from '@elastic/eui'; import styled from 'styled-components'; import { Case, SubCase } from '../../containers/types'; import { CasesColumns } from './columns'; -import { AssociationType } from '../../../common'; +import { AssociationType } from '../../../common/api'; type ExpandedRowMap = Record | {}; diff --git a/x-pack/plugins/cases/public/components/all_cases/helpers.ts b/x-pack/plugins/cases/public/components/all_cases/helpers.ts index ca5b2e422c15c..f84f19d3030ae 100644 --- a/x-pack/plugins/cases/public/components/all_cases/helpers.ts +++ b/x-pack/plugins/cases/public/components/all_cases/helpers.ts @@ -6,7 +6,7 @@ */ import { filter } from 'lodash/fp'; -import { AssociationType, CaseStatuses, CaseType } from '../../../common'; +import { AssociationType, CaseStatuses, CaseType } from '../../../common/api'; import { Case, SubCase } from '../../containers/types'; import { statuses } from '../status'; diff --git a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx index 681bb65870c1e..9decb3a58f831 100644 --- a/x-pack/plugins/cases/public/components/all_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/index.test.tsx @@ -16,7 +16,7 @@ import { useGetReporters } from '../../containers/use_get_reporters'; import { useGetActionLicense } from '../../containers/use_get_action_license'; import { useConnectors } from '../../containers/configure/use_connectors'; import { useKibana } from '../../common/lib/kibana'; -import { CaseStatuses } from '../../../common'; +import { CaseStatuses } from '../../../common/api'; import { casesStatus, connectorsMock, useGetCasesMockState } from '../../containers/mock'; import { registerConnectorsToMockActionRegistry } from '../../common/mock/register_connectors'; import { useGetCases } from '../../containers/use_get_cases'; diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.test.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.test.tsx index fb062fe101db5..33eddeccb59b2 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.test.tsx @@ -11,7 +11,7 @@ import { mount } from 'enzyme'; import { AllCasesSelectorModal } from '.'; import { TestProviders } from '../../../common/mock'; import { AllCasesList } from '../all_cases_list'; -import { SECURITY_SOLUTION_OWNER } from '../../../../common'; +import { SECURITY_SOLUTION_OWNER } from '../../../../common/constants'; jest.mock('../../../methods'); jest.mock('../all_cases_list'); diff --git a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx index 227dd88c6f5a2..5db6531d8e140 100644 --- a/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/selector_modal/all_cases_selector_modal.tsx @@ -15,12 +15,8 @@ import { EuiModalHeaderTitle, } from '@elastic/eui'; import styled from 'styled-components'; -import { - Case, - CaseStatusWithAllStatus, - CommentRequestAlertType, - SubCase, -} from '../../../../common'; +import { Case, SubCase, CaseStatusWithAllStatus } from '../../../../common/ui/types'; +import { CommentRequestAlertType } from '../../../../common/api'; import * as i18n from '../../../common/translations'; import { AllCasesList } from '../all_cases_list'; export interface AllCasesSelectorModalProps { diff --git a/x-pack/plugins/cases/public/components/all_cases/status_filter.test.tsx b/x-pack/plugins/cases/public/components/all_cases/status_filter.test.tsx index 5d975c51c6569..5471c03a6f181 100644 --- a/x-pack/plugins/cases/public/components/all_cases/status_filter.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/status_filter.test.tsx @@ -9,7 +9,8 @@ import React from 'react'; import { mount } from 'enzyme'; import { waitFor } from '@testing-library/react'; -import { CaseStatuses, StatusAll } from '../../../common'; +import { StatusAll } from '../../../common/ui/types'; +import { CaseStatuses } from '../../../common/api'; import { StatusFilter } from './status_filter'; const stats = { diff --git a/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx b/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx index bb54fbe410951..71359c2e50582 100644 --- a/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/status_filter.tsx @@ -8,7 +8,7 @@ import React, { memo } from 'react'; import { EuiSuperSelect, EuiSuperSelectOption, EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { Status, statuses } from '../status'; -import { CaseStatusWithAllStatus, StatusAll } from '../../../common'; +import { CaseStatusWithAllStatus, StatusAll } from '../../../common/ui/types'; interface Props { stats: Record; diff --git a/x-pack/plugins/cases/public/components/all_cases/table.tsx b/x-pack/plugins/cases/public/components/all_cases/table.tsx index 40d61007f9056..94a44add3402f 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table.tsx @@ -18,7 +18,7 @@ import styled from 'styled-components'; import { CasesTableUtilityBar } from './utility_bar'; import { LinkButton } from '../links'; -import { AllCases, Case, FilterOptions } from '../../../common'; +import { AllCases, Case, FilterOptions } from '../../../common/ui/types'; import * as i18n from './translations'; import { useCreateCaseNavigation } from '../../common/navigation'; diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx index f71009a37b747..2d14ffe5738ca 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { CaseStatuses } from '../../../common'; +import { CaseStatuses } from '../../../common/api'; import { TestProviders } from '../../common/mock'; import { useGetTags } from '../../containers/use_get_tags'; import { useGetReporters } from '../../containers/use_get_reporters'; diff --git a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx index 47ab4cb210778..e1ed709e0d93f 100644 --- a/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/table_filters.tsx @@ -10,7 +10,8 @@ import { isEqual } from 'lodash/fp'; import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiFieldSearch, EuiFilterGroup } from '@elastic/eui'; -import { CaseStatuses, CaseStatusWithAllStatus, StatusAll } from '../../../common'; +import { StatusAll, CaseStatusWithAllStatus } from '../../../common/ui/types'; +import { CaseStatuses } from '../../../common/api'; import { FilterOptions } from '../../containers/types'; import { useGetTags } from '../../containers/use_get_tags'; import { useGetReporters } from '../../containers/use_get_reporters'; diff --git a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx index 26430482bc067..b6ab44517bb66 100644 --- a/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx +++ b/x-pack/plugins/cases/public/components/all_cases/utility_bar.tsx @@ -15,7 +15,7 @@ import { UtilityBarText, } from '../utility_bar'; import * as i18n from './translations'; -import { AllCases, Case, DeleteCase, FilterOptions } from '../../../common'; +import { AllCases, Case, DeleteCase, FilterOptions } from '../../../common/ui/types'; import { getBulkItems } from '../bulk_actions'; import { isSelectedCasesIncludeCollections } from './helpers'; import { useDeleteCases } from '../../containers/use_delete_cases'; diff --git a/x-pack/plugins/cases/public/components/app/types.ts b/x-pack/plugins/cases/public/components/app/types.ts index 9c825ad95618a..ebe174c095fa7 100644 --- a/x-pack/plugins/cases/public/components/app/types.ts +++ b/x-pack/plugins/cases/public/components/app/types.ts @@ -6,7 +6,7 @@ */ import { MutableRefObject } from 'react'; -import { Ecs, CaseViewRefreshPropInterface } from '../../../common'; +import { Ecs, CaseViewRefreshPropInterface } from '../../../common/ui/types'; import { CasesNavigation } from '../links'; import { CasesTimelineIntegration } from '../timeline_context'; diff --git a/x-pack/plugins/cases/public/components/bulk_actions/index.tsx b/x-pack/plugins/cases/public/components/bulk_actions/index.tsx index 751a45a706ef7..c8dbe2adaca0b 100644 --- a/x-pack/plugins/cases/public/components/bulk_actions/index.tsx +++ b/x-pack/plugins/cases/public/components/bulk_actions/index.tsx @@ -8,7 +8,8 @@ import React from 'react'; import { EuiContextMenuItem } from '@elastic/eui'; -import { CaseStatuses, CaseStatusWithAllStatus } from '../../../common'; +import { CaseStatusWithAllStatus } from '../../../common/ui/types'; +import { CaseStatuses } from '../../../common/api'; import { statuses } from '../status'; import * as i18n from './translations'; import { Case } from '../../containers/types'; diff --git a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx index ada2b61c816db..4cad00535d165 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/actions.tsx @@ -11,7 +11,7 @@ import * as i18n from '../case_view/translations'; import { useDeleteCases } from '../../containers/use_delete_cases'; import { ConfirmDeleteCaseModal } from '../confirm_delete_case'; import { PropertyActions } from '../property_actions'; -import { Case } from '../../../common'; +import { Case } from '../../../common/ui/types'; import { CaseService } from '../../containers/use_get_case_user_actions'; import { useAllCasesNavigation } from '../../common/navigation'; diff --git a/x-pack/plugins/cases/public/components/case_action_bar/helpers.test.ts b/x-pack/plugins/cases/public/components/case_action_bar/helpers.test.ts index ed5832d19b4da..f04ef94405db8 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/helpers.test.ts +++ b/x-pack/plugins/cases/public/components/case_action_bar/helpers.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseStatuses } from '../../../common'; +import { CaseStatuses } from '../../../common/api'; import { basicCase } from '../../containers/mock'; import { getStatusDate, getStatusTitle } from './helpers'; diff --git a/x-pack/plugins/cases/public/components/case_action_bar/helpers.ts b/x-pack/plugins/cases/public/components/case_action_bar/helpers.ts index 35cfdae3abe21..b26c33b0fd009 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/helpers.ts +++ b/x-pack/plugins/cases/public/components/case_action_bar/helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseStatuses } from '../../../common'; +import { CaseStatuses } from '../../../common/api'; import { Case } from '../../containers/types'; import { statuses } from '../status'; diff --git a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx index 9b326f3216084..ac81dfea2fd93 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/index.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/index.tsx @@ -16,7 +16,8 @@ import { EuiFlexItem, EuiIconTip, } from '@elastic/eui'; -import { Case, CaseStatuses, CaseType } from '../../../common'; +import { Case } from '../../../common/ui/types'; +import { CaseStatuses, CaseType } from '../../../common/api'; import * as i18n from '../case_view/translations'; import { FormattedRelativePreferenceDate } from '../formatted_date'; import { Actions } from './actions'; diff --git a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx index 93ecf4df997d2..4a67eada2e00d 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { CaseStatuses } from '../../../common'; +import { CaseStatuses } from '../../../common/api'; import { StatusContextMenu } from './status_context_menu'; describe('SyncAlertsSwitch', () => { diff --git a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx index ab86f589bfdd0..193ef4a708e38 100644 --- a/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx +++ b/x-pack/plugins/cases/public/components/case_action_bar/status_context_menu.tsx @@ -7,7 +7,7 @@ import React, { memo, useCallback, useMemo, useState } from 'react'; import { EuiPopover, EuiContextMenuPanel, EuiContextMenuItem } from '@elastic/eui'; -import { caseStatuses, CaseStatuses } from '../../../common'; +import { caseStatuses, CaseStatuses } from '../../../common/api'; import { Status } from '../status'; import { CHANGE_STATUS } from '../all_cases/translations'; diff --git a/x-pack/plugins/cases/public/components/case_view/helpers.test.tsx b/x-pack/plugins/cases/public/components/case_view/helpers.test.tsx index bf5a9fe5d0a22..e398c5edad145 100644 --- a/x-pack/plugins/cases/public/components/case_view/helpers.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/helpers.test.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { AssociationType, CommentType, SECURITY_SOLUTION_OWNER } from '../../../common'; +import { AssociationType, CommentType } from '../../../common/api'; +import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { Comment } from '../../containers/types'; import { getManualAlertIdsWithNoRuleId } from './helpers'; diff --git a/x-pack/plugins/cases/public/components/case_view/helpers.ts b/x-pack/plugins/cases/public/components/case_view/helpers.ts index ab26b132e0489..7f3924ef2564c 100644 --- a/x-pack/plugins/cases/public/components/case_view/helpers.ts +++ b/x-pack/plugins/cases/public/components/case_view/helpers.ts @@ -6,7 +6,7 @@ */ import { isEmpty } from 'lodash'; -import { CommentType } from '../../../common'; +import { CommentType } from '../../../common/api'; import { Comment } from '../../containers/types'; export const getManualAlertIdsWithNoRuleId = (comments: Comment[]): string[] => { diff --git a/x-pack/plugins/cases/public/components/case_view/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/index.test.tsx index aaf4928703896..daa3ad4416200 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.test.tsx @@ -27,7 +27,7 @@ import { useGetCaseUserActions } from '../../containers/use_get_case_user_action import { useConnectors } from '../../containers/configure/use_connectors'; import { connectorsMock } from '../../containers/configure/mock'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; -import { CaseType, ConnectorTypes } from '../../../common'; +import { CaseType, ConnectorTypes } from '../../../common/api'; import { useKibana } from '../../common/lib/kibana'; jest.mock('../../containers/use_update_case'); diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 2b78c31242ba6..c436547c9e2bd 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -9,15 +9,8 @@ import React, { useCallback, useEffect, useMemo, useState, useRef, MutableRefObj import styled from 'styled-components'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingContent, EuiLoadingSpinner } from '@elastic/eui'; -import { - CaseStatuses, - CaseAttributes, - CaseType, - Case, - CaseConnector, - Ecs, - CaseViewRefreshPropInterface, -} from '../../../common'; +import { Case, Ecs, CaseViewRefreshPropInterface } from '../../../common/ui/types'; +import { CaseStatuses, CaseAttributes, CaseType, CaseConnector } from '../../../common/api'; import { HeaderPage } from '../header_page'; import { EditableTitle } from '../header_page/editable_title'; import { TagList } from '../tag_list'; diff --git a/x-pack/plugins/cases/public/components/cases_context/index.tsx b/x-pack/plugins/cases/public/components/cases_context/index.tsx index 588bda245b044..ecc5719cb81b7 100644 --- a/x-pack/plugins/cases/public/components/cases_context/index.tsx +++ b/x-pack/plugins/cases/public/components/cases_context/index.tsx @@ -7,7 +7,8 @@ import React, { useState, useEffect } from 'react'; import { merge } from 'lodash'; -import { CasesContextValue, DEFAULT_FEATURES } from '../../../common'; +import { CasesContextValue } from '../../../common/ui/types'; +import { DEFAULT_FEATURES } from '../../../common/constants'; import { DEFAULT_BASE_PATH } from '../../common/navigation'; import { useApplication } from './use_application'; diff --git a/x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx index 983e32ba508fb..49ac373724336 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/__mock__/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { ActionTypeConnector, ConnectorTypes } from '../../../../common'; +import { ActionTypeConnector, ConnectorTypes } from '../../../../common/api'; import { ActionConnector } from '../../../containers/configure/types'; import { UseConnectorsResponse } from '../../../containers/configure/use_connectors'; import { ReturnUseCaseConfigure } from '../../../containers/configure/use_configure'; diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx index 9bbddfae2f9bd..7a6bca518ac3e 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.test.tsx @@ -13,7 +13,7 @@ import { Connectors, Props } from './connectors'; import { TestProviders } from '../../common/mock'; import { ConnectorsDropdown } from './connectors_dropdown'; import { connectors, actionTypes } from './__mock__'; -import { ConnectorTypes } from '../../../common'; +import { ConnectorTypes } from '../../../common/api'; import { useKibana } from '../../common/lib/kibana'; import { registerConnectorsToMockActionRegistry } from '../../common/mock/register_connectors'; diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx index b7bf7c322f76e..11026acde2bf6 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors.tsx @@ -21,7 +21,7 @@ import * as i18n from './translations'; import { ActionConnector, CaseConnectorMapping } from '../../containers/configure/types'; import { Mapping } from './mapping'; -import { ActionTypeConnector, ConnectorTypes } from '../../../common'; +import { ActionTypeConnector, ConnectorTypes } from '../../../common/api'; import { DeprecatedCallout } from '../connectors/deprecated_callout'; import { isDeprecatedConnector } from '../utils'; diff --git a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx index c7ce3c5b3c4b6..af518e3c773b6 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/connectors_dropdown.tsx @@ -9,7 +9,7 @@ import React, { useMemo } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiIcon, EuiIconTip, EuiSuperSelect } from '@elastic/eui'; import styled from 'styled-components'; -import { ConnectorTypes } from '../../../common'; +import { ConnectorTypes } from '../../../common/api'; import { ActionConnector } from '../../containers/configure/types'; import * as i18n from './translations'; import { useKibana } from '../../common/lib/kibana'; diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx index 55983df8f347d..918252369c26b 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.test.tsx @@ -26,7 +26,7 @@ import { useConnectorsResponse, useActionTypesResponse, } from './__mock__'; -import { ConnectorTypes } from '../../../common'; +import { ConnectorTypes } from '../../../common/api'; import { actionTypeRegistryMock } from '../../../../triggers_actions_ui/public/application/action_type_registry.mock'; jest.mock('../../common/lib/kibana'); diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx index 6b19fd911d10d..44c1979aa0fda 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx @@ -11,7 +11,7 @@ import styled, { css } from 'styled-components'; import { FormattedMessage } from '@kbn/i18n-react'; import { EuiCallOut, EuiLink } from '@elastic/eui'; -import { SUPPORTED_CONNECTORS } from '../../../common'; +import { SUPPORTED_CONNECTORS } from '../../../common/constants'; import { useKibana } from '../../common/lib/kibana'; import { useConnectors } from '../../containers/configure/use_connectors'; import { useActionTypes } from '../../containers/configure/use_action_types'; diff --git a/x-pack/plugins/cases/public/components/configure_cases/utils.ts b/x-pack/plugins/cases/public/components/configure_cases/utils.ts index 6597417b5068a..d7de06e9c5aee 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/utils.ts +++ b/x-pack/plugins/cases/public/components/configure_cases/utils.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConnectorTypeFields, ConnectorTypes } from '../../../common'; +import { ConnectorTypeFields, ConnectorTypes } from '../../../common/api'; import { CaseField, ActionType, diff --git a/x-pack/plugins/cases/public/components/connector_selector/form.tsx b/x-pack/plugins/cases/public/components/connector_selector/form.tsx index 71a65ae030d9d..05db3474fdb99 100644 --- a/x-pack/plugins/cases/public/components/connector_selector/form.tsx +++ b/x-pack/plugins/cases/public/components/connector_selector/form.tsx @@ -12,7 +12,7 @@ import styled from 'styled-components'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../common/shared_imports'; import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown'; -import { ActionConnector } from '../../../common'; +import { ActionConnector } from '../../../common/api'; interface ConnectorSelectorProps { connectors: ActionConnector[]; diff --git a/x-pack/plugins/cases/public/components/connectors/card.test.tsx b/x-pack/plugins/cases/public/components/connectors/card.test.tsx index 384442814ffef..7a07e87a1da4c 100644 --- a/x-pack/plugins/cases/public/components/connectors/card.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/card.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { ConnectorTypes } from '../../../common'; +import { ConnectorTypes } from '../../../common/api'; import { useKibana } from '../../common/lib/kibana'; import { connectors } from '../configure_cases/__mock__'; import { ConnectorCard } from './card'; diff --git a/x-pack/plugins/cases/public/components/connectors/card.tsx b/x-pack/plugins/cases/public/components/connectors/card.tsx index ec4b52c54f707..9870c77fda743 100644 --- a/x-pack/plugins/cases/public/components/connectors/card.tsx +++ b/x-pack/plugins/cases/public/components/connectors/card.tsx @@ -9,7 +9,7 @@ import React, { memo, useMemo } from 'react'; import { EuiCard, EuiFlexGroup, EuiFlexItem, EuiIcon, EuiLoadingSpinner } from '@elastic/eui'; import styled from 'styled-components'; -import { ConnectorTypes } from '../../../common'; +import { ConnectorTypes } from '../../../common/api'; import { useKibana } from '../../common/lib/kibana'; import { getConnectorIcon } from '../utils'; diff --git a/x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx b/x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx index a330ae339b338..7cd9b5f6a367c 100644 --- a/x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/case/alert_fields.tsx @@ -12,7 +12,7 @@ import styled from 'styled-components'; import { EuiCallOut, EuiSpacer } from '@elastic/eui'; import { ActionParamsProps } from '../../../../../triggers_actions_ui/public/types'; -import { CommentType } from '../../../../common'; +import { CommentType } from '../../../../common/api'; import { CaseActionParams } from './types'; import { ExistingCase } from './existing_case'; diff --git a/x-pack/plugins/cases/public/components/connectors/fields_form.tsx b/x-pack/plugins/cases/public/components/connectors/fields_form.tsx index 062695fa41cc2..56c56436c08c7 100644 --- a/x-pack/plugins/cases/public/components/connectors/fields_form.tsx +++ b/x-pack/plugins/cases/public/components/connectors/fields_form.tsx @@ -11,7 +11,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; import { CaseActionConnector } from '../types'; import { ConnectorFieldsProps } from './types'; import { getCaseConnectors } from '.'; -import { ConnectorTypeFields } from '../../../common'; +import { ConnectorTypeFields } from '../../../common/api'; interface Props extends Omit, 'connector'> { connector: CaseActionConnector | null; diff --git a/x-pack/plugins/cases/public/components/connectors/index.ts b/x-pack/plugins/cases/public/components/connectors/index.ts index 3aa10c56dd8e9..0d5e33a818d3a 100644 --- a/x-pack/plugins/cases/public/components/connectors/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/index.ts @@ -17,7 +17,7 @@ import { ServiceNowSIRFieldsType, ResilientFieldsType, SwimlaneFieldsType, -} from '../../../common'; +} from '../../../common/api'; export { getActionType as getCaseConnectorUi } from './case'; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/jira/case_fields.tsx index 6aff81f380015..b9326a08330cd 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/case_fields.tsx @@ -10,7 +10,7 @@ import { map } from 'lodash/fp'; import { EuiFormRow, EuiSelect, EuiFlexGroup, EuiFlexItem, EuiSpacer } from '@elastic/eui'; import * as i18n from './translations'; -import { ConnectorTypes, JiraFieldsType } from '../../../../common'; +import { ConnectorTypes, JiraFieldsType } from '../../../../common/api'; import { useKibana } from '../../../common/lib/kibana'; import { ConnectorFieldsProps } from '../types'; import { useGetIssueTypes } from './use_get_issue_types'; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/index.ts b/x-pack/plugins/cases/public/components/connectors/jira/index.ts index d59d20177c14d..afb53ffcb87cf 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/jira/index.ts @@ -8,7 +8,7 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { ConnectorTypes, JiraFieldsType } from '../../../../common'; +import { ConnectorTypes, JiraFieldsType } from '../../../../common/api'; import * as i18n from './translations'; export * from './types'; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx b/x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx index 61866d126dfd7..a9ed87fa81346 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/search_issues.tsx @@ -9,7 +9,7 @@ import React, { useMemo, useEffect, useCallback, useState, memo } from 'react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; import { useKibana } from '../../../common/lib/kibana'; -import { ActionConnector } from '../../../../common'; +import { ActionConnector } from '../../../../common/api'; import { useGetIssues } from './use_get_issues'; import { useGetSingleIssue } from './use_get_single_issue'; import * as i18n from './translations'; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.tsx index d762c9d3aaf20..f3d14f02ca1f3 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_fields_by_issue_type.tsx @@ -7,7 +7,7 @@ import { useState, useEffect, useRef } from 'react'; import { HttpSetup, IToasts } from 'kibana/public'; -import { ActionConnector } from '../../../../common'; +import { ActionConnector } from '../../../../common/api'; import { getFieldsByIssueType } from './api'; import { Fields } from './types'; import * as i18n from './translations'; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.tsx index 6f409f1ddef8d..6322b59527e4e 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issue_types.tsx @@ -7,7 +7,7 @@ import { useState, useEffect, useRef } from 'react'; import { HttpSetup, IToasts } from 'kibana/public'; -import { ActionConnector } from '../../../../common'; +import { ActionConnector } from '../../../../common/api'; import { getIssueTypes } from './api'; import { IssueTypes } from './types'; import * as i18n from './translations'; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.tsx index e4b6f5e4dea01..f4ab31c9daa2d 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_issues.tsx @@ -8,7 +8,7 @@ import { isEmpty, debounce } from 'lodash/fp'; import { useState, useEffect, useRef } from 'react'; import { HttpSetup, ToastsApi } from 'kibana/public'; -import { ActionConnector } from '../../../../common'; +import { ActionConnector } from '../../../../common/api'; import { getIssues } from './api'; import { Issues } from './types'; import * as i18n from './translations'; diff --git a/x-pack/plugins/cases/public/components/connectors/jira/use_get_single_issue.tsx b/x-pack/plugins/cases/public/components/connectors/jira/use_get_single_issue.tsx index e26940a40d39f..857b07e41d2f2 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/use_get_single_issue.tsx +++ b/x-pack/plugins/cases/public/components/connectors/jira/use_get_single_issue.tsx @@ -7,7 +7,7 @@ import { useState, useEffect, useRef } from 'react'; import { HttpSetup, ToastsApi } from 'kibana/public'; -import { ActionConnector } from '../../../../common'; +import { ActionConnector } from '../../../../common/api'; import { getIssue } from './api'; import { Issue } from './types'; import * as i18n from './translations'; diff --git a/x-pack/plugins/cases/public/components/connectors/mock.ts b/x-pack/plugins/cases/public/components/connectors/mock.ts index 663b397e6f4fe..2882622b29269 100644 --- a/x-pack/plugins/cases/public/components/connectors/mock.ts +++ b/x-pack/plugins/cases/public/components/connectors/mock.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SwimlaneConnectorType } from '../../../common'; +import { SwimlaneConnectorType } from '../../../common/api'; export const connector = { id: '123', diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/resilient/case_fields.tsx index 44f06f92093dd..9dc76fb48cf17 100644 --- a/x-pack/plugins/cases/public/components/connectors/resilient/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/resilient/case_fields.tsx @@ -21,7 +21,7 @@ import { useGetIncidentTypes } from './use_get_incident_types'; import { useGetSeverity } from './use_get_severity'; import * as i18n from './translations'; -import { ConnectorTypes, ResilientFieldsType } from '../../../../common'; +import { ConnectorTypes, ResilientFieldsType } from '../../../../common/api'; import { ConnectorCard } from '../card'; const ResilientFieldsComponent: React.FunctionComponent> = diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/index.ts b/x-pack/plugins/cases/public/components/connectors/resilient/index.ts index 8a429c0dea091..0da7448e62a65 100644 --- a/x-pack/plugins/cases/public/components/connectors/resilient/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/resilient/index.ts @@ -8,7 +8,7 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { ConnectorTypes, ResilientFieldsType } from '../../../../common'; +import { ConnectorTypes, ResilientFieldsType } from '../../../../common/api'; import * as i18n from './translations'; export * from './types'; diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.tsx b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.tsx index 530b56de8796d..588e2ee715a88 100644 --- a/x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.tsx +++ b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_incident_types.tsx @@ -7,7 +7,7 @@ import { useState, useEffect, useRef } from 'react'; import { HttpSetup, ToastsApi } from 'kibana/public'; -import { ActionConnector } from '../../../../common'; +import { ActionConnector } from '../../../../common/api'; import { getIncidentTypes } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.tsx b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.tsx index 8753e3926ffe5..1d647ca1848fe 100644 --- a/x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.tsx +++ b/x-pack/plugins/cases/public/components/connectors/resilient/use_get_severity.tsx @@ -7,7 +7,7 @@ import { useState, useEffect, useRef } from 'react'; import { HttpSetup, ToastsApi } from 'kibana/public'; -import { ActionConnector } from '../../../../common'; +import { ActionConnector } from '../../../../common/api'; import { getSeverity } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts index 88afd902ccf60..1c466d08e9bcb 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts @@ -12,7 +12,7 @@ import { ConnectorTypes, ServiceNowITSMFieldsType, ServiceNowSIRFieldsType, -} from '../../../../common'; +} from '../../../../common/api'; import * as i18n from './translations'; export const getServiceNowITSMCaseConnector = (): CaseConnector => ({ diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx index e24b25065a1c8..521b8609b4eac 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_itsm_case_fields.tsx @@ -10,7 +10,7 @@ import { EuiFormRow, EuiSelect, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@el import * as i18n from './translations'; import { ConnectorFieldsProps } from '../types'; -import { ConnectorTypes, ServiceNowITSMFieldsType } from '../../../../common'; +import { ConnectorTypes, ServiceNowITSMFieldsType } from '../../../../common/api'; import { useKibana } from '../../../common/lib/kibana'; import { ConnectorCard } from '../card'; import { useGetChoices } from './use_get_choices'; diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx index d502b7382664b..095393adb77cb 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/servicenow_sir_case_fields.tsx @@ -8,7 +8,7 @@ import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { EuiFormRow, EuiSelect, EuiFlexGroup, EuiFlexItem, EuiCheckbox } from '@elastic/eui'; -import { ConnectorTypes, ServiceNowSIRFieldsType } from '../../../../common'; +import { ConnectorTypes, ServiceNowSIRFieldsType } from '../../../../common/api'; import { useKibana } from '../../../common/lib/kibana'; import { ConnectorFieldsProps } from '../types'; import { ConnectorCard } from '../card'; diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx index 9f88da9f35eb5..950b17d6f784f 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.test.tsx @@ -8,7 +8,7 @@ import { renderHook } from '@testing-library/react-hooks'; import { useKibana } from '../../../common/lib/kibana'; -import { ActionConnector } from '../../../../common'; +import { ActionConnector } from '../../../../common/api'; import { choices } from '../mock'; import { useGetChoices, UseGetChoices, UseGetChoicesProps } from './use_get_choices'; import * as api from './api'; diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.tsx b/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.tsx index 2c6181dd08eb1..fa8e648a0981e 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.tsx +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/use_get_choices.tsx @@ -7,7 +7,7 @@ import { useState, useEffect, useRef } from 'react'; import { HttpSetup, IToasts } from 'kibana/public'; -import { ActionConnector } from '../../../../common'; +import { ActionConnector } from '../../../../common/api'; import { getChoices } from './api'; import { Choice } from './types'; import * as i18n from './translations'; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx index 1a035d92611bd..cca74b83ddb80 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { render, screen } from '@testing-library/react'; -import { SwimlaneConnectorType } from '../../../../common'; +import { SwimlaneConnectorType } from '../../../../common/api'; import Fields from './case_fields'; import * as i18n from './translations'; import { swimlaneConnector as connector } from '../mock'; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx index b6370504edbb6..a7e584f7c22e2 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -9,7 +9,7 @@ import React, { useMemo } from 'react'; import { EuiCallOut } from '@elastic/eui'; import * as i18n from './translations'; -import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common'; +import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common/api'; import { ConnectorFieldsProps } from '../types'; import { ConnectorCard } from '../card'; import { connectorValidator } from './validator'; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts index bd2eaae9e0174..394b93b961004 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts @@ -8,7 +8,7 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common'; +import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common/api'; import * as i18n from './translations'; export const getCaseConnector = (): CaseConnector => { diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts index 552d988c26330..c8cb142232972 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SwimlaneConnectorType } from '../../../../common'; +import { SwimlaneConnectorType } from '../../../../common/api'; import { swimlaneConnector as connector } from '../mock'; import { isAnyRequiredFieldNotSet, connectorValidator } from './validator'; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts index 4ead75e5854f9..90d9946d4adb8 100644 --- a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { SwimlaneConnectorType } from '../../../../common'; +import { SwimlaneConnectorType } from '../../../../common/api'; import { ValidationConfig } from '../../../common/shared_imports'; import { CaseActionConnector } from '../../types'; diff --git a/x-pack/plugins/cases/public/components/connectors/types.ts b/x-pack/plugins/cases/public/components/connectors/types.ts index 8bc978152b796..66e5d519ac752 100644 --- a/x-pack/plugins/cases/public/components/connectors/types.ts +++ b/x-pack/plugins/cases/public/components/connectors/types.ts @@ -12,10 +12,10 @@ import { ActionType as ThirdPartySupportedActions, CaseField, ConnectorTypeFields, -} from '../../../common'; +} from '../../../common/api'; import { CaseActionConnector } from '../types'; -export type { ThirdPartyField as AllThirdPartyFields } from '../../../common'; +export type { ThirdPartyField as AllThirdPartyFields } from '../../../common/api'; export interface ThirdPartyField { label: string; diff --git a/x-pack/plugins/cases/public/components/create/connector.tsx b/x-pack/plugins/cases/public/components/create/connector.tsx index 84695d4011f55..aa0eb024a3b0d 100644 --- a/x-pack/plugins/cases/public/components/create/connector.tsx +++ b/x-pack/plugins/cases/public/components/create/connector.tsx @@ -8,7 +8,7 @@ import React, { memo, useCallback, useMemo, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { ConnectorTypes, ActionConnector } from '../../../common'; +import { ConnectorTypes, ActionConnector } from '../../../common/api'; import { UseField, useFormData, diff --git a/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx b/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx index e77f72929ecd8..eeebcb29ed2a9 100644 --- a/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx +++ b/x-pack/plugins/cases/public/components/create/flyout/create_case_flyout.tsx @@ -10,7 +10,7 @@ import styled, { createGlobalStyle } from 'styled-components'; import { EuiFlyout, EuiFlyoutHeader, EuiTitle, EuiFlyoutBody } from '@elastic/eui'; import * as i18n from '../translations'; -import { Case } from '../../../../common'; +import { Case } from '../../../../common/ui/types'; import { CreateCaseForm } from '../form'; export interface CreateCaseFlyoutProps { diff --git a/x-pack/plugins/cases/public/components/create/form.tsx b/x-pack/plugins/cases/public/components/create/form.tsx index 2c775cb5fd86d..396c72fa54c0d 100644 --- a/x-pack/plugins/cases/public/components/create/form.tsx +++ b/x-pack/plugins/cases/public/components/create/form.tsx @@ -23,7 +23,7 @@ import { Tags } from './tags'; import { Connector } from './connector'; import * as i18n from './translations'; import { SyncAlertsToggle } from './sync_alerts_toggle'; -import { ActionConnector, CaseType } from '../../../common'; +import { ActionConnector, CaseType } from '../../../common/api'; import { Case } from '../../containers/types'; import { CasesTimelineIntegration, CasesTimelineIntegrationProvider } from '../timeline_context'; import { InsertTimeline } from '../insert_timeline'; diff --git a/x-pack/plugins/cases/public/components/create/form_context.test.tsx b/x-pack/plugins/cases/public/components/create/form_context.test.tsx index 15ffa5376e418..6e406386b48ef 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.test.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.test.tsx @@ -10,7 +10,7 @@ import { mount, ReactWrapper } from 'enzyme'; import { act, waitFor } from '@testing-library/react'; import { EuiComboBox, EuiComboBoxOptionOption } from '@elastic/eui'; -import { ConnectorTypes } from '../../../common'; +import { ConnectorTypes } from '../../../common/api'; import { useKibana } from '../../common/lib/kibana'; import { TestProviders } from '../../common/mock'; import { usePostCase } from '../../containers/use_post_case'; diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index a513056ba31a5..b76a4640507be 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -14,7 +14,7 @@ import { usePostPushToService } from '../../containers/use_post_push_to_service' import { useConnectors } from '../../containers/configure/use_connectors'; import { Case } from '../../containers/types'; -import { CaseType } from '../../../common'; +import { CaseType } from '../../../common/api'; import { UsePostComment, usePostComment } from '../../containers/use_post_comment'; import { useCasesContext } from '../cases_context/use_cases_context'; import { useCasesFeatures } from '../cases_context/use_cases_features'; diff --git a/x-pack/plugins/cases/public/components/create/mock.ts b/x-pack/plugins/cases/public/components/create/mock.ts index fb00f114f480c..321194826e484 100644 --- a/x-pack/plugins/cases/public/components/create/mock.ts +++ b/x-pack/plugins/cases/public/components/create/mock.ts @@ -5,12 +5,8 @@ * 2.0. */ -import { - CasePostRequest, - CaseType, - ConnectorTypes, - SECURITY_SOLUTION_OWNER, -} from '../../../common'; +import { CasePostRequest, CaseType, ConnectorTypes } from '../../../common/api'; +import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { choices } from '../connectors/mock'; export const sampleTags = ['coke', 'pepsi']; diff --git a/x-pack/plugins/cases/public/components/create/schema.tsx b/x-pack/plugins/cases/public/components/create/schema.tsx index 57cf2f63a3fd2..435ebefd943ca 100644 --- a/x-pack/plugins/cases/public/components/create/schema.tsx +++ b/x-pack/plugins/cases/public/components/create/schema.tsx @@ -5,7 +5,8 @@ * 2.0. */ -import { CasePostRequest, ConnectorTypeFields, MAX_TITLE_LENGTH } from '../../../common'; +import { CasePostRequest, ConnectorTypeFields } from '../../../common/api'; +import { MAX_TITLE_LENGTH } from '../../../common/constants'; import { FIELD_TYPES, fieldValidators, diff --git a/x-pack/plugins/cases/public/components/edit_connector/helpers.test.ts b/x-pack/plugins/cases/public/components/edit_connector/helpers.test.ts index e20d6b37258bc..e1cc8cefcafb8 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/helpers.test.ts +++ b/x-pack/plugins/cases/public/components/edit_connector/helpers.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseUserActionConnector, ConnectorTypes } from '../../../common'; +import { CaseUserActionConnector, ConnectorTypes } from '../../../common/api'; import { CaseUserActions } from '../../containers/types'; import { getConnectorFieldsFromUserActions } from './helpers'; diff --git a/x-pack/plugins/cases/public/components/edit_connector/helpers.ts b/x-pack/plugins/cases/public/components/edit_connector/helpers.ts index b97035c458aca..c6027bb7b570e 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/helpers.ts +++ b/x-pack/plugins/cases/public/components/edit_connector/helpers.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConnectorTypeFields } from '../../../common'; +import { ConnectorTypeFields } from '../../../common/api'; import { CaseUserActions } from '../../containers/types'; import { parseStringAsConnector } from '../../common/user_actions'; diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx index 25cb17cdd8c98..efee35a8ba134 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx @@ -21,7 +21,8 @@ import styled from 'styled-components'; import { isEmpty, noop } from 'lodash/fp'; import { FieldConfig, Form, UseField, useForm } from '../../common/shared_imports'; -import { ActionConnector, Case, ConnectorTypeFields } from '../../../common'; +import { Case } from '../../../common/ui/types'; +import { ActionConnector, ConnectorTypeFields } from '../../../common/api'; import { ConnectorSelector } from '../connector_selector/form'; import { ConnectorFieldsForm } from '../connectors/fields_form'; import { CaseUserActions } from '../../containers/types'; diff --git a/x-pack/plugins/cases/public/components/header_page/editable_title.tsx b/x-pack/plugins/cases/public/components/header_page/editable_title.tsx index 954ee0b031e51..43b210b5a5afb 100644 --- a/x-pack/plugins/cases/public/components/header_page/editable_title.tsx +++ b/x-pack/plugins/cases/public/components/header_page/editable_title.tsx @@ -19,7 +19,7 @@ import { EuiFormRow, } from '@elastic/eui'; -import { MAX_TITLE_LENGTH } from '../../../common'; +import { MAX_TITLE_LENGTH } from '../../../common/constants'; import * as i18n from './translations'; import { Title } from './title'; diff --git a/x-pack/plugins/cases/public/components/status/button.test.tsx b/x-pack/plugins/cases/public/components/status/button.test.tsx index a4d4a53ff4a62..32df83b4b2ddf 100644 --- a/x-pack/plugins/cases/public/components/status/button.test.tsx +++ b/x-pack/plugins/cases/public/components/status/button.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { CaseStatuses } from '../../../common'; +import { CaseStatuses } from '../../../common/api'; import { StatusActionButton } from './button'; describe('StatusActionButton', () => { diff --git a/x-pack/plugins/cases/public/components/status/button.tsx b/x-pack/plugins/cases/public/components/status/button.tsx index 675d83c759bc7..c9dcd509c1002 100644 --- a/x-pack/plugins/cases/public/components/status/button.tsx +++ b/x-pack/plugins/cases/public/components/status/button.tsx @@ -8,7 +8,7 @@ import React, { memo, useCallback, useMemo } from 'react'; import { EuiButton } from '@elastic/eui'; -import { CaseStatuses, caseStatuses } from '../../../common'; +import { CaseStatuses, caseStatuses } from '../../../common/api'; import { statuses } from './config'; interface Props { diff --git a/x-pack/plugins/cases/public/components/status/config.ts b/x-pack/plugins/cases/public/components/status/config.ts index 0202507aa3721..6c5ff18ad977a 100644 --- a/x-pack/plugins/cases/public/components/status/config.ts +++ b/x-pack/plugins/cases/public/components/status/config.ts @@ -4,7 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { CaseStatuses, StatusAll } from '../../../common'; +import { StatusAll } from '../../../common/ui/types'; +import { CaseStatuses } from '../../../common/api'; import * as i18n from './translations'; import { AllCaseStatus, Statuses } from './types'; diff --git a/x-pack/plugins/cases/public/components/status/stats.test.tsx b/x-pack/plugins/cases/public/components/status/stats.test.tsx index b2da828da77b0..ea0f54bf8055b 100644 --- a/x-pack/plugins/cases/public/components/status/stats.test.tsx +++ b/x-pack/plugins/cases/public/components/status/stats.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { CaseStatuses } from '../../../common'; +import { CaseStatuses } from '../../../common/api'; import { Stats } from './stats'; describe('Stats', () => { diff --git a/x-pack/plugins/cases/public/components/status/stats.tsx b/x-pack/plugins/cases/public/components/status/stats.tsx index 071ea43746fdc..98720ad75a656 100644 --- a/x-pack/plugins/cases/public/components/status/stats.tsx +++ b/x-pack/plugins/cases/public/components/status/stats.tsx @@ -7,7 +7,7 @@ import React, { memo, useMemo } from 'react'; import { EuiDescriptionList, EuiLoadingSpinner } from '@elastic/eui'; -import { CaseStatuses } from '../../../common'; +import { CaseStatuses } from '../../../common/api'; import { statuses } from './config'; export interface Props { diff --git a/x-pack/plugins/cases/public/components/status/status.test.tsx b/x-pack/plugins/cases/public/components/status/status.test.tsx index a685256741c43..9ea71dfd52393 100644 --- a/x-pack/plugins/cases/public/components/status/status.test.tsx +++ b/x-pack/plugins/cases/public/components/status/status.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { CaseStatuses } from '../../../common'; +import { CaseStatuses } from '../../../common/api'; import { Status } from './status'; describe('Stats', () => { diff --git a/x-pack/plugins/cases/public/components/status/status.tsx b/x-pack/plugins/cases/public/components/status/status.tsx index 3c186313a151a..47c30a7761264 100644 --- a/x-pack/plugins/cases/public/components/status/status.tsx +++ b/x-pack/plugins/cases/public/components/status/status.tsx @@ -11,7 +11,7 @@ import { EuiBadge } from '@elastic/eui'; import { allCaseStatus, statuses } from './config'; import * as i18n from './translations'; -import { CaseStatusWithAllStatus, StatusAll } from '../../../common'; +import { CaseStatusWithAllStatus, StatusAll } from '../../../common/ui/types'; interface Props { disabled?: boolean; diff --git a/x-pack/plugins/cases/public/components/status/types.ts b/x-pack/plugins/cases/public/components/status/types.ts index f8115b8d692b3..0b4a1184633e1 100644 --- a/x-pack/plugins/cases/public/components/status/types.ts +++ b/x-pack/plugins/cases/public/components/status/types.ts @@ -6,7 +6,8 @@ */ import { EuiIconType } from '@elastic/eui/src/components/icon/icon'; -import { CaseStatuses, StatusAllType } from '../../../common'; +import { StatusAllType } from '../../../common/ui/types'; +import { CaseStatuses } from '../../../common/api'; export type AllCaseStatus = Record; diff --git a/x-pack/plugins/cases/public/components/types.ts b/x-pack/plugins/cases/public/components/types.ts index 71c846eb922d7..6d72a74fa5d81 100644 --- a/x-pack/plugins/cases/public/components/types.ts +++ b/x-pack/plugins/cases/public/components/types.ts @@ -5,4 +5,4 @@ * 2.0. */ -export type { CaseActionConnector } from '../../common'; +export type { CaseActionConnector } from '../../common/ui/types'; diff --git a/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx index 7c88d9425bd7e..9ee4a78b7d817 100644 --- a/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx +++ b/x-pack/plugins/cases/public/components/use_create_case_modal/create_case_modal.test.tsx @@ -10,7 +10,7 @@ import { mount } from 'enzyme'; import { CreateCaseModal } from './create_case_modal'; import { TestProviders } from '../../common/mock'; -import { SECURITY_SOLUTION_OWNER } from '../../../common'; +import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { CreateCase } from '../create'; jest.mock('../create', () => ({ diff --git a/x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx b/x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx index 2e9f3559d98d0..afae43b462a5b 100644 --- a/x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx +++ b/x-pack/plugins/cases/public/components/use_create_case_modal/index.tsx @@ -6,7 +6,7 @@ */ import React, { useState, useCallback, useMemo } from 'react'; -import { Case } from '../../../common'; +import { Case } from '../../../common/ui/types'; import { CreateCaseModal } from './create_case_modal'; export interface UseCreateCaseModalProps { diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx index 19c0bf58de7bf..cf81b5195a961 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/index.test.tsx @@ -12,7 +12,7 @@ import { render, screen } from '@testing-library/react'; import '../../common/mock/match_media'; import { usePushToService, ReturnUsePushToService, UsePushToService } from '.'; import { TestProviders } from '../../common/mock'; -import { CaseStatuses, ConnectorTypes } from '../../../common'; +import { CaseStatuses, ConnectorTypes } from '../../../common/api'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; import { basicPush, actionLicenses } from '../../containers/mock'; import { useGetActionLicense } from '../../containers/use_get_action_license'; diff --git a/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx b/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx index c1ba6f1fbeb25..b079a9d3d1b3d 100644 --- a/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx +++ b/x-pack/plugins/cases/public/components/use_push_to_service/index.tsx @@ -19,7 +19,8 @@ import { getCaseClosedInfo, } from './helpers'; import * as i18n from './translations'; -import { Case, CaseConnector, ActionConnector, CaseStatuses } from '../../../common'; +import { Case } from '../../../common/ui/types'; +import { CaseConnector, ActionConnector, CaseStatuses } from '../../../common/api'; import { CaseServices } from '../../containers/use_get_case_user_actions'; import { ErrorMessage } from './callout/types'; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/helpers.test.tsx b/x-pack/plugins/cases/public/components/user_action_tree/helpers.test.tsx index 841f0d36bbf17..5c0d64857d0cb 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/helpers.test.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/helpers.test.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { mount } from 'enzyme'; -import { CaseStatuses, ConnectorTypes } from '../../../common'; +import { CaseStatuses, ConnectorTypes } from '../../../common/api'; import { basicPush, getUserAction } from '../../containers/mock'; import { getLabelTitle, diff --git a/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx b/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx index a7b4624835882..6dd4032d7cdce 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/helpers.tsx @@ -16,15 +16,15 @@ import { import React, { useContext } from 'react'; import classNames from 'classnames'; import { ThemeContext } from 'styled-components'; +import { Comment } from '../../../common/ui/types'; import { CaseFullExternalService, ActionConnector, CaseStatuses, CommentType, - Comment, CommentRequestActionsType, noneConnectorId, -} from '../../../common'; +} from '../../../common/api'; import { CaseUserActions } from '../../containers/types'; import { CaseServices } from '../../containers/use_get_case_user_actions'; import { parseStringAsConnector, parseStringAsExternalService } from '../../common/user_actions'; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx index e9cd556706646..94d0ca413192a 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.test.tsx @@ -22,7 +22,7 @@ import { } from '../../containers/mock'; import { UserActionTree } from '.'; import { TestProviders } from '../../common/mock'; -import { Ecs } from '../../../common'; +import { Ecs } from '../../../common/ui/types'; const fetchUserActions = jest.fn(); const onUpdateField = jest.fn(); diff --git a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx index 6197303a8d7ce..b5b76f36013c5 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/index.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/index.tsx @@ -25,16 +25,14 @@ import * as i18n from './translations'; import { useUpdateComment } from '../../containers/use_update_comment'; import { useCurrentUser } from '../../common/lib/kibana'; import { AddComment, AddCommentRefObject } from '../add_comment'; +import { Case, CaseUserActions, Ecs } from '../../../common/ui/types'; import { ActionConnector, ActionsCommentRequestRt, AlertCommentRequestRt, - Case, - CaseUserActions, CommentType, ContextTypeUserRt, - Ecs, -} from '../../../common'; +} from '../../../common/api'; import { CaseServices } from '../../containers/use_get_case_user_actions'; import { parseStringAsExternalService } from '../../common/user_actions'; import { OnUpdateFields } from '../case_view'; diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_alert_comment_event.test.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_alert_comment_event.test.tsx index 73a61ed3afd5f..858b54038286d 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_alert_comment_event.test.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_alert_comment_event.test.tsx @@ -11,7 +11,7 @@ import { mount } from 'enzyme'; import { TestProviders } from '../../common/mock'; import { useKibana } from '../../common/lib/kibana'; import { AlertCommentEvent } from './user_action_alert_comment_event'; -import { CommentType } from '../../../common'; +import { CommentType } from '../../../common/api'; const props = { alertId: 'alert-id-1', diff --git a/x-pack/plugins/cases/public/components/user_action_tree/user_action_alert_comment_event.tsx b/x-pack/plugins/cases/public/components/user_action_tree/user_action_alert_comment_event.tsx index 8f405caa153f1..4236691a16bb2 100644 --- a/x-pack/plugins/cases/public/components/user_action_tree/user_action_alert_comment_event.tsx +++ b/x-pack/plugins/cases/public/components/user_action_tree/user_action_alert_comment_event.tsx @@ -10,7 +10,7 @@ import { isEmpty } from 'lodash'; import { EuiText, EuiLoadingSpinner } from '@elastic/eui'; import * as i18n from './translations'; -import { CommentType } from '../../../common'; +import { CommentType } from '../../../common/api'; import { LinkAnchor } from '../links'; import { RuleDetailsNavigation } from './helpers'; diff --git a/x-pack/plugins/cases/public/components/utils.ts b/x-pack/plugins/cases/public/components/utils.ts index 82d2682e65fad..1fafe5afe6990 100644 --- a/x-pack/plugins/cases/public/components/utils.ts +++ b/x-pack/plugins/cases/public/components/utils.ts @@ -6,7 +6,7 @@ */ import { IconType } from '@elastic/eui'; -import { ConnectorTypes } from '../../common'; +import { ConnectorTypes } from '../../common/api'; import { FieldConfig, ValidationConfig } from '../common/shared_imports'; import { StartPlugins } from '../types'; import { connectorValidator as swimlaneConnectorValidator } from './connectors/swimlane/validator'; diff --git a/x-pack/plugins/cases/public/containers/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/__mocks__/api.ts index 96e75a96ca115..843a9d81d8013 100644 --- a/x-pack/plugins/cases/public/containers/__mocks__/api.ts +++ b/x-pack/plugins/cases/public/containers/__mocks__/api.ts @@ -28,14 +28,14 @@ import { respReporters, tags, } from '../mock'; +import { ResolvedCase } from '../../../common/ui/types'; import { CasePatchRequest, CasePostRequest, CommentRequest, User, CaseStatuses, - ResolvedCase, -} from '../../../common'; +} from '../../../common/api'; export const getCase = async ( caseId: string, diff --git a/x-pack/plugins/cases/public/containers/api.test.tsx b/x-pack/plugins/cases/public/containers/api.test.tsx index 654ade308ed44..c83f5601da64b 100644 --- a/x-pack/plugins/cases/public/containers/api.test.tsx +++ b/x-pack/plugins/cases/public/containers/api.test.tsx @@ -7,13 +7,8 @@ import { KibanaServices } from '../common/lib/kibana'; -import { - CASES_URL, - ConnectorTypes, - CommentType, - CaseStatuses, - SECURITY_SOLUTION_OWNER, -} from '../../common'; +import { ConnectorTypes, CommentType, CaseStatuses } from '../../common/api'; +import { CASES_URL, SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { deleteCases, diff --git a/x-pack/plugins/cases/public/containers/api.ts b/x-pack/plugins/cases/public/containers/api.ts index 14f617b19db52..81bd6b39be5fd 100644 --- a/x-pack/plugins/cases/public/containers/api.ts +++ b/x-pack/plugins/cases/public/containers/api.ts @@ -7,15 +7,12 @@ import { assign, omit } from 'lodash'; +import { StatusAll, ResolvedCase } from '../../common/ui/types'; import { - CASE_REPORTERS_URL, - CASE_STATUS_URL, - CASE_TAGS_URL, CasePatchRequest, CasePostRequest, CaseResponse, CaseResolveResponse, - CASES_URL, CasesFindResponse, CasesResponse, CasesStatusResponse, @@ -29,16 +26,19 @@ import { getCaseUserActionUrl, getSubCaseDetailsUrl, getSubCaseUserActionUrl, - StatusAll, - SUB_CASE_DETAILS_URL, - SUB_CASES_PATCH_DEL_URL, SubCasePatchRequest, SubCaseResponse, SubCasesResponse, User, - ResolvedCase, -} from '../../common'; - +} from '../../common/api'; +import { + CASE_REPORTERS_URL, + CASE_STATUS_URL, + CASE_TAGS_URL, + CASES_URL, + SUB_CASE_DETAILS_URL, + SUB_CASES_PATCH_DEL_URL, +} from '../../common/constants'; import { getAllConnectorTypesUrl } from '../../common/utils/connectors_api'; import { KibanaServices } from '../common/lib/kibana'; diff --git a/x-pack/plugins/cases/public/containers/configure/__mocks__/api.ts b/x-pack/plugins/cases/public/containers/configure/__mocks__/api.ts index ea4b92706b4d1..10cfde0c5ef9c 100644 --- a/x-pack/plugins/cases/public/containers/configure/__mocks__/api.ts +++ b/x-pack/plugins/cases/public/containers/configure/__mocks__/api.ts @@ -10,7 +10,7 @@ import { CasesConfigureRequest, ActionConnector, ActionTypeConnector, -} from '../../../../common'; +} from '../../../../common/api'; import { ApiProps } from '../../types'; import { CaseConfigure } from '../types'; diff --git a/x-pack/plugins/cases/public/containers/configure/api.test.ts b/x-pack/plugins/cases/public/containers/configure/api.test.ts index 141b5e3711ceb..a315a455ec2a2 100644 --- a/x-pack/plugins/cases/public/containers/configure/api.test.ts +++ b/x-pack/plugins/cases/public/containers/configure/api.test.ts @@ -19,7 +19,8 @@ import { caseConfigurationResposeMock, caseConfigurationCamelCaseResponseMock, } from './mock'; -import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../../common'; +import { ConnectorTypes } from '../../../common/api'; +import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { KibanaServices } from '../../common/lib/kibana'; const abortCtrl = new AbortController(); diff --git a/x-pack/plugins/cases/public/containers/configure/api.ts b/x-pack/plugins/cases/public/containers/configure/api.ts index 1fd358e4dae9d..32202afc34881 100644 --- a/x-pack/plugins/cases/public/containers/configure/api.ts +++ b/x-pack/plugins/cases/public/containers/configure/api.ts @@ -10,14 +10,13 @@ import { getAllConnectorTypesUrl } from '../../../common/utils/connectors_api'; import { ActionConnector, ActionTypeConnector, - CASE_CONFIGURE_CONNECTORS_URL, - CASE_CONFIGURE_URL, CasesConfigurePatch, CasesConfigureRequest, CasesConfigureResponse, CasesConfigurationsResponse, getCaseConfigurationDetailsUrl, -} from '../../../common'; +} from '../../../common/api'; +import { CASE_CONFIGURE_CONNECTORS_URL, CASE_CONFIGURE_URL } from '../../../common/constants'; import { KibanaServices } from '../../common/lib/kibana'; import { ApiProps } from '../types'; diff --git a/x-pack/plugins/cases/public/containers/configure/mock.ts b/x-pack/plugins/cases/public/containers/configure/mock.ts index a5483e524e92d..bbcf420324c83 100644 --- a/x-pack/plugins/cases/public/containers/configure/mock.ts +++ b/x-pack/plugins/cases/public/containers/configure/mock.ts @@ -11,8 +11,8 @@ import { CasesConfigureResponse, CasesConfigureRequest, ConnectorTypes, - SECURITY_SOLUTION_OWNER, -} from '../../../common'; +} from '../../../common/api'; +import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { CaseConfigure, CaseConnectorMapping } from './types'; export const mappings: CaseConnectorMapping[] = [ diff --git a/x-pack/plugins/cases/public/containers/configure/types.ts b/x-pack/plugins/cases/public/containers/configure/types.ts index 5ee09add196bd..55401a2fbfd2c 100644 --- a/x-pack/plugins/cases/public/containers/configure/types.ts +++ b/x-pack/plugins/cases/public/containers/configure/types.ts @@ -15,7 +15,7 @@ import { CasesConfigure, ClosureType, ThirdPartyField, -} from '../../../common'; +} from '../../../common/api'; export type { ActionConnector, diff --git a/x-pack/plugins/cases/public/containers/configure/use_configure.test.tsx b/x-pack/plugins/cases/public/containers/configure/use_configure.test.tsx index 1814020de8465..1c9139b913617 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_configure.test.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_configure.test.tsx @@ -15,7 +15,7 @@ import { } from './use_configure'; import { mappings, caseConfigurationCamelCaseResponseMock } from './mock'; import * as api from './api'; -import { ConnectorTypes } from '../../../common'; +import { ConnectorTypes } from '../../../common/api'; import { TestProviders } from '../../common/mock'; const mockErrorToast = jest.fn(); diff --git a/x-pack/plugins/cases/public/containers/configure/use_configure.tsx b/x-pack/plugins/cases/public/containers/configure/use_configure.tsx index afac625c7682e..21c6e9e0b388e 100644 --- a/x-pack/plugins/cases/public/containers/configure/use_configure.tsx +++ b/x-pack/plugins/cases/public/containers/configure/use_configure.tsx @@ -10,7 +10,7 @@ import { getCaseConfigure, patchCaseConfigure, postCaseConfigure } from './api'; import * as i18n from './translations'; import { ClosureType, CaseConfigure, CaseConnector, CaseConnectorMapping } from './types'; -import { ConnectorTypes } from '../../../common'; +import { ConnectorTypes } from '../../../common/api'; import { useToasts } from '../../common/lib/kibana'; import { useCasesContext } from '../../components/cases_context/use_cases_context'; diff --git a/x-pack/plugins/cases/public/containers/mock.ts b/x-pack/plugins/cases/public/containers/mock.ts index f7d1daabd60ea..92fa8caa3ac5b 100644 --- a/x-pack/plugins/cases/public/containers/mock.ts +++ b/x-pack/plugins/cases/public/containers/mock.ts @@ -7,6 +7,8 @@ import { ActionLicense, AllCases, Case, CasesStatus, CaseUserActions, Comment } from './types'; +import { isCreateConnector, isPush, isUpdateConnector } from '../../common/utils/user_actions'; +import { ResolvedCase } from '../../common/ui/types'; import { AssociationType, CaseUserActionConnector, @@ -20,14 +22,10 @@ import { CommentResponse, CommentType, ConnectorTypes, - ResolvedCase, - isCreateConnector, - isPush, - isUpdateConnector, - SECURITY_SOLUTION_OWNER, UserAction, UserActionField, -} from '../../common'; +} from '../../common/api'; +import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { UseGetCasesState, DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS } from './use_get_cases'; export { connectorsMock } from './configure/mock'; diff --git a/x-pack/plugins/cases/public/containers/use_bulk_update_case.test.tsx b/x-pack/plugins/cases/public/containers/use_bulk_update_case.test.tsx index 67f202e6adbad..d00b361828a6e 100644 --- a/x-pack/plugins/cases/public/containers/use_bulk_update_case.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_bulk_update_case.test.tsx @@ -6,7 +6,7 @@ */ import { renderHook, act } from '@testing-library/react-hooks'; -import { CaseStatuses } from '../../common'; +import { CaseStatuses } from '../../common/api'; import { useUpdateCases, UseUpdateCases } from './use_bulk_update_case'; import { basicCase } from './mock'; import * as api from './api'; diff --git a/x-pack/plugins/cases/public/containers/use_bulk_update_case.tsx b/x-pack/plugins/cases/public/containers/use_bulk_update_case.tsx index 449ca0ab77f13..715b0c611c3b8 100644 --- a/x-pack/plugins/cases/public/containers/use_bulk_update_case.tsx +++ b/x-pack/plugins/cases/public/containers/use_bulk_update_case.tsx @@ -6,7 +6,7 @@ */ import { useCallback, useReducer, useRef, useEffect } from 'react'; -import { CaseStatuses } from '../../common'; +import { CaseStatuses } from '../../common/api'; import * as i18n from './translations'; import { patchCasesStatus } from './api'; import { BulkUpdateStatus, Case } from './types'; diff --git a/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx b/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx index 691af580b333a..307dc0941e398 100644 --- a/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_delete_cases.test.tsx @@ -7,7 +7,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; -import { CaseType } from '../../common'; +import { CaseType } from '../../common/api'; import { useDeleteCases, UseDeleteCase } from './use_delete_cases'; import * as api from './api'; diff --git a/x-pack/plugins/cases/public/containers/use_get_action_license.tsx b/x-pack/plugins/cases/public/containers/use_get_action_license.tsx index e4ea6d05011a7..7618f8c06d9ae 100644 --- a/x-pack/plugins/cases/public/containers/use_get_action_license.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_action_license.tsx @@ -11,7 +11,7 @@ import { useToasts } from '../common/lib/kibana'; import { getActionLicense } from './api'; import * as i18n from './translations'; import { ActionLicense } from './types'; -import { ConnectorTypes } from '../../common'; +import { ConnectorTypes } from '../../common/api'; export interface ActionLicenseState { actionLicense: ActionLicense | null; diff --git a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx index 36d600c3f1c9d..d3864097f5fee 100644 --- a/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_case_user_actions.tsx @@ -9,13 +9,8 @@ import { isEmpty, uniqBy } from 'lodash/fp'; import { useCallback, useEffect, useState, useRef } from 'react'; import deepEqual from 'fast-deep-equal'; -import { - CaseFullExternalService, - CaseConnector, - CaseExternalService, - CaseUserActions, - ElasticUser, -} from '../../common'; +import { ElasticUser, CaseUserActions, CaseExternalService } from '../../common/ui/types'; +import { CaseFullExternalService, CaseConnector } from '../../common/api'; import { getCaseUserActions, getSubCaseUserActions } from './api'; import * as i18n from './translations'; import { convertToCamelCase } from './utils'; diff --git a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx index 97de7a9073269..99fbf48665138 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases.test.tsx @@ -7,7 +7,8 @@ import React from 'react'; import { renderHook, act } from '@testing-library/react-hooks'; -import { CaseStatuses, SECURITY_SOLUTION_OWNER } from '../../common'; +import { CaseStatuses } from '../../common/api'; +import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { DEFAULT_FILTER_OPTIONS, DEFAULT_QUERY_PARAMS, diff --git a/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx b/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx index 93da0ecbd14ae..2e3e42255145d 100644 --- a/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_cases_status.test.tsx @@ -11,7 +11,7 @@ import { useGetCasesStatus, UseGetCasesStatus } from './use_get_cases_status'; import { casesStatus } from './mock'; import * as api from './api'; import { TestProviders } from '../common/mock'; -import { SECURITY_SOLUTION_OWNER } from '../../common'; +import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; jest.mock('./api'); jest.mock('../common/lib/kibana'); diff --git a/x-pack/plugins/cases/public/containers/use_get_reporters.test.tsx b/x-pack/plugins/cases/public/containers/use_get_reporters.test.tsx index 21f9352a7cbc0..38d47d3aa9cbb 100644 --- a/x-pack/plugins/cases/public/containers/use_get_reporters.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_reporters.test.tsx @@ -11,7 +11,7 @@ import { useGetReporters, UseGetReporters } from './use_get_reporters'; import { reporters, respReporters } from './mock'; import * as api from './api'; import { TestProviders } from '../common/mock'; -import { SECURITY_SOLUTION_OWNER } from '../../common'; +import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; jest.mock('./api'); jest.mock('../common/lib/kibana'); diff --git a/x-pack/plugins/cases/public/containers/use_get_reporters.tsx b/x-pack/plugins/cases/public/containers/use_get_reporters.tsx index 881933419d60b..ce8aa4b961c23 100644 --- a/x-pack/plugins/cases/public/containers/use_get_reporters.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_reporters.tsx @@ -8,7 +8,7 @@ import { useCallback, useEffect, useState, useRef } from 'react'; import { isEmpty } from 'lodash/fp'; -import { User } from '../../common'; +import { User } from '../../common/api'; import { getReporters } from './api'; import * as i18n from './translations'; import { useToasts } from '../common/lib/kibana'; diff --git a/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx b/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx index b2bf4737356cc..2607129e5655d 100644 --- a/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_tags.test.tsx @@ -11,7 +11,7 @@ import { useGetTags, UseGetTags } from './use_get_tags'; import { tags } from './mock'; import * as api from './api'; import { TestProviders } from '../common/mock'; -import { SECURITY_SOLUTION_OWNER } from '../../common'; +import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; jest.mock('./api'); jest.mock('../common/lib/kibana'); diff --git a/x-pack/plugins/cases/public/containers/use_post_case.test.tsx b/x-pack/plugins/cases/public/containers/use_post_case.test.tsx index d2b638b4c846f..5d5b6ced44afc 100644 --- a/x-pack/plugins/cases/public/containers/use_post_case.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_post_case.test.tsx @@ -8,7 +8,8 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { usePostCase, UsePostCase } from './use_post_case'; import * as api from './api'; -import { ConnectorTypes, SECURITY_SOLUTION_OWNER } from '../../common'; +import { ConnectorTypes } from '../../common/api'; +import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { basicCasePost } from './mock'; jest.mock('./api'); diff --git a/x-pack/plugins/cases/public/containers/use_post_case.tsx b/x-pack/plugins/cases/public/containers/use_post_case.tsx index f13c250b96a3e..dc23c503b333b 100644 --- a/x-pack/plugins/cases/public/containers/use_post_case.tsx +++ b/x-pack/plugins/cases/public/containers/use_post_case.tsx @@ -6,7 +6,7 @@ */ import { useReducer, useCallback, useRef, useEffect } from 'react'; -import { CasePostRequest } from '../../common'; +import { CasePostRequest } from '../../common/api'; import { postCase } from './api'; import * as i18n from './translations'; import { Case } from './types'; diff --git a/x-pack/plugins/cases/public/containers/use_post_comment.test.tsx b/x-pack/plugins/cases/public/containers/use_post_comment.test.tsx index 8a86d9becdfde..dd9d73cff9bae 100644 --- a/x-pack/plugins/cases/public/containers/use_post_comment.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_post_comment.test.tsx @@ -7,7 +7,8 @@ import { renderHook, act } from '@testing-library/react-hooks'; -import { CommentType, SECURITY_SOLUTION_OWNER } from '../../common'; +import { CommentType } from '../../common/api'; +import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { usePostComment, UsePostComment } from './use_post_comment'; import { basicCaseId, basicSubCaseId } from './mock'; import * as api from './api'; diff --git a/x-pack/plugins/cases/public/containers/use_post_comment.tsx b/x-pack/plugins/cases/public/containers/use_post_comment.tsx index 2d4437826092a..d796c5035ff9d 100644 --- a/x-pack/plugins/cases/public/containers/use_post_comment.tsx +++ b/x-pack/plugins/cases/public/containers/use_post_comment.tsx @@ -6,7 +6,7 @@ */ import { useReducer, useCallback, useRef, useEffect } from 'react'; -import { CommentRequest } from '../../common'; +import { CommentRequest } from '../../common/api'; import { postComment } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/cases/public/containers/use_post_push_to_service.test.tsx b/x-pack/plugins/cases/public/containers/use_post_push_to_service.test.tsx index 18e3c4be493b8..dedde459ad557 100644 --- a/x-pack/plugins/cases/public/containers/use_post_push_to_service.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_post_push_to_service.test.tsx @@ -9,7 +9,7 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { usePostPushToService, UsePostPushToService } from './use_post_push_to_service'; import { pushedCase } from './mock'; import * as api from './api'; -import { CaseConnector, ConnectorTypes } from '../../common'; +import { CaseConnector, ConnectorTypes } from '../../common/api'; jest.mock('./api'); jest.mock('../common/lib/kibana'); diff --git a/x-pack/plugins/cases/public/containers/use_post_push_to_service.tsx b/x-pack/plugins/cases/public/containers/use_post_push_to_service.tsx index f4cf5b012e84f..90f1fbe212a02 100644 --- a/x-pack/plugins/cases/public/containers/use_post_push_to_service.tsx +++ b/x-pack/plugins/cases/public/containers/use_post_push_to_service.tsx @@ -6,7 +6,7 @@ */ import { useReducer, useCallback, useRef, useEffect } from 'react'; -import { CaseConnector } from '../../common'; +import { CaseConnector } from '../../common/api'; import { pushCase } from './api'; import * as i18n from './translations'; diff --git a/x-pack/plugins/cases/public/containers/use_update_case.tsx b/x-pack/plugins/cases/public/containers/use_update_case.tsx index afdc33bcc25e4..42e861d300341 100644 --- a/x-pack/plugins/cases/public/containers/use_update_case.tsx +++ b/x-pack/plugins/cases/public/containers/use_update_case.tsx @@ -9,7 +9,8 @@ import { useReducer, useCallback, useRef, useEffect } from 'react'; import { useToasts } from '../common/lib/kibana'; import { patchCase, patchSubCase } from './api'; -import { UpdateKey, UpdateByKey, CaseStatuses } from '../../common'; +import { UpdateKey, UpdateByKey } from '../../common/ui/types'; +import { CaseStatuses } from '../../common/api'; import * as i18n from './translations'; import { createUpdateSuccessToaster } from './utils'; diff --git a/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx b/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx index 14cc4dfab3599..836ec10e608a6 100644 --- a/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx +++ b/x-pack/plugins/cases/public/containers/use_update_comment.test.tsx @@ -11,7 +11,7 @@ import { useUpdateComment, UseUpdateComment } from './use_update_comment'; import { basicCase, basicCaseCommentPatch, basicSubCaseId } from './mock'; import * as api from './api'; import { TestProviders } from '../common/mock'; -import { SECURITY_SOLUTION_OWNER } from '../../common'; +import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; jest.mock('./api'); jest.mock('../common/lib/kibana'); diff --git a/x-pack/plugins/cases/public/containers/utils.ts b/x-pack/plugins/cases/public/containers/utils.ts index 458899e5f53c9..938724a632dcb 100644 --- a/x-pack/plugins/cases/public/containers/utils.ts +++ b/x-pack/plugins/cases/public/containers/utils.ts @@ -32,7 +32,7 @@ import { CasePatchRequest, CaseResolveResponse, CaseResolveResponseRt, -} from '../../common'; +} from '../../common/api'; import { AllCases, Case, UpdateByKey } from './types'; import * as i18n from './translations'; diff --git a/x-pack/plugins/cases/public/plugin.ts b/x-pack/plugins/cases/public/plugin.ts index b6b9643ea5856..48371f65b49e1 100644 --- a/x-pack/plugins/cases/public/plugin.ts +++ b/x-pack/plugins/cases/public/plugin.ts @@ -15,7 +15,8 @@ import { getAllCasesSelectorModalLazy, getCreateCaseFlyoutLazy, } from './methods'; -import { CasesUiConfigType, ENABLE_CASE_CONNECTOR } from '../common'; +import { CasesUiConfigType } from '../common/ui/types'; +import { ENABLE_CASE_CONNECTOR } from '../common/constants'; /** * @public diff --git a/x-pack/plugins/cases/server/authorization/authorization.ts b/x-pack/plugins/cases/server/authorization/authorization.ts index 7a474ff4db402..453d5c5783f8c 100644 --- a/x-pack/plugins/cases/server/authorization/authorization.ts +++ b/x-pack/plugins/cases/server/authorization/authorization.ts @@ -12,7 +12,7 @@ import { PluginStartContract as FeaturesPluginStart } from '../../../features/se import { AuthFilterHelpers, GetSpaceFn, OwnerEntity } from './types'; import { getOwnersFilter } from './utils'; import { AuthorizationAuditLogger, OperationDetails } from '.'; -import { createCaseError } from '../common'; +import { createCaseError } from '../common/error'; /** * This class handles ensuring that the user making a request has the correct permissions diff --git a/x-pack/plugins/cases/server/authorization/index.ts b/x-pack/plugins/cases/server/authorization/index.ts index c5208177554b5..057e85b460c2e 100644 --- a/x-pack/plugins/cases/server/authorization/index.ts +++ b/x-pack/plugins/cases/server/authorization/index.ts @@ -11,7 +11,7 @@ import { CASE_CONFIGURE_SAVED_OBJECT, CASE_SAVED_OBJECT, CASE_USER_ACTION_SAVED_OBJECT, -} from '../../common'; +} from '../../common/constants'; import { Verbs, ReadOperations, WriteOperations, OperationDetails } from './types'; export * from './authorization'; diff --git a/x-pack/plugins/cases/server/authorization/utils.test.ts b/x-pack/plugins/cases/server/authorization/utils.test.ts index 2afffbbb768b8..7717edfc909ef 100644 --- a/x-pack/plugins/cases/server/authorization/utils.test.ts +++ b/x-pack/plugins/cases/server/authorization/utils.test.ts @@ -6,7 +6,7 @@ */ import { nodeBuilder } from '@kbn/es-query'; -import { OWNER_FIELD } from '../../common'; +import { OWNER_FIELD } from '../../common/api'; import { combineFilterWithAuthorizationFilter, ensureFieldIsSafeForQuery, diff --git a/x-pack/plugins/cases/server/authorization/utils.ts b/x-pack/plugins/cases/server/authorization/utils.ts index f3a8512548430..ac88f96fb4e14 100644 --- a/x-pack/plugins/cases/server/authorization/utils.ts +++ b/x-pack/plugins/cases/server/authorization/utils.ts @@ -7,7 +7,7 @@ import { remove, uniq } from 'lodash'; import { nodeBuilder, KueryNode } from '@kbn/es-query'; -import { OWNER_FIELD } from '../../common'; +import { OWNER_FIELD } from '../../common/api'; export const getOwnersFilter = ( savedObjectType: string, diff --git a/x-pack/plugins/cases/server/client/alerts/get.ts b/x-pack/plugins/cases/server/client/alerts/get.ts index d95c4f536abc3..831ab0b05ed17 100644 --- a/x-pack/plugins/cases/server/client/alerts/get.ts +++ b/x-pack/plugins/cases/server/client/alerts/get.ts @@ -7,7 +7,7 @@ import { CasesClientGetAlertsResponse } from './types'; import { CasesClientArgs } from '..'; -import { AlertInfo } from '../../common'; +import { AlertInfo } from '../../common/types'; export const getAlerts = async ( alertsInfo: AlertInfo[], diff --git a/x-pack/plugins/cases/server/client/alerts/types.ts b/x-pack/plugins/cases/server/client/alerts/types.ts index 95cd9ae33bff9..cba18cc26e8b7 100644 --- a/x-pack/plugins/cases/server/client/alerts/types.ts +++ b/x-pack/plugins/cases/server/client/alerts/types.ts @@ -6,7 +6,7 @@ */ import { CaseStatuses } from '../../../common/api'; -import { AlertInfo } from '../../common'; +import { AlertInfo } from '../../common/types'; interface Alert { id: string; diff --git a/x-pack/plugins/cases/server/client/attachments/add.ts b/x-pack/plugins/cases/server/client/attachments/add.ts index 84dbc8921f0e4..88b4f493d694c 100644 --- a/x-pack/plugins/cases/server/client/attachments/add.ts +++ b/x-pack/plugins/cases/server/client/attachments/add.ts @@ -21,31 +21,30 @@ import { LensServerPluginSetup } from '../../../../lens/server'; import { AlertCommentRequestRt, - CASE_COMMENT_SAVED_OBJECT, CaseResponse, CaseStatuses, CaseType, CommentRequest, CommentRequestRt, CommentType, - ENABLE_CASE_CONNECTOR, - MAX_GENERATED_ALERTS_PER_SUB_CASE, SubCaseAttributes, throwErrors, User, -} from '../../../common'; +} from '../../../common/api'; +import { + CASE_COMMENT_SAVED_OBJECT, + ENABLE_CASE_CONNECTOR, + MAX_GENERATED_ALERTS_PER_SUB_CASE, +} from '../../../common/constants'; import { buildCaseUserActionItem, buildCommentUserActionItem, } from '../../services/user_actions/helpers'; import { AttachmentService, CasesService, CaseUserActionService } from '../../services'; -import { - createCaseError, - CommentableCase, - createAlertUpdateRequest, - isCommentRequestTypeGenAlert, -} from '../../common'; +import { CommentableCase } from '../../common/models'; +import { createCaseError } from '../../common/error'; +import { createAlertUpdateRequest, isCommentRequestTypeGenAlert } from '../../common/utils'; import { CasesClientArgs, CasesClientInternal } from '..'; import { decodeCommentRequest } from '../utils'; diff --git a/x-pack/plugins/cases/server/client/attachments/client.ts b/x-pack/plugins/cases/server/client/attachments/client.ts index a07633c0dd38c..d71496b764824 100644 --- a/x-pack/plugins/cases/server/client/attachments/client.ts +++ b/x-pack/plugins/cases/server/client/attachments/client.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { AlertResponse, CommentResponse } from '../../../common'; +import { AlertResponse, CommentResponse } from '../../../common/api'; import { CasesClient } from '../client'; import { CasesClientInternal } from '../client_internal'; diff --git a/x-pack/plugins/cases/server/client/attachments/delete.ts b/x-pack/plugins/cases/server/client/attachments/delete.ts index 89d097c5334b7..17fcd2235a034 100644 --- a/x-pack/plugins/cases/server/client/attachments/delete.ts +++ b/x-pack/plugins/cases/server/client/attachments/delete.ts @@ -9,16 +9,16 @@ import Boom from '@hapi/boom'; import pMap from 'p-map'; import { SavedObject } from 'kibana/public'; +import { AssociationType, CommentAttributes } from '../../../common/api'; import { - AssociationType, CASE_SAVED_OBJECT, - CommentAttributes, MAX_CONCURRENT_SEARCHES, SUB_CASE_SAVED_OBJECT, -} from '../../../common'; +} from '../../../common/constants'; import { CasesClientArgs } from '../types'; import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; -import { createCaseError, checkEnabledCaseConnectorOrThrow } from '../../common'; +import { createCaseError } from '../../common/error'; +import { checkEnabledCaseConnectorOrThrow } from '../../common/utils'; import { Operations } from '../../authorization'; /** diff --git a/x-pack/plugins/cases/server/client/attachments/get.ts b/x-pack/plugins/cases/server/client/attachments/get.ts index 9a06c5142600a..bbef0fe0ce348 100644 --- a/x-pack/plugins/cases/server/client/attachments/get.ts +++ b/x-pack/plugins/cases/server/client/attachments/get.ts @@ -18,18 +18,18 @@ import { CommentResponseRt, CommentsResponse, CommentsResponseRt, - ENABLE_CASE_CONNECTOR, FindQueryParams, -} from '../../../common'; +} from '../../../common/api'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { - createCaseError, checkEnabledCaseConnectorOrThrow, defaultSortField, transformComments, flattenCommentSavedObject, flattenCommentSavedObjects, getIDsAndIndicesAsArrays, -} from '../../common'; +} from '../../common/utils'; +import { createCaseError } from '../../common/error'; import { defaultPage, defaultPerPage } from '../../routes/api'; import { CasesClientArgs } from '../types'; import { combineFilters, stringToKueryNode } from '../utils'; diff --git a/x-pack/plugins/cases/server/client/attachments/update.ts b/x-pack/plugins/cases/server/client/attachments/update.ts index b5e9e6c372355..a74e6c140bf4e 100644 --- a/x-pack/plugins/cases/server/client/attachments/update.ts +++ b/x-pack/plugins/cases/server/client/attachments/update.ts @@ -10,15 +10,12 @@ import Boom from '@hapi/boom'; import { SavedObjectsClientContract, Logger } from 'kibana/server'; import { LensServerPluginSetup } from '../../../../lens/server'; -import { checkEnabledCaseConnectorOrThrow, CommentableCase, createCaseError } from '../../common'; +import { CommentableCase } from '../../common/models'; +import { createCaseError } from '../../common/error'; +import { checkEnabledCaseConnectorOrThrow } from '../../common/utils'; import { buildCommentUserActionItem } from '../../services/user_actions/helpers'; -import { - CASE_SAVED_OBJECT, - SUB_CASE_SAVED_OBJECT, - CaseResponse, - CommentPatchRequest, - CommentRequest, -} from '../../../common'; +import { CaseResponse, CommentPatchRequest, CommentRequest } from '../../../common/api'; +import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; import { AttachmentService, CasesService } from '../../services'; import { CasesClientArgs } from '..'; import { decodeCommentRequest } from '../utils'; diff --git a/x-pack/plugins/cases/server/client/cases/client.ts b/x-pack/plugins/cases/server/client/cases/client.ts index 09386431200ed..b2673eef33dd5 100644 --- a/x-pack/plugins/cases/server/client/cases/client.ts +++ b/x-pack/plugins/cases/server/client/cases/client.ts @@ -13,7 +13,7 @@ import { AllTagsFindRequest, AllReportersFindRequest, CasesByAlertId, -} from '../../../common'; +} from '../../../common/api'; import { CasesClient } from '../client'; import { CasesClientInternal } from '../client_internal'; import { diff --git a/x-pack/plugins/cases/server/client/cases/create.ts b/x-pack/plugins/cases/server/client/cases/create.ts index 488bc523f7796..8a668b7c5db09 100644 --- a/x-pack/plugins/cases/server/client/cases/create.ts +++ b/x-pack/plugins/cases/server/client/cases/create.ts @@ -21,13 +21,13 @@ import { CasePostRequest, CaseType, OWNER_FIELD, - ENABLE_CASE_CONNECTOR, - MAX_TITLE_LENGTH, -} from '../../../common'; +} from '../../../common/api'; +import { ENABLE_CASE_CONNECTOR, MAX_TITLE_LENGTH } from '../../../common/constants'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { Operations } from '../../authorization'; -import { createCaseError, flattenCaseSavedObject, transformNewCase } from '../../common'; +import { createCaseError } from '../../common/error'; +import { flattenCaseSavedObject, transformNewCase } from '../../common/utils'; import { CasesClientArgs } from '..'; /** diff --git a/x-pack/plugins/cases/server/client/cases/delete.ts b/x-pack/plugins/cases/server/client/cases/delete.ts index 4333535f17a24..cbe481dd07098 100644 --- a/x-pack/plugins/cases/server/client/cases/delete.ts +++ b/x-pack/plugins/cases/server/client/cases/delete.ts @@ -8,15 +8,10 @@ import pMap from 'p-map'; import { Boom } from '@hapi/boom'; import { SavedObject, SavedObjectsClientContract, SavedObjectsFindResponse } from 'kibana/server'; -import { - CommentAttributes, - ENABLE_CASE_CONNECTOR, - MAX_CONCURRENT_SEARCHES, - OWNER_FIELD, - SubCaseAttributes, -} from '../../../common'; +import { CommentAttributes, SubCaseAttributes, OWNER_FIELD } from '../../../common/api'; +import { ENABLE_CASE_CONNECTOR, MAX_CONCURRENT_SEARCHES } from '../../../common/constants'; import { CasesClientArgs } from '..'; -import { createCaseError } from '../../common'; +import { createCaseError } from '../../common/error'; import { AttachmentService, CasesService } from '../../services'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { Operations, OwnerEntity } from '../../authorization'; diff --git a/x-pack/plugins/cases/server/client/cases/find.ts b/x-pack/plugins/cases/server/client/cases/find.ts index 282ff956b7a6f..4257dfce6d5e3 100644 --- a/x-pack/plugins/cases/server/client/cases/find.ts +++ b/x-pack/plugins/cases/server/client/cases/find.ts @@ -18,9 +18,10 @@ import { caseStatuses, CasesFindResponseRt, excess, -} from '../../../common'; +} from '../../../common/api'; -import { createCaseError, transformCases } from '../../common'; +import { createCaseError } from '../../common/error'; +import { transformCases } from '../../common/utils'; import { constructQueryOptions } from '../utils'; import { includeFieldsRequiredForAuthentication } from '../../authorization/utils'; import { Operations } from '../../authorization'; diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 653df1efd2daa..b388abb58e449 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -25,12 +25,13 @@ import { AllReportersFindRequest, CasesByAlertIDRequest, CasesByAlertIDRequestRt, - ENABLE_CASE_CONNECTOR, CasesByAlertId, CasesByAlertIdRt, CaseAttributes, -} from '../../../common'; -import { countAlertsForID, createCaseError, flattenCaseSavedObject } from '../../common'; +} from '../../../common/api'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; +import { createCaseError } from '../../common/error'; +import { countAlertsForID, flattenCaseSavedObject } from '../../common/utils'; import { CasesClientArgs } from '..'; import { Operations } from '../../authorization'; import { combineAuthorizedAndOwnerFilter } from '../utils'; diff --git a/x-pack/plugins/cases/server/client/cases/mock.ts b/x-pack/plugins/cases/server/client/cases/mock.ts index 22520cea11014..5f677bdbf4a73 100644 --- a/x-pack/plugins/cases/server/client/cases/mock.ts +++ b/x-pack/plugins/cases/server/client/cases/mock.ts @@ -12,8 +12,8 @@ import { CaseUserActionsResponse, AssociationType, CommentResponseAlertsType, - SECURITY_SOLUTION_OWNER, -} from '../../../common'; +} from '../../../common/api'; +import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { BasicParams } from './types'; diff --git a/x-pack/plugins/cases/server/client/cases/push.ts b/x-pack/plugins/cases/server/client/cases/push.ts index 953f8b88c990b..c05705dd03586 100644 --- a/x-pack/plugins/cases/server/client/cases/push.ts +++ b/x-pack/plugins/cases/server/client/cases/push.ts @@ -15,14 +15,15 @@ import { CaseStatuses, ExternalServiceResponse, CaseType, - ENABLE_CASE_CONNECTOR, CasesConfigureAttributes, CaseAttributes, -} from '../../../common'; +} from '../../../common/api'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { createIncident, getCommentContextFromAttributes } from './utils'; -import { createCaseError, flattenCaseSavedObject, getAlertInfoFromComments } from '../../common'; +import { createCaseError } from '../../common/error'; +import { flattenCaseSavedObject, getAlertInfoFromComments } from '../../common/utils'; import { CasesClient, CasesClientArgs, CasesClientInternal } from '..'; import { Operations } from '../../authorization'; import { casesConnectors } from '../../connectors'; diff --git a/x-pack/plugins/cases/server/client/cases/types.ts b/x-pack/plugins/cases/server/client/cases/types.ts index fb400675136ef..f1d56e7132bd1 100644 --- a/x-pack/plugins/cases/server/client/cases/types.ts +++ b/x-pack/plugins/cases/server/client/cases/types.ts @@ -19,7 +19,7 @@ import { PushToServiceApiParamsSIR as ServiceNowSIRPushToServiceApiParams, ServiceNowITSMIncident, } from '../../../../actions/server/builtin_action_types/servicenow/types'; -import { CaseResponse, ConnectorMappingsAttributes } from '../../../common'; +import { CaseResponse, ConnectorMappingsAttributes } from '../../../common/api'; export type Incident = JiraIncident | ResilientIncident | ServiceNowITSMIncident; export type PushToServiceApiParams = diff --git a/x-pack/plugins/cases/server/client/cases/update.ts b/x-pack/plugins/cases/server/client/cases/update.ts index 455665dc7012c..786ba28343490 100644 --- a/x-pack/plugins/cases/server/client/cases/update.ts +++ b/x-pack/plugins/cases/server/client/cases/update.ts @@ -22,8 +22,6 @@ import { nodeBuilder } from '@kbn/es-query'; import { AssociationType, - CASE_COMMENT_SAVED_OBJECT, - CASE_SAVED_OBJECT, CasePatchRequest, CasesPatchRequest, CasesPatchRequestRt, @@ -33,24 +31,28 @@ import { CaseType, CommentAttributes, CommentType, - ENABLE_CASE_CONNECTOR, excess, + throwErrors, + CaseAttributes, +} from '../../../common/api'; +import { + CASE_COMMENT_SAVED_OBJECT, + CASE_SAVED_OBJECT, + ENABLE_CASE_CONNECTOR, MAX_CONCURRENT_SEARCHES, SUB_CASE_SAVED_OBJECT, - throwErrors, MAX_TITLE_LENGTH, - CaseAttributes, -} from '../../../common'; +} from '../../../common/constants'; import { buildCaseUserActions } from '../../services/user_actions/helpers'; import { getCaseToUpdate } from '../utils'; import { AlertService, CasesService } from '../../services'; +import { createCaseError } from '../../common/error'; import { createAlertUpdateRequest, - createCaseError, flattenCaseSavedObject, isCommentRequestTypeAlertOrGenAlert, -} from '../../common'; +} from '../../common/utils'; import { UpdateAlertRequest } from '../alerts/types'; import { CasesClientArgs } from '..'; import { Operations, OwnerEntity } from '../../authorization'; diff --git a/x-pack/plugins/cases/server/client/cases/utils.test.ts b/x-pack/plugins/cases/server/client/cases/utils.test.ts index 315e9966d347b..3c9e8e7255e76 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.test.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.test.ts @@ -31,8 +31,8 @@ import { transformers, transformFields, } from './utils'; -import { flattenCaseSavedObject } from '../../common'; -import { SECURITY_SOLUTION_OWNER } from '../../../common'; +import { flattenCaseSavedObject } from '../../common/utils'; +import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { casesConnectors } from '../../connectors'; const formatComment = { diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts index f5cf2fe4b3f51..e992c6e25fb4e 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.ts @@ -7,6 +7,7 @@ import { i18n } from '@kbn/i18n'; import { flow } from 'lodash'; +import { isPush } from '../../../common/utils/user_actions'; import { ActionConnector, CaseFullExternalService, @@ -21,8 +22,7 @@ import { CommentRequestAlertType, CommentRequestActionsType, CaseUserActionResponse, - isPush, -} from '../../../common'; +} from '../../../common/api'; import { ActionsClient } from '../../../../actions/server'; import { CasesClientGetAlertsResponse } from '../../client/alerts/types'; import { diff --git a/x-pack/plugins/cases/server/client/client.ts b/x-pack/plugins/cases/server/client/client.ts index bd4e36bb7c177..f85667bee7bc3 100644 --- a/x-pack/plugins/cases/server/client/client.ts +++ b/x-pack/plugins/cases/server/client/client.ts @@ -11,7 +11,7 @@ import { AttachmentsSubClient, createAttachmentsSubClient } from './attachments/ import { UserActionsSubClient, createUserActionsSubClient } from './user_actions/client'; import { CasesClientInternal, createCasesClientInternal } from './client_internal'; import { createSubCasesClient, SubCasesClient } from './sub_cases/client'; -import { ENABLE_CASE_CONNECTOR } from '../../common'; +import { ENABLE_CASE_CONNECTOR } from '../../common/constants'; import { ConfigureSubClient, createConfigurationSubClient } from './configure/client'; import { createStatsSubClient, StatsSubClient } from './stats/client'; import { createMetricsSubClient, MetricsSubClient } from './metrics/client'; diff --git a/x-pack/plugins/cases/server/client/configure/client.ts b/x-pack/plugins/cases/server/client/configure/client.ts index 791fcc70947db..c7b94df879142 100644 --- a/x-pack/plugins/cases/server/client/configure/client.ts +++ b/x-pack/plugins/cases/server/client/configure/client.ts @@ -30,11 +30,10 @@ import { excess, GetConfigureFindRequest, GetConfigureFindRequestRt, - MAX_CONCURRENT_SEARCHES, - SUPPORTED_CONNECTORS, throwErrors, -} from '../../../common'; -import { createCaseError } from '../../common'; +} from '../../../common/api'; +import { MAX_CONCURRENT_SEARCHES, SUPPORTED_CONNECTORS } from '../../../common/constants'; +import { createCaseError } from '../../common/error'; import { CasesClientInternal } from '../client_internal'; import { CasesClientArgs } from '../types'; import { getMappings } from './get_mappings'; diff --git a/x-pack/plugins/cases/server/client/configure/create_mappings.ts b/x-pack/plugins/cases/server/client/configure/create_mappings.ts index 2e9280b968d20..bb4c32ae57071 100644 --- a/x-pack/plugins/cases/server/client/configure/create_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/create_mappings.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { ConnectorMappingsAttributes } from '../../../common'; +import { ConnectorMappingsAttributes } from '../../../common/api'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { createCaseError } from '../../common'; +import { createCaseError } from '../../common/error'; import { CasesClientArgs } from '..'; import { CreateMappingsArgs } from './types'; import { casesConnectors } from '../../connectors'; diff --git a/x-pack/plugins/cases/server/client/configure/get_mappings.ts b/x-pack/plugins/cases/server/client/configure/get_mappings.ts index c080159488cf2..2fa0e8454bacf 100644 --- a/x-pack/plugins/cases/server/client/configure/get_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/get_mappings.ts @@ -6,9 +6,9 @@ */ import { SavedObjectsFindResponse } from 'kibana/server'; -import { ConnectorMappings } from '../../../common'; +import { ConnectorMappings } from '../../../common/api'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { createCaseError } from '../../common'; +import { createCaseError } from '../../common/error'; import { CasesClientArgs } from '..'; import { MappingsArgs } from './types'; diff --git a/x-pack/plugins/cases/server/client/configure/types.ts b/x-pack/plugins/cases/server/client/configure/types.ts index aca3436c59082..c7ac49c9e94aa 100644 --- a/x-pack/plugins/cases/server/client/configure/types.ts +++ b/x-pack/plugins/cases/server/client/configure/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseConnector } from '../../../common'; +import { CaseConnector } from '../../../common/api'; export interface MappingsArgs { connector: CaseConnector; diff --git a/x-pack/plugins/cases/server/client/configure/update_mappings.ts b/x-pack/plugins/cases/server/client/configure/update_mappings.ts index 43fe527facd52..3d529e51e7561 100644 --- a/x-pack/plugins/cases/server/client/configure/update_mappings.ts +++ b/x-pack/plugins/cases/server/client/configure/update_mappings.ts @@ -5,9 +5,9 @@ * 2.0. */ -import { ConnectorMappingsAttributes } from '../../../common'; +import { ConnectorMappingsAttributes } from '../../../common/api'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { createCaseError } from '../../common'; +import { createCaseError } from '../../common/error'; import { CasesClientArgs } from '..'; import { UpdateMappingsArgs } from './types'; import { casesConnectors } from '../../connectors'; diff --git a/x-pack/plugins/cases/server/client/factory.ts b/x-pack/plugins/cases/server/client/factory.ts index 4f506b5e0b4f7..d657f1a3f4f48 100644 --- a/x-pack/plugins/cases/server/client/factory.ts +++ b/x-pack/plugins/cases/server/client/factory.ts @@ -12,7 +12,7 @@ import { ElasticsearchClient, } from 'kibana/server'; import { SecurityPluginSetup, SecurityPluginStart } from '../../../security/server'; -import { SAVED_OBJECT_TYPES } from '../../common'; +import { SAVED_OBJECT_TYPES } from '../../common/constants'; import { Authorization } from '../authorization/authorization'; import { GetSpaceFn } from '../authorization/types'; import { diff --git a/x-pack/plugins/cases/server/client/metrics/alert_details.ts b/x-pack/plugins/cases/server/client/metrics/alert_details.ts index 4bdd820d99ef6..5d25ab5dc1226 100644 --- a/x-pack/plugins/cases/server/client/metrics/alert_details.ts +++ b/x-pack/plugins/cases/server/client/metrics/alert_details.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseMetricsResponse } from '../../../common'; +import { CaseMetricsResponse } from '../../../common/api'; import { MetricsHandler } from './types'; export class AlertDetails implements MetricsHandler { diff --git a/x-pack/plugins/cases/server/client/metrics/alerts_count.ts b/x-pack/plugins/cases/server/client/metrics/alerts_count.ts index 11e2d32db7ca2..aa0e945bc5fcf 100644 --- a/x-pack/plugins/cases/server/client/metrics/alerts_count.ts +++ b/x-pack/plugins/cases/server/client/metrics/alerts_count.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseMetricsResponse } from '../../../common'; +import { CaseMetricsResponse } from '../../../common/api'; import { MetricsHandler } from './types'; export class AlertsCount implements MetricsHandler { diff --git a/x-pack/plugins/cases/server/client/metrics/client.ts b/x-pack/plugins/cases/server/client/metrics/client.ts index 527ce527d0cc2..c5420213f3f97 100644 --- a/x-pack/plugins/cases/server/client/metrics/client.ts +++ b/x-pack/plugins/cases/server/client/metrics/client.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseMetricsResponse } from '../../../common'; +import { CaseMetricsResponse } from '../../../common/api'; import { CasesClient } from '../client'; import { CasesClientArgs } from '../types'; diff --git a/x-pack/plugins/cases/server/client/metrics/connectors.ts b/x-pack/plugins/cases/server/client/metrics/connectors.ts index 6ad5fcc056ee5..727b5576b4fa2 100644 --- a/x-pack/plugins/cases/server/client/metrics/connectors.ts +++ b/x-pack/plugins/cases/server/client/metrics/connectors.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseMetricsResponse } from '../../../common'; +import { CaseMetricsResponse } from '../../../common/api'; import { MetricsHandler } from './types'; export class Connectors implements MetricsHandler { diff --git a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts index 072525d080f0a..cd3c9204e3c03 100644 --- a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts +++ b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.test.ts @@ -6,7 +6,7 @@ */ import { getCaseMetrics } from './get_case_metrics'; -import { CaseAttributes, CaseResponse } from '../../../common'; +import { CaseAttributes, CaseResponse } from '../../../common/api'; import { createCasesClientMock } from '../mocks'; import { CasesClientArgs } from '../types'; import { createAuthorizationMock } from '../../authorization/mock'; diff --git a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.ts b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.ts index a64325da8453e..74628ebd8c9ee 100644 --- a/x-pack/plugins/cases/server/client/metrics/get_case_metrics.ts +++ b/x-pack/plugins/cases/server/client/metrics/get_case_metrics.ts @@ -6,9 +6,9 @@ */ import { merge } from 'lodash'; -import { CaseMetricsResponseRt, CaseMetricsResponse } from '../../../common'; +import { CaseMetricsResponseRt, CaseMetricsResponse } from '../../../common/api'; import { Operations } from '../../authorization'; -import { createCaseError } from '../../common'; +import { createCaseError } from '../../common/error'; import { CasesClient } from '../client'; import { CasesClientArgs } from '../types'; import { AlertsCount } from './alerts_count'; diff --git a/x-pack/plugins/cases/server/client/metrics/lifespan.ts b/x-pack/plugins/cases/server/client/metrics/lifespan.ts index ed1470738b366..5302b610c7aa0 100644 --- a/x-pack/plugins/cases/server/client/metrics/lifespan.ts +++ b/x-pack/plugins/cases/server/client/metrics/lifespan.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseMetricsResponse } from '../../../common'; +import { CaseMetricsResponse } from '../../../common/api'; import { CasesClient } from '../client'; import { MetricsHandler } from './types'; diff --git a/x-pack/plugins/cases/server/client/metrics/types.ts b/x-pack/plugins/cases/server/client/metrics/types.ts index 82038f76feaa2..7dd3b22821538 100644 --- a/x-pack/plugins/cases/server/client/metrics/types.ts +++ b/x-pack/plugins/cases/server/client/metrics/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseMetricsResponse } from '../../../common'; +import { CaseMetricsResponse } from '../../../common/api'; export interface MetricsHandler { getFeatures(): Set; diff --git a/x-pack/plugins/cases/server/client/stats/client.ts b/x-pack/plugins/cases/server/client/stats/client.ts index b13a15c799f9d..a7716ba7a3da2 100644 --- a/x-pack/plugins/cases/server/client/stats/client.ts +++ b/x-pack/plugins/cases/server/client/stats/client.ts @@ -19,9 +19,9 @@ import { throwErrors, excess, CasesStatusRequestRt, -} from '../../../common'; +} from '../../../common/api'; import { Operations } from '../../authorization'; -import { createCaseError } from '../../common'; +import { createCaseError } from '../../common/error'; import { constructQueryOptions } from '../utils'; /** diff --git a/x-pack/plugins/cases/server/client/sub_cases/client.ts b/x-pack/plugins/cases/server/client/sub_cases/client.ts index 9b0395bbcb3b6..7350b6d6cab79 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/client.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/client.ts @@ -10,24 +10,19 @@ import Boom from '@hapi/boom'; import { SavedObject } from 'kibana/server'; import { - CASE_SAVED_OBJECT, caseStatuses, CommentAttributes, - MAX_CONCURRENT_SEARCHES, SubCaseResponse, SubCaseResponseRt, SubCasesFindRequest, SubCasesFindResponse, SubCasesFindResponseRt, SubCasesPatchRequest, -} from '../../../common'; +} from '../../../common/api'; +import { CASE_SAVED_OBJECT, MAX_CONCURRENT_SEARCHES } from '../../../common/constants'; import { CasesClientArgs } from '..'; -import { - countAlertsForID, - createCaseError, - flattenSubCaseSavedObject, - transformSubCases, -} from '../../common'; +import { createCaseError } from '../../common/error'; +import { countAlertsForID, flattenSubCaseSavedObject, transformSubCases } from '../../common/utils'; import { buildCaseUserActionItem } from '../../services/user_actions/helpers'; import { constructQueryOptions } from '../utils'; import { defaultPage, defaultPerPage } from '../../routes/api'; diff --git a/x-pack/plugins/cases/server/client/sub_cases/update.ts b/x-pack/plugins/cases/server/client/sub_cases/update.ts index 3f602f7979d1f..80c516ad0ac34 100644 --- a/x-pack/plugins/cases/server/client/sub_cases/update.ts +++ b/x-pack/plugins/cases/server/client/sub_cases/update.ts @@ -19,12 +19,10 @@ import { import { nodeBuilder } from '@kbn/es-query'; import { AlertService, CasesService } from '../../services'; import { - CASE_COMMENT_SAVED_OBJECT, CaseStatuses, CommentAttributes, CommentType, excess, - SUB_CASE_SAVED_OBJECT, SubCaseAttributes, SubCasePatchRequest, SubCaseResponse, @@ -35,15 +33,16 @@ import { throwErrors, User, CaseAttributes, -} from '../../../common'; +} from '../../../common/api'; +import { CASE_COMMENT_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; import { getCaseToUpdate } from '../utils'; import { buildSubCaseUserActions } from '../../services/user_actions/helpers'; +import { createCaseError } from '../../common/error'; import { createAlertUpdateRequest, - createCaseError, isCommentRequestTypeAlertOrGenAlert, flattenSubCaseSavedObject, -} from '../../common'; +} from '../../common/utils'; import { UpdateAlertRequest } from '../../client/alerts/types'; import { CasesClientArgs } from '../types'; diff --git a/x-pack/plugins/cases/server/client/typedoc_interfaces.ts b/x-pack/plugins/cases/server/client/typedoc_interfaces.ts index feeaa6b6dcb58..b1dd4c47219d8 100644 --- a/x-pack/plugins/cases/server/client/typedoc_interfaces.ts +++ b/x-pack/plugins/cases/server/client/typedoc_interfaces.ts @@ -30,7 +30,7 @@ import { SubCaseResponse, SubCasesFindResponse, SubCasesResponse, -} from '../../common'; +} from '../../common/api'; /** * These are simply to make typedoc not attempt to expand the type aliases. If it attempts to expand them diff --git a/x-pack/plugins/cases/server/client/types.ts b/x-pack/plugins/cases/server/client/types.ts index f6c97df4f8b71..e3d7b8a541b9d 100644 --- a/x-pack/plugins/cases/server/client/types.ts +++ b/x-pack/plugins/cases/server/client/types.ts @@ -7,7 +7,7 @@ import type { PublicMethodsOf } from '@kbn/utility-types'; import { SavedObjectsClientContract, Logger } from 'kibana/server'; -import { User } from '../../common'; +import { User } from '../../common/api'; import { Authorization } from '../authorization/authorization'; import { CaseConfigureService, diff --git a/x-pack/plugins/cases/server/client/user_actions/get.test.ts b/x-pack/plugins/cases/server/client/user_actions/get.test.ts index 302e069cde4d1..c735c7d41dfcf 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.test.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.test.ts @@ -5,8 +5,9 @@ * 2.0. */ -import { CaseUserActionResponse, SUB_CASE_SAVED_OBJECT } from '../../../common'; -import { SUB_CASE_REF_NAME } from '../../common'; +import { CaseUserActionResponse } from '../../../common/api'; +import { SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; +import { SUB_CASE_REF_NAME } from '../../common/constants'; import { extractAttributesWithoutSubCases } from './get'; describe('get', () => { diff --git a/x-pack/plugins/cases/server/client/user_actions/get.ts b/x-pack/plugins/cases/server/client/user_actions/get.ts index 660cf1b6a336e..0d5ee29529f83 100644 --- a/x-pack/plugins/cases/server/client/user_actions/get.ts +++ b/x-pack/plugins/cases/server/client/user_actions/get.ts @@ -9,10 +9,12 @@ import { SavedObjectReference, SavedObjectsFindResponse } from 'kibana/server'; import { CaseUserActionsResponse, CaseUserActionsResponseRt, - SUB_CASE_SAVED_OBJECT, CaseUserActionResponse, -} from '../../../common'; -import { createCaseError, checkEnabledCaseConnectorOrThrow, SUB_CASE_REF_NAME } from '../../common'; +} from '../../../common/api'; +import { SUB_CASE_SAVED_OBJECT } from '../../../common/constants'; +import { createCaseError } from '../../common/error'; +import { checkEnabledCaseConnectorOrThrow } from '../../common/utils'; +import { SUB_CASE_REF_NAME } from '../../common/constants'; import { CasesClientArgs } from '..'; import { Operations } from '../../authorization'; import { UserActionGet } from './client'; diff --git a/x-pack/plugins/cases/server/client/utils.test.ts b/x-pack/plugins/cases/server/client/utils.test.ts index 45ea6bacb0f51..e4e6777dca524 100644 --- a/x-pack/plugins/cases/server/client/utils.test.ts +++ b/x-pack/plugins/cases/server/client/utils.test.ts @@ -7,7 +7,7 @@ import { CaseConnector, CaseType, ConnectorTypes } from '../../common/api'; import { newCase } from '../routes/api/__mocks__/request_responses'; -import { transformNewCase } from '../common'; +import { transformNewCase } from '../common/utils'; import { sortToSnake } from './utils'; describe('utils', () => { diff --git a/x-pack/plugins/cases/server/client/utils.ts b/x-pack/plugins/cases/server/client/utils.ts index 87bf4d04b3e8f..a29e52b026a27 100644 --- a/x-pack/plugins/cases/server/client/utils.ts +++ b/x-pack/plugins/cases/server/client/utils.ts @@ -13,27 +13,26 @@ import { identity } from 'fp-ts/lib/function'; import { pipe } from 'fp-ts/lib/pipeable'; import { nodeBuilder, fromKueryExpression, KueryNode } from '@kbn/es-query'; +import { CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../common/constants'; import { + OWNER_FIELD, AlertCommentRequestRt, ActionsCommentRequestRt, - CASE_SAVED_OBJECT, CaseStatuses, CaseType, CommentRequest, ContextTypeUserRt, excess, - OWNER_FIELD, - SUB_CASE_SAVED_OBJECT, throwErrors, -} from '../../common'; +} from '../../common/api'; import { combineFilterWithAuthorizationFilter } from '../authorization/utils'; import { getIDsAndIndicesAsArrays, isCommentRequestTypeAlertOrGenAlert, isCommentRequestTypeUser, isCommentRequestTypeActions, - SavedObjectFindOptionsKueryNode, -} from '../common'; +} from '../common/utils'; +import { SavedObjectFindOptionsKueryNode } from '../common/types'; export const decodeCommentRequest = (comment: CommentRequest) => { if (isCommentRequestTypeUser(comment)) { diff --git a/x-pack/plugins/cases/server/common/constants.ts b/x-pack/plugins/cases/server/common/constants.ts index eba0a64a5c0be..556f34c208314 100644 --- a/x-pack/plugins/cases/server/common/constants.ts +++ b/x-pack/plugins/cases/server/common/constants.ts @@ -5,7 +5,11 @@ * 2.0. */ -import { CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, SUB_CASE_SAVED_OBJECT } from '../../common'; +import { + CASE_COMMENT_SAVED_OBJECT, + CASE_SAVED_OBJECT, + SUB_CASE_SAVED_OBJECT, +} from '../../common/constants'; /** * The name of the saved object reference indicating the action connector ID. This is stored in the Saved Object reference diff --git a/x-pack/plugins/cases/server/common/index.ts b/x-pack/plugins/cases/server/common/index.ts index 7a1e905c79a98..18bedd3ebeca8 100644 --- a/x-pack/plugins/cases/server/common/index.ts +++ b/x-pack/plugins/cases/server/common/index.ts @@ -5,11 +5,10 @@ * 2.0. */ -// TODO: https://github.com/elastic/kibana/issues/110896 -/* eslint-disable @kbn/eslint/no_export_all */ +// Careful of exporting anything from this file as any file(s) you export here will cause your functions to be exposed as public. +// If you're using functions/types/etc... internally or within integration tests it's best to import directly from their paths +// than expose the functions/types/etc... here. You should _only_ expose functions/types/etc... that need to be shared with other plugins here. -export * from './models'; -export * from './utils'; -export * from './types'; -export * from './error'; -export * from './constants'; +// When you do have to add things here you might want to consider creating a package such to share with other plugins instead as packages +// are easier to break down. +// See: https://docs.elastic.dev/kibana-dev-docs/key-concepts/platform-intro#public-plugin-api diff --git a/x-pack/plugins/cases/server/common/models/commentable_case.ts b/x-pack/plugins/cases/server/common/models/commentable_case.ts index 856d6378d5900..1c6d5ad61000a 100644 --- a/x-pack/plugins/cases/server/common/models/commentable_case.ts +++ b/x-pack/plugins/cases/server/common/models/commentable_case.ts @@ -17,7 +17,6 @@ import { import { LensServerPluginSetup } from '../../../../lens/server'; import { AssociationType, - CASE_SAVED_OBJECT, CaseResponse, CaseResponseRt, CaseSettings, @@ -27,18 +26,25 @@ import { CommentPatchRequest, CommentRequest, CommentType, - MAX_DOCS_PER_PAGE, - SUB_CASE_SAVED_OBJECT, SubCaseAttributes, User, CommentRequestUserType, CaseAttributes, -} from '../../../common'; -import { flattenCommentSavedObjects, flattenSubCaseSavedObject, transformNewComment } from '..'; +} from '../../../common/api'; +import { + CASE_SAVED_OBJECT, + MAX_DOCS_PER_PAGE, + SUB_CASE_SAVED_OBJECT, +} from '../../../common/constants'; import { AttachmentService, CasesService } from '../../services'; import { createCaseError } from '../error'; -import { countAlertsForID } from '../index'; -import { getOrUpdateLensReferences } from '../utils'; +import { + countAlertsForID, + flattenCommentSavedObjects, + flattenSubCaseSavedObject, + transformNewComment, + getOrUpdateLensReferences, +} from '../utils'; interface UpdateCommentResp { comment: SavedObjectsUpdateResponse; diff --git a/x-pack/plugins/cases/server/common/types.ts b/x-pack/plugins/cases/server/common/types.ts index 364be027221d0..7a0d46148cf26 100644 --- a/x-pack/plugins/cases/server/common/types.ts +++ b/x-pack/plugins/cases/server/common/types.ts @@ -6,7 +6,7 @@ */ import type { KueryNode } from '@kbn/es-query'; -import { SavedObjectFindOptions } from '../../common'; +import { SavedObjectFindOptions } from '../../common/api'; /** * This structure holds the alert ID and index from an alert comment diff --git a/x-pack/plugins/cases/server/common/utils.test.ts b/x-pack/plugins/cases/server/common/utils.test.ts index 51d787a0334a2..841831b70eac5 100644 --- a/x-pack/plugins/cases/server/common/utils.test.ts +++ b/x-pack/plugins/cases/server/common/utils.test.ts @@ -7,7 +7,7 @@ import { SavedObject, SavedObjectsFindResponse } from 'kibana/server'; import { lensEmbeddableFactory } from '../../../lens/server/embeddable/lens_embeddable_factory'; -import { SECURITY_SOLUTION_OWNER } from '../../common'; +import { SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { AssociationType, CaseResponse, diff --git a/x-pack/plugins/cases/server/common/utils.ts b/x-pack/plugins/cases/server/common/utils.ts index ae14603d44567..c2ab01bfa5d3d 100644 --- a/x-pack/plugins/cases/server/common/utils.ts +++ b/x-pack/plugins/cases/server/common/utils.ts @@ -13,7 +13,7 @@ import { SavedObjectReference, } from 'kibana/server'; import { flatMap, uniqWith, isEmpty, xorWith } from 'lodash'; -import { AlertInfo } from '.'; +import { AlertInfo } from './types'; import { LensServerPluginSetup } from '../../../lens/server'; import { @@ -32,12 +32,12 @@ import { CommentsResponse, CommentType, ConnectorTypes, - ENABLE_CASE_CONNECTOR, SubCaseAttributes, SubCaseResponse, SubCasesFindResponse, User, -} from '../../common'; +} from '../../common/api'; +import { ENABLE_CASE_CONNECTOR } from '../../common/constants'; import { UpdateAlertRequest } from '../client/alerts/types'; import { parseCommentString, diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index 51c45bd25444e..e5d8dbf3bb38d 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -27,7 +27,7 @@ import { createCasesClientFactory, createCasesClientMock, } from '../../client/mocks'; -import { SECURITY_SOLUTION_OWNER } from '../../../common'; +import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; const services = actionsMock.createServices(); let caseActionType: CaseActionType; diff --git a/x-pack/plugins/cases/server/connectors/case/index.ts b/x-pack/plugins/cases/server/connectors/case/index.ts index e566ab7cacc3f..f40a04349068e 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.ts @@ -13,8 +13,8 @@ import { CasePostRequest, CommentRequest, CommentType, - ENABLE_CASE_CONNECTOR, -} from '../../../common'; +} from '../../../common/api'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { CaseExecutorParamsSchema, CaseConfigurationSchema, CommentSchemaType } from './schema'; import { CaseExecutorResponse, @@ -25,7 +25,7 @@ import { import * as i18n from './translations'; import { GetActionTypeParams, isCommentGeneratedAlert, separator } from '..'; -import { createCaseError } from '../../common'; +import { createCaseError } from '../../common/error'; import { CasesClient } from '../../client'; const supportedSubActions: string[] = ['create', 'update', 'addComment']; diff --git a/x-pack/plugins/cases/server/connectors/case/schema.ts b/x-pack/plugins/cases/server/connectors/case/schema.ts index b8e46fdf5aa8c..86ee34284a661 100644 --- a/x-pack/plugins/cases/server/connectors/case/schema.ts +++ b/x-pack/plugins/cases/server/connectors/case/schema.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { CommentType, ConnectorTypes } from '../../../common'; +import { CommentType, ConnectorTypes } from '../../../common/api'; import { validateConnector } from './validators'; // Reserved for future implementation diff --git a/x-pack/plugins/cases/server/connectors/case/types.ts b/x-pack/plugins/cases/server/connectors/case/types.ts index a71007f0b4946..6a7dfd9c2e687 100644 --- a/x-pack/plugins/cases/server/connectors/case/types.ts +++ b/x-pack/plugins/cases/server/connectors/case/types.ts @@ -16,7 +16,7 @@ import { ConnectorSchema, CommentSchema, } from './schema'; -import { CaseResponse, CasesResponse } from '../../../common'; +import { CaseResponse, CasesResponse } from '../../../common/api'; export type CaseConfiguration = TypeOf; export type Connector = TypeOf; diff --git a/x-pack/plugins/cases/server/connectors/case/validators.ts b/x-pack/plugins/cases/server/connectors/case/validators.ts index 6ab4f3a21a24f..163959eec4a6a 100644 --- a/x-pack/plugins/cases/server/connectors/case/validators.ts +++ b/x-pack/plugins/cases/server/connectors/case/validators.ts @@ -6,7 +6,7 @@ */ import { Connector } from './types'; -import { ConnectorTypes } from '../../../common'; +import { ConnectorTypes } from '../../../common/api'; export const validateConnector = (connector: Connector) => { if (connector.type === ConnectorTypes.none && connector.fields !== null) { diff --git a/x-pack/plugins/cases/server/connectors/factory.ts b/x-pack/plugins/cases/server/connectors/factory.ts index d0ae7154fe5d9..40a6702f11b0f 100644 --- a/x-pack/plugins/cases/server/connectors/factory.ts +++ b/x-pack/plugins/cases/server/connectors/factory.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConnectorTypes } from '../../common'; +import { ConnectorTypes } from '../../common/api'; import { ICasesConnector, CasesConnectorsMap } from './types'; import { getCaseConnector as getJiraCaseConnector } from './jira'; import { getCaseConnector as getResilientCaseConnector } from './resilient'; diff --git a/x-pack/plugins/cases/server/connectors/index.ts b/x-pack/plugins/cases/server/connectors/index.ts index ee7c692c1525b..b5dc1cc4a8ff9 100644 --- a/x-pack/plugins/cases/server/connectors/index.ts +++ b/x-pack/plugins/cases/server/connectors/index.ts @@ -12,7 +12,7 @@ import { ContextTypeAlertSchemaType, } from './types'; import { getActionType as getCaseConnector } from './case'; -import { CommentRequest, CommentType } from '../../common'; +import { CommentRequest, CommentType } from '../../common/api'; export * from './types'; export { transformConnectorComment } from './case'; diff --git a/x-pack/plugins/cases/server/connectors/jira/format.ts b/x-pack/plugins/cases/server/connectors/jira/format.ts index b281d94062f4d..e283aff4b4ce9 100644 --- a/x-pack/plugins/cases/server/connectors/jira/format.ts +++ b/x-pack/plugins/cases/server/connectors/jira/format.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConnectorJiraTypeFields } from '../../../common'; +import { ConnectorJiraTypeFields } from '../../../common/api'; import { Format } from './types'; export const format: Format = (theCase, alerts) => { diff --git a/x-pack/plugins/cases/server/connectors/jira/types.ts b/x-pack/plugins/cases/server/connectors/jira/types.ts index 1941485ccecee..59d5741d381b9 100644 --- a/x-pack/plugins/cases/server/connectors/jira/types.ts +++ b/x-pack/plugins/cases/server/connectors/jira/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { JiraFieldsType } from '../../../common'; +import { JiraFieldsType } from '../../../common/api'; import { ICasesConnector } from '../types'; interface ExternalServiceFormatterParams extends JiraFieldsType { diff --git a/x-pack/plugins/cases/server/connectors/resilient/format.test.ts b/x-pack/plugins/cases/server/connectors/resilient/format.test.ts index 20ba0bc378934..5cfd089b9aa8d 100644 --- a/x-pack/plugins/cases/server/connectors/resilient/format.test.ts +++ b/x-pack/plugins/cases/server/connectors/resilient/format.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseResponse } from '../../../common'; +import { CaseResponse } from '../../../common/api'; import { format } from './format'; describe('IBM Resilient formatter', () => { diff --git a/x-pack/plugins/cases/server/connectors/resilient/format.ts b/x-pack/plugins/cases/server/connectors/resilient/format.ts index ba82e2e8d1ea3..64b701731c33f 100644 --- a/x-pack/plugins/cases/server/connectors/resilient/format.ts +++ b/x-pack/plugins/cases/server/connectors/resilient/format.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConnectorResilientTypeFields } from '../../../common'; +import { ConnectorResilientTypeFields } from '../../../common/api'; import { Format } from './types'; export const format: Format = (theCase, alerts) => { diff --git a/x-pack/plugins/cases/server/connectors/resilient/types.ts b/x-pack/plugins/cases/server/connectors/resilient/types.ts index 40cde0500280c..f895dccf65214 100644 --- a/x-pack/plugins/cases/server/connectors/resilient/types.ts +++ b/x-pack/plugins/cases/server/connectors/resilient/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ResilientFieldsType } from '../../../common'; +import { ResilientFieldsType } from '../../../common/api'; import { ICasesConnector } from '../types'; export type ResilientCaseConnector = ICasesConnector; diff --git a/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.ts b/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.ts index 1859ea1246f21..81a20d006c22e 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/itsm_format.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConnectorServiceNowITSMTypeFields } from '../../../common'; +import { ConnectorServiceNowITSMTypeFields } from '../../../common/api'; import { ServiceNowITSMFormat } from './types'; export const format: ServiceNowITSMFormat = (theCase, alerts) => { diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts index 706b9f2f23ab5..9b24dfa672bf4 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseResponse } from '../../../common'; +import { CaseResponse } from '../../../common/api'; import { format } from './sir_format'; describe('SIR formatter', () => { diff --git a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts index 02c9fe629f4f8..dae1045502460 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/sir_format.ts @@ -5,7 +5,7 @@ * 2.0. */ import { get } from 'lodash/fp'; -import { ConnectorServiceNowSIRTypeFields } from '../../../common'; +import { ConnectorServiceNowSIRTypeFields } from '../../../common/api'; import { ServiceNowSIRFormat, SirFieldKey, AlertFieldMappingAndValues } from './types'; export const format: ServiceNowSIRFormat = (theCase, alerts) => { diff --git a/x-pack/plugins/cases/server/connectors/servicenow/types.ts b/x-pack/plugins/cases/server/connectors/servicenow/types.ts index b0e71cbe5e743..531786730ff9a 100644 --- a/x-pack/plugins/cases/server/connectors/servicenow/types.ts +++ b/x-pack/plugins/cases/server/connectors/servicenow/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ServiceNowITSMFieldsType } from '../../../common'; +import { ServiceNowITSMFieldsType } from '../../../common/api'; import { ICasesConnector } from '../types'; interface CorrelationValues { diff --git a/x-pack/plugins/cases/server/connectors/swimlane/format.test.ts b/x-pack/plugins/cases/server/connectors/swimlane/format.test.ts index 55cbbdb68691e..e72ca3d145c99 100644 --- a/x-pack/plugins/cases/server/connectors/swimlane/format.test.ts +++ b/x-pack/plugins/cases/server/connectors/swimlane/format.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseResponse } from '../../../common'; +import { CaseResponse } from '../../../common/api'; import { format } from './format'; describe('Swimlane formatter', () => { diff --git a/x-pack/plugins/cases/server/connectors/swimlane/format.ts b/x-pack/plugins/cases/server/connectors/swimlane/format.ts index 9531e4099a4f4..48983d745150b 100644 --- a/x-pack/plugins/cases/server/connectors/swimlane/format.ts +++ b/x-pack/plugins/cases/server/connectors/swimlane/format.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { ConnectorSwimlaneTypeFields } from '../../../common'; +import { ConnectorSwimlaneTypeFields } from '../../../common/api'; import { Format } from './types'; export const format: Format = (theCase) => { diff --git a/x-pack/plugins/cases/server/connectors/types.ts b/x-pack/plugins/cases/server/connectors/types.ts index 62b2c8e6f1551..3754dfd62b7c5 100644 --- a/x-pack/plugins/cases/server/connectors/types.ts +++ b/x-pack/plugins/cases/server/connectors/types.ts @@ -6,7 +6,7 @@ */ import { Logger } from 'kibana/server'; -import { CaseResponse, ConnectorMappingsAttributes } from '../../common'; +import { CaseResponse, ConnectorMappingsAttributes } from '../../common/api'; import { CasesClientGetAlertsResponse } from '../client/alerts/types'; import { CasesClientFactory } from '../client/factory'; import { RegisterActionType } from '../types'; diff --git a/x-pack/plugins/cases/server/plugin.ts b/x-pack/plugins/cases/server/plugin.ts index 9bbc7089c033c..21ee1313ddb11 100644 --- a/x-pack/plugins/cases/server/plugin.ts +++ b/x-pack/plugins/cases/server/plugin.ts @@ -13,7 +13,7 @@ import { PluginSetupContract as ActionsPluginSetup, PluginStartContract as ActionsPluginStart, } from '../../actions/server'; -import { APP_ID, ENABLE_CASE_CONNECTOR } from '../common'; +import { APP_ID, ENABLE_CASE_CONNECTOR } from '../common/constants'; import { initCaseApi } from './routes/api'; import { diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/authc_mock.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/authc_mock.ts index a9292229d5eea..3c6bec265e3ea 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/authc_mock.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/authc_mock.ts @@ -7,7 +7,7 @@ import { AuthenticatedUser } from '../../../../../security/server'; import { securityMock } from '../../../../../security/server/mocks'; -import { nullUser } from '../../../common'; +import { nullUser } from '../../../common/utils'; function createAuthenticationMock({ currentUser, diff --git a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts index 1551f0fa611b7..6bff6c7d21725 100644 --- a/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts +++ b/x-pack/plugins/cases/server/routes/api/__fixtures__/mock_saved_objects.ts @@ -14,8 +14,8 @@ import { CommentAttributes, CommentType, ConnectorTypes, - SECURITY_SOLUTION_OWNER, -} from '../../../../common'; +} from '../../../../common/api'; +import { SECURITY_SOLUTION_OWNER } from '../../../../common/constants'; export const mockCases: Array> = [ { diff --git a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts index f3e6bcd7fc9ff..b398f9cfd1ba8 100644 --- a/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts +++ b/x-pack/plugins/cases/server/routes/api/__mocks__/request_responses.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { SECURITY_SOLUTION_OWNER, CasePostRequest, ConnectorTypes } from '../../../../common'; +import { CasePostRequest, ConnectorTypes } from '../../../../common/api'; +import { SECURITY_SOLUTION_OWNER } from '../../../../common/constants'; export const newCase: CasePostRequest = { title: 'My new case', diff --git a/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts index 3471c1dec6208..8a490e2f68bd0 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/alerts/get_cases.ts @@ -10,7 +10,8 @@ import Boom from '@hapi/boom'; import { RouteDeps } from '../../types'; import { escapeHatch, wrapError } from '../../utils'; -import { CASE_ALERTS_URL, CasesByAlertIDRequest } from '../../../../../common'; +import { CasesByAlertIDRequest } from '../../../../../common/api'; +import { CASE_ALERTS_URL } from '../../../../../common/constants'; export function initGetCasesByAlertIdApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts index 383f9b82706a4..1784a434292cc 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/delete_cases.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASES_URL } from '../../../../common'; +import { CASES_URL } from '../../../../common/constants'; export function initDeleteCasesApi({ router, logger }: RouteDeps) { router.delete( diff --git a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts index 9b3d186ca0adc..8474d781a202a 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/find_cases.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { CASES_URL, CasesFindRequest } from '../../../../common'; +import { CasesFindRequest } from '../../../../common/api'; +import { CASES_URL } from '../../../../common/constants'; import { wrapError, escapeHatch } from '../utils'; import { RouteDeps } from '../types'; diff --git a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts index 4d81b6d5e11b3..b9c50a86fb8fe 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/get_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/get_case.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASE_DETAILS_URL } from '../../../../common'; +import { CASE_DETAILS_URL } from '../../../../common/constants'; export function initGetCaseApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts index e518d3717fcda..5cde28bcb01f9 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/patch_cases.ts @@ -7,7 +7,8 @@ import { escapeHatch, wrapError } from '../utils'; import { RouteDeps } from '../types'; -import { CASES_URL, CasesPatchRequest } from '../../../../common'; +import { CasesPatchRequest } from '../../../../common/api'; +import { CASES_URL } from '../../../../common/constants'; export function initPatchCasesApi({ router, logger }: RouteDeps) { router.patch( diff --git a/x-pack/plugins/cases/server/routes/api/cases/post_case.ts b/x-pack/plugins/cases/server/routes/api/cases/post_case.ts index 6ee94df007d64..df994f18c5bbd 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/post_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/post_case.ts @@ -8,7 +8,8 @@ import { wrapError, escapeHatch } from '../utils'; import { RouteDeps } from '../types'; -import { CASES_URL, CasePostRequest } from '../../../../common'; +import { CasePostRequest } from '../../../../common/api'; +import { CASES_URL } from '../../../../common/constants'; export function initPostCaseApi({ router, logger }: RouteDeps) { router.post( diff --git a/x-pack/plugins/cases/server/routes/api/cases/push_case.ts b/x-pack/plugins/cases/server/routes/api/cases/push_case.ts index 4c467c3840c2b..2b3e7954febfe 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/push_case.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/push_case.ts @@ -12,7 +12,8 @@ import { identity } from 'fp-ts/lib/function'; import { wrapError, escapeHatch } from '../utils'; -import { throwErrors, CasePushRequestParamsRt, CASE_PUSH_URL } from '../../../../common'; +import { throwErrors, CasePushRequestParamsRt } from '../../../../common/api'; +import { CASE_PUSH_URL } from '../../../../common/constants'; import { RouteDeps } from '../types'; export function initPushCaseApi({ router, logger }: RouteDeps) { diff --git a/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts b/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts index 109cac0d977ca..8e0d0640263ec 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/reporters/get_reporters.ts @@ -7,7 +7,8 @@ import { RouteDeps } from '../../types'; import { wrapError, escapeHatch } from '../../utils'; -import { CASE_REPORTERS_URL, AllReportersFindRequest } from '../../../../../common'; +import { AllReportersFindRequest } from '../../../../../common/api'; +import { CASE_REPORTERS_URL } from '../../../../../common/constants'; export function initGetReportersApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts b/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts index 778261c048bf0..2afa96be95bc1 100644 --- a/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts +++ b/x-pack/plugins/cases/server/routes/api/cases/tags/get_tags.ts @@ -7,7 +7,8 @@ import { RouteDeps } from '../../types'; import { wrapError, escapeHatch } from '../../utils'; -import { CASE_TAGS_URL, AllTagsFindRequest } from '../../../../../common'; +import { AllTagsFindRequest } from '../../../../../common/api'; +import { CASE_TAGS_URL } from '../../../../../common/constants'; export function initGetTagsApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/comments/delete_all_comments.ts b/x-pack/plugins/cases/server/routes/api/comments/delete_all_comments.ts index 9c77b1814376f..a41d4683af2d0 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/delete_all_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/delete_all_comments.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASE_COMMENTS_URL } from '../../../../common'; +import { CASE_COMMENTS_URL } from '../../../../common/constants'; export function initDeleteAllCommentsApi({ router, logger }: RouteDeps) { router.delete( diff --git a/x-pack/plugins/cases/server/routes/api/comments/delete_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/delete_comment.ts index 6dfb188763aa1..f145fc62efc8a 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/delete_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/delete_comment.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASE_COMMENT_DETAILS_URL } from '../../../../common'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../common/constants'; export function initDeleteCommentApi({ router, logger }: RouteDeps) { router.delete( diff --git a/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts b/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts index c0e4d8901eec6..d4c65e6306a63 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/find_comments.ts @@ -12,7 +12,8 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { CASE_COMMENTS_URL, FindQueryParamsRt, throwErrors, excess } from '../../../../common'; +import { FindQueryParamsRt, throwErrors, excess } from '../../../../common/api'; +import { CASE_COMMENTS_URL } from '../../../../common/constants'; import { RouteDeps } from '../types'; import { escapeHatch, wrapError } from '../utils'; diff --git a/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts index 41a4b6f796655..b916e22c6b0ed 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/get_all_comment.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASE_COMMENTS_URL } from '../../../../common'; +import { CASE_COMMENTS_URL } from '../../../../common/constants'; export function initGetAllCommentsApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/comments/get_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/get_comment.ts index a3ba0d3f23c37..09805c00cb10a 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/get_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/get_comment.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASE_COMMENT_DETAILS_URL } from '../../../../common'; +import { CASE_COMMENT_DETAILS_URL } from '../../../../common/constants'; export function initGetCommentApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/comments/patch_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/patch_comment.ts index 687b568e67d7f..d6ac39f11b91e 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/patch_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/patch_comment.ts @@ -13,7 +13,8 @@ import Boom from '@hapi/boom'; import { RouteDeps } from '../types'; import { escapeHatch, wrapError } from '../utils'; -import { CASE_COMMENTS_URL, CommentPatchRequestRt, throwErrors } from '../../../../common'; +import { CommentPatchRequestRt, throwErrors } from '../../../../common/api'; +import { CASE_COMMENTS_URL } from '../../../../common/constants'; export function initPatchCommentApi({ router, logger }: RouteDeps) { router.patch( diff --git a/x-pack/plugins/cases/server/routes/api/comments/post_comment.ts b/x-pack/plugins/cases/server/routes/api/comments/post_comment.ts index 44871f7f0c81c..1919aef7b72b4 100644 --- a/x-pack/plugins/cases/server/routes/api/comments/post_comment.ts +++ b/x-pack/plugins/cases/server/routes/api/comments/post_comment.ts @@ -9,7 +9,8 @@ import Boom from '@hapi/boom'; import { schema } from '@kbn/config-schema'; import { escapeHatch, wrapError } from '../utils'; import { RouteDeps } from '../types'; -import { CASE_COMMENTS_URL, ENABLE_CASE_CONNECTOR, CommentRequest } from '../../../../common'; +import { CASE_COMMENTS_URL, ENABLE_CASE_CONNECTOR } from '../../../../common/constants'; +import { CommentRequest } from '../../../../common/api'; export function initPostCommentApi({ router, logger }: RouteDeps) { router.post( diff --git a/x-pack/plugins/cases/server/routes/api/configure/get_configure.ts b/x-pack/plugins/cases/server/routes/api/configure/get_configure.ts index 59f136b971da4..8222ac8fe5690 100644 --- a/x-pack/plugins/cases/server/routes/api/configure/get_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/configure/get_configure.ts @@ -7,7 +7,8 @@ import { RouteDeps } from '../types'; import { escapeHatch, wrapError } from '../utils'; -import { CASE_CONFIGURE_URL, GetConfigureFindRequest } from '../../../../common'; +import { CASE_CONFIGURE_URL } from '../../../../common/constants'; +import { GetConfigureFindRequest } from '../../../../common/api'; export function initGetCaseConfigure({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/configure/get_connectors.ts b/x-pack/plugins/cases/server/routes/api/configure/get_connectors.ts index 220481e8ff07e..46c110bbb8ba5 100644 --- a/x-pack/plugins/cases/server/routes/api/configure/get_connectors.ts +++ b/x-pack/plugins/cases/server/routes/api/configure/get_connectors.ts @@ -8,7 +8,7 @@ import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../common'; +import { CASE_CONFIGURE_CONNECTORS_URL } from '../../../../common/constants'; /* * Be aware that this api will only return 20 connectors diff --git a/x-pack/plugins/cases/server/routes/api/configure/patch_configure.ts b/x-pack/plugins/cases/server/routes/api/configure/patch_configure.ts index a50753413585b..e856a568f387a 100644 --- a/x-pack/plugins/cases/server/routes/api/configure/patch_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/configure/patch_configure.ts @@ -11,12 +11,12 @@ import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; import { - CASE_CONFIGURE_DETAILS_URL, CaseConfigureRequestParamsRt, throwErrors, CasesConfigurePatch, excess, -} from '../../../../common'; +} from '../../../../common/api'; +import { CASE_CONFIGURE_DETAILS_URL } from '../../../../common/constants'; import { RouteDeps } from '../types'; import { wrapError, escapeHatch } from '../utils'; diff --git a/x-pack/plugins/cases/server/routes/api/configure/post_configure.ts b/x-pack/plugins/cases/server/routes/api/configure/post_configure.ts index b444ed119318d..ed4c3529f2ca0 100644 --- a/x-pack/plugins/cases/server/routes/api/configure/post_configure.ts +++ b/x-pack/plugins/cases/server/routes/api/configure/post_configure.ts @@ -10,7 +10,8 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { CASE_CONFIGURE_URL, CasesConfigureRequestRt, throwErrors } from '../../../../common'; +import { CasesConfigureRequestRt, throwErrors } from '../../../../common/api'; +import { CASE_CONFIGURE_URL } from '../../../../common/constants'; import { RouteDeps } from '../types'; import { wrapError, escapeHatch } from '../utils'; diff --git a/x-pack/plugins/cases/server/routes/api/index.ts b/x-pack/plugins/cases/server/routes/api/index.ts index 70daef5b528d6..f844505369f93 100644 --- a/x-pack/plugins/cases/server/routes/api/index.ts +++ b/x-pack/plugins/cases/server/routes/api/index.ts @@ -37,7 +37,7 @@ import { initGetSubCaseApi } from './sub_case/get_sub_case'; import { initPatchSubCasesApi } from './sub_case/patch_sub_cases'; import { initFindSubCasesApi } from './sub_case/find_sub_cases'; import { initDeleteSubCasesApi } from './sub_case/delete_sub_cases'; -import { ENABLE_CASE_CONNECTOR } from '../../../common'; +import { ENABLE_CASE_CONNECTOR } from '../../../common/constants'; import { initGetCasesByAlertIdApi } from './cases/alerts/get_cases'; import { initGetAllAlertsAttachToCaseApi } from './comments/get_alerts'; import { initGetCaseMetricsApi } from './metrics/get_case_metrics'; diff --git a/x-pack/plugins/cases/server/routes/api/metrics/get_case_metrics.ts b/x-pack/plugins/cases/server/routes/api/metrics/get_case_metrics.ts index 27b9d139770ce..0cfad10b28316 100644 --- a/x-pack/plugins/cases/server/routes/api/metrics/get_case_metrics.ts +++ b/x-pack/plugins/cases/server/routes/api/metrics/get_case_metrics.ts @@ -10,7 +10,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASE_METRICS_DETAILS_URL } from '../../../../common'; +import { CASE_METRICS_DETAILS_URL } from '../../../../common/constants'; export function initGetCaseMetricsApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/stats/get_status.ts b/x-pack/plugins/cases/server/routes/api/stats/get_status.ts index 469e32b466224..4f666c399d8fd 100644 --- a/x-pack/plugins/cases/server/routes/api/stats/get_status.ts +++ b/x-pack/plugins/cases/server/routes/api/stats/get_status.ts @@ -8,7 +8,8 @@ import { RouteDeps } from '../types'; import { escapeHatch, wrapError } from '../utils'; -import { CASE_STATUS_URL, CasesStatusRequest } from '../../../../common'; +import { CasesStatusRequest } from '../../../../common/api'; +import { CASE_STATUS_URL } from '../../../../common/constants'; export function initGetCasesStatusApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/sub_case/delete_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/sub_case/delete_sub_cases.ts index 0fe436f2269b5..11b68b70390fe 100644 --- a/x-pack/plugins/cases/server/routes/api/sub_case/delete_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/sub_case/delete_sub_cases.ts @@ -8,7 +8,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { SUB_CASES_PATCH_DEL_URL } from '../../../../common'; +import { SUB_CASES_PATCH_DEL_URL } from '../../../../common/constants'; export function initDeleteSubCasesApi({ router, logger }: RouteDeps) { router.delete( diff --git a/x-pack/plugins/cases/server/routes/api/sub_case/find_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/sub_case/find_sub_cases.ts index 3049f05337b40..8ee5fa21c3a3e 100644 --- a/x-pack/plugins/cases/server/routes/api/sub_case/find_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/sub_case/find_sub_cases.ts @@ -12,7 +12,8 @@ import { pipe } from 'fp-ts/lib/pipeable'; import { fold } from 'fp-ts/lib/Either'; import { identity } from 'fp-ts/lib/function'; -import { SubCasesFindRequestRt, SUB_CASES_URL, throwErrors } from '../../../../common'; +import { SUB_CASES_URL } from '../../../../common/constants'; +import { SubCasesFindRequestRt, throwErrors } from '../../../../common/api'; import { RouteDeps } from '../types'; import { escapeHatch, wrapError } from '../utils'; diff --git a/x-pack/plugins/cases/server/routes/api/sub_case/get_sub_case.ts b/x-pack/plugins/cases/server/routes/api/sub_case/get_sub_case.ts index fea81524b526e..db3e29f5ed96e 100644 --- a/x-pack/plugins/cases/server/routes/api/sub_case/get_sub_case.ts +++ b/x-pack/plugins/cases/server/routes/api/sub_case/get_sub_case.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { SUB_CASE_DETAILS_URL } from '../../../../common'; +import { SUB_CASE_DETAILS_URL } from '../../../../common/constants'; export function initGetSubCaseApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/sub_case/patch_sub_cases.ts b/x-pack/plugins/cases/server/routes/api/sub_case/patch_sub_cases.ts index d3b24a1e3c06f..1fb260453d188 100644 --- a/x-pack/plugins/cases/server/routes/api/sub_case/patch_sub_cases.ts +++ b/x-pack/plugins/cases/server/routes/api/sub_case/patch_sub_cases.ts @@ -5,7 +5,8 @@ * 2.0. */ -import { SubCasesPatchRequest, SUB_CASES_PATCH_DEL_URL } from '../../../../common'; +import { SubCasesPatchRequest } from '../../../../common/api'; +import { SUB_CASES_PATCH_DEL_URL } from '../../../../common/constants'; import { RouteDeps } from '../types'; import { escapeHatch, wrapError } from '../utils'; diff --git a/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts b/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts index 39b277e2239ad..5944ff6176d78 100644 --- a/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts +++ b/x-pack/plugins/cases/server/routes/api/user_actions/get_all_user_actions.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { RouteDeps } from '../types'; import { wrapError } from '../utils'; -import { CASE_USER_ACTIONS_URL, SUB_CASE_USER_ACTIONS_URL } from '../../../../common'; +import { CASE_USER_ACTIONS_URL, SUB_CASE_USER_ACTIONS_URL } from '../../../../common/constants'; export function initGetAllCaseUserActionsApi({ router, logger }: RouteDeps) { router.get( diff --git a/x-pack/plugins/cases/server/routes/api/utils.test.ts b/x-pack/plugins/cases/server/routes/api/utils.test.ts index fd7c038f06bc1..c2cff04f56a49 100644 --- a/x-pack/plugins/cases/server/routes/api/utils.test.ts +++ b/x-pack/plugins/cases/server/routes/api/utils.test.ts @@ -6,7 +6,7 @@ */ import { isBoom, boomify } from '@hapi/boom'; -import { HTTPError } from '../../common'; +import { HTTPError } from '../../common/error'; import { wrapError } from './utils'; describe('Utils', () => { diff --git a/x-pack/plugins/cases/server/routes/api/utils.ts b/x-pack/plugins/cases/server/routes/api/utils.ts index cb4804aab0054..a09fd4cc9c746 100644 --- a/x-pack/plugins/cases/server/routes/api/utils.ts +++ b/x-pack/plugins/cases/server/routes/api/utils.ts @@ -9,7 +9,7 @@ import { Boom, boomify, isBoom } from '@hapi/boom'; import { schema } from '@kbn/config-schema'; import { CustomHttpResponseOptions, ResponseError } from 'kibana/server'; -import { CaseError, isCaseError, HTTPError, isHTTPError } from '../../common'; +import { CaseError, isCaseError, HTTPError, isHTTPError } from '../../common/error'; /** * Transforms an error into the correct format for a kibana response. diff --git a/x-pack/plugins/cases/server/saved_object_types/cases.ts b/x-pack/plugins/cases/server/saved_object_types/cases.ts index 53c52c03afa12..b94b387798262 100644 --- a/x-pack/plugins/cases/server/saved_object_types/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/cases.ts @@ -12,7 +12,7 @@ import { SavedObjectsExportTransformContext, SavedObjectsType, } from 'src/core/server'; -import { CASE_SAVED_OBJECT } from '../../common'; +import { CASE_SAVED_OBJECT } from '../../common/constants'; import { ESCaseAttributes } from '../services/cases/types'; import { handleExport } from './import_export/export'; import { caseMigrations } from './migrations'; diff --git a/x-pack/plugins/cases/server/saved_object_types/comments.ts b/x-pack/plugins/cases/server/saved_object_types/comments.ts index c950a432a3440..bc0993a345a5b 100644 --- a/x-pack/plugins/cases/server/saved_object_types/comments.ts +++ b/x-pack/plugins/cases/server/saved_object_types/comments.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsType } from 'src/core/server'; -import { CASE_COMMENT_SAVED_OBJECT } from '../../common'; +import { CASE_COMMENT_SAVED_OBJECT } from '../../common/constants'; import { createCommentsMigrations, CreateCommentsMigrationsDeps } from './migrations'; export const createCaseCommentSavedObjectType = ({ diff --git a/x-pack/plugins/cases/server/saved_object_types/configure.ts b/x-pack/plugins/cases/server/saved_object_types/configure.ts index de478cae9326e..c9303b848dc23 100644 --- a/x-pack/plugins/cases/server/saved_object_types/configure.ts +++ b/x-pack/plugins/cases/server/saved_object_types/configure.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsType } from 'src/core/server'; -import { CASE_CONFIGURE_SAVED_OBJECT } from '../../common'; +import { CASE_CONFIGURE_SAVED_OBJECT } from '../../common/constants'; import { configureMigrations } from './migrations'; export const caseConfigureSavedObjectType: SavedObjectsType = { diff --git a/x-pack/plugins/cases/server/saved_object_types/connector_mappings.ts b/x-pack/plugins/cases/server/saved_object_types/connector_mappings.ts index 479edcba21534..b9bb275f080cf 100644 --- a/x-pack/plugins/cases/server/saved_object_types/connector_mappings.ts +++ b/x-pack/plugins/cases/server/saved_object_types/connector_mappings.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsType } from 'src/core/server'; -import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../common'; +import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../common/constants'; import { connectorMappingsMigrations } from './migrations'; export const caseConnectorMappingsSavedObjectType: SavedObjectsType = { diff --git a/x-pack/plugins/cases/server/saved_object_types/import_export/export.ts b/x-pack/plugins/cases/server/saved_object_types/import_export/export.ts index d089079314443..2a07d1ee978a0 100644 --- a/x-pack/plugins/cases/server/saved_object_types/import_export/export.ts +++ b/x-pack/plugins/cases/server/saved_object_types/import_export/export.ts @@ -12,16 +12,16 @@ import { SavedObjectsClientContract, SavedObjectsExportTransformContext, } from 'kibana/server'; +import { CaseUserActionAttributes, CommentAttributes } from '../../../common/api'; import { - CaseUserActionAttributes, CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, CASE_USER_ACTION_SAVED_OBJECT, - CommentAttributes, MAX_DOCS_PER_PAGE, SAVED_OBJECT_TYPES, -} from '../../../common'; -import { createCaseError, defaultSortField } from '../../common'; +} from '../../../common/constants'; +import { defaultSortField } from '../../common/utils'; +import { createCaseError } from '../../common/error'; import { ESCaseAttributes } from '../../services/cases/types'; export async function handleExport({ diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts index 9020f65ae352c..7bfaec76adf21 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.test.ts @@ -9,11 +9,11 @@ import { SavedObjectSanitizedDoc } from 'kibana/server'; import { CaseAttributes, CaseFullExternalService, - CASE_SAVED_OBJECT, ConnectorTypes, noneConnectorId, -} from '../../../common'; -import { getNoneCaseConnector } from '../../common'; +} from '../../../common/api'; +import { CASE_SAVED_OBJECT } from '../../../common/constants'; +import { getNoneCaseConnector } from '../../common/utils'; import { createExternalService, ESCaseConnectorWithId } from '../../services/test_utils'; import { caseConnectorIdMigration } from './cases'; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts index 80f02fa3bf6a6..bc85253270cb0 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/cases.ts @@ -13,12 +13,15 @@ import { SavedObjectSanitizedDoc, } from '../../../../../../src/core/server'; import { ESConnectorFields } from '../../services'; -import { ConnectorTypes, CaseType } from '../../../common'; +import { ConnectorTypes, CaseType } from '../../../common/api'; import { transformConnectorIdToReference, transformPushConnectorIdToReference, } from '../../services/user_actions/transform'; -import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../../common'; +import { + CONNECTOR_ID_REFERENCE_NAME, + PUSH_CONNECTOR_ID_REFERENCE_NAME, +} from '../../common/constants'; interface UnsanitizedCaseConnector { connector_id: string; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/comments.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/comments.ts index d754aec636693..e67e1c8b59887 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/comments.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/comments.ts @@ -20,7 +20,7 @@ import { SavedObjectMigrationFn, SavedObjectMigrationMap, } from '../../../../../../src/core/server'; -import { CommentType, AssociationType } from '../../../common'; +import { CommentType, AssociationType } from '../../../common/api'; import { isLensMarkdownNode, LensMarkdownNode, diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts index 9ae0285598dbf..7d9189673079d 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.test.ts @@ -7,12 +7,10 @@ import { SavedObjectSanitizedDoc } from 'kibana/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { - CASE_CONFIGURE_SAVED_OBJECT, - ConnectorTypes, - SECURITY_SOLUTION_OWNER, -} from '../../../common'; -import { getNoneCaseConnector, CONNECTOR_ID_REFERENCE_NAME } from '../../common'; +import { ConnectorTypes } from '../../../common/api'; +import { CASE_CONFIGURE_SAVED_OBJECT, SECURITY_SOLUTION_OWNER } from '../../../common/constants'; +import { CONNECTOR_ID_REFERENCE_NAME } from '../../common/constants'; +import { getNoneCaseConnector } from '../../common/utils'; import { ESCaseConnectorWithId } from '../../services/test_utils'; import { ESCasesConfigureAttributes } from '../../services/configure/types'; import { configureConnectorIdMigration } from './configuration'; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts index f9937253e0d2f..6cd9b5455b978 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/configuration.ts @@ -11,10 +11,10 @@ import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc, } from '../../../../../../src/core/server'; -import { ConnectorTypes } from '../../../common'; +import { ConnectorTypes } from '../../../common/api'; import { addOwnerToSO, SanitizedCaseOwner } from '.'; import { transformConnectorIdToReference } from '../../services/user_actions/transform'; -import { CONNECTOR_ID_REFERENCE_NAME } from '../../common'; +import { CONNECTOR_ID_REFERENCE_NAME } from '../../common/constants'; interface UnsanitizedConfigureConnector { connector_id: string; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts index b0f9c7d2145de..105692b667fb5 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/index.ts @@ -9,7 +9,7 @@ import { SavedObjectUnsanitizedDoc, SavedObjectSanitizedDoc, } from '../../../../../../src/core/server'; -import { SECURITY_SOLUTION_OWNER } from '../../../common'; +import { SECURITY_SOLUTION_OWNER } from '../../../common/constants'; export { caseMigrations } from './cases'; export { configureMigrations } from './configuration'; diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.test.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.test.ts index e71c8db0db694..e9ba80322c222 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.test.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.test.ts @@ -9,7 +9,8 @@ import { SavedObjectMigrationContext, SavedObjectSanitizedDoc } from 'kibana/server'; import { migrationMocks } from 'src/core/server/mocks'; -import { CaseUserActionAttributes, CASE_USER_ACTION_SAVED_OBJECT } from '../../../common'; +import { CaseUserActionAttributes } from '../../../common/api'; +import { CASE_USER_ACTION_SAVED_OBJECT } from '../../../common/constants'; import { createConnectorObject, createExternalService, diff --git a/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.ts b/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.ts index ed6b57ef647f9..a47104dfed5f7 100644 --- a/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.ts +++ b/x-pack/plugins/cases/server/saved_object_types/migrations/user_actions.ts @@ -14,7 +14,8 @@ import { SavedObjectMigrationContext, LogMeta, } from '../../../../../../src/core/server'; -import { ConnectorTypes, isCreateConnector, isPush, isUpdateConnector } from '../../../common'; +import { isPush, isUpdateConnector, isCreateConnector } from '../../../common/utils/user_actions'; +import { ConnectorTypes } from '../../../common/api'; import { extractConnectorIdFromJson } from '../../services/user_actions/transform'; import { UserActionFieldType } from '../../services/user_actions/types'; diff --git a/x-pack/plugins/cases/server/saved_object_types/sub_case.ts b/x-pack/plugins/cases/server/saved_object_types/sub_case.ts index f7cf2aa65f821..469b27d4b40ba 100644 --- a/x-pack/plugins/cases/server/saved_object_types/sub_case.ts +++ b/x-pack/plugins/cases/server/saved_object_types/sub_case.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsType } from 'src/core/server'; -import { SUB_CASE_SAVED_OBJECT } from '../../common'; +import { SUB_CASE_SAVED_OBJECT } from '../../common/constants'; import { subCasesMigrations } from './migrations'; export const subCaseSavedObjectType: SavedObjectsType = { diff --git a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts index 8fea9460e77c5..2af2fd4c7e883 100644 --- a/x-pack/plugins/cases/server/saved_object_types/user_actions.ts +++ b/x-pack/plugins/cases/server/saved_object_types/user_actions.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsType } from 'src/core/server'; -import { CASE_USER_ACTION_SAVED_OBJECT } from '../../common'; +import { CASE_USER_ACTION_SAVED_OBJECT } from '../../common/constants'; import { userActionsMigrations } from './migrations'; export const caseUserActionSavedObjectType: SavedObjectsType = { diff --git a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts index 28672160a0737..75a896a4b81fd 100644 --- a/x-pack/plugins/cases/server/scripts/sub_cases/index.ts +++ b/x-pack/plugins/cases/server/scripts/sub_cases/index.ts @@ -12,14 +12,8 @@ import yargs from 'yargs'; import { ToolingLog } from '@kbn/dev-utils'; import { KbnClient } from '@kbn/test'; -import { - CaseResponse, - CaseType, - CommentType, - ConnectorTypes, - CASES_URL, - SECURITY_SOLUTION_OWNER, -} from '../../../common'; +import { CaseResponse, CaseType, CommentType, ConnectorTypes } from '../../../common/api'; +import { CASES_URL, SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { ActionResult, ActionTypeExecutorResult } from '../../../../actions/common'; import { ContextTypeGeneratedAlertType, createAlertsString } from '../../connectors'; import { diff --git a/x-pack/plugins/cases/server/services/alerts/index.test.ts b/x-pack/plugins/cases/server/services/alerts/index.test.ts index 2c98da198fa07..3104b85e0b0b9 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.test.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CaseStatuses } from '../../../common'; +import { CaseStatuses } from '../../../common/api'; import { AlertService } from '.'; import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks'; diff --git a/x-pack/plugins/cases/server/services/alerts/index.ts b/x-pack/plugins/cases/server/services/alerts/index.ts index ca7bfe66804f3..424bbd9814e97 100644 --- a/x-pack/plugins/cases/server/services/alerts/index.ts +++ b/x-pack/plugins/cases/server/services/alerts/index.ts @@ -9,8 +9,10 @@ import pMap from 'p-map'; import { isEmpty } from 'lodash'; import { ElasticsearchClient, Logger } from 'kibana/server'; -import { CaseStatuses, MAX_ALERTS_PER_SUB_CASE, MAX_CONCURRENT_SEARCHES } from '../../../common'; -import { AlertInfo, createCaseError } from '../../common'; +import { CaseStatuses } from '../../../common/api'; +import { MAX_ALERTS_PER_SUB_CASE, MAX_CONCURRENT_SEARCHES } from '../../../common/constants'; +import { createCaseError } from '../../common/error'; +import { AlertInfo } from '../../common/types'; import { UpdateAlertRequest } from '../../client/alerts/types'; import { ALERT_WORKFLOW_STATUS, diff --git a/x-pack/plugins/cases/server/services/attachments/index.ts b/x-pack/plugins/cases/server/services/attachments/index.ts index 95a66fd9af192..f4e858eb0ed4f 100644 --- a/x-pack/plugins/cases/server/services/attachments/index.ts +++ b/x-pack/plugins/cases/server/services/attachments/index.ts @@ -15,13 +15,15 @@ import { import type { KueryNode } from '@kbn/es-query'; import { AttributesTypeAlerts, - CASE_COMMENT_SAVED_OBJECT, CommentAttributes as AttachmentAttributes, CommentPatchAttributes as AttachmentPatchAttributes, + CommentType, +} from '../../../common/api'; +import { + CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, MAX_DOCS_PER_PAGE, - CommentType, -} from '../../../common'; +} from '../../../common/constants'; import { ClientArgs } from '..'; import { buildFilter, combineFilters } from '../../client/utils'; diff --git a/x-pack/plugins/cases/server/services/cases/index.test.ts b/x-pack/plugins/cases/server/services/cases/index.test.ts index 8c71abe5bff4f..d813a9fc06a66 100644 --- a/x-pack/plugins/cases/server/services/cases/index.test.ts +++ b/x-pack/plugins/cases/server/services/cases/index.test.ts @@ -13,12 +13,8 @@ * connector.id. */ -import { - CaseAttributes, - CaseConnector, - CaseFullExternalService, - CASE_SAVED_OBJECT, -} from '../../../common'; +import { CaseAttributes, CaseConnector, CaseFullExternalService } from '../../../common/api'; +import { CASE_SAVED_OBJECT } from '../../../common/constants'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { SavedObject, @@ -30,7 +26,8 @@ import { } from 'kibana/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/mocks'; -import { getNoneCaseConnector, CONNECTOR_ID_REFERENCE_NAME } from '../../common'; +import { CONNECTOR_ID_REFERENCE_NAME } from '../../common/constants'; +import { getNoneCaseConnector } from '../../common/utils'; import { CasesService } from '.'; import { createESJiraConnector, diff --git a/x-pack/plugins/cases/server/services/cases/index.ts b/x-pack/plugins/cases/server/services/cases/index.ts index 15e60c49768a5..7285761e6558a 100644 --- a/x-pack/plugins/cases/server/services/cases/index.ts +++ b/x-pack/plugins/cases/server/services/cases/index.ts @@ -24,9 +24,17 @@ import { nodeBuilder, KueryNode } from '@kbn/es-query'; import { SecurityPluginSetup } from '../../../../security/server'; import { - AssociationType, CASE_COMMENT_SAVED_OBJECT, CASE_SAVED_OBJECT, + ENABLE_CASE_CONNECTOR, + MAX_CONCURRENT_SEARCHES, + MAX_DOCS_PER_PAGE, + SUB_CASE_SAVED_OBJECT, +} from '../../../common/constants'; +import { + OWNER_FIELD, + GetCaseIdsByAlertIdAggs, + AssociationType, CaseResponse, CasesFindRequest, CaseStatuses, @@ -34,24 +42,18 @@ import { caseTypeField, CommentAttributes, CommentType, - ENABLE_CASE_CONNECTOR, - GetCaseIdsByAlertIdAggs, - MAX_CONCURRENT_SEARCHES, - MAX_DOCS_PER_PAGE, - OWNER_FIELD, - SUB_CASE_SAVED_OBJECT, SubCaseAttributes, SubCaseResponse, User, CaseAttributes, -} from '../../../common'; +} from '../../../common/api'; +import { SavedObjectFindOptionsKueryNode } from '../../common/types'; import { defaultSortField, flattenCaseSavedObject, flattenSubCaseSavedObject, groupTotalAlertsByID, - SavedObjectFindOptionsKueryNode, -} from '../../common'; +} from '../../common/utils'; import { defaultPage, defaultPerPage } from '../../routes/api'; import { ClientArgs } from '..'; import { combineFilters } from '../../client/utils'; diff --git a/x-pack/plugins/cases/server/services/cases/transform.test.ts b/x-pack/plugins/cases/server/services/cases/transform.test.ts index 96312d00b37dd..b28f364dbd03f 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.test.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.test.ts @@ -17,12 +17,12 @@ import { transformUpdateResponseToExternalModel, } from './transform'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; -import { ConnectorTypes } from '../../../common'; +import { ConnectorTypes } from '../../../common/api'; import { - getNoneCaseConnector, CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME, -} from '../../common'; +} from '../../common/constants'; +import { getNoneCaseConnector } from '../../common/utils'; describe('case transforms', () => { describe('transformUpdateResponseToExternalModel', () => { diff --git a/x-pack/plugins/cases/server/services/cases/transform.ts b/x-pack/plugins/cases/server/services/cases/transform.ts index e3609689871d2..260847b326a86 100644 --- a/x-pack/plugins/cases/server/services/cases/transform.ts +++ b/x-pack/plugins/cases/server/services/cases/transform.ts @@ -17,8 +17,11 @@ import { } from 'kibana/server'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from './types'; -import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../../common'; -import { CaseAttributes, CaseFullExternalService } from '../../../common'; +import { + CONNECTOR_ID_REFERENCE_NAME, + PUSH_CONNECTOR_ID_REFERENCE_NAME, +} from '../../common/constants'; +import { CaseAttributes, CaseFullExternalService } from '../../../common/api'; import { findConnectorIdReference, transformFieldsToESModel, diff --git a/x-pack/plugins/cases/server/services/cases/types.ts b/x-pack/plugins/cases/server/services/cases/types.ts index 55c736b032590..eb47850eeef31 100644 --- a/x-pack/plugins/cases/server/services/cases/types.ts +++ b/x-pack/plugins/cases/server/services/cases/types.ts @@ -6,7 +6,7 @@ */ import * as rt from 'io-ts'; -import { CaseAttributes, CaseExternalServiceBasicRt } from '../../../common'; +import { CaseAttributes, CaseExternalServiceBasicRt } from '../../../common/api'; import { ESCaseConnector } from '..'; /** diff --git a/x-pack/plugins/cases/server/services/configure/index.test.ts b/x-pack/plugins/cases/server/services/configure/index.test.ts index 876cb7b21f81a..2b30e4d4de628 100644 --- a/x-pack/plugins/cases/server/services/configure/index.test.ts +++ b/x-pack/plugins/cases/server/services/configure/index.test.ts @@ -9,10 +9,9 @@ import { CaseConnector, CasesConfigureAttributes, CasesConfigurePatch, - CASE_CONFIGURE_SAVED_OBJECT, ConnectorTypes, - SECURITY_SOLUTION_OWNER, -} from '../../../common'; +} from '../../../common/api'; +import { CASE_CONFIGURE_SAVED_OBJECT, SECURITY_SOLUTION_OWNER } from '../../../common/constants'; import { savedObjectsClientMock } from '../../../../../../src/core/server/mocks'; import { SavedObject, @@ -26,7 +25,8 @@ import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { loggerMock } from '@kbn/logging/mocks'; import { CaseConfigureService } from '.'; import { ESCasesConfigureAttributes } from './types'; -import { getNoneCaseConnector, CONNECTOR_ID_REFERENCE_NAME } from '../../common'; +import { CONNECTOR_ID_REFERENCE_NAME } from '../../common/constants'; +import { getNoneCaseConnector } from '../../common/utils'; import { createESJiraConnector, createJiraConnector, ESCaseConnectorWithId } from '../test_utils'; const basicConfigFields = { diff --git a/x-pack/plugins/cases/server/services/configure/index.ts b/x-pack/plugins/cases/server/services/configure/index.ts index db6d033f23ca8..0c22d95a5ee33 100644 --- a/x-pack/plugins/cases/server/services/configure/index.ts +++ b/x-pack/plugins/cases/server/services/configure/index.ts @@ -13,12 +13,10 @@ import { SavedObjectsUpdateResponse, } from 'kibana/server'; -import { SavedObjectFindOptionsKueryNode, CONNECTOR_ID_REFERENCE_NAME } from '../../common'; -import { - CASE_CONFIGURE_SAVED_OBJECT, - CasesConfigureAttributes, - CasesConfigurePatch, -} from '../../../common'; +import { SavedObjectFindOptionsKueryNode } from '../../common/types'; +import { CONNECTOR_ID_REFERENCE_NAME } from '../../common/constants'; +import { CasesConfigureAttributes, CasesConfigurePatch } from '../../../common/api'; +import { CASE_CONFIGURE_SAVED_OBJECT } from '../../../common/constants'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { transformFieldsToESModel, diff --git a/x-pack/plugins/cases/server/services/configure/types.ts b/x-pack/plugins/cases/server/services/configure/types.ts index f52e05a2ff9b5..3c4405e532e69 100644 --- a/x-pack/plugins/cases/server/services/configure/types.ts +++ b/x-pack/plugins/cases/server/services/configure/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CasesConfigureAttributes } from '../../../common'; +import { CasesConfigureAttributes } from '../../../common/api'; import { ESCaseConnector } from '..'; /** diff --git a/x-pack/plugins/cases/server/services/connector_mappings/index.ts b/x-pack/plugins/cases/server/services/connector_mappings/index.ts index 0798b35a78a4c..46b9f4bac0064 100644 --- a/x-pack/plugins/cases/server/services/connector_mappings/index.ts +++ b/x-pack/plugins/cases/server/services/connector_mappings/index.ts @@ -7,8 +7,9 @@ import { Logger, SavedObjectReference, SavedObjectsClientContract } from 'kibana/server'; -import { ConnectorMappings, CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../../common'; -import { SavedObjectFindOptionsKueryNode } from '../../common'; +import { CASE_CONNECTOR_MAPPINGS_SAVED_OBJECT } from '../../../common/constants'; +import { ConnectorMappings } from '../../../common/api'; +import { SavedObjectFindOptionsKueryNode } from '../../common/types'; interface ClientArgs { unsecuredSavedObjectsClient: SavedObjectsClientContract; diff --git a/x-pack/plugins/cases/server/services/connector_reference_handler.test.ts b/x-pack/plugins/cases/server/services/connector_reference_handler.test.ts index 4c42332d10627..8b0bc527f9909 100644 --- a/x-pack/plugins/cases/server/services/connector_reference_handler.test.ts +++ b/x-pack/plugins/cases/server/services/connector_reference_handler.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { noneConnectorId } from '../../common'; +import { noneConnectorId } from '../../common/api'; import { ConnectorReferenceHandler } from './connector_reference_handler'; describe('ConnectorReferenceHandler', () => { diff --git a/x-pack/plugins/cases/server/services/connector_reference_handler.ts b/x-pack/plugins/cases/server/services/connector_reference_handler.ts index 81e1541366ab5..833cba26f0d1e 100644 --- a/x-pack/plugins/cases/server/services/connector_reference_handler.ts +++ b/x-pack/plugins/cases/server/services/connector_reference_handler.ts @@ -6,7 +6,7 @@ */ import { SavedObjectReference } from 'kibana/server'; -import { noneConnectorId } from '../../common'; +import { noneConnectorId } from '../../common/api'; interface Reference { soReference?: SavedObjectReference; diff --git a/x-pack/plugins/cases/server/services/index.ts b/x-pack/plugins/cases/server/services/index.ts index c14e1fab4a410..9a43aa43c2874 100644 --- a/x-pack/plugins/cases/server/services/index.ts +++ b/x-pack/plugins/cases/server/services/index.ts @@ -6,7 +6,7 @@ */ import { SavedObjectsClientContract } from 'kibana/server'; -import { ConnectorTypes } from '../../common'; +import { ConnectorTypes } from '../../common/api'; export { CasesService } from './cases'; export { CaseConfigureService } from './configure'; diff --git a/x-pack/plugins/cases/server/services/test_utils.ts b/x-pack/plugins/cases/server/services/test_utils.ts index 07743eda61212..c76ad0d83410b 100644 --- a/x-pack/plugins/cases/server/services/test_utils.ts +++ b/x-pack/plugins/cases/server/services/test_utils.ts @@ -7,17 +7,16 @@ import { SavedObject, SavedObjectReference, SavedObjectsFindResult } from 'kibana/server'; import { ESConnectorFields } from '.'; -import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../common'; +import { CONNECTOR_ID_REFERENCE_NAME, PUSH_CONNECTOR_ID_REFERENCE_NAME } from '../common/constants'; import { CaseConnector, CaseFullExternalService, CaseStatuses, CaseType, - CASE_SAVED_OBJECT, ConnectorTypes, noneConnectorId, - SECURITY_SOLUTION_OWNER, -} from '../../common'; +} from '../../common/api'; +import { CASE_SAVED_OBJECT, SECURITY_SOLUTION_OWNER } from '../../common/constants'; import { ESCaseAttributes, ExternalServicesWithoutConnectorId } from './cases/types'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; diff --git a/x-pack/plugins/cases/server/services/transform.test.ts b/x-pack/plugins/cases/server/services/transform.test.ts index b4346595e4998..f7f49d285b80c 100644 --- a/x-pack/plugins/cases/server/services/transform.test.ts +++ b/x-pack/plugins/cases/server/services/transform.test.ts @@ -6,7 +6,7 @@ */ import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; -import { ConnectorTypes } from '../../common'; +import { ConnectorTypes } from '../../common/api'; import { createESJiraConnector, createJiraConnector } from './test_utils'; import { findConnectorIdReference, diff --git a/x-pack/plugins/cases/server/services/transform.ts b/x-pack/plugins/cases/server/services/transform.ts index 39351d3a4b50a..8956bfe42954e 100644 --- a/x-pack/plugins/cases/server/services/transform.ts +++ b/x-pack/plugins/cases/server/services/transform.ts @@ -6,9 +6,9 @@ */ import { SavedObjectReference } from 'kibana/server'; -import { CaseConnector, ConnectorTypeFields } from '../../common'; +import { CaseConnector, ConnectorTypeFields } from '../../common/api'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../actions/server'; -import { getNoneCaseConnector } from '../common'; +import { getNoneCaseConnector } from '../common/utils'; import { ESCaseConnector, ESConnectorFields } from '.'; export function findConnectorIdReference( diff --git a/x-pack/plugins/cases/server/services/user_actions/helpers.test.ts b/x-pack/plugins/cases/server/services/user_actions/helpers.test.ts index 7bcbaf58d0f6e..e528ca67ce4c2 100644 --- a/x-pack/plugins/cases/server/services/user_actions/helpers.test.ts +++ b/x-pack/plugins/cases/server/services/user_actions/helpers.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { UserActionField } from '../../../common'; +import { UserActionField } from '../../../common/api'; import { createConnectorObject, createExternalService, createJiraConnector } from '../test_utils'; import { buildCaseUserActionItem } from './helpers'; diff --git a/x-pack/plugins/cases/server/services/user_actions/helpers.ts b/x-pack/plugins/cases/server/services/user_actions/helpers.ts index e91b69f0995bd..d99c1dbbb29e4 100644 --- a/x-pack/plugins/cases/server/services/user_actions/helpers.ts +++ b/x-pack/plugins/cases/server/services/user_actions/helpers.ts @@ -10,22 +10,24 @@ import { get, isPlainObject, isString } from 'lodash'; import deepEqual from 'fast-deep-equal'; import { - CASE_COMMENT_SAVED_OBJECT, - CASE_SAVED_OBJECT, CaseUserActionAttributes, - OWNER_FIELD, - SUB_CASE_SAVED_OBJECT, SubCaseAttributes, User, UserAction, UserActionField, CaseAttributes, -} from '../../../common'; + OWNER_FIELD, +} from '../../../common/api'; +import { + CASE_COMMENT_SAVED_OBJECT, + CASE_SAVED_OBJECT, + SUB_CASE_SAVED_OBJECT, +} from '../../../common/constants'; import { isTwoArraysDifference } from '../../client/utils'; import { UserActionItem } from '.'; import { extractConnectorId } from './transform'; import { UserActionFieldType } from './types'; -import { CASE_REF_NAME, COMMENT_REF_NAME, SUB_CASE_REF_NAME } from '../../common'; +import { CASE_REF_NAME, COMMENT_REF_NAME, SUB_CASE_REF_NAME } from '../../common/constants'; interface BuildCaseUserActionParams { action: UserAction; diff --git a/x-pack/plugins/cases/server/services/user_actions/index.test.ts b/x-pack/plugins/cases/server/services/user_actions/index.test.ts index c4a350f4ac015..a35fb8f1baba7 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.test.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.test.ts @@ -7,12 +7,8 @@ import { SavedObject, SavedObjectsFindResult } from 'kibana/server'; import { transformFindResponseToExternalModel, UserActionItem } from '.'; -import { - CaseUserActionAttributes, - CASE_USER_ACTION_SAVED_OBJECT, - UserAction, - UserActionField, -} from '../../../common'; +import { CaseUserActionAttributes, UserAction, UserActionField } from '../../../common/api'; +import { CASE_USER_ACTION_SAVED_OBJECT } from '../../../common/constants'; import { createConnectorObject, diff --git a/x-pack/plugins/cases/server/services/user_actions/index.ts b/x-pack/plugins/cases/server/services/user_actions/index.ts index 4f158862e3d63..507c36f866611 100644 --- a/x-pack/plugins/cases/server/services/user_actions/index.ts +++ b/x-pack/plugins/cases/server/services/user_actions/index.ts @@ -12,21 +12,18 @@ import { SavedObjectsFindResult, } from 'kibana/server'; +import { isCreateConnector, isPush, isUpdateConnector } from '../../../common/utils/user_actions'; +import { CaseUserActionAttributes, CaseUserActionResponse } from '../../../common/api'; import { CASE_SAVED_OBJECT, CASE_USER_ACTION_SAVED_OBJECT, - CaseUserActionAttributes, MAX_DOCS_PER_PAGE, SUB_CASE_SAVED_OBJECT, - CaseUserActionResponse, CASE_COMMENT_SAVED_OBJECT, - isCreateConnector, - isPush, - isUpdateConnector, -} from '../../../common'; +} from '../../../common/constants'; import { ClientArgs } from '..'; import { UserActionFieldType } from './types'; -import { CASE_REF_NAME, COMMENT_REF_NAME, SUB_CASE_REF_NAME } from '../../common'; +import { CASE_REF_NAME, COMMENT_REF_NAME, SUB_CASE_REF_NAME } from '../../common/constants'; import { ConnectorIdReferenceName, PushConnectorIdReferenceName } from './transform'; import { findConnectorIdReference } from '../transform'; diff --git a/x-pack/plugins/cases/server/services/user_actions/transform.test.ts b/x-pack/plugins/cases/server/services/user_actions/transform.test.ts index 2d28770617094..a75d16c4764b6 100644 --- a/x-pack/plugins/cases/server/services/user_actions/transform.test.ts +++ b/x-pack/plugins/cases/server/services/user_actions/transform.test.ts @@ -5,14 +5,14 @@ * 2.0. */ -import { noneConnectorId } from '../../../common'; +import { noneConnectorId } from '../../../common/api'; import { CONNECTOR_ID_REFERENCE_NAME, - getNoneCaseConnector, PUSH_CONNECTOR_ID_REFERENCE_NAME, USER_ACTION_OLD_ID_REF_NAME, USER_ACTION_OLD_PUSH_ID_REF_NAME, -} from '../../common'; +} from '../../common/constants'; +import { getNoneCaseConnector } from '../../common/utils'; import { createConnectorObject, createExternalService, createJiraConnector } from '../test_utils'; import { extractConnectorIdHelper, diff --git a/x-pack/plugins/cases/server/services/user_actions/transform.ts b/x-pack/plugins/cases/server/services/user_actions/transform.ts index 93595374208a3..a3ec8a2c115b6 100644 --- a/x-pack/plugins/cases/server/services/user_actions/transform.ts +++ b/x-pack/plugins/cases/server/services/user_actions/transform.ts @@ -11,23 +11,21 @@ import * as rt from 'io-ts'; import { isString } from 'lodash'; import { SavedObjectReference } from '../../../../../../src/core/server'; +import { isCreateConnector, isPush, isUpdateConnector } from '../../../common/utils/user_actions'; import { CaseAttributes, CaseConnector, CaseConnectorRt, CaseExternalServiceBasicRt, - isCreateConnector, - isPush, - isUpdateConnector, noneConnectorId, -} from '../../../common'; +} from '../../../common/api'; import { CONNECTOR_ID_REFERENCE_NAME, - getNoneCaseConnector, PUSH_CONNECTOR_ID_REFERENCE_NAME, USER_ACTION_OLD_ID_REF_NAME, USER_ACTION_OLD_PUSH_ID_REF_NAME, -} from '../../common'; +} from '../../common/constants'; +import { getNoneCaseConnector } from '../../common/utils'; import { ACTION_SAVED_OBJECT_TYPE } from '../../../../actions/server'; import { UserActionFieldType } from './types'; diff --git a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts index 8cd5b86314a2c..376941b018f6a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts +++ b/x-pack/plugins/enterprise_search/public/applications/shared/doc_links/doc_links.ts @@ -34,6 +34,7 @@ class DocLinks { public enterpriseSearchMailService: string; public enterpriseSearchUsersAccess: string; public licenseManagement: string; + public workplaceSearchApiKeys: string; public workplaceSearchBox: string; public workplaceSearchConfluenceCloud: string; public workplaceSearchConfluenceServer: string; @@ -86,6 +87,7 @@ class DocLinks { this.enterpriseSearchMailService = ''; this.enterpriseSearchUsersAccess = ''; this.licenseManagement = ''; + this.workplaceSearchApiKeys = ''; this.workplaceSearchBox = ''; this.workplaceSearchConfluenceCloud = ''; this.workplaceSearchConfluenceServer = ''; @@ -139,6 +141,7 @@ class DocLinks { this.enterpriseSearchMailService = docLinks.links.enterpriseSearch.mailService; this.enterpriseSearchUsersAccess = docLinks.links.enterpriseSearch.usersAccess; this.licenseManagement = docLinks.links.enterpriseSearch.licenseManagement; + this.workplaceSearchApiKeys = docLinks.links.workplaceSearch.apiKeys; this.workplaceSearchBox = docLinks.links.workplaceSearch.box; this.workplaceSearchConfluenceCloud = docLinks.links.workplaceSearch.confluenceCloud; this.workplaceSearchConfluenceServer = docLinks.links.workplaceSearch.confluenceServer; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts index 85ffde0acfea3..7af40b23d9f64 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/__mocks__/content_sources.mock.ts @@ -123,6 +123,11 @@ export const fullContentSources = [ urlFieldIsLinkable: true, createdAt: '2021-01-20', serviceName: 'myService', + secret: { + app_id: '99999', + fingerprint: '65xM7s0RE6tEWNhnuXpK5EvZ5OAMIcbDHIISm/0T23Y=', + base_url: 'http://github.com', + }, }, { ...contentSources[1], diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx index 65a6a798b032a..01df4bdd02d55 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.test.tsx @@ -49,6 +49,11 @@ describe('useWorkplaceSearchNav', () => { name: 'Users and roles', href: '/users_and_roles', }, + { + id: 'apiKeys', + name: 'API keys', + href: '/api_keys', + }, { id: 'security', name: 'Security', diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx index 7dc005a56bf10..05ec569dcd292 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/layout/nav.tsx @@ -10,6 +10,7 @@ import { EuiSideNavItemType } from '@elastic/eui'; import { generateNavLink } from '../../../shared/layout'; import { NAV } from '../../constants'; import { + API_KEYS_PATH, SOURCES_PATH, SECURITY_PATH, USERS_AND_ROLES_PATH, @@ -47,6 +48,11 @@ export const useWorkplaceSearchNav = () => { name: NAV.ROLE_MAPPINGS, ...generateNavLink({ to: USERS_AND_ROLES_PATH }), }, + { + id: 'apiKeys', + name: NAV.API_KEYS, + ...generateNavLink({ to: API_KEYS_PATH }), + }, { id: 'security', name: NAV.SECURITY, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts index e6a994d05f3ff..fdccd536c3c6d 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/components/shared/assets/source_icons/index.ts @@ -31,6 +31,8 @@ export const images = { dropbox, github, githubEnterpriseServer: github, + githubViaApp: github, + githubEnterpriseServerViaApp: github, gmail, googleDrive, jira, diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts index 43da4ccef223a..9d3b2cb8aaefd 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/constants.ts @@ -42,6 +42,9 @@ export const NAV = { ROLE_MAPPINGS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.roleMappings', { defaultMessage: 'Users and roles', }), + API_KEYS: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.apiKeys', { + defaultMessage: 'API keys', + }), SECURITY: i18n.translate('xpack.enterpriseSearch.workplaceSearch.nav.security', { defaultMessage: 'Security', }), @@ -329,6 +332,20 @@ export const SOURCE_OBJ_TYPES = { ), }; +export const API_KEYS_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.apiKeysTitle', + { + defaultMessage: 'API keys', + } +); + +export const API_KEY_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.sources.apiKeyLabel', + { + defaultMessage: 'API key', + } +); + export const GITHUB_LINK_TITLE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.sources.applicationLinkTitles.github', { @@ -336,6 +353,9 @@ export const GITHUB_LINK_TITLE = i18n.translate( } ); +export const GITHUB_VIA_APP_SERVICE_TYPE = 'github_via_app'; +export const GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE = 'github_enterprise_server_via_app'; + export const CUSTOM_SERVICE_TYPE = 'custom'; export const WORKPLACE_SEARCH_URL_PREFIX = '/app/enterprise_search/workplace_search'; @@ -863,3 +883,14 @@ export const PLATINUM_FEATURE = i18n.translate( defaultMessage: 'Platinum feature', } ); + +export const COPY_TOOLTIP = i18n.translate('xpack.enterpriseSearch.workplaceSearch.copy.tooltip', { + defaultMessage: 'Copy to clipboard', +}); + +export const COPIED_TOOLTIP = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.copied.tooltip', + { + defaultMessage: 'Copied!', + } +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx index 2b24e09f96315..e7ffabd54a88c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/index.tsx @@ -28,11 +28,13 @@ import { PRIVATE_SOURCES_PATH, ORG_SETTINGS_PATH, USERS_AND_ROLES_PATH, + API_KEYS_PATH, SECURITY_PATH, PERSONAL_SETTINGS_PATH, PERSONAL_PATH, } from './routes'; import { AccountSettings } from './views/account_settings'; +import { ApiKeys } from './views/api_keys'; import { SourcesRouter } from './views/content_sources'; import { SourceAdded } from './views/content_sources/components/source_added'; import { ErrorState } from './views/error_state'; @@ -133,6 +135,9 @@ export const WorkplaceSearchConfigured: React.FC = (props) => { + + + diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts index 5f3c79f9432e7..1b630a47e2f86 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/routes.ts @@ -9,6 +9,11 @@ import { generatePath } from 'react-router-dom'; import { docLinks } from '../shared/doc_links'; +import { + GITHUB_VIA_APP_SERVICE_TYPE, + GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE, +} from './constants'; + export const SETUP_GUIDE_PATH = '/setup_guide'; export const NOT_FOUND_PATH = '/404'; @@ -53,6 +58,8 @@ export const SEARCH_AUTHORIZE_PATH = `${PERSONAL_PATH}/authorize_search`; export const USERS_AND_ROLES_PATH = '/users_and_roles'; +export const API_KEYS_PATH = '/api_keys'; + export const SECURITY_PATH = '/security'; export const GROUPS_PATH = '/groups'; @@ -70,7 +77,8 @@ export const ADD_CONFLUENCE_SERVER_PATH = `${SOURCES_PATH}/add/confluence_server export const ADD_DROPBOX_PATH = `${SOURCES_PATH}/add/dropbox`; export const ADD_GITHUB_ENTERPRISE_PATH = `${SOURCES_PATH}/add/github_enterprise_server`; export const ADD_GITHUB_PATH = `${SOURCES_PATH}/add/github`; -export const ADD_GITHUB_APP_PATH = `${SOURCES_PATH}/add/github_app`; +export const ADD_GITHUB_VIA_APP_PATH = `${SOURCES_PATH}/add/${GITHUB_VIA_APP_SERVICE_TYPE}`; +export const ADD_GITHUB_ENTERPRISE_SERVER_VIA_APP_PATH = `${SOURCES_PATH}/add/${GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE}`; export const ADD_GMAIL_PATH = `${SOURCES_PATH}/add/gmail`; export const ADD_GOOGLE_DRIVE_PATH = `${SOURCES_PATH}/add/google_drive`; export const ADD_JIRA_PATH = `${SOURCES_PATH}/add/jira_cloud`; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts index 0fa8c00409d1a..2e933d7bdf94a 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/types.ts @@ -81,7 +81,7 @@ export interface SourceDataItem { features?: Features; objTypes?: string[]; addPath: string; - editPath: string; + editPath?: string; // undefined for GitHub apps, as they are configured on a source level, and don't use a connector where you can edit the configuration accountContextOnly: boolean; } @@ -181,6 +181,12 @@ export interface IndexingConfig { schedule: IndexingSchedule; } +interface AppSecret { + app_id: string; + fingerprint: string; + base_url?: string; +} + export interface ContentSourceFullData extends ContentSourceDetails { activities: SourceActivity[]; details: DescriptionList[]; @@ -201,6 +207,7 @@ export interface ContentSourceFullData extends ContentSourceDetails { urlFieldIsLinkable: boolean; createdAt: string; serviceName: string; + secret?: AppSecret; // undefined for all content sources except GitHub apps } export interface ContentSourceStatus { @@ -295,3 +302,9 @@ export interface WSRoleMapping extends RoleMapping { allGroups: boolean; groups: RoleGroup[]; } + +export interface ApiToken { + key?: string; + id?: string; + name: string; +} diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/handle_private_key_upload.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/handle_private_key_upload.ts new file mode 100644 index 0000000000000..b1a8877c165e0 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/handle_private_key_upload.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { readUploadedFileAsText } from './read_uploaded_file_as_text'; + +export const handlePrivateKeyUpload = async ( + files: FileList | null, + callback: (text: string) => void +) => { + if (!files || files.length < 1) { + return null; + } + const file = files[0]; + const text = await readUploadedFileAsText(file); + + callback(text); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts index fb9846dbccde8..92f27500d7262 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/index.ts @@ -9,3 +9,5 @@ export { toSentenceSerial } from './to_sentence_serial'; export { getAsLocalDateTimeString } from './get_as_local_datetime_string'; export { mimeType } from './mime_types'; export { readUploadedFileAsBase64 } from './read_uploaded_file_as_base64'; +export { readUploadedFileAsText } from './read_uploaded_file_as_text'; +export { handlePrivateKeyUpload } from './handle_private_key_upload'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_text.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_text.ts new file mode 100644 index 0000000000000..c4e8e54057545 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/utils/read_uploaded_file_as_text.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const readUploadedFileAsText = (fileInput: File): Promise => { + const reader = new FileReader(); + + return new Promise((resolve, reject) => { + reader.onload = () => { + resolve(reader.result as string); + }; + try { + reader.readAsText(fileInput); + } catch { + reader.abort(); + reject(new Error()); + } + }); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/api_keys.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/api_keys.test.tsx new file mode 100644 index 0000000000000..caea725ca67a9 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/api_keys.test.tsx @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues, setMockActions } from '../../../__mocks__/kea_logic'; +import { unmountHandler } from '../../../__mocks__/shallow_useeffect.mock'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiEmptyPrompt, EuiCopy } from '@elastic/eui'; + +import { DEFAULT_META } from '../../../shared/constants'; +import { externalUrl } from '../../../shared/enterprise_search_url'; + +import { ApiKeys } from './api_keys'; +import { ApiKeyFlyout } from './components/api_key_flyout'; +import { ApiKeysList } from './components/api_keys_list'; + +describe('ApiKeys', () => { + const fetchApiKeys = jest.fn(); + const resetApiKeys = jest.fn(); + const showApiKeysForm = jest.fn(); + const apiToken = { + id: '1', + name: 'test', + key: 'foo', + }; + + const values = { + apiKeyFormVisible: false, + meta: DEFAULT_META, + dataLoading: false, + apiTokens: [apiToken], + }; + + beforeEach(() => { + jest.clearAllMocks(); + setMockValues(values); + setMockActions({ + fetchApiKeys, + resetApiKeys, + showApiKeysForm, + }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(ApiKeysList)).toHaveLength(1); + }); + + it('renders EuiEmptyPrompt when no api keys present', () => { + setMockValues({ ...values, apiTokens: [] }); + const wrapper = shallow(); + + expect(wrapper.find(ApiKeysList)).toHaveLength(0); + expect(wrapper.find(EuiEmptyPrompt)).toHaveLength(1); + }); + + it('fetches data on mount', () => { + shallow(); + + expect(fetchApiKeys).toHaveBeenCalledTimes(1); + }); + + it('calls resetApiKeys on unmount', () => { + shallow(); + unmountHandler(); + + expect(resetApiKeys).toHaveBeenCalledTimes(1); + }); + + it('renders the API endpoint and a button to copy it', () => { + externalUrl.enterpriseSearchUrl = 'http://localhost:3002'; + const copyMock = jest.fn(); + const wrapper = shallow(); + // We wrap children in a div so that `shallow` can render it. + const copyEl = shallow(
{wrapper.find(EuiCopy).props().children(copyMock)}
); + + expect(copyEl.find('EuiButtonIcon').props().onClick).toEqual(copyMock); + expect(copyEl.text().replace('', '')).toEqual('http://localhost:3002'); + }); + + it('will render ApiKeyFlyout if apiKeyFormVisible is true', () => { + setMockValues({ ...values, apiKeyFormVisible: true }); + const wrapper = shallow(); + + expect(wrapper.find(ApiKeyFlyout)).toHaveLength(1); + }); + + it('will NOT render ApiKeyFlyout if apiKeyFormVisible is false', () => { + setMockValues({ ...values, apiKeyFormVisible: false }); + const wrapper = shallow(); + + expect(wrapper.find(ApiKeyFlyout)).toHaveLength(0); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/api_keys.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/api_keys.tsx new file mode 100644 index 0000000000000..dd20020c619c8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/api_keys.tsx @@ -0,0 +1,109 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; + +import { useActions, useValues } from 'kea'; + +import { + EuiButton, + EuiTitle, + EuiPanel, + EuiCopy, + EuiButtonIcon, + EuiSpacer, + EuiEmptyPrompt, +} from '@elastic/eui'; + +import { docLinks } from '../../../shared/doc_links'; +import { externalUrl } from '../../../shared/enterprise_search_url/external_url'; + +import { WorkplaceSearchPageTemplate } from '../../components/layout'; +import { NAV, API_KEYS_TITLE } from '../../constants'; + +import { ApiKeysLogic } from './api_keys_logic'; +import { ApiKeyFlyout } from './components/api_key_flyout'; +import { ApiKeysList } from './components/api_keys_list'; +import { + API_KEYS_EMPTY_TITLE, + API_KEYS_EMPTY_BODY, + API_KEYS_EMPTY_BUTTON_LABEL, + CREATE_KEY_BUTTON_LABEL, + ENDPOINT_TITLE, + COPIED_TOOLTIP, + COPY_API_ENDPOINT_BUTTON_LABEL, +} from './constants'; + +export const ApiKeys: React.FC = () => { + const { fetchApiKeys, resetApiKeys, showApiKeyForm } = useActions(ApiKeysLogic); + + const { meta, dataLoading, apiKeyFormVisible, apiTokens } = useValues(ApiKeysLogic); + + useEffect(() => { + fetchApiKeys(); + return resetApiKeys; + }, [meta.page.current]); + + const hasApiKeys = apiTokens.length > 0; + + const addKeyButton = ( + + {CREATE_KEY_BUTTON_LABEL} + + ); + + const emptyPrompt = ( + {API_KEYS_EMPTY_TITLE}} + body={API_KEYS_EMPTY_BODY} + actions={ + + {API_KEYS_EMPTY_BUTTON_LABEL} + + } + /> + ); + + return ( + + {apiKeyFormVisible && } + + +

{ENDPOINT_TITLE}

+
+ + {(copy) => ( + <> + + {externalUrl.enterpriseSearchUrl} + + )} + +
+ + {hasApiKeys ? : emptyPrompt} +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/api_keys_logic.test.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/api_keys_logic.test.ts new file mode 100644 index 0000000000000..a02b1578bd38a --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/api_keys_logic.test.ts @@ -0,0 +1,491 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + LogicMounter, + mockFlashMessageHelpers, + mockHttpValues, +} from '../../../__mocks__/kea_logic'; + +import { nextTick } from '@kbn/test/jest'; + +import { DEFAULT_META } from '../../../shared/constants'; +import { itShowsServerErrorAsFlashMessage } from '../../../test_helpers'; + +import { ApiKeysLogic } from './api_keys_logic'; + +describe('ApiKeysLogic', () => { + const { mount } = new LogicMounter(ApiKeysLogic); + const { http } = mockHttpValues; + const { clearFlashMessages, flashSuccessToast } = mockFlashMessageHelpers; + + const DEFAULT_VALUES = { + dataLoading: true, + apiTokens: [], + meta: DEFAULT_META, + nameInputBlurred: false, + activeApiToken: { + name: '', + }, + activeApiTokenRawName: '', + apiKeyFormVisible: false, + apiTokenNameToDelete: '', + deleteModalVisible: false, + formErrors: [], + }; + + const newToken = { + id: '1', + name: 'myToken', + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('has expected default values', () => { + mount(); + expect(ApiKeysLogic.values).toEqual(DEFAULT_VALUES); + }); + + describe('actions', () => { + describe('onApiTokenCreateSuccess', () => { + const values = { + ...DEFAULT_VALUES, + apiTokens: expect.any(Array), + activeApiToken: expect.any(Object), + activeApiTokenRawName: expect.any(String), + apiKeyFormVisible: expect.any(Boolean), + formErrors: expect.any(Array), + }; + + describe('apiTokens', () => { + const existingToken = { + name: 'some_token', + }; + + it('should add the provided token to the apiTokens list', () => { + mount({ + apiTokens: [existingToken], + }); + + ApiKeysLogic.actions.onApiTokenCreateSuccess(newToken); + expect(ApiKeysLogic.values).toEqual({ + ...values, + apiTokens: [existingToken, newToken], + }); + }); + }); + + describe('activeApiToken', () => { + it('should reset to the default value, which effectively clears out the current form', () => { + mount({ + activeApiToken: newToken, + }); + + ApiKeysLogic.actions.onApiTokenCreateSuccess(newToken); + expect(ApiKeysLogic.values).toEqual({ + ...values, + activeApiToken: DEFAULT_VALUES.activeApiToken, + }); + }); + }); + + describe('activeApiTokenRawName', () => { + it('should reset to the default value, which effectively clears out the current form', () => { + mount({ + activeApiTokenRawName: 'foo', + }); + + ApiKeysLogic.actions.onApiTokenCreateSuccess(newToken); + expect(ApiKeysLogic.values).toEqual({ + ...values, + activeApiTokenRawName: DEFAULT_VALUES.activeApiTokenRawName, + }); + }); + }); + + describe('apiKeyFormVisible', () => { + it('should reset to the default value, which closes the api key form', () => { + mount({ + apiKeyFormVisible: true, + }); + + ApiKeysLogic.actions.onApiTokenCreateSuccess(newToken); + expect(ApiKeysLogic.values).toEqual({ + ...values, + apiKeyFormVisible: false, + }); + }); + }); + + describe('deleteModalVisible', () => { + const tokenName = 'my-token'; + + it('should set deleteModalVisible to true and set apiTokenNameToDelete', () => { + ApiKeysLogic.actions.stageTokenNameForDeletion(tokenName); + + expect(ApiKeysLogic.values).toEqual({ + ...values, + deleteModalVisible: true, + apiTokenNameToDelete: tokenName, + }); + }); + + it('should set deleteModalVisible to false and reset apiTokenNameToDelete', () => { + mount({ + deleteModalVisible: true, + apiTokenNameToDelete: tokenName, + }); + ApiKeysLogic.actions.hideDeleteModal(); + + expect(ApiKeysLogic.values).toEqual(values); + }); + }); + + describe('formErrors', () => { + it('should reset `formErrors`', () => { + mount({ + formErrors: ['I am an error'], + }); + + ApiKeysLogic.actions.onApiTokenCreateSuccess(newToken); + expect(ApiKeysLogic.values).toEqual({ + ...values, + formErrors: [], + }); + }); + }); + }); + + describe('onApiTokenError', () => { + const values = { + ...DEFAULT_VALUES, + formErrors: expect.any(Array), + }; + + describe('formErrors', () => { + it('should set `formErrors`', () => { + mount({ + formErrors: ['I am an error'], + }); + + ApiKeysLogic.actions.onApiTokenError(['I am the NEW error']); + expect(ApiKeysLogic.values).toEqual({ + ...values, + formErrors: ['I am the NEW error'], + }); + }); + }); + }); + + describe('setApiKeysData', () => { + const meta = { + page: { + current: 1, + size: 1, + total_pages: 1, + total_results: 1, + }, + }; + + const values = { + ...DEFAULT_VALUES, + dataLoading: false, + apiTokens: expect.any(Array), + meta: expect.any(Object), + }; + + describe('apiTokens', () => { + it('should be set', () => { + mount(); + + ApiKeysLogic.actions.setApiKeysData(meta, [newToken, newToken]); + expect(ApiKeysLogic.values).toEqual({ + ...values, + apiTokens: [newToken, newToken], + }); + }); + }); + + describe('meta', () => { + it('should be set', () => { + mount(); + + ApiKeysLogic.actions.setApiKeysData(meta, [newToken, newToken]); + expect(ApiKeysLogic.values).toEqual({ + ...values, + meta, + }); + }); + }); + }); + + describe('setNameInputBlurred', () => { + const values = { + ...DEFAULT_VALUES, + nameInputBlurred: expect.any(Boolean), + }; + + describe('nameInputBlurred', () => { + it('should set this value', () => { + mount({ + nameInputBlurred: false, + }); + + ApiKeysLogic.actions.setNameInputBlurred(true); + expect(ApiKeysLogic.values).toEqual({ + ...values, + nameInputBlurred: true, + }); + }); + }); + }); + + describe('setApiKeyName', () => { + const values = { + ...DEFAULT_VALUES, + activeApiToken: expect.any(Object), + activeApiTokenRawName: expect.any(String), + }; + + describe('activeApiToken', () => { + it('update the name property on the activeApiToken, formatted correctly', () => { + mount({ + activeApiToken: { + ...newToken, + name: 'bar', + }, + }); + + ApiKeysLogic.actions.setApiKeyName('New Name'); + expect(ApiKeysLogic.values).toEqual({ + ...values, + activeApiToken: { ...newToken, name: 'new-name' }, + }); + }); + }); + + describe('activeApiTokenRawName', () => { + it('updates the raw name, with no formatting applied', () => { + mount(); + + ApiKeysLogic.actions.setApiKeyName('New Name'); + expect(ApiKeysLogic.values).toEqual({ + ...values, + activeApiTokenRawName: 'New Name', + }); + }); + }); + }); + + describe('showApiKeyForm', () => { + const values = { + ...DEFAULT_VALUES, + activeApiToken: expect.any(Object), + activeApiTokenRawName: expect.any(String), + formErrors: expect.any(Array), + apiKeyFormVisible: expect.any(Boolean), + }; + + describe('apiKeyFormVisible', () => { + it('should toggle `apiKeyFormVisible`', () => { + mount({ + apiKeyFormVisible: false, + }); + + ApiKeysLogic.actions.showApiKeyForm(); + expect(ApiKeysLogic.values).toEqual({ + ...values, + apiKeyFormVisible: true, + }); + }); + }); + + describe('formErrors', () => { + it('should reset `formErrors`', () => { + mount({ + formErrors: ['I am an error'], + }); + + ApiKeysLogic.actions.showApiKeyForm(); + expect(ApiKeysLogic.values).toEqual({ + ...values, + formErrors: [], + }); + }); + }); + + describe('listener side-effects', () => { + it('should clear flashMessages whenever the api key form flyout is opened', () => { + ApiKeysLogic.actions.showApiKeyForm(); + expect(clearFlashMessages).toHaveBeenCalled(); + }); + }); + }); + + describe('hideApiKeyForm', () => { + const values = { + ...DEFAULT_VALUES, + apiKeyFormVisible: expect.any(Boolean), + activeApiTokenRawName: expect.any(String), + }; + + describe('activeApiTokenRawName', () => { + it('resets this value', () => { + mount({ + activeApiTokenRawName: 'foo', + }); + + ApiKeysLogic.actions.hideApiKeyForm(); + expect(ApiKeysLogic.values).toEqual({ + ...values, + activeApiTokenRawName: '', + }); + }); + }); + + describe('apiKeyFormVisible', () => { + it('resets this value', () => { + mount({ + apiKeyFormVisible: true, + }); + + ApiKeysLogic.actions.hideApiKeyForm(); + expect(ApiKeysLogic.values).toEqual({ + ...values, + apiKeyFormVisible: false, + }); + }); + }); + }); + + describe('resetApiKeys', () => { + const values = { + ...DEFAULT_VALUES, + formErrors: expect.any(Array), + }; + + describe('formErrors', () => { + it('should reset', () => { + mount({ + formErrors: ['I am an error'], + }); + + ApiKeysLogic.actions.resetApiKeys(); + expect(ApiKeysLogic.values).toEqual({ + ...values, + formErrors: [], + }); + }); + }); + }); + + describe('onPaginate', () => { + it('should set meta.page.current', () => { + mount({ meta: DEFAULT_META }); + + ApiKeysLogic.actions.onPaginate(5); + expect(ApiKeysLogic.values).toEqual({ + ...DEFAULT_VALUES, + meta: { + page: { + ...DEFAULT_META.page, + current: 5, + }, + }, + }); + }); + }); + }); + + describe('listeners', () => { + describe('fetchApiKeys', () => { + const meta = { + page: { + current: 1, + size: 1, + total_pages: 1, + total_results: 1, + }, + }; + const results: object[] = []; + + it('will call an API endpoint and set the results with the `setApiKeysData` action', async () => { + mount(); + jest.spyOn(ApiKeysLogic.actions, 'setApiKeysData').mockImplementationOnce(() => {}); + http.get.mockReturnValue(Promise.resolve({ meta, results })); + + ApiKeysLogic.actions.fetchApiKeys(); + expect(http.get).toHaveBeenCalledWith('/internal/workplace_search/api_keys', { + query: { + 'page[current]': 1, + 'page[size]': 10, + }, + }); + await nextTick(); + expect(ApiKeysLogic.actions.setApiKeysData).toHaveBeenCalledWith(meta, results); + }); + + itShowsServerErrorAsFlashMessage(http.get, () => { + mount(); + ApiKeysLogic.actions.fetchApiKeys(); + }); + }); + + describe('deleteApiKey', () => { + const tokenName = 'abc123'; + + it('will call an API endpoint and re-fetch the api keys list', async () => { + mount(); + jest.spyOn(ApiKeysLogic.actions, 'fetchApiKeys').mockImplementationOnce(() => {}); + http.delete.mockReturnValue(Promise.resolve()); + + ApiKeysLogic.actions.stageTokenNameForDeletion(tokenName); + ApiKeysLogic.actions.deleteApiKey(); + expect(http.delete).toHaveBeenCalledWith( + `/internal/workplace_search/api_keys/${tokenName}` + ); + await nextTick(); + + expect(ApiKeysLogic.actions.fetchApiKeys).toHaveBeenCalled(); + expect(flashSuccessToast).toHaveBeenCalled(); + }); + + itShowsServerErrorAsFlashMessage(http.delete, () => { + mount(); + ApiKeysLogic.actions.deleteApiKey(); + }); + }); + + describe('onApiFormSubmit', () => { + it('calls a POST API endpoint that creates a new token if the active token does not exist yet', async () => { + const createdToken = { + name: 'new-key', + }; + mount({ + activeApiToken: createdToken, + }); + jest.spyOn(ApiKeysLogic.actions, 'onApiTokenCreateSuccess'); + http.post.mockReturnValue(Promise.resolve(createdToken)); + + ApiKeysLogic.actions.onApiFormSubmit(); + expect(http.post).toHaveBeenCalledWith('/internal/workplace_search/api_keys', { + body: JSON.stringify(createdToken), + }); + await nextTick(); + expect(ApiKeysLogic.actions.onApiTokenCreateSuccess).toHaveBeenCalledWith(createdToken); + expect(flashSuccessToast).toHaveBeenCalled(); + }); + + itShowsServerErrorAsFlashMessage(http.post, () => { + mount(); + ApiKeysLogic.actions.onApiFormSubmit(); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/api_keys_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/api_keys_logic.ts new file mode 100644 index 0000000000000..ca3662a8784c8 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/api_keys_logic.ts @@ -0,0 +1,213 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { Meta } from '../../../../../common/types'; +import { DEFAULT_META } from '../../../shared/constants'; +import { + clearFlashMessages, + flashSuccessToast, + flashAPIErrors, +} from '../../../shared/flash_messages'; +import { HttpLogic } from '../../../shared/http'; +import { updateMetaPageIndex } from '../../../shared/table_pagination'; + +import { ApiToken } from '../../types'; + +import { CREATE_MESSAGE, DELETE_MESSAGE } from './constants'; + +const formatApiName = (rawName: string): string => + rawName + .trim() + .replace(/[^a-zA-Z0-9]+/g, '-') // Replace all special/non-alphanumerical characters with dashes + .replace(/^[-]+|[-]+$/g, '') // Strip all leading and trailing dashes + .toLowerCase(); + +export const defaultApiToken: ApiToken = { + name: '', +}; + +interface ApiKeysLogicActions { + onApiTokenCreateSuccess(apiToken: ApiToken): ApiToken; + onApiTokenError(formErrors: string[]): string[]; + setApiKeysData(meta: Meta, apiTokens: ApiToken[]): { meta: Meta; apiTokens: ApiToken[] }; + setNameInputBlurred(isBlurred: boolean): boolean; + setApiKeyName(name: string): string; + showApiKeyForm(): void; + hideApiKeyForm(): { value: boolean }; + resetApiKeys(): { value: boolean }; + fetchApiKeys(): void; + onPaginate(newPageIndex: number): { newPageIndex: number }; + deleteApiKey(): void; + onApiFormSubmit(): void; + stageTokenNameForDeletion(tokenName: string): string; + hideDeleteModal(): void; +} + +interface ApiKeysLogicValues { + activeApiToken: ApiToken; + activeApiTokenRawName: string; + apiTokens: ApiToken[]; + dataLoading: boolean; + formErrors: string[]; + meta: Meta; + nameInputBlurred: boolean; + apiKeyFormVisible: boolean; + deleteModalVisible: boolean; + apiTokenNameToDelete: string; +} + +export const ApiKeysLogic = kea>({ + path: ['enterprise_search', 'workplace_search', 'api_keys_logic'], + actions: () => ({ + onApiTokenCreateSuccess: (apiToken) => apiToken, + onApiTokenError: (formErrors) => formErrors, + setApiKeysData: (meta, apiTokens) => ({ meta, apiTokens }), + setNameInputBlurred: (nameInputBlurred) => nameInputBlurred, + setApiKeyName: (name) => name, + showApiKeyForm: true, + hideApiKeyForm: false, + resetApiKeys: false, + fetchApiKeys: true, + onPaginate: (newPageIndex) => ({ newPageIndex }), + deleteApiKey: true, + stageTokenNameForDeletion: (tokenName) => tokenName, + hideDeleteModal: true, + onApiFormSubmit: () => null, + }), + reducers: () => ({ + dataLoading: [ + true, + { + setApiKeysData: () => false, + }, + ], + apiTokens: [ + [], + { + setApiKeysData: (_, { apiTokens }) => apiTokens, + onApiTokenCreateSuccess: (apiTokens, apiToken) => [...apiTokens, apiToken], + }, + ], + meta: [ + DEFAULT_META, + { + setApiKeysData: (_, { meta }) => meta, + onPaginate: (state, { newPageIndex }) => updateMetaPageIndex(state, newPageIndex), + }, + ], + nameInputBlurred: [ + false, + { + setNameInputBlurred: (_, nameInputBlurred) => nameInputBlurred, + }, + ], + activeApiToken: [ + defaultApiToken, + { + onApiTokenCreateSuccess: () => defaultApiToken, + hideApiKeyForm: () => defaultApiToken, + setApiKeyName: (activeApiToken, name) => ({ ...activeApiToken, name: formatApiName(name) }), + }, + ], + activeApiTokenRawName: [ + '', + { + setApiKeyName: (_, activeApiTokenRawName) => activeApiTokenRawName, + hideApiKeyForm: () => '', + onApiTokenCreateSuccess: () => '', + }, + ], + apiKeyFormVisible: [ + false, + { + showApiKeyForm: () => true, + hideApiKeyForm: () => false, + onApiTokenCreateSuccess: () => false, + }, + ], + deleteModalVisible: [ + false, + { + stageTokenNameForDeletion: () => true, + hideDeleteModal: () => false, + }, + ], + apiTokenNameToDelete: [ + '', + { + stageTokenNameForDeletion: (_, tokenName) => tokenName, + hideDeleteModal: () => '', + }, + ], + formErrors: [ + [], + { + onApiTokenError: (_, formErrors) => formErrors, + onApiTokenCreateSuccess: () => [], + showApiKeyForm: () => [], + resetApiKeys: () => [], + }, + ], + }), + listeners: ({ actions, values }) => ({ + showApiKeyForm: () => { + clearFlashMessages(); + }, + fetchApiKeys: async () => { + try { + const { http } = HttpLogic.values; + const { meta } = values; + const query = { + 'page[current]': meta.page.current, + 'page[size]': meta.page.size, + }; + const response = await http.get<{ meta: Meta; results: ApiToken[] }>( + '/internal/workplace_search/api_keys', + { query } + ); + actions.setApiKeysData(response.meta, response.results); + } catch (e) { + flashAPIErrors(e); + } + }, + deleteApiKey: async () => { + const { apiTokenNameToDelete } = values; + + try { + const { http } = HttpLogic.values; + await http.delete(`/internal/workplace_search/api_keys/${apiTokenNameToDelete}`); + + actions.fetchApiKeys(); + flashSuccessToast(DELETE_MESSAGE(apiTokenNameToDelete)); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.hideDeleteModal(); + } + }, + onApiFormSubmit: async () => { + const { name } = values.activeApiToken; + + const data: ApiToken = { + name, + }; + + try { + const { http } = HttpLogic.values; + const body = JSON.stringify(data); + + const response = await http.post('/internal/workplace_search/api_keys', { body }); + actions.onApiTokenCreateSuccess(response); + flashSuccessToast(CREATE_MESSAGE(name)); + } catch (e) { + flashAPIErrors(e); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_key.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_key.test.tsx new file mode 100644 index 0000000000000..d99ab3f260c77 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_key.test.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiButtonIcon } from '@elastic/eui'; + +import { ApiKey } from './api_key'; + +describe('ApiKey', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + const props = { + copy: jest.fn(), + toggleIsHidden: jest.fn(), + isHidden: true, + text: 'some-api-key', + }; + + it('renders', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiButtonIcon).length).toEqual(2); + }); + + it('will call copy when the first button is clicked', () => { + const wrapper = shallow(); + wrapper.find(EuiButtonIcon).first().simulate('click'); + expect(props.copy).toHaveBeenCalled(); + }); + + it('will call hide when the second button is clicked', () => { + const wrapper = shallow(); + wrapper.find(EuiButtonIcon).last().simulate('click'); + expect(props.toggleIsHidden).toHaveBeenCalled(); + }); + + it('will render the "eye" icon when isHidden is true', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiButtonIcon).last().prop('iconType')).toBe('eye'); + }); + + it('will render the "eyeClosed" icon when isHidden is false', () => { + const wrapper = shallow(); + expect(wrapper.find(EuiButtonIcon).last().prop('iconType')).toBe('eyeClosed'); + }); + + it('will render the provided text', () => { + const wrapper = shallow(); + expect(wrapper.text()).toContain('some-api-key'); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_key.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_key.tsx new file mode 100644 index 0000000000000..0ea24d9b684ea --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_key.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { EuiButtonIcon } from '@elastic/eui'; + +import { SHOW_API_KEY_LABEL, HIDE_API_KEY_LABEL, COPY_API_KEY_BUTTON_LABEL } from '../constants'; + +interface Props { + copy: () => void; + toggleIsHidden: () => void; + isHidden: boolean; + text: React.ReactNode; +} + +export const ApiKey: React.FC = ({ copy, toggleIsHidden, isHidden, text }) => { + const hideIcon = isHidden ? 'eye' : 'eyeClosed'; + const hideIconLabel = isHidden ? SHOW_API_KEY_LABEL : HIDE_API_KEY_LABEL; + + return ( + <> + + + {text} + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_key_flyout.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_key_flyout.test.tsx new file mode 100644 index 0000000000000..e31ae94e968ce --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_key_flyout.test.tsx @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues, setMockActions } from '../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiFlyout, EuiForm, EuiFieldText, EuiFormRow } from '@elastic/eui'; + +import { ApiKeyFlyout } from './api_key_flyout'; + +describe('ApiKeyFlyout', () => { + const setNameInputBlurred = jest.fn(); + const setApiKeyName = jest.fn(); + const onApiFormSubmit = jest.fn(); + const hideApiKeyForm = jest.fn(); + + const apiKey = { + id: '123', + name: 'test', + }; + + const values = { + activeApiToken: apiKey, + }; + + beforeEach(() => { + setMockValues(values); + setMockActions({ + setNameInputBlurred, + setApiKeyName, + onApiFormSubmit, + hideApiKeyForm, + }); + }); + + it('renders', () => { + const wrapper = shallow(); + const flyout = wrapper.find(EuiFlyout); + + expect(flyout).toHaveLength(1); + expect(flyout.prop('onClose')).toEqual(hideApiKeyForm); + }); + + it('calls onApiTokenChange on form submit', () => { + const wrapper = shallow(); + const preventDefault = jest.fn(); + wrapper.find(EuiForm).simulate('submit', { preventDefault }); + + expect(preventDefault).toHaveBeenCalled(); + expect(onApiFormSubmit).toHaveBeenCalled(); + }); + + it('shows help text if the raw name does not match the expected name', () => { + setMockValues({ + ...values, + activeApiToken: { name: 'my-api-key' }, + activeApiTokenRawName: 'my api key!!', + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiFormRow).prop('helpText')).toEqual('Your key will be named: my-api-key'); + }); + + it('controls the input value', () => { + setMockValues({ + ...values, + activeApiTokenRawName: 'test', + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldText).prop('value')).toEqual('test'); + }); + + it('calls setApiKeyName when the input value is changed', () => { + const wrapper = shallow(); + wrapper.find(EuiFieldText).simulate('change', { target: { value: 'changed' } }); + + expect(setApiKeyName).toHaveBeenCalledWith('changed'); + }); + + it('calls setNameInputBlurred when the user stops focusing the input', () => { + const wrapper = shallow(); + wrapper.find(EuiFieldText).simulate('blur'); + + expect(setNameInputBlurred).toHaveBeenCalledWith(true); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_key_flyout.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_key_flyout.tsx new file mode 100644 index 0000000000000..150778ad7fdbc --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_key_flyout.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useValues, useActions } from 'kea'; + +import { + EuiPortal, + EuiFormRow, + EuiFieldText, + EuiFlyout, + EuiFlyoutHeader, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiButton, + EuiForm, + EuiTitle, +} from '@elastic/eui'; + +import { CLOSE_BUTTON_LABEL, SAVE_BUTTON_LABEL } from '../../../../shared/constants'; +import { FlashMessages } from '../../../../shared/flash_messages'; + +import { ApiKeysLogic } from '../api_keys_logic'; +import { + API_KEY_FLYOUT_TITLE, + API_KEY_FORM_LABEL, + API_KEY_FORM_HELP_TEXT, + API_KEY_NAME_PLACEHOLDER, +} from '../constants'; + +export const ApiKeyFlyout: React.FC = () => { + const { setNameInputBlurred, setApiKeyName, onApiFormSubmit, hideApiKeyForm } = + useActions(ApiKeysLogic); + const { + activeApiToken: { name }, + activeApiTokenRawName: rawName, + } = useValues(ApiKeysLogic); + + return ( + + + + +

{API_KEY_FLYOUT_TITLE}

+
+
+ + + { + e.preventDefault(); + onApiFormSubmit(); + }} + component="form" + > + + setApiKeyName(e.target.value)} + onBlur={() => setNameInputBlurred(true)} + autoComplete="off" + maxLength={64} + required + fullWidth + autoFocus + /> + + + + + + + + {CLOSE_BUTTON_LABEL} + + + + + {SAVE_BUTTON_LABEL} + + + + +
+
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_keys_list.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_keys_list.test.tsx new file mode 100644 index 0000000000000..3dd300d0eb5c5 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_keys_list.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { setMockValues, setMockActions } from '../../../../__mocks__/kea_logic'; + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiBasicTable, EuiCopy, EuiConfirmModal } from '@elastic/eui'; + +import { HiddenText } from '../../../../shared/hidden_text'; + +import { ApiKey } from './api_key'; +import { ApiKeysList } from './api_keys_list'; + +describe('ApiKeysList', () => { + const stageTokenNameForDeletion = jest.fn(); + const hideDeleteModal = jest.fn(); + const deleteApiKey = jest.fn(); + const onPaginate = jest.fn(); + const apiToken = { + id: '1', + name: 'test', + key: 'foo', + }; + const apiTokens = [apiToken]; + const meta = { + page: { + current: 1, + size: 10, + total_pages: 1, + total_results: 5, + }, + }; + + const values = { apiTokens, meta, dataLoading: false }; + + beforeEach(() => { + setMockValues(values); + setMockActions({ deleteApiKey, onPaginate, stageTokenNameForDeletion, hideDeleteModal }); + }); + + it('renders', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiBasicTable)).toHaveLength(1); + }); + + describe('loading state', () => { + it('renders as loading when dataLoading is true', () => { + setMockValues({ + ...values, + dataLoading: true, + }); + const wrapper = shallow(); + + expect(wrapper.find(EuiBasicTable).prop('loading')).toBe(true); + }); + }); + + describe('pagination', () => { + it('derives pagination from meta object', () => { + setMockValues({ + ...values, + meta: { + page: { + current: 6, + size: 55, + total_pages: 1, + total_results: 1004, + }, + }, + }); + const wrapper = shallow(); + const { pagination } = wrapper.find(EuiBasicTable).props(); + + expect(pagination).toEqual({ + pageIndex: 5, + pageSize: 55, + totalItemCount: 1004, + hidePerPageOptions: true, + }); + }); + }); + + it('handles confirmModal submission', () => { + setMockValues({ + ...values, + deleteModalVisible: true, + }); + const wrapper = shallow(); + const modal = wrapper.find(EuiConfirmModal); + modal.prop('onConfirm')!({} as any); + + expect(deleteApiKey).toHaveBeenCalled(); + }); + + describe('columns', () => { + let columns: any[]; + + beforeAll(() => { + setMockValues(values); + const wrapper = shallow(); + columns = wrapper.find(EuiBasicTable).props().columns; + }); + + describe('column 1 (name)', () => { + const token = { + ...apiToken, + name: 'some-name', + }; + + it('renders correctly', () => { + const column = columns[0]; + const wrapper = shallow(
{column.render(token)}
); + + expect(wrapper.text()).toEqual('some-name'); + }); + }); + + describe('column 2 (key)', () => { + const token = { + ...apiToken, + key: 'abc-123', + }; + + it('renders nothing if no key is present', () => { + const tokenWithNoKey = { + key: undefined, + }; + const column = columns[1]; + const wrapper = shallow(
{column.render(tokenWithNoKey)}
); + + expect(wrapper.text()).toBe(''); + }); + + it('renders an EuiCopy component with the key', () => { + const column = columns[1]; + const wrapper = shallow(
{column.render(token)}
); + + expect(wrapper.find(EuiCopy).props().textToCopy).toEqual('abc-123'); + }); + + it('renders a HiddenText component with the key', () => { + const column = columns[1]; + const wrapper = shallow(
{column.render(token)}
) + .find(EuiCopy) + .dive(); + + expect(wrapper.find(HiddenText).props().text).toEqual('abc-123'); + }); + + it('renders a Key component', () => { + const column = columns[1]; + const wrapper = shallow(
{column.render(token)}
) + .find(EuiCopy) + .dive() + .find(HiddenText) + .dive(); + + expect(wrapper.find(ApiKey).props()).toEqual({ + copy: expect.any(Function), + toggleIsHidden: expect.any(Function), + isHidden: expect.any(Boolean), + text: ( + + ••••••• + + ), + }); + }); + }); + + describe('column 3 (delete action)', () => { + const token = { + ...apiToken, + name: 'some-name', + }; + + it('calls stageTokenNameForDeletion when clicked', () => { + const action = columns[2].actions[0]; + action.onClick(token); + + expect(stageTokenNameForDeletion).toHaveBeenCalledWith('some-name'); + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_keys_list.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_keys_list.tsx new file mode 100644 index 0000000000000..5a79e965454b2 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/components/api_keys_list.tsx @@ -0,0 +1,112 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { useActions, useValues } from 'kea'; + +import { EuiBasicTable, EuiBasicTableColumn, EuiCopy, EuiConfirmModal } from '@elastic/eui'; + +import { DELETE_BUTTON_LABEL, CANCEL_BUTTON_LABEL } from '../../../../shared/constants'; +import { HiddenText } from '../../../../shared/hidden_text'; +import { convertMetaToPagination, handlePageChange } from '../../../../shared/table_pagination'; +import { ApiToken } from '../../../types'; + +import { ApiKeysLogic } from '../api_keys_logic'; +import { + DELETE_API_KEY_BUTTON_DESCRIPTION, + COPIED_TOOLTIP, + NAME_TITLE, + KEY_TITLE, + API_KEYS_CONFIRM_DELETE_TITLE, + API_KEYS_CONFIRM_DELETE_LABEL, +} from '../constants'; + +import { ApiKey } from './api_key'; + +export const ApiKeysList: React.FC = () => { + const { deleteApiKey, onPaginate, stageTokenNameForDeletion, hideDeleteModal } = + useActions(ApiKeysLogic); + const { apiTokens, meta, dataLoading, deleteModalVisible } = useValues(ApiKeysLogic); + + const deleteModal = ( + +

{API_KEYS_CONFIRM_DELETE_LABEL}

+
+ ); + + const columns: Array> = [ + { + name: NAME_TITLE, + render: (token: ApiToken) => token.name, + }, + { + name: KEY_TITLE, + className: 'eui-textBreakAll', + render: (token: ApiToken) => { + const { key } = token; + if (!key) return null; + + return ( + + {(copy) => ( + + {({ hiddenText, isHidden, toggle }) => ( + + )} + + )} + + ); + }, + mobileOptions: { + width: '100%', + }, + }, + { + actions: [ + { + name: DELETE_BUTTON_LABEL, + description: DELETE_API_KEY_BUTTON_DESCRIPTION, + type: 'icon', + icon: 'trash', + color: 'danger', + onClick: (token: ApiToken) => stageTokenNameForDeletion(token.name), + }, + ], + }, + ]; + + return ( + <> + {deleteModalVisible && deleteModal} + + + ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/constants.ts new file mode 100644 index 0000000000000..6c45dc38339c4 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/constants.ts @@ -0,0 +1,149 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const CREATE_KEY_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.createKey.buttonLabel', + { + defaultMessage: 'Create key', + } +); + +export const ENDPOINT_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.endpointTitle', + { + defaultMessage: 'Endpoint', + } +); + +export const NAME_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.nameTitle', + { + defaultMessage: 'Name', + } +); + +export const KEY_TITLE = i18n.translate('xpack.enterpriseSearch.workplaceSearch.apiKeys.keyTitle', { + defaultMessage: 'Key', +}); + +export const COPIED_TOOLTIP = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.copied.tooltip', + { + defaultMessage: 'Copied', + } +); + +export const COPY_API_ENDPOINT_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.copyApiEndpoint.buttonLabel', + { + defaultMessage: 'Copy API Endpoint to clipboard.', + } +); + +export const COPY_API_KEY_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.copyApiKey.buttonLabel', + { + defaultMessage: 'Copy API Key to clipboard.', + } +); + +export const DELETE_API_KEY_BUTTON_DESCRIPTION = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.deleteApiKey.buttonDescription', + { + defaultMessage: 'Delete API key', + } +); + +export const CREATE_MESSAGE = (name: string) => + i18n.translate('xpack.enterpriseSearch.workplaceSearch.apiKeys.createdMessage', { + defaultMessage: "API key '{name}' was created", + values: { name }, + }); + +export const DELETE_MESSAGE = (name: string) => + i18n.translate('xpack.enterpriseSearch.workplaceSearch.apiKeys.deletedMessage', { + defaultMessage: "API key '{name}' was deleted", + values: { name }, + }); + +export const API_KEY_FLYOUT_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.flyoutTitle', + { + defaultMessage: 'Create a new key', + } +); + +export const API_KEY_FORM_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.formLabel', + { + defaultMessage: 'Key name', + } +); + +export const API_KEY_FORM_HELP_TEXT = (name: string) => + i18n.translate('xpack.enterpriseSearch.workplaceSearch.apiKeys.formHelpText', { + defaultMessage: 'Your key will be named: {name}', + values: { name }, + }); + +export const API_KEY_NAME_PLACEHOLDER = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.namePlaceholder', + { + defaultMessage: 'i.e., my-api-key', + } +); + +export const SHOW_API_KEY_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.showApiKeyLabel', + { + defaultMessage: 'Show API Key', + } +); + +export const HIDE_API_KEY_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.hideApiKeyLabel', + { + defaultMessage: 'Hide API Key', + } +); + +export const API_KEYS_EMPTY_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.emptyTitle', + { + defaultMessage: 'Create your first API key', + } +); + +export const API_KEYS_EMPTY_BODY = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.emptyBody', + { + defaultMessage: 'Allow applications to access Elastic Workplace Search on your behalf.', + } +); + +export const API_KEYS_EMPTY_BUTTON_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.emptyButtonLabel', + { + defaultMessage: 'Learn about API keys', + } +); + +export const API_KEYS_CONFIRM_DELETE_TITLE = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.confirmDeleteTitle', + { + defaultMessage: 'Delete API key', + } +); + +export const API_KEYS_CONFIRM_DELETE_LABEL = i18n.translate( + 'xpack.enterpriseSearch.workplaceSearch.apiKeys.confirmDeleteLabel', + { + defaultMessage: 'Are you sure you want to delete this API key? This action cannot be undone.', + } +); diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/index.ts similarity index 85% rename from x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/index.ts rename to x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/index.ts index 47615a02668c6..4affd04611624 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/api_keys/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export * from './download_artifact_schema'; +export { ApiKeys } from './api_keys'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts index 05a5fd5a73fe8..6dbac2dcd1452 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/add_source_logic.ts @@ -571,7 +571,7 @@ export const AddSourceLogic = kea 0 ? githubOrganizations : undefined, - indexPermissions: indexPermissionsValue || undefined, + index_permissions: indexPermissionsValue || undefined, } as { [key: string]: string | string[] | undefined; }; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts index 079cb5e1a5a3d..cbc18f6d7a19e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/constants.ts @@ -282,7 +282,7 @@ export const SAVE_CUSTOM_BODY1 = i18n.translate( export const SAVE_CUSTOM_BODY2 = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.body2', { - defaultMessage: 'Be sure to copy your API keys below.', + defaultMessage: 'Be sure to copy your Source Identifier below.', } ); @@ -293,20 +293,6 @@ export const SAVE_CUSTOM_RETURN_BUTTON = i18n.translate( } ); -export const SAVE_CUSTOM_API_KEYS_TITLE = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.apiKeys.title', - { - defaultMessage: 'API Keys', - } -); - -export const SAVE_CUSTOM_API_KEYS_BODY = i18n.translate( - 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.apiKeys.body', - { - defaultMessage: "You'll need these keys to sync documents for this custom source.", - } -); - export const SAVE_CUSTOM_VISUAL_WALKTHROUGH_TITLE = i18n.translate( 'xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.visualWalkthrough.title', { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_app.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_app.tsx deleted file mode 100644 index 7f518d272d842..0000000000000 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_app.tsx +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; - -import { useValues } from 'kea'; - -import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiPanel, EuiSpacer } from '@elastic/eui'; - -import { AppLogic } from '../../../../app_logic'; -import { - WorkplaceSearchPageTemplate, - PersonalDashboardLayout, -} from '../../../../components/layout'; -import { NAV, SOURCE_NAMES } from '../../../../constants'; - -import { staticSourceData } from '../../source_data'; - -import { AddSourceHeader } from './add_source_header'; -import { SourceFeatures } from './source_features'; - -export const GitHubApp: React.FC = () => { - const { isOrganization } = useValues(AppLogic); - - const name = SOURCE_NAMES.GITHUB; - const data = staticSourceData.find((source) => (source.name = name)); - const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; - - return ( - -
'TODO: use method from add_source_logic'}> - - - - - - - - - - - - - form goes here - - -
-
- ); -}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app.tsx new file mode 100644 index 0000000000000..a08f49b8bbe78 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app.tsx @@ -0,0 +1,128 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useEffect } from 'react'; +import type { FormEvent } from 'react'; + +import { useValues, useActions } from 'kea'; + +import { + EuiHorizontalRule, + EuiPanel, + EuiSpacer, + EuiFieldText, + EuiFormRow, + EuiFilePicker, + EuiButton, +} from '@elastic/eui'; + +import { LicensingLogic } from '../../../../../shared/licensing'; +import { AppLogic } from '../../../../app_logic'; + +import { + WorkplaceSearchPageTemplate, + PersonalDashboardLayout, +} from '../../../../components/layout'; +import { NAV, SOURCE_NAMES } from '../../../../constants'; +import { handlePrivateKeyUpload } from '../../../../utils'; + +import { staticSourceData } from '../../source_data'; + +import { AddSourceHeader } from './add_source_header'; +import { DocumentPermissionsCallout } from './document_permissions_callout'; +import { DocumentPermissionsField } from './document_permissions_field'; +import { GithubViaAppLogic } from './github_via_app_logic'; +import { SourceFeatures } from './source_features'; + +interface GithubViaAppProps { + isGithubEnterpriseServer: boolean; +} + +export const GitHubViaApp: React.FC = ({ isGithubEnterpriseServer }) => { + const { isOrganization } = useValues(AppLogic); + const { githubAppId, githubEnterpriseServerUrl, isSubmitButtonLoading, indexPermissionsValue } = + useValues(GithubViaAppLogic); + const { + setGithubAppId, + setGithubEnterpriseServerUrl, + setStagedPrivateKey, + createContentSource, + setSourceIndexPermissionsValue, + } = useActions(GithubViaAppLogic); + + const { hasPlatinumLicense } = useValues(LicensingLogic); + const name = isGithubEnterpriseServer ? SOURCE_NAMES.GITHUB_ENTERPRISE : SOURCE_NAMES.GITHUB; + const data = staticSourceData.find((source) => source.name === name); + const Layout = isOrganization ? WorkplaceSearchPageTemplate : PersonalDashboardLayout; + + const handleSubmit = (e: FormEvent) => { + e.preventDefault(); + createContentSource(isGithubEnterpriseServer); + }; + + // Default indexPermissions to true, if needed + useEffect(() => { + setSourceIndexPermissionsValue(isOrganization && hasPlatinumLicense); + }, []); + + return ( + +
+ + + + + + + + + + + + + {!hasPlatinumLicense && } + {hasPlatinumLicense && isOrganization && ( + + )} + + + setGithubAppId(e.target.value)} /> + + {isGithubEnterpriseServer && ( + + setGithubEnterpriseServerUrl(e.target.value)} + /> + + )} + + handlePrivateKeyUpload(files, setStagedPrivateKey)} + accept=".pem" + /> + + + {isSubmitButtonLoading ? 'Connecting…' : `Connect ${name}`} + + +
+ ); +}; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app_logic.ts new file mode 100644 index 0000000000000..e779d53b6a1eb --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/github_via_app_logic.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kea, MakeLogicType } from 'kea'; + +import { flashAPIErrors, flashSuccessToast } from '../../../../../shared/flash_messages'; +import { HttpLogic } from '../../../../../shared/http'; +import { KibanaLogic } from '../../../../../shared/kibana'; +import { AppLogic } from '../../../../app_logic'; +import { + GITHUB_VIA_APP_SERVICE_TYPE, + GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE, +} from '../../../../constants'; +import { SOURCES_PATH, getSourcesPath } from '../../../../routes'; +import { ContentSourceFullData } from '../../../../types'; + +interface GithubViaAppValues { + githubAppId: string; + githubEnterpriseServerUrl: string; + stagedPrivateKey: string | null; + isSubmitButtonLoading: boolean; + indexPermissionsValue: boolean; +} + +interface GithubViaAppActions { + setGithubAppId(githubAppId: string): string; + setGithubEnterpriseServerUrl(githubEnterpriseServerUrl: string): string; + setStagedPrivateKey(stagedPrivateKey: string | null): string | null; + setButtonNotLoading(): void; + createContentSource(isGithubEnterpriseServer: boolean): boolean; + setSourceIndexPermissionsValue(indexPermissionsValue: boolean): boolean; +} + +export const GithubViaAppLogic = kea>({ + path: ['enterprise_search', 'workplace_search', 'github_via_app_logic'], + actions: { + setGithubAppId: (githubAppId: string) => githubAppId, + setGithubEnterpriseServerUrl: (githubEnterpriseServerUrl: string) => githubEnterpriseServerUrl, + createContentSource: (isGithubEnterpriseServer: boolean) => isGithubEnterpriseServer, + setStagedPrivateKey: (stagedPrivateKey: string) => stagedPrivateKey, + setButtonNotLoading: false, + setSourceIndexPermissionsValue: (indexPermissionsValue: boolean) => indexPermissionsValue, + }, + reducers: { + githubAppId: [ + '', + { + setGithubAppId: (_, githubAppId) => githubAppId, + }, + ], + githubEnterpriseServerUrl: [ + '', + { + setGithubEnterpriseServerUrl: (_, githubEnterpriseServerUrl) => githubEnterpriseServerUrl, + }, + ], + stagedPrivateKey: [ + null, + { + setStagedPrivateKey: (_, stagedPrivateKey) => stagedPrivateKey, + }, + ], + isSubmitButtonLoading: [ + false, + { + createContentSource: () => true, + setButtonNotLoading: () => false, + }, + ], + indexPermissionsValue: [ + true, + { + setSourceIndexPermissionsValue: (_, indexPermissionsValue) => indexPermissionsValue, + resetSourceState: () => false, + }, + ], + }, + listeners: ({ actions, values }) => ({ + createContentSource: async (isGithubEnterpriseServer) => { + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? '/internal/workplace_search/org/create_source' + : '/internal/workplace_search/account/create_source'; + + const { githubAppId, githubEnterpriseServerUrl, stagedPrivateKey, indexPermissionsValue } = + values; + + const params = { + service_type: isGithubEnterpriseServer + ? GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE + : GITHUB_VIA_APP_SERVICE_TYPE, + app_id: githubAppId, + base_url: githubEnterpriseServerUrl, + private_key: stagedPrivateKey, + index_permissions: indexPermissionsValue, + }; + + try { + const response = await HttpLogic.values.http.post(route, { + body: JSON.stringify({ ...params }), + }); + + KibanaLogic.values.navigateToUrl(`${getSourcesPath(SOURCES_PATH, isOrganization)}`); + flashSuccessToast(`${response.serviceName} connected`); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.setButtonNotLoading(); + } + }, + }), +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/index.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/index.ts index 033cf9f356342..8daa71672d203 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/index.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/index.ts @@ -7,4 +7,4 @@ export { AddSource } from './add_source'; export { AddSourceList } from './add_source_list'; -export { GitHubApp } from './github_app'; +export { GitHubViaApp } from './github_via_app'; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx index a8a5810e7c0a2..4715c50e4233c 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.test.tsx @@ -36,7 +36,7 @@ describe('SaveCustom', () => { const wrapper = shallow(); expect(wrapper.find(EuiPanel)).toHaveLength(1); - expect(wrapper.find(EuiTitle)).toHaveLength(5); + expect(wrapper.find(EuiTitle)).toHaveLength(4); expect(wrapper.find(EuiLinkTo)).toHaveLength(1); }); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx index 8108f8211f93d..bbf1b66277c70 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/add_source/save_custom.tsx @@ -26,7 +26,6 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { LicensingLogic } from '../../../../../shared/licensing'; import { EuiLinkTo } from '../../../../../shared/react_router_helpers'; -import { CredentialItem } from '../../../../components/shared/credential_item'; import { LicenseBadge } from '../../../../components/shared/license_badge'; import { SOURCES_PATH, @@ -37,14 +36,14 @@ import { getSourcesPath, } from '../../../../routes'; import { CustomSource } from '../../../../types'; -import { ACCESS_TOKEN_LABEL, ID_LABEL, LEARN_CUSTOM_FEATURES_BUTTON } from '../../constants'; +import { LEARN_CUSTOM_FEATURES_BUTTON } from '../../constants'; + +import { SourceIdentifier } from '../source_identifier'; import { SAVE_CUSTOM_BODY1, SAVE_CUSTOM_BODY2, SAVE_CUSTOM_RETURN_BUTTON, - SAVE_CUSTOM_API_KEYS_TITLE, - SAVE_CUSTOM_API_KEYS_BODY, SAVE_CUSTOM_VISUAL_WALKTHROUGH_TITLE, SAVE_CUSTOM_VISUAL_WALKTHROUGH_LINK, SAVE_CUSTOM_STYLING_RESULTS_TITLE, @@ -62,7 +61,7 @@ interface SaveCustomProps { export const SaveCustom: React.FC = ({ documentationUrl, - newCustomSource: { id, accessToken, name }, + newCustomSource: { id, name }, isOrganization, header, }) => { @@ -106,24 +105,8 @@ export const SaveCustom: React.FC = ({
- - - -

{SAVE_CUSTOM_API_KEYS_TITLE}

-
- -

{SAVE_CUSTOM_API_KEYS_BODY}

-
- - - - -
-
+ +
diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx index fd29b5f590967..29abbf94db397 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/overview.tsx @@ -37,7 +37,6 @@ import { EuiListGroupItemTo, EuiLinkTo } from '../../../../shared/react_router_h import { AppLogic } from '../../../app_logic'; import aclImage from '../../../assets/supports_acl.svg'; import { ComponentLoader } from '../../../components/shared/component_loader'; -import { CredentialItem } from '../../../components/shared/credential_item'; import { LicenseBadge } from '../../../components/shared/license_badge'; import { StatusItem } from '../../../components/shared/status_item'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; @@ -78,8 +77,6 @@ import { STATUS_TEXT, ADDITIONAL_CONFIG_HEADING, EXTERNAL_IDENTITIES_LINK, - ACCESS_TOKEN_LABEL, - ID_LABEL, LEARN_CUSTOM_FEATURES_BUTTON, DOC_PERMISSIONS_DESCRIPTION, CUSTOM_CALLOUT_TITLE, @@ -92,6 +89,7 @@ import { } from '../constants'; import { SourceLogic } from '../source_logic'; +import { SourceIdentifier } from './source_identifier'; import { SourceLayout } from './source_layout'; export const Overview: React.FC = () => { @@ -106,7 +104,6 @@ export const Overview: React.FC = () => { groups, details, custom, - accessToken, licenseSupportsPermissions, serviceTypeSupportsPermissions, indexPermissions, @@ -432,9 +429,7 @@ export const Overview: React.FC = () => {
- - - + ); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_identifier.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_identifier.test.tsx new file mode 100644 index 0000000000000..2a9af72f596ed --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_identifier.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { shallow } from 'enzyme'; + +import { EuiCopy, EuiButtonIcon, EuiFieldText } from '@elastic/eui'; + +import { SourceIdentifier } from './source_identifier'; + +describe('SourceIdentifier', () => { + const id = 'foo123'; + + it('renders the Source Identifier', () => { + const wrapper = shallow(); + + expect(wrapper.find(EuiFieldText).prop('value')).toEqual(id); + }); + + it('renders the copy button', () => { + const copyMock = jest.fn(); + const wrapper = shallow(); + + const copyEl = shallow(
{wrapper.find(EuiCopy).props().children(copyMock)}
); + expect(copyEl.find(EuiButtonIcon).props().onClick).toEqual(copyMock); + }); +}); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_identifier.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_identifier.tsx new file mode 100644 index 0000000000000..2c7784a554a25 --- /dev/null +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_identifier.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { + EuiFlexGroup, + EuiFlexItem, + EuiText, + EuiCopy, + EuiButtonIcon, + EuiFieldText, + EuiSpacer, +} from '@elastic/eui'; +import { FormattedMessage } from '@kbn/i18n-react'; + +import { EuiLinkTo } from '../../../../shared/react_router_helpers'; + +import { API_KEY_LABEL, COPY_TOOLTIP, COPIED_TOOLTIP } from '../../../constants'; +import { API_KEYS_PATH } from '../../../routes'; + +import { ID_LABEL } from '../constants'; + +interface Props { + id: string; +} + +export const SourceIdentifier: React.FC = ({ id }) => ( + <> + + + + {ID_LABEL} + + + + + {(copy) => ( + + )} + + + + + + + + +

+ + {API_KEY_LABEL} + + ), + }} + /> +

+
+ +); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx index f0ccfb201e3b3..e5924b672c771 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/components/source_settings.tsx @@ -17,6 +17,9 @@ import { EuiFlexGroup, EuiFlexItem, EuiFormRow, + EuiForm, + EuiSpacer, + EuiFilePicker, } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -26,7 +29,11 @@ import { AppLogic } from '../../../app_logic'; import { ContentSection } from '../../../components/shared/content_section'; import { SourceConfigFields } from '../../../components/shared/source_config_fields'; import { ViewContentHeader } from '../../../components/shared/view_content_header'; -import { NAV } from '../../../constants'; +import { + NAV, + GITHUB_VIA_APP_SERVICE_TYPE, + GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE, +} from '../../../constants'; import { CANCEL_BUTTON, @@ -36,6 +43,7 @@ import { REMOVE_BUTTON, } from '../../../constants'; import { SourceDataItem } from '../../../types'; +import { handlePrivateKeyUpload } from '../../../utils'; import { AddSourceLogic } from '../components/add_source/add_source_logic'; import { SOURCE_SETTINGS_HEADING, @@ -58,12 +66,19 @@ import { SourceLayout } from './source_layout'; export const SourceSettings: React.FC = () => { const { http } = useValues(HttpLogic); - const { updateContentSource, removeContentSource } = useActions(SourceLogic); + const { + updateContentSource, + removeContentSource, + setStagedPrivateKey, + updateContentSourceConfiguration, + } = useActions(SourceLogic); const { getSourceConfigData } = useActions(AddSourceLogic); const { - contentSource: { name, id, serviceType, isOauth1 }, + contentSource: { name, id, serviceType, isOauth1, secret }, buttonLoading, + stagedPrivateKey, + isConfigurationUpdateButtonLoading, } = useValues(SourceLogic); const { @@ -76,16 +91,22 @@ export const SourceSettings: React.FC = () => { getSourceConfigData(serviceType); }, []); - const { editPath } = staticSourceData.find( - (source) => source.serviceType === serviceType - ) as SourceDataItem; + const isGithubApp = + serviceType === GITHUB_VIA_APP_SERVICE_TYPE || + serviceType === GITHUB_ENTERPRISE_SERVER_VIA_APP_SERVICE_TYPE; + + const editPath = isGithubApp + ? undefined // undefined for GitHub apps, as they are configured source-wide, and don't use a connector where you can edit the configuration + : (staticSourceData.find((source) => source.serviceType === serviceType) as SourceDataItem) + .editPath; const [inputValue, setValue] = useState(name); const [confirmModalVisible, setModalVisibility] = useState(false); const showConfirm = () => setModalVisibility(true); const hideConfirm = () => setModalVisibility(false); - const showConfig = isOrganization && !isEmpty(configuredFields); + const showOauthConfig = !isGithubApp && isOrganization && !isEmpty(configuredFields); + const showGithubAppConfig = isGithubApp; const { clientId, clientSecret, publicKey, consumerKey, baseUrl } = configuredFields || {}; @@ -102,6 +123,11 @@ export const SourceSettings: React.FC = () => { updateContentSource(id, { name: inputValue }); }; + const submitConfigurationChange = (e: FormEvent) => { + e.preventDefault(); + updateContentSourceConfiguration(id, { private_key: stagedPrivateKey }); + }; + const handleSourceRemoval = () => { /** * The modal was just hanging while the UI waited for the server to respond. @@ -164,7 +190,7 @@ export const SourceSettings: React.FC = () => { - {showConfig && ( + {showOauthConfig && ( { baseUrl={baseUrl} /> - + {SOURCE_CONFIG_LINK} )} + {showGithubAppConfig && ( + + + +
{secret!.app_id}
+
+ {secret!.base_url && ( + +
{secret!.base_url}
+
+ )} + + <> +
SHA256:{secret!.fingerprint}
+ + handlePrivateKeyUpload(files, setStagedPrivateKey)} + initialPromptText="Upload a new .pem file to rotate the private key" + accept=".pem" + /> + +
+ + {isConfigurationUpdateButtonLoading ? 'Loading…' : 'Save'} + +
+
+ )} { buttonLoading: false, contentMeta: DEFAULT_META, contentFilterValue: '', + isConfigurationUpdateButtonLoading: false, + stagedPrivateKey: null, }; const searchServerResponse = { diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts index e97d48889d809..b76627f57b3a3 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/source_logic.ts @@ -39,10 +39,19 @@ export interface SourceActions { sourceId: string; source: ContentSourceFullData; }; + updateContentSourceConfiguration( + sourceId: string, + source: SourceUpdatePayload + ): { + sourceId: string; + source: ContentSourceFullData; + }; resetSourceState(): void; removeContentSource(sourceId: string): { sourceId: string }; initializeSource(sourceId: string): { sourceId: string }; setButtonNotLoading(): void; + setStagedPrivateKey(stagedPrivateKey: string | null): string | null; + setConfigurationUpdateButtonNotLoading(): void; } interface SourceValues { @@ -53,6 +62,8 @@ interface SourceValues { contentItems: SourceContentItem[]; contentMeta: Meta; contentFilterValue: string; + stagedPrivateKey: string | null; + isConfigurationUpdateButtonLoading: boolean; } interface SearchResultsResponse { @@ -62,6 +73,7 @@ interface SearchResultsResponse { interface SourceUpdatePayload { name?: string; + private_key?: string | null; indexing?: { enabled?: boolean; features?: { @@ -85,11 +97,17 @@ export const SourceLogic = kea>({ initializeSourceSynchronization: (sourceId: string) => ({ sourceId }), searchContentSourceDocuments: (sourceId: string) => ({ sourceId }), updateContentSource: (sourceId: string, source: SourceUpdatePayload) => ({ sourceId, source }), + updateContentSourceConfiguration: (sourceId: string, source: SourceUpdatePayload) => ({ + sourceId, + source, + }), removeContentSource: (sourceId: string) => ({ sourceId, }), resetSourceState: () => true, setButtonNotLoading: () => false, + setStagedPrivateKey: (stagedPrivateKey: string) => stagedPrivateKey, + setConfigurationUpdateButtonNotLoading: () => false, }, reducers: { contentSource: [ @@ -150,6 +168,20 @@ export const SourceLogic = kea>({ resetSourceState: () => '', }, ], + stagedPrivateKey: [ + null, + { + setStagedPrivateKey: (_, stagedPrivateKey) => stagedPrivateKey, + setContentSource: () => null, + }, + ], + isConfigurationUpdateButtonLoading: [ + false, + { + updateContentSourceConfiguration: () => true, + setConfigurationUpdateButtonNotLoading: () => false, + }, + ], }, listeners: ({ actions, values }) => ({ initializeSource: async ({ sourceId }) => { @@ -233,6 +265,26 @@ export const SourceLogic = kea>({ flashAPIErrors(e); } }, + updateContentSourceConfiguration: async ({ sourceId, source }) => { + const { isOrganization } = AppLogic.values; + const route = isOrganization + ? `/internal/workplace_search/org/sources/${sourceId}/settings` + : `/internal/workplace_search/account/sources/${sourceId}/settings`; + + try { + const response = await HttpLogic.values.http.patch(route, { + body: JSON.stringify({ content_source: source }), + }); + + actions.setContentSource(response); + + flashSuccessToast('Content source configuration was updated.'); + } catch (e) { + flashAPIErrors(e); + } finally { + actions.setConfigurationUpdateButtonNotLoading(); + } + }, removeContentSource: async ({ sourceId }) => { clearFlashMessages(); const { isOrganization } = AppLogic.values; diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx index bcf2b2792c5d5..cf5dc48682ae8 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.test.tsx @@ -34,7 +34,7 @@ describe('SourcesRouter', () => { }); it('renders sources routes', () => { - const TOTAL_ROUTES = 62; + const TOTAL_ROUTES = 63; const wrapper = shallow(); expect(wrapper.find(Switch)).toHaveLength(1); diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx index 5142f5d6597ae..23109506b364e 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/content_sources/sources_router.tsx @@ -14,7 +14,8 @@ import { useActions, useValues } from 'kea'; import { LicensingLogic } from '../../../shared/licensing'; import { AppLogic } from '../../app_logic'; import { - ADD_GITHUB_APP_PATH, + ADD_GITHUB_VIA_APP_PATH, + ADD_GITHUB_ENTERPRISE_SERVER_VIA_APP_PATH, ADD_SOURCE_PATH, SOURCE_DETAILS_PATH, PRIVATE_SOURCES_PATH, @@ -22,7 +23,7 @@ import { getSourcesPath, } from '../../routes'; -import { AddSource, AddSourceList, GitHubApp } from './components/add_source'; +import { AddSource, AddSourceList, GitHubViaApp } from './components/add_source'; import { OrganizationSources } from './organization_sources'; import { PrivateSources } from './private_sources'; import { staticSourceData } from './source_data'; @@ -67,8 +68,11 @@ export const SourcesRouter: React.FC = () => { - - + + + + + {staticSourceData.map(({ addPath, accountContextOnly }, i) => ( diff --git a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx index f86b390f99ceb..85f91f769cc77 100644 --- a/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx +++ b/x-pack/plugins/enterprise_search/public/applications/workplace_search/views/settings/components/connectors.tsx @@ -60,7 +60,7 @@ export const Connectors: React.FC = () => { const updateButtons = ( - + {UPDATE_BUTTON} diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/api_keys.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/api_keys.test.ts new file mode 100644 index 0000000000000..4855716cfc2fd --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/api_keys.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { MockRouter, mockRequestHandler, mockDependencies } from '../../__mocks__'; + +import { registerApiKeysRoute } from './api_keys'; + +describe('api keys routes', () => { + describe('GET /internal/workplace_search/api_keys', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'get', + path: '/internal/workplace_search/api_keys', + }); + + registerApiKeysRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/api_tokens', + }); + }); + }); + + describe('POST /internal/workplace_search/api_keys', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'post', + path: '/internal/workplace_search/api_keys', + }); + + registerApiKeysRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/api_tokens', + }); + }); + + describe('validates', () => { + it('correctly', () => { + const request = { + body: { + name: 'my-api-key', + }, + }; + mockRouter.shouldValidate(request); + }); + }); + }); + + describe('DELETE /internal/workplace_search/api_keys/{tokenName}', () => { + let mockRouter: MockRouter; + + beforeEach(() => { + jest.clearAllMocks(); + mockRouter = new MockRouter({ + method: 'delete', + path: '/internal/workplace_search/api_keys/{tokenName}', + }); + + registerApiKeysRoute({ + ...mockDependencies, + router: mockRouter.router, + }); + }); + + it('creates a request handler', () => { + expect(mockRequestHandler.createRequest).toHaveBeenCalledWith({ + path: '/ws/org/api_tokens/:tokenName', + }); + }); + }); +}); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/api_keys.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/api_keys.ts new file mode 100644 index 0000000000000..ff63c7b146750 --- /dev/null +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/api_keys.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema } from '@kbn/config-schema'; + +import { RouteDependencies } from '../../plugin'; + +export function registerApiKeysRoute({ + router, + enterpriseSearchRequestHandler, +}: RouteDependencies) { + router.get( + { + path: '/internal/workplace_search/api_keys', + validate: false, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/api_tokens', + }) + ); + + router.post( + { + path: '/internal/workplace_search/api_keys', + validate: { + body: schema.object({ + name: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/api_tokens', + }) + ); + + router.delete( + { + path: '/internal/workplace_search/api_keys/{tokenName}', + validate: { + params: schema.object({ + tokenName: schema.string(), + }), + }, + }, + enterpriseSearchRequestHandler.createRequest({ + path: '/ws/org/api_tokens/:tokenName', + }) + ); +} + +export const registerApiKeysRoutes = (dependencies: RouteDependencies) => { + registerApiKeysRoute(dependencies); +}; diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts index aa3b60a5ba047..24eff218c3345 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/index.ts @@ -7,6 +7,7 @@ import { RouteDependencies } from '../../plugin'; +import { registerApiKeysRoutes } from './api_keys'; import { registerGroupsRoutes } from './groups'; import { registerOAuthRoutes } from './oauth'; import { registerOverviewRoute } from './overview'; @@ -16,6 +17,7 @@ import { registerSettingsRoutes } from './settings'; import { registerSourcesRoutes } from './sources'; export const registerWorkplaceSearchRoutes = (dependencies: RouteDependencies) => { + registerApiKeysRoutes(dependencies); registerOverviewRoute(dependencies); registerOAuthRoutes(dependencies); registerGroupsRoutes(dependencies); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts index 961635c3f9001..3702298e8bcae 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.test.ts @@ -180,7 +180,7 @@ describe('sources routes', () => { login: 'user', password: 'changeme', organizations: ['swiftype'], - indexPermissions: true, + index_permissions: true, }, }; mockRouter.shouldValidate(request); @@ -688,7 +688,7 @@ describe('sources routes', () => { login: 'user', password: 'changeme', organizations: ['swiftype'], - indexPermissions: true, + index_permissions: true, }, }; mockRouter.shouldValidate(request); diff --git a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts index 011fe341d6edf..12f4844461409 100644 --- a/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts +++ b/x-pack/plugins/enterprise_search/server/routes/workplace_search/sources.ts @@ -60,6 +60,7 @@ const displaySettingsSchema = schema.object({ const sourceSettingsSchema = schema.object({ content_source: schema.object({ name: schema.maybe(schema.string()), + private_key: schema.maybe(schema.nullable(schema.string())), indexing: schema.maybe( schema.object({ enabled: schema.maybe(schema.boolean()), @@ -178,7 +179,7 @@ export function registerAccountCreateSourceRoute({ login: schema.maybe(schema.string()), password: schema.maybe(schema.string()), organizations: schema.maybe(schema.arrayOf(schema.string())), - indexPermissions: schema.maybe(schema.boolean()), + index_permissions: schema.maybe(schema.boolean()), }), }, }, @@ -522,7 +523,10 @@ export function registerOrgCreateSourceRoute({ login: schema.maybe(schema.string()), password: schema.maybe(schema.string()), organizations: schema.maybe(schema.arrayOf(schema.string())), - indexPermissions: schema.maybe(schema.boolean()), + index_permissions: schema.maybe(schema.boolean()), + app_id: schema.maybe(schema.string()), + base_url: schema.maybe(schema.string()), + private_key: schema.nullable(schema.maybe(schema.string())), }), }, }, diff --git a/x-pack/plugins/fleet/common/openapi/bundled.json b/x-pack/plugins/fleet/common/openapi/bundled.json index 1e17c693e01b9..f30369b5792b8 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.json +++ b/x-pack/plugins/fleet/common/openapi/bundled.json @@ -2673,8 +2673,7 @@ }, "required": [ "name", - "version", - "title" + "version" ] }, "namespace": { @@ -2713,8 +2712,7 @@ }, "required": [ "type", - "enabled", - "streams" + "enabled" ] } }, @@ -2729,9 +2727,7 @@ } }, "required": [ - "output_id", "inputs", - "policy_id", "name" ] }, @@ -2858,19 +2854,86 @@ }, "update_package_policy": { "title": "Update package policy", - "allOf": [ - { + "type": "object", + "description": "", + "properties": { + "version": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "package": { "type": "object", "properties": { + "name": { + "type": "string" + }, "version": { "type": "string" + }, + "title": { + "type": "string" } + }, + "required": [ + "name", + "title", + "version" + ] + }, + "namespace": { + "type": "string" + }, + "output_id": { + "type": "string" + }, + "inputs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "processors": { + "type": "array", + "items": { + "type": "string" + } + }, + "streams": { + "type": "array", + "items": {} + }, + "config": { + "type": "object" + }, + "vars": { + "type": "object" + } + }, + "required": [ + "type", + "enabled", + "streams" + ] } }, - { - "$ref": "#/components/schemas/new_package_policy" + "policy_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "type": "string" } - ] + }, + "required": null }, "output": { "title": "Output", diff --git a/x-pack/plugins/fleet/common/openapi/bundled.yaml b/x-pack/plugins/fleet/common/openapi/bundled.yaml index 1d7f1cb9ccf1f..44242423aa420 100644 --- a/x-pack/plugins/fleet/common/openapi/bundled.yaml +++ b/x-pack/plugins/fleet/common/openapi/bundled.yaml @@ -1683,7 +1683,6 @@ components: required: - name - version - - title namespace: type: string output_id: @@ -1711,7 +1710,6 @@ components: required: - type - enabled - - streams policy_id: type: string name: @@ -1719,9 +1717,7 @@ components: description: type: string required: - - output_id - inputs - - policy_id - name package_policy: title: Package policy @@ -1801,12 +1797,61 @@ components: items: {} update_package_policy: title: Update package policy - allOf: - - type: object + type: object + description: '' + properties: + version: + type: string + enabled: + type: boolean + package: + type: object properties: + name: + type: string version: type: string - - $ref: '#/components/schemas/new_package_policy' + title: + type: string + required: + - name + - title + - version + namespace: + type: string + output_id: + type: string + inputs: + type: array + items: + type: object + properties: + type: + type: string + enabled: + type: boolean + processors: + type: array + items: + type: string + streams: + type: array + items: {} + config: + type: object + vars: + type: object + required: + - type + - enabled + - streams + policy_id: + type: string + name: + type: string + description: + type: string + required: null output: title: Output type: object diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/new_package_policy.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/new_package_policy.yaml index e5e4451881b57..ad400a9eb8e0c 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/new_package_policy.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/new_package_policy.yaml @@ -16,7 +16,6 @@ properties: required: - name - version - - title namespace: type: string output_id: @@ -44,7 +43,6 @@ properties: required: - type - enabled - - streams policy_id: type: string name: @@ -52,7 +50,5 @@ properties: description: type: string required: - - output_id - inputs - - policy_id - name diff --git a/x-pack/plugins/fleet/common/openapi/components/schemas/update_package_policy.yaml b/x-pack/plugins/fleet/common/openapi/components/schemas/update_package_policy.yaml index 8f7f856a6649f..1d7fb2e7213de 100644 --- a/x-pack/plugins/fleet/common/openapi/components/schemas/update_package_policy.yaml +++ b/x-pack/plugins/fleet/common/openapi/components/schemas/update_package_policy.yaml @@ -1,7 +1,56 @@ title: Update package policy -allOf: - - type: object +type: object +description: '' +properties: + version: + type: string + enabled: + type: boolean + package: + type: object properties: + name: + type: string version: type: string - - $ref: ./new_package_policy.yaml + title: + type: string + required: + - name + - title + - version + namespace: + type: string + output_id: + type: string + inputs: + type: array + items: + type: object + properties: + type: + type: string + enabled: + type: boolean + processors: + type: array + items: + type: string + streams: + type: array + items: {} + config: + type: object + vars: + type: object + required: + - type + - enabled + - streams + policy_id: + type: string + name: + type: string + description: + type: string +required: \ No newline at end of file diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx index 16a16205261c9..dc931f835b043 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/assets/assets.tsx @@ -11,6 +11,8 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { EuiFlexGroup, EuiFlexItem, EuiTitle, EuiSpacer } from '@elastic/eui'; import { groupBy } from 'lodash'; +import type { ResolvedSimpleSavedObject } from 'src/core/public'; + import { Loading, Error, ExtensionWrapper } from '../../../../../components'; import type { PackageInfo } from '../../../../../types'; @@ -27,6 +29,7 @@ import type { AssetSavedObject } from './types'; import { allowedAssetTypes } from './constants'; import { AssetsAccordion } from './assets_accordion'; +const allowedAssetTypesLookup = new Set(allowedAssetTypes); interface AssetsPanelProps { packageInfo: PackageInfo; } @@ -74,19 +77,32 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => { const objectsByType = await Promise.all( Object.entries(groupBy(objectsToGet, 'type')).map(([type, objects]) => savedObjectsClient - .bulkGet(objects) + .bulkResolve(objects) // Ignore privilege errors .catch((e: any) => { if (e?.body?.statusCode === 403) { - return { savedObjects: [] }; + return { resolved_objects: [] }; } else { throw e; } }) - .then(({ savedObjects }) => savedObjects as AssetSavedObject[]) + .then( + ({ + resolved_objects: resolvedObjects, + }: { + resolved_objects: ResolvedSimpleSavedObject[]; + }) => { + return resolvedObjects + .map(({ saved_object: savedObject }) => savedObject) + .filter( + (savedObject) => + savedObject?.error?.statusCode !== 404 && + allowedAssetTypesLookup.has(savedObject.type) + ) as AssetSavedObject[]; + } + ) ) ); - setAssetsSavedObjects(objectsByType.flat()); } catch (e) { setFetchError(e); @@ -107,7 +123,6 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => { } let content: JSX.Element | Array; - if (isLoading) { content = ; } else if (fetchError) { @@ -122,7 +137,7 @@ export const AssetsPage = ({ packageInfo }: AssetsPanelProps) => { error={fetchError} /> ); - } else if (assetSavedObjects === undefined) { + } else if (assetSavedObjects === undefined || assetSavedObjects.length === 0) { if (customAssetsExtension) { // If a UI extension for custom asset entries is defined, render the custom component here depisite // there being no saved objects found diff --git a/x-pack/plugins/fleet/server/mocks/index.ts b/x-pack/plugins/fleet/server/mocks/index.ts index 90c9181b5007a..acdc0ba5e3fdd 100644 --- a/x-pack/plugins/fleet/server/mocks/index.ts +++ b/x-pack/plugins/fleet/server/mocks/index.ts @@ -99,6 +99,7 @@ export const createPackagePolicyServiceMock = (): jest.Mocked { try { + // Fleet remains `available` during setup as to excessively delay Kibana's boot process. + // This should be reevaluated as Fleet's setup process is optimized and stabilized. this.fleetStatus$.next({ - level: ServiceStatusLevels.degraded, + level: ServiceStatusLevels.available, summary: 'Fleet is setting up', }); diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts index 5441af0af686a..2408f8226f5d6 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.test.ts @@ -17,7 +17,12 @@ import type { PostPackagePolicyCreateCallback, PutPackagePolicyUpdateCallback, } from '../..'; -import type { CreatePackagePolicyRequestSchema } from '../../types/rest_spec'; +import type { + CreatePackagePolicyRequestSchema, + UpdatePackagePolicyRequestSchema, +} from '../../types/rest_spec'; + +import type { PackagePolicy } from '../../types'; import { registerRoutes } from './index'; @@ -72,6 +77,9 @@ jest.mock( ), upgrade: jest.fn(), getUpgradeDryRunDiff: jest.fn(), + enrichPolicyWithDefaultsFromPackage: jest + .fn() + .mockImplementation((soClient, newPolicy) => newPolicy), }, }; } @@ -91,7 +99,7 @@ describe('When calling package policy', () => { let context: ReturnType; let response: ReturnType; - beforeAll(() => { + beforeEach(() => { routerMock = httpServiceMock.createRouter(); registerRoutes(routerMock); }); @@ -132,7 +140,7 @@ describe('When calling package policy', () => { }; // Set the routeConfig and routeHandler to the Create API - beforeAll(() => { + beforeEach(() => { [routeConfig, routeHandler] = routerMock.post.mock.calls.find(([{ path }]) => path.startsWith(PACKAGE_POLICY_API_ROUTES.CREATE_PATTERN) )!; @@ -259,4 +267,148 @@ describe('When calling package policy', () => { }); }); }); + + describe('update api handler', () => { + const getUpdateKibanaRequest = ( + newData?: typeof UpdatePackagePolicyRequestSchema.body + ): KibanaRequest< + typeof UpdatePackagePolicyRequestSchema.params, + undefined, + typeof UpdatePackagePolicyRequestSchema.body + > => { + return httpServerMock.createKibanaRequest< + typeof UpdatePackagePolicyRequestSchema.params, + undefined, + typeof UpdatePackagePolicyRequestSchema.body + >({ + path: routeConfig.path, + method: 'put', + params: { packagePolicyId: '1' }, + body: newData || {}, + }); + }; + + const existingPolicy = { + name: 'endpoint-1', + description: 'desc', + policy_id: '2', + enabled: true, + output_id: '3', + inputs: [ + { + type: 'logfile', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { + type: 'logs', + dataset: 'apache.access', + }, + id: '1', + }, + ], + }, + ], + namespace: 'default', + package: { name: 'endpoint', title: 'Elastic Endpoint', version: '0.5.0' }, + vars: { + paths: { + value: ['/var/log/apache2/access.log*'], + type: 'text', + }, + }, + }; + + beforeEach(() => { + [routeConfig, routeHandler] = routerMock.put.mock.calls.find(([{ path }]) => + path.startsWith(PACKAGE_POLICY_API_ROUTES.UPDATE_PATTERN) + )!; + }); + + beforeEach(() => { + packagePolicyServiceMock.update.mockImplementation((soClient, esClient, policyId, newData) => + Promise.resolve(newData as PackagePolicy) + ); + packagePolicyServiceMock.get.mockResolvedValue({ + id: '1', + revision: 1, + created_at: '', + created_by: '', + updated_at: '', + updated_by: '', + ...existingPolicy, + inputs: [ + { + ...existingPolicy.inputs[0], + compiled_input: '', + streams: [ + { + ...existingPolicy.inputs[0].streams[0], + compiled_stream: {}, + }, + ], + }, + ], + }); + }); + + it('should use existing package policy props if not provided by request', async () => { + const request = getUpdateKibanaRequest(); + await routeHandler(context, request, response); + expect(response.ok).toHaveBeenCalledWith({ + body: { item: existingPolicy }, + }); + }); + + it('should use request package policy props if provided by request', async () => { + const newData = { + name: 'endpoint-2', + description: '', + policy_id: '3', + enabled: false, + output_id: '', + inputs: [ + { + type: 'metrics', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { + type: 'metrics', + dataset: 'apache.access', + }, + id: '1', + }, + ], + }, + ], + namespace: 'namespace', + package: { name: 'endpoint', title: 'Elastic Endpoint', version: '0.6.0' }, + vars: { + paths: { + value: ['/my/access.log*'], + type: 'text', + }, + }, + }; + const request = getUpdateKibanaRequest(newData as any); + await routeHandler(context, request, response); + expect(response.ok).toHaveBeenCalledWith({ + body: { item: newData }, + }); + }); + + it('should override props provided by request only', async () => { + const newData = { + namespace: 'namespace', + }; + const request = getUpdateKibanaRequest(newData as any); + await routeHandler(context, request, response); + expect(response.ok).toHaveBeenCalledWith({ + body: { item: { ...existingPolicy, namespace: 'namespace' } }, + }); + }); + }); }); diff --git a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts index 402a60e512145..33553a8699180 100644 --- a/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts +++ b/x-pack/plugins/fleet/server/routes/package_policy/handlers.ts @@ -23,6 +23,7 @@ import type { import type { CreatePackagePolicyResponse, DeletePackagePoliciesResponse, + NewPackagePolicy, UpgradePackagePolicyDryRunResponse, UpgradePackagePolicyResponse, } from '../../../common'; @@ -89,9 +90,14 @@ export const createPackagePolicyHandler: RequestHandler< const user = appContextService.getSecurity()?.authc.getCurrentUser(request) || undefined; const { force, ...newPolicy } = request.body; try { + const newPackagePolicy = await packagePolicyService.enrichPolicyWithDefaultsFromPackage( + soClient, + newPolicy as NewPackagePolicy + ); + const newData = await packagePolicyService.runExternalCallbacks( 'packagePolicyCreate', - newPolicy, + newPackagePolicy, context, request ); @@ -130,9 +136,33 @@ export const updatePackagePolicyHandler: RequestHandler< throw Boom.notFound('Package policy not found'); } - let newData = { ...request.body }; - const pkg = newData.package || packagePolicy.package; - const inputs = newData.inputs || packagePolicy.inputs; + const body = { ...request.body }; + // removed fields not recognized by schema + const packagePolicyInputs = packagePolicy.inputs.map((input) => { + const newInput = { + ...input, + streams: input.streams.map((stream) => { + const newStream = { ...stream }; + delete newStream.compiled_stream; + return newStream; + }), + }; + delete newInput.compiled_input; + return newInput; + }); + // listing down accepted properties, because loaded packagePolicy contains some that are not accepted in update + let newData = { + ...body, + name: body.name ?? packagePolicy.name, + description: body.description ?? packagePolicy.description, + namespace: body.namespace ?? packagePolicy.namespace, + policy_id: body.policy_id ?? packagePolicy.policy_id, + enabled: body.enabled ?? packagePolicy.enabled, + output_id: body.output_id ?? packagePolicy.output_id, + package: body.package ?? packagePolicy.package, + inputs: body.inputs ?? packagePolicyInputs, + vars: body.vars ?? packagePolicy.vars, + } as NewPackagePolicy; try { newData = await packagePolicyService.runExternalCallbacks( @@ -146,7 +176,7 @@ export const updatePackagePolicyHandler: RequestHandler< soClient, esClient, request.params.packagePolicyId, - { ...newData, package: pkg, inputs }, + newData, { user }, packagePolicy.package?.version ); diff --git a/x-pack/plugins/fleet/server/routes/security.ts b/x-pack/plugins/fleet/server/routes/security.ts index 9853877dc2d61..e0a2a557391df 100644 --- a/x-pack/plugins/fleet/server/routes/security.ts +++ b/x-pack/plugins/fleet/server/routes/security.ts @@ -151,8 +151,8 @@ export async function getAuthzFromRequest(req: KibanaRequest): Promise; +const formatImportErrorsForLog = (errors: SavedObjectsImportFailure[]) => + JSON.stringify( + errors.map(({ type, id, error }) => ({ type, id, error })) // discard other fields + ); +const validKibanaAssetTypes = new Set(Object.values(KibanaAssetType)); type SavedObjectToBe = Required> & { type: KibanaSavedObjectType; }; @@ -42,23 +54,8 @@ const KibanaSavedObjectTypeMapping: Record Promise>> -> = { - [KibanaAssetType.dashboard]: installKibanaSavedObjects, - [KibanaAssetType.indexPattern]: installKibanaIndexPatterns, - [KibanaAssetType.map]: installKibanaSavedObjects, - [KibanaAssetType.search]: installKibanaSavedObjects, - [KibanaAssetType.visualization]: installKibanaSavedObjects, - [KibanaAssetType.lens]: installKibanaSavedObjects, - [KibanaAssetType.mlModule]: installKibanaSavedObjects, - [KibanaAssetType.securityRule]: installKibanaSavedObjects, - [KibanaAssetType.tag]: installKibanaSavedObjects, +const AssetFilters: Record ArchiveAsset[]> = { + [KibanaAssetType.indexPattern]: removeReservedIndexPatterns, }; export async function getKibanaAsset(key: string): Promise { @@ -79,29 +76,46 @@ export function createSavedObjectKibanaAsset(asset: ArchiveAsset): SavedObjectTo }; } -// TODO: make it an exhaustive list -// e.g. switch statement with cases for each enum key returning `never` for default case export async function installKibanaAssets(options: { - savedObjectsClient: SavedObjectsClientContract; + savedObjectsImporter: SavedObjectsImporterContract; + logger: Logger; pkgName: string; kibanaAssets: Record; -}): Promise { - const { savedObjectsClient, kibanaAssets } = options; +}): Promise { + const { kibanaAssets, savedObjectsImporter, logger } = options; + const assetsToInstall = Object.entries(kibanaAssets).flatMap(([assetType, assets]) => { + if (!validKibanaAssetTypes.has(assetType as KibanaAssetType)) { + return []; + } - // install the assets - const kibanaAssetTypes = Object.values(KibanaAssetType); - const installedAssets = await Promise.all( - kibanaAssetTypes.map((assetType) => { - if (kibanaAssets[assetType]) { - return AssetInstallers[assetType]({ - savedObjectsClient, - kibanaAssets: kibanaAssets[assetType], - }); - } + if (!assets.length) { return []; - }) - ); - return installedAssets.flat(); + } + + const assetFilter = AssetFilters[assetType]; + if (assetFilter) { + return assetFilter(assets); + } + + return assets; + }); + + if (!assetsToInstall.length) { + return []; + } + + // As we use `import` to create our saved objects, we have to install + // their references (the index patterns) at the same time + // to prevent a reference error + const indexPatternSavedObjects = getIndexPatternSavedObjects() as ArchiveAsset[]; + + const installedAssets = await installKibanaSavedObjects({ + logger, + savedObjectsImporter, + kibanaAssets: [...indexPatternSavedObjects, ...assetsToInstall], + }); + + return installedAssets; } export const deleteKibanaInstalledRefs = async ( savedObjectsClient: SavedObjectsClientContract, @@ -153,39 +167,95 @@ export async function getKibanaAssets( } async function installKibanaSavedObjects({ - savedObjectsClient, + savedObjectsImporter, kibanaAssets, + logger, }: { - savedObjectsClient: SavedObjectsClientContract; kibanaAssets: ArchiveAsset[]; + savedObjectsImporter: SavedObjectsImporterContract; + logger: Logger; }) { const toBeSavedObjects = await Promise.all( kibanaAssets.map((asset) => createSavedObjectKibanaAsset(asset)) ); + let allSuccessResults = []; + if (toBeSavedObjects.length === 0) { return []; } else { - const createResults = await savedObjectsClient.bulkCreate(toBeSavedObjects, { - overwrite: true, - }); - return createResults.saved_objects; + const { successResults: importSuccessResults = [], errors: importErrors = [] } = + await savedObjectsImporter.import({ + overwrite: true, + readStream: createListStream(toBeSavedObjects), + createNewCopies: false, + }); + + allSuccessResults = importSuccessResults; + const [referenceErrors, otherErrors] = partition( + importErrors, + (e) => e?.error?.type === 'missing_references' + ); + + if (otherErrors?.length) { + throw new Error( + `Encountered ${ + otherErrors.length + } errors creating saved objects: ${formatImportErrorsForLog(otherErrors)}` + ); + } + /* + A reference error here means that a saved object reference in the references + array cannot be found. This is an error in the package its-self but not a fatal + one. For example a dashboard may still refer to the legacy `metricbeat-*` index + pattern. We ignore reference errors here so that legacy version of a package + can still be installed, but if a warning is logged it should be reported to + the integrations team. */ + if (referenceErrors.length) { + logger.debug( + `Resolving ${ + referenceErrors.length + } reference errors creating saved objects: ${formatImportErrorsForLog(referenceErrors)}` + ); + + const idsToResolve = new Set(referenceErrors.map(({ id }) => id)); + + const resolveSavedObjects = toBeSavedObjects.filter(({ id }) => idsToResolve.has(id)); + const retries = referenceErrors.map(({ id, type }) => ({ + id, + type, + ignoreMissingReferences: true, + replaceReferences: [], + overwrite: true, + })); + + const { successResults: resolveSuccessResults = [], errors: resolveErrors = [] } = + await savedObjectsImporter.resolveImportErrors({ + readStream: createListStream(resolveSavedObjects), + createNewCopies: false, + retries, + }); + + if (resolveErrors?.length) { + throw new Error( + `Encountered ${ + resolveErrors.length + } errors resolving reference errors: ${formatImportErrorsForLog(resolveErrors)}` + ); + } + + allSuccessResults = [...allSuccessResults, ...resolveSuccessResults]; + } + + return allSuccessResults; } } -async function installKibanaIndexPatterns({ - savedObjectsClient, - kibanaAssets, -}: { - savedObjectsClient: SavedObjectsClientContract; - kibanaAssets: ArchiveAsset[]; -}) { - // Filter out any reserved index patterns +// Filter out any reserved index patterns +function removeReservedIndexPatterns(kibanaAssets: ArchiveAsset[]) { const reservedPatterns = indexPatternTypes.map((pattern) => `${pattern}-*`); - const nonReservedPatterns = kibanaAssets.filter((asset) => !reservedPatterns.includes(asset.id)); - - return installKibanaSavedObjects({ savedObjectsClient, kibanaAssets: nonReservedPatterns }); + return kibanaAssets.filter((asset) => !reservedPatterns.includes(asset.id)); } export function toAssetReference({ id, type }: SavedObject) { diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/__snapshots__/install.test.ts.snap b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/__snapshots__/install.test.ts.snap deleted file mode 100644 index da870290329a8..0000000000000 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/__snapshots__/install.test.ts.snap +++ /dev/null @@ -1,935 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`creating index patterns from yaml fields createFieldFormatMap creates correct map based on inputs all variations and all the params get passed through: createFieldFormatMap 1`] = ` -{ - "fieldPattern": { - "params": { - "pattern": "patternVal" - } - }, - "fieldFormat": { - "id": "formatVal" - }, - "fieldFormatWithParam": { - "id": "formatVal", - "params": { - "outputPrecision": 2 - } - }, - "fieldFormatAndPattern": { - "id": "formatVal", - "params": { - "pattern": "patternVal" - } - }, - "fieldFormatAndAllParams": { - "id": "formatVal", - "params": { - "pattern": "pattenVal", - "inputFormat": "inputFormatVal", - "outputFormat": "outputFormalVal", - "outputPrecision": 3, - "labelTemplate": "labelTemplateVal", - "urlTemplate": "urlTemplateVal" - } - } -} -`; - -exports[`creating index patterns from yaml fields createIndexPattern function creates Kibana index pattern: createIndexPattern 1`] = ` -{ - "title": "logs-*", - "timeFieldName": "@timestamp", - "fields": "[{\\"name\\":\\"coredns.id\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.allParams\\",\\"type\\":\\"number\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.query.length\\",\\"type\\":\\"number\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.query.size\\",\\"type\\":\\"number\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.query.class\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.query.name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.query.type\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.response.code\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.response.flags\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.response.size\\",\\"type\\":\\"number\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"coredns.dnssec_ok\\",\\"type\\":\\"boolean\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"@timestamp\\",\\"type\\":\\"date\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"labels\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"message\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":false,\\"readFromDocValues\\":true},{\\"name\\":\\"tags\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"agent.ephemeral_id\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"agent.id\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"agent.name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"agent.type\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"agent.version\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"as.number\\",\\"type\\":\\"number\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"as.organization.name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.remote_ip_list\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.body_sent.bytes\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.method\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.url\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.http_version\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.response_code\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.referrer\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.agent\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_agent.device\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_agent.name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_agent.os\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_agent.os_name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.user_agent.original\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.continent_name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":false,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.country_iso_code\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.location\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.region_name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.city_name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"nginx.access.geoip.region_iso_code\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"source.geo.continent_name\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":false,\\"readFromDocValues\\":true},{\\"name\\":\\"country\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"country.keyword\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":true,\\"readFromDocValues\\":true},{\\"name\\":\\"country.text\\",\\"type\\":\\"string\\",\\"count\\":0,\\"scripted\\":false,\\"indexed\\":true,\\"searchable\\":true,\\"aggregatable\\":false,\\"readFromDocValues\\":true}]", - "fieldFormatMap": "{\\"coredns.allParams\\":{\\"id\\":\\"bytes\\",\\"params\\":{\\"pattern\\":\\"patternValQueryWeight\\",\\"inputFormat\\":\\"inputFormatVal,\\",\\"outputFormat\\":\\"outputFormalVal,\\",\\"outputPrecision\\":\\"3,\\",\\"labelTemplate\\":\\"labelTemplateVal,\\",\\"urlTemplate\\":\\"urlTemplateVal,\\"}},\\"coredns.query.length\\":{\\"params\\":{\\"pattern\\":\\"patternValQueryLength\\"}},\\"coredns.query.size\\":{\\"id\\":\\"bytes\\",\\"params\\":{\\"pattern\\":\\"patternValQuerySize\\"}},\\"coredns.response.size\\":{\\"id\\":\\"bytes\\"}}", - "allowNoIndex": true -} -`; - -exports[`creating index patterns from yaml fields createIndexPatternFields function creates Kibana index pattern fields and fieldFormatMap: createIndexPatternFields 1`] = ` -{ - "indexPatternFields": [ - { - "name": "coredns.id", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "coredns.allParams", - "type": "number", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "coredns.query.length", - "type": "number", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "coredns.query.size", - "type": "number", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "coredns.query.class", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "coredns.query.name", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "coredns.query.type", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "coredns.response.code", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "coredns.response.flags", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "coredns.response.size", - "type": "number", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "coredns.dnssec_ok", - "type": "boolean", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "@timestamp", - "type": "date", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "labels", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "message", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": false, - "readFromDocValues": true - }, - { - "name": "tags", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "agent.ephemeral_id", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "agent.id", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "agent.name", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "agent.type", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "agent.version", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "as.number", - "type": "number", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "as.organization.name", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.remote_ip_list", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.body_sent.bytes", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.user_name", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.method", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.url", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.http_version", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.response_code", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.referrer", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.agent", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.user_agent.device", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.user_agent.name", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.user_agent.os", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.user_agent.os_name", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.user_agent.original", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.geoip.continent_name", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": false, - "readFromDocValues": true - }, - { - "name": "nginx.access.geoip.country_iso_code", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.geoip.location", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.geoip.region_name", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.geoip.city_name", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "nginx.access.geoip.region_iso_code", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "source.geo.continent_name", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": false, - "readFromDocValues": true - }, - { - "name": "country", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "country.keyword", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": true, - "readFromDocValues": true - }, - { - "name": "country.text", - "type": "string", - "count": 0, - "scripted": false, - "indexed": true, - "searchable": true, - "aggregatable": false, - "readFromDocValues": true - } - ], - "fieldFormatMap": { - "coredns.allParams": { - "id": "bytes", - "params": { - "pattern": "patternValQueryWeight", - "inputFormat": "inputFormatVal,", - "outputFormat": "outputFormalVal,", - "outputPrecision": "3,", - "labelTemplate": "labelTemplateVal,", - "urlTemplate": "urlTemplateVal," - } - }, - "coredns.query.length": { - "params": { - "pattern": "patternValQueryLength" - } - }, - "coredns.query.size": { - "id": "bytes", - "params": { - "pattern": "patternValQuerySize" - } - }, - "coredns.response.size": { - "id": "bytes" - } - } -} -`; - -exports[`creating index patterns from yaml fields flattenFields function flattens recursively and handles copying alias fields flattenFields matches snapshot: flattenFields 1`] = ` -[ - { - "name": "coredns.id", - "type": "keyword", - "description": "id of the DNS transaction\\n" - }, - { - "name": "coredns.allParams", - "type": "integer", - "format": "bytes", - "pattern": "patternValQueryWeight", - "input_format": "inputFormatVal,", - "output_format": "outputFormalVal,", - "output_precision": "3,", - "label_template": "labelTemplateVal,", - "url_template": "urlTemplateVal,", - "openLinkInCurrentTab": "true,", - "description": "weight of the DNS query\\n" - }, - { - "name": "coredns.query.length", - "type": "integer", - "pattern": "patternValQueryLength", - "description": "length of the DNS query\\n" - }, - { - "name": "coredns.query.size", - "type": "integer", - "format": "bytes", - "pattern": "patternValQuerySize", - "description": "size of the DNS query\\n" - }, - { - "name": "coredns.query.class", - "type": "keyword", - "description": "DNS query class\\n" - }, - { - "name": "coredns.query.name", - "type": "keyword", - "description": "DNS query name\\n" - }, - { - "name": "coredns.query.type", - "type": "keyword", - "description": "DNS query type\\n" - }, - { - "name": "coredns.response.code", - "type": "keyword", - "description": "DNS response code\\n" - }, - { - "name": "coredns.response.flags", - "type": "keyword", - "description": "DNS response flags\\n" - }, - { - "name": "coredns.response.size", - "type": "integer", - "format": "bytes", - "description": "size of the DNS response\\n" - }, - { - "name": "coredns.dnssec_ok", - "type": "boolean", - "description": "dnssec flag\\n" - }, - { - "name": "@timestamp", - "level": "core", - "required": true, - "type": "date", - "description": "Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.", - "example": "2016-05-23T08:05:34.853Z" - }, - { - "name": "labels", - "level": "core", - "type": "object", - "object_type": "keyword", - "description": "Custom key/value pairs. Can be used to add meta information to events. Should not contain nested objects. All values are stored as keyword. Example: \`docker\` and \`k8s\` labels.", - "example": { - "application": "foo-bar", - "env": "production" - } - }, - { - "name": "message", - "level": "core", - "type": "text", - "description": "For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.", - "example": "Hello World" - }, - { - "name": "tags", - "level": "core", - "type": "keyword", - "ignore_above": 1024, - "description": "List of keywords used to tag each event.", - "example": "[\\"production\\", \\"env2\\"]" - }, - { - "name": "agent.ephemeral_id", - "level": "extended", - "type": "keyword", - "ignore_above": 1024, - "description": "Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but \`agent.id\` does not.", - "example": "8a4f500f" - }, - { - "name": "agent.id", - "level": "core", - "type": "keyword", - "ignore_above": 1024, - "description": "Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.", - "example": "8a4f500d" - }, - { - "name": "agent.name", - "level": "core", - "type": "keyword", - "ignore_above": 1024, - "description": "Custom name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.", - "example": "foo" - }, - { - "name": "agent.type", - "level": "core", - "type": "keyword", - "ignore_above": 1024, - "description": "Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.", - "example": "filebeat" - }, - { - "name": "agent.version", - "level": "core", - "type": "keyword", - "ignore_above": 1024, - "description": "Version of the agent.", - "example": "6.0.0-rc2" - }, - { - "name": "as.number", - "level": "extended", - "type": "long", - "description": "Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.", - "example": 15169 - }, - { - "name": "as.organization.name", - "level": "extended", - "type": "keyword", - "ignore_above": 1024, - "description": "Organization name.", - "example": "Google LLC" - }, - { - "name": "@timestamp", - "level": "core", - "required": true, - "type": "date", - "description": "Date/time when the event originated. This is the date/time extracted from the event, typically representing when the event was generated by the source. If the event source has no original timestamp, this value is typically populated by the first time the event was received by the pipeline. Required field for all events.", - "example": "2016-05-23T08:05:34.853Z" - }, - { - "name": "labels", - "level": "core", - "type": "object", - "object_type": "keyword", - "description": "Custom key/value pairs. Can be used to add meta information to events. Should not contain nested objects. All values are stored as keyword. Example: \`docker\` and \`k8s\` labels.", - "example": { - "application": "foo-bar", - "env": "production" - } - }, - { - "name": "message", - "level": "core", - "type": "text", - "description": "For log events the message field contains the log message, optimized for viewing in a log viewer. For structured logs without an original message field, other fields can be concatenated to form a human-readable summary of the event. If multiple messages exist, they can be combined into one message.", - "example": "Hello World" - }, - { - "name": "tags", - "level": "core", - "type": "keyword", - "ignore_above": 1024, - "description": "List of keywords used to tag each event.", - "example": "[\\"production\\", \\"env2\\"]" - }, - { - "name": "agent.ephemeral_id", - "level": "extended", - "type": "keyword", - "ignore_above": 1024, - "description": "Ephemeral identifier of this agent (if one exists). This id normally changes across restarts, but \`agent.id\` does not.", - "example": "8a4f500f" - }, - { - "name": "agent.id", - "level": "core", - "type": "keyword", - "ignore_above": 1024, - "description": "Unique identifier of this agent (if one exists). Example: For Beats this would be beat.id.", - "example": "8a4f500d" - }, - { - "name": "agent.name", - "level": "core", - "type": "keyword", - "ignore_above": 1024, - "description": "Custom name of the agent. This is a name that can be given to an agent. This can be helpful if for example two Filebeat instances are running on the same host but a human readable separation is needed on which Filebeat instance data is coming from. If no name is given, the name is often left empty.", - "example": "foo" - }, - { - "name": "agent.type", - "level": "core", - "type": "keyword", - "ignore_above": 1024, - "description": "Type of the agent. The agent type stays always the same and should be given by the agent used. In case of Filebeat the agent would always be Filebeat also if two Filebeat instances are run on the same machine.", - "example": "filebeat" - }, - { - "name": "agent.version", - "level": "core", - "type": "keyword", - "ignore_above": 1024, - "description": "Version of the agent.", - "example": "6.0.0-rc2" - }, - { - "name": "as.number", - "level": "extended", - "type": "long", - "description": "Unique number allocated to the autonomous system. The autonomous system number (ASN) uniquely identifies each network on the Internet.", - "example": 15169 - }, - { - "name": "as.organization.name", - "level": "extended", - "type": "keyword", - "ignore_above": 1024, - "description": "Organization name.", - "example": "Google LLC" - }, - { - "name": "nginx.access.remote_ip_list", - "type": "array", - "description": "An array of remote IP addresses. It is a list because it is common to include, besides the client IP address, IP addresses from headers like \`X-Forwarded-For\`. Real source IP is restored to \`source.ip\`.\\n" - }, - { - "name": "nginx.access.body_sent.bytes", - "type": "alias", - "path": "http.response.body.bytes", - "migration": true - }, - { - "name": "nginx.access.user_name", - "type": "alias", - "path": "user.name", - "migration": true - }, - { - "name": "nginx.access.method", - "type": "alias", - "path": "http.request.method", - "migration": true - }, - { - "name": "nginx.access.url", - "type": "alias", - "path": "url.original", - "migration": true - }, - { - "name": "nginx.access.http_version", - "type": "alias", - "path": "http.version", - "migration": true - }, - { - "name": "nginx.access.response_code", - "type": "alias", - "path": "http.response.status_code", - "migration": true - }, - { - "name": "nginx.access.referrer", - "type": "alias", - "path": "http.request.referrer", - "migration": true - }, - { - "name": "nginx.access.agent", - "type": "alias", - "path": "user_agent.original", - "migration": true - }, - { - "name": "nginx.access.user_agent.device", - "type": "alias", - "path": "user_agent.device.name", - "migration": true - }, - { - "name": "nginx.access.user_agent.name", - "type": "alias", - "path": "user_agent.name", - "migration": true - }, - { - "name": "nginx.access.user_agent.os", - "type": "alias", - "path": "user_agent.os.full_name", - "migration": true - }, - { - "name": "nginx.access.user_agent.os_name", - "type": "alias", - "path": "user_agent.os.name", - "migration": true - }, - { - "name": "nginx.access.user_agent.original", - "type": "alias", - "path": "user_agent.original", - "migration": true - }, - { - "name": "nginx.access.geoip.continent_name", - "type": "text", - "path": "source.geo.continent_name" - }, - { - "name": "nginx.access.geoip.country_iso_code", - "type": "alias", - "path": "source.geo.country_iso_code", - "migration": true - }, - { - "name": "nginx.access.geoip.location", - "type": "alias", - "path": "source.geo.location", - "migration": true - }, - { - "name": "nginx.access.geoip.region_name", - "type": "alias", - "path": "source.geo.region_name", - "migration": true - }, - { - "name": "nginx.access.geoip.city_name", - "type": "alias", - "path": "source.geo.city_name", - "migration": true - }, - { - "name": "nginx.access.geoip.region_iso_code", - "type": "alias", - "path": "source.geo.region_iso_code", - "migration": true - }, - { - "name": "source.geo.continent_name", - "type": "text" - }, - { - "name": "country", - "type": "", - "multi_fields": [ - { - "name": "keyword", - "type": "keyword" - }, - { - "name": "text", - "type": "text" - } - ] - }, - { - "name": "country.keyword", - "type": "keyword" - }, - { - "name": "country.text", - "type": "text" - } -] -`; diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.test.ts b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.test.ts deleted file mode 100644 index dfdaa66a7b43e..0000000000000 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.test.ts +++ /dev/null @@ -1,309 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import path from 'path'; -import { readFileSync } from 'fs'; - -import glob from 'glob'; -import { safeLoad } from 'js-yaml'; - -import type { FieldSpec } from 'src/plugins/data/common'; - -import type { Fields, Field } from '../../fields/field'; - -import { - flattenFields, - dedupeFields, - transformField, - findFieldByPath, - createFieldFormatMap, - createIndexPatternFields, - createIndexPattern, -} from './install'; -import { dupeFields } from './tests/test_data'; - -// Add our own serialiser to just do JSON.stringify -expect.addSnapshotSerializer({ - print(val) { - return JSON.stringify(val, null, 2); - }, - - test(val) { - return val; - }, -}); -const files = glob.sync(path.join(__dirname, '/tests/*.yml')); -let fields: Fields = []; -for (const file of files) { - const fieldsYML = readFileSync(file, 'utf-8'); - fields = fields.concat(safeLoad(fieldsYML)); -} - -describe('creating index patterns from yaml fields', () => { - interface Test { - fields: Field[]; - expect: string | number | boolean | undefined; - } - - const name = 'testField'; - - test('createIndexPatternFields function creates Kibana index pattern fields and fieldFormatMap', () => { - const indexPatternFields = createIndexPatternFields(fields); - expect(indexPatternFields).toMatchSnapshot('createIndexPatternFields'); - }); - - test('createIndexPattern function creates Kibana index pattern', () => { - const indexPattern = createIndexPattern('logs', fields); - expect(indexPattern).toMatchSnapshot('createIndexPattern'); - }); - - describe('flattenFields function flattens recursively and handles copying alias fields', () => { - test('a field of type group with no nested fields is skipped', () => { - const flattened = flattenFields([{ name: 'nginx', type: 'group' }]); - expect(flattened.length).toBe(0); - }); - test('flattenFields matches snapshot', () => { - const flattened = flattenFields(fields); - expect(flattened).toMatchSnapshot('flattenFields'); - }); - }); - - describe('dedupFields', () => { - const deduped = dedupeFields(dupeFields); - const checkIfDup = (field: Field) => { - return deduped.filter((item) => item.name === field.name); - }; - test('there there is one field object with name of "1"', () => { - expect(checkIfDup({ name: '1' }).length).toBe(1); - }); - test('there there is one field object with name of "1.1"', () => { - expect(checkIfDup({ name: '1.1' }).length).toBe(1); - }); - test('there there is one field object with name of "2"', () => { - expect(checkIfDup({ name: '2' }).length).toBe(1); - }); - test('there there is one field object with name of "4"', () => { - expect(checkIfDup({ name: '4' }).length).toBe(1); - }); - // existing field takes precendence - test('the new merged field has correct attributes', () => { - const mergedField = deduped.find((field) => field.name === '1'); - expect(mergedField?.searchable).toBe(true); - expect(mergedField?.aggregatable).toBe(true); - expect(mergedField?.count).toBe(0); - }); - }); - - describe('getFieldByPath searches recursively for field in fields given dot separated path', () => { - const searchFields: Fields = [ - { - name: '1', - fields: [ - { - name: '1-1', - }, - { - name: '1-2', - }, - ], - }, - { - name: '2', - fields: [ - { - name: '2-1', - }, - { - name: '2-2', - fields: [ - { - name: '2-2-1', - }, - { - name: '2-2-2', - }, - ], - }, - ], - }, - ]; - test('returns undefined when the field does not exist', () => { - expect(findFieldByPath(searchFields, '0')).toBe(undefined); - }); - test('returns undefined if the field is not a leaf node', () => { - expect(findFieldByPath(searchFields, '1')?.name).toBe(undefined); - }); - test('returns undefined searching for a nested field that does not exist', () => { - expect(findFieldByPath(searchFields, '1.1-3')?.name).toBe(undefined); - }); - test('returns nested field that is a leaf node', () => { - expect(findFieldByPath(searchFields, '2.2-2.2-2-1')?.name).toBe('2-2-1'); - }); - }); - - test('transformField maps field types to kibana index pattern data types', () => { - const tests: Test[] = [ - { fields: [{ name: 'testField' }], expect: 'string' }, - { fields: [{ name: 'testField', type: 'half_float' }], expect: 'number' }, - { fields: [{ name: 'testField', type: 'scaled_float' }], expect: 'number' }, - { fields: [{ name: 'testField', type: 'float' }], expect: 'number' }, - { fields: [{ name: 'testField', type: 'integer' }], expect: 'number' }, - { fields: [{ name: 'testField', type: 'long' }], expect: 'number' }, - { fields: [{ name: 'testField', type: 'short' }], expect: 'number' }, - { fields: [{ name: 'testField', type: 'byte' }], expect: 'number' }, - { fields: [{ name: 'testField', type: 'keyword' }], expect: 'string' }, - { fields: [{ name: 'testField', type: 'invalidType' }], expect: 'string' }, - { fields: [{ name: 'testField', type: 'text' }], expect: 'string' }, - { fields: [{ name: 'testField', type: 'date' }], expect: 'date' }, - { fields: [{ name: 'testField', type: 'geo_point' }], expect: 'geo_point' }, - { fields: [{ name: 'testField', type: 'constant_keyword' }], expect: 'string' }, - ]; - - tests.forEach((test) => { - const res = test.fields.map(transformField); - expect(res[0].type).toBe(test.expect); - }); - }); - - test('transformField changes values based on other values', () => { - interface TestWithAttr extends Test { - attr: keyof FieldSpec; - } - - const tests: TestWithAttr[] = [ - // count - { fields: [{ name }], expect: 0, attr: 'count' }, - { fields: [{ name, count: 4 }], expect: 4, attr: 'count' }, - - // searchable - { fields: [{ name }], expect: true, attr: 'searchable' }, - { fields: [{ name, searchable: true }], expect: true, attr: 'searchable' }, - { fields: [{ name, searchable: false }], expect: false, attr: 'searchable' }, - { fields: [{ name, type: 'binary' }], expect: false, attr: 'searchable' }, - { fields: [{ name, searchable: true, type: 'binary' }], expect: false, attr: 'searchable' }, - { - fields: [{ name, searchable: true, type: 'object', enabled: false }], - expect: false, - attr: 'searchable', - }, - - // aggregatable - { fields: [{ name }], expect: true, attr: 'aggregatable' }, - { fields: [{ name, aggregatable: true }], expect: true, attr: 'aggregatable' }, - { fields: [{ name, aggregatable: false }], expect: false, attr: 'aggregatable' }, - { fields: [{ name, type: 'binary' }], expect: false, attr: 'aggregatable' }, - { - fields: [{ name, aggregatable: true, type: 'binary' }], - expect: false, - attr: 'aggregatable', - }, - { fields: [{ name, type: 'keyword' }], expect: true, attr: 'aggregatable' }, - { fields: [{ name, type: 'constant_keyword' }], expect: true, attr: 'aggregatable' }, - { fields: [{ name, type: 'text', aggregatable: true }], expect: false, attr: 'aggregatable' }, - { fields: [{ name, type: 'text' }], expect: false, attr: 'aggregatable' }, - { - fields: [{ name, aggregatable: true, type: 'object', enabled: false }], - expect: false, - attr: 'aggregatable', - }, - - // indexed - { fields: [{ name, type: 'binary' }], expect: false, attr: 'indexed' }, - { - fields: [{ name, index: true, type: 'binary' }], - expect: false, - attr: 'indexed', - }, - { - fields: [{ name, index: true, type: 'object', enabled: false }], - expect: false, - attr: 'indexed', - }, - - // script, scripted - { fields: [{ name }], expect: false, attr: 'scripted' }, - { fields: [{ name }], expect: undefined, attr: 'script' }, - { fields: [{ name, script: 'doc[]' }], expect: true, attr: 'scripted' }, - { fields: [{ name, script: 'doc[]' }], expect: 'doc[]', attr: 'script' }, - - // lang - { fields: [{ name }], expect: undefined, attr: 'lang' }, - { fields: [{ name, script: 'doc[]' }], expect: 'painless', attr: 'lang' }, - ]; - tests.forEach((test) => { - const res = test.fields.map(transformField); - expect(res[0][test.attr]).toBe(test.expect); - }); - }); - - describe('createFieldFormatMap creates correct map based on inputs', () => { - test('field with no format or pattern have empty fieldFormatMap', () => { - const fieldsToFormat = [{ name: 'fieldName', input_format: 'inputFormatVal' }]; - const fieldFormatMap = createFieldFormatMap(fieldsToFormat); - expect(fieldFormatMap).toEqual({}); - }); - test('field with pattern and no format creates fieldFormatMap with no id', () => { - const fieldsToFormat = [ - { name: 'fieldName', pattern: 'patternVal', input_format: 'inputFormatVal' }, - ]; - const fieldFormatMap = createFieldFormatMap(fieldsToFormat); - const expectedFieldFormatMap = { - fieldName: { - params: { - pattern: 'patternVal', - inputFormat: 'inputFormatVal', - }, - }, - }; - expect(fieldFormatMap).toEqual(expectedFieldFormatMap); - }); - - test('field with format and params creates fieldFormatMap with id', () => { - const fieldsToFormat = [ - { - name: 'fieldName', - format: 'formatVal', - pattern: 'patternVal', - input_format: 'inputFormatVal', - }, - ]; - const fieldFormatMap = createFieldFormatMap(fieldsToFormat); - const expectedFieldFormatMap = { - fieldName: { - id: 'formatVal', - params: { - pattern: 'patternVal', - inputFormat: 'inputFormatVal', - }, - }, - }; - expect(fieldFormatMap).toEqual(expectedFieldFormatMap); - }); - - test('all variations and all the params get passed through', () => { - const fieldsToFormat = [ - { name: 'fieldPattern', pattern: 'patternVal' }, - { name: 'fieldFormat', format: 'formatVal' }, - { name: 'fieldFormatWithParam', format: 'formatVal', output_precision: 2 }, - { name: 'fieldFormatAndPattern', format: 'formatVal', pattern: 'patternVal' }, - { - name: 'fieldFormatAndAllParams', - format: 'formatVal', - pattern: 'pattenVal', - input_format: 'inputFormatVal', - output_format: 'outputFormalVal', - output_precision: 3, - label_template: 'labelTemplateVal', - url_template: 'urlTemplateVal', - openLinkInCurrentTab: true, - }, - ]; - const fieldFormatMap = createFieldFormatMap(fieldsToFormat); - expect(fieldFormatMap).toMatchSnapshot('createFieldFormatMap'); - }); - }); -}); diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts index 61d6f6ed8818a..c42029f2c453d 100644 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/install.ts @@ -5,380 +5,58 @@ * 2.0. */ -import type { SavedObjectsClientContract, ElasticsearchClient } from 'src/core/server'; -import type { FieldSpec } from 'src/plugins/data/common'; +import type { SavedObjectsClientContract } from 'src/core/server'; -import { loadFieldsFromYaml } from '../../fields/field'; -import type { Fields, Field } from '../../fields/field'; import { dataTypes, installationStatuses } from '../../../../../common/constants'; -import type { - ArchivePackage, - Installation, - InstallSource, - ValueOf, -} from '../../../../../common/types'; import { appContextService } from '../../../../services'; -import type { RegistryPackage, DataType } from '../../../../types'; -import { getInstallation, getPackageFromSource, getPackageSavedObjects } from '../../packages/get'; - -interface FieldFormatMap { - [key: string]: FieldFormatMapItem; -} -interface FieldFormatMapItem { - id?: string; - params?: FieldFormatParams; -} -interface FieldFormatParams { - pattern?: string; - inputFormat?: string; - outputFormat?: string; - outputPrecision?: number; - labelTemplate?: string; - urlTemplate?: string; - openLinkInCurrentTab?: boolean; -} -/* this should match https://github.com/elastic/beats/blob/d9a4c9c240a9820fab15002592e5bb6db318543b/libbeat/kibana/fields_transformer.go */ -interface TypeMap { - [key: string]: string; -} -const typeMap: TypeMap = { - binary: 'binary', - half_float: 'number', - scaled_float: 'number', - float: 'number', - integer: 'number', - long: 'number', - short: 'number', - byte: 'number', - text: 'string', - keyword: 'string', - '': 'string', - geo_point: 'geo_point', - date: 'date', - ip: 'ip', - boolean: 'boolean', - constant_keyword: 'string', -}; - +import { getPackageSavedObjects } from '../../packages/get'; const INDEX_PATTERN_SAVED_OBJECT_TYPE = 'index-pattern'; export const indexPatternTypes = Object.values(dataTypes); -export async function installIndexPatterns({ - savedObjectsClient, - pkgName, - pkgVersion, - installSource, -}: { - savedObjectsClient: SavedObjectsClientContract; - esClient: ElasticsearchClient; - pkgName?: string; - pkgVersion?: string; - installSource?: InstallSource; -}) { - const logger = appContextService.getLogger(); - logger.debug( - `kicking off installation of index patterns for ${ - pkgName && pkgVersion ? `${pkgName}-${pkgVersion}` : 'no specific package' - }` - ); +export function getIndexPatternSavedObjects() { + return indexPatternTypes.map((indexPatternType) => ({ + id: `${indexPatternType}-*`, + type: INDEX_PATTERN_SAVED_OBJECT_TYPE, + attributes: { + title: `${indexPatternType}-*`, + timeFieldName: '@timestamp', + allowNoIndex: true, + }, + })); +} +export async function removeUnusedIndexPatterns(savedObjectsClient: SavedObjectsClientContract) { + const logger = appContextService.getLogger(); // get all user installed packages const installedPackagesRes = await getPackageSavedObjects(savedObjectsClient); const installedPackagesSavedObjects = installedPackagesRes.saved_objects.filter( (so) => so.attributes.install_status === installationStatuses.Installed ); - const packagesToFetch = installedPackagesSavedObjects.reduce< - Array<{ name: string; version: string; installedPkg: Installation | undefined }> - >((acc, pkg) => { - acc.push({ - name: pkg.attributes.name, - version: pkg.attributes.version, - installedPkg: pkg.attributes, - }); - return acc; - }, []); - - if (pkgName && pkgVersion && installSource) { - const packageToInstall = packagesToFetch.find((pkg) => pkg.name === pkgName); - if (packageToInstall) { - // set the version to the one we want to install - // if we're reinstalling the number will be the same - // if this is an upgrade then we'll be modifying the version number to the upgrade version - packageToInstall.version = pkgVersion; - } else { - // if we're installing for the first time, add to the list - packagesToFetch.push({ - name: pkgName, - version: pkgVersion, - installedPkg: await getInstallation({ savedObjectsClient, pkgName }), - }); - } + if (installedPackagesSavedObjects.length > 0) { + return []; } - // get each package's registry info - const packagesToFetchPromise = packagesToFetch.map((pkg) => - getPackageFromSource({ - pkgName: pkg.name, - pkgVersion: pkg.version, - installedPkg: pkg.installedPkg, - savedObjectsClient, - }) + const patternsToDelete = indexPatternTypes.map((indexPatternType) => `${indexPatternType}-*`); + + const { resolved_objects: resolvedObjects } = await savedObjectsClient.bulkResolve( + patternsToDelete.map((pattern) => ({ id: pattern, type: INDEX_PATTERN_SAVED_OBJECT_TYPE })) ); - const packages = await Promise.all(packagesToFetchPromise); - // for each index pattern type, create an index pattern + // eslint-disable-next-line @typescript-eslint/naming-convention + const idsToDelete = resolvedObjects.map(({ saved_object }) => saved_object.id); + return Promise.all( - indexPatternTypes.map(async (indexPatternType) => { - // if this is an update because a package is being uninstalled (no pkgkey argument passed) and no other packages are installed, remove the index pattern - if (!pkgName && installedPackagesSavedObjects.length === 0) { - try { - logger.debug(`deleting index pattern ${indexPatternType}-*`); - await savedObjectsClient.delete(INDEX_PATTERN_SAVED_OBJECT_TYPE, `${indexPatternType}-*`); - } catch (err) { - // index pattern was probably deleted by the user already - } - return; + idsToDelete.map(async (id) => { + try { + logger.debug(`deleting index pattern ${id}`); + await savedObjectsClient.delete(INDEX_PATTERN_SAVED_OBJECT_TYPE, id); + } catch (err) { + // index pattern was probably deleted by the user already + logger.debug(`Non fatal error encountered deleting index pattern ${id} : ${err}`); } - const packagesWithInfo = packages.map((pkg) => pkg.packageInfo); - // get all data stream fields from all installed packages - const fields = await getAllDataStreamFieldsByType(packagesWithInfo, indexPatternType); - const kibanaIndexPattern = createIndexPattern(indexPatternType, fields); - - // create or overwrite the index pattern - await savedObjectsClient.create(INDEX_PATTERN_SAVED_OBJECT_TYPE, kibanaIndexPattern, { - id: `${indexPatternType}-*`, - overwrite: true, - }); - logger.debug(`created index pattern ${kibanaIndexPattern.title}`); + return; }) ); } - -// loops through all given packages and returns an array -// of all fields from all data streams matching data stream type -export const getAllDataStreamFieldsByType = async ( - packages: Array, - dataStreamType: ValueOf -): Promise => { - const dataStreamsPromises = packages.reduce>>((acc, pkg) => { - if (pkg.data_streams) { - // filter out data streams by data stream type - const matchingDataStreams = pkg.data_streams.filter( - (dataStream) => dataStream.type === dataStreamType - ); - matchingDataStreams.forEach((dataStream) => { - acc.push(loadFieldsFromYaml(pkg, dataStream.path)); - }); - } - return acc; - }, []); - - // get all the data stream fields for each installed package into one array - const allDataStreamFields: Fields[] = await Promise.all(dataStreamsPromises); - return allDataStreamFields.flat(); -}; - -// creates or updates index pattern -export const createIndexPattern = (indexPatternType: string, fields: Fields) => { - const { indexPatternFields, fieldFormatMap } = createIndexPatternFields(fields); - - return { - title: `${indexPatternType}-*`, - timeFieldName: '@timestamp', - fields: JSON.stringify(indexPatternFields), - fieldFormatMap: JSON.stringify(fieldFormatMap), - allowNoIndex: true, - }; -}; - -// takes fields from yaml files and transforms into Kibana Index Pattern fields -// and also returns the fieldFormatMap -export const createIndexPatternFields = ( - fields: Fields -): { indexPatternFields: FieldSpec[]; fieldFormatMap: FieldFormatMap } => { - const flattenedFields = flattenFields(fields); - const fieldFormatMap = createFieldFormatMap(flattenedFields); - const transformedFields = flattenedFields.map(transformField); - const dedupedFields = dedupeFields(transformedFields); - return { indexPatternFields: dedupedFields, fieldFormatMap }; -}; - -// merges fields that are duplicates with the existing taking precedence -export const dedupeFields = (fields: FieldSpec[]) => { - const uniqueObj = fields.reduce<{ [name: string]: FieldSpec }>((acc, field) => { - // if field doesn't exist yet - if (!acc[field.name]) { - acc[field.name] = field; - // if field exists already - } else { - const existingField = acc[field.name]; - // if the existing field and this field have the same type, merge - if (existingField.type === field.type) { - const mergedField = { ...field, ...existingField }; - acc[field.name] = mergedField; - } else { - // log when there is a dup with different types - } - } - return acc; - }, {}); - - return Object.values(uniqueObj); -}; - -/** - * search through fields with field's path property - * returns undefined if field not found or field is not a leaf node - * @param allFields fields to search - * @param path dot separated path from field.path - */ -export const findFieldByPath = (allFields: Fields, path: string): Field | undefined => { - const pathParts = path.split('.'); - return getField(allFields, pathParts); -}; - -const getField = (fields: Fields, pathNames: string[]): Field | undefined => { - if (!pathNames.length) return undefined; - // get the first rest of path names - const [name, ...restPathNames] = pathNames; - for (const field of fields) { - if (field.name === name) { - // check field's fields, passing in the remaining path names - if (field.fields && field.fields.length > 0) { - return getField(field.fields, restPathNames); - } - // no nested fields to search, but still more names - not found - if (restPathNames.length) { - return undefined; - } - return field; - } - } - return undefined; -}; - -export const transformField = (field: Field, i: number, fields: Fields): FieldSpec => { - const newField: FieldSpec = { - name: field.name, - type: field.type && typeMap[field.type] ? typeMap[field.type] : 'string', - count: field.count ?? 0, - scripted: false, - indexed: field.index ?? true, - searchable: field.searchable ?? true, - aggregatable: field.aggregatable ?? true, - readFromDocValues: field.doc_values ?? true, - }; - - if (newField.type === 'binary') { - newField.aggregatable = false; - newField.readFromDocValues = field.doc_values ?? false; - newField.indexed = false; - newField.searchable = false; - } - - if (field.type === 'object' && field.hasOwnProperty('enabled')) { - const enabled = field.enabled ?? true; - if (!enabled) { - newField.aggregatable = false; - newField.readFromDocValues = false; - newField.indexed = false; - newField.searchable = false; - } - } - - if (field.type === 'text') { - newField.aggregatable = false; - } - - if (field.hasOwnProperty('script')) { - newField.scripted = true; - newField.script = field.script; - newField.lang = 'painless'; - newField.readFromDocValues = false; - } - - return newField; -}; - -/** - * flattenFields - * - * flattens fields and renames them with a path of the parent names - */ - -export const flattenFields = (allFields: Fields): Fields => { - const flatten = (fields: Fields): Fields => - fields.reduce((acc, field) => { - // if this is a group fields with no fields, skip the field - if (field.type === 'group' && !field.fields?.length) { - return acc; - } - // recurse through nested fields - if (field.type === 'group' && field.fields?.length) { - // skip if field.enabled is not explicitly set to false - if (!field.hasOwnProperty('enabled') || field.enabled === true) { - acc = renameAndFlatten(field, field.fields, [...acc]); - } - } else { - // handle alias type fields - if (field.type === 'alias' && field.path) { - const foundField = findFieldByPath(allFields, field.path); - // if aliased leaf field is found copy its props over except path and name - if (foundField) { - const { path, name } = field; - field = { ...foundField, path, name }; - } - } - // add field before going through multi_fields because we still want to add the parent field - acc.push(field); - - // for each field in multi_field add new field - if (field.multi_fields?.length) { - acc = renameAndFlatten(field, field.multi_fields, [...acc]); - } - } - return acc; - }, []); - - // helper function to call flatten() and rename the fields - const renameAndFlatten = (field: Field, fields: Fields, acc: Fields): Fields => { - const flattenedFields = flatten(fields); - flattenedFields.forEach((nestedField) => { - acc.push({ - ...nestedField, - name: `${field.name}.${nestedField.name}`, - }); - }); - return acc; - }; - - return flatten(allFields); -}; - -export const createFieldFormatMap = (fields: Fields): FieldFormatMap => - fields.reduce((acc, field) => { - if (field.format || field.pattern) { - const fieldFormatMapItem: FieldFormatMapItem = {}; - if (field.format) { - fieldFormatMapItem.id = field.format; - } - const params = getFieldFormatParams(field); - if (Object.keys(params).length) fieldFormatMapItem.params = params; - acc[field.name] = fieldFormatMapItem; - } - return acc; - }, {}); - -const getFieldFormatParams = (field: Field): FieldFormatParams => { - const params: FieldFormatParams = {}; - if (field.pattern) params.pattern = field.pattern; - if (field.input_format) params.inputFormat = field.input_format; - if (field.output_format) params.outputFormat = field.output_format; - if (field.output_precision) params.outputPrecision = field.output_precision; - if (field.label_template) params.labelTemplate = field.label_template; - if (field.url_template) params.urlTemplate = field.url_template; - if (field.open_link_in_current_tab) params.openLinkInCurrentTab = field.open_link_in_current_tab; - return params; -}; diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/coredns.logs.yml b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/coredns.logs.yml deleted file mode 100644 index d66a4cf62bc41..0000000000000 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/coredns.logs.yml +++ /dev/null @@ -1,71 +0,0 @@ -- name: coredns - type: group - description: > - coredns fields after normalization - fields: - - name: id - type: keyword - description: > - id of the DNS transaction - - - name: allParams - type: integer - format: bytes - pattern: patternValQueryWeight - input_format: inputFormatVal, - output_format: outputFormalVal, - output_precision: 3, - label_template: labelTemplateVal, - url_template: urlTemplateVal, - openLinkInCurrentTab: true, - description: > - weight of the DNS query - - - name: query.length - type: integer - pattern: patternValQueryLength - description: > - length of the DNS query - - - name: query.size - type: integer - format: bytes - pattern: patternValQuerySize - description: > - size of the DNS query - - - name: query.class - type: keyword - description: > - DNS query class - - - name: query.name - type: keyword - description: > - DNS query name - - - name: query.type - type: keyword - description: > - DNS query type - - - name: response.code - type: keyword - description: > - DNS response code - - - name: response.flags - type: keyword - description: > - DNS response flags - - - name: response.size - type: integer - format: bytes - description: > - size of the DNS response - - - name: dnssec_ok - type: boolean - description: > - dnssec flag diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/nginx.access.ecs.yml b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/nginx.access.ecs.yml deleted file mode 100644 index 51090a0fe7cf0..0000000000000 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/nginx.access.ecs.yml +++ /dev/null @@ -1,112 +0,0 @@ -- name: '@timestamp' - level: core - required: true - type: date - description: 'Date/time when the event originated. - This is the date/time extracted from the event, typically representing when - the event was generated by the source. - If the event source has no original timestamp, this value is typically populated - by the first time the event was received by the pipeline. - Required field for all events.' - example: '2016-05-23T08:05:34.853Z' -- name: labels - level: core - type: object - object_type: keyword - description: 'Custom key/value pairs. - Can be used to add meta information to events. Should not contain nested objects. - All values are stored as keyword. - Example: `docker` and `k8s` labels.' - example: - application: foo-bar - env: production -- name: message - level: core - type: text - description: 'For log events the message field contains the log message, optimized - for viewing in a log viewer. - For structured logs without an original message field, other fields can be concatenated - to form a human-readable summary of the event. - If multiple messages exist, they can be combined into one message.' - example: Hello World -- name: tags - level: core - type: keyword - ignore_above: 1024 - description: List of keywords used to tag each event. - example: '["production", "env2"]' -- name: agent - title: Agent - group: 2 - description: 'The agent fields contain the data about the software entity, if - any, that collects, detects, or observes events on a host, or takes measurements - on a host. - Examples include Beats. Agents may also run on observers. ECS agent.* fields - shall be populated with details of the agent running on the host or observer - where the event happened or the measurement was taken.' - footnote: 'Examples: In the case of Beats for logs, the agent.name is filebeat. - For APM, it is the agent running in the app/service. The agent information does - not change if data is sent through queuing systems like Kafka, Redis, or processing - systems such as Logstash or APM Server.' - type: group - fields: - - name: ephemeral_id - level: extended - type: keyword - ignore_above: 1024 - description: 'Ephemeral identifier of this agent (if one exists). - This id normally changes across restarts, but `agent.id` does not.' - example: 8a4f500f - - name: id - level: core - type: keyword - ignore_above: 1024 - description: 'Unique identifier of this agent (if one exists). - Example: For Beats this would be beat.id.' - example: 8a4f500d - - name: name - level: core - type: keyword - ignore_above: 1024 - description: 'Custom name of the agent. - This is a name that can be given to an agent. This can be helpful if for example - two Filebeat instances are running on the same host but a human readable separation - is needed on which Filebeat instance data is coming from. - If no name is given, the name is often left empty.' - example: foo - - name: type - level: core - type: keyword - ignore_above: 1024 - description: 'Type of the agent. - The agent type stays always the same and should be given by the agent used. - In case of Filebeat the agent would always be Filebeat also if two Filebeat - instances are run on the same machine.' - example: filebeat - - name: version - level: core - type: keyword - ignore_above: 1024 - description: Version of the agent. - example: 6.0.0-rc2 -- name: as - title: Autonomous System - group: 2 - description: An autonomous system (AS) is a collection of connected Internet Protocol - (IP) routing prefixes under the control of one or more network operators on - behalf of a single administrative entity or domain that presents a common, clearly - defined routing policy to the internet. - type: group - fields: - - name: number - level: extended - type: long - description: Unique number allocated to the autonomous system. The autonomous - system number (ASN) uniquely identifies each network on the Internet. - example: 15169 - - name: organization.name - level: extended - type: keyword - ignore_above: 1024 - description: Organization name. - example: Google LLC \ No newline at end of file diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/nginx.error.ecs.yml b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/nginx.error.ecs.yml deleted file mode 100644 index 51090a0fe7cf0..0000000000000 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/nginx.error.ecs.yml +++ /dev/null @@ -1,112 +0,0 @@ -- name: '@timestamp' - level: core - required: true - type: date - description: 'Date/time when the event originated. - This is the date/time extracted from the event, typically representing when - the event was generated by the source. - If the event source has no original timestamp, this value is typically populated - by the first time the event was received by the pipeline. - Required field for all events.' - example: '2016-05-23T08:05:34.853Z' -- name: labels - level: core - type: object - object_type: keyword - description: 'Custom key/value pairs. - Can be used to add meta information to events. Should not contain nested objects. - All values are stored as keyword. - Example: `docker` and `k8s` labels.' - example: - application: foo-bar - env: production -- name: message - level: core - type: text - description: 'For log events the message field contains the log message, optimized - for viewing in a log viewer. - For structured logs without an original message field, other fields can be concatenated - to form a human-readable summary of the event. - If multiple messages exist, they can be combined into one message.' - example: Hello World -- name: tags - level: core - type: keyword - ignore_above: 1024 - description: List of keywords used to tag each event. - example: '["production", "env2"]' -- name: agent - title: Agent - group: 2 - description: 'The agent fields contain the data about the software entity, if - any, that collects, detects, or observes events on a host, or takes measurements - on a host. - Examples include Beats. Agents may also run on observers. ECS agent.* fields - shall be populated with details of the agent running on the host or observer - where the event happened or the measurement was taken.' - footnote: 'Examples: In the case of Beats for logs, the agent.name is filebeat. - For APM, it is the agent running in the app/service. The agent information does - not change if data is sent through queuing systems like Kafka, Redis, or processing - systems such as Logstash or APM Server.' - type: group - fields: - - name: ephemeral_id - level: extended - type: keyword - ignore_above: 1024 - description: 'Ephemeral identifier of this agent (if one exists). - This id normally changes across restarts, but `agent.id` does not.' - example: 8a4f500f - - name: id - level: core - type: keyword - ignore_above: 1024 - description: 'Unique identifier of this agent (if one exists). - Example: For Beats this would be beat.id.' - example: 8a4f500d - - name: name - level: core - type: keyword - ignore_above: 1024 - description: 'Custom name of the agent. - This is a name that can be given to an agent. This can be helpful if for example - two Filebeat instances are running on the same host but a human readable separation - is needed on which Filebeat instance data is coming from. - If no name is given, the name is often left empty.' - example: foo - - name: type - level: core - type: keyword - ignore_above: 1024 - description: 'Type of the agent. - The agent type stays always the same and should be given by the agent used. - In case of Filebeat the agent would always be Filebeat also if two Filebeat - instances are run on the same machine.' - example: filebeat - - name: version - level: core - type: keyword - ignore_above: 1024 - description: Version of the agent. - example: 6.0.0-rc2 -- name: as - title: Autonomous System - group: 2 - description: An autonomous system (AS) is a collection of connected Internet Protocol - (IP) routing prefixes under the control of one or more network operators on - behalf of a single administrative entity or domain that presents a common, clearly - defined routing policy to the internet. - type: group - fields: - - name: number - level: extended - type: long - description: Unique number allocated to the autonomous system. The autonomous - system number (ASN) uniquely identifies each network on the Internet. - example: 15169 - - name: organization.name - level: extended - type: keyword - ignore_above: 1024 - description: Organization name. - example: Google LLC \ No newline at end of file diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/nginx.fields.yml b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/nginx.fields.yml deleted file mode 100644 index 7c2e721d564e7..0000000000000 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/nginx.fields.yml +++ /dev/null @@ -1,120 +0,0 @@ -- name: nginx.access - type: group - description: > - Contains fields for the Nginx access logs. - fields: - - name: group_disabled - type: group - enabled: false - fields: - - name: message - type: text - - name: remote_ip_list - type: array - description: > - An array of remote IP addresses. It is a list because it is common to include, besides the client - IP address, IP addresses from headers like `X-Forwarded-For`. - Real source IP is restored to `source.ip`. - - - name: body_sent.bytes - type: alias - path: http.response.body.bytes - migration: true - - name: user_name - type: alias - path: user.name - migration: true - - name: method - type: alias - path: http.request.method - migration: true - - name: url - type: alias - path: url.original - migration: true - - name: http_version - type: alias - path: http.version - migration: true - - name: response_code - type: alias - path: http.response.status_code - migration: true - - name: referrer - type: alias - path: http.request.referrer - migration: true - - name: agent - type: alias - path: user_agent.original - migration: true - - - name: user_agent - type: group - fields: - - name: device - type: alias - path: user_agent.device.name - migration: true - - name: name - type: alias - path: user_agent.name - migration: true - - name: os - type: alias - path: user_agent.os.full_name - migration: true - - name: os_name - type: alias - path: user_agent.os.name - migration: true - - name: original - type: alias - path: user_agent.original - migration: true - - - name: geoip - type: group - fields: - - name: continent_name - type: alias - path: source.geo.continent_name - migration: true - - name: country_iso_code - type: alias - path: source.geo.country_iso_code - migration: true - - name: location - type: alias - path: source.geo.location - migration: true - - name: region_name - type: alias - path: source.geo.region_name - migration: true - - name: city_name - type: alias - path: source.geo.city_name - migration: true - - name: region_iso_code - type: alias - path: source.geo.region_iso_code - migration: true - -- name: source - type: group - fields: - - name: geo - type: group - fields: - - name: continent_name - type: text -- name: country - type: "" - multi_fields: - - name: keyword - type: keyword - - name: text - type: text -- name: nginx - type: group \ No newline at end of file diff --git a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/test_data.ts b/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/test_data.ts deleted file mode 100644 index d9bcf36651081..0000000000000 --- a/x-pack/plugins/fleet/server/services/epm/kibana/index_pattern/tests/test_data.ts +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { FieldSpec } from 'src/plugins/data/common'; - -export const dupeFields: FieldSpec[] = [ - { - name: '1', - type: 'integer', - searchable: true, - aggregatable: true, - count: 0, - indexed: true, - readFromDocValues: true, - scripted: false, - }, - { - name: '2', - type: 'integer', - searchable: true, - aggregatable: true, - count: 0, - indexed: true, - readFromDocValues: true, - scripted: false, - }, - { - name: '3', - type: 'integer', - searchable: true, - aggregatable: true, - count: 0, - indexed: true, - readFromDocValues: true, - scripted: false, - }, - { - name: '1', - type: 'integer', - searchable: false, - aggregatable: false, - count: 2, - indexed: true, - readFromDocValues: true, - scripted: false, - }, - { - name: '1.1', - type: 'integer', - searchable: false, - aggregatable: false, - count: 0, - indexed: true, - readFromDocValues: true, - scripted: false, - }, - { - name: '4', - type: 'integer', - searchable: false, - aggregatable: false, - count: 0, - indexed: true, - readFromDocValues: true, - scripted: false, - }, - { - name: '2', - type: 'integer', - searchable: false, - aggregatable: false, - count: 0, - indexed: true, - readFromDocValues: true, - scripted: false, - }, - { - name: '1', - type: 'integer', - searchable: false, - aggregatable: false, - count: 1, - indexed: true, - readFromDocValues: true, - scripted: false, - }, -]; diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts index 5ee0f57b6e03a..dbec18851cfc9 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.test.ts @@ -20,7 +20,6 @@ jest.mock('./get'); import { updateCurrentWriteIndices } from '../elasticsearch/template/template'; import { installKibanaAssets } from '../kibana/assets/install'; -import { installIndexPatterns } from '../kibana/index_pattern/install'; import { _installPackage } from './_install_package'; @@ -30,9 +29,6 @@ const mockedUpdateCurrentWriteIndices = updateCurrentWriteIndices as jest.Mocked const mockedGetKibanaAssets = installKibanaAssets as jest.MockedFunction< typeof installKibanaAssets >; -const mockedInstallIndexPatterns = installIndexPatterns as jest.MockedFunction< - typeof installIndexPatterns ->; function sleep(millis: number) { return new Promise((resolve) => setTimeout(resolve, millis)); @@ -50,14 +46,11 @@ describe('_installPackage', () => { afterEach(async () => { appContextService.stop(); }); - it('handles errors from installIndexPatterns or installKibanaAssets', async () => { - // force errors from either/both these functions + it('handles errors from installKibanaAssets', async () => { + // force errors from this function mockedGetKibanaAssets.mockImplementation(async () => { throw new Error('mocked async error A: should be caught'); }); - mockedInstallIndexPatterns.mockImplementation(async () => { - throw new Error('mocked async error B: should be caught'); - }); // pick any function between when those are called and when await Promise.all is defined later // and force it to take long enough for the errors to occur @@ -66,6 +59,8 @@ describe('_installPackage', () => { const installationPromise = _installPackage({ savedObjectsClient: soClient, + // @ts-ignore + savedObjectsImporter: jest.fn(), esClient, logger: loggerMock.create(), paths: [], diff --git a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts index e2027a99463fc..ac0c7e1729913 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/_install_package.ts @@ -10,6 +10,7 @@ import type { Logger, SavedObject, SavedObjectsClientContract, + SavedObjectsImporter, } from 'src/core/server'; import { @@ -36,7 +37,6 @@ import { installMlModel } from '../elasticsearch/ml_model/'; import { installIlmForDataStream } from '../elasticsearch/datastream_ilm/install'; import { saveArchiveEntries } from '../archive/storage'; import { ConcurrentInstallOperationError } from '../../../errors'; - import { packagePolicyService } from '../..'; import { createInstallation, saveKibanaAssetsRefs, updateVersion } from './install'; @@ -48,6 +48,7 @@ import { deleteKibanaSavedObjectsAssets } from './remove'; export async function _installPackage({ savedObjectsClient, + savedObjectsImporter, esClient, logger, installedPkg, @@ -57,6 +58,7 @@ export async function _installPackage({ installSource, }: { savedObjectsClient: SavedObjectsClientContract; + savedObjectsImporter: Pick; esClient: ElasticsearchClient; logger: Logger; installedPkg?: SavedObject; @@ -100,21 +102,6 @@ export async function _installPackage({ }); } - // kick off `installKibanaAssets` as early as possible because they're the longest running operations - // we don't `await` here because we don't want to delay starting the many other `install*` functions - // however, without an `await` or a `.catch` we haven't defined how to handle a promise rejection - // we define it many lines and potentially seconds of wall clock time later in - // `await installKibanaAssetsPromise` - // if we encounter an error before we there, we'll have an "unhandled rejection" which causes its own problems - // the program will log something like this _and exit/crash_ - // Unhandled Promise rejection detected: - // RegistryResponseError or some other error - // Terminating process... - // server crashed with status code 1 - // - // add a `.catch` to prevent the "unhandled rejection" case - // in that `.catch`, set something that indicates a failure - // check for that failure later and act accordingly (throw, ignore, return) const kibanaAssets = await getKibanaAssets(paths); if (installedPkg) await deleteKibanaSavedObjectsAssets( @@ -127,12 +114,13 @@ export async function _installPackage({ pkgName, kibanaAssets ); - let installKibanaAssetsError; - const installKibanaAssetsPromise = installKibanaAssets({ - savedObjectsClient, + + await installKibanaAssets({ + logger, + savedObjectsImporter, pkgName, kibanaAssets, - }).catch((reason) => (installKibanaAssetsError = reason)); + }); // the rest of the installation must happen in sequential order // currently only the base package has an ILM policy @@ -211,10 +199,6 @@ export async function _installPackage({ } const installedTemplateRefs = getAllTemplateRefs(installedTemplates); - // make sure the assets are installed (or didn't error) - if (installKibanaAssetsError) throw installKibanaAssetsError; - await installKibanaAssetsPromise; - const packageAssetResults = await saveArchiveEntries({ savedObjectsClient, paths, diff --git a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts index c77e2a0a22a0a..8a7fb9ae005d0 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/bulk_install_packages.ts @@ -9,7 +9,6 @@ import type { ElasticsearchClient, SavedObjectsClientContract } from 'src/core/s import { appContextService } from '../../app_context'; import * as Registry from '../registry'; -import { installIndexPatterns } from '../kibana/index_pattern/install'; import type { InstallResult } from '../../../types'; @@ -71,7 +70,6 @@ export async function bulkInstallPackages({ esClient, pkgkey: Registry.pkgToPkgKey(pkgKeyProps), installSource, - skipPostInstall: true, force, }); if (installResult.error) { @@ -92,19 +90,6 @@ export async function bulkInstallPackages({ }) ); - // only install index patterns if we completed install for any package-version for the - // first time, aka fresh installs or upgrades - if ( - bulkInstallResults.find( - (result) => - result.status === 'fulfilled' && - !result.value.result?.error && - result.value.result?.status === 'installed' - ) - ) { - await installIndexPatterns({ savedObjectsClient, esClient, installSource }); - } - return bulkInstallResults.map((result, index) => { const packageName = getNameFromPackagesToInstall(packagesToInstall, index); if (result.status === 'fulfilled') { diff --git a/x-pack/plugins/fleet/server/services/epm/packages/index.ts b/x-pack/plugins/fleet/server/services/epm/packages/index.ts index a6970a8d19db4..feee4277ab0e1 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/index.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/index.ts @@ -7,7 +7,11 @@ import type { SavedObject } from 'src/core/server'; -import { unremovablePackages, installationStatuses } from '../../../../common'; +import { + unremovablePackages, + installationStatuses, + KibanaSavedObjectType, +} from '../../../../common'; import { KibanaAssetType } from '../../../types'; import type { AssetType, Installable, Installation } from '../../../types'; @@ -40,7 +44,7 @@ export class PackageNotInstalledError extends Error { // only Kibana Assets use Saved Objects at this point export const savedObjectTypes: AssetType[] = Object.values(KibanaAssetType); - +export const kibanaSavedObjectTypes: KibanaSavedObjectType[] = Object.values(KibanaSavedObjectType); export function createInstallableFrom( from: T, savedObject?: SavedObject diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts index a9bb235c22cb8..261a0d9a6d688 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.test.ts @@ -26,6 +26,9 @@ jest.mock('../../app_context', () => { return { error: jest.fn(), debug: jest.fn(), warn: jest.fn() }; }), getTelemetryEventsSender: jest.fn(), + getSavedObjects: jest.fn(() => ({ + createImporter: jest.fn(), + })), }, }; }); diff --git a/x-pack/plugins/fleet/server/services/epm/packages/install.ts b/x-pack/plugins/fleet/server/services/epm/packages/install.ts index 330fd84e789b8..a580248b43731 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/install.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/install.ts @@ -39,7 +39,6 @@ import * as Registry from '../registry'; import { setPackageInfo, parseAndVerifyArchiveEntries, unpackBufferToCache } from '../archive'; import { toAssetReference } from '../kibana/assets/install'; import type { ArchiveAsset } from '../kibana/assets/install'; -import { installIndexPatterns } from '../kibana/index_pattern/install'; import type { PackageUpdateEvent } from '../../upgrade_sender'; import { sendTelemetryEvents, UpdateEventType } from '../../upgrade_sender'; @@ -303,10 +302,15 @@ async function installPackageFromRegistry({ return { error: err, installType }; } + const savedObjectsImporter = appContextService + .getSavedObjects() + .createImporter(savedObjectsClient); + // try installing the package, if there was an error, call error handler and rethrow // @ts-expect-error status is string instead of InstallResult.status 'installed' | 'already_installed' return _installPackage({ savedObjectsClient, + savedObjectsImporter, esClient, logger, installedPkg, @@ -407,9 +411,15 @@ async function installPackageByUpload({ version: packageInfo.version, packageInfo, }); + + const savedObjectsImporter = appContextService + .getSavedObjects() + .createImporter(savedObjectsClient); + // @ts-expect-error status is string instead of InstallResult.status 'installed' | 'already_installed' return _installPackage({ savedObjectsClient, + savedObjectsImporter, esClient, logger, installedPkg, @@ -441,41 +451,25 @@ async function installPackageByUpload({ } } -export type InstallPackageParams = { - skipPostInstall?: boolean; -} & ( +export type InstallPackageParams = | ({ installSource: Extract } & InstallRegistryPackageParams) - | ({ installSource: Extract } & InstallUploadedArchiveParams) -); + | ({ installSource: Extract } & InstallUploadedArchiveParams); export async function installPackage(args: InstallPackageParams) { if (!('installSource' in args)) { throw new Error('installSource is required'); } const logger = appContextService.getLogger(); - const { savedObjectsClient, esClient, skipPostInstall = false, installSource } = args; + const { savedObjectsClient, esClient } = args; if (args.installSource === 'registry') { const { pkgkey, force } = args; - const { pkgName, pkgVersion } = Registry.splitPkgKey(pkgkey); logger.debug(`kicking off install of ${pkgkey} from registry`); const response = installPackageFromRegistry({ savedObjectsClient, pkgkey, esClient, force, - }).then(async (installResult) => { - if (skipPostInstall || installResult.error) { - return installResult; - } - logger.debug(`install of ${pkgkey} finished, running post-install`); - return installIndexPatterns({ - savedObjectsClient, - esClient, - pkgName, - pkgVersion, - installSource, - }).then(() => installResult); }); return response; } else if (args.installSource === 'upload') { @@ -486,16 +480,6 @@ export async function installPackage(args: InstallPackageParams) { esClient, archiveBuffer, contentType, - }).then(async (installResult) => { - if (skipPostInstall || installResult.error) { - return installResult; - } - logger.debug(`install of uploaded package finished, running post-install`); - return installIndexPatterns({ - savedObjectsClient, - esClient, - installSource, - }).then(() => installResult); }); return response; } diff --git a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts index cd85eecbf1e78..957dac8c1aacb 100644 --- a/x-pack/plugins/fleet/server/services/epm/packages/remove.ts +++ b/x-pack/plugins/fleet/server/services/epm/packages/remove.ts @@ -18,7 +18,7 @@ import type { Installation, } from '../../../types'; import { deletePipeline } from '../elasticsearch/ingest_pipeline/'; -import { installIndexPatterns } from '../kibana/index_pattern/install'; +import { removeUnusedIndexPatterns } from '../kibana/index_pattern/install'; import { deleteTransforms } from '../elasticsearch/transform/remove'; import { deleteMlModel } from '../elasticsearch/ml_model'; import { packagePolicyService, appContextService } from '../..'; @@ -27,7 +27,7 @@ import { deletePackageCache } from '../archive'; import { deleteIlms } from '../elasticsearch/datastream_ilm/remove'; import { removeArchiveEntries } from '../archive/storage'; -import { getInstallation, savedObjectTypes } from './index'; +import { getInstallation, kibanaSavedObjectTypes } from './index'; export async function removeInstallation(options: { savedObjectsClient: SavedObjectsClientContract; @@ -62,10 +62,10 @@ export async function removeInstallation(options: { // could also update with [] or some other state await savedObjectsClient.delete(PACKAGES_SAVED_OBJECT_TYPE, pkgName); - // recreate or delete index patterns when a package is uninstalled + // delete the index patterns if no packages are installed // this must be done after deleting the saved object for the current package otherwise it will retrieve the package - // from the registry again and reinstall the index patterns - await installIndexPatterns({ savedObjectsClient, esClient }); + // from the registry again and keep the index patterns + await removeUnusedIndexPatterns(savedObjectsClient); // remove the package archive and its contents from the cache so that a reinstall fetches // a fresh copy from the registry @@ -80,14 +80,26 @@ export async function removeInstallation(options: { return installedAssets; } -// TODO: this is very much like deleteKibanaSavedObjectsAssets below -function deleteKibanaAssets( +async function deleteKibanaAssets( installedObjects: KibanaAssetReference[], savedObjectsClient: SavedObjectsClientContract ) { - return installedObjects.map(async ({ id, type }) => { + const { resolved_objects: resolvedObjects } = await savedObjectsClient.bulkResolve( + installedObjects + ); + + const foundObjects = resolvedObjects.filter( + ({ saved_object: savedObject }) => savedObject?.error?.statusCode !== 404 + ); + + // in the case of a partial install, it is expected that some assets will be not found + // we filter these out before calling delete + const assetsToDelete = foundObjects.map(({ saved_object: { id, type } }) => ({ id, type })); + const promises = assetsToDelete.map(async ({ id, type }) => { return savedObjectsClient.delete(type, id); }); + + return Promise.all(promises); } function deleteESAssets( @@ -145,7 +157,7 @@ async function deleteAssets( // then the other asset types await Promise.all([ ...deleteESAssets(otherAssets, esClient), - ...deleteKibanaAssets(installedKibana, savedObjectsClient), + deleteKibanaAssets(installedKibana, savedObjectsClient), ]); } catch (err) { // in the rollback case, partial installs are likely, so missing assets are not an error @@ -177,23 +189,19 @@ async function deleteComponentTemplate(esClient: ElasticsearchClient, name: stri } } -// TODO: this is very much like deleteKibanaAssets above export async function deleteKibanaSavedObjectsAssets( savedObjectsClient: SavedObjectsClientContract, - installedRefs: AssetReference[] + installedRefs: KibanaAssetReference[] ) { if (!installedRefs.length) return; const logger = appContextService.getLogger(); - const deletePromises = installedRefs.map(({ id, type }) => { - const assetType = type as AssetType; + const assetsToDelete = installedRefs + .filter(({ type }) => kibanaSavedObjectTypes.includes(type)) + .map(({ id, type }) => ({ id, type })); - if (savedObjectTypes.includes(assetType)) { - return savedObjectsClient.delete(assetType, id); - } - }); try { - await Promise.all(deletePromises); + await deleteKibanaAssets(assetsToDelete, savedObjectsClient); } catch (err) { // in the rollback case, partial installs are likely, so missing assets are not an error if (!savedObjectsClient.errors.isNotFoundError(err)) { diff --git a/x-pack/plugins/fleet/server/services/package_policy.test.ts b/x-pack/plugins/fleet/server/services/package_policy.test.ts index 78a4d3d1a778d..2e69cd03242f9 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.test.ts @@ -33,8 +33,10 @@ import type { InputsOverride, NewPackagePolicy, NewPackagePolicyInput, + PackagePolicyPackage, RegistryPackage, } from '../../common'; +import { packageToPackagePolicy } from '../../common'; import { IngestManagerError } from '../errors'; @@ -107,6 +109,11 @@ jest.mock('./epm/packages', () => { }; }); +jest.mock('../../common', () => ({ + ...jest.requireActual('../../common'), + packageToPackagePolicy: jest.fn(), +})); + jest.mock('./epm/registry'); jest.mock('./agent_policy', () => { @@ -125,6 +132,7 @@ jest.mock('./agent_policy', () => { return agentPolicy; }, bumpRevision: () => {}, + getDefaultAgentPolicyId: () => Promise.resolve('1'), }, }; }); @@ -2815,6 +2823,216 @@ describe('Package policy service', () => { }); }); }); + + describe('enrich package policy on create', () => { + beforeEach(() => { + (packageToPackagePolicy as jest.Mock).mockReturnValue({ + package: { name: 'apache', title: 'Apache', version: '1.0.0' }, + inputs: [ + { + type: 'logfile', + policy_template: 'log', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { + type: 'logs', + dataset: 'apache.access', + }, + }, + ], + }, + ], + vars: { + paths: { + value: ['/var/log/apache2/access.log*'], + type: 'text', + }, + }, + }); + }); + + it('should enrich from epm with defaults', async () => { + const newPolicy = { + name: 'apache-1', + inputs: [{ type: 'logfile', enabled: false }], + package: { name: 'apache', version: '0.3.3' }, + } as NewPackagePolicy; + const result = await packagePolicyService.enrichPolicyWithDefaultsFromPackage( + savedObjectsClientMock.create(), + newPolicy + ); + expect(result).toEqual({ + name: 'apache-1', + namespace: 'default', + description: '', + package: { name: 'apache', title: 'Apache', version: '1.0.0' }, + enabled: true, + policy_id: '1', + output_id: '', + inputs: [ + { + enabled: false, + type: 'logfile', + policy_template: 'log', + streams: [ + { + enabled: false, + data_stream: { + type: 'logs', + dataset: 'apache.access', + }, + }, + ], + }, + ], + vars: { + paths: { + value: ['/var/log/apache2/access.log*'], + type: 'text', + }, + }, + }); + }); + + it('should enrich from epm with defaults using policy template', async () => { + (packageToPackagePolicy as jest.Mock).mockReturnValueOnce({ + package: { name: 'aws', title: 'AWS', version: '1.0.0' }, + inputs: [ + { + type: 'aws/metrics', + policy_template: 'cloudtrail', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { + type: 'metrics', + dataset: 'cloudtrail', + }, + }, + ], + }, + { + type: 'aws/metrics', + policy_template: 'cloudwatch', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { + type: 'metrics', + dataset: 'cloudwatch', + }, + }, + ], + }, + ], + }); + const newPolicy = { + name: 'aws-1', + inputs: [{ type: 'aws/metrics', policy_template: 'cloudwatch', enabled: true }], + package: { name: 'aws', version: '1.0.0' }, + } as NewPackagePolicy; + const result = await packagePolicyService.enrichPolicyWithDefaultsFromPackage( + savedObjectsClientMock.create(), + newPolicy + ); + expect(result).toEqual({ + name: 'aws-1', + namespace: 'default', + description: '', + package: { name: 'aws', title: 'AWS', version: '1.0.0' }, + enabled: true, + policy_id: '1', + output_id: '', + inputs: [ + { + type: 'aws/metrics', + policy_template: 'cloudwatch', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { + type: 'metrics', + dataset: 'cloudwatch', + }, + }, + ], + }, + ], + }); + }); + + it('should override defaults with new values', async () => { + const newPolicy = { + name: 'apache-2', + namespace: 'namespace', + description: 'desc', + enabled: false, + policy_id: '2', + output_id: '3', + inputs: [ + { + type: 'logfile', + enabled: true, + streams: [ + { + enabled: true, + data_stream: { + type: 'logs', + dataset: 'apache.error', + }, + }, + ], + }, + ], + vars: { + paths: { + value: ['/my/access.log*'], + type: 'text', + }, + }, + package: { name: 'apache', version: '1.0.0' } as PackagePolicyPackage, + } as NewPackagePolicy; + const result = await packagePolicyService.enrichPolicyWithDefaultsFromPackage( + savedObjectsClientMock.create(), + newPolicy + ); + expect(result).toEqual({ + name: 'apache-2', + namespace: 'namespace', + description: 'desc', + package: { name: 'apache', title: 'Apache', version: '1.0.0' }, + enabled: false, + policy_id: '2', + output_id: '3', + inputs: [ + { + enabled: true, + type: 'logfile', + streams: [ + { + enabled: true, + data_stream: { + type: 'logs', + dataset: 'apache.error', + }, + }, + ], + }, + ], + vars: { + paths: { + value: ['/my/access.log*'], + type: 'text', + }, + }, + }); + }); + }); }); describe('_applyIndexPrivileges()', () => { diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index cef27d79a184a..6ebfb84ebb523 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -710,6 +710,67 @@ class PackagePolicyService { } } + public async enrichPolicyWithDefaultsFromPackage( + soClient: SavedObjectsClientContract, + newPolicy: NewPackagePolicy + ): Promise { + let newPackagePolicy: NewPackagePolicy = newPolicy; + if (newPolicy.package) { + const newPP = await this.buildPackagePolicyFromPackageWithVersion( + soClient, + newPolicy.package.name, + newPolicy.package.version + ); + if (newPP) { + const inputs = newPolicy.inputs.map((input) => { + const defaultInput = newPP.inputs.find( + (i) => + i.type === input.type && + (!input.policy_template || input.policy_template === i.policy_template) + ); + return { + ...defaultInput, + enabled: input.enabled, + type: input.type, + // to propagate "enabled: false" to streams + streams: defaultInput?.streams?.map((stream) => ({ + ...stream, + enabled: input.enabled, + })), + } as NewPackagePolicyInput; + }); + newPackagePolicy = { + ...newPP, + name: newPolicy.name, + namespace: newPolicy.namespace ?? 'default', + description: newPolicy.description ?? '', + enabled: newPolicy.enabled ?? true, + policy_id: + newPolicy.policy_id ?? (await agentPolicyService.getDefaultAgentPolicyId(soClient)), + output_id: newPolicy.output_id ?? '', + inputs: newPolicy.inputs[0]?.streams ? newPolicy.inputs : inputs, + vars: newPolicy.vars || newPP.vars, + }; + } + } + return newPackagePolicy; + } + + public async buildPackagePolicyFromPackageWithVersion( + soClient: SavedObjectsClientContract, + pkgName: string, + pkgVersion: string + ): Promise { + const packageInfo = await getPackageInfo({ + savedObjectsClient: soClient, + pkgName, + pkgVersion, + }); + if (packageInfo) { + return packageToPackagePolicy(packageInfo, '', ''); + } + } + public async buildPackagePolicyFromPackage( soClient: SavedObjectsClientContract, pkgName: string diff --git a/x-pack/plugins/fleet/server/types/models/package_policy.ts b/x-pack/plugins/fleet/server/types/models/package_policy.ts index 30321bdca3309..904e4e18a8541 100644 --- a/x-pack/plugins/fleet/server/types/models/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/models/package_policy.ts @@ -28,6 +28,53 @@ const ConfigRecordSchema = schema.recordOf( }) ); +const PackagePolicyStreamsSchema = { + id: schema.maybe(schema.string()), // BWC < 7.11 + enabled: schema.boolean(), + keep_enabled: schema.maybe(schema.boolean()), + data_stream: schema.object({ + dataset: schema.string(), + type: schema.string(), + elasticsearch: schema.maybe( + schema.object({ + privileges: schema.maybe( + schema.object({ + indices: schema.maybe(schema.arrayOf(schema.string())), + }) + ), + }) + ), + }), + vars: schema.maybe(ConfigRecordSchema), + config: schema.maybe( + schema.recordOf( + schema.string(), + schema.object({ + type: schema.maybe(schema.string()), + value: schema.maybe(schema.any()), + }) + ) + ), +}; + +const PackagePolicyInputsSchema = { + type: schema.string(), + policy_template: schema.maybe(schema.string()), + enabled: schema.boolean(), + keep_enabled: schema.maybe(schema.boolean()), + vars: schema.maybe(ConfigRecordSchema), + config: schema.maybe( + schema.recordOf( + schema.string(), + schema.object({ + type: schema.maybe(schema.string()), + value: schema.maybe(schema.any()), + }) + ) + ), + streams: schema.arrayOf(schema.object(PackagePolicyStreamsSchema)), +}; + const PackagePolicyBaseSchema = { name: schema.string(), description: schema.maybe(schema.string()), @@ -42,63 +89,57 @@ const PackagePolicyBaseSchema = { }) ), output_id: schema.string(), + inputs: schema.arrayOf(schema.object(PackagePolicyInputsSchema)), + vars: schema.maybe(ConfigRecordSchema), +}; + +export const NewPackagePolicySchema = schema.object({ + ...PackagePolicyBaseSchema, + id: schema.maybe(schema.string()), + force: schema.maybe(schema.boolean()), +}); + +const CreatePackagePolicyProps = { + ...PackagePolicyBaseSchema, + namespace: schema.maybe(NamespaceSchema), + policy_id: schema.maybe(schema.string()), + enabled: schema.maybe(schema.boolean()), + package: schema.maybe( + schema.object({ + name: schema.string(), + title: schema.maybe(schema.string()), + version: schema.string(), + }) + ), + output_id: schema.maybe(schema.string()), inputs: schema.arrayOf( schema.object({ - type: schema.string(), - policy_template: schema.maybe(schema.string()), - enabled: schema.boolean(), - keep_enabled: schema.maybe(schema.boolean()), - vars: schema.maybe(ConfigRecordSchema), - config: schema.maybe( - schema.recordOf( - schema.string(), - schema.object({ - type: schema.maybe(schema.string()), - value: schema.maybe(schema.any()), - }) - ) - ), - streams: schema.arrayOf( - schema.object({ - id: schema.maybe(schema.string()), // BWC < 7.11 - enabled: schema.boolean(), - keep_enabled: schema.maybe(schema.boolean()), - data_stream: schema.object({ - dataset: schema.string(), - type: schema.string(), - elasticsearch: schema.maybe( - schema.object({ - privileges: schema.maybe( - schema.object({ - indices: schema.maybe(schema.arrayOf(schema.string())), - }) - ), - }) - ), - }), - vars: schema.maybe(ConfigRecordSchema), - config: schema.maybe( - schema.recordOf( - schema.string(), - schema.object({ - type: schema.maybe(schema.string()), - value: schema.maybe(schema.any()), - }) - ) - ), - }) - ), + ...PackagePolicyInputsSchema, + streams: schema.maybe(schema.arrayOf(schema.object(PackagePolicyStreamsSchema))), }) ), - vars: schema.maybe(ConfigRecordSchema), }; -export const NewPackagePolicySchema = schema.object({ - ...PackagePolicyBaseSchema, +export const CreatePackagePolicyRequestBodySchema = schema.object({ + ...CreatePackagePolicyProps, id: schema.maybe(schema.string()), force: schema.maybe(schema.boolean()), }); +export const UpdatePackagePolicyRequestBodySchema = schema.object({ + ...CreatePackagePolicyProps, + name: schema.maybe(schema.string()), + inputs: schema.maybe( + schema.arrayOf( + schema.object({ + ...PackagePolicyInputsSchema, + streams: schema.maybe(schema.arrayOf(schema.object(PackagePolicyStreamsSchema))), + }) + ) + ), + version: schema.maybe(schema.string()), +}); + export const UpdatePackagePolicySchema = schema.object({ ...PackagePolicyBaseSchema, version: schema.maybe(schema.string()), diff --git a/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts b/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts index 34649602d2a02..010cd10492bf0 100644 --- a/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts +++ b/x-pack/plugins/fleet/server/types/rest_spec/package_policy.ts @@ -7,7 +7,10 @@ import { schema } from '@kbn/config-schema'; -import { NewPackagePolicySchema, UpdatePackagePolicySchema } from '../models'; +import { + CreatePackagePolicyRequestBodySchema, + UpdatePackagePolicyRequestBodySchema, +} from '../models'; import { ListWithKuerySchema } from './index'; @@ -22,12 +25,12 @@ export const GetOnePackagePolicyRequestSchema = { }; export const CreatePackagePolicyRequestSchema = { - body: NewPackagePolicySchema, + body: CreatePackagePolicyRequestBodySchema, }; export const UpdatePackagePolicyRequestSchema = { ...GetOnePackagePolicyRequestSchema, - body: UpdatePackagePolicySchema, + body: UpdatePackagePolicyRequestBodySchema, }; export const DeletePackagePoliciesRequestSchema = { diff --git a/x-pack/plugins/infra/public/alerting/inventory/index.ts b/x-pack/plugins/infra/public/alerting/inventory/index.ts index 5e3c8a219ae47..4a724694bbad8 100644 --- a/x-pack/plugins/infra/public/alerting/inventory/index.ts +++ b/x-pack/plugins/infra/public/alerting/inventory/index.ts @@ -15,15 +15,15 @@ import { import { ObservabilityRuleTypeModel } from '../../../../observability/public'; -import { AlertTypeParams } from '../../../../alerting/common'; +import { AlertTypeParams as RuleTypeParams } from '../../../../alerting/common'; import { validateMetricThreshold } from './components/validation'; import { formatReason } from './rule_data_formatters'; -interface InventoryMetricAlertTypeParams extends AlertTypeParams { +interface InventoryMetricRuleTypeParams extends RuleTypeParams { criteria: InventoryMetricConditions[]; } -export function createInventoryMetricAlertType(): ObservabilityRuleTypeModel { +export function createInventoryMetricRuleType(): ObservabilityRuleTypeModel { return { id: METRIC_INVENTORY_THRESHOLD_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.metrics.inventory.alertFlyout.alertDescription', { diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/index.ts b/x-pack/plugins/infra/public/alerting/log_threshold/index.ts index 0f2746b446927..b6eff8ef3826e 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/index.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/index.ts @@ -5,5 +5,5 @@ * 2.0. */ -export * from './log_threshold_alert_type'; +export * from './log_threshold_rule_type'; export { AlertDropdown } from './components/alert_dropdown'; diff --git a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.ts similarity index 92% rename from x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts rename to x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.ts index c6b2385f93c65..b0a8737a994a1 100644 --- a/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/public/alerting/log_threshold/log_threshold_rule_type.ts @@ -11,11 +11,11 @@ import { ObservabilityRuleTypeModel } from '../../../../observability/public'; import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, PartialAlertParams, -} from '../../../common/alerting/logs/log_threshold/types'; +} from '../../../common/alerting/logs/log_threshold'; import { formatRuleData } from './rule_data_formatters'; import { validateExpression } from './validation'; -export function createLogThresholdAlertType(): ObservabilityRuleTypeModel { +export function createLogThresholdRuleType(): ObservabilityRuleTypeModel { return { id: LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.logs.alertFlyout.alertDescription', { diff --git a/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts b/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts index f8dbd46a0776c..1fab0ea89fe5a 100644 --- a/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts +++ b/x-pack/plugins/infra/public/alerting/metric_anomaly/index.ts @@ -8,16 +8,15 @@ import { i18n } from '@kbn/i18n'; import React from 'react'; import { METRIC_ANOMALY_ALERT_TYPE_ID } from '../../../common/alerting/metrics'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { AlertTypeModel } from '../../../../triggers_actions_ui/public/types'; -import { AlertTypeParams } from '../../../../alerting/common'; +import { AlertTypeModel } from '../../../../triggers_actions_ui/public'; +import { AlertTypeParams as RuleTypeParams } from '../../../../alerting/common'; import { validateMetricAnomaly } from './components/validation'; -interface MetricAnomalyAlertTypeParams extends AlertTypeParams { +interface MetricAnomalyRuleTypeParams extends RuleTypeParams { hasInfraMLCapabilities: boolean; } -export function createMetricAnomalyAlertType(): AlertTypeModel { +export function createMetricAnomalyRuleType(): AlertTypeModel { return { id: METRIC_ANOMALY_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.metrics.anomaly.alertFlyout.alertDescription', { diff --git a/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts index 679019eb2e520..d45d090e0ec92 100644 --- a/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts +++ b/x-pack/plugins/infra/public/alerting/metric_threshold/index.ts @@ -10,18 +10,18 @@ import React from 'react'; import { ObservabilityRuleTypeModel } from '../../../../observability/public'; import { validateMetricThreshold } from './components/validation'; import { formatReason } from './rule_data_formatters'; -import { AlertTypeParams } from '../../../../alerting/common'; +import { AlertTypeParams as RuleTypeParams } from '../../../../alerting/common'; import { MetricExpressionParams, METRIC_THRESHOLD_ALERT_TYPE_ID, // eslint-disable-next-line @kbn/eslint/no-restricted-paths } from '../../../server/lib/alerting/metric_threshold/types'; -interface MetricThresholdAlertTypeParams extends AlertTypeParams { +interface MetricThresholdRuleTypeParams extends RuleTypeParams { criteria: MetricExpressionParams[]; } -export function createMetricThresholdAlertType(): ObservabilityRuleTypeModel { +export function createMetricThresholdRuleType(): ObservabilityRuleTypeModel { return { id: METRIC_THRESHOLD_ALERT_TYPE_ID, description: i18n.translate('xpack.infra.metrics.alertFlyout.alertDescription', { diff --git a/x-pack/plugins/infra/public/apps/common_providers.tsx b/x-pack/plugins/infra/public/apps/common_providers.tsx index 64867c5743d0d..39808ab7d3ac4 100644 --- a/x-pack/plugins/infra/public/apps/common_providers.tsx +++ b/x-pack/plugins/infra/public/apps/common_providers.tsx @@ -10,6 +10,7 @@ import React, { useMemo } from 'react'; import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; import { KibanaContextProvider, + KibanaThemeProvider, useUiSetting$, } from '../../../../../src/plugins/kibana_react/public'; import { Storage } from '../../../../../src/plugins/kibana_utils/public'; @@ -44,7 +45,8 @@ export const CommonInfraProviders: React.FC<{ export const CoreProviders: React.FC<{ core: CoreStart; plugins: InfraClientStartDeps; -}> = ({ children, core, plugins }) => { + theme$: AppMountParameters['theme$']; +}> = ({ children, core, plugins, theme$ }) => { const { Provider: KibanaContextProviderForPlugin } = useMemo( () => createKibanaContextForPlugin(core, plugins), [core, plugins] @@ -52,7 +54,9 @@ export const CoreProviders: React.FC<{ return ( - {children} + + {children} + ); }; diff --git a/x-pack/plugins/infra/public/apps/logs_app.tsx b/x-pack/plugins/infra/public/apps/logs_app.tsx index b512b5ce4a176..6ebaf3e805d91 100644 --- a/x-pack/plugins/infra/public/apps/logs_app.tsx +++ b/x-pack/plugins/infra/public/apps/logs_app.tsx @@ -23,7 +23,7 @@ import { prepareMountElement } from './common_styles'; export const renderApp = ( core: CoreStart, plugins: InfraClientStartDeps, - { element, history, setHeaderActionMenu }: AppMountParameters + { element, history, setHeaderActionMenu, theme$ }: AppMountParameters ) => { const storage = new Storage(window.localStorage); @@ -36,6 +36,7 @@ export const renderApp = ( history={history} plugins={plugins} setHeaderActionMenu={setHeaderActionMenu} + theme$={theme$} />, element ); @@ -51,11 +52,12 @@ const LogsApp: React.FC<{ plugins: InfraClientStartDeps; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; storage: Storage; -}> = ({ core, history, plugins, setHeaderActionMenu, storage }) => { + theme$: AppMountParameters['theme$']; +}> = ({ core, history, plugins, setHeaderActionMenu, storage, theme$ }) => { const uiCapabilities = core.application.capabilities; return ( - + { const storage = new Storage(window.localStorage); @@ -37,6 +37,7 @@ export const renderApp = ( plugins={plugins} setHeaderActionMenu={setHeaderActionMenu} storage={storage} + theme$={theme$} />, element ); @@ -52,11 +53,12 @@ const MetricsApp: React.FC<{ plugins: InfraClientStartDeps; setHeaderActionMenu: AppMountParameters['setHeaderActionMenu']; storage: Storage; -}> = ({ core, history, plugins, setHeaderActionMenu, storage }) => { + theme$: AppMountParameters['theme$']; +}> = ({ core, history, plugins, setHeaderActionMenu, storage, theme$ }) => { const uiCapabilities = core.application.capabilities; return ( - + ( - -
{wrappedStory()}
-
- ), + (wrappedStory) =>
{wrappedStory()}
, + decorateWithGlobalStorybookThemeProviders, ], parameters: { layout: 'padded', diff --git a/x-pack/plugins/infra/public/components/data_search_progress.stories.tsx b/x-pack/plugins/infra/public/components/data_search_progress.stories.tsx index 7a4966ebeb6f2..0efbfb25c9749 100644 --- a/x-pack/plugins/infra/public/components/data_search_progress.stories.tsx +++ b/x-pack/plugins/infra/public/components/data_search_progress.stories.tsx @@ -8,17 +8,14 @@ import { PropsOf } from '@elastic/eui'; import { Meta, Story } from '@storybook/react/types-6-0'; import React from 'react'; -import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; +import { decorateWithGlobalStorybookThemeProviders } from '../test_utils/use_global_storybook_theme'; import { DataSearchProgress } from './data_search_progress'; export default { title: 'infra/dataSearch/DataSearchProgress', decorators: [ - (wrappedStory) => ( - -
{wrappedStory()}
-
- ), + (wrappedStory) =>
{wrappedStory()}
, + decorateWithGlobalStorybookThemeProviders, ], parameters: { layout: 'padded', diff --git a/x-pack/plugins/infra/public/components/loading/__examples__/index.stories.tsx b/x-pack/plugins/infra/public/components/loading/__examples__/index.stories.tsx index b43991f4b9df4..161708b2bd358 100644 --- a/x-pack/plugins/infra/public/components/loading/__examples__/index.stories.tsx +++ b/x-pack/plugins/infra/public/components/loading/__examples__/index.stories.tsx @@ -5,10 +5,17 @@ * 2.0. */ -import { storiesOf } from '@storybook/react'; +import { Meta, Story } from '@storybook/react/types-6-0'; import React from 'react'; import { InfraLoadingPanel } from '..'; +import { decorateWithGlobalStorybookThemeProviders } from '../../../test_utils/use_global_storybook_theme'; -storiesOf('infra/InfraLoadingPanel', module).add('example', () => ( - -)); +export default { + title: 'infra/InfraLoadingPanel', + decorators: [ + (wrappedStory) =>
{wrappedStory()}
, + decorateWithGlobalStorybookThemeProviders, + ], +} as Meta; + +export const LoadingPanel: Story = () => ; diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx b/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx index 82b02059ecc1f..f11430586764d 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream.stories.mdx @@ -4,12 +4,12 @@ import { delay } from 'rxjs/operators'; import { I18nProvider } from '@kbn/i18n-react'; import { KBN_FIELD_TYPES } from '../../../../../../src/plugins/data/public'; -import { EuiThemeProvider } from '../../../../../../src/plugins/kibana_react/common'; import { KibanaContextProvider } from '../../../../../../src/plugins/kibana_react/public'; import { LOG_ENTRIES_SEARCH_STRATEGY } from '../../../common/search_strategies/log_entries/log_entries'; import { createIndexPatternMock, createIndexPatternsMock } from '../../hooks/use_kibana_index_patterns.mock'; import { DEFAULT_SOURCE_CONFIGURATION } from '../../test_utils/source_configuration'; import { generateFakeEntries, ENTRIES_EMPTY } from '../../test_utils/entries'; +import { decorateWithGlobalStorybookThemeProviders } from '../../test_utils/use_global_storybook_theme'; import { LogStream } from './'; @@ -145,13 +145,12 @@ export const Template = (args) => ; decorators={[ (story) => ( - - - {story()} - - + + {story()} + ), + decorateWithGlobalStorybookThemeProviders, ]} /> @@ -172,7 +171,8 @@ To use the component your plugin needs to follow certain criteria: - Ensure `"infra"` and `"data"` are specified as a `requiredPlugins` in your plugin's `kibana.json`. - Ensure the `` component is mounted inside the hierachy of a [`kibana-react` provider](https://github.com/elastic/kibana/blob/b2d0aa7b7fae1c89c8f9e8854ae73e71be64e765/src/plugins/kibana_react/README.md#L45). At a minimum, the kibana-react provider must pass `http` (from core start services) and `data` (from core plugin start dependencies). -- Ensure the `` component is mounted inside the hierachy of a [`EuiThemeProvider`](https://github.com/elastic/kibana/blob/main/src/plugins/kibana_react/common/eui_styled_components.tsx). +- Ensure the `` component is mounted inside the hierachy of a [`KibanaThemeProvider`](https://github.com/elastic/kibana/blob/31d2db035c905fb5819fa6dc2354f3be795a34cf/src/plugins/kibana_react/public/theme/kibana_theme_provider.tsx#L27). +- Ensure the `` component is mounted inside the hierachy of a [`EuiThemeProvider`](https://github.com/elastic/kibana/blob/main/src/plugins/kibana_react/common/eui_styled_components.tsx). This is not the same as the provider exported by EUI. It bridges the gap between EUI and styled components and predates the css-in-js support in EUI. ## Usage diff --git a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx index e3fc4ca1de565..19f7d7f1b4a7a 100644 --- a/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx +++ b/x-pack/plugins/infra/public/components/log_stream/log_stream_embeddable.tsx @@ -77,7 +77,7 @@ export class LogStreamEmbeddable extends Embeddable { } ReactDOM.render( - +
{renderStory()}) + .addDecorator(decorateWithGlobalStorybookThemeProviders) .add('Partitioned warnings', () => { return ( ( - -
{renderStory()}
-
- )) + .addDecorator((renderStory) =>
{renderStory()}
) + .addDecorator(decorateWithGlobalStorybookThemeProviders) .add('Reconfiguration with partitioned warnings', () => { return ( { return ( - - - - - - - - - - - + + + + + + + + + ); }, + decorateWithGlobalStorybookThemeProviders, ], argTypes: { logIndices: { diff --git a/x-pack/plugins/infra/public/plugin.ts b/x-pack/plugins/infra/public/plugin.ts index 5565c90970ecd..bc3aff9f01637 100644 --- a/x-pack/plugins/infra/public/plugin.ts +++ b/x-pack/plugins/infra/public/plugin.ts @@ -31,19 +31,17 @@ export class Plugin implements InfraClientPluginClass { registerFeatures(pluginsSetup.home); } - const { createInventoryMetricAlertType } = await import('./alerting/inventory'); - const { createLogThresholdAlertType } = await import('./alerting/log_threshold'); - const { createMetricThresholdAlertType } = await import('./alerting/metric_threshold'); + const { createInventoryMetricRuleType } = await import('./alerting/inventory'); + const { createLogThresholdRuleType } = await import('./alerting/log_threshold'); + const { createMetricThresholdRuleType } = await import('./alerting/metric_threshold'); pluginsSetup.observability.observabilityRuleTypeRegistry.register( - createInventoryMetricAlertType() + createInventoryMetricRuleType() ); + pluginsSetup.observability.observabilityRuleTypeRegistry.register(createLogThresholdRuleType()); pluginsSetup.observability.observabilityRuleTypeRegistry.register( - createLogThresholdAlertType() - ); - pluginsSetup.observability.observabilityRuleTypeRegistry.register( - createMetricThresholdAlertType() + createMetricThresholdRuleType() ); pluginsSetup.observability.dashboard.register({ appName: 'infra_logs', diff --git a/x-pack/plugins/infra/public/test_utils/use_global_storybook_theme.tsx b/x-pack/plugins/infra/public/test_utils/use_global_storybook_theme.tsx new file mode 100644 index 0000000000000..9330f83cd968a --- /dev/null +++ b/x-pack/plugins/infra/public/test_utils/use_global_storybook_theme.tsx @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { StoryContext } from '@storybook/addons'; +import React, { useEffect, useMemo, useState } from 'react'; +import { BehaviorSubject } from 'rxjs'; +import type { CoreTheme } from '../../../../../src/core/public'; +import { EuiThemeProvider } from '../../../../../src/plugins/kibana_react/common'; +import { KibanaThemeProvider } from '../../../../../src/plugins/kibana_react/public'; + +export const useGlobalStorybookTheme = ({ globals: { euiTheme } }: StoryContext) => { + const theme = useMemo(() => euiThemeFromId(euiTheme), [euiTheme]); + const [theme$] = useState(() => new BehaviorSubject(theme)); + + useEffect(() => { + theme$.next(theme); + }, [theme$, theme]); + + return { + theme, + theme$, + }; +}; + +export const GlobalStorybookThemeProviders: React.FC<{ storyContext: StoryContext }> = ({ + children, + storyContext, +}) => { + const { theme, theme$ } = useGlobalStorybookTheme(storyContext); + return ( + + {children} + + ); +}; + +export const decorateWithGlobalStorybookThemeProviders = < + StoryFnReactReturnType extends React.ReactNode +>( + wrappedStory: () => StoryFnReactReturnType, + storyContext: StoryContext +) => ( + + {wrappedStory()} + +); + +const euiThemeFromId = (themeId: string): CoreTheme => { + switch (themeId) { + case 'v8.dark': + return { darkMode: true }; + default: + return { darkMode: false }; + } +}; diff --git a/x-pack/plugins/infra/server/lib/alerting/index.ts b/x-pack/plugins/infra/server/lib/alerting/index.ts index fb3cd7702c3df..570b44fd44db9 100644 --- a/x-pack/plugins/infra/server/lib/alerting/index.ts +++ b/x-pack/plugins/infra/server/lib/alerting/index.ts @@ -5,4 +5,4 @@ * 2.0. */ -export { registerAlertTypes } from './register_alert_types'; +export { registerRuleTypes } from './register_rule_types'; 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 922b10e8bd2b0..364c6b5a0d23a 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 @@ -8,18 +8,18 @@ import { mapValues, last, first } from 'lodash'; import moment from 'moment'; import { ElasticsearchClient } from 'kibana/server'; -import { SnapshotCustomMetricInput } from '../../../../common/http_api/snapshot_api'; import { isTooManyBucketsPreviewException, TOO_MANY_BUCKETS_PREVIEW_EXCEPTION, } from '../../../../common/alerting/metrics'; -import { - InfraDatabaseSearchResponse, - CallWithRequestParams, -} from '../../adapters/framework/adapter_types'; +import { InfraDatabaseSearchResponse, CallWithRequestParams } from '../../adapters/framework'; import { Comparator, InventoryMetricConditions } from './types'; import { InventoryItemType, SnapshotMetricType } from '../../../../common/inventory_models/types'; -import { InfraTimerangeInput, SnapshotRequest } from '../../../../common/http_api/snapshot_api'; +import { + InfraTimerangeInput, + SnapshotRequest, + SnapshotCustomMetricInput, +} from '../../../../common/http_api'; import { InfraSource } from '../../sources'; import { UNGROUPED_FACTORY_KEY } from '../common/utils'; import { getNodes } from '../../../routes/snapshot/lib/get_nodes'; 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 654d69eb7fabb..dda43178713c2 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 @@ -15,11 +15,14 @@ import { AlertStates } from './types'; import { ActionGroupIdsOf, ActionGroup, - AlertInstanceContext, - AlertInstanceState, + AlertInstanceContext as AlertContext, + AlertInstanceState as AlertState, RecoveredActionGroup, } from '../../../../../alerting/common'; -import { AlertInstance, AlertTypeState } from '../../../../../alerting/server'; +import { + AlertInstance as Alert, + AlertTypeState as RuleTypeState, +} from '../../../../../alerting/server'; import { SnapshotMetricType } from '../../../../common/inventory_models/types'; import { InfraBackendLibs } from '../../infra_types'; import { METRIC_FORMATTERS } from '../../../../common/formatters/snapshot_metric_formats'; @@ -39,34 +42,34 @@ type InventoryMetricThresholdAllowedActionGroups = ActionGroupIdsOf< typeof FIRED_ACTIONS | typeof WARNING_ACTIONS >; -export type InventoryMetricThresholdAlertTypeState = AlertTypeState; // no specific state used -export type InventoryMetricThresholdAlertInstanceState = AlertInstanceState; // no specific state used -export type InventoryMetricThresholdAlertInstanceContext = AlertInstanceContext; // no specific instance context used +export type InventoryMetricThresholdRuleTypeState = RuleTypeState; // no specific state used +export type InventoryMetricThresholdAlertState = AlertState; // no specific state used +export type InventoryMetricThresholdAlertContext = AlertContext; // no specific instance context used -type InventoryMetricThresholdAlertInstance = AlertInstance< - InventoryMetricThresholdAlertInstanceState, - InventoryMetricThresholdAlertInstanceContext, +type InventoryMetricThresholdAlert = Alert< + InventoryMetricThresholdAlertState, + InventoryMetricThresholdAlertContext, InventoryMetricThresholdAllowedActionGroups >; -type InventoryMetricThresholdAlertInstanceFactory = ( +type InventoryMetricThresholdAlertFactory = ( id: string, reason: string, threshold?: number | undefined, value?: number | undefined -) => InventoryMetricThresholdAlertInstance; +) => InventoryMetricThresholdAlert; export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) => libs.metricsRules.createLifecycleRuleExecutor< InventoryMetricThresholdParams & Record, - InventoryMetricThresholdAlertTypeState, - InventoryMetricThresholdAlertInstanceState, - InventoryMetricThresholdAlertInstanceContext, + InventoryMetricThresholdRuleTypeState, + InventoryMetricThresholdAlertState, + InventoryMetricThresholdAlertContext, InventoryMetricThresholdAllowedActionGroups >(async ({ services, params }) => { const { criteria, filterQuery, sourceId, nodeType, alertOnNoData } = params; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); const { alertWithLifecycle, savedObjectsClient } = services; - const alertInstanceFactory: InventoryMetricThresholdAlertInstanceFactory = (id, reason) => + const alertFactory: InventoryMetricThresholdAlertFactory = (id, reason) => alertWithLifecycle({ id, fields: { @@ -82,8 +85,8 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = } catch (e) { const actionGroupId = FIRED_ACTIONS.id; // Change this to an Error action group when able const reason = buildInvalidQueryAlertReason(params.filterQueryText); - const alertInstance = alertInstanceFactory('*', reason); - alertInstance.scheduleActions(actionGroupId, { + const alert = alertFactory('*', reason); + alert.scheduleActions(actionGroupId, { group: '*', alertState: stateToAlertMessage[AlertStates.ERROR], reason, @@ -191,8 +194,8 @@ export const createInventoryMetricThresholdExecutor = (libs: InfraBackendLibs) = ? WARNING_ACTIONS.id : FIRED_ACTIONS.id; - const alertInstance = alertInstanceFactory(`${group}`, reason); - alertInstance.scheduleActions( + const alert = alertFactory(`${group}`, reason); + alert.scheduleActions( /** * TODO: We're lying to the compiler here as explicitly calling `scheduleActions` on * the RecoveredActionGroup isn't allowed diff --git a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_rule_type.ts similarity index 98% rename from x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts rename to x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_rule_type.ts index 77c85967e64f6..9776d1ab66915 100644 --- a/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/inventory_metric_threshold/register_inventory_metric_threshold_rule_type.ts @@ -58,7 +58,7 @@ const condition = schema.object({ ), }); -export async function registerMetricInventoryThresholdAlertType( +export async function registerMetricInventoryThresholdRuleType( alertingPlugin: PluginSetupContract, libs: InfraBackendLibs ) { diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts index e5d8bab948581..b5cf05512b353 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.test.ts @@ -22,7 +22,7 @@ import { Criterion, UngroupedSearchQueryResponse, GroupedSearchQueryResponse, -} from '../../../../common/alerting/logs/log_threshold/types'; +} from '../../../../common/alerting/logs/log_threshold'; import { alertsMock } from '../../../../../alerting/server/mocks'; import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; @@ -407,7 +407,7 @@ describe('Log threshold executor', () => { describe('Results processors', () => { describe('Can process ungrouped results', () => { test('It handles the ALERT state correctly', () => { - const alertInstanceUpdaterMock = jest.fn(); + const alertUpdaterMock = jest.fn(); const alertParams = { ...baseAlertParams, criteria: [positiveCriteria[0]], @@ -423,12 +423,12 @@ describe('Log threshold executor', () => { results, alertParams, alertsMock.createAlertInstanceFactory, - alertInstanceUpdaterMock + alertUpdaterMock ); // First call, second argument - expect(alertInstanceUpdaterMock.mock.calls[0][1]).toBe(AlertStates.ALERT); + expect(alertUpdaterMock.mock.calls[0][1]).toBe(AlertStates.ALERT); // First call, third argument - expect(alertInstanceUpdaterMock.mock.calls[0][2]).toEqual([ + expect(alertUpdaterMock.mock.calls[0][2]).toEqual([ { actionGroup: 'logs.threshold.fired', context: { @@ -444,7 +444,7 @@ describe('Log threshold executor', () => { describe('Can process grouped results', () => { test('It handles the ALERT state correctly', () => { - const alertInstanceUpdaterMock = jest.fn(); + const alertUpdaterMock = jest.fn(); const alertParams = { ...baseAlertParams, criteria: [positiveCriteria[0]], @@ -487,13 +487,13 @@ describe('Log threshold executor', () => { results, alertParams, alertsMock.createAlertInstanceFactory, - alertInstanceUpdaterMock + alertUpdaterMock ); - expect(alertInstanceUpdaterMock.mock.calls.length).toBe(2); + expect(alertUpdaterMock.mock.calls.length).toBe(2); // First call, second argument - expect(alertInstanceUpdaterMock.mock.calls[0][1]).toBe(AlertStates.ALERT); + expect(alertUpdaterMock.mock.calls[0][1]).toBe(AlertStates.ALERT); // First call, third argument - expect(alertInstanceUpdaterMock.mock.calls[0][2]).toEqual([ + expect(alertUpdaterMock.mock.calls[0][2]).toEqual([ { actionGroup: 'logs.threshold.fired', context: { @@ -506,9 +506,9 @@ describe('Log threshold executor', () => { ]); // Second call, second argument - expect(alertInstanceUpdaterMock.mock.calls[1][1]).toBe(AlertStates.ALERT); + expect(alertUpdaterMock.mock.calls[1][1]).toBe(AlertStates.ALERT); // Second call, third argument - expect(alertInstanceUpdaterMock.mock.calls[1][2]).toEqual([ + expect(alertUpdaterMock.mock.calls[1][2]).toEqual([ { actionGroup: 'logs.threshold.fired', context: { diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts index 6d2b074c45bb0..a41c70f5c2869 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/log_threshold_executor.ts @@ -16,10 +16,10 @@ import { ElasticsearchClient } from 'kibana/server'; import { ActionGroup, ActionGroupIdsOf, - AlertInstance, - AlertInstanceContext, - AlertInstanceState, - AlertTypeState, + AlertInstance as Alert, + AlertInstanceContext as AlertContext, + AlertInstanceState as AlertState, + AlertTypeState as RuleTypeState, } from '../../../../../alerting/server'; import { AlertParams, @@ -40,7 +40,7 @@ import { RatioAlertParams, UngroupedSearchQueryResponse, UngroupedSearchQueryResponseRT, -} from '../../../../common/alerting/logs/log_threshold/types'; +} from '../../../../common/alerting/logs/log_threshold'; import { resolveLogSourceConfiguration } from '../../../../common/log_sources'; import { decodeOrThrow } from '../../../../common/runtime_types'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; @@ -54,22 +54,22 @@ import { } from './reason_formatters'; export type LogThresholdActionGroups = ActionGroupIdsOf; -export type LogThresholdAlertTypeParams = AlertParams; -export type LogThresholdAlertTypeState = AlertTypeState; // no specific state used -export type LogThresholdAlertInstanceState = AlertInstanceState; // no specific state used -export type LogThresholdAlertInstanceContext = AlertInstanceContext; // no specific instance context used - -type LogThresholdAlertInstance = AlertInstance< - LogThresholdAlertInstanceState, - LogThresholdAlertInstanceContext, +export type LogThresholdRuleTypeParams = AlertParams; +export type LogThresholdRuleTypeState = RuleTypeState; // no specific state used +export type LogThresholdAlertState = AlertState; // no specific state used +export type LogThresholdAlertContext = AlertContext; // no specific instance context used + +type LogThresholdAlert = Alert< + LogThresholdAlertState, + LogThresholdAlertContext, LogThresholdActionGroups >; -type LogThresholdAlertInstanceFactory = ( +type LogThresholdAlertFactory = ( id: string, reason: string, value: number, threshold: number -) => LogThresholdAlertInstance; +) => LogThresholdAlert; const COMPOSITE_GROUP_SIZE = 2000; @@ -88,15 +88,15 @@ const checkValueAgainstComparatorMap: { export const createLogThresholdExecutor = (libs: InfraBackendLibs) => libs.logsRules.createLifecycleRuleExecutor< - LogThresholdAlertTypeParams, - LogThresholdAlertTypeState, - LogThresholdAlertInstanceState, - LogThresholdAlertInstanceContext, + LogThresholdRuleTypeParams, + LogThresholdRuleTypeState, + LogThresholdAlertState, + LogThresholdAlertContext, LogThresholdActionGroups >(async ({ services, params }) => { const { alertWithLifecycle, savedObjectsClient, scopedClusterClient } = services; const { sources } = libs; - const alertInstanceFactory: LogThresholdAlertInstanceFactory = (id, reason, value, threshold) => + const alertFactory: LogThresholdAlertFactory = (id, reason, value, threshold) => alertWithLifecycle({ id, fields: { @@ -125,7 +125,7 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => indices, runtimeMappings, scopedClusterClient.asCurrentUser, - alertInstanceFactory + alertFactory ); } else { await executeRatioAlert( @@ -134,7 +134,7 @@ export const createLogThresholdExecutor = (libs: InfraBackendLibs) => indices, runtimeMappings, scopedClusterClient.asCurrentUser, - alertInstanceFactory + alertFactory ); } } catch (e) { @@ -148,7 +148,7 @@ async function executeAlert( indexPattern: string, runtimeMappings: estypes.MappingRuntimeFields, esClient: ElasticsearchClient, - alertInstanceFactory: LogThresholdAlertInstanceFactory + alertFactory: LogThresholdAlertFactory ) { const query = getESQuery(alertParams, timestampField, indexPattern, runtimeMappings); @@ -160,15 +160,15 @@ async function executeAlert( processGroupByResults( await getGroupedResults(query, esClient), alertParams, - alertInstanceFactory, - updateAlertInstance + alertFactory, + updateAlert ); } else { processUngroupedResults( await getUngroupedResults(query, esClient), alertParams, - alertInstanceFactory, - updateAlertInstance + alertFactory, + updateAlert ); } } @@ -179,7 +179,7 @@ async function executeRatioAlert( indexPattern: string, runtimeMappings: estypes.MappingRuntimeFields, esClient: ElasticsearchClient, - alertInstanceFactory: LogThresholdAlertInstanceFactory + alertFactory: LogThresholdAlertFactory ) { // Ratio alert params are separated out into two standard sets of alert params const numeratorParams: AlertParams = { @@ -211,8 +211,8 @@ async function executeRatioAlert( numeratorGroupedResults, denominatorGroupedResults, alertParams, - alertInstanceFactory, - updateAlertInstance + alertFactory, + updateAlert ); } else { const numeratorUngroupedResults = await getUngroupedResults(numeratorQuery, esClient); @@ -221,8 +221,8 @@ async function executeRatioAlert( numeratorUngroupedResults, denominatorUngroupedResults, alertParams, - alertInstanceFactory, - updateAlertInstance + alertFactory, + updateAlert ); } } @@ -241,20 +241,20 @@ const getESQuery = ( export const processUngroupedResults = ( results: UngroupedSearchQueryResponse, params: CountAlertParams, - alertInstanceFactory: LogThresholdAlertInstanceFactory, - alertInstaceUpdater: AlertInstanceUpdater + alertFactory: LogThresholdAlertFactory, + alertUpdater: AlertUpdater ) => { const { count, criteria } = params; const documentCount = results.hits.total.value; if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { - const alertInstance = alertInstanceFactory( + const alert = alertFactory( UNGROUPED_FACTORY_KEY, getReasonMessageForUngroupedCountAlert(documentCount, count.value, count.comparator), documentCount, count.value ); - alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ + alertUpdater(alert, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, context: { @@ -272,8 +272,8 @@ export const processUngroupedRatioResults = ( numeratorResults: UngroupedSearchQueryResponse, denominatorResults: UngroupedSearchQueryResponse, params: RatioAlertParams, - alertInstanceFactory: LogThresholdAlertInstanceFactory, - alertInstaceUpdater: AlertInstanceUpdater + alertFactory: LogThresholdAlertFactory, + alertUpdater: AlertUpdater ) => { const { count, criteria } = params; @@ -282,13 +282,13 @@ export const processUngroupedRatioResults = ( const ratio = getRatio(numeratorCount, denominatorCount); if (ratio !== undefined && checkValueAgainstComparatorMap[count.comparator](ratio, count.value)) { - const alertInstance = alertInstanceFactory( + const alert = alertFactory( UNGROUPED_FACTORY_KEY, getReasonMessageForUngroupedRatioAlert(ratio, count.value, count.comparator), ratio, count.value ); - alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ + alertUpdater(alert, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, context: { @@ -345,8 +345,8 @@ const getReducedGroupByResults = ( export const processGroupByResults = ( results: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], params: CountAlertParams, - alertInstanceFactory: LogThresholdAlertInstanceFactory, - alertInstaceUpdater: AlertInstanceUpdater + alertFactory: LogThresholdAlertFactory, + alertUpdater: AlertUpdater ) => { const { count, criteria } = params; @@ -356,7 +356,7 @@ export const processGroupByResults = ( const documentCount = group.documentCount; if (checkValueAgainstComparatorMap[count.comparator](documentCount, count.value)) { - const alertInstance = alertInstanceFactory( + const alert = alertFactory( group.name, getReasonMessageForGroupedCountAlert( documentCount, @@ -367,7 +367,7 @@ export const processGroupByResults = ( documentCount, count.value ); - alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ + alertUpdater(alert, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, context: { @@ -386,8 +386,8 @@ export const processGroupByRatioResults = ( numeratorResults: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], denominatorResults: GroupedSearchQueryResponse['aggregations']['groups']['buckets'], params: RatioAlertParams, - alertInstanceFactory: LogThresholdAlertInstanceFactory, - alertInstaceUpdater: AlertInstanceUpdater + alertFactory: LogThresholdAlertFactory, + alertUpdater: AlertUpdater ) => { const { count, criteria } = params; @@ -407,7 +407,7 @@ export const processGroupByRatioResults = ( ratio !== undefined && checkValueAgainstComparatorMap[count.comparator](ratio, count.value) ) { - const alertInstance = alertInstanceFactory( + const alert = alertFactory( numeratorGroup.name, getReasonMessageForGroupedRatioAlert( ratio, @@ -418,7 +418,7 @@ export const processGroupByRatioResults = ( ratio, count.value ); - alertInstaceUpdater(alertInstance, AlertStates.ALERT, [ + alertUpdater(alert, AlertStates.ALERT, [ { actionGroup: FIRED_ACTIONS.id, context: { @@ -434,24 +434,24 @@ export const processGroupByRatioResults = ( }); }; -type AlertInstanceUpdater = ( - alertInstance: AlertInstance, +type AlertUpdater = ( + alert: Alert, state: AlertStates, - actions?: Array<{ actionGroup: LogThresholdActionGroups; context: AlertInstanceContext }> + actions?: Array<{ actionGroup: LogThresholdActionGroups; context: AlertContext }> ) => void; -export const updateAlertInstance: AlertInstanceUpdater = (alertInstance, state, actions) => { +export const updateAlert: AlertUpdater = (alert, state, actions) => { if (actions && actions.length > 0) { const sharedContext = { timestamp: new Date().toISOString(), }; actions.forEach((actionSet) => { const { actionGroup, context } = actionSet; - alertInstance.scheduleActions(actionGroup, { ...sharedContext, ...context }); + alert.scheduleActions(actionGroup, { ...sharedContext, ...context }); }); } - alertInstance.replaceState({ + alert.replaceState({ alertState: state, }); }; diff --git a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_rule_type.ts similarity index 97% rename from x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts rename to x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_rule_type.ts index 3d0bac3dd2bf5..05dc2682fc3b7 100644 --- a/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/log_threshold/register_log_threshold_rule_type.ts @@ -11,7 +11,7 @@ import { createLogThresholdExecutor, FIRED_ACTIONS } from './log_threshold_execu import { LOG_DOCUMENT_COUNT_ALERT_TYPE_ID, alertParamsRT, -} from '../../../../common/alerting/logs/log_threshold/types'; +} from '../../../../common/alerting/logs/log_threshold'; import { InfraBackendLibs } from '../../infra_types'; import { decodeOrThrow } from '../../../../common/runtime_types'; @@ -71,7 +71,7 @@ const denominatorConditionsActionVariableDescription = i18n.translate( } ); -export async function registerLogThresholdAlertType( +export async function registerLogThresholdRuleType( alertingPlugin: PluginSetupContract, libs: InfraBackendLibs ) { diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts index f7dbe95b4161c..a0eac87ed161e 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/metric_anomaly_executor.ts @@ -8,20 +8,20 @@ import { i18n } from '@kbn/i18n'; import { first } from 'lodash'; import moment from 'moment'; +import { KibanaRequest } from 'kibana/server'; import { stateToAlertMessage } from '../common/messages'; import { MetricAnomalyParams } from '../../../../common/alerting/metrics'; import { MappedAnomalyHit } from '../../infra_ml'; import { AlertStates } from '../common/types'; import { ActionGroup, - AlertInstanceContext, - AlertInstanceState, + AlertInstanceContext as AlertContext, + AlertInstanceState as AlertState, } from '../../../../../alerting/common'; -import { AlertExecutorOptions } from '../../../../../alerting/server'; +import { AlertExecutorOptions as RuleExecutorOptions } from '../../../../../alerting/server'; import { getIntervalInSeconds } from '../../../utils/get_interval_in_seconds'; -import { MetricAnomalyAllowedActionGroups } from './register_metric_anomaly_alert_type'; +import { MetricAnomalyAllowedActionGroups } from './register_metric_anomaly_rule_type'; import { MlPluginSetup } from '../../../../../ml/server'; -import { KibanaRequest } from '../../../../../../../src/core/server'; import { InfraBackendLibs } from '../../infra_types'; import { evaluateCondition } from './evaluate_condition'; @@ -31,14 +31,14 @@ export const createMetricAnomalyExecutor = services, params, startedAt, - }: AlertExecutorOptions< + }: RuleExecutorOptions< /** * TODO: Remove this use of `any` by utilizing a proper type */ Record, Record, - AlertInstanceState, - AlertInstanceContext, + AlertState, + AlertContext, MetricAnomalyAllowedActionGroups >) => { if (!ml) { @@ -84,9 +84,9 @@ export const createMetricAnomalyExecutor = typical, influencers, } = first(data as MappedAnomalyHit[])!; - const alertInstance = services.alertInstanceFactory(`${nodeType}-${metric}`); + const alert = services.alertInstanceFactory(`${nodeType}-${metric}`); - alertInstance.scheduleActions(FIRED_ACTIONS_ID, { + alert.scheduleActions(FIRED_ACTIONS_ID, { alertState: stateToAlertMessage[AlertStates.ALERT], timestamp: moment(anomalyStartTime).toISOString(), anomalyScore, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_rule_type.ts similarity index 95% rename from x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts rename to x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_rule_type.ts index 2e3660c901b4a..dd90297355742 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_anomaly/register_metric_anomaly_rule_type.ts @@ -9,9 +9,9 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { MlPluginSetup } from '../../../../../ml/server'; import { - AlertType, - AlertInstanceState, - AlertInstanceContext, + AlertType as RuleType, + AlertInstanceState as AlertState, + AlertInstanceContext as AlertContext, } from '../../../../../alerting/server'; import { createMetricAnomalyExecutor, @@ -26,18 +26,18 @@ import { RecoveredActionGroupId } from '../../../../../alerting/common'; export type MetricAnomalyAllowedActionGroups = typeof FIRED_ACTIONS_ID; -export const registerMetricAnomalyAlertType = ( +export const registerMetricAnomalyRuleType = ( libs: InfraBackendLibs, ml?: MlPluginSetup -): AlertType< +): RuleType< /** * TODO: Remove this use of `any` by utilizing a proper type */ Record, never, // Only use if defining useSavedObjectReferences hook Record, - AlertInstanceState, - AlertInstanceContext, + AlertState, + AlertContext, MetricAnomalyAllowedActionGroups, RecoveredActionGroupId > => ({ diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts similarity index 98% rename from x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts rename to x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts index e8910572d4a09..47727314cc64f 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule.ts @@ -53,7 +53,7 @@ interface CompositeAggregationsResponse { }; } -export interface EvaluatedAlertParams { +export interface EvaluatedRuleParams { criteria: MetricExpressionParams[]; groupBy: string | undefined | string[]; filterQuery?: string; @@ -61,7 +61,7 @@ export interface EvaluatedAlertParams { shouldDropPartialBuckets?: boolean; } -export const evaluateAlert = ( +export const evaluateRule = ( esClient: ElasticsearchClient, params: Params, config: InfraSource['configuration'], diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts index 792e3a60747d0..5a75b18e47590 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.test.ts @@ -19,7 +19,10 @@ import { createLifecycleRuleExecutorMock } from '../../../../../rule_registry/se import { InfraSources } from '../../sources'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { elasticsearchClientMock } from 'src/core/server/elasticsearch/client/mocks'; -import { AlertInstanceContext, AlertInstanceState } from '../../../../../alerting/server'; +import { + AlertInstanceContext as AlertContext, + AlertInstanceState as AlertState, +} from '../../../../../alerting/server'; import { Aggregators, Comparator, @@ -763,8 +766,7 @@ const mockLibs: any = { const executor = createMetricThresholdExecutor(mockLibs); const alertsServices = alertsMock.createAlertServices(); -const services: AlertServicesMock & - LifecycleAlertServices = { +const services: AlertServicesMock & LifecycleAlertServices = { ...alertsServices, ...ruleRegistryMocks.createLifecycleAlertServices(alertsServices), }; diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts index c4e485af5bdb1..810055fc1771a 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/metric_threshold_executor.ts @@ -12,10 +12,13 @@ import { ALERT_REASON } from '@kbn/rule-data-utils'; import { ActionGroupIdsOf, RecoveredActionGroup, - AlertInstanceState, - AlertInstanceContext, + AlertInstanceState as AlertState, + AlertInstanceContext as AlertContext, } from '../../../../../alerting/common'; -import { AlertTypeState, AlertInstance } from '../../../../../alerting/server'; +import { + AlertTypeState as RuleTypeState, + AlertInstance as Alert, +} from '../../../../../alerting/server'; import { InfraBackendLibs } from '../../infra_types'; import { buildErrorAlertReason, @@ -28,47 +31,47 @@ import { import { UNGROUPED_FACTORY_KEY } from '../common/utils'; import { createFormatter } from '../../../../common/formatters'; import { AlertStates, Comparator } from './types'; -import { evaluateAlert, EvaluatedAlertParams } from './lib/evaluate_alert'; +import { evaluateRule, EvaluatedRuleParams } from './lib/evaluate_rule'; -export type MetricThresholdAlertTypeParams = Record; -export type MetricThresholdAlertTypeState = AlertTypeState & { +export type MetricThresholdRuleParams = Record; +export type MetricThresholdRuleTypeState = RuleTypeState & { groups: string[]; groupBy?: string | string[]; filterQuery?: string; }; -export type MetricThresholdAlertInstanceState = AlertInstanceState; // no specific instace state used -export type MetricThresholdAlertInstanceContext = AlertInstanceContext; // no specific instace state used +export type MetricThresholdAlertState = AlertState; // no specific instace state used +export type MetricThresholdAlertContext = AlertContext; // no specific instace state used type MetricThresholdAllowedActionGroups = ActionGroupIdsOf< typeof FIRED_ACTIONS | typeof WARNING_ACTIONS >; -type MetricThresholdAlertInstance = AlertInstance< - MetricThresholdAlertInstanceState, - MetricThresholdAlertInstanceContext, +type MetricThresholdAlert = Alert< + MetricThresholdAlertState, + MetricThresholdAlertContext, MetricThresholdAllowedActionGroups >; -type MetricThresholdAlertInstanceFactory = ( +type MetricThresholdAlertFactory = ( id: string, reason: string, threshold?: number | undefined, value?: number | undefined -) => MetricThresholdAlertInstance; +) => MetricThresholdAlert; export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => libs.metricsRules.createLifecycleRuleExecutor< - MetricThresholdAlertTypeParams, - MetricThresholdAlertTypeState, - MetricThresholdAlertInstanceState, - MetricThresholdAlertInstanceContext, + MetricThresholdRuleParams, + MetricThresholdRuleTypeState, + MetricThresholdAlertState, + MetricThresholdAlertContext, MetricThresholdAllowedActionGroups >(async function (options) { const { services, params, state } = options; const { criteria } = params; if (criteria.length === 0) throw new Error('Cannot execute an alert with 0 conditions'); const { alertWithLifecycle, savedObjectsClient } = services; - const alertInstanceFactory: MetricThresholdAlertInstanceFactory = (id, reason) => + const alertFactory: MetricThresholdAlertFactory = (id, reason) => alertWithLifecycle({ id, fields: { @@ -94,8 +97,8 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => const timestamp = moment().toISOString(); const actionGroupId = FIRED_ACTIONS.id; // Change this to an Error action group when able const reason = buildInvalidQueryAlertReason(params.filterQueryText); - const alertInstance = alertInstanceFactory(UNGROUPED_FACTORY_KEY, reason); - alertInstance.scheduleActions(actionGroupId, { + const alert = alertFactory(UNGROUPED_FACTORY_KEY, reason); + alert.scheduleActions(actionGroupId, { group: UNGROUPED_FACTORY_KEY, alertState: stateToAlertMessage[AlertStates.ERROR], reason, @@ -128,9 +131,9 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => state.groups?.filter((g) => g !== UNGROUPED_FACTORY_KEY) ?? [] : []; - const alertResults = await evaluateAlert( + const alertResults = await evaluateRule( services.scopedClusterClient.asCurrentUser, - params as EvaluatedAlertParams, + params as EvaluatedRuleParams, config, prevGroups ); @@ -227,8 +230,8 @@ export const createMetricThresholdExecutor = (libs: InfraBackendLibs) => : nextState === AlertStates.WARNING ? WARNING_ACTIONS.id : FIRED_ACTIONS.id; - const alertInstance = alertInstanceFactory(`${group}`, reason); - alertInstance.scheduleActions(actionGroupId, { + const alert = alertFactory(`${group}`, reason); + alert.scheduleActions(actionGroupId, { group, alertState: stateToAlertMessage[nextState], reason, diff --git a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts similarity index 97% rename from x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts rename to x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts index 251531b4515a9..0a67dbdc3190f 100644 --- a/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_alert_type.ts +++ b/x-pack/plugins/infra/server/lib/alerting/metric_threshold/register_metric_threshold_rule_type.ts @@ -9,7 +9,7 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { ActionGroupIdsOf } from '../../../../../alerting/common'; import { AlertType, PluginSetupContract } from '../../../../../alerting/server'; -import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api/metrics_explorer'; +import { METRIC_EXPLORER_AGGREGATIONS } from '../../../../common/http_api'; import { createMetricThresholdExecutor, FIRED_ACTIONS, @@ -35,7 +35,7 @@ export type MetricThresholdAlertType = Omit & { ActionGroupIdsOf: MetricThresholdAllowedActionGroups; }; -export async function registerMetricThresholdAlertType( +export async function registerMetricThresholdRuleType( alertingPlugin: PluginSetupContract, libs: InfraBackendLibs ) { diff --git a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts deleted file mode 100644 index d0af9ac4ce669..0000000000000 --- a/x-pack/plugins/infra/server/lib/alerting/register_alert_types.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { PluginSetupContract } from '../../../../alerting/server'; -import { registerMetricThresholdAlertType } from './metric_threshold/register_metric_threshold_alert_type'; -import { registerMetricInventoryThresholdAlertType } from './inventory_metric_threshold/register_inventory_metric_threshold_alert_type'; -import { registerMetricAnomalyAlertType } from './metric_anomaly/register_metric_anomaly_alert_type'; - -import { registerLogThresholdAlertType } from './log_threshold/register_log_threshold_alert_type'; -import { InfraBackendLibs } from '../infra_types'; -import { MlPluginSetup } from '../../../../ml/server'; - -const registerAlertTypes = ( - alertingPlugin: PluginSetupContract, - libs: InfraBackendLibs, - ml?: MlPluginSetup -) => { - if (alertingPlugin) { - alertingPlugin.registerType(registerMetricAnomalyAlertType(libs, ml)); - - const registerFns = [ - registerLogThresholdAlertType, - registerMetricInventoryThresholdAlertType, - registerMetricThresholdAlertType, - ]; - registerFns.forEach((fn) => { - fn(alertingPlugin, libs); - }); - } -}; - -export { registerAlertTypes }; diff --git a/x-pack/plugins/infra/server/lib/alerting/register_rule_types.ts b/x-pack/plugins/infra/server/lib/alerting/register_rule_types.ts new file mode 100644 index 0000000000000..a60de8b9c0ff9 --- /dev/null +++ b/x-pack/plugins/infra/server/lib/alerting/register_rule_types.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { PluginSetupContract } from '../../../../alerting/server'; +import { registerMetricThresholdRuleType } from './metric_threshold/register_metric_threshold_rule_type'; +import { registerMetricInventoryThresholdRuleType } from './inventory_metric_threshold/register_inventory_metric_threshold_rule_type'; +import { registerMetricAnomalyRuleType } from './metric_anomaly/register_metric_anomaly_rule_type'; +import { registerLogThresholdRuleType } from './log_threshold/register_log_threshold_rule_type'; +import { InfraBackendLibs } from '../infra_types'; +import { MlPluginSetup } from '../../../../ml/server'; + +const registerRuleTypes = ( + alertingPlugin: PluginSetupContract, + libs: InfraBackendLibs, + ml?: MlPluginSetup +) => { + if (alertingPlugin) { + alertingPlugin.registerType(registerMetricAnomalyRuleType(libs, ml)); + + const registerFns = [ + registerLogThresholdRuleType, + registerMetricInventoryThresholdRuleType, + registerMetricThresholdRuleType, + ]; + registerFns.forEach((fn) => { + fn(alertingPlugin, libs); + }); + } +}; + +export { registerRuleTypes }; diff --git a/x-pack/plugins/infra/server/plugin.ts b/x-pack/plugins/infra/server/plugin.ts index 4e655f200d94f..79998ae6d5690 100644 --- a/x-pack/plugins/infra/server/plugin.ts +++ b/x-pack/plugins/infra/server/plugin.ts @@ -27,7 +27,7 @@ import { KibanaFramework } from './lib/adapters/framework/kibana_framework_adapt import { InfraKibanaLogEntriesAdapter } from './lib/adapters/log_entries/kibana_log_entries_adapter'; import { KibanaMetricsAdapter } from './lib/adapters/metrics/kibana_metrics_adapter'; import { InfraElasticsearchSourceStatusAdapter } from './lib/adapters/source_status'; -import { registerAlertTypes } from './lib/alerting'; +import { registerRuleTypes } from './lib/alerting'; import { InfraFieldsDomain } from './lib/domains/fields_domain'; import { InfraLogEntriesDomain } from './lib/domains/log_entries_domain'; import { InfraMetricsDomain } from './lib/domains/metrics_domain'; @@ -161,7 +161,7 @@ export class InfraServerPlugin implements Plugin { ]); initInfraServer(this.libs); - registerAlertTypes(plugins.alerting, this.libs, plugins.ml); + registerRuleTypes(plugins.alerting, this.libs, plugins.ml); core.http.registerRouteHandlerContext( 'infra', diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts b/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts index 053b46e480c7b..4f2736d739b11 100644 --- a/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts +++ b/x-pack/plugins/lens/common/expressions/pie_chart/pie_chart.ts @@ -83,6 +83,10 @@ export const pie: ExpressionFunctionDefinition< types: ['boolean'], help: '', }, + showValuesInLegend: { + types: ['boolean'], + help: '', + }, legendPosition: { types: ['string'], options: [Position.Top, Position.Right, Position.Bottom, Position.Left], diff --git a/x-pack/plugins/lens/common/expressions/pie_chart/types.ts b/x-pack/plugins/lens/common/expressions/pie_chart/types.ts index 8c9ec4e5a54e7..a7aa92369dce2 100644 --- a/x-pack/plugins/lens/common/expressions/pie_chart/types.ts +++ b/x-pack/plugins/lens/common/expressions/pie_chart/types.ts @@ -17,6 +17,7 @@ export interface SharedPieLayerState { categoryDisplay: 'default' | 'inside' | 'hide'; legendDisplay: 'default' | 'show' | 'hide'; legendPosition?: 'left' | 'right' | 'top' | 'bottom'; + showValuesInLegend?: boolean; nestedLegend?: boolean; percentDecimals?: number; legendMaxLines?: number; diff --git a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx index 3b9fdaf094822..559a3cfc48164 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_function.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/render_function.tsx @@ -86,6 +86,7 @@ export function PieComponent( truncateLegend, hideLabels, palette, + showValuesInLegend, } = props.args; const chartTheme = chartsThemeService.useChartsTheme(); const chartBaseTheme = chartsThemeService.useChartsBaseTheme(); @@ -315,7 +316,7 @@ export function PieComponent( (legend.getShowLegendDefault?.(bucketColumns) ?? false))) } flatLegend={legend.flat} - showLegendExtra={legend.showValues} + showLegendExtra={showValuesInLegend} legendPosition={legendPosition || Position.Right} legendMaxDepth={nestedLegend ? undefined : 1 /* Color is based only on first layer */} onElementClick={props.interactive ?? true ? onElementClickHandler : undefined} diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts index d86500ff8a4fa..bcd9d79babbab 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.test.ts @@ -14,8 +14,10 @@ import { byDataColorPaletteMap, extractUniqTermsMap, checkTableForContainsSmallValues, + shouldShowValuesInLegend, } from './render_helpers'; import { chartPluginMock } from '../../../../../src/plugins/charts/public/mocks'; +import type { PieLayerState } from '../../common/expressions'; describe('render helpers', () => { describe('#getSliceValue', () => { @@ -374,4 +376,28 @@ describe('render helpers', () => { expect(checkTableForContainsSmallValues(datatable, columnId, 1)).toBeFalsy(); }); }); + + describe('#shouldShowValuesInLegend', () => { + it('should firstly read the state value', () => { + expect( + shouldShowValuesInLegend({ showValuesInLegend: true } as PieLayerState, 'waffle') + ).toBeTruthy(); + + expect( + shouldShowValuesInLegend({ showValuesInLegend: false } as PieLayerState, 'waffle') + ).toBeFalsy(); + }); + + it('should read value from meta in case of value in state is undefined', () => { + expect( + shouldShowValuesInLegend({ showValuesInLegend: undefined } as PieLayerState, 'waffle') + ).toBeTruthy(); + + expect(shouldShowValuesInLegend({} as PieLayerState, 'waffle')).toBeTruthy(); + + expect( + shouldShowValuesInLegend({ showValuesInLegend: undefined } as PieLayerState, 'pie') + ).toBeFalsy(); + }); + }); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts index fa20eb6f20fa8..a9685e13e1774 100644 --- a/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts +++ b/x-pack/plugins/lens/public/pie_visualization/render_helpers.ts @@ -8,8 +8,9 @@ import type { Datum, LayerValue } from '@elastic/charts'; import type { Datatable, DatatableColumn } from 'src/plugins/expressions/public'; import type { LensFilterEvent } from '../types'; -import type { PieChartTypes } from '../../common/expressions/pie_chart/types'; +import type { PieChartTypes, PieLayerState } from '../../common/expressions/pie_chart/types'; import type { PaletteDefinition, PaletteOutput } from '../../../../../src/plugins/charts/public'; +import { PartitionChartsMeta } from './partition_charts_meta'; export function getSliceValue(d: Datum, metricColumn: DatatableColumn) { const value = d[metricColumn.id]; @@ -44,6 +45,14 @@ export const isPartitionShape = (shape: PieChartTypes | string) => export const isTreemapOrMosaicShape = (shape: PieChartTypes | string) => ['treemap', 'mosaic'].includes(shape); +export const shouldShowValuesInLegend = (layer: PieLayerState, shape: PieChartTypes) => { + if ('showValues' in PartitionChartsMeta[shape]?.legend) { + return layer.showValuesInLegend ?? PartitionChartsMeta[shape]?.legend?.showValues ?? true; + } + + return false; +}; + export const extractUniqTermsMap = (dataTable: Datatable, columnId: string) => [...new Set(dataTable.rows.map((item) => item[columnId]))].reduce( (acc, item, index) => ({ diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts index 92dde282da502..a2e3f6d3ca865 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.test.ts @@ -784,7 +784,7 @@ describe('suggestions', () => { ).toHaveLength(0); }); - it('mosaic type should be added only in case of 2 groups', () => { + it('mosaic type should be hidden from the suggestion list', () => { expect( suggestions({ table: { @@ -827,97 +827,6 @@ describe('suggestions', () => { }, keptLayerIds: ['first'], }).filter(({ hide, state }) => !hide && state.shape === 'mosaic') - ).toMatchInlineSnapshot(` - Array [ - Object { - "hide": false, - "previewIcon": "bullseye", - "score": 0.6, - "state": Object { - "layers": Array [ - Object { - "categoryDisplay": "default", - "groups": Array [ - "a", - "b", - ], - "layerId": "first", - "layerType": "data", - "legendDisplay": "show", - "legendMaxLines": 1, - "metric": "c", - "nestedLegend": true, - "numberDisplay": "hidden", - "percentDecimals": 0, - "truncateLegend": true, - }, - ], - "palette": undefined, - "shape": "mosaic", - }, - "title": "As Mosaic", - }, - ] - `); - }); - - it('mosaic type should be added only in case of 2 groups (negative test)', () => { - const meta: Parameters[0] = { - table: { - layerId: 'first', - isMultiRow: true, - columns: [ - { - columnId: 'a', - operation: { label: 'Top 5', dataType: 'string' as DataType, isBucketed: true }, - }, - { - columnId: 'c', - operation: { label: 'Count', dataType: 'number' as DataType, isBucketed: false }, - }, - ], - changeType: 'unchanged', - }, - state: { - shape: 'pie', - layers: [ - { - layerId: 'first', - layerType: layerTypes.DATA, - groups: ['a', 'b'], - metric: 'c', - - numberDisplay: 'hidden', - categoryDisplay: 'inside', - legendDisplay: 'show', - percentDecimals: 0, - legendMaxLines: 1, - truncateLegend: true, - nestedLegend: true, - }, - ], - }, - keptLayerIds: ['first'], - }; - - // test with 1 group - expect( - suggestions(meta).filter(({ hide, state }) => !hide && state.shape === 'mosaic') - ).toMatchInlineSnapshot(`Array []`); - - meta.table.columns.push({ - columnId: 'b', - operation: { label: 'Top 6', dataType: 'string' as DataType, isBucketed: true }, - }); - - meta.table.columns.push({ - columnId: 'c', - operation: { label: 'Top 7', dataType: 'string' as DataType, isBucketed: true }, - }); - - // test with 3 groups - expect( - suggestions(meta).filter(({ hide, state }) => !hide && state.shape === 'mosaic') ).toMatchInlineSnapshot(`Array []`); }); }); @@ -952,7 +861,7 @@ describe('suggestions', () => { ).toHaveLength(0); }); - it('waffle type should be added only in case of 1 group', () => { + it('waffle type should be hidden from the suggestion list', () => { expect( suggestions({ table: { @@ -971,14 +880,13 @@ describe('suggestions', () => { changeType: 'unchanged', }, state: { - shape: 'waffle', + shape: 'pie', layers: [ { layerId: 'first', layerType: layerTypes.DATA, groups: ['a', 'b'], metric: 'c', - numberDisplay: 'hidden', categoryDisplay: 'inside', legendDisplay: 'show', @@ -993,61 +901,5 @@ describe('suggestions', () => { }).filter(({ hide, state }) => !hide && state.shape === 'waffle') ).toMatchInlineSnapshot(`Array []`); }); - - it('waffle type should be added only in case of 1 group (negative test)', () => { - const meta: Parameters[0] = { - table: { - layerId: 'first', - isMultiRow: true, - columns: [ - { - columnId: 'c', - operation: { label: 'Count', dataType: 'number' as DataType, isBucketed: false }, - }, - ], - changeType: 'unchanged', - }, - state: { - shape: 'pie', - layers: [ - { - layerId: 'first', - layerType: layerTypes.DATA, - groups: ['a', 'b'], - metric: 'c', - - numberDisplay: 'hidden', - categoryDisplay: 'inside', - legendDisplay: 'show', - percentDecimals: 0, - legendMaxLines: 1, - truncateLegend: true, - nestedLegend: true, - }, - ], - }, - keptLayerIds: ['first'], - }; - - // test with no group - expect( - suggestions(meta).filter(({ hide, state }) => !hide && state.shape === 'waffle') - ).toMatchInlineSnapshot(`Array []`); - - meta.table.columns.push({ - columnId: 'b', - operation: { label: 'Top 6', dataType: 'string' as DataType, isBucketed: true }, - }); - - meta.table.columns.push({ - columnId: 'c', - operation: { label: 'Top 7', dataType: 'string' as DataType, isBucketed: true }, - }); - - // test with 2 groups - expect( - suggestions(meta).filter(({ hide, state }) => !hide && state.shape === 'waffle') - ).toMatchInlineSnapshot(`Array []`); - }); }); }); diff --git a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts index f638bfd908be4..248f4a82b1694 100644 --- a/x-pack/plugins/lens/public/pie_visualization/suggestions.ts +++ b/x-pack/plugins/lens/public/pie_visualization/suggestions.ts @@ -232,11 +232,7 @@ export function suggestions({ ], }, previewIcon: 'bullseye', - hide: - groups.length !== 2 || - table.changeType === 'reduced' || - hasIntervalScale(groups) || - (state && state.shape === 'mosaic'), + hide: true, }); } @@ -275,11 +271,7 @@ export function suggestions({ ], }, previewIcon: 'bullseye', - hide: - groups.length !== 1 || - table.changeType === 'reduced' || - hasIntervalScale(groups) || - (state && state.shape === 'waffle'), + hide: true, }); } diff --git a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts index e13fbf62708ee..57270337e67a4 100644 --- a/x-pack/plugins/lens/public/pie_visualization/to_expression.ts +++ b/x-pack/plugins/lens/public/pie_visualization/to_expression.ts @@ -5,10 +5,12 @@ * 2.0. */ -import { Ast } from '@kbn/interpreter/common'; -import { PaletteRegistry } from 'src/plugins/charts/public'; -import { Operation, DatasourcePublicAPI } from '../types'; +import type { Ast } from '@kbn/interpreter/common'; +import type { PaletteRegistry } from 'src/plugins/charts/public'; +import type { Operation, DatasourcePublicAPI } from '../types'; import { DEFAULT_PERCENT_DECIMALS } from './constants'; +import { shouldShowValuesInLegend } from './render_helpers'; + import type { PieVisualizationState } from '../../common/expressions'; export function toExpression( @@ -34,6 +36,7 @@ function expressionHelper( const operations = layer.groups .map((columnId) => ({ columnId, operation: datasource.getOperationForColumnId(columnId) })) .filter((o): o is { columnId: string; operation: Operation } => !!o.operation); + if (!layer.metric || !operations.length) { return null; } @@ -55,6 +58,7 @@ function expressionHelper( categoryDisplay: [layer.categoryDisplay], legendDisplay: [layer.legendDisplay], legendPosition: [layer.legendPosition || 'right'], + showValuesInLegend: [shouldShowValuesInLegend(layer, state.shape)], percentDecimals: [ state.shape === 'waffle' ? DEFAULT_PERCENT_DECIMALS diff --git a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx index 195a72cca9fed..70ad4d8c07daa 100644 --- a/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/toolbar.tsx @@ -6,7 +6,7 @@ */ import './toolbar.scss'; -import React from 'react'; +import React, { useCallback } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiFlexGroup, @@ -23,6 +23,7 @@ import type { PieVisualizationState, SharedPieLayerState } from '../../common/ex import { VisualizationDimensionEditorProps, VisualizationToolbarProps } from '../types'; import { ToolbarPopover, LegendSettingsPopover, useDebouncedValue } from '../shared_components'; import { PalettePicker } from '../shared_components'; +import { shouldShowValuesInLegend } from './render_helpers'; const legendOptions: Array<{ value: SharedPieLayerState['legendDisplay']; @@ -55,6 +56,67 @@ const legendOptions: Array<{ export function PieToolbar(props: VisualizationToolbarProps) { const { state, setState } = props; const layer = state.layers[0]; + + const onStateChange = useCallback( + (part: Record) => { + setState({ + ...state, + layers: [{ ...layer, ...part }], + }); + }, + [layer, state, setState] + ); + + const onCategoryDisplayChange = useCallback( + (option) => onStateChange({ categoryDisplay: option }), + [onStateChange] + ); + + const onNumberDisplayChange = useCallback( + (option) => onStateChange({ numberDisplay: option }), + [onStateChange] + ); + + const onPercentDecimalsChange = useCallback( + (option) => { + onStateChange({ percentDecimals: option }); + }, + [onStateChange] + ); + + const onLegendDisplayChange = useCallback( + (optionId) => { + onStateChange({ legendDisplay: legendOptions.find(({ id }) => id === optionId)!.value }); + }, + [onStateChange] + ); + + const onLegendPositionChange = useCallback( + (id) => onStateChange({ legendPosition: id as Position }), + [onStateChange] + ); + + const onNestedLegendChange = useCallback( + (id) => onStateChange({ nestedLegend: !layer.nestedLegend }), + [layer, onStateChange] + ); + + const onTruncateLegendChange = useCallback(() => { + const current = layer.truncateLegend ?? true; + onStateChange({ truncateLegend: !current }); + }, [layer, onStateChange]); + + const onLegendMaxLinesChange = useCallback( + (val) => onStateChange({ legendMaxLines: val }), + [onStateChange] + ); + + const onValueInLegendChange = useCallback(() => { + onStateChange({ + showValuesInLegend: !shouldShowValuesInLegend(layer, state.shape), + }); + }, [layer, state.shape, onStateChange]); + if (!layer) { return null; } @@ -87,12 +149,7 @@ export function PieToolbar(props: VisualizationToolbarProps { - setState({ - ...state, - layers: [{ ...layer, categoryDisplay: option }], - }); - }} + onChange={onCategoryDisplayChange} /> ) : null} @@ -110,12 +167,7 @@ export function PieToolbar(props: VisualizationToolbarProps { - setState({ - ...state, - layers: [{ ...layer, numberDisplay: option }], - }); - }} + onChange={onNumberDisplayChange} /> ) : null} @@ -131,59 +183,28 @@ export function PieToolbar(props: VisualizationToolbarProps { - setState({ - ...state, - layers: [{ ...layer, percentDecimals: value }], - }); - }} + setValue={onPercentDecimalsChange} /> { - setState({ - ...state, - layers: [ - { - ...layer, - legendDisplay: legendOptions.find(({ id }) => id === optionId)!.value, - }, - ], - }); - }} + onDisplayChange={onLegendDisplayChange} + valueInLegend={shouldShowValuesInLegend(layer, state.shape)} + renderValueInLegendSwitch={ + 'showValues' in PartitionChartsMeta[state.shape]?.legend ?? false + } + onValueInLegendChange={onValueInLegendChange} position={layer.legendPosition} - onPositionChange={(id) => { - setState({ - ...state, - layers: [{ ...layer, legendPosition: id as Position }], - }); - }} + onPositionChange={onLegendPositionChange} renderNestedLegendSwitch nestedLegend={!!layer.nestedLegend} - onNestedLegendChange={() => { - setState({ - ...state, - layers: [{ ...layer, nestedLegend: !layer.nestedLegend }], - }); - }} + onNestedLegendChange={onNestedLegendChange} shouldTruncate={layer.truncateLegend ?? true} - onTruncateLegendChange={() => { - const current = layer.truncateLegend ?? true; - setState({ - ...state, - layers: [{ ...layer, truncateLegend: !current }], - }); - }} + onTruncateLegendChange={onTruncateLegendChange} maxLines={layer?.legendMaxLines} - onMaxLinesChange={(val) => { - setState({ - ...state, - layers: [{ ...layer, legendMaxLines: val }], - }); - }} + onMaxLinesChange={onLegendMaxLinesChange} /> ); diff --git a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx index 0eb56ce090aff..49a80b73da1c4 100644 --- a/x-pack/plugins/lens/public/pie_visualization/visualization.tsx +++ b/x-pack/plugins/lens/public/pie_visualization/visualization.tsx @@ -282,10 +282,7 @@ export const getPieVisualization = ({ warningMessages.push( {state.shape}, - }} + defaultMessage="Waffle charts are unable to effectively display small field values. To display all field values, use the Data table or Treemap." /> ); } diff --git a/x-pack/plugins/licensing/server/plugin.ts b/x-pack/plugins/licensing/server/plugin.ts index 83379fe48ac9e..43c8135127871 100644 --- a/x-pack/plugins/licensing/server/plugin.ts +++ b/x-pack/plugins/licensing/server/plugin.ts @@ -177,10 +177,7 @@ export class LicensingPlugin implements Plugin): Promise => { const client = isPromise(clusterClient) ? await clusterClient : clusterClient; try { - const { body: response } = await client.asInternalUser.xpack.info({ - // @ts-expect-error `accept_enterprise` is not present in the client definition - accept_enterprise: true, - }); + const { body: response } = await client.asInternalUser.xpack.info(); const normalizedLicense = response.license && response.license.type !== 'missing' ? normalizeServerLicense(response.license) diff --git a/x-pack/plugins/ml/common/index.ts b/x-pack/plugins/ml/common/index.ts index ea8ad43d6bb3b..c76b662df7a5a 100644 --- a/x-pack/plugins/ml/common/index.ts +++ b/x-pack/plugins/ml/common/index.ts @@ -15,3 +15,4 @@ export { isRuntimeMappings, isRuntimeField } from './util/runtime_field_utils'; export { extractErrorMessage } from './util/errors'; export type { RuntimeMappings } from './types/fields'; export { getDefaultCapabilities as getDefaultMlCapabilities } from './types/capabilities'; +export { DATAFEED_STATE, JOB_STATE } from './constants/states'; diff --git a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.scss b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.scss index 756804a0e6aa0..551734bc2fcdc 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/column_chart.scss +++ b/x-pack/plugins/ml/public/application/components/data_grid/column_chart.scss @@ -25,8 +25,3 @@ min-width: $euiButtonMinWidth; td { text-align: center } } - -/* Override to align column header to bottom of cell when no chart is available */ -.mlDataGrid .euiDataGridHeaderCell__content { - margin-top: auto; -} diff --git a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.scss b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.scss index b445c82b35ff9..f9cc09ef8c425 100644 --- a/x-pack/plugins/ml/public/application/components/data_grid/data_grid.scss +++ b/x-pack/plugins/ml/public/application/components/data_grid/data_grid.scss @@ -1,11 +1,18 @@ .mlDataGrid { + .euiDataGridRowCell--boolean { text-transform: none; } - // Override to align the sorting arrow at the bottom when histogram charts are enabled - .euiDataGridHeaderCell .euiDataGridHeaderCell__sortingArrow { - margin-top: auto; - margin-bottom: 0; + // Overrides to align the sorting arrow, actions icon and the column header when no chart is available, + // to the bottom of the cell when histogram charts are enabled. + // Note that overrides have to be used as currently it is not possible to add a custom class name + // for the EuiDataGridHeaderCell - see https://github.com/elastic/eui/issues/5106 + .euiDataGridHeaderCell { + .euiDataGridHeaderCell__sortingArrow, + .euiDataGridHeaderCell__icon, + .euiPopover { + margin-top: auto; + } } } 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 5240dbb1ec474..a77f43e68daef 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 @@ -114,14 +114,6 @@ export const DataGrid: FC = memo( // }; // }; - // If the charts are visible, hide the column actions icon. - const columnsWithChartsActionized = columnsWithCharts.map((d) => { - if (chartsVisible === true) { - d.actions = false; - } - return d; - }); - const popOverContent = useMemo(() => { return analysisType === ANALYSIS_CONFIG_TYPE.REGRESSION || analysisType === ANALYSIS_CONFIG_TYPE.CLASSIFICATION || @@ -341,7 +333,7 @@ export const DataGrid: FC = memo(
{ + columns={columnsWithCharts.map((c) => { c.initialWidth = 165; return c; })} diff --git a/x-pack/plugins/ml/server/saved_objects/saved_objects.ts b/x-pack/plugins/ml/server/saved_objects/saved_objects.ts index 004b5e8e554cc..f3061d3e38d7a 100644 --- a/x-pack/plugins/ml/server/saved_objects/saved_objects.ts +++ b/x-pack/plugins/ml/server/saved_objects/saved_objects.ts @@ -25,6 +25,10 @@ export function setupSavedObjects(savedObjects: SavedObjectsServiceSetup) { savedObjects.registerType({ name: ML_MODULE_SAVED_OBJECT_TYPE, hidden: false, + management: { + importableAndExportable: true, + visibleInManagement: false, + }, namespaceType: 'agnostic', migrations, mappings: mlModule, diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx index ba03640669330..e183194259c3a 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/embeddable/embeddable.tsx @@ -26,7 +26,7 @@ export interface ExploratoryEmbeddableProps { showCalculationMethod?: boolean; axisTitlesVisibility?: XYState['axisTitlesVisibilitySettings']; legendIsVisible?: boolean; - dataTypesIndexPatterns?: Record; + dataTypesIndexPatterns?: Partial>; reportConfigMap?: ReportConfigMap; } diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx index 640e928b8ab98..074a9e1ca6780 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_actions.tsx @@ -112,7 +112,7 @@ const HIDE_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.hide' }); const COPY_SERIES_LABEL = i18n.translate('xpack.observability.seriesEditor.clone', { - defaultMessage: 'Copy series', + defaultMessage: 'Duplicate series', }); const VIEW_SAMPLE_DOCUMENTS_LABEL = i18n.translate( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx index ccad461209313..cbd7efc42d964 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.test.tsx @@ -7,10 +7,17 @@ import React from 'react'; import { fireEvent, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { mockUxSeries, render } from '../../rtl_helpers'; import { SeriesName } from './series_name'; -describe.skip('SeriesChartTypesSelect', function () { +// ensures that fields appropriately match to their label +jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ + ...jest.requireActual('@elastic/eui/lib/services/accessibility/html_id_generator'), + htmlIdGenerator: () => () => `id-${Math.random()}`, +})); + +describe('SeriesName', function () { it('should render properly', async function () { render(); @@ -20,7 +27,7 @@ describe.skip('SeriesChartTypesSelect', function () { it('should display input when editing name', async function () { render(); - let input = screen.queryByLabelText(mockUxSeries.name); + let input = screen.queryByTestId('exploratoryViewSeriesNameInput') as HTMLInputElement; // read only expect(input).not.toBeInTheDocument(); @@ -30,17 +37,52 @@ describe.skip('SeriesChartTypesSelect', function () { fireEvent.click(editButton); await waitFor(() => { - input = screen.getByLabelText(mockUxSeries.name); + input = screen.getByTestId('exploratoryViewSeriesNameInput') as HTMLInputElement; expect(input).toBeInTheDocument(); + expect(input.value).toBe(mockUxSeries.name); }); // toggle readonly fireEvent.click(editButton); await waitFor(() => { - input = screen.getByLabelText(mockUxSeries.name); + input = screen.queryByTestId('exploratoryViewSeriesNameInput') as HTMLInputElement; + + expect(screen.getByText(mockUxSeries.name)).toBeInTheDocument(); + expect(input).not.toBeInTheDocument(); + }); + }); + + it('should save name on enter key', async function () { + const newName = '-test-new-name'; + render(); + + let input = screen.queryByTestId('exploratoryViewSeriesNameInput') as HTMLInputElement; + + // read only + expect(input).not.toBeInTheDocument(); + + const editButton = screen.getByRole('button'); + // toggle editing + userEvent.click(editButton); + + await waitFor(() => { + input = screen.getByTestId('exploratoryViewSeriesNameInput') as HTMLInputElement; + + expect(input).toBeInTheDocument(); + }); + + userEvent.click(input); + userEvent.type(input, newName); + + // submit + userEvent.keyboard('{enter}'); + + await waitFor(() => { + input = screen.queryByTestId('exploratoryViewSeriesNameInput') as HTMLInputElement; + expect(screen.getByText(`${mockUxSeries.name}${newName}`)).toBeInTheDocument(); expect(input).not.toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx index cff30a2b35059..68a628e23292c 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/columns/series_name.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import React, { useState, ChangeEvent, useEffect, useRef } from 'react'; +import React, { useState, ChangeEvent, useEffect, useRef, KeyboardEventHandler } from 'react'; import styled from 'styled-components'; import { i18n } from '@kbn/i18n'; import { @@ -57,6 +57,12 @@ export function SeriesName({ series, seriesId }: Props) { } }; + const onKeyDown: KeyboardEventHandler = (event) => { + if (event.key === 'Enter') { + setIsEditingEnabled(false); + } + }; + useEffect(() => { setValue(series.name); }, [series.name]); @@ -75,12 +81,14 @@ export function SeriesName({ series, seriesId }: Props) { diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx index 0c1f41437ede7..e3e63af94118e 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/series_editor/report_metric_options.tsx @@ -169,7 +169,7 @@ const NO_DATA_AVAILABLE = i18n.translate('xpack.observability.expView.seriesEdit const NO_PERMISSIONS = i18n.translate('xpack.observability.expView.seriesEditor.noPermissions', { defaultMessage: - "Unable to create Index Pattern. You don't have the required permission, please contact your admin.", + "Unable to create Data View. You don't have the required permission, please contact your admin.", }); const REPORT_METRIC_TOOLTIP = i18n.translate( diff --git a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts index afff4a333f7b1..29b0ac417f50f 100644 --- a/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts +++ b/x-pack/plugins/observability/public/components/shared/exploratory_view/utils/stringify_kueries.ts @@ -19,11 +19,16 @@ const buildOrCondition = (values: string[]) => { } return `(${values.join(' or ')})`; }; + +function addSlashes(str: string) { + return (str + '').replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0'); +} + export const urlFiltersToKueryString = (urlFilters: UrlFilter[]): string => { let kueryString = ''; urlFilters.forEach(({ field, values, notValues, wildcards, notWildcards }) => { - const valuesT = values?.map((val) => `"${val}"`); - const notValuesT = notValues?.map((val) => `"${val}"`); + const valuesT = values?.map((val) => `"${addSlashes(val)}"`); + const notValuesT = notValues?.map((val) => `"${addSlashes(val)}"`); const wildcardsT = wildcards?.map((val) => `*${val}*`); const notWildcardsT = notWildcards?.map((val) => `*${val}*`); diff --git a/x-pack/plugins/reporting/common/constants.ts b/x-pack/plugins/reporting/common/constants.ts index 65d196b6e068a..1fe37f86b037f 100644 --- a/x-pack/plugins/reporting/common/constants.ts +++ b/x-pack/plugins/reporting/common/constants.ts @@ -55,17 +55,6 @@ export const UI_SETTINGS_CSV_SEPARATOR = 'csv:separator'; export const UI_SETTINGS_CSV_QUOTE_VALUES = 'csv:quoteValues'; export const UI_SETTINGS_DATEFORMAT_TZ = 'dateFormat:tz'; -export const LAYOUT_TYPES = { - CANVAS: 'canvas', - PRESERVE_LAYOUT: 'preserve_layout', - PRINT: 'print', -}; - -export const DEFAULT_VIEWPORT = { - width: 1950, - height: 1200, -}; - // Export Type Definitions export const CSV_REPORT_TYPE = 'CSV'; export const CSV_JOB_TYPE = 'csv_searchsource'; diff --git a/x-pack/plugins/reporting/common/test/fixtures.ts b/x-pack/plugins/reporting/common/test/fixtures.ts index c7489d54e9504..5cc6cf274c340 100644 --- a/x-pack/plugins/reporting/common/test/fixtures.ts +++ b/x-pack/plugins/reporting/common/test/fixtures.ts @@ -11,7 +11,6 @@ import type { ReportMock } from './types'; const buildMockReport = (baseObj: ReportMock) => ({ index: '.reporting-2020.04.12', migration_version: '7.15.0', - browser_type: 'chromium', max_attempts: 1, timeout: 300000, created_by: 'elastic', diff --git a/x-pack/plugins/reporting/common/types/base.ts b/x-pack/plugins/reporting/common/types/base.ts index a44378979ac3c..234467a16921e 100644 --- a/x-pack/plugins/reporting/common/types/base.ts +++ b/x-pack/plugins/reporting/common/types/base.ts @@ -6,7 +6,7 @@ */ import type { Ensure, SerializableRecord } from '@kbn/utility-types'; -import type { LayoutParams } from './layout'; +import type { LayoutParams } from '../../../screenshotting/common'; import { LocatorParams } from './url'; export type JobId = string; diff --git a/x-pack/plugins/reporting/common/types/export_types/png.ts b/x-pack/plugins/reporting/common/types/export_types/png.ts index 3b850b5bd8b33..5afde424127a1 100644 --- a/x-pack/plugins/reporting/common/types/export_types/png.ts +++ b/x-pack/plugins/reporting/common/types/export_types/png.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { LayoutParams } from '../layout'; +import type { LayoutParams } from '../../../../screenshotting/common'; import type { BaseParams, BasePayload } from '../base'; interface BaseParamsPNG { diff --git a/x-pack/plugins/reporting/common/types/export_types/png_v2.ts b/x-pack/plugins/reporting/common/types/export_types/png_v2.ts index c937d01ce0be1..1469437fe6199 100644 --- a/x-pack/plugins/reporting/common/types/export_types/png_v2.ts +++ b/x-pack/plugins/reporting/common/types/export_types/png_v2.ts @@ -6,7 +6,7 @@ */ import type { LocatorParams } from '../url'; -import type { LayoutParams } from '../layout'; +import type { LayoutParams } from '../../../../screenshotting/common'; import type { BaseParams, BasePayload } from '../base'; // Job params: structure of incoming user request data diff --git a/x-pack/plugins/reporting/common/types/export_types/printable_pdf.ts b/x-pack/plugins/reporting/common/types/export_types/printable_pdf.ts index a424706430f2c..57e5a90595d5c 100644 --- a/x-pack/plugins/reporting/common/types/export_types/printable_pdf.ts +++ b/x-pack/plugins/reporting/common/types/export_types/printable_pdf.ts @@ -5,7 +5,7 @@ * 2.0. */ -import type { LayoutParams } from '../layout'; +import type { LayoutParams } from '../../../../screenshotting/common'; import type { BaseParams, BasePayload } from '../base'; interface BaseParamsPDF { diff --git a/x-pack/plugins/reporting/common/types/export_types/printable_pdf_v2.ts b/x-pack/plugins/reporting/common/types/export_types/printable_pdf_v2.ts index c9a7a2ce2331a..b3fbbc1653dfb 100644 --- a/x-pack/plugins/reporting/common/types/export_types/printable_pdf_v2.ts +++ b/x-pack/plugins/reporting/common/types/export_types/printable_pdf_v2.ts @@ -6,7 +6,7 @@ */ import type { LocatorParams } from '../url'; -import type { LayoutParams } from '../layout'; +import type { LayoutParams } from '../../../../screenshotting/common'; import type { BaseParams, BasePayload } from '../base'; interface BaseParamsPDFV2 { diff --git a/x-pack/plugins/reporting/common/types/index.ts b/x-pack/plugins/reporting/common/types/index.ts index 8612400e8b390..056ef81e70a0a 100644 --- a/x-pack/plugins/reporting/common/types/index.ts +++ b/x-pack/plugins/reporting/common/types/index.ts @@ -5,11 +5,9 @@ * 2.0. */ -import type { Size, LayoutParams } from './layout'; import type { JobId, BaseParams, BaseParamsV2, BasePayload, BasePayloadV2 } from './base'; export type { JobId, BaseParams, BaseParamsV2, BasePayload, BasePayloadV2 }; -export type { Size, LayoutParams }; export type { DownloadReportFn, IlmPolicyMigrationStatus, @@ -20,20 +18,6 @@ export type { } from './url'; export * from './export_types'; -export interface PageSizeParams { - pageMarginTop: number; - pageMarginBottom: number; - pageMarginWidth: number; - tableBorderWidth: number; - headingHeight: number; - subheadingHeight: number; -} - -export interface PdfImageSize { - width: number; - height?: number; -} - export interface ReportDocumentHead { _id: string; _index: string; @@ -83,7 +67,6 @@ export interface ReportSource { */ kibana_name?: string; // for troubleshooting kibana_id?: string; // for troubleshooting - browser_type?: string; // no longer used since chromium is the only option (used to allow phantomjs) timeout?: number; // for troubleshooting: the actual comparison uses the config setting xpack.reporting.queue.timeout max_attempts?: number; // for troubleshooting: the actual comparison uses the config setting xpack.reporting.capture.maxAttempts started_at?: string; // timestamp in UTC diff --git a/x-pack/plugins/reporting/common/types/layout.ts b/x-pack/plugins/reporting/common/types/layout.ts deleted file mode 100644 index b22d6b59d0873..0000000000000 --- a/x-pack/plugins/reporting/common/types/layout.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { Ensure, SerializableRecord } from '@kbn/utility-types'; - -export type Size = Ensure< - { - width: number; - height: number; - }, - SerializableRecord ->; - -export type LayoutParams = Ensure< - { - id: string; - dimensions?: Size; - }, - SerializableRecord ->; diff --git a/x-pack/plugins/reporting/kibana.json b/x-pack/plugins/reporting/kibana.json index 4bddfae96756d..123c23e5e1c29 100644 --- a/x-pack/plugins/reporting/kibana.json +++ b/x-pack/plugins/reporting/kibana.json @@ -18,6 +18,7 @@ "uiActions", "taskManager", "embeddable", + "screenshotting", "screenshotMode", "share", "features" diff --git a/x-pack/plugins/reporting/public/lib/job.tsx b/x-pack/plugins/reporting/public/lib/job.tsx index 8e2db6a9d998e..d24695b1041c7 100644 --- a/x-pack/plugins/reporting/public/lib/job.tsx +++ b/x-pack/plugins/reporting/public/lib/job.tsx @@ -51,7 +51,6 @@ export class Job { public timeout: ReportSource['timeout']; public kibana_name: ReportSource['kibana_name']; public kibana_id: ReportSource['kibana_id']; - public browser_type: ReportSource['browser_type']; public size?: ReportOutput['size']; public content_type?: TaskRunResult['content_type']; @@ -80,7 +79,6 @@ export class Job { this.timeout = report.timeout; this.kibana_name = report.kibana_name; this.kibana_id = report.kibana_id; - this.browser_type = report.browser_type; this.browserTimezone = report.payload.browserTimezone; this.size = report.output?.size; this.content_type = report.output?.content_type; diff --git a/x-pack/plugins/reporting/public/management/components/report_info_flyout_content.tsx b/x-pack/plugins/reporting/public/management/components/report_info_flyout_content.tsx index 25199c4abaa68..00ce9069d81ce 100644 --- a/x-pack/plugins/reporting/public/management/components/report_info_flyout_content.tsx +++ b/x-pack/plugins/reporting/public/management/components/report_info_flyout_content.tsx @@ -141,12 +141,6 @@ export const ReportInfoFlyoutContent: FunctionComponent = ({ info }) => { }), description: info.layout?.id || UNKNOWN, }, - { - title: i18n.translate('xpack.reporting.listing.infoPanel.browserTypeInfo', { - defaultMessage: 'Browser type', - }), - description: info.browser_type || NA, - }, ]; const warnings = info.getWarnings(); diff --git a/x-pack/plugins/reporting/public/plugin.ts b/x-pack/plugins/reporting/public/plugin.ts index fe80ed679c8ed..ea48bb253ad9f 100644 --- a/x-pack/plugins/reporting/public/plugin.ts +++ b/x-pack/plugins/reporting/public/plugin.ts @@ -18,6 +18,7 @@ import { Plugin, PluginInitializerContext, } from 'src/core/public'; +import type { ScreenshottingSetup } from '../../screenshotting/public'; import { CONTEXT_MENU_TRIGGER } from '../../../../src/plugins/embeddable/public'; import { FeatureCatalogueCategory, @@ -73,6 +74,7 @@ export interface ReportingPublicPluginSetupDendencies { management: ManagementSetup; licensing: LicensingPluginSetup; uiActions: UiActionsSetup; + screenshotting: ScreenshottingSetup; share: SharePluginSetup; } @@ -145,6 +147,7 @@ export class ReportingPublicPlugin home, management, licensing: { license$ }, // FIXME: 'license$' is deprecated + screenshotting, share, uiActions, } = setupDeps; @@ -203,7 +206,7 @@ export class ReportingPublicPlugin id: 'reportingRedirect', mount: async (params) => { const { mountRedirectApp } = await import('./redirect'); - return mountRedirectApp({ ...params, share, apiClient }); + return mountRedirectApp({ ...params, apiClient, screenshotting, share }); }, title: 'Reporting redirect app', searchable: false, diff --git a/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx b/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx index eb34fc71cbf4e..fa658126efebc 100644 --- a/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx +++ b/x-pack/plugins/reporting/public/redirect/mount_redirect_app.tsx @@ -10,6 +10,7 @@ import React from 'react'; import { EuiErrorBoundary } from '@elastic/eui'; import type { AppMountParameters } from 'kibana/public'; +import type { ScreenshottingSetup } from '../../../screenshotting/public'; import type { SharePluginSetup } from '../shared_imports'; import type { ReportingAPIClient } from '../lib/reporting_api_client'; @@ -17,13 +18,25 @@ import { RedirectApp } from './redirect_app'; interface MountParams extends AppMountParameters { apiClient: ReportingAPIClient; + screenshotting: ScreenshottingSetup; share: SharePluginSetup; } -export const mountRedirectApp = ({ element, apiClient, history, share }: MountParams) => { +export const mountRedirectApp = ({ + element, + apiClient, + history, + screenshotting, + share, +}: MountParams) => { render( - + , element ); diff --git a/x-pack/plugins/reporting/public/redirect/redirect_app.tsx b/x-pack/plugins/reporting/public/redirect/redirect_app.tsx index 4b271b17c5e85..9f0b3f51f2731 100644 --- a/x-pack/plugins/reporting/public/redirect/redirect_app.tsx +++ b/x-pack/plugins/reporting/public/redirect/redirect_app.tsx @@ -12,6 +12,7 @@ import { i18n } from '@kbn/i18n'; import { EuiCallOut, EuiCodeBlock } from '@elastic/eui'; import type { ScopedHistory } from 'src/core/public'; +import type { ScreenshottingSetup } from '../../../screenshotting/public'; import { REPORTING_REDIRECT_LOCATOR_STORE_KEY } from '../../common/constants'; import { LocatorParams } from '../../common/types'; @@ -24,6 +25,7 @@ import './redirect_app.scss'; interface Props { apiClient: ReportingAPIClient; history: ScopedHistory; + screenshotting: ScreenshottingSetup; share: SharePluginSetup; } @@ -39,7 +41,9 @@ const i18nTexts = { ), }; -export const RedirectApp: FunctionComponent = ({ share, apiClient }) => { +type ReportingContext = Record; + +export const RedirectApp: FunctionComponent = ({ apiClient, screenshotting, share }) => { const [error, setError] = useState(); useEffect(() => { @@ -53,9 +57,8 @@ export const RedirectApp: FunctionComponent = ({ share, apiClient }) => { const result = await apiClient.getInfo(jobId as string); locatorParams = result?.locatorParams?.[0]; } else { - locatorParams = (window as unknown as Record)[ - REPORTING_REDIRECT_LOCATOR_STORE_KEY - ]; + locatorParams = + screenshotting.getContext()?.[REPORTING_REDIRECT_LOCATOR_STORE_KEY]; } if (!locatorParams) { @@ -70,7 +73,7 @@ export const RedirectApp: FunctionComponent = ({ share, apiClient }) => { throw e; } })(); - }, [share, apiClient]); + }, [apiClient, screenshotting, share]); return (
diff --git a/x-pack/plugins/reporting/public/share_context_menu/index.ts b/x-pack/plugins/reporting/public/share_context_menu/index.ts index b0d6f2e6a2b52..321a5a29281af 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/index.ts +++ b/x-pack/plugins/reporting/public/share_context_menu/index.ts @@ -8,8 +8,8 @@ import * as Rx from 'rxjs'; import type { IUiSettingsClient, ToastsSetup } from 'src/core/public'; import { CoreStart } from 'src/core/public'; +import type { LayoutParams } from '../../../screenshotting/common'; import type { LicensingPluginSetup } from '../../../licensing/public'; -import type { LayoutParams } from '../../common/types'; import type { ReportingAPIClient } from '../lib/reporting_api_client'; export interface ExportPanelShareOpts { diff --git a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.tsx b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.tsx index de3cc89b31fd0..f9e2908c0f733 100644 --- a/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.tsx +++ b/x-pack/plugins/reporting/public/share_context_menu/screen_capture_panel_content.tsx @@ -8,7 +8,7 @@ import { EuiFormRow, EuiSwitch, EuiSwitchEvent } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; import React, { Component } from 'react'; -import { LayoutParams } from '../../common/types'; +import type { LayoutParams } from '../../../screenshotting/common'; import { ReportingPanelContent, ReportingPanelProps } from './reporting_panel_content'; export interface Props extends ReportingPanelProps { @@ -103,7 +103,7 @@ export class ScreenCapturePanelContent extends Component { this.setState({ useCanvasLayout: evt.target.checked, usePrintLayout: false }); }; - private getLayout = (): Required => { + private getLayout = (): LayoutParams => { const { layout: outerLayout } = this.props.getJobParams(); let dimensions = outerLayout?.dimensions; diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.test.ts b/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.test.ts deleted file mode 100644 index dae692fae8825..0000000000000 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import puppeteer from 'puppeteer'; -import * as Rx from 'rxjs'; -import { take } from 'rxjs/operators'; -import { HeadlessChromiumDriverFactory } from '.'; -import type { ReportingCore } from '../../..'; -import { - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../../test_helpers'; - -jest.mock('puppeteer'); - -const mock = (browserDriverFactory: HeadlessChromiumDriverFactory) => { - browserDriverFactory.getBrowserLogger = jest.fn(() => new Rx.Observable()); - browserDriverFactory.getProcessLogger = jest.fn(() => new Rx.Observable()); - browserDriverFactory.getPageExit = jest.fn(() => new Rx.Observable()); - return browserDriverFactory; -}; - -describe('class HeadlessChromiumDriverFactory', () => { - let reporting: ReportingCore; - const logger = createMockLevelLogger(); - const path = 'path/to/headless_shell'; - - beforeEach(async () => { - (puppeteer as jest.Mocked).launch.mockResolvedValue({ - newPage: jest.fn().mockResolvedValue({ - target: jest.fn(() => ({ - createCDPSession: jest.fn().mockResolvedValue({ - send: jest.fn(), - }), - })), - emulateTimezone: jest.fn(), - setDefaultTimeout: jest.fn(), - }), - close: jest.fn(), - process: jest.fn(), - } as unknown as puppeteer.Browser); - - reporting = await createMockReportingCore( - createMockConfigSchema({ - capture: { - browser: { chromium: { proxy: {} } }, - timeouts: { openUrl: 50000 }, - }, - }) - ); - }); - - it('createPage returns browser driver and process exit observable', async () => { - const factory = mock(new HeadlessChromiumDriverFactory(reporting, path, logger)); - const utils = await factory.createPage({}).pipe(take(1)).toPromise(); - expect(utils).toHaveProperty('driver'); - expect(utils).toHaveProperty('exit$'); - }); - - it('createPage rejects if Puppeteer launch fails', async () => { - (puppeteer as jest.Mocked).launch.mockRejectedValue( - `Puppeteer Launch mock fail.` - ); - const factory = mock(new HeadlessChromiumDriverFactory(reporting, path, logger)); - expect(() => - factory.createPage({}).pipe(take(1)).toPromise() - ).rejects.toThrowErrorMatchingInlineSnapshot( - `"Error spawning Chromium browser! Puppeteer Launch mock fail."` - ); - }); -}); 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 deleted file mode 100644 index 2aef62f59985b..0000000000000 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/index.ts +++ /dev/null @@ -1,268 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { getDataPath } from '@kbn/utils'; -import del from 'del'; -import apm from 'elastic-apm-node'; -import fs from 'fs'; -import path from 'path'; -import puppeteer from 'puppeteer'; -import * as Rx from 'rxjs'; -import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; -import { ignoreElements, map, mergeMap, tap } from 'rxjs/operators'; -import { getChromiumDisconnectedError } from '../'; -import { ReportingCore } from '../../..'; -import { durationToNumber } from '../../../../common/schema_utils'; -import { CaptureConfig } from '../../../../server/types'; -import { LevelLogger } from '../../../lib'; -import { safeChildProcess } from '../../safe_child_process'; -import { HeadlessChromiumDriver } from '../driver'; -import { args } from './args'; -import { getMetrics } from './metrics'; - -type BrowserConfig = CaptureConfig['browser']['chromium']; - -export class HeadlessChromiumDriverFactory { - private binaryPath: string; - private captureConfig: CaptureConfig; - private browserConfig: BrowserConfig; - private userDataDir: string; - private getChromiumArgs: () => string[]; - private core: ReportingCore; - - constructor(core: ReportingCore, binaryPath: string, private logger: LevelLogger) { - this.core = core; - this.binaryPath = binaryPath; - const config = core.getConfig(); - this.captureConfig = config.get('capture'); - this.browserConfig = this.captureConfig.browser.chromium; - - if (this.browserConfig.disableSandbox) { - logger.warning(`Enabling the Chromium sandbox provides an additional layer of protection.`); - } - - this.userDataDir = fs.mkdtempSync(path.join(getDataPath(), 'chromium-')); - this.getChromiumArgs = () => - args({ - userDataDir: this.userDataDir, - disableSandbox: this.browserConfig.disableSandbox, - proxy: this.browserConfig.proxy, - }); - } - - type = 'chromium'; - - /* - * Return an observable to objects which will drive screenshot capture for a page - */ - createPage( - { browserTimezone }: { browserTimezone?: string }, - pLogger = this.logger - ): Rx.Observable<{ driver: HeadlessChromiumDriver; exit$: Rx.Observable }> { - // FIXME: 'create' is deprecated - return Rx.Observable.create(async (observer: InnerSubscriber) => { - const logger = pLogger.clone(['browser-driver']); - logger.info(`Creating browser page driver`); - - const chromiumArgs = this.getChromiumArgs(); - logger.debug(`Chromium launch args set to: ${chromiumArgs}`); - - let browser: puppeteer.Browser | null = null; - - try { - browser = await puppeteer.launch({ - pipe: !this.browserConfig.inspect, - userDataDir: this.userDataDir, - executablePath: this.binaryPath, - ignoreHTTPSErrors: true, - handleSIGHUP: false, - args: chromiumArgs, - env: { - TZ: browserTimezone, - }, - }); - } catch (err) { - observer.error(new Error(`Error spawning Chromium browser! ${err}`)); - return; - } - - const page = await browser.newPage(); - const devTools = await page.target().createCDPSession(); - - await devTools.send('Performance.enable', { timeDomain: 'timeTicks' }); - const startMetrics = await devTools.send('Performance.getMetrics'); - - // Log version info for debugging / maintenance - const versionInfo = await devTools.send('Browser.getVersion'); - logger.debug(`Browser version: ${JSON.stringify(versionInfo)}`); - - await page.emulateTimezone(browserTimezone); - - // Set the default timeout for all navigation methods to the openUrl timeout - // All waitFor methods have their own timeout config passed in to them - page.setDefaultTimeout(durationToNumber(this.captureConfig.timeouts.openUrl)); - - logger.debug(`Browser page driver created`); - - const childProcess = { - async kill() { - try { - if (devTools && startMetrics) { - const endMetrics = await devTools.send('Performance.getMetrics'); - const { cpu, cpuInPercentage, memory, memoryInMegabytes } = getMetrics( - startMetrics, - endMetrics - ); - - apm.currentTransaction?.setLabel('cpu', cpu, false); - apm.currentTransaction?.setLabel('memory', memory, false); - logger.debug( - `Chromium consumed CPU ${cpuInPercentage}% Memory ${memoryInMegabytes}MB` - ); - } - } catch (error) { - logger.error(error); - } - - try { - await browser?.close(); - } catch (err) { - // do not throw - logger.error(err); - } - }, - }; - const { terminate$ } = safeChildProcess(logger, childProcess); - - // this is adding unsubscribe logic to our observer - // so that if our observer unsubscribes, we terminate our child-process - observer.add(() => { - logger.debug(`The browser process observer has unsubscribed. Closing the browser...`); - childProcess.kill(); // ignore async - }); - - // make the observer subscribe to terminate$ - observer.add( - terminate$ - .pipe( - tap((signal) => { - logger.debug(`Termination signal received: ${signal}`); - }), - ignoreElements() - ) - .subscribe(observer) - ); - - // taps the browser log streams and combine them to Kibana logs - this.getBrowserLogger(page, logger).subscribe(); - this.getProcessLogger(browser, logger).subscribe(); - - // HeadlessChromiumDriver: object to "drive" a browser page - const driver = new HeadlessChromiumDriver(this.core, page, { - inspect: !!this.browserConfig.inspect, - networkPolicy: this.captureConfig.networkPolicy, - }); - - // Rx.Observable: stream to interrupt page capture - const exit$ = this.getPageExit(browser, page); - - observer.next({ driver, exit$ }); - - // unsubscribe logic makes a best-effort attempt to delete the user data directory used by chromium - observer.add(() => { - const userDataDir = this.userDataDir; - logger.debug(`deleting chromium user data directory at [${userDataDir}]`); - // the unsubscribe function isn't `async` so we're going to make our best effort at - // deleting the userDataDir and if it fails log an error. - del(userDataDir, { force: true }).catch((error) => { - logger.error(`error deleting user data directory at [${userDataDir}]!`); - logger.error(error); - }); - }); - }); - } - - getBrowserLogger(page: puppeteer.Page, logger: LevelLogger): Rx.Observable { - const consoleMessages$ = Rx.fromEvent(page, 'console').pipe( - map((line) => { - const formatLine = () => `{ text: "${line.text()?.trim()}", url: ${line.location()?.url} }`; - - if (line.type() === 'error') { - logger.error(`Error in browser console: ${formatLine()}`, ['headless-browser-console']); - } else { - logger.debug(`Message in browser console: ${formatLine()}`, [ - `headless-browser-console:${line.type()}`, - ]); - } - }) - ); - - const uncaughtExceptionPageError$ = Rx.fromEvent(page, 'pageerror').pipe( - map((err) => { - logger.warning( - i18n.translate('xpack.reporting.browsers.chromium.pageErrorDetected', { - defaultMessage: `Reporting encountered an uncaught error on the page that will be ignored: {err}`, - values: { err: err.toString() }, - }) - ); - }) - ); - - const pageRequestFailed$ = Rx.fromEvent(page, 'requestfailed').pipe( - map((req) => { - const failure = req.failure && req.failure(); - if (failure) { - logger.warning( - `Request to [${req.url()}] failed! [${failure.errorText}]. This error will be ignored.` - ); - } - }) - ); - - return Rx.merge(consoleMessages$, uncaughtExceptionPageError$, pageRequestFailed$); - } - - getProcessLogger(browser: puppeteer.Browser, logger: LevelLogger): Rx.Observable { - const childProcess = browser.process(); - // NOTE: The browser driver can not observe stdout and stderr of the child process - // Puppeteer doesn't give a handle to the original ChildProcess object - // See https://github.com/GoogleChrome/puppeteer/issues/1292#issuecomment-521470627 - - if (childProcess == null) { - throw new TypeError('childProcess is null or undefined!'); - } - - // just log closing of the process - const processClose$ = Rx.fromEvent(childProcess, 'close').pipe( - tap(() => { - logger.debug('child process closed', ['headless-browser-process']); - }) - ); - - return processClose$; // ideally, this would also merge with observers for stdout and stderr - } - - getPageExit(browser: puppeteer.Browser, page: puppeteer.Page) { - const pageError$ = Rx.fromEvent(page, 'error').pipe( - mergeMap((err) => { - return Rx.throwError( - i18n.translate('xpack.reporting.browsers.chromium.errorDetected', { - defaultMessage: 'Reporting encountered an error: {err}', - values: { err: err.toString() }, - }) - ); - }) - ); - - const browserDisconnect$ = Rx.fromEvent(browser, 'disconnected').pipe( - mergeMap(() => Rx.throwError(getChromiumDisconnectedError())) - ); - - return Rx.merge(pageError$, browserDisconnect$); - } -} 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 deleted file mode 100644 index 1a739488bf6ed..0000000000000 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/start_logs.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { spawn } from 'child_process'; -import del from 'del'; -import { mkdtempSync } from 'fs'; -import { uniq } from 'lodash'; -import os from 'os'; -import { join } from 'path'; -import { createInterface } from 'readline'; -import { getDataPath } from '@kbn/utils'; -import { fromEvent, merge, of, timer } from 'rxjs'; -import { catchError, map, reduce, takeUntil, tap } from 'rxjs/operators'; -import { ReportingCore } from '../../../'; -import { LevelLogger } from '../../../lib'; -import { ChromiumArchivePaths } from '../paths'; -import { args } from './args'; - -const paths = new ChromiumArchivePaths(); -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(getDataPath(), 'chromium-')); - - const platform = process.platform; - const architecture = os.arch(); - const pkg = paths.find(platform, architecture); - if (!pkg) { - throw new Error(`Unsupported platform: ${platform}-${architecture}`); - } - const binaryPath = paths.getBinaryPath(pkg); - - const kbnArgs = args({ - userDataDir, - 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((err) => { - logger.error(`Browser process threw an error on startup`); - logger.error(err as string | Error); - 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/chromium/index.ts b/x-pack/plugins/reporting/server/browsers/chromium/index.ts deleted file mode 100644 index e0d043f821ab4..0000000000000 --- a/x-pack/plugins/reporting/server/browsers/chromium/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; -import { BrowserDownload } from '../'; -import { ReportingCore } from '../../../server'; -import { LevelLogger } from '../../lib'; -import { HeadlessChromiumDriverFactory } from './driver_factory'; -import { ChromiumArchivePaths } from './paths'; - -export const chromium: BrowserDownload = { - paths: new ChromiumArchivePaths(), - createDriverFactory: (core: ReportingCore, binaryPath: string, logger: LevelLogger) => - new HeadlessChromiumDriverFactory(core, binaryPath, logger), -}; - -export const getChromiumDisconnectedError = () => - new Error( - i18n.translate('xpack.reporting.screencapture.browserWasClosed', { - defaultMessage: 'Browser was closed unexpectedly! Check the server logs for more info.', - }) - ); - -export const getDisallowedOutgoingUrlError = (interceptedUrl: string) => - new Error( - i18n.translate('xpack.reporting.chromiumDriver.disallowedOutgoingUrl', { - defaultMessage: `Received disallowed outgoing URL: "{interceptedUrl}". Failing the request and closing the browser.`, - values: { interceptedUrl }, - }) - ); - -export { ChromiumArchivePaths }; diff --git a/x-pack/plugins/reporting/server/browsers/download/download.test.ts b/x-pack/plugins/reporting/server/browsers/download/download.test.ts deleted file mode 100644 index 688a746826e54..0000000000000 --- a/x-pack/plugins/reporting/server/browsers/download/download.test.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { createHash } from 'crypto'; -import del from 'del'; -import { readFileSync } from 'fs'; -import { resolve as resolvePath } from 'path'; -import { Readable } from 'stream'; -import { LevelLogger } from '../../lib'; -import { download } from './download'; - -const TEMP_DIR = resolvePath(__dirname, '__tmp__'); -const TEMP_FILE = resolvePath(TEMP_DIR, 'foo/bar/download'); - -class ReadableOf extends Readable { - constructor(private readonly responseBody: string) { - super(); - } - - _read() { - this.push(this.responseBody); - this.push(null); - } -} - -jest.mock('axios'); -const request: jest.Mock = jest.requireMock('axios').request; - -const mockLogger = { - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), -} as unknown as LevelLogger; - -test('downloads the url to the path', async () => { - const BODY = 'abdcefg'; - request.mockImplementationOnce(async () => { - return { - data: new ReadableOf(BODY), - }; - }); - - await download('url', TEMP_FILE, mockLogger); - expect(readFileSync(TEMP_FILE, 'utf8')).toEqual(BODY); -}); - -test('returns the md5 hex hash of the http body', async () => { - const BODY = 'foobar'; - const HASH = createHash('md5').update(BODY).digest('hex'); - request.mockImplementationOnce(async () => { - return { - data: new ReadableOf(BODY), - }; - }); - - const returned = await download('url', TEMP_FILE, mockLogger); - expect(returned).toEqual(HASH); -}); - -test('throws if request emits an error', async () => { - request.mockImplementationOnce(async () => { - throw new Error('foo'); - }); - - return expect(download('url', TEMP_FILE, mockLogger)).rejects.toThrow('foo'); -}); - -afterEach(async () => await del(TEMP_DIR)); diff --git a/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.test.ts b/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.test.ts deleted file mode 100644 index 9db128c019ac0..0000000000000 --- a/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import path from 'path'; -import mockFs from 'mock-fs'; -import { existsSync, readdirSync } from 'fs'; -import { chromium } from '../chromium'; -import { download } from './download'; -import { md5 } from './checksum'; -import { ensureBrowserDownloaded } from './ensure_downloaded'; -import { LevelLogger } from '../../lib'; - -jest.mock('./checksum'); -jest.mock('./download'); - -// https://github.com/elastic/kibana/issues/115881 -describe.skip('ensureBrowserDownloaded', () => { - let logger: jest.Mocked; - - beforeEach(() => { - logger = { - debug: jest.fn(), - error: jest.fn(), - warning: jest.fn(), - } as unknown as typeof logger; - - (md5 as jest.MockedFunction).mockImplementation( - async (packagePath) => - chromium.paths.packages.find( - (packageInfo) => chromium.paths.resolvePath(packageInfo) === packagePath - )?.archiveChecksum ?? 'some-md5' - ); - - (download as jest.MockedFunction).mockImplementation( - async (_url, packagePath) => - chromium.paths.packages.find( - (packageInfo) => chromium.paths.resolvePath(packageInfo) === packagePath - )?.archiveChecksum ?? 'some-md5' - ); - - mockFs(); - }); - - afterEach(() => { - mockFs.restore(); - jest.resetAllMocks(); - }); - - it('should remove unexpected files', async () => { - const unexpectedPath1 = `${chromium.paths.archivesPath}/unexpected1`; - const unexpectedPath2 = `${chromium.paths.archivesPath}/unexpected2`; - - mockFs({ - [unexpectedPath1]: 'test', - [unexpectedPath2]: 'test', - }); - - await ensureBrowserDownloaded(logger); - - expect(existsSync(unexpectedPath1)).toBe(false); - expect(existsSync(unexpectedPath2)).toBe(false); - }); - - it('should reject when download fails', async () => { - (download as jest.MockedFunction).mockRejectedValueOnce( - new Error('some error') - ); - - await expect(ensureBrowserDownloaded(logger)).rejects.toBeInstanceOf(Error); - }); - - it('should reject when downloaded md5 hash is different', async () => { - (download as jest.MockedFunction).mockResolvedValue('random-md5'); - - await expect(ensureBrowserDownloaded(logger)).rejects.toBeInstanceOf(Error); - }); - - describe('when archives are already present', () => { - beforeEach(() => { - mockFs( - Object.fromEntries( - chromium.paths.packages.map((packageInfo) => [ - chromium.paths.resolvePath(packageInfo), - '', - ]) - ) - ); - }); - - it('should not download again', async () => { - await ensureBrowserDownloaded(logger); - - expect(download).not.toHaveBeenCalled(); - const paths = [ - readdirSync(path.resolve(chromium.paths.archivesPath + '/x64')), - readdirSync(path.resolve(chromium.paths.archivesPath + '/arm64')), - ]; - - expect(paths).toEqual([ - expect.arrayContaining([ - 'chrome-win.zip', - 'chromium-70f5d88-linux_x64.zip', - 'chromium-d163fd7-darwin_x64.zip', - ]), - expect.arrayContaining(['chromium-70f5d88-linux_arm64.zip']), - ]); - }); - - it('should download again if md5 hash different', async () => { - (md5 as jest.MockedFunction).mockResolvedValueOnce('random-md5'); - await ensureBrowserDownloaded(logger); - - expect(download).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts b/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts deleted file mode 100644 index 2766b404f1dd1..0000000000000 --- a/x-pack/plugins/reporting/server/browsers/download/ensure_downloaded.ts +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { existsSync } from 'fs'; -import del from 'del'; -import { BrowserDownload, chromium } from '../'; -import { GenericLevelLogger } from '../../lib/level_logger'; -import { md5 } from './checksum'; -import { download } from './download'; - -/** - * Check for the downloaded archive of each requested browser type and - * download them if they are missing or their checksum is invalid - */ -export async function ensureBrowserDownloaded(logger: GenericLevelLogger) { - await ensureDownloaded([chromium], logger); -} - -/** - * Clears the unexpected files in the browsers archivesPath - * and ensures that all packages/archives are downloaded and - * that their checksums match the declared value - */ -async function ensureDownloaded(browsers: BrowserDownload[], logger: GenericLevelLogger) { - await Promise.all( - browsers.map(async ({ paths: pSet }) => { - const removedFiles = await del(`${pSet.archivesPath}/**/*`, { - force: true, - onlyFiles: true, - ignore: pSet.getAllArchiveFilenames(), - }); - - removedFiles.forEach((path) => { - logger.warning(`Deleting unexpected file ${path}`); - }); - - const invalidChecksums: string[] = []; - await Promise.all( - pSet.packages.map(async (p) => { - const { archiveFilename, archiveChecksum } = p; - if (archiveFilename && archiveChecksum) { - const path = pSet.resolvePath(p); - const pathExists = existsSync(path); - - let foundChecksum: string; - try { - foundChecksum = await md5(path).catch(); - } catch { - foundChecksum = 'MISSING'; - } - - if (pathExists && foundChecksum === archiveChecksum) { - logger.debug(`Browser archive for ${p.platform}/${p.architecture} found in ${path} `); - return; - } - - if (!pathExists) { - logger.warning( - `Browser archive for ${p.platform}/${p.architecture} not found in ${path}.` - ); - } - if (foundChecksum !== archiveChecksum) { - logger.warning( - `Browser archive checksum for ${p.platform}/${p.architecture} ` + - `is ${foundChecksum} but ${archiveChecksum} was expected.` - ); - } - - const url = pSet.getDownloadUrl(p); - try { - const downloadedChecksum = await download(url, path, logger); - if (downloadedChecksum !== archiveChecksum) { - logger.warning( - `Invalid checksum for ${p.platform}/${p.architecture}: ` + - `expected ${archiveChecksum} got ${downloadedChecksum}` - ); - invalidChecksums.push(`${url} => ${path}`); - } - } catch (err) { - throw new Error(`Failed to download ${url}: ${err}`); - } - } - }) - ); - - if (invalidChecksums.length) { - const err = new Error( - `Error downloading browsers, checksums incorrect for:\n - ${invalidChecksums.join( - '\n - ' - )}` - ); - logger.error(err); - throw err; - } - }) - ); -} diff --git a/x-pack/plugins/reporting/server/browsers/index.ts b/x-pack/plugins/reporting/server/browsers/index.ts deleted file mode 100644 index be5c85a6e9581..0000000000000 --- a/x-pack/plugins/reporting/server/browsers/index.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { first } from 'rxjs/operators'; -import { ReportingCore } from '../'; -import { LevelLogger } from '../lib'; -import { chromium, ChromiumArchivePaths } from './chromium'; -import { HeadlessChromiumDriverFactory } from './chromium/driver_factory'; -import { installBrowser } from './install'; - -export { chromium } from './chromium'; -export { HeadlessChromiumDriver } from './chromium/driver'; -export { HeadlessChromiumDriverFactory } from './chromium/driver_factory'; - -type CreateDriverFactory = ( - core: ReportingCore, - binaryPath: string, - logger: LevelLogger -) => HeadlessChromiumDriverFactory; - -export interface BrowserDownload { - createDriverFactory: CreateDriverFactory; - paths: ChromiumArchivePaths; -} - -export const initializeBrowserDriverFactory = async (core: ReportingCore, logger: LevelLogger) => { - const chromiumLogger = logger.clone(['chromium']); - const { binaryPath$ } = installBrowser(chromiumLogger); - const binaryPath = await binaryPath$.pipe(first()).toPromise(); - return chromium.createDriverFactory(core, binaryPath, chromiumLogger); -}; diff --git a/x-pack/plugins/reporting/server/browsers/install.ts b/x-pack/plugins/reporting/server/browsers/install.ts deleted file mode 100644 index 0441bbcfb5306..0000000000000 --- a/x-pack/plugins/reporting/server/browsers/install.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import del from 'del'; -import os from 'os'; -import path from 'path'; -import * as Rx from 'rxjs'; -import { GenericLevelLogger } from '../lib/level_logger'; -import { ChromiumArchivePaths } from './chromium'; -import { ensureBrowserDownloaded } from './download'; -import { md5 } from './download/checksum'; -import { extract } from './extract'; - -/** - * "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 - */ -export function installBrowser( - logger: GenericLevelLogger, - chromiumPath: string = path.resolve(__dirname, '../../chromium'), - platform: string = process.platform, - architecture: string = os.arch() -): { binaryPath$: Rx.Subject } { - const binaryPath$ = new Rx.Subject(); - - const paths = new ChromiumArchivePaths(); - const pkg = paths.find(platform, architecture); - - if (!pkg) { - throw new Error(`Unsupported platform: ${platform}-${architecture}`); - } - - const backgroundInstall = async () => { - const binaryPath = paths.getBinaryPath(pkg); - const binaryChecksum = await md5(binaryPath).catch(() => ''); - - if (binaryChecksum !== pkg.binaryChecksum) { - logger.warning( - `Found browser binary checksum for ${pkg.platform}/${pkg.architecture} ` + - `is ${binaryChecksum} but ${pkg.binaryChecksum} was expected. Re-installing...` - ); - try { - await del(chromiumPath); - } catch (err) { - logger.error(err); - } - - try { - await ensureBrowserDownloaded(logger); - const archive = path.join(paths.archivesPath, pkg.architecture, pkg.archiveFilename); - logger.info(`Extracting [${archive}] to [${chromiumPath}]`); - await extract(archive, chromiumPath); - } catch (err) { - logger.error(err); - } - } - - logger.info(`Browser executable: ${binaryPath}`); - - binaryPath$.next(binaryPath); // subscribers wait for download and extract to complete - }; - - backgroundInstall(); - - return { - binaryPath$, - }; -} diff --git a/x-pack/plugins/reporting/server/config/__snapshots__/schema.test.ts.snap b/x-pack/plugins/reporting/server/config/__snapshots__/schema.test.ts.snap index a384550f18462..65f3c45fb2255 100644 --- a/x-pack/plugins/reporting/server/config/__snapshots__/schema.test.ts.snap +++ b/x-pack/plugins/reporting/server/config/__snapshots__/schema.test.ts.snap @@ -3,52 +3,8 @@ exports[`Reporting Config Schema context {"dev":false,"dist":false} produces correct config 1`] = ` Object { "capture": Object { - "browser": Object { - "autoDownload": true, - "chromium": Object { - "proxy": Object { - "enabled": false, - }, - }, - "type": "chromium", - }, "loadDelay": "PT3S", "maxAttempts": 1, - "networkPolicy": Object { - "enabled": true, - "rules": Array [ - Object { - "allow": true, - "host": undefined, - "protocol": "http:", - }, - Object { - "allow": true, - "host": undefined, - "protocol": "https:", - }, - Object { - "allow": true, - "host": undefined, - "protocol": "ws:", - }, - Object { - "allow": true, - "host": undefined, - "protocol": "wss:", - }, - Object { - "allow": true, - "host": undefined, - "protocol": "data:", - }, - Object { - "allow": false, - "host": undefined, - "protocol": undefined, - }, - ], - }, "timeouts": Object { "openUrl": "PT1M", "renderComplete": "PT30S", @@ -101,53 +57,8 @@ Object { exports[`Reporting Config Schema context {"dev":false,"dist":true} produces correct config 1`] = ` Object { "capture": Object { - "browser": Object { - "autoDownload": false, - "chromium": Object { - "inspect": false, - "proxy": Object { - "enabled": false, - }, - }, - "type": "chromium", - }, "loadDelay": "PT3S", "maxAttempts": 3, - "networkPolicy": Object { - "enabled": true, - "rules": Array [ - Object { - "allow": true, - "host": undefined, - "protocol": "http:", - }, - Object { - "allow": true, - "host": undefined, - "protocol": "https:", - }, - Object { - "allow": true, - "host": undefined, - "protocol": "ws:", - }, - Object { - "allow": true, - "host": undefined, - "protocol": "wss:", - }, - Object { - "allow": true, - "host": undefined, - "protocol": "data:", - }, - Object { - "allow": false, - "host": undefined, - "protocol": undefined, - }, - ], - }, "timeouts": Object { "openUrl": "PT1M", "renderComplete": "PT30S", diff --git a/x-pack/plugins/reporting/server/config/create_config.test.ts b/x-pack/plugins/reporting/server/config/create_config.test.ts index 3c5ecdc1dab0b..fd8180bd46a05 100644 --- a/x-pack/plugins/reporting/server/config/create_config.test.ts +++ b/x-pack/plugins/reporting/server/config/create_config.test.ts @@ -77,13 +77,6 @@ describe('Reporting server createConfig$', () => { expect(result).toMatchInlineSnapshot(` Object { - "capture": Object { - "browser": Object { - "chromium": Object { - "disableSandbox": true, - }, - }, - }, "csv": Object {}, "encryptionKey": "iiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiiii", "index": ".reporting", @@ -106,47 +99,6 @@ describe('Reporting server createConfig$', () => { expect(mockLogger.warn).not.toHaveBeenCalled(); }); - it('uses user-provided disableSandbox: false', async () => { - mockInitContext = coreMock.createPluginInitializerContext( - createMockConfigSchema({ - encryptionKey: '888888888888888888888888888888888', - capture: { browser: { chromium: { disableSandbox: false } } }, - }) - ); - const mockConfig$ = createMockConfig(mockInitContext); - const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise(); - - expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: false }); - expect(mockLogger.warn).not.toHaveBeenCalled(); - }); - - it('uses user-provided disableSandbox: true', async () => { - mockInitContext = coreMock.createPluginInitializerContext( - createMockConfigSchema({ - encryptionKey: '888888888888888888888888888888888', - capture: { browser: { chromium: { disableSandbox: true } } }, - }) - ); - const mockConfig$ = createMockConfig(mockInitContext); - const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise(); - - expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: true }); - expect(mockLogger.warn).not.toHaveBeenCalled(); - }); - - it('provides a default for disableSandbox', async () => { - mockInitContext = coreMock.createPluginInitializerContext( - createMockConfigSchema({ - encryptionKey: '888888888888888888888888888888888', - }) - ); - const mockConfig$ = createMockConfig(mockInitContext); - const result = await createConfig$(mockCoreSetup, mockConfig$, mockLogger).toPromise(); - - expect(result.capture.browser.chromium).toMatchObject({ disableSandbox: expect.any(Boolean) }); - expect(mockLogger.warn).not.toHaveBeenCalled(); - }); - it.each(['0', '0.0', '0.0.0', '0.0.0.0', '0000:0000:0000:0000:0000:0000:0000:0000', '::'])( `apply failover logic when hostname is given as "%s"`, async (hostname) => { diff --git a/x-pack/plugins/reporting/server/config/create_config.ts b/x-pack/plugins/reporting/server/config/create_config.ts index 5de54a43582ab..2ac225ec4576a 100644 --- a/x-pack/plugins/reporting/server/config/create_config.ts +++ b/x-pack/plugins/reporting/server/config/create_config.ts @@ -7,17 +7,15 @@ import crypto from 'crypto'; import ipaddr from 'ipaddr.js'; -import { sum, upperFirst } from 'lodash'; +import { sum } from 'lodash'; import { Observable } from 'rxjs'; -import { map, mergeMap } from 'rxjs/operators'; +import { map } from 'rxjs/operators'; import { CoreSetup } from 'src/core/server'; import { LevelLogger } from '../lib'; -import { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled'; import { ReportingConfigType } from './schema'; /* * Set up dynamic config defaults - * - xpack.capture.browser.chromium.disableSandbox * - xpack.kibanaServer * - xpack.reporting.encryptionKey */ @@ -71,41 +69,6 @@ export function createConfig$( protocol: kibanaServerProtocol, }, }; - }), - mergeMap(async (config) => { - if (config.capture.browser.chromium.disableSandbox != null) { - // disableSandbox was set by user - return { ...config }; - } - - // disableSandbox was not set by user, apply default for OS - const { os, disableSandbox } = await getDefaultChromiumSandboxDisabled(); - const osName = [os.os, os.dist, os.release].filter(Boolean).map(upperFirst).join(' '); - - logger.debug(`Running on OS: '{osName}'`); - - if (disableSandbox === true) { - logger.warn( - `Chromium sandbox provides an additional layer of protection, but is not supported for ${osName} OS.` + - ` Automatically setting 'xpack.reporting.capture.browser.chromium.disableSandbox: true'.` - ); - } else { - logger.info( - `Chromium sandbox provides an additional layer of protection, and is supported for ${osName} OS.` + - ` Automatically enabling Chromium sandbox.` - ); - } - - return { - ...config, - capture: { - ...config.capture, - browser: { - ...config.capture.browser, - chromium: { ...config.capture.browser.chromium, disableSandbox }, - }, - }, - }; }) ); } diff --git a/x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.test.ts b/x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.test.ts deleted file mode 100644 index 6ca75b7a1701b..0000000000000 --- a/x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -jest.mock('getos', () => { - return jest.fn(); -}); - -import { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled'; -import getos from 'getos'; - -interface TestObject { - os: string; - dist?: string; - release?: string; -} - -function defaultTest(os: TestObject, expectedDefault: boolean) { - test(`${expectedDefault ? 'disabled' : 'enabled'} on ${JSON.stringify(os)}`, async () => { - (getos as jest.Mock).mockImplementation((cb) => cb(null, os)); - const actualDefault = await getDefaultChromiumSandboxDisabled(); - expect(actualDefault.disableSandbox).toBe(expectedDefault); - }); -} - -defaultTest({ os: 'win32' }, false); -defaultTest({ os: 'darwin' }, false); -defaultTest({ os: 'linux', dist: 'Centos', release: '7.0' }, true); -defaultTest({ os: 'linux', dist: 'Red Hat Linux', release: '7.0' }, true); -defaultTest({ os: 'linux', dist: 'Ubuntu Linux', release: '14.04' }, false); -defaultTest({ os: 'linux', dist: 'Ubuntu Linux', release: '16.04' }, false); -defaultTest({ os: 'linux', dist: 'SUSE Linux', release: '11' }, false); -defaultTest({ os: 'linux', dist: 'SUSE Linux', release: '12' }, false); -defaultTest({ os: 'linux', dist: 'SUSE Linux', release: '42.0' }, false); -defaultTest({ os: 'linux', dist: 'Debian', release: '8' }, true); -defaultTest({ os: 'linux', dist: 'Debian', release: '9' }, true); diff --git a/x-pack/plugins/reporting/server/config/index.ts b/x-pack/plugins/reporting/server/config/index.ts index 711e930484e01..963895d1fe583 100644 --- a/x-pack/plugins/reporting/server/config/index.ts +++ b/x-pack/plugins/reporting/server/config/index.ts @@ -19,6 +19,7 @@ export const config: PluginConfigDescriptor = { schema: ConfigSchema, deprecations: ({ unused }) => [ unused('capture.browser.chromium.maxScreenshotDimension', { level: 'warning' }), // unused since 7.8 + unused('capture.browser.type'), unused('poll.jobCompletionNotifier.intervalErrorMultiplier', { level: 'warning' }), // unused since 7.10 unused('poll.jobsRefresh.intervalErrorMultiplier', { level: 'warning' }), // unused since 7.10 unused('capture.viewport', { level: 'warning' }), // deprecated as unused since 7.16 @@ -72,7 +73,6 @@ export const config: PluginConfigDescriptor = { capture: { maxAttempts: true, timeouts: { openUrl: true, renderComplete: true, waitForElements: true }, - networkPolicy: false, // show as [redacted] zoom: true, }, csv: { maxSizeBytes: true, scroll: { size: true, duration: true } }, diff --git a/x-pack/plugins/reporting/server/config/schema.test.ts b/x-pack/plugins/reporting/server/config/schema.test.ts index c49490be87a15..3af7a4e5cfe4c 100644 --- a/x-pack/plugins/reporting/server/config/schema.test.ts +++ b/x-pack/plugins/reporting/server/config/schema.test.ts @@ -55,47 +55,12 @@ describe('Reporting Config Schema', () => { ).toBe('qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq'); expect(ConfigSchema.validate({ encryptionKey: 'weaksauce' }).encryptionKey).toBe('weaksauce'); - - // disableSandbox - expect( - ConfigSchema.validate({ capture: { browser: { chromium: { disableSandbox: true } } } }) - .capture.browser.chromium - ).toMatchObject({ disableSandbox: true, proxy: { enabled: false } }); - // kibanaServer expect( ConfigSchema.validate({ kibanaServer: { hostname: 'Frodo' } }).kibanaServer ).toMatchObject({ hostname: 'Frodo' }); }); - it('allows setting a wildcard for chrome proxy bypass', () => { - expect( - ConfigSchema.validate({ - capture: { - browser: { - chromium: { - proxy: { - enabled: true, - server: 'http://example.com:8080', - bypass: ['*.example.com', '*bar.example.com', 'bats.example.com'], - }, - }, - }, - }, - }).capture.browser.chromium.proxy - ).toMatchInlineSnapshot(` - Object { - "bypass": Array [ - "*.example.com", - "*bar.example.com", - "bats.example.com", - ], - "enabled": true, - "server": "http://example.com:8080", - } - `); - }); - it.each(['0', '0.0', '0.0.0'])( `fails to validate "kibanaServer.hostname" with an invalid hostname: "%s"`, (address) => { diff --git a/x-pack/plugins/reporting/server/config/schema.ts b/x-pack/plugins/reporting/server/config/schema.ts index 4c56fc4c6db60..c031ed4f94f9d 100644 --- a/x-pack/plugins/reporting/server/config/schema.ts +++ b/x-pack/plugins/reporting/server/config/schema.ts @@ -46,20 +46,6 @@ const QueueSchema = schema.object({ }), }); -const RulesSchema = schema.object({ - allow: schema.boolean(), - host: schema.maybe(schema.string()), - protocol: schema.maybe( - schema.string({ - validate(value) { - if (!/:$/.test(value)) { - return 'must end in colon'; - } - }, - }) - ), -}); - const CaptureSchema = schema.object({ timeouts: schema.object({ openUrl: schema.oneOf([schema.number(), schema.duration()], { @@ -72,56 +58,10 @@ const CaptureSchema = schema.object({ defaultValue: moment.duration({ seconds: 30 }), }), }), - networkPolicy: schema.object({ - enabled: schema.boolean({ defaultValue: true }), - rules: schema.arrayOf(RulesSchema, { - defaultValue: [ - { host: undefined, allow: true, protocol: 'http:' }, - { host: undefined, allow: true, protocol: 'https:' }, - { host: undefined, allow: true, protocol: 'ws:' }, - { host: undefined, allow: true, protocol: 'wss:' }, - { host: undefined, allow: true, protocol: 'data:' }, - { host: undefined, allow: false, protocol: undefined }, // Default action is to deny! - ], - }), - }), zoom: schema.number({ defaultValue: 2 }), loadDelay: schema.oneOf([schema.number(), schema.duration()], { defaultValue: moment.duration({ seconds: 3 }), }), - browser: schema.object({ - autoDownload: schema.conditional( - schema.contextRef('dist'), - true, - schema.boolean({ defaultValue: false }), - schema.boolean({ defaultValue: true }) - ), - chromium: schema.object({ - inspect: schema.conditional( - schema.contextRef('dist'), - true, - schema.boolean({ defaultValue: false }), - schema.maybe(schema.never()) - ), - disableSandbox: schema.maybe(schema.boolean()), // default value is dynamic in createConfig$ - proxy: schema.object({ - enabled: schema.boolean({ defaultValue: false }), - server: schema.conditional( - schema.siblingRef('enabled'), - true, - schema.uri({ scheme: ['http', 'https'] }), - schema.maybe(schema.never()) - ), - bypass: schema.conditional( - schema.siblingRef('enabled'), - true, - schema.arrayOf(schema.string()), - schema.maybe(schema.never()) - ), - }), - }), - type: schema.string({ defaultValue: 'chromium' }), - }), maxAttempts: schema.conditional( schema.contextRef('dist'), true, diff --git a/x-pack/plugins/reporting/server/core.ts b/x-pack/plugins/reporting/server/core.ts index 43aefb73aebb9..63900db4016b5 100644 --- a/x-pack/plugins/reporting/server/core.ts +++ b/x-pack/plugins/reporting/server/core.ts @@ -7,8 +7,8 @@ import Hapi from '@hapi/hapi'; import * as Rx from 'rxjs'; -import { filter, first, map, take } from 'rxjs/operators'; -import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; +import { filter, first, map, switchMap, take } from 'rxjs/operators'; +import type { ScreenshottingStart, ScreenshotResult } from '../../screenshotting/server'; import { BasePath, IClusterClient, @@ -28,13 +28,14 @@ import { SecurityPluginSetup } from '../../security/server'; import { DEFAULT_SPACE_ID } from '../../spaces/common/constants'; import { SpacesPluginSetup } from '../../spaces/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; +import { REPORTING_REDIRECT_LOCATOR_STORE_KEY } from '../common/constants'; +import { durationToNumber } from '../common/schema_utils'; import { ReportingConfig, ReportingSetup } from './'; -import { HeadlessChromiumDriverFactory } from './browsers/chromium/driver_factory'; import { ReportingConfigType } from './config'; import { checkLicense, getExportTypesRegistry, LevelLogger } from './lib'; import { ReportingStore } from './lib/store'; import { ExecuteReportTask, MonitorReportsTask, ReportTaskParams } from './lib/tasks'; -import { ReportingPluginRouter } from './types'; +import { ReportingPluginRouter, ScreenshotOptions } from './types'; export interface ReportingInternalSetup { basePath: Pick; @@ -44,13 +45,11 @@ export interface ReportingInternalSetup { security?: SecurityPluginSetup; spaces?: SpacesPluginSetup; taskManager: TaskManagerSetupContract; - screenshotMode: ScreenshotModePluginSetup; logger: LevelLogger; status: StatusServiceSetup; } export interface ReportingInternalStart { - browserDriverFactory: HeadlessChromiumDriverFactory; store: ReportingStore; savedObjects: SavedObjectsServiceStart; uiSettings: UiSettingsServiceStart; @@ -58,6 +57,7 @@ export interface ReportingInternalStart { data: DataPluginStart; taskManager: TaskManagerStartContract; logger: LevelLogger; + screenshotting: ScreenshottingStart; } export class ReportingCore { @@ -253,18 +253,6 @@ export class ReportingCore { .toPromise(); } - private getScreenshotModeDep() { - return this.getPluginSetupDeps().screenshotMode; - } - - public getEnableScreenshotMode() { - return this.getScreenshotModeDep().setScreenshotModeEnabled; - } - - public getSetScreenshotLayout() { - return this.getScreenshotModeDep().setScreenshotLayout; - } - /* * Gives synchronous access to the setupDeps */ @@ -350,6 +338,35 @@ export class ReportingCore { return startDeps.esClient; } + public getScreenshots(options: ScreenshotOptions): Rx.Observable { + return Rx.defer(() => this.getPluginStartDeps()).pipe( + switchMap(({ screenshotting }) => { + const config = this.getConfig(); + return screenshotting.getScreenshots({ + ...options, + + timeouts: { + loadDelay: durationToNumber(config.get('capture', 'loadDelay')), + openUrl: durationToNumber(config.get('capture', 'timeouts', 'openUrl')), + waitForElements: durationToNumber(config.get('capture', 'timeouts', 'waitForElements')), + renderComplete: durationToNumber(config.get('capture', 'timeouts', 'renderComplete')), + }, + + layout: { + zoom: config.get('capture', 'zoom'), + ...options.layout, + }, + + urls: options.urls.map((url) => + typeof url === 'string' + ? url + : [url[0], { [REPORTING_REDIRECT_LOCATOR_STORE_KEY]: url[1] }] + ), + }); + }) + ); + } + public trackReport(reportId: string) { this.executing.add(reportId); } diff --git a/x-pack/plugins/reporting/server/export_types/common/generate_png.ts b/x-pack/plugins/reporting/server/export_types/common/generate_png.ts index c5e70a6c93eff..8c83e0ae73527 100644 --- a/x-pack/plugins/reporting/server/export_types/common/generate_png.ts +++ b/x-pack/plugins/reporting/server/export_types/common/generate_png.ts @@ -8,70 +8,60 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; import { finalize, map, tap } from 'rxjs/operators'; +import { LayoutTypes } from '../../../../screenshotting/common'; import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; import { ReportingCore } from '../../'; -import { UrlOrUrlLocatorTuple } from '../../../common/types'; +import { ScreenshotOptions } from '../../types'; import { LevelLogger } from '../../lib'; -import { LayoutParams, LayoutSelectorDictionary, PreserveLayout } from '../../lib/layouts'; -import { getScreenshots$, ScreenshotResults } from '../../lib/screenshots'; -import { ConditionalHeaders } from '../common'; -export async function generatePngObservableFactory(reporting: ReportingCore) { - const config = reporting.getConfig(); - const captureConfig = config.get('capture'); - const { browserDriverFactory } = await reporting.getPluginStartDeps(); - - return function generatePngObservable( - logger: LevelLogger, - urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple, - browserTimezone: string | undefined, - conditionalHeaders: ConditionalHeaders, - layoutParams: LayoutParams & { selectors?: Partial } - ): Rx.Observable<{ buffer: Buffer; warnings: string[] }> { - const apmTrans = apm.startTransaction('generate-png', REPORTING_TRANSACTION_TYPE); - const apmLayout = apmTrans?.startSpan('create-layout', 'setup'); - if (!layoutParams || !layoutParams.dimensions) { - throw new Error(`LayoutParams.Dimensions is undefined.`); - } - const layout = new PreserveLayout(layoutParams.dimensions, layoutParams.selectors); +export function generatePngObservable( + reporting: ReportingCore, + logger: LevelLogger, + options: ScreenshotOptions +): Rx.Observable<{ buffer: Buffer; warnings: string[] }> { + const apmTrans = apm.startTransaction('generate-png', REPORTING_TRANSACTION_TYPE); + const apmLayout = apmTrans?.startSpan('create-layout', 'setup'); + if (!options.layout.dimensions) { + throw new Error(`LayoutParams.Dimensions is undefined.`); + } + const layout = { + id: LayoutTypes.PRESERVE_LAYOUT, + ...options.layout, + }; - if (apmLayout) apmLayout.end(); + apmLayout?.end(); - const apmScreenshots = apmTrans?.startSpan('screenshots-pipeline', 'setup'); - let apmBuffer: typeof apm.currentSpan; - const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, { - logger, - urlsOrUrlLocatorTuples: [urlOrUrlLocatorTuple], - conditionalHeaders, - layout, - browserTimezone, - }).pipe( - tap(() => { - apmScreenshots?.end(); - apmBuffer = apmTrans?.startSpan('get-buffer', 'output') ?? null; - }), - map((results: ScreenshotResults[]) => ({ - buffer: results[0].screenshots[0].data, - warnings: results.reduce((found, current) => { - if (current.error) { - found.push(current.error.message); - } - if (current.renderErrors) { - found.push(...current.renderErrors); - } - return found; - }, [] as string[]), - })), - tap(({ buffer }) => { - logger.debug(`PNG buffer byte length: ${buffer.byteLength}`); - apmTrans?.setLabel('byte-length', buffer.byteLength, false); - }), - finalize(() => { - apmBuffer?.end(); - apmTrans?.end(); - }) - ); + const apmScreenshots = apmTrans?.startSpan('screenshots-pipeline', 'setup'); + let apmBuffer: typeof apm.currentSpan; - return screenshots$; - }; + return reporting.getScreenshots({ ...options, layout }).pipe( + tap(({ metrics$ }) => { + metrics$.subscribe(({ cpu, memory }) => { + apmTrans?.setLabel('cpu', cpu, false); + apmTrans?.setLabel('memory', memory, false); + }); + apmScreenshots?.end(); + apmBuffer = apmTrans?.startSpan('get-buffer', 'output') ?? null; + }), + map(({ results }) => ({ + buffer: results[0].screenshots[0].data, + warnings: results.reduce((found, current) => { + if (current.error) { + found.push(current.error.message); + } + if (current.renderErrors) { + found.push(...current.renderErrors); + } + return found; + }, [] as string[]), + })), + tap(({ buffer }) => { + logger.debug(`PNG buffer byte length: ${buffer.byteLength}`); + apmTrans?.setLabel('byte-length', buffer.byteLength, false); + }), + finalize(() => { + apmBuffer?.end(); + apmTrans?.end(); + }) + ); } diff --git a/x-pack/plugins/reporting/server/export_types/common/index.ts b/x-pack/plugins/reporting/server/export_types/common/index.ts index c35dcb5344e21..501de48e0450a 100644 --- a/x-pack/plugins/reporting/server/export_types/common/index.ts +++ b/x-pack/plugins/reporting/server/export_types/common/index.ts @@ -10,7 +10,7 @@ export { getConditionalHeaders } from './get_conditional_headers'; export { getFullUrls } from './get_full_urls'; export { omitBlockedHeaders } from './omit_blocked_headers'; export { validateUrls } from './validate_urls'; -export { generatePngObservableFactory } from './generate_png'; +export { generatePngObservable } from './generate_png'; export { getCustomLogo } from './get_custom_logo'; export interface TimeRangeParams { diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/get_template.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/get_template.ts index 58ddeb51e7a4f..0c7fedc8f7b7e 100644 --- a/x-pack/plugins/reporting/server/export_types/common/pdf/get_template.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/get_template.ts @@ -13,12 +13,12 @@ import { StyleDictionary, TDocumentDefinitions, } from 'pdfmake/interfaces'; -import { LayoutInstance } from '../../../lib/layouts'; +import type { Layout } from '../../../../../screenshotting/server'; import { REPORTING_TABLE_LAYOUT } from './get_doc_options'; import { getFont } from './get_font'; export function getTemplate( - layout: LayoutInstance, + layout: Layout, logo: string | undefined, title: string, tableBorderWidth: number, diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/index.test.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/index.test.ts index 74a247d4568ab..2df98c6c79357 100644 --- a/x-pack/plugins/reporting/server/export_types/common/pdf/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/index.test.ts @@ -5,8 +5,7 @@ * 2.0. */ -import { PreserveLayout, PrintLayout } from '../../../lib/layouts'; -import { createMockConfig, createMockConfigSchema } from '../../../test_helpers'; +import { createMockLayout } from '../../../../../screenshotting/server/layouts/mock'; import { PdfMaker } from './'; const imageBase64 = Buffer.from( @@ -16,66 +15,22 @@ const imageBase64 = Buffer.from( // FLAKY: https://github.com/elastic/kibana/issues/118484 describe.skip('PdfMaker', () => { - it('makes PDF using PrintLayout mode', async () => { - const config = createMockConfig(createMockConfigSchema()); - const layout = new PrintLayout(config.get('capture')); - const pdf = new PdfMaker(layout, undefined); + let layout: ReturnType; + let pdf: PdfMaker; - expect(pdf.setTitle('the best PDF in the world')).toBe(undefined); - expect([ - pdf.addImage(imageBase64, { title: 'first viz', description: '☃️' }), - pdf.addImage(imageBase64, { title: 'second viz', description: '❄️' }), - ]).toEqual([undefined, undefined]); - - const { _layout: testLayout, _title: testTitle } = pdf as unknown as { - _layout: object; - _title: string; - }; - expect(testLayout).toMatchObject({ - captureConfig: { browser: { chromium: { disableSandbox: true } } }, // NOTE: irrelevant data? - groupCount: 2, - id: 'print', - selectors: { - itemsCountAttribute: 'data-shared-items-count', - renderComplete: '[data-shared-item]', - screenshot: '[data-shared-item]', - timefilterDurationAttribute: 'data-shared-timefilter-duration', - }, - }); - expect(testTitle).toBe('the best PDF in the world'); - - // generate buffer - pdf.generate(); - const result = await pdf.getBuffer(); - expect(Buffer.isBuffer(result)).toBe(true); + beforeEach(() => { + layout = createMockLayout(); + pdf = new PdfMaker(layout, undefined); }); - it('makes PDF using PreserveLayout mode', async () => { - const layout = new PreserveLayout({ width: 400, height: 300 }); - const pdf = new PdfMaker(layout, undefined); + describe('getBuffer', () => { + it('should generate PDF buffer', async () => { + pdf.setTitle('the best PDF in the world'); + pdf.addImage(imageBase64, { title: 'first viz', description: '☃️' }); + pdf.addImage(imageBase64, { title: 'second viz', description: '❄️' }); + pdf.generate(); - expect(pdf.setTitle('the finest PDF in the world')).toBe(undefined); - expect(pdf.addImage(imageBase64, { title: 'cool times', description: '☃️' })).toBe(undefined); - - const { _layout: testLayout, _title: testTitle } = pdf as unknown as { - _layout: object; - _title: string; - }; - expect(testLayout).toMatchObject({ - groupCount: 1, - id: 'preserve_layout', - selectors: { - itemsCountAttribute: 'data-shared-items-count', - renderComplete: '[data-shared-item]', - screenshot: '[data-shared-items-container]', - timefilterDurationAttribute: 'data-shared-timefilter-duration', - }, + await expect(pdf.getBuffer()).resolves.toBeInstanceOf(Buffer); }); - expect(testTitle).toBe('the finest PDF in the world'); - - // generate buffer - pdf.generate(); - const result = await pdf.getBuffer(); - expect(Buffer.isBuffer(result)).toBe(true); }); }); diff --git a/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts b/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts index 0cd054d3e3709..d6c0ec9dd844c 100644 --- a/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts +++ b/x-pack/plugins/reporting/server/export_types/common/pdf/index.ts @@ -12,7 +12,7 @@ import _ from 'lodash'; import path from 'path'; import Printer from 'pdfmake'; import { Content, ContentImage, ContentText } from 'pdfmake/interfaces'; -import { LayoutInstance } from '../../../lib/layouts'; +import type { Layout } from '../../../../../screenshotting/server'; import { getDocOptions, REPORTING_TABLE_LAYOUT } from './get_doc_options'; import { getFont } from './get_font'; import { getTemplate } from './get_template'; @@ -21,14 +21,14 @@ const assetPath = path.resolve(__dirname, '..', '..', 'common', 'assets'); const tableBorderWidth = 1; export class PdfMaker { - private _layout: LayoutInstance; + private _layout: Layout; private _logo: string | undefined; private _title: string; private _content: Content[]; private _printer: Printer; private _pdfDoc: PDFKit.PDFDocument | undefined; - constructor(layout: LayoutInstance, logo: string | undefined) { + constructor(layout: Layout, logo: string | undefined) { const fontPath = (filename: string) => path.resolve(assetPath, 'fonts', filename); const fonts = { Roboto: { diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts index ed4709d501b43..7356da4da3a11 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.test.ts @@ -15,11 +15,11 @@ import { createMockConfigSchema, createMockReportingCore, } from '../../../test_helpers'; -import { generatePngObservableFactory } from '../../common'; +import { generatePngObservable } from '../../common'; import { TaskPayloadPNG } from '../types'; import { runTaskFnFactory } from './'; -jest.mock('../../common/generate_png', () => ({ generatePngObservableFactory: jest.fn() })); +jest.mock('../../common/generate_png'); let content: string; let mockReporting: ReportingCore; @@ -61,16 +61,13 @@ beforeEach(async () => { mockReporting = await createMockReportingCore(mockReportingConfig); mockReporting.setConfig(createMockConfig(mockReportingConfig)); - - (generatePngObservableFactory as jest.Mock).mockReturnValue(jest.fn()); }); -afterEach(() => (generatePngObservableFactory as jest.Mock).mockReset()); +afterEach(() => (generatePngObservable as jest.Mock).mockReset()); test(`passes browserTimezone to generatePng`, async () => { const encryptedHeaders = await encryptHeaders({}); - const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock; - generatePngObservable.mockReturnValue(Rx.of({ buffer: Buffer.from('') })); + (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('') })); const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); const browserTimezone = 'UTC'; @@ -85,42 +82,24 @@ test(`passes browserTimezone to generatePng`, async () => { stream ); - expect(generatePngObservable.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - LevelLogger { - "_logger": Object { - "get": [MockFunction], - }, - "_tags": Array [ - "PNG", - "execute", - "pngJobId", - ], - "warning": [Function], - }, - "localhost:80undefined/app/kibana#/something", - "UTC", - Object { - "conditions": Object { - "basePath": undefined, - "hostname": "localhost", - "port": 80, - "protocol": undefined, - }, - "headers": Object {}, - }, - undefined, - ], - ] - `); + expect(generatePngObservable).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + urls: ['localhost:80undefined/app/kibana#/something'], + browserTimezone: 'UTC', + conditionalHeaders: expect.objectContaining({ + conditions: expect.any(Object), + headers: {}, + }), + }) + ); }); test(`returns content_type of application/png`, async () => { const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); - const generatePngObservable = await generatePngObservableFactory(mockReporting); (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('foo') })); const { content_type: contentType } = await runTask( @@ -134,7 +113,6 @@ test(`returns content_type of application/png`, async () => { test(`returns content of generatePng`, async () => { const testContent = 'raw string from get_screenhots'; - const generatePngObservable = await generatePngObservableFactory(mockReporting); (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); diff --git a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts index 2446e7a7d1c51..e6cbfb45eb095 100644 --- a/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/png/execute_job/index.ts @@ -7,7 +7,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; -import { catchError, finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; +import { finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; import { PNG_JOB_TYPE, REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; import { TaskRunResult } from '../../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../../types'; @@ -16,7 +16,7 @@ import { getConditionalHeaders, getFullUrls, omitBlockedHeaders, - generatePngObservableFactory, + generatePngObservable, } from '../../common'; import { TaskPayloadPNG } from '../types'; @@ -25,40 +25,35 @@ export const runTaskFnFactory: RunTaskFnFactory> = const config = reporting.getConfig(); const encryptionKey = config.get('encryptionKey'); - return async function runTask(jobId, job, cancellationToken, stream) { + return function runTask(jobId, job, cancellationToken, stream) { const apmTrans = apm.startTransaction('execute-job-png', REPORTING_TRANSACTION_TYPE); const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePng: { end: () => void } | null | undefined; - const generatePngObservable = await generatePngObservableFactory(reporting); const jobLogger = parentLogger.clone([PNG_JOB_TYPE, 'execute', jobId]); const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)), map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)), map((filteredHeaders) => getConditionalHeaders(config, filteredHeaders)), mergeMap((conditionalHeaders) => { - const urls = getFullUrls(config, job); - const hashUrl = urls[0]; - if (apmGetAssets) apmGetAssets.end(); + const [url] = getFullUrls(config, job); + apmGetAssets?.end(); apmGeneratePng = apmTrans?.startSpan('generate-png-pipeline', 'execute'); - return generatePngObservable( - jobLogger, - hashUrl, - job.browserTimezone, + + return generatePngObservable(reporting, jobLogger, { conditionalHeaders, - job.layout - ); + urls: [url], + browserTimezone: job.browserTimezone, + layout: job.layout, + }); }), tap(({ buffer }) => stream.write(buffer)), map(({ warnings }) => ({ content_type: 'image/png', warnings, })), - catchError((err) => { - jobLogger.error(err); - return Rx.throwError(err); - }), + tap({ error: (error) => jobLogger.error(error) }), finalize(() => apmGeneratePng?.end()) ); diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts index ba076f98996b1..783c8f8e8f880 100644 --- a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.test.ts @@ -16,11 +16,11 @@ import { createMockConfigSchema, createMockReportingCore, } from '../../test_helpers'; -import { generatePngObservableFactory } from '../common'; +import { generatePngObservable } from '../common'; import { runTaskFnFactory } from './execute_job'; import { TaskPayloadPNGV2 } from './types'; -jest.mock('../common/generate_png', () => ({ generatePngObservableFactory: jest.fn() })); +jest.mock('../common/generate_png'); let content: string; let mockReporting: ReportingCore; @@ -62,16 +62,13 @@ beforeEach(async () => { mockReporting = await createMockReportingCore(mockReportingConfig); mockReporting.setConfig(createMockConfig(mockReportingConfig)); - - (generatePngObservableFactory as jest.Mock).mockReturnValue(jest.fn()); }); -afterEach(() => (generatePngObservableFactory as jest.Mock).mockReset()); +afterEach(() => (generatePngObservable as jest.Mock).mockReset()); test(`passes browserTimezone to generatePng`, async () => { const encryptedHeaders = await encryptHeaders({}); - const generatePngObservable = (await generatePngObservableFactory(mockReporting)) as jest.Mock; - generatePngObservable.mockReturnValue(Rx.of({ buffer: Buffer.from('') })); + (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('') })); const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); const browserTimezone = 'UTC'; @@ -87,49 +84,29 @@ test(`passes browserTimezone to generatePng`, async () => { stream ); - expect(generatePngObservable.mock.calls).toMatchInlineSnapshot(` - Array [ - Array [ - LevelLogger { - "_logger": Object { - "get": [MockFunction], - }, - "_tags": Array [ - "PNGV2", - "execute", - "pngJobId", - ], - "warning": [Function], - }, - Array [ - "localhost:80undefined/app/reportingRedirect?forceNow=test", - Object { - "id": "test", - "params": Object {}, - "version": "test", - }, + expect(generatePngObservable).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.objectContaining({ + urls: [ + [ + 'localhost:80undefined/app/reportingRedirect?forceNow=test', + { id: 'test', params: {}, version: 'test' }, ], - "UTC", - Object { - "conditions": Object { - "basePath": undefined, - "hostname": "localhost", - "port": 80, - "protocol": undefined, - }, - "headers": Object {}, - }, - undefined, ], - ] - `); + browserTimezone: 'UTC', + conditionalHeaders: expect.objectContaining({ + conditions: expect.any(Object), + headers: {}, + }), + }) + ); }); test(`returns content_type of application/png`, async () => { const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); const encryptedHeaders = await encryptHeaders({}); - const generatePngObservable = await generatePngObservableFactory(mockReporting); (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('foo') })); const { content_type: contentType } = await runTask( @@ -146,7 +123,6 @@ test(`returns content_type of application/png`, async () => { test(`returns content of generatePng getBuffer base64 encoded`, async () => { const testContent = 'raw string from get_screenhots'; - const generatePngObservable = await generatePngObservableFactory(mockReporting); (generatePngObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); const runTask = await runTaskFnFactory(mockReporting, getMockLogger()); diff --git a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts index 00652309b88c1..a8ab6c4355000 100644 --- a/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/png_v2/execute_job.ts @@ -7,7 +7,7 @@ import apm from 'elastic-apm-node'; import * as Rx from 'rxjs'; -import { catchError, finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; +import { finalize, map, mergeMap, takeUntil, tap } from 'rxjs/operators'; import { PNG_JOB_TYPE_V2, REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; import { TaskRunResult } from '../../lib/tasks'; import { RunTaskFn, RunTaskFnFactory } from '../../types'; @@ -15,7 +15,7 @@ import { decryptJobHeaders, getConditionalHeaders, omitBlockedHeaders, - generatePngObservableFactory, + generatePngObservable, } from '../common'; import { getFullRedirectAppUrl } from '../common/v2/get_full_redirect_app_url'; import { TaskPayloadPNGV2 } from './types'; @@ -25,12 +25,11 @@ export const runTaskFnFactory: RunTaskFnFactory> = const config = reporting.getConfig(); const encryptionKey = config.get('encryptionKey'); - return async function runTask(jobId, job, cancellationToken, stream) { + return function runTask(jobId, job, cancellationToken, stream) { const apmTrans = apm.startTransaction('execute-job-png-v2', REPORTING_TRANSACTION_TYPE); const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePng: { end: () => void } | null | undefined; - const generatePngObservable = await generatePngObservableFactory(reporting); const jobLogger = parentLogger.clone([PNG_JOB_TYPE_V2, 'execute', jobId]); const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)), @@ -41,25 +40,21 @@ export const runTaskFnFactory: RunTaskFnFactory> = const [locatorParams] = job.locatorParams; apmGetAssets?.end(); - apmGeneratePng = apmTrans?.startSpan('generate-png-pipeline', 'execute'); - return generatePngObservable( - jobLogger, - [url, locatorParams], - job.browserTimezone, + + return generatePngObservable(reporting, jobLogger, { conditionalHeaders, - job.layout - ); + browserTimezone: job.browserTimezone, + layout: job.layout, + urls: [[url, locatorParams]], + }); }), tap(({ buffer }) => stream.write(buffer)), map(({ warnings }) => ({ content_type: 'image/png', warnings, })), - catchError((err) => { - jobLogger.error(err); - return Rx.throwError(err); - }), + tap({ error: (error) => jobLogger.error(error) }), finalize(() => apmGeneratePng?.end()) ); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts index 02f9c93929ea1..eb02097ec7924 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.test.ts @@ -5,18 +5,18 @@ * 2.0. */ -jest.mock('../lib/generate_pdf', () => ({ generatePdfObservableFactory: jest.fn() })); - import * as Rx from 'rxjs'; import { Writable } from 'stream'; import { ReportingCore } from '../../../'; import { CancellationToken } from '../../../../common'; import { cryptoFactory, LevelLogger } from '../../../lib'; import { createMockConfigSchema, createMockReportingCore } from '../../../test_helpers'; -import { generatePdfObservableFactory } from '../lib/generate_pdf'; +import { generatePdfObservable } from '../lib/generate_pdf'; import { TaskPayloadPDF } from '../types'; import { runTaskFnFactory } from './'; +jest.mock('../lib/generate_pdf'); + let content: string; let mockReporting: ReportingCore; let stream: jest.Mocked; @@ -56,16 +56,13 @@ beforeEach(async () => { }; const mockSchema = createMockConfigSchema(reportingConfig); mockReporting = await createMockReportingCore(mockSchema); - - (generatePdfObservableFactory as jest.Mock).mockReturnValue(jest.fn()); }); -afterEach(() => (generatePdfObservableFactory as jest.Mock).mockReset()); +afterEach(() => (generatePdfObservable as jest.Mock).mockReset()); test(`passes browserTimezone to generatePdf`, async () => { const encryptedHeaders = await encryptHeaders({}); - const generatePdfObservable = (await generatePdfObservableFactory(mockReporting)) as jest.Mock; - generatePdfObservable.mockReturnValue(Rx.of({ buffer: Buffer.from('') })); + (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('') })); const runTask = runTaskFnFactory(mockReporting, getMockLogger()); const browserTimezone = 'UTC'; @@ -81,8 +78,13 @@ test(`passes browserTimezone to generatePdf`, async () => { stream ); - const tzParam = generatePdfObservable.mock.calls[0][3]; - expect(tzParam).toBe('UTC'); + expect(generatePdfObservable).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + expect.objectContaining({ browserTimezone: 'UTC' }), + undefined + ); }); test(`returns content_type of application/pdf`, async () => { @@ -90,7 +92,6 @@ test(`returns content_type of application/pdf`, async () => { const runTask = runTaskFnFactory(mockReporting, logger); const encryptedHeaders = await encryptHeaders({}); - const generatePdfObservable = await generatePdfObservableFactory(mockReporting); (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('') })); const { content_type: contentType } = await runTask( @@ -104,7 +105,6 @@ test(`returns content_type of application/pdf`, async () => { test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const testContent = 'test content'; - const generatePdfObservable = await generatePdfObservableFactory(mockReporting); (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); const runTask = runTaskFnFactory(mockReporting, getMockLogger()); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts index 2358333bbe7ef..f301b3e1e6ef2 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/execute_job/index.ts @@ -18,7 +18,7 @@ import { omitBlockedHeaders, getCustomLogo, } from '../../common'; -import { generatePdfObservableFactory } from '../lib/generate_pdf'; +import { generatePdfObservable } from '../lib/generate_pdf'; import { TaskPayloadPDF } from '../types'; export const runTaskFnFactory: RunTaskFnFactory> = @@ -32,8 +32,6 @@ export const runTaskFnFactory: RunTaskFnFactory> = const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePdf: { end: () => void } | null | undefined; - const generatePdfObservable = await generatePdfObservableFactory(reporting); - const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)), map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)), @@ -49,12 +47,15 @@ export const runTaskFnFactory: RunTaskFnFactory> = apmGeneratePdf = apmTrans?.startSpan('generate-pdf-pipeline', 'execute'); return generatePdfObservable( + reporting, jobLogger, title, - urls, - browserTimezone, - conditionalHeaders, - layout, + { + urls, + browserTimezone, + conditionalHeaders, + layout, + }, logo ); }), diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts index dce6ea678bded..5bf087fecd10a 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/generate_pdf.ts @@ -8,16 +8,15 @@ import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap } from 'rxjs/operators'; +import { ScreenshotResult } from '../../../../../screenshotting/server'; import { ReportingCore } from '../../../'; import { LevelLogger } from '../../../lib'; -import { createLayout, LayoutParams } from '../../../lib/layouts'; -import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots'; -import { ConditionalHeaders } from '../../common'; +import { ScreenshotOptions } from '../../../types'; import { PdfMaker } from '../../common/pdf'; import { getTracker } from './tracker'; -const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { - const grouped = groupBy(urlScreenshots.map((u) => u.timeRange)); +const getTimeRange = (urlScreenshots: ScreenshotResult['results']) => { + const grouped = groupBy(urlScreenshots.map(({ timeRange }) => timeRange)); const values = Object.values(grouped); if (values.length === 1) { return values[0][0]; @@ -26,97 +25,80 @@ const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { return null; }; -export async function generatePdfObservableFactory(reporting: ReportingCore) { - const config = reporting.getConfig(); - const captureConfig = config.get('capture'); - const { browserDriverFactory } = await reporting.getPluginStartDeps(); - - return function generatePdfObservable( - logger: LevelLogger, - title: string, - urls: string[], - browserTimezone: string | undefined, - conditionalHeaders: ConditionalHeaders, - layoutParams: LayoutParams, - logo?: string - ): Rx.Observable<{ buffer: Buffer | null; warnings: string[] }> { - const tracker = getTracker(); - tracker.startLayout(); - - const layout = createLayout(captureConfig, layoutParams); - logger.debug(`Layout: width=${layout.width} height=${layout.height}`); - tracker.endLayout(); - - tracker.startScreenshots(); - const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, { - logger, - urlsOrUrlLocatorTuples: urls, - conditionalHeaders, - layout, - browserTimezone, - }).pipe( - mergeMap(async (results: ScreenshotResults[]) => { - tracker.endScreenshots(); - - tracker.startSetup(); - const pdfOutput = new PdfMaker(layout, logo); - if (title) { - const timeRange = getTimeRange(results); - title += timeRange ? ` - ${timeRange}` : ''; - pdfOutput.setTitle(title); - } - tracker.endSetup(); - - results.forEach((r) => { - r.screenshots.forEach((screenshot) => { - logger.debug(`Adding image to PDF. Image size: ${screenshot.data.byteLength}`); // prettier-ignore - tracker.startAddImage(); - tracker.endAddImage(); - pdfOutput.addImage(screenshot.data, { - title: screenshot.title ?? undefined, - description: screenshot.description ?? undefined, - }); +export function generatePdfObservable( + reporting: ReportingCore, + logger: LevelLogger, + title: string, + options: ScreenshotOptions, + logo?: string +): Rx.Observable<{ buffer: Buffer | null; warnings: string[] }> { + const tracker = getTracker(); + tracker.startScreenshots(); + + return reporting.getScreenshots(options).pipe( + mergeMap(async ({ layout, metrics$, results }) => { + metrics$.subscribe(({ cpu, memory }) => { + tracker.setCpuUsage(cpu); + tracker.setMemoryUsage(memory); + }); + tracker.endScreenshots(); + tracker.startSetup(); + + const pdfOutput = new PdfMaker(layout, logo); + if (title) { + const timeRange = getTimeRange(results); + title += timeRange ? ` - ${timeRange}` : ''; + pdfOutput.setTitle(title); + } + tracker.endSetup(); + + results.forEach((r) => { + r.screenshots.forEach((screenshot) => { + logger.debug(`Adding image to PDF. Image size: ${screenshot.data.byteLength}`); // prettier-ignore + tracker.startAddImage(); + tracker.endAddImage(); + pdfOutput.addImage(screenshot.data, { + title: screenshot.title ?? undefined, + description: screenshot.description ?? undefined, }); }); - - let buffer: Buffer | null = null; - try { - tracker.startCompile(); - logger.debug(`Compiling PDF using "${layout.id}" layout...`); - pdfOutput.generate(); - tracker.endCompile(); - - tracker.startGetBuffer(); - logger.debug(`Generating PDF Buffer...`); - buffer = await pdfOutput.getBuffer(); - - const byteLength = buffer?.byteLength ?? 0; - logger.debug(`PDF buffer byte length: ${byteLength}`); - tracker.setByteLength(byteLength); - - tracker.endGetBuffer(); - } catch (err) { - logger.error(`Could not generate the PDF buffer!`); - logger.error(err); - } - - tracker.end(); - - return { - buffer, - warnings: results.reduce((found, current) => { - if (current.error) { - found.push(current.error.message); - } - if (current.renderErrors) { - found.push(...current.renderErrors); - } - return found; - }, [] as string[]), - }; - }) - ); - - return screenshots$; - }; + }); + + let buffer: Buffer | null = null; + try { + tracker.startCompile(); + logger.debug(`Compiling PDF using "${layout.id}" layout...`); + pdfOutput.generate(); + tracker.endCompile(); + + tracker.startGetBuffer(); + logger.debug(`Generating PDF Buffer...`); + buffer = await pdfOutput.getBuffer(); + + const byteLength = buffer?.byteLength ?? 0; + logger.debug(`PDF buffer byte length: ${byteLength}`); + tracker.setByteLength(byteLength); + + tracker.endGetBuffer(); + } catch (err) { + logger.error(`Could not generate the PDF buffer!`); + logger.error(err); + } + + tracker.end(); + + return { + buffer, + warnings: results.reduce((found, current) => { + if (current.error) { + found.push(current.error.message); + } + if (current.renderErrors) { + found.push(...current.renderErrors); + } + return found; + }, [] as string[]), + }; + }) + ); } diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/tracker.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/tracker.ts index 3d720ccade546..d1cf2b96817d2 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/tracker.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf/lib/tracker.ts @@ -10,8 +10,8 @@ import { REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; interface PdfTracker { setByteLength: (byteLength: number) => void; - startLayout: () => void; - endLayout: () => void; + setCpuUsage: (cpu: number) => void; + setMemoryUsage: (memory: number) => void; startScreenshots: () => void; endScreenshots: () => void; startSetup: () => void; @@ -35,7 +35,6 @@ interface ApmSpan { export function getTracker(): PdfTracker { const apmTrans = apm.startTransaction('generate-pdf', REPORTING_TRANSACTION_TYPE); - let apmLayout: ApmSpan | null = null; let apmScreenshots: ApmSpan | null = null; let apmSetup: ApmSpan | null = null; let apmAddImage: ApmSpan | null = null; @@ -43,12 +42,6 @@ export function getTracker(): PdfTracker { let apmGetBuffer: ApmSpan | null = null; return { - startLayout() { - apmLayout = apmTrans?.startSpan('create-layout', SPANTYPE_SETUP) || null; - }, - endLayout() { - if (apmLayout) apmLayout.end(); - }, startScreenshots() { apmScreenshots = apmTrans?.startSpan('screenshots-pipeline', SPANTYPE_SETUP) || null; }, @@ -82,6 +75,12 @@ export function getTracker(): PdfTracker { setByteLength(byteLength: number) { apmTrans?.setLabel('byte-length', byteLength, false); }, + setCpuUsage(cpu: number) { + apmTrans?.setLabel('cpu', cpu, false); + }, + setMemoryUsage(memory: number) { + apmTrans?.setLabel('memory', memory, false); + }, end() { if (apmTrans) apmTrans.end(); }, diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts index 197bd3866b8f6..9a73595ff32da 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -jest.mock('./lib/generate_pdf', () => ({ generatePdfObservableFactory: jest.fn() })); +jest.mock('./lib/generate_pdf'); import * as Rx from 'rxjs'; import { Writable } from 'stream'; @@ -15,7 +15,7 @@ import { LocatorParams } from '../../../common/types'; import { cryptoFactory, LevelLogger } from '../../lib'; import { createMockConfigSchema, createMockReportingCore } from '../../test_helpers'; import { runTaskFnFactory } from './execute_job'; -import { generatePdfObservableFactory } from './lib/generate_pdf'; +import { generatePdfObservable } from './lib/generate_pdf'; import { TaskPayloadPDFV2 } from './types'; let content: string; @@ -61,16 +61,13 @@ beforeEach(async () => { }; const mockSchema = createMockConfigSchema(reportingConfig); mockReporting = await createMockReportingCore(mockSchema); - - (generatePdfObservableFactory as jest.Mock).mockReturnValue(jest.fn()); }); -afterEach(() => (generatePdfObservableFactory as jest.Mock).mockReset()); +afterEach(() => (generatePdfObservable as jest.Mock).mockReset()); test(`passes browserTimezone to generatePdf`, async () => { const encryptedHeaders = await encryptHeaders({}); - const generatePdfObservable = (await generatePdfObservableFactory(mockReporting)) as jest.Mock; - generatePdfObservable.mockReturnValue(Rx.of(Buffer.from(''))); + (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of(Buffer.from(''))); const runTask = runTaskFnFactory(mockReporting, getMockLogger()); const browserTimezone = 'UTC'; @@ -87,8 +84,15 @@ test(`passes browserTimezone to generatePdf`, async () => { stream ); - const tzParam = generatePdfObservable.mock.calls[0][4]; - expect(tzParam).toBe('UTC'); + expect(generatePdfObservable).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.anything(), + expect.objectContaining({ browserTimezone: 'UTC' }), + undefined + ); }); test(`returns content_type of application/pdf`, async () => { @@ -96,7 +100,6 @@ test(`returns content_type of application/pdf`, async () => { const runTask = runTaskFnFactory(mockReporting, logger); const encryptedHeaders = await encryptHeaders({}); - const generatePdfObservable = await generatePdfObservableFactory(mockReporting); (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from('') })); const { content_type: contentType } = await runTask( @@ -110,7 +113,6 @@ test(`returns content_type of application/pdf`, async () => { test(`returns content of generatePdf getBuffer base64 encoded`, async () => { const testContent = 'test content'; - const generatePdfObservable = await generatePdfObservableFactory(mockReporting); (generatePdfObservable as jest.Mock).mockReturnValue(Rx.of({ buffer: Buffer.from(testContent) })); const runTask = runTaskFnFactory(mockReporting, getMockLogger()); diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts index b1b6f3f79aee3..890c0c9cde731 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/execute_job.ts @@ -17,7 +17,7 @@ import { omitBlockedHeaders, getCustomLogo, } from '../common'; -import { generatePdfObservableFactory } from './lib/generate_pdf'; +import { generatePdfObservable } from './lib/generate_pdf'; import { TaskPayloadPDFV2 } from './types'; export const runTaskFnFactory: RunTaskFnFactory> = @@ -31,8 +31,6 @@ export const runTaskFnFactory: RunTaskFnFactory> = const apmGetAssets = apmTrans?.startSpan('get-assets', 'setup'); let apmGeneratePdf: { end: () => void } | null | undefined; - const generatePdfObservable = await generatePdfObservableFactory(reporting); - const process$: Rx.Observable = Rx.of(1).pipe( mergeMap(() => decryptJobHeaders(encryptionKey, job.headers, jobLogger)), map((decryptedHeaders) => omitBlockedHeaders(decryptedHeaders)), @@ -46,13 +44,16 @@ export const runTaskFnFactory: RunTaskFnFactory> = apmGeneratePdf = apmTrans?.startSpan('generate-pdf-pipeline', 'execute'); return generatePdfObservable( + reporting, jobLogger, job, title, locatorParams, - browserTimezone, - conditionalHeaders, - layout, + { + browserTimezone, + conditionalHeaders, + layout, + }, logo ); }), diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts index b44e2ca4441eb..3d790beb41b39 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/generate_pdf.ts @@ -5,21 +5,20 @@ * 2.0. */ -import { groupBy, zip } from 'lodash'; +import { groupBy } from 'lodash'; import * as Rx from 'rxjs'; import { mergeMap } from 'rxjs/operators'; import { ReportingCore } from '../../../'; import { LocatorParams, UrlOrUrlLocatorTuple } from '../../../../common/types'; import { LevelLogger } from '../../../lib'; -import { createLayout, LayoutParams } from '../../../lib/layouts'; -import { getScreenshots$, ScreenshotResults } from '../../../lib/screenshots'; -import { ConditionalHeaders } from '../../common'; +import { ScreenshotResult } from '../../../../../screenshotting/server'; +import { ScreenshotOptions } from '../../../types'; import { PdfMaker } from '../../common/pdf'; import { getFullRedirectAppUrl } from '../../common/v2/get_full_redirect_app_url'; import type { TaskPayloadPDFV2 } from '../types'; import { getTracker } from './tracker'; -const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { +const getTimeRange = (urlScreenshots: ScreenshotResult['results']) => { const grouped = groupBy(urlScreenshots.map((u) => u.timeRange)); const values = Object.values(grouped); if (values.length === 1) { @@ -29,106 +28,92 @@ const getTimeRange = (urlScreenshots: ScreenshotResults[]) => { return null; }; -export async function generatePdfObservableFactory(reporting: ReportingCore) { - const config = reporting.getConfig(); - const captureConfig = config.get('capture'); - const { browserDriverFactory } = await reporting.getPluginStartDeps(); - - return function generatePdfObservable( - logger: LevelLogger, - job: TaskPayloadPDFV2, - title: string, - locatorParams: LocatorParams[], - browserTimezone: string | undefined, - conditionalHeaders: ConditionalHeaders, - layoutParams: LayoutParams, - logo?: string - ): Rx.Observable<{ buffer: Buffer | null; warnings: string[] }> { - const tracker = getTracker(); - tracker.startLayout(); - - const layout = createLayout(captureConfig, layoutParams); - logger.debug(`Layout: width=${layout.width} height=${layout.height}`); - tracker.endLayout(); - - tracker.startScreenshots(); - - /** - * For each locator we get the relative URL to the redirect app - */ - const urls = locatorParams.map(() => - getFullRedirectAppUrl(reporting.getConfig(), job.spaceId, job.forceNow) - ); - - const screenshots$ = getScreenshots$(captureConfig, browserDriverFactory, { - logger, - urlsOrUrlLocatorTuples: zip(urls, locatorParams) as UrlOrUrlLocatorTuple[], - conditionalHeaders, - layout, - browserTimezone, - }).pipe( - mergeMap(async (results: ScreenshotResults[]) => { - tracker.endScreenshots(); - - tracker.startSetup(); - const pdfOutput = new PdfMaker(layout, logo); - if (title) { - const timeRange = getTimeRange(results); - title += timeRange ? ` - ${timeRange}` : ''; - pdfOutput.setTitle(title); - } - tracker.endSetup(); - - results.forEach((r) => { - r.screenshots.forEach((screenshot) => { - logger.debug(`Adding image to PDF. Image base64 size: ${screenshot.data.byteLength}`); // prettier-ignore - tracker.startAddImage(); - tracker.endAddImage(); - pdfOutput.addImage(screenshot.data, { - title: screenshot.title ?? undefined, - description: screenshot.description ?? undefined, - }); +export function generatePdfObservable( + reporting: ReportingCore, + logger: LevelLogger, + job: TaskPayloadPDFV2, + title: string, + locatorParams: LocatorParams[], + options: Omit, + logo?: string +): Rx.Observable<{ buffer: Buffer | null; warnings: string[] }> { + const tracker = getTracker(); + tracker.startScreenshots(); + + /** + * For each locator we get the relative URL to the redirect app + */ + const urls = locatorParams.map((locator) => [ + getFullRedirectAppUrl(reporting.getConfig(), job.spaceId, job.forceNow), + locator, + ]) as UrlOrUrlLocatorTuple[]; + + const screenshots$ = reporting.getScreenshots({ ...options, urls }).pipe( + mergeMap(async ({ layout, metrics$, results }) => { + metrics$.subscribe(({ cpu, memory }) => { + tracker.setCpuUsage(cpu); + tracker.setMemoryUsage(memory); + }); + tracker.endScreenshots(); + tracker.startSetup(); + + const pdfOutput = new PdfMaker(layout, logo); + if (title) { + const timeRange = getTimeRange(results); + title += timeRange ? ` - ${timeRange}` : ''; + pdfOutput.setTitle(title); + } + tracker.endSetup(); + + results.forEach((r) => { + r.screenshots.forEach((screenshot) => { + logger.debug(`Adding image to PDF. Image base64 size: ${screenshot.data.byteLength}`); // prettier-ignore + tracker.startAddImage(); + tracker.endAddImage(); + pdfOutput.addImage(screenshot.data, { + title: screenshot.title ?? undefined, + description: screenshot.description ?? undefined, }); }); - - let buffer: Buffer | null = null; - try { - tracker.startCompile(); - logger.debug(`Compiling PDF using "${layout.id}" layout...`); - pdfOutput.generate(); - tracker.endCompile(); - - tracker.startGetBuffer(); - logger.debug(`Generating PDF Buffer...`); - buffer = await pdfOutput.getBuffer(); - - const byteLength = buffer?.byteLength ?? 0; - logger.debug(`PDF buffer byte length: ${byteLength}`); - tracker.setByteLength(byteLength); - - tracker.endGetBuffer(); - } catch (err) { - logger.error(`Could not generate the PDF buffer!`); - logger.error(err); - } - - tracker.end(); - - return { - buffer, - warnings: results.reduce((found, current) => { - if (current.error) { - found.push(current.error.message); - } - if (current.renderErrors) { - found.push(...current.renderErrors); - } - return found; - }, [] as string[]), - }; - }) - ); - - return screenshots$; - }; + }); + + let buffer: Buffer | null = null; + try { + tracker.startCompile(); + logger.debug(`Compiling PDF using "${layout.id}" layout...`); + pdfOutput.generate(); + tracker.endCompile(); + + tracker.startGetBuffer(); + logger.debug(`Generating PDF Buffer...`); + buffer = await pdfOutput.getBuffer(); + + const byteLength = buffer?.byteLength ?? 0; + logger.debug(`PDF buffer byte length: ${byteLength}`); + tracker.setByteLength(byteLength); + + tracker.endGetBuffer(); + } catch (err) { + logger.error(`Could not generate the PDF buffer!`); + logger.error(err); + } + + tracker.end(); + + return { + buffer, + warnings: results.reduce((found, current) => { + if (current.error) { + found.push(current.error.message); + } + if (current.renderErrors) { + found.push(...current.renderErrors); + } + return found; + }, [] as string[]), + }; + }) + ); + + return screenshots$; } diff --git a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/tracker.ts b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/tracker.ts index 3d720ccade546..d1cf2b96817d2 100644 --- a/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/tracker.ts +++ b/x-pack/plugins/reporting/server/export_types/printable_pdf_v2/lib/tracker.ts @@ -10,8 +10,8 @@ import { REPORTING_TRANSACTION_TYPE } from '../../../../common/constants'; interface PdfTracker { setByteLength: (byteLength: number) => void; - startLayout: () => void; - endLayout: () => void; + setCpuUsage: (cpu: number) => void; + setMemoryUsage: (memory: number) => void; startScreenshots: () => void; endScreenshots: () => void; startSetup: () => void; @@ -35,7 +35,6 @@ interface ApmSpan { export function getTracker(): PdfTracker { const apmTrans = apm.startTransaction('generate-pdf', REPORTING_TRANSACTION_TYPE); - let apmLayout: ApmSpan | null = null; let apmScreenshots: ApmSpan | null = null; let apmSetup: ApmSpan | null = null; let apmAddImage: ApmSpan | null = null; @@ -43,12 +42,6 @@ export function getTracker(): PdfTracker { let apmGetBuffer: ApmSpan | null = null; return { - startLayout() { - apmLayout = apmTrans?.startSpan('create-layout', SPANTYPE_SETUP) || null; - }, - endLayout() { - if (apmLayout) apmLayout.end(); - }, startScreenshots() { apmScreenshots = apmTrans?.startSpan('screenshots-pipeline', SPANTYPE_SETUP) || null; }, @@ -82,6 +75,12 @@ export function getTracker(): PdfTracker { setByteLength(byteLength: number) { apmTrans?.setLabel('byte-length', byteLength, false); }, + setCpuUsage(cpu: number) { + apmTrans?.setLabel('cpu', cpu, false); + }, + setMemoryUsage(memory: number) { + apmTrans?.setLabel('memory', memory, false); + }, end() { if (apmTrans) apmTrans.end(); }, diff --git a/x-pack/plugins/reporting/server/lib/content_stream.ts b/x-pack/plugins/reporting/server/lib/content_stream.ts index 9719ac57b119c..b8ae7d0a17670 100644 --- a/x-pack/plugins/reporting/server/lib/content_stream.ts +++ b/x-pack/plugins/reporting/server/lib/content_stream.ts @@ -66,7 +66,9 @@ export class ContentStream extends Duplex { return Math.floor(max / 2); } - private buffer = Buffer.from(''); + private buffers: Buffer[] = []; + private bytesBuffered = 0; + private bytesRead = 0; private chunksRead = 0; private chunksWritten = 0; @@ -249,8 +251,43 @@ export class ContentStream extends Duplex { }); } - private async flush(size = this.buffer.byteLength) { - const chunk = this.buffer.slice(0, size); + private async flush(size = this.bytesBuffered) { + const buffersToFlush: Buffer[] = []; + let bytesToFlush = 0; + + /* + Loop over each buffer, keeping track of how many bytes we have added + to the array of buffers to be flushed. The array of buffers to be flushed + contains buffers by reference, not copies. This avoids putting pressure on + the CPU for copying buffers or for gc activity. Please profile performance + with a large byte configuration and a large number of records (900k+) + before changing this code. Config used at time of writing: + + xpack.reporting: + csv.maxSizeBytes: 500000000 + csv.scroll.size: 1000 + + At the moment this can put memory pressure on Kibana. Up to 1,1 GB in a dev + build. It is not recommended to have overly large max size bytes but we + need this code to be as performant as possible. + */ + while (this.buffers.length) { + const remainder = size - bytesToFlush; + if (remainder <= 0) { + break; + } + const buffer = this.buffers.shift()!; + const chunkedBuffer = buffer.slice(0, remainder); + buffersToFlush.push(chunkedBuffer); + bytesToFlush += chunkedBuffer.byteLength; + + if (buffer.byteLength > remainder) { + this.buffers.unshift(buffer.slice(remainder)); + } + } + + // We call Buffer.concat with the fewest number of buffers possible + const chunk = Buffer.concat(buffersToFlush); const content = this.encode(chunk); if (!this.chunksWritten) { @@ -265,22 +302,21 @@ export class ContentStream extends Duplex { } this.bytesWritten += chunk.byteLength; - this.buffer = this.buffer.slice(size); + this.bytesBuffered -= bytesToFlush; } private async flushAllFullChunks() { const maxChunkSize = await this.getMaxChunkSize(); - while (this.buffer.byteLength >= maxChunkSize) { + while (this.bytesBuffered >= maxChunkSize && this.buffers.length) { await this.flush(maxChunkSize); } } _write(chunk: Buffer | string, encoding: BufferEncoding, callback: Callback) { - this.buffer = Buffer.concat([ - this.buffer, - Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding), - ]); + const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding); + this.bytesBuffered += buffer.byteLength; + this.buffers.push(buffer); this.flushAllFullChunks() .then(() => callback()) diff --git a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts b/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts deleted file mode 100644 index f62ee6ab720c3..0000000000000 --- a/x-pack/plugins/reporting/server/lib/layouts/create_layout.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { LAYOUT_TYPES } from '../../../common/constants'; -import { CaptureConfig } from '../../types'; -import { LayoutInstance, LayoutParams, LayoutTypes } from './'; -import { CanvasLayout } from './canvas_layout'; -import { PreserveLayout } from './preserve_layout'; -import { PrintLayout } from './print_layout'; - -export function createLayout( - captureConfig: CaptureConfig, - layoutParams?: LayoutParams -): LayoutInstance { - if (layoutParams && layoutParams.dimensions && layoutParams.id === LAYOUT_TYPES.PRESERVE_LAYOUT) { - return new PreserveLayout(layoutParams.dimensions); - } - - if (layoutParams && layoutParams.dimensions && layoutParams.id === LayoutTypes.CANVAS) { - return new CanvasLayout(layoutParams.dimensions); - } - - // layoutParams is optional as PrintLayout doesn't use it - return new PrintLayout(captureConfig); -} diff --git a/x-pack/plugins/reporting/server/lib/layouts/index.ts b/x-pack/plugins/reporting/server/lib/layouts/index.ts deleted file mode 100644 index daff568ab0067..0000000000000 --- a/x-pack/plugins/reporting/server/lib/layouts/index.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { LevelLogger } from '../'; -import { Size } from '../../../common/types'; -import { HeadlessChromiumDriver } from '../../browsers'; -import type { Layout } from './layout'; - -export interface LayoutSelectorDictionary { - screenshot: string; - renderComplete: string; - renderError: string; - renderErrorAttribute: string; - itemsCountAttribute: string; - timefilterDurationAttribute: string; -} - -export type { LayoutParams, PageSizeParams, PdfImageSize, Size } from '../../../common/types'; -export { CanvasLayout } from './canvas_layout'; -export { createLayout } from './create_layout'; -export type { Layout } from './layout'; -export { PreserveLayout } from './preserve_layout'; -export { PrintLayout } from './print_layout'; - -export const LayoutTypes = { - PRESERVE_LAYOUT: 'preserve_layout', - PRINT: 'print', - CANVAS: 'canvas', // no margins or branding in the layout -}; - -export const getDefaultLayoutSelectors = (): LayoutSelectorDictionary => ({ - screenshot: '[data-shared-items-container]', - renderComplete: '[data-shared-item]', - renderError: '[data-render-error]', - renderErrorAttribute: 'data-render-error', - itemsCountAttribute: 'data-shared-items-count', - timefilterDurationAttribute: 'data-shared-timefilter-duration', -}); - -interface LayoutSelectors { - // Fields that are not part of Layout: the instances - // independently implement these fields on their own - selectors: LayoutSelectorDictionary; - positionElements?: (browser: HeadlessChromiumDriver, logger: LevelLogger) => Promise; -} - -export type LayoutInstance = Layout & LayoutSelectors & Partial; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.test.ts deleted file mode 100644 index f160fcb8b27ad..0000000000000 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.test.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { set } from 'lodash'; -import { durationToNumber } from '../../../common/schema_utils'; -import { HeadlessChromiumDriver } from '../../browsers'; -import { - createMockBrowserDriverFactory, - createMockConfig, - createMockConfigSchema, - createMockLayoutInstance, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; -import { CaptureConfig } from '../../types'; -import { LayoutInstance } from '../layouts'; -import { LevelLogger } from '../level_logger'; -import { getNumberOfItems } from './get_number_of_items'; - -describe('getNumberOfItems', () => { - let captureConfig: CaptureConfig; - let layout: LayoutInstance; - let logger: jest.Mocked; - let browser: HeadlessChromiumDriver; - let timeout: number; - - beforeEach(async () => { - const schema = createMockConfigSchema(set({}, 'capture.timeouts.waitForElements', 0)); - const config = createMockConfig(schema); - const core = await createMockReportingCore(schema); - - captureConfig = config.get('capture'); - layout = createMockLayoutInstance(captureConfig); - logger = createMockLevelLogger(); - timeout = durationToNumber(captureConfig.timeouts.waitForElements); - - await createMockBrowserDriverFactory(core, logger, { - evaluate: jest.fn( - async unknown>({ - fn, - args, - }: { - fn: T; - args: Parameters; - }) => fn(...args) - ), - getCreatePage: (driver) => { - browser = driver; - - return jest.fn(); - }, - }); - }); - - afterEach(() => { - document.body.innerHTML = ''; - }); - - it('should determine the number of items by attribute', async () => { - document.body.innerHTML = ` -
- `; - - await expect(getNumberOfItems(timeout, browser, layout, logger)).resolves.toBe(10); - }); - - it('should determine the number of items by selector ', async () => { - document.body.innerHTML = ` - - - - `; - - await expect(getNumberOfItems(timeout, browser, layout, logger)).resolves.toBe(3); - }); - - it('should fall back to the selector when the attribute is empty', async () => { - document.body.innerHTML = ` -
- - - `; - - await expect(getNumberOfItems(timeout, browser, layout, logger)).resolves.toBe(2); - }); -}); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_render_errors.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_render_errors.test.ts deleted file mode 100644 index d29c0936bfceb..0000000000000 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_render_errors.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { HeadlessChromiumDriver } from '../../browsers'; -import { - createMockBrowserDriverFactory, - createMockConfig, - createMockConfigSchema, - createMockLayoutInstance, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; -import { CaptureConfig } from '../../types'; -import { LayoutInstance } from '../layouts'; -import { LevelLogger } from '../level_logger'; -import { getRenderErrors } from './get_render_errors'; - -describe('getRenderErrors', () => { - let captureConfig: CaptureConfig; - let layout: LayoutInstance; - let logger: jest.Mocked; - let browser: HeadlessChromiumDriver; - - beforeEach(async () => { - const schema = createMockConfigSchema(); - const config = createMockConfig(schema); - const core = await createMockReportingCore(schema); - - captureConfig = config.get('capture'); - layout = createMockLayoutInstance(captureConfig); - logger = createMockLevelLogger(); - - await createMockBrowserDriverFactory(core, logger, { - evaluate: jest.fn( - async unknown>({ - fn, - args, - }: { - fn: T; - args: Parameters; - }) => fn(...args) - ), - getCreatePage: (driver) => { - browser = driver; - - return jest.fn(); - }, - }); - }); - - afterEach(() => { - document.body.innerHTML = ''; - }); - - it('should extract the error messages', async () => { - document.body.innerHTML = ` -
-
-
-
- `; - - await expect(getRenderErrors(browser, layout, logger)).resolves.toEqual([ - 'a test error', - 'a test error', - 'a test error', - 'a test error', - ]); - }); - - it('should extract the error messages, even when there are none', async () => { - document.body.innerHTML = ` - - `; - - await expect(getRenderErrors(browser, layout, logger)).resolves.toEqual(undefined); - }); -}); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_time_range.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/get_time_range.test.ts deleted file mode 100644 index 003d1dc254a2a..0000000000000 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_time_range.test.ts +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { HeadlessChromiumDriver } from '../../browsers'; -import { - createMockBrowserDriverFactory, - createMockConfig, - createMockConfigSchema, - createMockLayoutInstance, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; -import { LayoutInstance } from '../layouts'; -import { LevelLogger } from '../level_logger'; -import { getTimeRange } from './get_time_range'; - -describe('getTimeRange', () => { - let layout: LayoutInstance; - let logger: jest.Mocked; - let browser: HeadlessChromiumDriver; - - beforeEach(async () => { - const schema = createMockConfigSchema(); - const config = createMockConfig(schema); - const captureConfig = config.get('capture'); - const core = await createMockReportingCore(schema); - - layout = createMockLayoutInstance(captureConfig); - logger = createMockLevelLogger(); - - await createMockBrowserDriverFactory(core, logger, { - evaluate: jest.fn( - async unknown>({ - fn, - args, - }: { - fn: T; - args: Parameters; - }) => fn(...args) - ), - getCreatePage: (driver) => { - browser = driver; - - return jest.fn(); - }, - }); - }); - - afterEach(() => { - document.body.innerHTML = ''; - }); - - it('should return null when there is no duration element', async () => { - await expect(getTimeRange(browser, layout, logger)).resolves.toBeNull(); - }); - - it('should return null when duration attrbute is empty', async () => { - document.body.innerHTML = ` -
- `; - - await expect(getTimeRange(browser, layout, logger)).resolves.toBeNull(); - }); - - it('should return duration', async () => { - document.body.innerHTML = ` -
- `; - - await expect(getTimeRange(browser, layout, logger)).resolves.toBe('10'); - }); -}); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/index.ts b/x-pack/plugins/reporting/server/lib/screenshots/index.ts deleted file mode 100644 index 2b8a0d6207a9b..0000000000000 --- a/x-pack/plugins/reporting/server/lib/screenshots/index.ts +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { LevelLogger } from '../'; -import { UrlOrUrlLocatorTuple } from '../../../common/types'; -import { ConditionalHeaders } from '../../export_types/common'; -import { LayoutInstance } from '../layouts'; - -export { getScreenshots$ } from './observable'; - -export interface PhaseInstance { - timeoutValue: number; - configValue: string; - label: string; -} - -export interface PhaseTimeouts { - openUrl: PhaseInstance; - waitForElements: PhaseInstance; - renderComplete: PhaseInstance; - loadDelay: number; -} - -export interface ScreenshotObservableOpts { - logger: LevelLogger; - urlsOrUrlLocatorTuples: UrlOrUrlLocatorTuple[]; - conditionalHeaders: ConditionalHeaders; - layout: LayoutInstance; - browserTimezone?: string; -} - -export interface AttributesMap { - [key: string]: string | null; -} - -export interface ElementPosition { - boundingClientRect: { - // modern browsers support x/y, but older ones don't - top: number; - left: number; - width: number; - height: number; - }; - scroll: { - x: number; - y: number; - }; -} - -export interface ElementsPositionAndAttribute { - position: ElementPosition; - attributes: AttributesMap; -} - -export interface Screenshot { - data: Buffer; - title: string | null; - description: string | null; -} - -export interface PageSetupResults { - elementsPositionAndAttributes: ElementsPositionAndAttribute[] | null; - timeRange: string | null; - error?: Error; -} - -export interface ScreenshotResults { - timeRange: string | null; - screenshots: Screenshot[]; - error?: Error; - - /** - * Individual visualizations might encounter errors at runtime. If there are any they are added to this - * field. Any text captured here is intended to be shown to the user for debugging purposes, reporting - * does no further sanitization on these strings. - */ - renderErrors?: string[]; - elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing -} diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts deleted file mode 100644 index 3071ecb54dc26..0000000000000 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.test.ts +++ /dev/null @@ -1,490 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -jest.mock('puppeteer', () => ({ - launch: () => ({ - // Fixme needs event emitters - newPage: () => ({ - emulateTimezone: jest.fn(), - setDefaultTimeout: jest.fn(), - }), - process: jest.fn(), - close: jest.fn(), - }), -})); - -import moment from 'moment'; -import * as Rx from 'rxjs'; -import { ReportingCore } from '../..'; -import { HeadlessChromiumDriver } from '../../browsers'; -import { ConditionalHeaders } from '../../export_types/common'; -import { - createMockBrowserDriverFactory, - createMockConfig, - createMockConfigSchema, - createMockLayoutInstance, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; -import * as contexts from './constants'; -import { getScreenshots$ } from './'; - -/* - * Mocks - */ -const logger = createMockLevelLogger(); - -const mockSchema = createMockConfigSchema({ - capture: { - loadDelay: moment.duration(2, 's'), - timeouts: { - openUrl: moment.duration(2, 'm'), - waitForElements: moment.duration(20, 's'), - renderComplete: moment.duration(10, 's'), - }, - }, -}); -const mockConfig = createMockConfig(mockSchema); -const captureConfig = mockConfig.get('capture'); -const mockLayout = createMockLayoutInstance(captureConfig); - -let core: ReportingCore; - -/* - * Tests - */ -describe('Screenshot Observable Pipeline', () => { - let mockBrowserDriverFactory: any; - - beforeEach(async () => { - core = await createMockReportingCore(mockSchema); - mockBrowserDriverFactory = await createMockBrowserDriverFactory(core, logger, {}); - }); - - it('pipelines a single url into screenshot and timeRange', async () => { - const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, { - logger, - urlsOrUrlLocatorTuples: ['/welcome/home/start/index.htm'], - conditionalHeaders: {} as ConditionalHeaders, - layout: mockLayout, - browserTimezone: 'UTC', - }).toPromise(); - - expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "elementsPositionAndAttributes": Array [ - Object { - "attributes": Object { - "description": "Default ", - "title": "Default Mock Title", - }, - "position": Object { - "boundingClientRect": Object { - "height": 600, - "left": 0, - "top": 0, - "width": 800, - }, - "scroll": Object { - "x": 0, - "y": 0, - }, - }, - }, - ], - "error": undefined, - "screenshots": Array [ - Object { - "data": Object { - "data": Array [ - 115, - 99, - 114, - 101, - 101, - 110, - 115, - 104, - 111, - 116, - ], - "type": "Buffer", - }, - "description": "Default ", - "title": "Default Mock Title", - }, - ], - "timeRange": "Default GetTimeRange Result", - }, - ] - `); - }); - - it('pipelines multiple urls into', async () => { - // mock implementations - const mockScreenshot = jest.fn(async () => Buffer.from('some screenshots')); - const mockOpen = jest.fn(); - - // mocks - mockBrowserDriverFactory = await createMockBrowserDriverFactory(core, logger, { - screenshot: mockScreenshot, - open: mockOpen, - }); - - // test - const result = await getScreenshots$(captureConfig, mockBrowserDriverFactory, { - logger, - urlsOrUrlLocatorTuples: [ - '/welcome/home/start/index2.htm', - '/welcome/home/start/index.php3?page=./home.php', - ], - conditionalHeaders: {} as ConditionalHeaders, - layout: mockLayout, - browserTimezone: 'UTC', - }).toPromise(); - - expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "elementsPositionAndAttributes": Array [ - Object { - "attributes": Object { - "description": "Default ", - "title": "Default Mock Title", - }, - "position": Object { - "boundingClientRect": Object { - "height": 600, - "left": 0, - "top": 0, - "width": 800, - }, - "scroll": Object { - "x": 0, - "y": 0, - }, - }, - }, - ], - "error": undefined, - "screenshots": Array [ - Object { - "data": Object { - "data": Array [ - 115, - 111, - 109, - 101, - 32, - 115, - 99, - 114, - 101, - 101, - 110, - 115, - 104, - 111, - 116, - 115, - ], - "type": "Buffer", - }, - "description": "Default ", - "title": "Default Mock Title", - }, - ], - "timeRange": "Default GetTimeRange Result", - }, - Object { - "elementsPositionAndAttributes": Array [ - Object { - "attributes": Object { - "description": "Default ", - "title": "Default Mock Title", - }, - "position": Object { - "boundingClientRect": Object { - "height": 600, - "left": 0, - "top": 0, - "width": 800, - }, - "scroll": Object { - "x": 0, - "y": 0, - }, - }, - }, - ], - "error": undefined, - "screenshots": Array [ - Object { - "data": Object { - "data": Array [ - 115, - 111, - 109, - 101, - 32, - 115, - 99, - 114, - 101, - 101, - 110, - 115, - 104, - 111, - 116, - 115, - ], - "type": "Buffer", - }, - "description": "Default ", - "title": "Default Mock Title", - }, - ], - "timeRange": "Default GetTimeRange Result", - }, - ] - `); - - // ensures the correct selectors are waited on for multi URL jobs - expect(mockOpen.mock.calls.length).toBe(2); - - const firstSelector = mockOpen.mock.calls[0][1].waitForSelector; - expect(firstSelector).toBe('.kbnAppWrapper'); - - const secondSelector = mockOpen.mock.calls[1][1].waitForSelector; - expect(secondSelector).toBe('[data-shared-page="2"]'); - }); - - describe('error handling', () => { - it('recovers if waitForSelector fails', async () => { - // mock implementations - const mockWaitForSelector = jest.fn().mockImplementation((selectorArg: string) => { - throw new Error('Mock error!'); - }); - - // mocks - mockBrowserDriverFactory = await createMockBrowserDriverFactory(core, logger, { - waitForSelector: mockWaitForSelector, - }); - - // test - const getScreenshot = async () => { - return await getScreenshots$(captureConfig, mockBrowserDriverFactory, { - logger, - urlsOrUrlLocatorTuples: [ - '/welcome/home/start/index2.htm', - '/welcome/home/start/index.php3?page=./home.php3', - ], - conditionalHeaders: {} as ConditionalHeaders, - layout: mockLayout, - browserTimezone: 'UTC', - }).toPromise(); - }; - - await expect(getScreenshot()).resolves.toMatchInlineSnapshot(` - Array [ - Object { - "elementsPositionAndAttributes": Array [ - Object { - "attributes": Object {}, - "position": Object { - "boundingClientRect": Object { - "height": 100, - "left": 0, - "top": 0, - "width": 100, - }, - "scroll": Object { - "x": 0, - "y": 0, - }, - }, - }, - ], - "error": [Error: The "wait for elements" phase encountered an error: Error: An error occurred when trying to read the page for visualization panel info: Error: Mock error!], - "screenshots": Array [ - Object { - "data": Object { - "data": Array [ - 115, - 99, - 114, - 101, - 101, - 110, - 115, - 104, - 111, - 116, - ], - "type": "Buffer", - }, - "description": undefined, - "title": undefined, - }, - ], - "timeRange": null, - }, - Object { - "elementsPositionAndAttributes": Array [ - Object { - "attributes": Object {}, - "position": Object { - "boundingClientRect": Object { - "height": 100, - "left": 0, - "top": 0, - "width": 100, - }, - "scroll": Object { - "x": 0, - "y": 0, - }, - }, - }, - ], - "error": [Error: The "wait for elements" phase encountered an error: Error: An error occurred when trying to read the page for visualization panel info: Error: Mock error!], - "screenshots": Array [ - Object { - "data": Object { - "data": Array [ - 115, - 99, - 114, - 101, - 101, - 110, - 115, - 104, - 111, - 116, - ], - "type": "Buffer", - }, - "description": undefined, - "title": undefined, - }, - ], - "timeRange": null, - }, - ] - `); - }); - - it('observes page exit', async () => { - // mocks - const mockGetCreatePage = (driver: HeadlessChromiumDriver) => - jest - .fn() - .mockImplementation(() => - Rx.of({ driver, exit$: Rx.throwError('Instant timeout has fired!') }) - ); - - const mockWaitForSelector = jest.fn().mockImplementation((selectorArg: string) => { - return Rx.never().toPromise(); - }); - - mockBrowserDriverFactory = await createMockBrowserDriverFactory(core, logger, { - getCreatePage: mockGetCreatePage, - waitForSelector: mockWaitForSelector, - }); - - // test - const getScreenshot = async () => { - return await getScreenshots$(captureConfig, mockBrowserDriverFactory, { - logger, - urlsOrUrlLocatorTuples: ['/welcome/home/start/index.php3?page=./home.php3'], - conditionalHeaders: {} as ConditionalHeaders, - layout: mockLayout, - browserTimezone: 'UTC', - }).toPromise(); - }; - - await expect(getScreenshot()).rejects.toMatchInlineSnapshot(`"Instant timeout has fired!"`); - }); - - it(`uses defaults for element positions and size when Kibana page is not ready`, async () => { - // mocks - const mockBrowserEvaluate = jest.fn(); - mockBrowserEvaluate.mockImplementation(() => { - const lastCallIndex = mockBrowserEvaluate.mock.calls.length - 1; - const { context: mockCall } = mockBrowserEvaluate.mock.calls[lastCallIndex][1]; - - if (mockCall === contexts.CONTEXT_ELEMENTATTRIBUTES) { - return Promise.resolve(null); - } else { - return Promise.resolve(); - } - }); - mockBrowserDriverFactory = await createMockBrowserDriverFactory(core, logger, { - evaluate: mockBrowserEvaluate, - }); - mockLayout.getViewport = () => null; - - const screenshots = await getScreenshots$(captureConfig, mockBrowserDriverFactory, { - logger, - urlsOrUrlLocatorTuples: ['/welcome/home/start/index.php3?page=./home.php3'], - conditionalHeaders: {} as ConditionalHeaders, - layout: mockLayout, - browserTimezone: 'UTC', - }).toPromise(); - - expect(screenshots).toMatchInlineSnapshot(` - Array [ - Object { - "elementsPositionAndAttributes": Array [ - Object { - "attributes": Object {}, - "position": Object { - "boundingClientRect": Object { - "height": 1200, - "left": 0, - "top": 0, - "width": 1800, - }, - "scroll": Object { - "x": 0, - "y": 0, - }, - }, - }, - ], - "error": undefined, - "screenshots": Array [ - Object { - "data": Object { - "data": Array [ - 115, - 99, - 114, - 101, - 101, - 110, - 115, - 104, - 111, - 116, - ], - "type": "Buffer", - }, - "description": undefined, - "title": undefined, - }, - ], - "timeRange": undefined, - }, - ] - `); - }); - }); -}); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable.ts deleted file mode 100644 index 8ba2a125a5504..0000000000000 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable.ts +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import apm from 'elastic-apm-node'; -import * as Rx from 'rxjs'; -import { catchError, concatMap, first, mergeMap, take, takeUntil, toArray } from 'rxjs/operators'; -import { durationToNumber } from '../../../common/schema_utils'; -import { REPORTING_TRANSACTION_TYPE } from '../../../common/constants'; -import { HeadlessChromiumDriverFactory } from '../../browsers'; -import { CaptureConfig } from '../../types'; -import { - ElementPosition, - ElementsPositionAndAttribute, - PageSetupResults, - ScreenshotObservableOpts, - ScreenshotResults, -} from './'; -import { ScreenshotObservableHandler } from './observable_handler'; - -export type { ElementPosition, ElementsPositionAndAttribute, ScreenshotResults }; - -const getTimeouts = (captureConfig: CaptureConfig) => ({ - openUrl: { - timeoutValue: durationToNumber(captureConfig.timeouts.openUrl), - configValue: `xpack.reporting.capture.timeouts.openUrl`, - label: 'open URL', - }, - waitForElements: { - timeoutValue: durationToNumber(captureConfig.timeouts.waitForElements), - configValue: `xpack.reporting.capture.timeouts.waitForElements`, - label: 'wait for elements', - }, - renderComplete: { - timeoutValue: durationToNumber(captureConfig.timeouts.renderComplete), - configValue: `xpack.reporting.capture.timeouts.renderComplete`, - label: 'render complete', - }, - loadDelay: durationToNumber(captureConfig.loadDelay), -}); - -export function getScreenshots$( - captureConfig: CaptureConfig, - browserDriverFactory: HeadlessChromiumDriverFactory, - opts: ScreenshotObservableOpts -): Rx.Observable { - const apmTrans = apm.startTransaction('screenshot-pipeline', REPORTING_TRANSACTION_TYPE); - const apmCreatePage = apmTrans?.startSpan('create-page', 'wait'); - const { browserTimezone, logger } = opts; - - return browserDriverFactory.createPage({ browserTimezone }, logger).pipe( - mergeMap(({ driver, exit$ }) => { - apmCreatePage?.end(); - exit$.subscribe({ error: () => apmTrans?.end() }); - - const screen = new ScreenshotObservableHandler(driver, opts, getTimeouts(captureConfig)); - - return Rx.from(opts.urlsOrUrlLocatorTuples).pipe( - concatMap((urlOrUrlLocatorTuple, index) => - screen.setupPage(index, urlOrUrlLocatorTuple, apmTrans).pipe( - catchError((err) => { - screen.checkPageIsOpen(); // this fails the job if the browser has closed - - logger.error(err); - return Rx.of({ ...defaultSetupResult, error: err }); // allow failover screenshot capture - }), - takeUntil(exit$), - screen.getScreenshots() - ) - ), - take(opts.urlsOrUrlLocatorTuples.length), - toArray() - ); - }), - first() - ); -} - -const defaultSetupResult: PageSetupResults = { - elementsPositionAndAttributes: null, - timeRange: null, -}; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.test.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.test.ts deleted file mode 100644 index cb0a513992722..0000000000000 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.test.ts +++ /dev/null @@ -1,160 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as Rx from 'rxjs'; -import { first, map } from 'rxjs/operators'; -import { HeadlessChromiumDriver } from '../../browsers'; -import { ReportingConfigType } from '../../config'; -import { ConditionalHeaders } from '../../export_types/common'; -import { - createMockBrowserDriverFactory, - createMockConfigSchema, - createMockLayoutInstance, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; -import { LayoutInstance } from '../layouts'; -import { PhaseTimeouts, ScreenshotObservableOpts } from './'; -import { ScreenshotObservableHandler } from './observable_handler'; - -const logger = createMockLevelLogger(); - -describe('ScreenshotObservableHandler', () => { - let captureConfig: ReportingConfigType['capture']; - let layout: LayoutInstance; - let conditionalHeaders: ConditionalHeaders; - let opts: ScreenshotObservableOpts; - let timeouts: PhaseTimeouts; - let driver: HeadlessChromiumDriver; - - beforeAll(async () => { - captureConfig = { - timeouts: { - openUrl: 30000, - waitForElements: 30000, - renderComplete: 30000, - }, - loadDelay: 5000, - } as unknown as typeof captureConfig; - - layout = createMockLayoutInstance(captureConfig); - - conditionalHeaders = { - headers: { testHeader: 'testHeadValue' }, - conditions: {} as unknown as ConditionalHeaders['conditions'], - }; - - opts = { - conditionalHeaders, - layout, - logger, - urlsOrUrlLocatorTuples: [], - }; - - timeouts = { - openUrl: { - timeoutValue: 60000, - configValue: `xpack.reporting.capture.timeouts.openUrl`, - label: 'open URL', - }, - waitForElements: { - timeoutValue: 30000, - configValue: `xpack.reporting.capture.timeouts.waitForElements`, - label: 'wait for elements', - }, - renderComplete: { - timeoutValue: 60000, - configValue: `xpack.reporting.capture.timeouts.renderComplete`, - label: 'render complete', - }, - loadDelay: 5000, - }; - }); - - beforeEach(async () => { - const reporting = await createMockReportingCore(createMockConfigSchema()); - const driverFactory = await createMockBrowserDriverFactory(reporting, logger); - ({ driver } = await driverFactory.createPage({}, logger).pipe(first()).toPromise()); - driver.isPageOpen = jest.fn().mockImplementation(() => true); - }); - - describe('waitUntil', () => { - it('catches TimeoutError and references the timeout config in a custom message', async () => { - const screenshots = new ScreenshotObservableHandler(driver, opts, timeouts); - const test$ = Rx.interval(1000).pipe( - screenshots.waitUntil({ - timeoutValue: 200, - configValue: 'test.config.value', - label: 'Test Config', - }) - ); - - const testPipeline = () => test$.toPromise(); - await expect(testPipeline).rejects.toMatchInlineSnapshot( - `[Error: The "Test Config" phase took longer than 0.2 seconds. You may need to increase "test.config.value"]` - ); - }); - - it('catches other Errors and explains where they were thrown', async () => { - const screenshots = new ScreenshotObservableHandler(driver, opts, timeouts); - const test$ = Rx.throwError(new Error(`Test Error to Throw`)).pipe( - screenshots.waitUntil({ - timeoutValue: 200, - configValue: 'test.config.value', - label: 'Test Config', - }) - ); - - const testPipeline = () => test$.toPromise(); - await expect(testPipeline).rejects.toMatchInlineSnapshot( - `[Error: The "Test Config" phase encountered an error: Error: Test Error to Throw]` - ); - }); - - it('is a pass-through if there is no Error', async () => { - const screenshots = new ScreenshotObservableHandler(driver, opts, timeouts); - const test$ = Rx.of('nice to see you').pipe( - screenshots.waitUntil({ - timeoutValue: 20, - configValue: 'xxxxxxxxxxxxxxxxx', - label: 'xxxxxxxxxxx', - }) - ); - - await expect(test$.toPromise()).resolves.toBe(`nice to see you`); - }); - }); - - describe('checkPageIsOpen', () => { - it('throws a decorated Error when page is not open', async () => { - driver.isPageOpen = jest.fn().mockImplementation(() => false); - const screenshots = new ScreenshotObservableHandler(driver, opts, timeouts); - const test$ = Rx.of(234455).pipe( - map((input) => { - screenshots.checkPageIsOpen(); - return input; - }) - ); - - await expect(test$.toPromise()).rejects.toMatchInlineSnapshot( - `[Error: Browser was closed unexpectedly! Check the server logs for more info.]` - ); - }); - - it('is a pass-through when the page is open', async () => { - const screenshots = new ScreenshotObservableHandler(driver, opts, timeouts); - const test$ = Rx.of(234455).pipe( - map((input) => { - screenshots.checkPageIsOpen(); - return input; - }) - ); - - await expect(test$.toPromise()).resolves.toBe(234455); - }); - }); -}); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.ts b/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.ts deleted file mode 100644 index c241a529818fa..0000000000000 --- a/x-pack/plugins/reporting/server/lib/screenshots/observable_handler.ts +++ /dev/null @@ -1,197 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import apm from 'elastic-apm-node'; -import * as Rx from 'rxjs'; -import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators'; -import { numberToDuration } from '../../../common/schema_utils'; -import { UrlOrUrlLocatorTuple } from '../../../common/types'; -import { HeadlessChromiumDriver } from '../../browsers'; -import { getChromiumDisconnectedError } from '../../browsers/chromium'; -import { - PageSetupResults, - PhaseInstance, - PhaseTimeouts, - ScreenshotObservableOpts, - ScreenshotResults, -} from './'; -import { getElementPositionAndAttributes } from './get_element_position_data'; -import { getNumberOfItems } from './get_number_of_items'; -import { getRenderErrors } from './get_render_errors'; -import { getScreenshots } from './get_screenshots'; -import { getTimeRange } from './get_time_range'; -import { injectCustomCss } from './inject_css'; -import { openUrl } from './open_url'; -import { waitForRenderComplete } from './wait_for_render'; -import { waitForVisualizations } from './wait_for_visualizations'; - -export class ScreenshotObservableHandler { - private conditionalHeaders: ScreenshotObservableOpts['conditionalHeaders']; - private layout: ScreenshotObservableOpts['layout']; - private logger: ScreenshotObservableOpts['logger']; - - constructor( - private readonly driver: HeadlessChromiumDriver, - opts: ScreenshotObservableOpts, - private timeouts: PhaseTimeouts - ) { - this.conditionalHeaders = opts.conditionalHeaders; - this.layout = opts.layout; - this.logger = opts.logger; - } - - /* - * Decorates a TimeoutError with context of the phase that has timed out. - */ - public waitUntil(phase: PhaseInstance) { - const { timeoutValue, label, configValue } = phase; - - return (source: Rx.Observable) => - source.pipe( - catchError((error) => { - throw new Error(`The "${label}" phase encountered an error: ${error}`); - }), - timeoutWith( - timeoutValue, - Rx.throwError( - new Error( - `The "${label}" phase took longer than ${numberToDuration( - timeoutValue - ).asSeconds()} seconds. You may need to increase "${configValue}"` - ) - ) - ) - ); - } - - private openUrl(index: number, urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple) { - return Rx.defer(() => - openUrl( - this.timeouts.openUrl.timeoutValue, - this.driver, - index, - urlOrUrlLocatorTuple, - this.conditionalHeaders, - this.layout, - this.logger - ) - ).pipe(this.waitUntil(this.timeouts.openUrl)); - } - - private waitForElements() { - const driver = this.driver; - const waitTimeout = this.timeouts.waitForElements.timeoutValue; - - return Rx.defer(() => getNumberOfItems(waitTimeout, driver, this.layout, this.logger)).pipe( - mergeMap((itemsCount) => { - // set the viewport to the dimentions from the job, to allow elements to flow into the expected layout - const viewport = this.layout.getViewport(itemsCount) || getDefaultViewPort(); - - return Rx.forkJoin([ - driver.setViewport(viewport, this.logger), - waitForVisualizations(waitTimeout, driver, itemsCount, this.layout, this.logger), - ]); - }), - this.waitUntil(this.timeouts.waitForElements) - ); - } - - private completeRender(apmTrans: apm.Transaction | null) { - const driver = this.driver; - const layout = this.layout; - const logger = this.logger; - - return Rx.defer(async () => { - // Waiting till _after_ elements have rendered before injecting our CSS - // allows for them to be displayed properly in many cases - await injectCustomCss(driver, layout, logger); - - const apmPositionElements = apmTrans?.startSpan('position-elements', 'correction'); - // position panel elements for print layout - await layout.positionElements?.(driver, logger); - apmPositionElements?.end(); - - await waitForRenderComplete(this.timeouts.loadDelay, driver, layout, logger); - }).pipe( - mergeMap(() => - Rx.forkJoin({ - timeRange: getTimeRange(driver, layout, logger), - elementsPositionAndAttributes: getElementPositionAndAttributes(driver, layout, logger), - renderErrors: getRenderErrors(driver, layout, logger), - }) - ), - this.waitUntil(this.timeouts.renderComplete) - ); - } - - public setupPage( - index: number, - urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple, - apmTrans: apm.Transaction | null - ) { - return this.openUrl(index, urlOrUrlLocatorTuple).pipe( - switchMapTo(this.waitForElements()), - switchMapTo(this.completeRender(apmTrans)) - ); - } - - public getScreenshots() { - return (withRenderComplete: Rx.Observable) => - withRenderComplete.pipe( - mergeMap(async (data: PageSetupResults): Promise => { - this.checkPageIsOpen(); // fail the report job if the browser has closed - - const elements = - data.elementsPositionAndAttributes ?? - getDefaultElementPosition(this.layout.getViewport(1)); - const screenshots = await getScreenshots(this.driver, elements, this.logger); - const { timeRange, error: setupError } = data; - - return { - timeRange, - screenshots, - error: setupError, - elementsPositionAndAttributes: elements, - }; - }) - ); - } - - public checkPageIsOpen() { - if (!this.driver.isPageOpen()) { - throw getChromiumDisconnectedError(); - } - } -} - -const DEFAULT_SCREENSHOT_CLIP_HEIGHT = 1200; -const DEFAULT_SCREENSHOT_CLIP_WIDTH = 1800; - -const getDefaultElementPosition = (dimensions: { height?: number; width?: number } | null) => { - const height = dimensions?.height || DEFAULT_SCREENSHOT_CLIP_HEIGHT; - const width = dimensions?.width || DEFAULT_SCREENSHOT_CLIP_WIDTH; - - return [ - { - position: { - boundingClientRect: { top: 0, left: 0, height, width }, - scroll: { x: 0, y: 0 }, - }, - attributes: {}, - }, - ]; -}; - -/* - * If Kibana is showing a non-HTML error message, the viewport might not be - * provided by the browser. - */ -const getDefaultViewPort = () => ({ - height: DEFAULT_SCREENSHOT_CLIP_HEIGHT, - width: DEFAULT_SCREENSHOT_CLIP_WIDTH, - zoom: 1, -}); diff --git a/x-pack/plugins/reporting/server/lib/store/mapping.ts b/x-pack/plugins/reporting/server/lib/store/mapping.ts index a43b4494fe913..667648d3372c5 100644 --- a/x-pack/plugins/reporting/server/lib/store/mapping.ts +++ b/x-pack/plugins/reporting/server/lib/store/mapping.ts @@ -34,7 +34,6 @@ export const mapping = { }, }, }, - browser_type: { type: 'keyword' }, migration_version: { type: 'keyword' }, // new field (7.14) to distinguish reports that were scheduled with Task Manager jobtype: { type: 'keyword' }, payload: { type: 'object', enabled: false }, diff --git a/x-pack/plugins/reporting/server/lib/store/report.test.ts b/x-pack/plugins/reporting/server/lib/store/report.test.ts index f9cd413b3e5a7..f6cbbade4df7b 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.test.ts @@ -13,7 +13,6 @@ describe('Class Report', () => { _index: '.reporting-test-index-12345', jobtype: 'test-report', created_by: 'created_by_test_string', - browser_type: 'browser_type_test_string', max_attempts: 50, payload: { headers: 'payload_test_field', @@ -28,7 +27,6 @@ describe('Class Report', () => { expect(report.toReportSource()).toMatchObject({ attempts: 0, - browser_type: 'browser_type_test_string', completed_at: undefined, created_by: 'created_by_test_string', jobtype: 'test-report', @@ -49,7 +47,6 @@ describe('Class Report', () => { }); expect(report.toApiJSON()).toMatchObject({ attempts: 0, - browser_type: 'browser_type_test_string', created_by: 'created_by_test_string', index: '.reporting-test-index-12345', jobtype: 'test-report', @@ -68,7 +65,6 @@ describe('Class Report', () => { _index: '.reporting-test-index-12345', jobtype: 'test-report', created_by: 'created_by_test_string', - browser_type: 'browser_type_test_string', max_attempts: 50, payload: { headers: 'payload_test_field', @@ -91,7 +87,6 @@ describe('Class Report', () => { expect(report.toReportSource()).toMatchObject({ attempts: 0, - browser_type: 'browser_type_test_string', completed_at: undefined, created_by: 'created_by_test_string', jobtype: 'test-report', @@ -113,7 +108,6 @@ describe('Class Report', () => { }); expect(report.toApiJSON()).toMatchObject({ attempts: 0, - browser_type: 'browser_type_test_string', completed_at: undefined, created_by: 'created_by_test_string', id: '12342p9o387549o2345', diff --git a/x-pack/plugins/reporting/server/lib/store/report.ts b/x-pack/plugins/reporting/server/lib/store/report.ts index 2f802334eb6ff..67f1ccdea5db8 100644 --- a/x-pack/plugins/reporting/server/lib/store/report.ts +++ b/x-pack/plugins/reporting/server/lib/store/report.ts @@ -38,7 +38,6 @@ export class Report implements Partial { public readonly payload: ReportSource['payload']; public readonly meta: ReportSource['meta']; - public readonly browser_type: ReportSource['browser_type']; public readonly status: ReportSource['status']; public readonly attempts: ReportSource['attempts']; @@ -82,7 +81,6 @@ export class Report implements Partial { this.max_attempts = opts.max_attempts; this.attempts = opts.attempts || 0; this.timeout = opts.timeout; - this.browser_type = opts.browser_type; this.process_expiration = opts.process_expiration; this.started_at = opts.started_at; @@ -125,7 +123,6 @@ export class Report implements Partial { meta: this.meta, timeout: this.timeout, max_attempts: this.max_attempts, - browser_type: this.browser_type, status: this.status, attempts: this.attempts, started_at: this.started_at, @@ -170,7 +167,6 @@ export class Report implements Partial { meta: this.meta, timeout: this.timeout, max_attempts: this.max_attempts, - browser_type: this.browser_type, status: this.status, attempts: this.attempts, started_at: this.started_at, 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 a28197d261ba2..c67dc3fa2d992 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.test.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.test.ts @@ -193,7 +193,6 @@ describe('ReportingStore', () => { status: 'pending', meta: { testMeta: 'meta' } as any, payload: { testPayload: 'payload' } as any, - browser_type: 'browser type string', attempts: 0, max_attempts: 1, timeout: 30000, @@ -214,7 +213,6 @@ describe('ReportingStore', () => { "_primary_term": 1234, "_seq_no": 5678, "attempts": 0, - "browser_type": "browser type string", "completed_at": undefined, "created_at": "some time", "created_by": "some security person", @@ -247,7 +245,6 @@ describe('ReportingStore', () => { _primary_term: 10002, jobtype: 'test-report', created_by: 'created_by_test_string', - browser_type: 'browser_type_test_string', max_attempts: 50, payload: { title: 'test report', @@ -279,7 +276,6 @@ describe('ReportingStore', () => { _primary_term: 10002, jobtype: 'test-report', created_by: 'created_by_test_string', - browser_type: 'browser_type_test_string', max_attempts: 50, payload: { title: 'test report', @@ -310,7 +306,6 @@ describe('ReportingStore', () => { _primary_term: 10002, jobtype: 'test-report', created_by: 'created_by_test_string', - browser_type: 'browser_type_test_string', max_attempts: 50, payload: { title: 'test report', @@ -341,7 +336,6 @@ describe('ReportingStore', () => { _primary_term: 10002, jobtype: 'test-report', created_by: 'created_by_test_string', - browser_type: 'browser_type_test_string', max_attempts: 50, payload: { title: 'test report', @@ -385,7 +379,6 @@ describe('ReportingStore', () => { _primary_term: 10002, jobtype: 'test-report-2', created_by: 'created_by_test_string', - browser_type: 'browser_type_test_string', status: 'processing', process_expiration: '2002', max_attempts: 3, diff --git a/x-pack/plugins/reporting/server/lib/store/store.ts b/x-pack/plugins/reporting/server/lib/store/store.ts index 43f57da8c21f7..7ddef6d66e275 100644 --- a/x-pack/plugins/reporting/server/lib/store/store.ts +++ b/x-pack/plugins/reporting/server/lib/store/store.ts @@ -24,7 +24,6 @@ import { MIGRATION_VERSION } from './report'; export type ReportProcessingFields = Required<{ kibana_id: Report['kibana_id']; kibana_name: Report['kibana_name']; - browser_type: Report['browser_type']; attempts: Report['attempts']; started_at: Report['started_at']; max_attempts: Report['max_attempts']; @@ -252,7 +251,6 @@ export class ReportingStore { _primary_term: document._primary_term, jobtype: document._source?.jobtype, attempts: document._source?.attempts, - browser_type: document._source?.browser_type, created_at: document._source?.created_at, created_by: document._source?.created_by, max_attempts: document._source?.max_attempts, diff --git a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts index 5f885ad127b43..b725c31da398d 100644 --- a/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts +++ b/x-pack/plugins/reporting/server/lib/tasks/execute_report.ts @@ -159,7 +159,6 @@ export class ExecuteReportTask implements ReportingTask { const doc: ReportProcessingFields = { kibana_id: this.kibanaId, kibana_name: this.kibanaName, - browser_type: this.config.capture.browser.type, attempts: report.attempts + 1, max_attempts: maxAttempts, started_at: startTime, diff --git a/x-pack/plugins/reporting/server/plugin.test.ts b/x-pack/plugins/reporting/server/plugin.test.ts index 9a2acc4a51202..4c04eb0c004e5 100644 --- a/x-pack/plugins/reporting/server/plugin.test.ts +++ b/x-pack/plugins/reporting/server/plugin.test.ts @@ -5,16 +5,6 @@ * 2.0. */ -jest.mock('./browsers/install', () => ({ - installBrowser: jest.fn().mockImplementation(() => ({ - binaryPath$: { - pipe: jest.fn().mockImplementation(() => ({ - toPromise: () => Promise.resolve(), - })), - }, - })), -})); - import { coreMock } from 'src/core/server/mocks'; import { featuresPluginMock } from '../../features/server/mocks'; import { TaskManagerSetupContract } from '../../task_manager/server'; diff --git a/x-pack/plugins/reporting/server/plugin.ts b/x-pack/plugins/reporting/server/plugin.ts index 8969a698a8ce4..0a2318daded02 100644 --- a/x-pack/plugins/reporting/server/plugin.ts +++ b/x-pack/plugins/reporting/server/plugin.ts @@ -8,7 +8,6 @@ import type { CoreSetup, CoreStart, Plugin, PluginInitializerContext } from 'src/core/server'; import { PLUGIN_ID } from '../common/constants'; import { ReportingCore } from './'; -import { initializeBrowserDriverFactory } from './browsers'; import { buildConfig, registerUiSettings, ReportingConfigType } from './config'; import { registerDeprecations } from './deprecations'; import { LevelLogger, ReportingStore } from './lib'; @@ -35,7 +34,7 @@ export class ReportingPlugin public setup(core: CoreSetup, plugins: ReportingSetupDeps) { const { http } = core; - const { screenshotMode, features, licensing, security, spaces, taskManager } = plugins; + const { features, licensing, security, spaces, taskManager } = plugins; const reportingCore = new ReportingCore(this.logger, this.initContext); @@ -53,7 +52,6 @@ export class ReportingPlugin const router = http.createRouter(); const basePath = http.basePath; reportingCore.pluginSetup({ - screenshotMode, features, licensing, basePath, @@ -98,11 +96,9 @@ export class ReportingPlugin (async () => { await reportingCore.pluginSetsUp(); - const browserDriverFactory = await initializeBrowserDriverFactory(reportingCore, this.logger); const store = new ReportingStore(reportingCore, this.logger); await reportingCore.pluginStart({ - browserDriverFactory, savedObjects: core.savedObjects, uiSettings: core.uiSettings, store, @@ -110,6 +106,7 @@ export class ReportingPlugin data: plugins.data, taskManager: plugins.taskManager, logger: this.logger, + screenshotting: plugins.screenshotting, }); // Note: this must be called after ReportingCore.pluginStart diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts index a27ce6a49b1a2..47dae7f96daa4 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.test.ts @@ -6,11 +6,10 @@ */ 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 * as Rx from 'rxjs'; +import type { ScreenshottingStart } from '../../../../screenshotting/server'; import { ReportingCore } from '../..'; import { createMockConfigSchema, @@ -21,17 +20,11 @@ import { import type { ReportingRequestHandlerContext } from '../../types'; 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'; -const wait = (ms: number): Rx.Observable<0> => - Rx.from(new Promise<0>((resolve) => setTimeout(() => resolve(0), ms))); - describe('POST /diagnose/browser', () => { jest.setTimeout(6000); const reportingSymbol = Symbol('reporting'); @@ -40,12 +33,11 @@ describe('POST /diagnose/browser', () => { let server: SetupServerReturn['server']; let httpSetup: SetupServerReturn['httpSetup']; let core: ReportingCore; - const mockedSpawn: any = spawn; - const mockedCreateInterface: any = createInterface; + let screenshotting: jest.Mocked; const config = createMockConfigSchema({ queue: { timeout: 120000 }, - capture: { browser: { chromium: { proxy: { enabled: false } } } }, + capture: {}, }); beforeEach(async () => { @@ -56,9 +48,6 @@ describe('POST /diagnose/browser', () => { () => ({ usesUiCapabilities: () => false }) ); - // Make all uses of 'Rx.timer' return an observable that completes in 50ms - jest.spyOn(Rx, 'timer').mockImplementation(() => wait(50)); - core = await createMockReportingCore( config, createMockPluginSetup({ @@ -67,21 +56,7 @@ describe('POST /diagnose/browser', () => { }) ); - 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(), - })); + screenshotting = (await core.getPluginStartDeps()).screenshotting as typeof screenshotting; }); afterEach(async () => { @@ -94,12 +69,7 @@ describe('POST /diagnose/browser', () => { await server.start(); - mockedCreateInterface.mockImplementation(() => ({ - addEventListener: (_e: string, cb: any) => setTimeout(() => cb(devtoolMessage), 0), - removeEventListener: jest.fn(), - removeAllListeners: jest.fn(), - close: jest.fn(), - })); + screenshotting.diagnose.mockReturnValue(Rx.of(devtoolMessage)); return supertest(httpSetup.server.listener) .post('/api/reporting/diagnose/browser') @@ -115,20 +85,7 @@ describe('POST /diagnose/browser', () => { 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(), - })); + screenshotting.diagnose.mockReturnValue(Rx.of(logs)); return supertest(httpSetup.server.listener) .post('/api/reporting/diagnose/browser') @@ -139,8 +96,7 @@ describe('POST /diagnose/browser', () => { "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 - ", + "logs": "Could not find the default font", "success": false, } `); @@ -151,23 +107,7 @@ describe('POST /diagnose/browser', () => { 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(), - })); + screenshotting.diagnose.mockReturnValue(Rx.of(`${devtoolMessage}\n${fontNotFoundMessage}`)); return supertest(httpSetup.server.listener) .post('/api/reporting/diagnose/browser') @@ -179,89 +119,10 @@ describe('POST /diagnose/browser', () => { "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 - ", + 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 }) => { - const helpArray = [...body.help]; - helpArray.sort(); - expect(helpArray).toMatchInlineSnapshot(` - 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.", - ] - `); - expect(body.logs).toMatch(/Could not find the default font/); - expect(body.logs).toMatch(/Browser exited abnormally during startup/); - expect(body.success).toBe(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 index 7793fc658c535..f68df294b4118 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/browser.ts @@ -8,7 +8,6 @@ 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 { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; import { DiagnosticResponse } from './'; @@ -52,7 +51,8 @@ export const registerDiagnoseBrowser = (reporting: ReportingCore, logger: Logger }, authorizedUserPreRouting(reporting, async (_user, _context, _req, res) => { try { - const logs = await browserStartLogs(reporting, logger).toPromise(); + const { screenshotting } = await reporting.getPluginStartDeps(); + const logs = await screenshotting.diagnose().toPromise(); const knownIssues = Object.keys(logsToHelpMap) as Array; const boundSuccessfully = logs.includes(`DevTools listening on`); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts index dd543707fe66a..4bc33d20d6fcf 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.test.ts @@ -20,7 +20,7 @@ import type { ReportingRequestHandlerContext } from '../../types'; jest.mock('../../export_types/common/generate_png'); -import { generatePngObservableFactory } from '../../export_types/common'; +import { generatePngObservable } from '../../export_types/common'; type SetupServerReturn = UnwrapPromise>; @@ -31,12 +31,12 @@ describe('POST /diagnose/screenshot', () => { let core: ReportingCore; const setScreenshotResponse = (resp: object | Error) => { - const generateMock = Promise.resolve(() => ({ + const generateMock = { pipe: () => ({ toPromise: () => (resp instanceof Error ? Promise.reject(resp) : Promise.resolve(resp)), }), - })); - (generatePngObservableFactory as jest.Mock).mockResolvedValue(generateMock); + }; + (generatePngObservable as jest.Mock).mockReturnValue(generateMock); }; const config = createMockConfigSchema({ queue: { timeout: 120000 } }); diff --git a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts index f2002dd945882..2d5a254045104 100644 --- a/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts +++ b/x-pack/plugins/reporting/server/routes/diagnostic/screenshot.ts @@ -9,7 +9,7 @@ import { i18n } from '@kbn/i18n'; import { ReportingCore } from '../..'; import { APP_WRAPPER_CLASS } from '../../../../../../src/core/server'; import { API_DIAGNOSE_URL } from '../../../common/constants'; -import { omitBlockedHeaders, generatePngObservableFactory } from '../../export_types/common'; +import { omitBlockedHeaders, generatePngObservable } from '../../export_types/common'; import { getAbsoluteUrlFactory } from '../../export_types/common/get_absolute_url'; import { LevelLogger as Logger } from '../../lib'; import { authorizedUserPreRouting } from '../lib/authorized_user_pre_routing'; @@ -25,7 +25,6 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log validate: {}, }, authorizedUserPreRouting(reporting, 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] = [ @@ -40,7 +39,6 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log // Hack the layout to make the base/login page work const layout = { - id: 'png', dimensions: { width: 1440, height: 2024, @@ -53,7 +51,7 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log }, }; - const headers = { + const conditionalHeaders = { headers: omitBlockedHeaders(decryptedHeaders), conditions: { hostname, @@ -63,7 +61,12 @@ export const registerDiagnoseScreenshot = (reporting: ReportingCore, logger: Log }, }; - return generatePngObservable(logger, hashUrl, 'America/Los_Angeles', headers, layout) + return generatePngObservable(reporting, logger, { + conditionalHeaders, + layout, + browserTimezone: 'America/Los_Angeles', + urls: [hashUrl], + }) .pipe() .toPromise() .then((screenshot) => { diff --git a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts index 5900a151f92da..6d73a3ec7ee74 100644 --- a/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts +++ b/x-pack/plugins/reporting/server/routes/lib/request_handler.test.ts @@ -103,7 +103,6 @@ describe('Handle request to generate', () => { "_primary_term": undefined, "_seq_no": undefined, "attempts": 0, - "browser_type": undefined, "completed_at": undefined, "created_by": "testymcgee", "jobtype": "printable_pdf", @@ -180,7 +179,6 @@ describe('Handle request to generate', () => { expect(snapObj).toMatchInlineSnapshot(` Object { "attempts": 0, - "browser_type": undefined, "completed_at": undefined, "created_by": "testymcgee", "index": ".reporting-foo-index-234", diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts b/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts deleted file mode 100644 index d42fb73b447a5..0000000000000 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_browserdriverfactory.ts +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import moment from 'moment'; -import { Page } from 'puppeteer'; -import * as Rx from 'rxjs'; -import { ReportingCore } from '..'; -import { chromium, HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from '../browsers'; -import { LevelLogger } from '../lib'; -import { ElementsPositionAndAttribute } from '../lib/screenshots'; -import * as contexts from '../lib/screenshots/constants'; -import { CaptureConfig } from '../types'; - -interface CreateMockBrowserDriverFactoryOpts { - evaluate: jest.Mock, any[]>; - waitForSelector: jest.Mock, any[]>; - waitFor: jest.Mock, any[]>; - screenshot: jest.Mock, any[]>; - open: jest.Mock, any[]>; - getCreatePage: (driver: HeadlessChromiumDriver) => jest.Mock; -} - -const mockSelectors = { - renderComplete: 'renderedSelector', - itemsCountAttribute: 'itemsSelector', - screenshot: 'screenshotSelector', - timefilterDurationAttribute: 'timefilterDurationSelector', - toastHeader: 'toastHeaderSelector', -}; - -const getMockElementsPositionAndAttributes = ( - title: string, - description: string -): ElementsPositionAndAttribute[] => [ - { - position: { - boundingClientRect: { top: 0, left: 0, width: 800, height: 600 }, - scroll: { x: 0, y: 0 }, - }, - attributes: { title, description }, - }, -]; - -const mockWaitForSelector = jest.fn(); -mockWaitForSelector.mockImplementation((selectorArg: string) => { - const { renderComplete, itemsCountAttribute, toastHeader } = mockSelectors; - if (selectorArg === `${renderComplete},[${itemsCountAttribute}]`) { - return Promise.resolve(true); - } else if (selectorArg === toastHeader) { - return Rx.never().toPromise(); - } - throw new Error(selectorArg); -}); -const mockBrowserEvaluate = jest.fn(); -mockBrowserEvaluate.mockImplementation(() => { - const lastCallIndex = mockBrowserEvaluate.mock.calls.length - 1; - const { context: mockCall } = mockBrowserEvaluate.mock.calls[lastCallIndex][1]; - - if (mockCall === contexts.CONTEXT_SKIPTELEMETRY) { - return Promise.resolve(); - } - if (mockCall === contexts.CONTEXT_GETNUMBEROFITEMS) { - return Promise.resolve(1); - } - if (mockCall === contexts.CONTEXT_INJECTCSS) { - return Promise.resolve(); - } - if (mockCall === contexts.CONTEXT_WAITFORRENDER) { - return Promise.resolve(); - } - if (mockCall === contexts.CONTEXT_GETTIMERANGE) { - return Promise.resolve('Default GetTimeRange Result'); - } - if (mockCall === contexts.CONTEXT_ELEMENTATTRIBUTES) { - return Promise.resolve(getMockElementsPositionAndAttributes('Default Mock Title', 'Default ')); - } - if (mockCall === contexts.CONTEXT_GETRENDERERRORS) { - return Promise.resolve(); - } - throw new Error(mockCall); -}); -const mockScreenshot = jest.fn(async () => Buffer.from('screenshot')); -const getCreatePage = (driver: HeadlessChromiumDriver) => - jest.fn().mockImplementation(() => Rx.of({ driver, exit$: Rx.never() })); - -const defaultOpts: CreateMockBrowserDriverFactoryOpts = { - evaluate: mockBrowserEvaluate, - waitForSelector: mockWaitForSelector, - waitFor: jest.fn(), - screenshot: mockScreenshot, - open: jest.fn(), - getCreatePage, -}; - -export const createMockBrowserDriverFactory = async ( - core: ReportingCore, - logger: LevelLogger, - opts: Partial = {} -): Promise => { - const captureConfig: CaptureConfig = { - timeouts: { - openUrl: moment.duration(60, 's'), - waitForElements: moment.duration(30, 's'), - renderComplete: moment.duration(30, 's'), - }, - browser: { - type: 'chromium', - chromium: { - inspect: false, - disableSandbox: false, - proxy: { enabled: false, server: undefined, bypass: undefined }, - }, - autoDownload: false, - }, - networkPolicy: { enabled: true, rules: [] }, - loadDelay: moment.duration(2, 's'), - zoom: 2, - maxAttempts: 1, - }; - - const binaryPath = '/usr/local/share/common/secure/super_awesome_binary'; - const mockBrowserDriverFactory = chromium.createDriverFactory(core, binaryPath, logger); - const mockPage = { setViewport: () => {} } as unknown as Page; - const mockBrowserDriver = new HeadlessChromiumDriver(core, mockPage, { - inspect: true, - networkPolicy: captureConfig.networkPolicy, - }); - - // mock the driver methods as either default mocks or passed-in - mockBrowserDriver.waitForSelector = opts.waitForSelector ? opts.waitForSelector : defaultOpts.waitForSelector; // prettier-ignore - mockBrowserDriver.waitFor = opts.waitFor ? opts.waitFor : defaultOpts.waitFor; - mockBrowserDriver.evaluate = opts.evaluate ? opts.evaluate : defaultOpts.evaluate; - mockBrowserDriver.screenshot = opts.screenshot ? opts.screenshot : defaultOpts.screenshot; - mockBrowserDriver.open = opts.open ? opts.open : defaultOpts.open; - mockBrowserDriver.isPageOpen = () => true; - - mockBrowserDriverFactory.createPage = opts.getCreatePage - ? opts.getCreatePage(mockBrowserDriver) - : getCreatePage(mockBrowserDriver); - - return mockBrowserDriverFactory; -}; 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 c05b2c54aeabf..0569ea1400555 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 @@ -7,7 +7,6 @@ jest.mock('../routes'); jest.mock('../usage'); -jest.mock('../browsers'); import _ from 'lodash'; import * as Rx from 'rxjs'; @@ -18,24 +17,15 @@ import { FieldFormatsRegistry } from 'src/plugins/field_formats/common'; import { ReportingConfig, ReportingCore } from '../'; import { featuresPluginMock } from '../../../features/server/mocks'; import { securityMock } from '../../../security/server/mocks'; +// eslint-disable-next-line @kbn/eslint/no-restricted-paths +import { createMockScreenshottingStart } from '../../../screenshotting/server/mock'; import { taskManagerMock } from '../../../task_manager/server/mocks'; -import { - chromium, - HeadlessChromiumDriverFactory, - initializeBrowserDriverFactory, -} from '../browsers'; import { ReportingConfigType } from '../config'; import { ReportingInternalSetup, ReportingInternalStart } from '../core'; import { ReportingStore } from '../lib'; import { setFieldFormats } from '../services'; import { createMockLevelLogger } from './create_mock_levellogger'; -( - initializeBrowserDriverFactory as jest.Mock> -).mockImplementation(() => Promise.resolve({} as HeadlessChromiumDriverFactory)); - -(chromium as any).createDriverFactory.mockImplementation(() => ({})); - export const createMockPluginSetup = (setupMock?: any): ReportingInternalSetup => { return { features: featuresPluginMock.createSetup(), @@ -63,7 +53,6 @@ export const createMockPluginStart = ( : createMockReportingStore(); return { - browserDriverFactory: startMock.browserDriverFactory, esClient: elasticsearchServiceMock.createClusterClient(), savedObjects: startMock.savedObjects || { getScopedClient: jest.fn() }, uiSettings: startMock.uiSettings || { asScopedToClient: () => ({ get: jest.fn() }) }, @@ -74,6 +63,7 @@ export const createMockPluginStart = ( ensureScheduled: jest.fn(), } as any, logger: createMockLevelLogger(), + screenshotting: startMock.screenshotting || createMockScreenshottingStart(), ...startMock, }; }; @@ -102,14 +92,6 @@ export const createMockConfigSchema = ( port: 80, ...overrides.kibanaServer, }, - capture: { - browser: { - chromium: { - disableSandbox: true, - }, - }, - ...overrides.capture, - }, queue: { indexInterval: 'week', pollEnabled: true, diff --git a/x-pack/plugins/reporting/server/test_helpers/index.ts b/x-pack/plugins/reporting/server/test_helpers/index.ts index fe8c92d928af5..667c85c24a35d 100644 --- a/x-pack/plugins/reporting/server/test_helpers/index.ts +++ b/x-pack/plugins/reporting/server/test_helpers/index.ts @@ -5,8 +5,6 @@ * 2.0. */ -export { createMockBrowserDriverFactory } from './create_mock_browserdriverfactory'; -export { createMockLayoutInstance } from './create_mock_layoutinstance'; export { createMockLevelLogger } from './create_mock_levellogger'; export { createMockConfig, diff --git a/x-pack/plugins/reporting/server/types.ts b/x-pack/plugins/reporting/server/types.ts index af9a973b0bb45..3b1e819f0863c 100644 --- a/x-pack/plugins/reporting/server/types.ts +++ b/x-pack/plugins/reporting/server/types.ts @@ -11,13 +11,17 @@ import { DataPluginStart } from 'src/plugins/data/server/plugin'; import { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { Writable } from 'stream'; +import type { + ScreenshottingStart, + ScreenshotOptions as BaseScreenshotOptions, +} from '../../screenshotting/server'; import { PluginSetupContract as FeaturesPluginSetup } from '../../features/server'; import { LicensingPluginSetup } from '../../licensing/server'; import { AuthenticatedUser, SecurityPluginSetup } from '../../security/server'; import { SpacesPluginSetup } from '../../spaces/server'; import { TaskManagerSetupContract, TaskManagerStartContract } from '../../task_manager/server'; import { CancellationToken } from '../common'; -import { BaseParams, BasePayload, TaskRunResult } from '../common/types'; +import { BaseParams, BasePayload, TaskRunResult, UrlOrUrlLocatorTuple } from '../common/types'; import { ReportingConfigType } from './config'; import { ReportingCore } from './core'; import { LevelLogger } from './lib'; @@ -39,6 +43,7 @@ export interface ReportingSetupDeps { export interface ReportingStartDeps { data: DataPluginStart; + screenshotting: ScreenshottingStart; taskManager: TaskManagerStartContract; } @@ -109,3 +114,10 @@ export interface ReportingRequestHandlerContext { * @internal */ export type ReportingPluginRouter = IRouter; + +/** + * @internal + */ +export interface ScreenshotOptions extends Omit { + urls: UrlOrUrlLocatorTuple[]; +} diff --git a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap index 2017ae0be59c7..78bb9ab6df51f 100644 --- a/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap +++ b/x-pack/plugins/reporting/server/usage/__snapshots__/reporting_usage_collector.test.ts.snap @@ -129,9 +129,6 @@ Object { "available": Object { "type": "boolean", }, - "browser_type": Object { - "type": "keyword", - }, "csv": Object { "app": Object { "canvas workpad": Object { @@ -1973,7 +1970,6 @@ Object { }, "_all": 9, "available": true, - "browser_type": undefined, "csv": Object { "app": Object { "canvas workpad": 0, @@ -2243,7 +2239,6 @@ Object { }, "_all": 0, "available": true, - "browser_type": undefined, "csv": Object { "app": Object { "canvas workpad": 0, @@ -2492,7 +2487,6 @@ Object { }, "_all": 4, "available": true, - "browser_type": undefined, "csv": Object { "app": Object { "canvas workpad": 0, @@ -2768,7 +2762,6 @@ Object { }, "_all": 11, "available": true, - "browser_type": undefined, "csv": Object { "app": Object { "canvas workpad": 0, diff --git a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts index 73a4920b350e3..59387923e3755 100644 --- a/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts +++ b/x-pack/plugins/reporting/server/usage/get_reporting_usage.ts @@ -206,10 +206,6 @@ export async function getReportingUsage( .search(params) .then(({ body: response }) => handleResponse(response)) .then((usage: Partial): ReportingUsageType => { - // Allow this to explicitly throw an exception if/when this config is deprecated, - // because we shouldn't collect browserType in that case! - const browserType = config.get('capture', 'browser', 'type'); - const exportTypesHandler = getExportTypesHandler(exportTypesRegistry); const availability = exportTypesHandler.getAvailability( featureAvailability @@ -219,7 +215,6 @@ export async function getReportingUsage( return { available: true, - browser_type: browserType, enabled: true, last7Days: getExportStats(last7Days, availability, exportTypesHandler), ...getExportStats(all, availability, exportTypesHandler), diff --git a/x-pack/plugins/reporting/server/usage/schema.ts b/x-pack/plugins/reporting/server/usage/schema.ts index 9580ddb935dfb..fc464903edaee 100644 --- a/x-pack/plugins/reporting/server/usage/schema.ts +++ b/x-pack/plugins/reporting/server/usage/schema.ts @@ -92,7 +92,6 @@ const rangeStatsSchema: MakeSchemaFrom = { export const reportingSchema: MakeSchemaFrom = { ...rangeStatsSchema, available: { type: 'boolean' }, - browser_type: { type: 'keyword' }, enabled: { type: 'boolean' }, last7Days: rangeStatsSchema, }; diff --git a/x-pack/plugins/reporting/server/usage/types.ts b/x-pack/plugins/reporting/server/usage/types.ts index 856d3ad10cb26..e6695abc8da74 100644 --- a/x-pack/plugins/reporting/server/usage/types.ts +++ b/x-pack/plugins/reporting/server/usage/types.ts @@ -129,7 +129,6 @@ export type RangeStats = JobTypes & { export type ReportingUsageType = RangeStats & { available: boolean; - browser_type: string; enabled: boolean; last7Days: RangeStats; }; diff --git a/x-pack/plugins/reporting/tsconfig.json b/x-pack/plugins/reporting/tsconfig.json index 3e58450565720..4e09708915f95 100644 --- a/x-pack/plugins/reporting/tsconfig.json +++ b/x-pack/plugins/reporting/tsconfig.json @@ -26,6 +26,7 @@ { "path": "../../../src/plugins/field_formats/tsconfig.json" }, { "path": "../features/tsconfig.json" }, { "path": "../licensing/tsconfig.json" }, + { "path": "../screenshotting/tsconfig.json" }, { "path": "../security/tsconfig.json" }, { "path": "../spaces/tsconfig.json" }, ] diff --git a/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts index 1ea85e5a5434e..114d54eb7b4bb 100644 --- a/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts +++ b/x-pack/plugins/rule_registry/common/assets/field_maps/ecs_field_map.ts @@ -2925,6 +2925,16 @@ export const ecsFieldMap = { array: false, required: false, }, + 'threat.enrichments.feed': { + type: 'object', + array: false, + required: false, + }, + 'threat.enrichments.feed.name': { + type: 'keyword', + array: false, + required: false, + }, 'threat.enrichments.matched.atomic': { type: 'keyword', array: false, diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.mock.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.mock.ts new file mode 100644 index 0000000000000..0b3940b936424 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/resource_installer.mock.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import type { PublicMethodsOf } from '@kbn/utility-types'; +import { ResourceInstaller } from './resource_installer'; + +type Schema = PublicMethodsOf; +export type ResourceInstallerMock = jest.Mocked; +const createResourceInstallerMock = () => { + return { + installCommonResources: jest.fn(), + installIndexLevelResources: jest.fn(), + installAndUpdateNamespaceLevelResources: jest.fn(), + }; +}; + +export const resourceInstallerMock: { + create: () => ResourceInstallerMock; +} = { + create: createResourceInstallerMock, +}; diff --git a/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.ts new file mode 100644 index 0000000000000..4d217d1a181e9 --- /dev/null +++ b/x-pack/plugins/rule_registry/server/rule_data_plugin_service/rule_data_plugin_service.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loggerMock } from '@kbn/logging/mocks'; +import { RuleDataService } from './rule_data_plugin_service'; +import { elasticsearchServiceMock } from 'src/core/server/mocks'; +import { AlertConsumers } from '@kbn/rule-data-utils/alerts_as_data_rbac'; +import { Dataset } from './index_options'; +import { RuleDataClient } from '../rule_data_client/rule_data_client'; +import { createRuleDataClientMock as mockCreateRuleDataClient } from '../rule_data_client/rule_data_client.mock'; + +jest.mock('../rule_data_client/rule_data_client', () => ({ + RuleDataClient: jest.fn().mockImplementation(() => mockCreateRuleDataClient()), +})); + +describe('ruleDataPluginService', () => { + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('isRegistrationContextDisabled', () => { + it('should return true', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); + + const ruleDataService = new RuleDataService({ + logger: loggerMock.create(), + getClusterClient, + kibanaVersion: '8.1.0', + isWriteEnabled: true, + disabledRegistrationContexts: ['observability.logs'], + isWriterCacheEnabled: true, + }); + expect(ruleDataService.isRegistrationContextDisabled('observability.logs')).toBe(true); + }); + + it('should return false', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); + + const ruleDataService = new RuleDataService({ + logger: loggerMock.create(), + getClusterClient, + kibanaVersion: '8.1.0', + isWriteEnabled: true, + disabledRegistrationContexts: ['observability.logs'], + isWriterCacheEnabled: true, + }); + expect(ruleDataService.isRegistrationContextDisabled('observability.apm')).toBe(false); + }); + }); + + describe('isWriteEnabled', () => { + it('should return true', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); + + const ruleDataService = new RuleDataService({ + logger: loggerMock.create(), + getClusterClient, + kibanaVersion: '8.1.0', + isWriteEnabled: true, + disabledRegistrationContexts: ['observability.logs'], + isWriterCacheEnabled: true, + }); + + expect(ruleDataService.isWriteEnabled('observability.logs')).toBe(false); + }); + }); + + describe('initializeIndex', () => { + it('calls RuleDataClient', async () => { + const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient(); + const getClusterClient = jest.fn(() => Promise.resolve(mockClusterClient)); + + const ruleDataService = new RuleDataService({ + logger: loggerMock.create(), + getClusterClient, + kibanaVersion: '8.1.0', + isWriteEnabled: true, + disabledRegistrationContexts: ['observability.logs'], + isWriterCacheEnabled: true, + }); + const indexOptions = { + feature: AlertConsumers.LOGS, + registrationContext: 'observability.logs', + dataset: Dataset.alerts, + componentTemplateRefs: [], + componentTemplates: [ + { + name: 'mappings', + }, + ], + }; + await ruleDataService.initializeService(); + await ruleDataService.initializeIndex(indexOptions); + expect(RuleDataClient).toHaveBeenCalled(); + expect(RuleDataClient).toHaveBeenCalledWith( + expect.objectContaining({ + indexInfo: expect.objectContaining({ baseName: '.alerts-observability.logs.alerts' }), + }) + ); + }); + }); +}); diff --git a/x-pack/plugins/screenshotting/README.md b/x-pack/plugins/screenshotting/README.md new file mode 100644 index 0000000000000..3439f06dff8e5 --- /dev/null +++ b/x-pack/plugins/screenshotting/README.md @@ -0,0 +1,11 @@ +# Kibana Screenshotting + +This plugin provides functionality to take screenshots of the Kibana pages. +It uses Chromium and Puppeteer underneath to run the browser in headless mode. + +## API + +The plugin exposes most of the functionality in the start contract. +The Chromium download and setup is happening during the setup stage. + +To learn more about the public API, please use automatically generated API reference or generated TypeDoc comments. diff --git a/x-pack/plugins/screenshotting/common/context.ts b/x-pack/plugins/screenshotting/common/context.ts new file mode 100644 index 0000000000000..c47f8706533b8 --- /dev/null +++ b/x-pack/plugins/screenshotting/common/context.ts @@ -0,0 +1,17 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/** + * Screenshot context. + * This is a serializable object that can be passed from the screenshotting backend and then deserialized on the target page. + */ +export type Context = Record; + +/** + * @interal + */ +export const SCREENSHOTTING_CONTEXT_KEY = '__SCREENSHOTTING_CONTEXT_KEY__'; diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/index.ts b/x-pack/plugins/screenshotting/common/index.ts similarity index 66% rename from x-pack/plugins/reporting/server/browsers/chromium/driver/index.ts rename to x-pack/plugins/screenshotting/common/index.ts index afd31608d5a6e..04296dd5426b5 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/index.ts +++ b/x-pack/plugins/screenshotting/common/index.ts @@ -5,4 +5,6 @@ * 2.0. */ -export { HeadlessChromiumDriver } from './chromium_driver'; +export type { Context } from './context'; +export type { LayoutParams } from './layout'; +export { LayoutTypes } from './layout'; diff --git a/x-pack/plugins/screenshotting/common/layout.ts b/x-pack/plugins/screenshotting/common/layout.ts new file mode 100644 index 0000000000000..aade05eeea04e --- /dev/null +++ b/x-pack/plugins/screenshotting/common/layout.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Ensure, SerializableRecord } from '@kbn/utility-types'; + +/** + * @internal + */ +export type Size = Ensure< + { + /** + * Layout width. + */ + width: number; + + /** + * Layout height. + */ + height: number; + }, + SerializableRecord +>; + +/** + * @internal + */ +export interface LayoutSelectorDictionary { + screenshot: string; + renderComplete: string; + renderError: string; + renderErrorAttribute: string; + itemsCountAttribute: string; + timefilterDurationAttribute: string; +} + +/** + * Screenshot layout parameters. + */ +export type LayoutParams = Ensure< + { + /** + * Unique layout name. + */ + id?: string; + + /** + * Layout sizing. + */ + dimensions?: Size; + + /** + * Element selectors determining the page state. + */ + selectors?: Partial; + + /** + * Page zoom. + */ + zoom?: number; + }, + SerializableRecord +>; + +/** + * Supported layout types. + */ +export const LayoutTypes = { + PRESERVE_LAYOUT: 'preserve_layout', + PRINT: 'print', + CANVAS: 'canvas', // no margins or branding in the layout +}; diff --git a/x-pack/plugins/screenshotting/jest.config.js b/x-pack/plugins/screenshotting/jest.config.js new file mode 100644 index 0000000000000..a02d667f86a19 --- /dev/null +++ b/x-pack/plugins/screenshotting/jest.config.js @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +module.exports = { + preset: '@kbn/test', + rootDir: '../../..', + roots: ['/x-pack/plugins/screenshotting'], + coverageDirectory: '/target/kibana-coverage/jest/x-pack/plugins/screenshotting', + coverageReporters: ['text', 'html'], + collectCoverageFrom: ['/x-pack/plugins/screenshotting/server/**/*.{ts}'], +}; diff --git a/x-pack/plugins/screenshotting/kibana.json b/x-pack/plugins/screenshotting/kibana.json new file mode 100644 index 0000000000000..32446551627e0 --- /dev/null +++ b/x-pack/plugins/screenshotting/kibana.json @@ -0,0 +1,14 @@ +{ + "id": "screenshotting", + "version": "8.0.0", + "kibanaVersion": "kibana", + "owner": { + "name": "Kibana Reporting Services", + "githubTeam": "kibana-reporting-services" + }, + "description": "Kibana Screenshotting Plugin", + "requiredPlugins": ["screenshotMode"], + "configPath": ["xpack", "screenshotting"], + "server": true, + "ui": true +} diff --git a/x-pack/plugins/screenshotting/public/context_storage.ts b/x-pack/plugins/screenshotting/public/context_storage.ts new file mode 100644 index 0000000000000..76a2cf231cf83 --- /dev/null +++ b/x-pack/plugins/screenshotting/public/context_storage.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Context, SCREENSHOTTING_CONTEXT_KEY } from '../common/context'; + +declare global { + interface Window { + [SCREENSHOTTING_CONTEXT_KEY]?: Context; + } +} + +export class ContextStorage { + get(): T { + return (window[SCREENSHOTTING_CONTEXT_KEY] ?? {}) as T; + } +} diff --git a/x-pack/plugins/screenshotting/public/index.ts b/x-pack/plugins/screenshotting/public/index.ts new file mode 100644 index 0000000000000..659dbc81917a7 --- /dev/null +++ b/x-pack/plugins/screenshotting/public/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ScreenshottingPlugin } from './plugin'; + +/** + * Screenshotting plugin entry point. + */ +export function plugin(...args: ConstructorParameters) { + return new ScreenshottingPlugin(...args); +} + +export { LayoutTypes } from '../common'; +export type { ScreenshottingSetup, ScreenshottingStart } from './plugin'; diff --git a/x-pack/plugins/screenshotting/public/plugin.tsx b/x-pack/plugins/screenshotting/public/plugin.tsx new file mode 100755 index 0000000000000..4ba5046b8a881 --- /dev/null +++ b/x-pack/plugins/screenshotting/public/plugin.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Plugin } from 'src/core/public'; +import { ContextStorage } from './context_storage'; + +/** + * Setup public contract. + */ +export interface ScreenshottingSetup { + /** + * Gathers screenshot context that has been set on the backend. + */ + getContext: ContextStorage['get']; +} + +/** + * Start public contract. + */ +export type ScreenshottingStart = ScreenshottingSetup; + +export class ScreenshottingPlugin implements Plugin { + private contextStorage = new ContextStorage(); + + setup(): ScreenshottingSetup { + return { + getContext: () => this.contextStorage.get(), + }; + } + + start(): ScreenshottingStart { + return { + getContext: () => this.contextStorage.get(), + }; + } + + stop() {} +} diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts similarity index 75% rename from x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts rename to x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts index 0f2572ff2b2e4..245572efe9348 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver/chromium_driver.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver.ts @@ -8,22 +8,56 @@ import { i18n } from '@kbn/i18n'; import { map, truncate } from 'lodash'; import open from 'opn'; -import puppeteer, { ElementHandle, EvaluateFn, SerializableOrJSHandle } from 'puppeteer'; +import puppeteer, { ElementHandle, EvaluateFn, Page, SerializableOrJSHandle } from 'puppeteer'; import { parse as parseUrl } from 'url'; -import type { LocatorParams } from '../../../../common/types'; -import { REPORTING_REDIRECT_LOCATOR_STORE_KEY } from '../../../../common/constants'; -import { getDisallowedOutgoingUrlError } from '../'; -import { ReportingCore } from '../../..'; -import { KBN_SCREENSHOT_MODE_HEADER } from '../../../../../../../src/plugins/screenshot_mode/server'; -import { ConditionalHeaders, ConditionalHeadersConditions } from '../../../export_types/common'; -import { LevelLogger } from '../../../lib'; -import { Layout, ViewZoomWidthHeight } from '../../../lib/layouts/layout'; -import { ElementPosition } from '../../../lib/screenshots'; -import { allowRequest, NetworkPolicy } from '../../network_policy'; - -export interface ChromiumDriverOptions { - inspect: boolean; - networkPolicy: NetworkPolicy; +import { Logger } from 'src/core/server'; +import type { Layout } from 'src/plugins/screenshot_mode/common'; +import { + KBN_SCREENSHOT_MODE_HEADER, + ScreenshotModePluginSetup, +} from '../../../../../../src/plugins/screenshot_mode/server'; +import { Context, SCREENSHOTTING_CONTEXT_KEY } from '../../../common/context'; +import { ConfigType } from '../../config'; +import { allowRequest } from '../network_policy'; + +export interface ConditionalHeadersConditions { + protocol: string; + hostname: string; + port: number; + basePath: string; +} + +export interface ConditionalHeaders { + headers: Record; + conditions: ConditionalHeadersConditions; +} + +export interface ElementPosition { + boundingClientRect: { + // modern browsers support x/y, but older ones don't + top: number; + left: number; + width: number; + height: number; + }; + scroll: { + x: number; + y: number; + }; +} + +export interface Viewport { + zoom: number; + width: number; + height: number; +} + +interface OpenOptions { + conditionalHeaders: ConditionalHeaders; + context?: Context; + waitForSelector: string; + timeout: number; + layout?: Layout; } interface WaitForSelectorOpts { @@ -56,28 +90,30 @@ interface InterceptedRequest { const WAIT_FOR_DELAY_MS: number = 100; -export class HeadlessChromiumDriver { - private readonly page: puppeteer.Page; - private readonly inspect: boolean; - private readonly networkPolicy: NetworkPolicy; +function getDisallowedOutgoingUrlError(interceptedUrl: string) { + return new Error( + i18n.translate('xpack.screenshotting.chromiumDriver.disallowedOutgoingUrl', { + defaultMessage: `Received disallowed outgoing URL: "{interceptedUrl}". Failing the request and closing the browser.`, + values: { interceptedUrl }, + }) + ); +} +/** + * @internal + */ +export class HeadlessChromiumDriver { private listenersAttached = false; private interceptedCount = 0; - private core: ReportingCore; constructor( - core: ReportingCore, - page: puppeteer.Page, - { inspect, networkPolicy }: ChromiumDriverOptions - ) { - this.core = core; - this.page = page; - this.inspect = inspect; - this.networkPolicy = networkPolicy; - } + private screenshotMode: ScreenshotModePluginSetup, + private config: ConfigType, + private readonly page: Page + ) {} private allowRequest(url: string) { - return !this.networkPolicy.enabled || allowRequest(url, this.networkPolicy.rules); + return !this.config.networkPolicy.enabled || allowRequest(url, this.config.networkPolicy.rules); } private truncateUrl(url: string) { @@ -90,22 +126,16 @@ export class HeadlessChromiumDriver { /* * Call Page.goto and wait to see the Kibana DOM content */ - public async open( + async open( url: string, { conditionalHeaders, + context, + layout, waitForSelector: pageLoadSelector, timeout, - locator, - layout, - }: { - conditionalHeaders: ConditionalHeaders; - waitForSelector: string; - timeout: number; - locator?: LocatorParams; - layout?: Layout; - }, - logger: LevelLogger + }: OpenOptions, + logger: Logger ): Promise { logger.info(`opening url ${url}`); @@ -116,13 +146,9 @@ export class HeadlessChromiumDriver { * Integrate with the screenshot mode plugin contract by calling this function before any other * scripts have run on the browser page. */ - await this.page.evaluateOnNewDocument(this.core.getEnableScreenshotMode()); - - if (layout) { - await this.page.evaluateOnNewDocument(this.core.getSetScreenshotLayout(), layout.id); - } + await this.page.evaluateOnNewDocument(this.screenshotMode.setScreenshotModeEnabled); - if (locator) { + if (context) { await this.page.evaluateOnNewDocument( (key: string, value: unknown) => { Object.defineProperty(window, key, { @@ -132,18 +158,20 @@ export class HeadlessChromiumDriver { value, }); }, - REPORTING_REDIRECT_LOCATOR_STORE_KEY, - locator + SCREENSHOTTING_CONTEXT_KEY, + context ); } - await this.page.setRequestInterception(true); + if (layout) { + await this.page.evaluateOnNewDocument(this.screenshotMode.setScreenshotLayout, layout); + } + await this.page.setRequestInterception(true); this.registerListeners(conditionalHeaders, logger); - await this.page.goto(url, { waitUntil: 'domcontentloaded' }); - if (this.inspect) { + if (this.config.browser.chromium.inspect) { await this.launchDebugger(); } @@ -159,14 +187,14 @@ export class HeadlessChromiumDriver { /* * Let modules poll if Chrome is still running so they can short circuit if needed */ - public isPageOpen() { + isPageOpen() { return !this.page.isClosed(); } /* * Call Page.screenshot and return a base64-encoded string of the image */ - public async screenshot(elementPosition: ElementPosition): Promise { + async screenshot(elementPosition: ElementPosition): Promise { const { boundingClientRect, scroll } = elementPosition; const screenshot = await this.page.screenshot({ clip: { @@ -188,32 +216,28 @@ export class HeadlessChromiumDriver { return undefined; } - public async evaluate( - { fn, args = [] }: EvaluateOpts, - meta: EvaluateMetaOpts, - logger: LevelLogger - ) { + evaluate({ fn, args = [] }: EvaluateOpts, meta: EvaluateMetaOpts, logger: Logger): Promise { logger.debug(`evaluate ${meta.context}`); - const result = await this.page.evaluate(fn, ...args); - return result; + + return this.page.evaluate(fn, ...args); } - public async waitForSelector( + async waitForSelector( selector: string, opts: WaitForSelectorOpts, context: EvaluateMetaOpts, - logger: LevelLogger + logger: Logger ): Promise> { const { timeout } = opts; logger.debug(`waitForSelector ${selector}`); - const resp = await this.page.waitForSelector(selector, { timeout }); // override default 30000ms + const response = await this.page.waitForSelector(selector, { timeout }); // override default 30000ms - if (!resp) { + if (!response) { throw new Error(`Failure in waitForSelector: void response! Context: ${context.context}`); } logger.debug(`waitForSelector ${selector} resolved`); - return resp; + return response; } public async waitFor({ @@ -228,9 +252,9 @@ export class HeadlessChromiumDriver { await this.page.waitForFunction(fn, { timeout, polling: WAIT_FOR_DELAY_MS }, ...args); } - public async setViewport( - { width: _width, height: _height, zoom }: ViewZoomWidthHeight, - logger: LevelLogger + async setViewport( + { width: _width, height: _height, zoom }: Viewport, + logger: Logger ): Promise { const width = Math.floor(_width); const height = Math.floor(_height); @@ -245,7 +269,7 @@ export class HeadlessChromiumDriver { }); } - private registerListeners(conditionalHeaders: ConditionalHeaders, logger: LevelLogger) { + private registerListeners(conditionalHeaders: ConditionalHeaders, logger: Logger) { if (this.listenersAttached) { return; } @@ -300,10 +324,13 @@ export class HeadlessChromiumDriver { }); } catch (err) { logger.error( - i18n.translate('xpack.reporting.chromiumDriver.failedToCompleteRequestUsingHeaders', { - defaultMessage: 'Failed to complete a request using headers: {error}', - values: { error: err }, - }) + i18n.translate( + 'xpack.screenshotting.chromiumDriver.failedToCompleteRequestUsingHeaders', + { + defaultMessage: 'Failed to complete a request using headers: {error}', + values: { error: err }, + } + ) ); } } else { @@ -313,7 +340,7 @@ export class HeadlessChromiumDriver { await client.send('Fetch.continueRequest', { requestId }); } catch (err) { logger.error( - i18n.translate('xpack.reporting.chromiumDriver.failedToCompleteRequest', { + i18n.translate('xpack.screenshotting.chromiumDriver.failedToCompleteRequest', { defaultMessage: 'Failed to complete a request: {error}', values: { error: err }, }) diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/args.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/args.ts similarity index 81% rename from x-pack/plugins/reporting/server/browsers/chromium/driver_factory/args.ts rename to x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/args.ts index 07ae13fa31849..e5985082b3c1c 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/args.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/args.ts @@ -5,18 +5,23 @@ * 2.0. */ -import { CaptureConfig } from '../../../../server/types'; -import { DEFAULT_VIEWPORT } from '../../../../common/constants'; +import type { ConfigType } from '../../../config'; -type BrowserConfig = CaptureConfig['browser']['chromium']; +interface Viewport { + height: number; + width: number; +} + +type Proxy = ConfigType['browser']['chromium']['proxy']; interface LaunchArgs { userDataDir: string; - disableSandbox: BrowserConfig['disableSandbox']; - proxy: BrowserConfig['proxy']; + viewport?: Viewport; + disableSandbox?: boolean; + proxy: Proxy; } -export const args = ({ userDataDir, disableSandbox, proxy: proxyConfig }: LaunchArgs) => { +export const args = ({ userDataDir, disableSandbox, viewport, proxy: proxyConfig }: LaunchArgs) => { const flags = [ // Disable built-in Google Translate service '--disable-translate', @@ -41,14 +46,17 @@ export const args = ({ userDataDir, disableSandbox, proxy: proxyConfig }: Launch '--disable-gpu', '--headless', '--hide-scrollbars', - // NOTE: setting the window size does NOT set the viewport size: viewport and window size are different. - // The viewport may later need to be resized depending on the position of the clip area. - // These numbers come from the job parameters, so this is a close guess. - `--window-size=${Math.floor(DEFAULT_VIEWPORT.width)},${Math.floor(DEFAULT_VIEWPORT.height)}`, // allow screenshot clip region to go outside of the viewport `--mainFrameClipsContent=false`, ]; + if (viewport) { + // NOTE: setting the window size does NOT set the viewport size: viewport and window size are different. + // The viewport may later need to be resized depending on the position of the clip area. + // These numbers come from the job parameters, so this is a close guess. + flags.push(`--window-size=${Math.floor(viewport.width)},${Math.floor(viewport.height)}`); + } + if (proxyConfig.enabled) { flags.push(`--proxy-server=${proxyConfig.server}`); if (proxyConfig.bypass) { diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.test.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.test.ts new file mode 100644 index 0000000000000..23e276541465a --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import puppeteer from 'puppeteer'; +import * as Rx from 'rxjs'; +import { take } from 'rxjs/operators'; +import type { Logger } from 'src/core/server'; +import type { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; +import { ConfigType } from '../../../config'; +import { HeadlessChromiumDriverFactory } from '.'; + +jest.mock('puppeteer'); + +describe('HeadlessChromiumDriverFactory', () => { + const path = 'path/to/headless_shell'; + const config = { + browser: { + chromium: { + proxy: {}, + }, + }, + } as ConfigType; + let logger: jest.Mocked; + let screenshotMode: jest.Mocked; + let factory: HeadlessChromiumDriverFactory; + + beforeEach(async () => { + logger = { + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + get: jest.fn(() => logger), + } as unknown as typeof logger; + screenshotMode = {} as unknown as typeof screenshotMode; + + (puppeteer as jest.Mocked).launch.mockResolvedValue({ + newPage: jest.fn().mockResolvedValue({ + target: jest.fn(() => ({ + createCDPSession: jest.fn().mockResolvedValue({ + send: jest.fn(), + }), + })), + emulateTimezone: jest.fn(), + setDefaultTimeout: jest.fn(), + }), + close: jest.fn(), + process: jest.fn(), + } as unknown as puppeteer.Browser); + + factory = new HeadlessChromiumDriverFactory(screenshotMode, config, logger, path); + jest.spyOn(factory, 'getBrowserLogger').mockReturnValue(Rx.EMPTY); + jest.spyOn(factory, 'getProcessLogger').mockReturnValue(Rx.EMPTY); + jest.spyOn(factory, 'getPageExit').mockReturnValue(Rx.EMPTY); + }); + + describe('createPage', () => { + it('returns browser driver and process exit observable', async () => { + await expect( + factory.createPage({ openUrlTimeout: 0 }).pipe(take(1)).toPromise() + ).resolves.toEqual( + expect.objectContaining({ + driver: expect.anything(), + exit$: expect.anything(), + }) + ); + }); + + it('rejects if Puppeteer launch fails', async () => { + (puppeteer as jest.Mocked).launch.mockRejectedValue( + `Puppeteer Launch mock fail.` + ); + expect(() => + factory.createPage({ openUrlTimeout: 0 }).pipe(take(1)).toPromise() + ).rejects.toThrowErrorMatchingInlineSnapshot( + `"Error spawning Chromium browser! Puppeteer Launch mock fail."` + ); + }); + }); +}); diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts new file mode 100644 index 0000000000000..e9656013140c2 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/index.ts @@ -0,0 +1,379 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { getDataPath } from '@kbn/utils'; +import { spawn } from 'child_process'; +import del from 'del'; +import fs from 'fs'; +import { uniq } from 'lodash'; +import path from 'path'; +import puppeteer, { Browser, ConsoleMessage, HTTPRequest, Page } from 'puppeteer'; +import { createInterface } from 'readline'; +import * as Rx from 'rxjs'; +import { InnerSubscriber } from 'rxjs/internal/InnerSubscriber'; +import { catchError, ignoreElements, map, mergeMap, reduce, takeUntil, tap } from 'rxjs/operators'; +import type { Logger } from 'src/core/server'; +import type { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; +import { ConfigType } from '../../../config'; +import { getChromiumDisconnectedError } from '../'; +import { safeChildProcess } from '../../safe_child_process'; +import { HeadlessChromiumDriver } from '../driver'; +import { args } from './args'; +import { getMetrics, PerformanceMetrics } from './metrics'; + +interface CreatePageOptions { + browserTimezone?: string; + openUrlTimeout: number; +} + +interface CreatePageResult { + driver: HeadlessChromiumDriver; + exit$: Rx.Observable; + metrics$: Rx.Observable; +} + +export const DEFAULT_VIEWPORT = { + width: 1950, + height: 1200, +}; + +// Default args used by pptr +// https://github.com/puppeteer/puppeteer/blob/13ea347/src/node/Launcher.ts#L168 +const DEFAULT_ARGS = [ + '--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', +]; + +const DIAGNOSTIC_TIME = 5 * 1000; + +export class HeadlessChromiumDriverFactory { + private userDataDir = fs.mkdtempSync(path.join(getDataPath(), 'chromium-')); + type = 'chromium'; + + constructor( + private screenshotMode: ScreenshotModePluginSetup, + private config: ConfigType, + private logger: Logger, + private binaryPath: string + ) { + if (this.config.browser.chromium.disableSandbox) { + logger.warn(`Enabling the Chromium sandbox provides an additional layer of protection.`); + } + } + + private getChromiumArgs() { + return args({ + userDataDir: this.userDataDir, + disableSandbox: this.config.browser.chromium.disableSandbox, + proxy: this.config.browser.chromium.proxy, + viewport: DEFAULT_VIEWPORT, + }); + } + + /* + * Return an observable to objects which will drive screenshot capture for a page + */ + createPage( + { browserTimezone, openUrlTimeout }: CreatePageOptions, + pLogger = this.logger + ): Rx.Observable { + // FIXME: 'create' is deprecated + return Rx.Observable.create(async (observer: InnerSubscriber) => { + const logger = pLogger.get('browser-driver'); + logger.info(`Creating browser page driver`); + + const chromiumArgs = this.getChromiumArgs(); + logger.debug(`Chromium launch args set to: ${chromiumArgs}`); + + let browser: Browser | undefined; + + try { + browser = await puppeteer.launch({ + pipe: !this.config.browser.chromium.inspect, + userDataDir: this.userDataDir, + executablePath: this.binaryPath, + ignoreHTTPSErrors: true, + handleSIGHUP: false, + args: chromiumArgs, + env: { + TZ: browserTimezone, + }, + }); + } catch (err) { + observer.error(new Error(`Error spawning Chromium browser! ${err}`)); + return; + } + + const page = await browser.newPage(); + const devTools = await page.target().createCDPSession(); + + await devTools.send('Performance.enable', { timeDomain: 'timeTicks' }); + const startMetrics = await devTools.send('Performance.getMetrics'); + const metrics$ = new Rx.Subject(); + + // Log version info for debugging / maintenance + const versionInfo = await devTools.send('Browser.getVersion'); + logger.debug(`Browser version: ${JSON.stringify(versionInfo)}`); + + await page.emulateTimezone(browserTimezone); + + // Set the default timeout for all navigation methods to the openUrl timeout + // All waitFor methods have their own timeout config passed in to them + page.setDefaultTimeout(openUrlTimeout); + + logger.debug(`Browser page driver created`); + + const childProcess = { + async kill() { + try { + if (devTools && startMetrics) { + const endMetrics = await devTools.send('Performance.getMetrics'); + const metrics = getMetrics(startMetrics, endMetrics); + const { cpuInPercentage, memoryInMegabytes } = metrics; + + metrics$.next(metrics); + logger.debug( + `Chromium consumed CPU ${cpuInPercentage}% Memory ${memoryInMegabytes}MB` + ); + } + } catch (error) { + logger.error(error); + } finally { + metrics$.complete(); + } + + try { + await browser?.close(); + } catch (err) { + // do not throw + logger.error(err); + } + }, + }; + const { terminate$ } = safeChildProcess(logger, childProcess); + + // this is adding unsubscribe logic to our observer + // so that if our observer unsubscribes, we terminate our child-process + observer.add(() => { + logger.debug(`The browser process observer has unsubscribed. Closing the browser...`); + childProcess.kill(); // ignore async + }); + + // make the observer subscribe to terminate$ + observer.add( + terminate$ + .pipe( + tap((signal) => { + logger.debug(`Termination signal received: ${signal}`); + }), + ignoreElements() + ) + .subscribe(observer) + ); + + // taps the browser log streams and combine them to Kibana logs + this.getBrowserLogger(page, logger).subscribe(); + this.getProcessLogger(browser, logger).subscribe(); + + // HeadlessChromiumDriver: object to "drive" a browser page + const driver = new HeadlessChromiumDriver(this.screenshotMode, this.config, page); + + // Rx.Observable: stream to interrupt page capture + const exit$ = this.getPageExit(browser, page); + + observer.next({ driver, exit$, metrics$: metrics$.asObservable() }); + + // unsubscribe logic makes a best-effort attempt to delete the user data directory used by chromium + observer.add(() => { + const userDataDir = this.userDataDir; + logger.debug(`deleting chromium user data directory at [${userDataDir}]`); + // the unsubscribe function isn't `async` so we're going to make our best effort at + // deleting the userDataDir and if it fails log an error. + del(userDataDir, { force: true }).catch((error) => { + logger.error(`error deleting user data directory at [${userDataDir}]!`); + logger.error(error); + }); + }); + }); + } + + getBrowserLogger(page: Page, logger: Logger): Rx.Observable { + const consoleMessages$ = Rx.fromEvent(page, 'console').pipe( + map((line) => { + const formatLine = () => `{ text: "${line.text()?.trim()}", url: ${line.location()?.url} }`; + + if (line.type() === 'error') { + logger.get('headless-browser-console').error(`Error in browser console: ${formatLine()}`); + } else { + logger + .get(`headless-browser-console:${line.type()}`) + .debug(`Message in browser console: ${formatLine()}`); + } + }) + ); + + const uncaughtExceptionPageError$ = Rx.fromEvent(page, 'pageerror').pipe( + map((err) => { + logger.warn( + i18n.translate('xpack.screenshotting.browsers.chromium.pageErrorDetected', { + defaultMessage: `Reporting encountered an uncaught error on the page that will be ignored: {err}`, + values: { err: err.toString() }, + }) + ); + }) + ); + + const pageRequestFailed$ = Rx.fromEvent(page, 'requestfailed').pipe( + map((req) => { + const failure = req.failure && req.failure(); + if (failure) { + logger.warn( + `Request to [${req.url()}] failed! [${failure.errorText}]. This error will be ignored.` + ); + } + }) + ); + + return Rx.merge(consoleMessages$, uncaughtExceptionPageError$, pageRequestFailed$); + } + + getProcessLogger(browser: Browser, logger: Logger): Rx.Observable { + const childProcess = browser.process(); + // NOTE: The browser driver can not observe stdout and stderr of the child process + // Puppeteer doesn't give a handle to the original ChildProcess object + // See https://github.com/GoogleChrome/puppeteer/issues/1292#issuecomment-521470627 + + if (childProcess == null) { + throw new TypeError('childProcess is null or undefined!'); + } + + // just log closing of the process + const processClose$ = Rx.fromEvent(childProcess, 'close').pipe( + tap(() => { + logger.get('headless-browser-process').debug('child process closed'); + }) + ); + + return processClose$; // ideally, this would also merge with observers for stdout and stderr + } + + getPageExit(browser: Browser, page: Page) { + const pageError$ = Rx.fromEvent(page, 'error').pipe( + mergeMap((err) => { + return Rx.throwError( + i18n.translate('xpack.screenshotting.browsers.chromium.errorDetected', { + defaultMessage: 'Reporting encountered an error: {err}', + values: { err: err.toString() }, + }) + ); + }) + ); + + const browserDisconnect$ = Rx.fromEvent(browser, 'disconnected').pipe( + mergeMap(() => Rx.throwError(getChromiumDisconnectedError())) + ); + + return Rx.merge(pageError$, browserDisconnect$); + } + + diagnose(overrideFlags: string[] = []): Rx.Observable { + const kbnArgs = this.getChromiumArgs(); + const finalArgs = uniq([...DEFAULT_ARGS, ...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(this.binaryPath, finalArgs, { + detached: process.platform !== 'win32', + }); + + const rl = createInterface({ input: browserProcess.stderr }); + + const exit$ = Rx.fromEvent(browserProcess, 'exit').pipe( + map((code) => { + this.logger.error(`Browser exited abnormally, received code: ${code}`); + return i18n.translate('xpack.screenshotting.diagnostic.browserCrashed', { + defaultMessage: `Browser exited abnormally during startup`, + }); + }) + ); + + const error$ = Rx.fromEvent(browserProcess, 'error').pipe( + map((err) => { + this.logger.error(`Browser process threw an error on startup`); + this.logger.error(err as string | Error); + return i18n.translate('xpack.screenshotting.diagnostic.browserErrored', { + defaultMessage: `Browser process threw an error on startup`, + }); + }) + ); + + const browserProcessLogger = this.logger.get('chromium-stderr'); + const log$ = Rx.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 Rx.merge(exit$, error$, log$).pipe( + takeUntil(Rx.timer(DIAGNOSTIC_TIME)), + reduce((acc, curr) => `${acc}${curr}\n`, ''), + tap(() => { + if (browserProcess && browserProcess.pid && !browserProcess.killed) { + browserProcess.kill('SIGKILL'); + this.logger.info( + `Successfully sent 'SIGKILL' to browser process (PID: ${browserProcess.pid})` + ); + } + browserProcess.removeAllListeners(); + rl.removeAllListeners(); + rl.close(); + del(this.userDataDir, { force: true }).catch((error) => { + this.logger.error(`Error deleting user data directory at [${this.userDataDir}]!`); + this.logger.error(error); + }); + }), + catchError((error) => { + this.logger.error(error); + + return Rx.of(error); + }) + ); + } +} + +export type { PerformanceMetrics }; diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/metrics.test.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/metrics.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/browsers/chromium/driver_factory/metrics.test.ts rename to x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/metrics.test.ts diff --git a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/metrics.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/metrics.ts similarity index 81% rename from x-pack/plugins/reporting/server/browsers/chromium/driver_factory/metrics.ts rename to x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/metrics.ts index 1659f28dea9b0..6e9971324ae4b 100644 --- a/x-pack/plugins/reporting/server/browsers/chromium/driver_factory/metrics.ts +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/driver_factory/metrics.ts @@ -29,10 +29,28 @@ interface NormalizedMetrics extends Required { ProcessTime: number; } -interface PerformanceMetrics { +/** + * Collected performance metrics during a screenshotting session. + */ +export interface PerformanceMetrics { + /** + * The percentage of CPU time spent by the browser divided by number or cores. + */ cpu: number; + + /** + * The percentage of CPU in percent untis. + */ cpuInPercentage: number; + + /** + * The total amount of memory used by the browser. + */ memory: number; + + /** + * The total amount of memory used by the browser in megabytes. + */ memoryInMegabytes: number; } diff --git a/x-pack/plugins/screenshotting/server/browsers/chromium/index.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/index.ts new file mode 100644 index 0000000000000..c51ee0e8b8651 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/chromium/index.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const getChromiumDisconnectedError = () => + new Error( + i18n.translate('xpack.screenshotting.screencapture.browserWasClosed', { + defaultMessage: 'Browser was closed unexpectedly! Check the server logs for more info.', + }) + ); + +export { ChromiumArchivePaths } from './paths'; +export type { ConditionalHeaders } from './driver'; +export { HeadlessChromiumDriver } from './driver'; +export type { PerformanceMetrics } from './driver_factory'; +export { DEFAULT_VIEWPORT, HeadlessChromiumDriverFactory } from './driver_factory'; diff --git a/x-pack/plugins/reporting/server/browsers/chromium/paths.ts b/x-pack/plugins/screenshotting/server/browsers/chromium/paths.ts similarity index 100% rename from x-pack/plugins/reporting/server/browsers/chromium/paths.ts rename to x-pack/plugins/screenshotting/server/browsers/chromium/paths.ts diff --git a/x-pack/plugins/reporting/server/browsers/download/checksum.test.ts b/x-pack/plugins/screenshotting/server/browsers/download/checksum.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/browsers/download/checksum.test.ts rename to x-pack/plugins/screenshotting/server/browsers/download/checksum.test.ts diff --git a/x-pack/plugins/reporting/server/browsers/download/checksum.ts b/x-pack/plugins/screenshotting/server/browsers/download/checksum.ts similarity index 62% rename from x-pack/plugins/reporting/server/browsers/download/checksum.ts rename to x-pack/plugins/screenshotting/server/browsers/download/checksum.ts index 35feb1ff534ab..9b177e0b4c756 100644 --- a/x-pack/plugins/reporting/server/browsers/download/checksum.ts +++ b/x-pack/plugins/screenshotting/server/browsers/download/checksum.ts @@ -7,16 +7,15 @@ import { createHash } from 'crypto'; import { createReadStream } from 'fs'; -import { Readable } from 'stream'; - -function readableEnd(stream: Readable) { - return new Promise((resolve, reject) => { - stream.on('error', reject).on('end', resolve); - }); -} +import { finished } from 'stream'; +import { promisify } from 'util'; export async function md5(path: string) { const hash = createHash('md5'); - await readableEnd(createReadStream(path).on('data', (chunk) => hash.update(chunk))); + const stream = createReadStream(path); + + stream.on('data', (chunk) => hash.update(chunk)); + await promisify(finished)(stream, { writable: false }); + return hash.digest('hex'); } diff --git a/x-pack/plugins/screenshotting/server/browsers/download/fetch.test.ts b/x-pack/plugins/screenshotting/server/browsers/download/fetch.test.ts new file mode 100644 index 0000000000000..cc22f152216af --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/download/fetch.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import mockFs from 'mock-fs'; +import axios from 'axios'; +import { createHash } from 'crypto'; +import { readFileSync } from 'fs'; +import { resolve as resolvePath } from 'path'; +import { Readable } from 'stream'; +import { fetch } from './fetch'; + +const TEMP_DIR = resolvePath(__dirname, '__tmp__'); +const TEMP_FILE = resolvePath(TEMP_DIR, 'foo/bar/download'); + +describe('fetch', () => { + beforeEach(() => { + jest.spyOn(axios, 'request').mockResolvedValue({ + data: new Readable({ + read() { + this.push('foobar'); + this.push(null); + }, + }), + }); + + mockFs(); + }); + + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + test('downloads the url to the path', async () => { + await fetch('url', TEMP_FILE); + + expect(readFileSync(TEMP_FILE, 'utf8')).toEqual('foobar'); + }); + + test('returns the md5 hex hash of the http body', async () => { + const hash = createHash('md5').update('foobar').digest('hex'); + + await expect(fetch('url', TEMP_FILE)).resolves.toEqual(hash); + }); + + test('throws if request emits an error', async () => { + (axios.request as jest.Mock).mockImplementationOnce(async () => { + throw new Error('foo'); + }); + + await expect(fetch('url', TEMP_FILE)).rejects.toThrow('foo'); + }); +}); diff --git a/x-pack/plugins/reporting/server/browsers/download/download.ts b/x-pack/plugins/screenshotting/server/browsers/download/fetch.ts similarity index 55% rename from x-pack/plugins/reporting/server/browsers/download/download.ts rename to x-pack/plugins/screenshotting/server/browsers/download/fetch.ts index 528395fe1afb2..aa52f7a4491c4 100644 --- a/x-pack/plugins/reporting/server/browsers/download/download.ts +++ b/x-pack/plugins/screenshotting/server/browsers/download/fetch.ts @@ -9,17 +9,15 @@ import Axios from 'axios'; import { createHash } from 'crypto'; import { closeSync, mkdirSync, openSync, writeSync } from 'fs'; import { dirname } from 'path'; -import { GenericLevelLogger } from '../../lib/level_logger'; +import { finished, Readable } from 'stream'; +import { promisify } from 'util'; +import type { Logger } from 'src/core/server'; /** * Download a url and calculate it's checksum */ -export async function download( - url: string, - path: string, - logger: GenericLevelLogger -): Promise { - logger.info(`Downloading ${url} to ${path}`); +export async function fetch(url: string, path: string, logger?: Logger): Promise { + logger?.info(`Downloading ${url} to ${path}`); const hash = createHash('md5'); @@ -27,30 +25,23 @@ export async function download( const handle = openSync(path, 'w'); try { - const resp = await Axios.request({ + const response = await Axios.request({ url, method: 'GET', responseType: 'stream', }); - resp.data.on('data', (chunk: Buffer) => { + response.data.on('data', (chunk: Buffer) => { writeSync(handle, chunk); hash.update(chunk); }); - await new Promise((resolve, reject) => { - resp.data - .on('error', (err: Error) => { - logger.error(err); - reject(err); - }) - .on('end', () => { - logger.info(`Downloaded ${url}`); - resolve(); - }); - }); - } catch (err) { - throw new Error(`Unable to download ${url}: ${err}`); + await promisify(finished)(response.data, { writable: false }); + logger?.info(`Downloaded ${url}`); + } catch (error) { + logger?.error(error); + + throw new Error(`Unable to download ${url}: ${error}`); } finally { closeSync(handle); } diff --git a/x-pack/plugins/screenshotting/server/browsers/download/index.test.ts b/x-pack/plugins/screenshotting/server/browsers/download/index.test.ts new file mode 100644 index 0000000000000..f960b65859172 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/download/index.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import path from 'path'; +import mockFs from 'mock-fs'; +import { existsSync, readdirSync } from 'fs'; +import { ChromiumArchivePaths } from '../chromium'; +import { fetch } from './fetch'; +import { md5 } from './checksum'; +import { download } from '.'; + +jest.mock('./checksum'); +jest.mock('./fetch'); + +describe('ensureDownloaded', () => { + let paths: ChromiumArchivePaths; + + beforeEach(() => { + paths = new ChromiumArchivePaths(); + + (md5 as jest.MockedFunction).mockImplementation( + async (packagePath) => + paths.packages.find((packageInfo) => paths.resolvePath(packageInfo) === packagePath) + ?.archiveChecksum ?? 'some-md5' + ); + + (fetch as jest.MockedFunction).mockImplementation( + async (_url, packagePath) => + paths.packages.find((packageInfo) => paths.resolvePath(packageInfo) === packagePath) + ?.archiveChecksum ?? 'some-md5' + ); + + mockFs(); + }); + + afterEach(() => { + mockFs.restore(); + jest.resetAllMocks(); + }); + + it('should remove unexpected files', async () => { + const unexpectedPath1 = `${paths.archivesPath}/unexpected1`; + const unexpectedPath2 = `${paths.archivesPath}/unexpected2`; + + mockFs({ + [unexpectedPath1]: 'test', + [unexpectedPath2]: 'test', + }); + + await download(paths); + + expect(existsSync(unexpectedPath1)).toBe(false); + expect(existsSync(unexpectedPath2)).toBe(false); + }); + + it('should reject when download fails', async () => { + (fetch as jest.MockedFunction).mockRejectedValueOnce(new Error('some error')); + + await expect(download(paths)).rejects.toBeInstanceOf(Error); + }); + + it('should reject when downloaded md5 hash is different', async () => { + (fetch as jest.MockedFunction).mockResolvedValue('random-md5'); + + await expect(download(paths)).rejects.toBeInstanceOf(Error); + }); + + describe('when archives are already present', () => { + beforeEach(() => { + mockFs( + Object.fromEntries( + paths.packages.map((packageInfo) => [paths.resolvePath(packageInfo), '']) + ) + ); + }); + + it('should not download again', async () => { + await download(paths); + + expect(fetch).not.toHaveBeenCalled(); + expect(readdirSync(path.resolve(`${paths.archivesPath}/x64`))).toEqual( + expect.arrayContaining([ + 'chrome-win.zip', + 'chromium-70f5d88-linux_x64.zip', + 'chromium-d163fd7-darwin_x64.zip', + ]) + ); + expect(readdirSync(path.resolve(`${paths.archivesPath}/arm64`))).toEqual( + expect.arrayContaining(['chromium-70f5d88-linux_arm64.zip']) + ); + }); + + it('should download again if md5 hash different', async () => { + (md5 as jest.MockedFunction).mockResolvedValueOnce('random-md5'); + await download(paths); + + expect(fetch).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/x-pack/plugins/screenshotting/server/browsers/download/index.ts b/x-pack/plugins/screenshotting/server/browsers/download/index.ts new file mode 100644 index 0000000000000..8866fcc1caf2b --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/download/index.ts @@ -0,0 +1,94 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { existsSync } from 'fs'; +import del from 'del'; +import type { Logger } from 'src/core/server'; +import type { ChromiumArchivePaths } from '../chromium'; +import { md5 } from './checksum'; +import { fetch } from './fetch'; + +/** + * Clears the unexpected files in the browsers archivesPath + * and ensures that all packages/archives are downloaded and + * that their checksums match the declared value + * @param {BrowserSpec} browsers + * @return {Promise} + */ +export async function download(paths: ChromiumArchivePaths, logger?: Logger) { + const removedFiles = await del(`${paths.archivesPath}/**/*`, { + force: true, + onlyFiles: true, + ignore: paths.getAllArchiveFilenames(), + }); + + removedFiles.forEach((path) => logger?.warn(`Deleting unexpected file ${path}`)); + + const invalidChecksums: string[] = []; + await Promise.all( + paths.packages.map(async (path) => { + const { archiveFilename, archiveChecksum } = path; + if (!archiveFilename || !archiveChecksum) { + return; + } + + const resolvedPath = paths.resolvePath(path); + const pathExists = existsSync(resolvedPath); + + let foundChecksum = 'MISSING'; + try { + foundChecksum = await md5(resolvedPath); + // eslint-disable-next-line no-empty + } catch {} + + if (pathExists && foundChecksum === archiveChecksum) { + logger?.debug( + `Browser archive for ${path.platform}/${path.architecture} found in ${resolvedPath}.` + ); + return; + } + + if (!pathExists) { + logger?.warn( + `Browser archive for ${path.platform}/${path.architecture} not found in ${resolvedPath}.` + ); + } + + if (foundChecksum !== archiveChecksum) { + logger?.warn( + `Browser archive checksum for ${path.platform}/${path.architecture} ` + + `is ${foundChecksum} but ${archiveChecksum} was expected.` + ); + } + + const url = paths.getDownloadUrl(path); + try { + const downloadedChecksum = await fetch(url, resolvedPath, logger); + if (downloadedChecksum !== archiveChecksum) { + logger?.warn( + `Invalid checksum for ${path.platform}/${path.architecture}: ` + + `expected ${archiveChecksum} got ${downloadedChecksum}` + ); + invalidChecksums.push(`${url} => ${resolvedPath}`); + } + } catch (error) { + throw new Error(`Failed to download ${url}: ${error}`); + } + }) + ); + + if (invalidChecksums.length) { + const error = new Error( + `Error downloading browsers, checksums incorrect for:\n - ${invalidChecksums.join( + '\n - ' + )}` + ); + logger?.error(error); + + throw error; + } +} diff --git a/x-pack/plugins/reporting/server/browsers/extract/__fixtures__/file.md b/x-pack/plugins/screenshotting/server/browsers/extract/__fixtures__/file.md similarity index 100% rename from x-pack/plugins/reporting/server/browsers/extract/__fixtures__/file.md rename to x-pack/plugins/screenshotting/server/browsers/extract/__fixtures__/file.md diff --git a/x-pack/plugins/reporting/server/browsers/extract/__fixtures__/file.md.zip b/x-pack/plugins/screenshotting/server/browsers/extract/__fixtures__/file.md.zip similarity index 100% rename from x-pack/plugins/reporting/server/browsers/extract/__fixtures__/file.md.zip rename to x-pack/plugins/screenshotting/server/browsers/extract/__fixtures__/file.md.zip diff --git a/x-pack/plugins/reporting/server/browsers/extract/extract.test.ts b/x-pack/plugins/screenshotting/server/browsers/extract/extract.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/browsers/extract/extract.test.ts rename to x-pack/plugins/screenshotting/server/browsers/extract/extract.test.ts diff --git a/x-pack/plugins/reporting/server/browsers/extract/extract.ts b/x-pack/plugins/screenshotting/server/browsers/extract/extract.ts similarity index 100% rename from x-pack/plugins/reporting/server/browsers/extract/extract.ts rename to x-pack/plugins/screenshotting/server/browsers/extract/extract.ts diff --git a/x-pack/plugins/reporting/server/browsers/extract/extract_error.ts b/x-pack/plugins/screenshotting/server/browsers/extract/extract_error.ts similarity index 99% rename from x-pack/plugins/reporting/server/browsers/extract/extract_error.ts rename to x-pack/plugins/screenshotting/server/browsers/extract/extract_error.ts index 838b8a7dbc158..04a915c2afc93 100644 --- a/x-pack/plugins/reporting/server/browsers/extract/extract_error.ts +++ b/x-pack/plugins/screenshotting/server/browsers/extract/extract_error.ts @@ -7,6 +7,7 @@ export class ExtractError extends Error { public readonly cause: string; + constructor(cause: string, message = 'Failed to extract the browser archive') { super(message); this.message = message; diff --git a/x-pack/plugins/reporting/server/browsers/extract/index.ts b/x-pack/plugins/screenshotting/server/browsers/extract/index.ts similarity index 100% rename from x-pack/plugins/reporting/server/browsers/extract/index.ts rename to x-pack/plugins/screenshotting/server/browsers/extract/index.ts diff --git a/x-pack/plugins/reporting/server/browsers/extract/unzip.test.ts b/x-pack/plugins/screenshotting/server/browsers/extract/unzip.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/browsers/extract/unzip.test.ts rename to x-pack/plugins/screenshotting/server/browsers/extract/unzip.test.ts diff --git a/x-pack/plugins/reporting/server/browsers/extract/unzip.ts b/x-pack/plugins/screenshotting/server/browsers/extract/unzip.ts similarity index 100% rename from x-pack/plugins/reporting/server/browsers/extract/unzip.ts rename to x-pack/plugins/screenshotting/server/browsers/extract/unzip.ts diff --git a/x-pack/plugins/screenshotting/server/browsers/index.ts b/x-pack/plugins/screenshotting/server/browsers/index.ts new file mode 100644 index 0000000000000..ef5069ae51112 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export { download } from './download'; +export { install } from './install'; +export type { ConditionalHeaders, PerformanceMetrics } from './chromium'; +export { + getChromiumDisconnectedError, + ChromiumArchivePaths, + DEFAULT_VIEWPORT, + HeadlessChromiumDriver, + HeadlessChromiumDriverFactory, +} from './chromium'; diff --git a/x-pack/plugins/screenshotting/server/browsers/install.ts b/x-pack/plugins/screenshotting/server/browsers/install.ts new file mode 100644 index 0000000000000..acd31ec8ef2b5 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/install.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import del from 'del'; +import os from 'os'; +import path from 'path'; +import type { Logger } from 'src/core/server'; +import { ChromiumArchivePaths } from './chromium'; +import { download } from './download'; +import { md5 } from './download/checksum'; +import { extract } from './extract'; + +/** + * "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 + */ +export async function install( + paths: ChromiumArchivePaths, + logger: Logger, + chromiumPath: string = path.resolve(__dirname, '../../chromium'), + platform: string = process.platform, + architecture: string = os.arch() +): Promise { + const pkg = paths.find(platform, architecture); + + if (!pkg) { + throw new Error(`Unsupported platform: ${platform}-${architecture}`); + } + + const binaryPath = paths.getBinaryPath(pkg); + const binaryChecksum = await md5(binaryPath).catch(() => ''); + + if (binaryChecksum !== pkg.binaryChecksum) { + logger?.warn( + `Found browser binary checksum for ${pkg.platform}/${pkg.architecture} ` + + `is ${binaryChecksum} but ${pkg.binaryChecksum} was expected. Re-installing...` + ); + try { + await del(chromiumPath); + } catch (error) { + logger.error(error); + } + + try { + await download(paths, logger); + const archive = path.join(paths.archivesPath, pkg.architecture, pkg.archiveFilename); + logger.info(`Extracting [${archive}] to [${chromiumPath}]`); + await extract(archive, chromiumPath); + } catch (error) { + logger.error(error); + } + } + + logger.info(`Browser executable: ${binaryPath}`); + + return binaryPath; +} diff --git a/x-pack/plugins/screenshotting/server/browsers/mock.ts b/x-pack/plugins/screenshotting/server/browsers/mock.ts new file mode 100644 index 0000000000000..4b9142b298588 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/browsers/mock.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { NEVER, of } from 'rxjs'; +import type { HeadlessChromiumDriver, HeadlessChromiumDriverFactory } from './chromium'; +import { + CONTEXT_SKIPTELEMETRY, + CONTEXT_GETNUMBEROFITEMS, + CONTEXT_INJECTCSS, + CONTEXT_WAITFORRENDER, + CONTEXT_GETTIMERANGE, + CONTEXT_ELEMENTATTRIBUTES, + CONTEXT_GETRENDERERRORS, +} from '../screenshots/constants'; + +const selectors = { + renderComplete: 'renderedSelector', + itemsCountAttribute: 'itemsSelector', + screenshot: 'screenshotSelector', + timefilterDurationAttribute: 'timefilterDurationSelector', + toastHeader: 'toastHeaderSelector', +}; + +function getElementsPositionAndAttributes(title: string, description: string) { + return [ + { + position: { + boundingClientRect: { top: 0, left: 0, width: 800, height: 600 }, + scroll: { x: 0, y: 0 }, + }, + attributes: { title, description }, + }, + ]; +} + +export function createMockBrowserDriver(): jest.Mocked { + const evaluate = jest.fn(async (_, { context }) => { + switch (context) { + case CONTEXT_SKIPTELEMETRY: + case CONTEXT_INJECTCSS: + case CONTEXT_WAITFORRENDER: + case CONTEXT_GETRENDERERRORS: + return; + case CONTEXT_GETNUMBEROFITEMS: + return 1; + case CONTEXT_GETTIMERANGE: + return 'Default GetTimeRange Result'; + case CONTEXT_ELEMENTATTRIBUTES: + return getElementsPositionAndAttributes('Default Mock Title', 'Default '); + } + + throw new Error(context); + }); + + const screenshot = jest.fn(async () => Buffer.from('screenshot')); + + const waitForSelector = jest.fn(async (selectorArg: string) => { + const { renderComplete, itemsCountAttribute, toastHeader } = selectors; + + if (selectorArg === `${renderComplete},[${itemsCountAttribute}]`) { + return true; + } + + if (selectorArg === toastHeader) { + return NEVER.toPromise(); + } + + throw new Error(selectorArg); + }); + + return { + evaluate, + screenshot, + waitForSelector, + isPageOpen: jest.fn(), + open: jest.fn(), + setViewport: jest.fn(async () => {}), + waitFor: jest.fn(), + } as unknown as ReturnType; +} + +export function createMockBrowserDriverFactory( + driver?: HeadlessChromiumDriver +): jest.Mocked { + return { + createPage: jest.fn(() => + of({ driver: driver ?? createMockBrowserDriver(), exit$: NEVER, metrics$: NEVER }) + ), + diagnose: jest.fn(() => of('message')), + } as unknown as ReturnType; +} diff --git a/x-pack/plugins/reporting/server/browsers/network_policy.test.ts b/x-pack/plugins/screenshotting/server/browsers/network_policy.test.ts similarity index 99% rename from x-pack/plugins/reporting/server/browsers/network_policy.test.ts rename to x-pack/plugins/screenshotting/server/browsers/network_policy.test.ts index 4f0c60f4d14d2..84e42347100ae 100644 --- a/x-pack/plugins/reporting/server/browsers/network_policy.test.ts +++ b/x-pack/plugins/screenshotting/server/browsers/network_policy.test.ts @@ -7,7 +7,7 @@ import { allowRequest } from './network_policy'; -describe('Network Policy', () => { +describe('allowRequest', () => { it('allows requests when there are no rules', () => { expect(allowRequest('https://kibana.com/cool/route/bro', [])).toEqual(true); }); diff --git a/x-pack/plugins/reporting/server/browsers/network_policy.ts b/x-pack/plugins/screenshotting/server/browsers/network_policy.ts similarity index 94% rename from x-pack/plugins/reporting/server/browsers/network_policy.ts rename to x-pack/plugins/screenshotting/server/browsers/network_policy.ts index 721094dce6edf..4d47b01889924 100644 --- a/x-pack/plugins/reporting/server/browsers/network_policy.ts +++ b/x-pack/plugins/screenshotting/server/browsers/network_policy.ts @@ -26,7 +26,7 @@ const isHostMatch = (actualHost: string, ruleHost: string) => { return every(ruleParts, (part, idx) => part === hostParts[idx]); }; -export const allowRequest = (url: string, rules: NetworkPolicyRule[]) => { +export function allowRequest(url: string, rules: NetworkPolicyRule[]): boolean { const parsed = parse(url); if (!rules.length) { @@ -52,4 +52,4 @@ export const allowRequest = (url: string, rules: NetworkPolicyRule[]) => { }, undefined); return typeof allowed !== 'undefined' ? allowed : false; -}; +} diff --git a/x-pack/plugins/reporting/server/browsers/safe_child_process.ts b/x-pack/plugins/screenshotting/server/browsers/safe_child_process.ts similarity index 70% rename from x-pack/plugins/reporting/server/browsers/safe_child_process.ts rename to x-pack/plugins/screenshotting/server/browsers/safe_child_process.ts index 70e45bf10803f..4bc378a4c8c86 100644 --- a/x-pack/plugins/reporting/server/browsers/safe_child_process.ts +++ b/x-pack/plugins/screenshotting/server/browsers/safe_child_process.ts @@ -5,9 +5,9 @@ * 2.0. */ -import * as Rx from 'rxjs'; +import { fromEvent, merge, Observable } from 'rxjs'; import { take, share, mapTo, delay, tap } from 'rxjs/operators'; -import { LevelLogger } from '../lib'; +import type { Logger } from 'src/core/server'; interface IChild { kill: (signal: string) => Promise; @@ -16,13 +16,13 @@ interface IChild { // Our process can get sent various signals, and when these occur we wish to // kill the subprocess and then kill our process as long as the observer isn't cancelled export function safeChildProcess( - logger: LevelLogger, + logger: Logger, childProcess: IChild -): { terminate$: Rx.Observable } { - const ownTerminateSignal$ = Rx.merge( - Rx.fromEvent(process as NodeJS.EventEmitter, 'SIGTERM').pipe(mapTo('SIGTERM')), - Rx.fromEvent(process as NodeJS.EventEmitter, 'SIGINT').pipe(mapTo('SIGINT')), - Rx.fromEvent(process as NodeJS.EventEmitter, 'SIGBREAK').pipe(mapTo('SIGBREAK')) +): { terminate$: Observable } { + const ownTerminateSignal$ = merge( + fromEvent(process as NodeJS.EventEmitter, 'SIGTERM').pipe(mapTo('SIGTERM')), + fromEvent(process as NodeJS.EventEmitter, 'SIGINT').pipe(mapTo('SIGINT')), + fromEvent(process as NodeJS.EventEmitter, 'SIGBREAK').pipe(mapTo('SIGBREAK')) ).pipe(take(1), share()); const ownTerminateMapToKill$ = ownTerminateSignal$.pipe( @@ -32,7 +32,7 @@ export function safeChildProcess( mapTo('SIGKILL') ); - const kibanaForceExit$ = Rx.fromEvent(process as NodeJS.EventEmitter, 'exit').pipe( + const kibanaForceExit$ = fromEvent(process as NodeJS.EventEmitter, 'exit').pipe( take(1), tap((signal) => { logger.debug(`Kibana process forcefully exited with signal: ${signal}`); @@ -40,7 +40,7 @@ export function safeChildProcess( mapTo('SIGKILL') ); - const signalForChildProcess$ = Rx.merge(ownTerminateMapToKill$, kibanaForceExit$); + const signalForChildProcess$ = merge(ownTerminateMapToKill$, kibanaForceExit$); const logAndKillChildProcess = tap((signal: string) => { logger.debug(`Child process terminate signal was: ${signal}. Closing the browser...`); @@ -48,7 +48,7 @@ export function safeChildProcess( }); // send termination signals - const terminate$ = Rx.merge( + const terminate$ = merge( signalForChildProcess$.pipe(logAndKillChildProcess), ownTerminateSignal$.pipe( diff --git a/x-pack/plugins/screenshotting/server/config/create_config.test.ts b/x-pack/plugins/screenshotting/server/config/create_config.test.ts new file mode 100644 index 0000000000000..18ac6ceb6874d --- /dev/null +++ b/x-pack/plugins/screenshotting/server/config/create_config.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from 'src/core/server'; +import { createConfig } from './create_config'; +import { ConfigType } from './schema'; + +describe('createConfig$', () => { + let logger: jest.Mocked; + + beforeEach(() => { + logger = { + debug: jest.fn(), + get: jest.fn(() => logger), + info: jest.fn(), + warn: jest.fn(), + } as unknown as typeof logger; + }); + + it('should use user-provided disableSandbox', async () => { + const result = await createConfig(logger, { + browser: { chromium: { disableSandbox: false } }, + } as ConfigType); + + expect(result).toHaveProperty('browser.chromium.disableSandbox', false); + expect(logger.warn).not.toHaveBeenCalled(); + }); + + it('should provide a default for disableSandbox', async () => { + const result = await createConfig(logger, { browser: { chromium: {} } } as ConfigType); + + expect(result).toHaveProperty('browser.chromium.disableSandbox', expect.any(Boolean)); + expect((logger.warn as any).mock.calls.length).toBe(0); + }); +}); diff --git a/x-pack/plugins/screenshotting/server/config/create_config.ts b/x-pack/plugins/screenshotting/server/config/create_config.ts new file mode 100644 index 0000000000000..1819f37e1bccd --- /dev/null +++ b/x-pack/plugins/screenshotting/server/config/create_config.ts @@ -0,0 +1,57 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; +import { cloneDeep, set, upperFirst } from 'lodash'; +import type { Logger } from 'src/core/server'; +import { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled'; +import { ConfigType } from './schema'; + +/* + * Set up dynamic config defaults + * - xpack.capture.browser.chromium.disableSandbox + */ +export async function createConfig(parentLogger: Logger, config: ConfigType) { + const logger = parentLogger.get('config'); + + if (config.browser.chromium.disableSandbox != null) { + // disableSandbox was set by user + return config; + } + + // disableSandbox was not set by user, apply default for OS + const { os, disableSandbox } = await getDefaultChromiumSandboxDisabled(); + const osName = [os.os, os.dist, os.release].filter(Boolean).map(upperFirst).join(' '); + + logger.debug( + i18n.translate('xpack.screenshotting.serverConfig.osDetected', { + defaultMessage: `Running on OS: '{osName}'`, + values: { osName }, + }) + ); + + if (disableSandbox === true) { + logger.warn( + i18n.translate('xpack.screenshotting.serverConfig.autoSet.sandboxDisabled', { + defaultMessage: `Chromium sandbox provides an additional layer of protection, but is not supported for {osName} OS. Automatically setting '{configKey}: true'.`, + values: { + configKey: 'xpack.screenshotting.capture.browser.chromium.disableSandbox', + osName, + }, + }) + ); + } else { + logger.info( + i18n.translate('xpack.screenshotting.serverConfig.autoSet.sandboxEnabled', { + defaultMessage: `Chromium sandbox provides an additional layer of protection, and is supported for {osName} OS. Automatically enabling Chromium sandbox.`, + values: { osName }, + }) + ); + } + + return set(cloneDeep(config), 'browser.chromium.disableSandbox', disableSandbox); +} diff --git a/x-pack/plugins/screenshotting/server/config/default_chromium_sandbox_disabled.test.ts b/x-pack/plugins/screenshotting/server/config/default_chromium_sandbox_disabled.test.ts new file mode 100644 index 0000000000000..7204230ef5160 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/config/default_chromium_sandbox_disabled.test.ts @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +jest.mock('getos', () => jest.fn()); + +import { getDefaultChromiumSandboxDisabled } from './default_chromium_sandbox_disabled'; +import getos from 'getos'; + +describe('getDefaultChromiumSandboxDisabled', () => { + it.each` + os | dist | release | expected + ${'win32'} | ${'Windows'} | ${'11'} | ${false} + ${'darwin'} | ${'macOS'} | ${'11.2.3'} | ${false} + ${'linux'} | ${'Centos'} | ${'7.0'} | ${true} + ${'linux'} | ${'Red Hat Linux'} | ${'7.0'} | ${true} + ${'linux'} | ${'Ubuntu Linux'} | ${'14.04'} | ${false} + ${'linux'} | ${'Ubuntu Linux'} | ${'16.04'} | ${false} + ${'linux'} | ${'SUSE Linux'} | ${'11'} | ${false} + ${'linux'} | ${'SUSE Linux'} | ${'12'} | ${false} + ${'linux'} | ${'SUSE Linux'} | ${'42.0'} | ${false} + ${'linux'} | ${'Debian'} | ${'8'} | ${true} + ${'linux'} | ${'Debian'} | ${'9'} | ${true} + `('should return $expected for $dist $release', async ({ expected, ...os }) => { + (getos as jest.Mock).mockImplementation((cb) => cb(null, os)); + + await expect(getDefaultChromiumSandboxDisabled()).resolves.toHaveProperty( + 'disableSandbox', + expected + ); + }); +}); diff --git a/x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.ts b/x-pack/plugins/screenshotting/server/config/default_chromium_sandbox_disabled.ts similarity index 80% rename from x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.ts rename to x-pack/plugins/screenshotting/server/config/default_chromium_sandbox_disabled.ts index 89872cf63d1bc..4461b53bf0fcf 100644 --- a/x-pack/plugins/reporting/server/config/default_chromium_sandbox_disabled.ts +++ b/x-pack/plugins/screenshotting/server/config/default_chromium_sandbox_disabled.ts @@ -5,10 +5,10 @@ * 2.0. */ -import getosSync from 'getos'; +import getOsSync from 'getos'; import { promisify } from 'util'; -const getos = promisify(getosSync); +const getOs = promisify(getOsSync); const distroSupportsUnprivilegedUsernamespaces = (distro: string) => { // Debian 7 and 8 don't support usernamespaces by default @@ -38,11 +38,10 @@ interface OsSummary { } export async function getDefaultChromiumSandboxDisabled(): Promise { - const os = await getos(); + const os = await getOs(); - if (os.os === 'linux' && !distroSupportsUnprivilegedUsernamespaces(os.dist)) { - return { os, disableSandbox: true }; - } else { - return { os, disableSandbox: false }; - } + return { + os, + disableSandbox: os.os === 'linux' && !distroSupportsUnprivilegedUsernamespaces(os.dist), + }; } diff --git a/x-pack/plugins/screenshotting/server/config/index.ts b/x-pack/plugins/screenshotting/server/config/index.ts new file mode 100644 index 0000000000000..38f5a6e8f20fa --- /dev/null +++ b/x-pack/plugins/screenshotting/server/config/index.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { PluginConfigDescriptor } from 'src/core/server'; +import { ConfigSchema, ConfigType } from './schema'; + +/** + * Screenshotting plugin configuration schema. + */ +export const config: PluginConfigDescriptor = { + schema: ConfigSchema, + deprecations: ({ renameFromRoot }) => [ + renameFromRoot('xpack.reporting.capture.networkPolicy', 'xpack.screenshotting.networkPolicy'), + renameFromRoot( + 'xpack.reporting.capture.browser.autoDownload', + 'xpack.screenshotting.browser.autoDownload' + ), + renameFromRoot( + 'xpack.reporting.capture.browser.chromium.inspect', + 'xpack.screenshotting.browser.chromium.inspect' + ), + renameFromRoot( + 'xpack.reporting.capture.browser.chromium.disableSandbox', + 'xpack.screenshotting.browser.chromium.disableSandbox' + ), + renameFromRoot( + 'xpack.reporting.capture.browser.chromium.proxy.enabled', + 'xpack.screenshotting.browser.chromium.proxy.enabled' + ), + renameFromRoot( + 'xpack.reporting.capture.browser.chromium.proxy.server', + 'xpack.screenshotting.browser.chromium.proxy.server' + ), + renameFromRoot( + 'xpack.reporting.capture.browser.chromium.proxy.bypass', + 'xpack.screenshotting.browser.chromium.proxy.bypass' + ), + ], + exposeToUsage: { + networkPolicy: false, // show as [redacted] + }, +}; + +export { createConfig } from './create_config'; +export type { ConfigType } from './schema'; diff --git a/x-pack/plugins/screenshotting/server/config/schema.test.ts b/x-pack/plugins/screenshotting/server/config/schema.test.ts new file mode 100644 index 0000000000000..9180f0d180d5f --- /dev/null +++ b/x-pack/plugins/screenshotting/server/config/schema.test.ts @@ -0,0 +1,146 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ConfigSchema } from './schema'; + +describe('ConfigSchema', () => { + it(`should produce correct config for context {"dev": false,"dist": false}`, () => { + expect(ConfigSchema.validate({}, { dev: false, dist: false })).toMatchInlineSnapshot(` + Object { + "browser": Object { + "autoDownload": true, + "chromium": Object { + "proxy": Object { + "enabled": false, + }, + }, + }, + "networkPolicy": Object { + "enabled": true, + "rules": Array [ + Object { + "allow": true, + "host": undefined, + "protocol": "http:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "https:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "ws:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "wss:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "data:", + }, + Object { + "allow": false, + "host": undefined, + "protocol": undefined, + }, + ], + }, + } + `); + }); + + it(`should produce correct config for context {"dev": false,"dist": true}`, () => { + expect(ConfigSchema.validate({}, { dev: false, dist: true })).toMatchInlineSnapshot(` + Object { + "browser": Object { + "autoDownload": false, + "chromium": Object { + "inspect": false, + "proxy": Object { + "enabled": false, + }, + }, + }, + "networkPolicy": Object { + "enabled": true, + "rules": Array [ + Object { + "allow": true, + "host": undefined, + "protocol": "http:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "https:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "ws:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "wss:", + }, + Object { + "allow": true, + "host": undefined, + "protocol": "data:", + }, + Object { + "allow": false, + "host": undefined, + "protocol": undefined, + }, + ], + }, + } + `); + }); + + it(`should allow optional settings`, () => { + const config = ConfigSchema.validate({ browser: { chromium: { disableSandbox: true } } }); + + expect(config).toHaveProperty('browser.chromium', { + disableSandbox: true, + proxy: { enabled: false }, + }); + }); + + it('should allow setting a wildcard for chrome proxy bypass', () => { + expect( + ConfigSchema.validate({ + browser: { + chromium: { + proxy: { + enabled: true, + server: 'http://example.com:8080', + bypass: ['*.example.com', '*bar.example.com', 'bats.example.com'], + }, + }, + }, + }).browser.chromium.proxy + ).toMatchInlineSnapshot(` + Object { + "bypass": Array [ + "*.example.com", + "*bar.example.com", + "bats.example.com", + ], + "enabled": true, + "server": "http://example.com:8080", + } + `); + }); +}); diff --git a/x-pack/plugins/screenshotting/server/config/schema.ts b/x-pack/plugins/screenshotting/server/config/schema.ts new file mode 100644 index 0000000000000..bcf2fa9feead9 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/config/schema.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { schema, TypeOf } from '@kbn/config-schema'; + +const RulesSchema = schema.object({ + allow: schema.boolean(), + host: schema.maybe(schema.string()), + protocol: schema.maybe( + schema.string({ + validate(value) { + if (!/:$/.test(value)) { + return 'must end in colon'; + } + }, + }) + ), +}); + +export const ConfigSchema = schema.object({ + networkPolicy: schema.object({ + enabled: schema.boolean({ defaultValue: true }), + rules: schema.arrayOf(RulesSchema, { + defaultValue: [ + { host: undefined, allow: true, protocol: 'http:' }, + { host: undefined, allow: true, protocol: 'https:' }, + { host: undefined, allow: true, protocol: 'ws:' }, + { host: undefined, allow: true, protocol: 'wss:' }, + { host: undefined, allow: true, protocol: 'data:' }, + { host: undefined, allow: false, protocol: undefined }, // Default action is to deny! + ], + }), + }), + browser: schema.object({ + autoDownload: schema.conditional( + schema.contextRef('dist'), + true, + schema.boolean({ defaultValue: false }), + schema.boolean({ defaultValue: true }) + ), + chromium: schema.object({ + inspect: schema.conditional( + schema.contextRef('dist'), + true, + schema.boolean({ defaultValue: false }), + schema.maybe(schema.never()) + ), + disableSandbox: schema.maybe(schema.boolean()), // default value is dynamic in createConfig$ + proxy: schema.object({ + enabled: schema.boolean({ defaultValue: false }), + server: schema.conditional( + schema.siblingRef('enabled'), + true, + schema.uri({ scheme: ['http', 'https'] }), + schema.maybe(schema.never()) + ), + bypass: schema.conditional( + schema.siblingRef('enabled'), + true, + schema.arrayOf(schema.string()), + schema.maybe(schema.never()) + ), + }), + }), + }), +}); + +export type ConfigType = TypeOf; diff --git a/x-pack/plugins/screenshotting/server/index.ts b/x-pack/plugins/screenshotting/server/index.ts new file mode 100755 index 0000000000000..340a6688e79eb --- /dev/null +++ b/x-pack/plugins/screenshotting/server/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ScreenshottingPlugin } from './plugin'; + +/** + * Screenshotting plugin entry point. + */ +export function plugin(...args: ConstructorParameters) { + return new ScreenshottingPlugin(...args); +} + +export { config } from './config'; +export type { Layout } from './layouts'; +export type { ScreenshottingStart } from './plugin'; +export type { ScreenshotOptions, ScreenshotResult } from './screenshots'; diff --git a/x-pack/plugins/reporting/server/lib/layouts/layout.ts b/x-pack/plugins/screenshotting/server/layouts/base_layout.ts similarity index 79% rename from x-pack/plugins/reporting/server/lib/layouts/layout.ts rename to x-pack/plugins/screenshotting/server/layouts/base_layout.ts index d68e7690d79f1..846904170a0c1 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/layout.ts +++ b/x-pack/plugins/screenshotting/server/layouts/base_layout.ts @@ -6,7 +6,7 @@ */ import type { CustomPageSize, PredefinedPageSize } from 'pdfmake/interfaces'; -import type { PageSizeParams, PdfImageSize, Size } from '../../../common/types'; +import type { Size } from '../../common/layout'; export interface ViewZoomWidthHeight { zoom: number; @@ -14,7 +14,21 @@ export interface ViewZoomWidthHeight { height: number; } -export abstract class Layout { +export interface PdfImageSize { + width: number; + height?: number; +} + +export interface PageSizeParams { + pageMarginTop: number; + pageMarginBottom: number; + pageMarginWidth: number; + tableBorderWidth: number; + headingHeight: number; + subheadingHeight: number; +} + +export abstract class BaseLayout { public id: string = ''; public groupCount: number = 0; diff --git a/x-pack/plugins/reporting/server/lib/layouts/canvas_layout.ts b/x-pack/plugins/screenshotting/server/layouts/canvas_layout.ts similarity index 80% rename from x-pack/plugins/reporting/server/lib/layouts/canvas_layout.ts rename to x-pack/plugins/screenshotting/server/layouts/canvas_layout.ts index ec95b0f75997d..d164f8c7e91e2 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/canvas_layout.ts +++ b/x-pack/plugins/screenshotting/server/layouts/canvas_layout.ts @@ -5,15 +5,11 @@ * 2.0. */ -import { - getDefaultLayoutSelectors, - LayoutInstance, - LayoutSelectorDictionary, - LayoutTypes, - PageSizeParams, - Size, -} from './'; -import { Layout } from './layout'; +import type { LayoutSelectorDictionary, Size } from '../../common/layout'; +import { LayoutTypes } from '../../common'; +import { DEFAULT_SELECTORS } from '.'; +import type { Layout } from '.'; +import { BaseLayout } from './base_layout'; // FIXME - should use zoom from capture config const ZOOM: number = 2; @@ -24,8 +20,8 @@ const ZOOM: number = 2; * The single image that was captured should be the only structural part of the * PDF document definition */ -export class CanvasLayout extends Layout implements LayoutInstance { - public readonly selectors: LayoutSelectorDictionary = getDefaultLayoutSelectors(); +export class CanvasLayout extends BaseLayout implements Layout { + public readonly selectors: LayoutSelectorDictionary = { ...DEFAULT_SELECTORS }; public readonly groupCount = 1; public readonly height: number; public readonly width: number; @@ -78,7 +74,7 @@ export class CanvasLayout extends Layout implements LayoutInstance { }; } - public getPdfPageSize(pageSizeParams: PageSizeParams): Size { + public getPdfPageSize(): Size { return { height: this.height, width: this.width, diff --git a/x-pack/plugins/reporting/server/lib/layouts/create_layout.test.ts b/x-pack/plugins/screenshotting/server/layouts/create_layout.test.ts similarity index 80% rename from x-pack/plugins/reporting/server/lib/layouts/create_layout.test.ts rename to x-pack/plugins/screenshotting/server/layouts/create_layout.test.ts index aebd20451b834..1ea6c7440b455 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/create_layout.test.ts +++ b/x-pack/plugins/screenshotting/server/layouts/create_layout.test.ts @@ -5,21 +5,16 @@ * 2.0. */ -import { ReportingConfig } from '../..'; -import { createMockConfig, createMockConfigSchema } from '../../test_helpers'; -import { createLayout, LayoutParams, PreserveLayout } from './'; +import type { LayoutParams } from '../../common/layout'; import { CanvasLayout } from './canvas_layout'; +import { PreserveLayout } from './preserve_layout'; +import { createLayout } from './create_layout'; describe('Create Layout', () => { - let config: ReportingConfig; - beforeEach(() => { - config = createMockConfig(createMockConfigSchema()); - }); - it('creates preserve layout instance', () => { const { id, height, width } = new PreserveLayout({ width: 16, height: 16 }); const preserveParams: LayoutParams = { id, dimensions: { height, width } }; - const layout = createLayout(config.get('capture'), preserveParams); + const layout = createLayout(preserveParams); expect(layout).toMatchInlineSnapshot(` PreserveLayout { "groupCount": 1, @@ -44,20 +39,14 @@ describe('Create Layout', () => { }); it('creates the print layout', () => { - const print = createLayout(config.get('capture')); + const print = createLayout({ zoom: 1 }); const printParams: LayoutParams = { id: print.id, + zoom: 1, }; - const layout = createLayout(config.get('capture'), printParams); + const layout = createLayout(printParams); expect(layout).toMatchInlineSnapshot(` PrintLayout { - "captureConfig": Object { - "browser": Object { - "chromium": Object { - "disableSandbox": true, - }, - }, - }, "groupCount": 2, "hasFooter": true, "hasHeader": true, @@ -75,6 +64,7 @@ describe('Create Layout', () => { "height": 1200, "width": 1950, }, + "zoom": 1, } `); }); @@ -82,7 +72,7 @@ describe('Create Layout', () => { it('creates the canvas layout', () => { const { id, height, width } = new CanvasLayout({ width: 18, height: 18 }); const canvasParams: LayoutParams = { id, dimensions: { height, width } }; - const layout = createLayout(config.get('capture'), canvasParams); + const layout = createLayout(canvasParams); expect(layout).toMatchInlineSnapshot(` CanvasLayout { "groupCount": 1, diff --git a/x-pack/plugins/screenshotting/server/layouts/create_layout.ts b/x-pack/plugins/screenshotting/server/layouts/create_layout.ts new file mode 100644 index 0000000000000..29a34a07e696f --- /dev/null +++ b/x-pack/plugins/screenshotting/server/layouts/create_layout.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { LayoutParams } from '../../common/layout'; +import { LayoutTypes } from '../../common'; +import type { Layout } from '.'; +import { CanvasLayout } from './canvas_layout'; +import { PreserveLayout } from './preserve_layout'; +import { PrintLayout } from './print_layout'; + +export function createLayout({ id, dimensions, selectors, ...config }: LayoutParams): Layout { + if (dimensions && id === LayoutTypes.PRESERVE_LAYOUT) { + return new PreserveLayout(dimensions, selectors); + } + + if (dimensions && id === LayoutTypes.CANVAS) { + return new CanvasLayout(dimensions); + } + + // layoutParams is optional as PrintLayout doesn't use it + return new PrintLayout(config); +} diff --git a/x-pack/plugins/screenshotting/server/layouts/index.ts b/x-pack/plugins/screenshotting/server/layouts/index.ts new file mode 100644 index 0000000000000..d21b06e6a688a --- /dev/null +++ b/x-pack/plugins/screenshotting/server/layouts/index.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from 'src/core/server'; +import type { LayoutSelectorDictionary, Size } from '../../common/layout'; +import type { HeadlessChromiumDriver } from '../browsers'; +import type { BaseLayout } from './base_layout'; + +interface LayoutSelectors { + /** + * Element selectors determining the page state. + */ + selectors: LayoutSelectorDictionary; + + /** + * A callback to position elements before taking a screenshot. + * @param browser Browser adapter instance. + * @param logger Message logger. + */ + positionElements?(browser: HeadlessChromiumDriver, logger: Logger): Promise; +} + +export type Layout = BaseLayout & LayoutSelectors & Partial; + +export const DEFAULT_SELECTORS: LayoutSelectorDictionary = { + screenshot: '[data-shared-items-container]', + renderComplete: '[data-shared-item]', + renderError: '[data-render-error]', + renderErrorAttribute: 'data-render-error', + itemsCountAttribute: 'data-shared-items-count', + timefilterDurationAttribute: 'data-shared-timefilter-duration', +}; + +export { createLayout } from './create_layout'; diff --git a/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts b/x-pack/plugins/screenshotting/server/layouts/mock.ts similarity index 59% rename from x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts rename to x-pack/plugins/screenshotting/server/layouts/mock.ts index e9b94c3c98bec..d5395c5db6f82 100644 --- a/x-pack/plugins/reporting/server/test_helpers/create_mock_layoutinstance.ts +++ b/x-pack/plugins/screenshotting/server/layouts/mock.ts @@ -5,16 +5,17 @@ * 2.0. */ -import { LAYOUT_TYPES } from '../../common/constants'; -import { createLayout, LayoutInstance } from '../lib/layouts'; -import { CaptureConfig } from '../types'; +import { LayoutTypes } from '../../common'; +import { createLayout, Layout } from '.'; -export const createMockLayoutInstance = (captureConfig: CaptureConfig) => { - const mockLayout = createLayout(captureConfig, { - id: LAYOUT_TYPES.PRESERVE_LAYOUT, +export function createMockLayout(): Layout { + const layout = createLayout({ + id: LayoutTypes.PRESERVE_LAYOUT, dimensions: { height: 100, width: 100 }, - }) as LayoutInstance; - mockLayout.selectors = { + zoom: 1, + }) as Layout; + + layout.selectors = { renderComplete: 'renderedSelector', itemsCountAttribute: 'itemsSelector', screenshot: 'screenshotSelector', @@ -22,5 +23,6 @@ export const createMockLayoutInstance = (captureConfig: CaptureConfig) => { renderErrorAttribute: 'dataRenderErrorSelector', timefilterDurationAttribute: 'timefilterDurationSelector', }; - return mockLayout; -}; + + return layout; +} diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css b/x-pack/plugins/screenshotting/server/layouts/preserve_layout.css similarity index 100% rename from x-pack/plugins/reporting/server/lib/layouts/preserve_layout.css rename to x-pack/plugins/screenshotting/server/layouts/preserve_layout.css diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.test.ts b/x-pack/plugins/screenshotting/server/layouts/preserve_layout.test.ts similarity index 100% rename from x-pack/plugins/reporting/server/lib/layouts/preserve_layout.test.ts rename to x-pack/plugins/screenshotting/server/layouts/preserve_layout.test.ts diff --git a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts b/x-pack/plugins/screenshotting/server/layouts/preserve_layout.ts similarity index 79% rename from x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts rename to x-pack/plugins/screenshotting/server/layouts/preserve_layout.ts index 7f6bc9e5d9505..f265920675f85 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/preserve_layout.ts +++ b/x-pack/plugins/screenshotting/server/layouts/preserve_layout.ts @@ -5,16 +5,18 @@ * 2.0. */ import path from 'path'; -import { CustomPageSize } from 'pdfmake/interfaces'; -import { LAYOUT_TYPES } from '../../../common/constants'; -import { PageSizeParams, Size } from '../../../common/types'; -import { getDefaultLayoutSelectors, LayoutInstance, LayoutSelectorDictionary } from './'; -import { Layout } from './layout'; +import type { CustomPageSize } from 'pdfmake/interfaces'; +import type { LayoutSelectorDictionary, Size } from '../../common/layout'; +import { LayoutTypes } from '../../common'; +import { DEFAULT_SELECTORS } from '.'; +import type { Layout } from '.'; +import { BaseLayout } from './base_layout'; +import type { PageSizeParams } from './base_layout'; // We use a zoom of two to bump up the resolution of the screenshot a bit. const ZOOM: number = 2; -export class PreserveLayout extends Layout implements LayoutInstance { +export class PreserveLayout extends BaseLayout implements Layout { public readonly selectors: LayoutSelectorDictionary; public readonly groupCount = 1; public readonly height: number; @@ -23,16 +25,13 @@ export class PreserveLayout extends Layout implements LayoutInstance { private readonly scaledWidth: number; constructor(size: Size, selectors?: Partial) { - super(LAYOUT_TYPES.PRESERVE_LAYOUT); + super(LayoutTypes.PRESERVE_LAYOUT); this.height = size.height; this.width = size.width; this.scaledHeight = size.height * ZOOM; this.scaledWidth = size.width * ZOOM; - this.selectors = { - ...getDefaultLayoutSelectors(), - ...selectors, - }; + this.selectors = { ...DEFAULT_SELECTORS, ...selectors }; } public getCssOverridesPath() { diff --git a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts b/x-pack/plugins/screenshotting/server/layouts/print_layout.ts similarity index 64% rename from x-pack/plugins/reporting/server/lib/layouts/print_layout.ts rename to x-pack/plugins/screenshotting/server/layouts/print_layout.ts index 68226affb41e4..bfcbe84842c40 100644 --- a/x-pack/plugins/reporting/server/lib/layouts/print_layout.ts +++ b/x-pack/plugins/screenshotting/server/layouts/print_layout.ts @@ -6,23 +6,26 @@ */ import { PageOrientation, PredefinedPageSize } from 'pdfmake/interfaces'; -import { DEFAULT_VIEWPORT, LAYOUT_TYPES } from '../../../common/constants'; -import { CaptureConfig } from '../../types'; -import { getDefaultLayoutSelectors, LayoutInstance, LayoutSelectorDictionary } from './'; -import { Layout } from './layout'; +import type { LayoutParams, LayoutSelectorDictionary } from '../../common/layout'; +import { LayoutTypes } from '../../common'; +import type { Layout } from '.'; +import { DEFAULT_SELECTORS } from '.'; +import { DEFAULT_VIEWPORT } from '../browsers'; +import { BaseLayout } from './base_layout'; -export class PrintLayout extends Layout implements LayoutInstance { +export class PrintLayout extends BaseLayout implements Layout { public readonly selectors: LayoutSelectorDictionary = { - ...getDefaultLayoutSelectors(), + ...DEFAULT_SELECTORS, screenshot: '[data-shared-item]', // override '[data-shared-items-container]' }; public readonly groupCount = 2; - private readonly captureConfig: CaptureConfig; private readonly viewport = DEFAULT_VIEWPORT; + private zoom: number; - constructor(captureConfig: CaptureConfig) { - super(LAYOUT_TYPES.PRINT); - this.captureConfig = captureConfig; + constructor({ zoom = 1 }: Pick) { + super(LayoutTypes.PRINT); + + this.zoom = zoom; } public getCssOverridesPath() { @@ -34,16 +37,17 @@ export class PrintLayout extends Layout implements LayoutInstance { } public getBrowserZoom() { - return this.captureConfig.zoom; + return this.zoom; } public getViewport(itemsCount: number) { return { - zoom: this.captureConfig.zoom, + zoom: this.zoom, width: this.viewport.width, height: this.viewport.height * itemsCount, }; } + public getPdfImageSize() { return { width: 500, diff --git a/x-pack/plugins/screenshotting/server/mock.ts b/x-pack/plugins/screenshotting/server/mock.ts new file mode 100644 index 0000000000000..49d69521f2c19 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/mock.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from 'src/core/server'; +import { createMockBrowserDriverFactory } from './browsers/mock'; +import { createMockScreenshots } from './screenshots/mock'; +import type { ScreenshottingStart } from '.'; + +export function createMockScreenshottingStart(): jest.Mocked { + const driver = createMockBrowserDriverFactory(); + const { getScreenshots } = createMockScreenshots(); + const { diagnose } = driver; + + return { + diagnose, + getScreenshots: jest.fn((options) => getScreenshots(driver, {} as Logger, options)), + }; +} diff --git a/x-pack/plugins/screenshotting/server/plugin.ts b/x-pack/plugins/screenshotting/server/plugin.ts new file mode 100755 index 0000000000000..53f855e1f544d --- /dev/null +++ b/x-pack/plugins/screenshotting/server/plugin.ts @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { from } from 'rxjs'; +import { switchMap } from 'rxjs/operators'; +import type { + CoreSetup, + CoreStart, + Logger, + Plugin, + PluginInitializerContext, +} from 'src/core/server'; +import type { ScreenshotModePluginSetup } from 'src/plugins/screenshot_mode/server'; +import { ChromiumArchivePaths, HeadlessChromiumDriverFactory, install } from './browsers'; +import { createConfig, ConfigType } from './config'; +import { getScreenshots, ScreenshotOptions } from './screenshots'; + +interface SetupDeps { + screenshotMode: ScreenshotModePluginSetup; +} + +/** + * Start public contract. + */ +export interface ScreenshottingStart { + /** + * Runs browser diagnostics. + * @returns Observable with output messages. + */ + diagnose: HeadlessChromiumDriverFactory['diagnose']; + + /** + * Takes screenshots of multiple pages. + * @param options Screenshots session options. + * @returns Observable with screenshotting results. + */ + getScreenshots(options: ScreenshotOptions): ReturnType; +} + +export class ScreenshottingPlugin implements Plugin { + private config: ConfigType; + private logger: Logger; + private screenshotMode!: ScreenshotModePluginSetup; + private browserDriverFactory!: Promise; + + constructor(context: PluginInitializerContext) { + this.logger = context.logger.get(); + this.config = context.config.get(); + } + + setup({}: CoreSetup, { screenshotMode }: SetupDeps) { + this.screenshotMode = screenshotMode; + this.browserDriverFactory = (async () => { + try { + const paths = new ChromiumArchivePaths(); + const logger = this.logger.get('chromium'); + const [config, binaryPath] = await Promise.all([ + createConfig(this.logger, this.config), + install(paths, logger), + ]); + + return new HeadlessChromiumDriverFactory(this.screenshotMode, config, logger, binaryPath); + } catch (error) { + this.logger.error('Error in screenshotting setup, it may not function properly.'); + + throw error; + } + })(); + + return {}; + } + + start({}: CoreStart): ScreenshottingStart { + return { + diagnose: () => + from(this.browserDriverFactory).pipe(switchMap((factory) => factory.diagnose())), + getScreenshots: (options) => + from(this.browserDriverFactory).pipe( + switchMap((factory) => getScreenshots(factory, this.logger.get('screenshot'), options)) + ), + }; + } + + stop() {} +} diff --git a/x-pack/plugins/reporting/server/lib/screenshots/constants.ts b/x-pack/plugins/screenshotting/server/screenshots/constants.ts similarity index 92% rename from x-pack/plugins/reporting/server/lib/screenshots/constants.ts rename to x-pack/plugins/screenshotting/server/screenshots/constants.ts index c62b910630874..b1064ec147745 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/constants.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/constants.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { APP_WRAPPER_CLASS } from '../../../../../../src/core/server'; +import { APP_WRAPPER_CLASS } from '../../../../../src/core/server'; export const DEFAULT_PAGELOAD_SELECTOR = `.${APP_WRAPPER_CLASS}`; export const CONTEXT_GETNUMBEROFITEMS = 'GetNumberOfItems'; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.test.ts similarity index 69% rename from x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.test.ts rename to x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.test.ts index 389ae4f49f3b6..7d5791f0dfeb1 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.test.ts @@ -5,48 +5,21 @@ * 2.0. */ -import { HeadlessChromiumDriver } from '../../browsers'; -import { - createMockBrowserDriverFactory, - createMockConfig, - createMockConfigSchema, - createMockLayoutInstance, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; -import { LayoutInstance } from '../layouts'; +import type { Logger } from 'src/core/server'; +import { createMockBrowserDriver } from '../browsers/mock'; +import { createMockLayout } from '../layouts/mock'; import { getElementPositionAndAttributes } from './get_element_position_data'; describe('getElementPositionAndAttributes', () => { - let layout: LayoutInstance; - let logger: ReturnType; - let browser: HeadlessChromiumDriver; + const logger = {} as jest.Mocked; + let browser: ReturnType; + let layout: ReturnType; beforeEach(async () => { - const schema = createMockConfigSchema(); - const config = createMockConfig(schema); - const captureConfig = config.get('capture'); - const core = await createMockReportingCore(schema); + browser = createMockBrowserDriver(); + layout = createMockLayout(); - layout = createMockLayoutInstance(captureConfig); - logger = createMockLevelLogger(); - - await createMockBrowserDriverFactory(core, logger, { - evaluate: jest.fn( - async unknown>({ - fn, - args, - }: { - fn: T; - args: Parameters; - }) => fn(...args) - ), - getCreatePage: (driver) => { - browser = driver; - - return jest.fn(); - }, - }); + browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args)); // @see https://github.com/jsdom/jsdom/issues/653 const querySelectorAll = document.querySelectorAll.bind(document); @@ -69,7 +42,6 @@ describe('getElementPositionAndAttributes', () => { }); afterEach(() => { - jest.restoreAllMocks(); document.body.innerHTML = ''; }); @@ -87,7 +59,7 @@ describe('getElementPositionAndAttributes', () => { /> `; - await expect(getElementPositionAndAttributes(browser, layout, logger)).resolves + await expect(getElementPositionAndAttributes(browser, logger, layout)).resolves .toMatchInlineSnapshot(` Array [ Object { @@ -131,6 +103,6 @@ describe('getElementPositionAndAttributes', () => { }); it('should return null when there are no elements matching', async () => { - await expect(getElementPositionAndAttributes(browser, layout, logger)).resolves.toBeNull(); + await expect(getElementPositionAndAttributes(browser, logger, layout)).resolves.toBeNull(); }); }); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.ts similarity index 75% rename from x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts rename to x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.ts index 39163843c732f..f7576a012e738 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_element_position_data.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_element_position_data.ts @@ -6,18 +6,41 @@ */ import { i18n } from '@kbn/i18n'; -import { LevelLogger, startTrace } from '../'; -import { HeadlessChromiumDriver } from '../../browsers'; -import { LayoutInstance } from '../layouts'; -import { AttributesMap, ElementsPositionAndAttribute } from './'; +import apm from 'elastic-apm-node'; +import type { Logger } from 'src/core/server'; +import type { HeadlessChromiumDriver } from '../browsers'; +import { Layout } from '../layouts'; import { CONTEXT_ELEMENTATTRIBUTES } from './constants'; +export interface AttributesMap { + [key: string]: string | null; +} + +export interface ElementPosition { + boundingClientRect: { + // modern browsers support x/y, but older ones don't + top: number; + left: number; + width: number; + height: number; + }; + scroll: { + x: number; + y: number; + }; +} + +export interface ElementsPositionAndAttribute { + position: ElementPosition; + attributes: AttributesMap; +} + export const getElementPositionAndAttributes = async ( browser: HeadlessChromiumDriver, - layout: LayoutInstance, - logger: LevelLogger + logger: Logger, + layout: Layout ): Promise => { - const endTrace = startTrace('get_element_position_data', 'read'); + const span = apm.startSpan('get_element_position_data', 'read'); const { screenshot: screenshotSelector } = layout.selectors; // data-shared-items-container let elementsPositionAndAttributes: ElementsPositionAndAttribute[] | null; try { @@ -60,7 +83,7 @@ export const getElementPositionAndAttributes = async ( if (!elementsPositionAndAttributes?.length) { throw new Error( - i18n.translate('xpack.reporting.screencapture.noElements', { + i18n.translate('xpack.screenshotting.screencapture.noElements', { defaultMessage: `An error occurred while reading the page for visualization panels: no panels were found.`, }) ); @@ -69,7 +92,7 @@ export const getElementPositionAndAttributes = async ( elementsPositionAndAttributes = null; } - endTrace(); + span?.end(); return elementsPositionAndAttributes; }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.test.ts new file mode 100644 index 0000000000000..e5e70f617339d --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from 'src/core/server'; +import { createMockBrowserDriver } from '../browsers/mock'; +import { createMockLayout } from '../layouts/mock'; +import { getNumberOfItems } from './get_number_of_items'; + +describe('getNumberOfItems', () => { + const timeout = 10; + let browser: ReturnType; + let layout: ReturnType; + let logger: jest.Mocked; + + beforeEach(async () => { + browser = createMockBrowserDriver(); + layout = createMockLayout(); + logger = { debug: jest.fn() } as unknown as jest.Mocked; + + browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args)); + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('should determine the number of items by attribute', async () => { + document.body.innerHTML = ` +
+ `; + + await expect(getNumberOfItems(browser, logger, timeout, layout)).resolves.toBe(10); + }); + + it('should determine the number of items by selector ', async () => { + document.body.innerHTML = ` + + + + `; + + await expect(getNumberOfItems(browser, logger, timeout, layout)).resolves.toBe(3); + }); + + it('should fall back to the selector when the attribute is empty', async () => { + document.body.innerHTML = ` +
+ + + `; + + await expect(getNumberOfItems(browser, logger, timeout, layout)).resolves.toBe(2); + }); +}); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.ts similarity index 79% rename from x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts rename to x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.ts index 9e5dfa180fd0f..3677fe99d932f 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_number_of_items.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_number_of_items.ts @@ -6,23 +6,24 @@ */ import { i18n } from '@kbn/i18n'; -import { LevelLogger, startTrace } from '../'; -import { HeadlessChromiumDriver } from '../../browsers'; -import { LayoutInstance } from '../layouts'; +import apm from 'elastic-apm-node'; +import type { Logger } from 'src/core/server'; +import type { HeadlessChromiumDriver } from '../browsers'; +import { Layout } from '../layouts'; import { CONTEXT_GETNUMBEROFITEMS, CONTEXT_READMETADATA } from './constants'; export const getNumberOfItems = async ( - timeout: number, browser: HeadlessChromiumDriver, - layout: LayoutInstance, - logger: LevelLogger + logger: Logger, + timeout: number, + layout: Layout ): Promise => { - const endTrace = startTrace('get_number_of_items', 'read'); + const span = apm.startSpan('get_number_of_items', 'read'); const { renderComplete: renderCompleteSelector, itemsCountAttribute } = layout.selectors; let itemsCount: number; logger.debug( - i18n.translate('xpack.reporting.screencapture.logWaitingForElements', { + i18n.translate('xpack.screenshotting.screencapture.logWaitingForElements', { defaultMessage: 'waiting for elements or items count attribute; or not found to interrupt', }) ); @@ -58,17 +59,17 @@ export const getNumberOfItems = async ( { context: CONTEXT_GETNUMBEROFITEMS }, logger ); - } catch (err) { - logger.error(err); + } catch (error) { + logger.error(error); throw new Error( - i18n.translate('xpack.reporting.screencapture.readVisualizationsError', { + i18n.translate('xpack.screenshotting.screencapture.readVisualizationsError', { defaultMessage: `An error occurred when trying to read the page for visualization panel info: {error}`, - values: { error: err }, + values: { error }, }) ); } - endTrace(); + span?.end(); return itemsCount; }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.test.ts new file mode 100644 index 0000000000000..75576d7221f5e --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.test.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from 'src/core/server'; +import { createMockBrowserDriver } from '../browsers/mock'; +import { createMockLayout } from '../layouts/mock'; +import { getRenderErrors } from './get_render_errors'; + +describe('getRenderErrors', () => { + let browser: ReturnType; + let layout: ReturnType; + let logger: jest.Mocked; + + beforeEach(async () => { + browser = createMockBrowserDriver(); + layout = createMockLayout(); + logger = { debug: jest.fn(), warn: jest.fn() } as unknown as jest.Mocked; + + browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args)); + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('should extract the error messages', async () => { + document.body.innerHTML = ` +
+
+
+
+ `; + + await expect(getRenderErrors(browser, logger, layout)).resolves.toEqual([ + 'a test error', + 'a test error', + 'a test error', + 'a test error', + ]); + }); + + it('should extract the error messages, even when there are none', async () => { + document.body.innerHTML = ` + + `; + + await expect(getRenderErrors(browser, logger, layout)).resolves.toEqual(undefined); + }); +}); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_render_errors.ts b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.ts similarity index 78% rename from x-pack/plugins/reporting/server/lib/screenshots/get_render_errors.ts rename to x-pack/plugins/screenshotting/server/screenshots/get_render_errors.ts index ded4ed6238872..ad3da8d0ef488 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_render_errors.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_render_errors.ts @@ -6,17 +6,18 @@ */ import { i18n } from '@kbn/i18n'; -import type { HeadlessChromiumDriver } from '../../browsers'; -import type { LayoutInstance } from '../layouts'; -import { LevelLogger, startTrace } from '../'; +import apm from 'elastic-apm-node'; +import type { Logger } from 'src/core/server'; +import type { HeadlessChromiumDriver } from '../browsers'; +import type { Layout } from '../layouts'; import { CONTEXT_GETRENDERERRORS } from './constants'; export const getRenderErrors = async ( browser: HeadlessChromiumDriver, - layout: LayoutInstance, - logger: LevelLogger + logger: Logger, + layout: Layout ): Promise => { - const endTrace = startTrace('get_render_errors', 'read'); + const span = apm.startSpan('get_render_errors', 'read'); logger.debug('reading render errors'); const errorsFound: undefined | string[] = await browser.evaluate( { @@ -38,11 +39,11 @@ export const getRenderErrors = async ( { context: CONTEXT_GETRENDERERRORS }, logger ); - endTrace(); + span?.end(); if (errorsFound?.length) { - logger.warning( - i18n.translate('xpack.reporting.screencapture.renderErrorsFound', { + logger.warn( + i18n.translate('xpack.screenshotting.screencapture.renderErrorsFound', { defaultMessage: 'Found {count} error messages. See report object for more information.', values: { count: errorsFound.length }, }) diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.test.ts similarity index 71% rename from x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.test.ts rename to x-pack/plugins/screenshotting/server/screenshots/get_screenshots.test.ts index edd346c9b8928..2bb00413c8231 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.test.ts @@ -5,13 +5,8 @@ * 2.0. */ -import { HeadlessChromiumDriver } from '../../browsers'; -import { - createMockBrowserDriverFactory, - createMockConfigSchema, - createMockLevelLogger, - createMockReportingCore, -} from '../../test_helpers'; +import type { Logger } from 'src/core/server'; +import { createMockBrowserDriver } from '../browsers/mock'; import { getScreenshots } from './get_screenshots'; describe('getScreenshots', () => { @@ -31,31 +26,14 @@ describe('getScreenshots', () => { }, }, ]; - - let logger: ReturnType; - let browser: jest.Mocked; + let browser: ReturnType; + let logger: jest.Mocked; beforeEach(async () => { - const core = await createMockReportingCore(createMockConfigSchema()); - - logger = createMockLevelLogger(); + browser = createMockBrowserDriver(); + logger = { info: jest.fn() } as unknown as jest.Mocked; - await createMockBrowserDriverFactory(core, logger, { - evaluate: jest.fn( - async unknown>({ - fn, - args, - }: { - fn: T; - args: Parameters; - }) => fn(...args) - ), - getCreatePage: (driver) => { - browser = driver as typeof browser; - - return jest.fn(); - }, - }); + browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args)); }); afterEach(() => { @@ -63,7 +41,7 @@ describe('getScreenshots', () => { }); it('should return screenshots', async () => { - await expect(getScreenshots(browser, elementsPositionAndAttributes, logger)).resolves + await expect(getScreenshots(browser, logger, elementsPositionAndAttributes)).resolves .toMatchInlineSnapshot(` Array [ Object { @@ -109,7 +87,7 @@ describe('getScreenshots', () => { }); it('should forward elements positions', async () => { - await getScreenshots(browser, elementsPositionAndAttributes, logger); + await getScreenshots(browser, logger, elementsPositionAndAttributes); expect(browser.screenshot).toHaveBeenCalledTimes(2); expect(browser.screenshot).toHaveBeenNthCalledWith( @@ -126,7 +104,7 @@ describe('getScreenshots', () => { browser.screenshot.mockResolvedValue(Buffer.from('')); await expect( - getScreenshots(browser, elementsPositionAndAttributes, logger) + getScreenshots(browser, logger, elementsPositionAndAttributes) ).rejects.toBeInstanceOf(Error); }); }); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts similarity index 59% rename from x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts rename to x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts index 9b5f234b78363..8e03bb8a77cc9 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_screenshots.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_screenshots.ts @@ -6,17 +6,35 @@ */ import { i18n } from '@kbn/i18n'; -import { LevelLogger, startTrace } from '../'; -import { HeadlessChromiumDriver } from '../../browsers'; -import { ElementsPositionAndAttribute, Screenshot } from './'; +import apm from 'elastic-apm-node'; +import type { Logger } from 'src/core/server'; +import type { HeadlessChromiumDriver } from '../browsers'; +import type { ElementsPositionAndAttribute } from './get_element_position_data'; + +export interface Screenshot { + /** + * Screenshot PNG image data. + */ + data: Buffer; + + /** + * Screenshot title. + */ + title: string | null; + + /** + * Screenshot description. + */ + description: string | null; +} export const getScreenshots = async ( browser: HeadlessChromiumDriver, - elementsPositionAndAttributes: ElementsPositionAndAttribute[], - logger: LevelLogger + logger: Logger, + elementsPositionAndAttributes: ElementsPositionAndAttribute[] ): Promise => { logger.info( - i18n.translate('xpack.reporting.screencapture.takingScreenshots', { + i18n.translate('xpack.screenshotting.screencapture.takingScreenshots', { defaultMessage: `taking screenshots`, }) ); @@ -24,7 +42,7 @@ export const getScreenshots = async ( const screenshots: Screenshot[] = []; for (let i = 0; i < elementsPositionAndAttributes.length; i++) { - const endTrace = startTrace('get_screenshots', 'read'); + const span = apm.startSpan('get_screenshots', 'read'); const item = elementsPositionAndAttributes[i]; const data = await browser.screenshot(item.position); @@ -39,11 +57,11 @@ export const getScreenshots = async ( description: item.attributes.description, }); - endTrace(); + span?.end(); } logger.info( - i18n.translate('xpack.reporting.screencapture.screenshotsTaken', { + i18n.translate('xpack.screenshotting.screencapture.screenshotsTaken', { defaultMessage: `screenshots taken: {numScreenhots}`, values: { numScreenhots: screenshots.length, diff --git a/x-pack/plugins/screenshotting/server/screenshots/get_time_range.test.ts b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.test.ts new file mode 100644 index 0000000000000..d277690a08282 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.test.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Logger } from 'src/core/server'; +import { createMockBrowserDriver } from '../browsers/mock'; +import { createMockLayout } from '../layouts/mock'; +import { getTimeRange } from './get_time_range'; + +describe('getTimeRange', () => { + let browser: ReturnType; + let layout: ReturnType; + let logger: jest.Mocked; + + beforeEach(async () => { + browser = createMockBrowserDriver(); + layout = createMockLayout(); + logger = { debug: jest.fn(), info: jest.fn() } as unknown as jest.Mocked; + + browser.evaluate.mockImplementation(({ fn, args }) => (fn as Function)(...args)); + }); + + afterEach(() => { + document.body.innerHTML = ''; + }); + + it('should return null when there is no duration element', async () => { + await expect(getTimeRange(browser, logger, layout)).resolves.toBeNull(); + }); + + it('should return null when duration attrbute is empty', async () => { + document.body.innerHTML = ` +
+ `; + + await expect(getTimeRange(browser, logger, layout)).resolves.toBeNull(); + }); + + it('should return duration', async () => { + document.body.innerHTML = ` +
+ `; + + await expect(getTimeRange(browser, logger, layout)).resolves.toBe('10'); + }); +}); diff --git a/x-pack/plugins/reporting/server/lib/screenshots/get_time_range.ts b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.ts similarity index 79% rename from x-pack/plugins/reporting/server/lib/screenshots/get_time_range.ts rename to x-pack/plugins/screenshotting/server/screenshots/get_time_range.ts index 111c68de62bdf..6734a35932b59 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/get_time_range.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/get_time_range.ts @@ -5,17 +5,18 @@ * 2.0. */ -import { LevelLogger, startTrace } from '../'; -import { LayoutInstance } from '../layouts'; -import { HeadlessChromiumDriver } from '../../browsers'; +import apm from 'elastic-apm-node'; +import type { Logger } from 'src/core/server'; +import type { HeadlessChromiumDriver } from '../browsers'; +import { Layout } from '../layouts'; import { CONTEXT_GETTIMERANGE } from './constants'; export const getTimeRange = async ( browser: HeadlessChromiumDriver, - layout: LayoutInstance, - logger: LevelLogger + logger: Logger, + layout: Layout ): Promise => { - const endTrace = startTrace('get_time_range', 'read'); + const span = apm.startSpan('get_time_range', 'read'); logger.debug('getting timeRange'); const timeRange = await browser.evaluate( @@ -46,7 +47,7 @@ export const getTimeRange = async ( logger.debug('no timeRange'); } - endTrace(); + span?.end(); return timeRange; }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.test.ts b/x-pack/plugins/screenshotting/server/screenshots/index.test.ts new file mode 100644 index 0000000000000..1fa7eb66192c8 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/index.test.ts @@ -0,0 +1,411 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { of, throwError, NEVER } from 'rxjs'; +import type { Logger } from 'src/core/server'; +import { createMockBrowserDriver, createMockBrowserDriverFactory } from '../browsers/mock'; +import type { HeadlessChromiumDriverFactory } from '../browsers'; +import * as Layouts from '../layouts/create_layout'; +import { createMockLayout } from '../layouts/mock'; +import { CONTEXT_ELEMENTATTRIBUTES } from './constants'; +import { getScreenshots, ScreenshotOptions } from '.'; + +/* + * Tests + */ +describe('Screenshot Observable Pipeline', () => { + let driver: ReturnType; + let driverFactory: jest.Mocked; + let layout: ReturnType; + let logger: jest.Mocked; + let options: ScreenshotOptions; + + beforeEach(async () => { + driver = createMockBrowserDriver(); + driverFactory = createMockBrowserDriverFactory(driver); + layout = createMockLayout(); + logger = { + debug: jest.fn(), + error: jest.fn(), + info: jest.fn(), + } as unknown as jest.Mocked; + options = { + browserTimezone: 'UTC', + conditionalHeaders: {}, + layout: {}, + timeouts: { + loadDelay: 2000, + openUrl: 120000, + waitForElements: 20000, + renderComplete: 10000, + }, + urls: ['/welcome/home/start/index.htm'], + } as unknown as typeof options; + + jest.spyOn(Layouts, 'createLayout').mockReturnValue(layout); + + driver.isPageOpen.mockReturnValue(true); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('pipelines a single url into screenshot and timeRange', async () => { + const result = await getScreenshots(driverFactory, logger, options).toPromise(); + + expect(result).toHaveProperty('results'); + expect(result.results).toMatchInlineSnapshot(` + Array [ + Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object { + "description": "Default ", + "title": "Default Mock Title", + }, + "position": Object { + "boundingClientRect": Object { + "height": 600, + "left": 0, + "top": 0, + "width": 800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], + "error": undefined, + "screenshots": Array [ + Object { + "data": Object { + "data": Array [ + 115, + 99, + 114, + 101, + 101, + 110, + 115, + 104, + 111, + 116, + ], + "type": "Buffer", + }, + "description": "Default ", + "title": "Default Mock Title", + }, + ], + "timeRange": "Default GetTimeRange Result", + }, + ] + `); + }); + + it('pipelines multiple urls into', async () => { + driver.screenshot.mockResolvedValue(Buffer.from('some screenshots')); + const result = await getScreenshots(driverFactory, logger, { + ...options, + urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php'], + }).toPromise(); + + expect(result).toHaveProperty('results'); + expect(result.results).toMatchInlineSnapshot(` + Array [ + Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object { + "description": "Default ", + "title": "Default Mock Title", + }, + "position": Object { + "boundingClientRect": Object { + "height": 600, + "left": 0, + "top": 0, + "width": 800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], + "error": undefined, + "screenshots": Array [ + Object { + "data": Object { + "data": Array [ + 115, + 111, + 109, + 101, + 32, + 115, + 99, + 114, + 101, + 101, + 110, + 115, + 104, + 111, + 116, + 115, + ], + "type": "Buffer", + }, + "description": "Default ", + "title": "Default Mock Title", + }, + ], + "timeRange": "Default GetTimeRange Result", + }, + Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object { + "description": "Default ", + "title": "Default Mock Title", + }, + "position": Object { + "boundingClientRect": Object { + "height": 600, + "left": 0, + "top": 0, + "width": 800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], + "error": undefined, + "screenshots": Array [ + Object { + "data": Object { + "data": Array [ + 115, + 111, + 109, + 101, + 32, + 115, + 99, + 114, + 101, + 101, + 110, + 115, + 104, + 111, + 116, + 115, + ], + "type": "Buffer", + }, + "description": "Default ", + "title": "Default Mock Title", + }, + ], + "timeRange": "Default GetTimeRange Result", + }, + ] + `); + + expect(driver.open).toHaveBeenCalledTimes(2); + expect(driver.open).nthCalledWith( + 1, + expect.anything(), + expect.objectContaining({ waitForSelector: '.kbnAppWrapper' }), + expect.anything() + ); + expect(driver.open).nthCalledWith( + 2, + expect.anything(), + expect.objectContaining({ waitForSelector: '[data-shared-page="2"]' }), + expect.anything() + ); + }); + + describe('error handling', () => { + it('recovers if waitForSelector fails', async () => { + driver.waitForSelector.mockImplementation((selectorArg: string) => { + throw new Error('Mock error!'); + }); + const result = await getScreenshots(driverFactory, logger, { + ...options, + urls: ['/welcome/home/start/index2.htm', '/welcome/home/start/index.php3?page=./home.php3'], + }).toPromise(); + + expect(result).toHaveProperty('results'); + expect(result.results).toMatchInlineSnapshot(` + Array [ + Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], + "error": [Error: The "wait for elements" phase encountered an error: Error: An error occurred when trying to read the page for visualization panel info: Error: Mock error!], + "screenshots": Array [ + Object { + "data": Object { + "data": Array [ + 115, + 99, + 114, + 101, + 101, + 110, + 115, + 104, + 111, + 116, + ], + "type": "Buffer", + }, + "description": undefined, + "title": undefined, + }, + ], + "timeRange": null, + }, + Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 100, + "left": 0, + "top": 0, + "width": 100, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], + "error": [Error: The "wait for elements" phase encountered an error: Error: An error occurred when trying to read the page for visualization panel info: Error: Mock error!], + "screenshots": Array [ + Object { + "data": Object { + "data": Array [ + 115, + 99, + 114, + 101, + 101, + 110, + 115, + 104, + 111, + 116, + ], + "type": "Buffer", + }, + "description": undefined, + "title": undefined, + }, + ], + "timeRange": null, + }, + ] + `); + }); + + it('observes page exit', async () => { + driverFactory.createPage.mockReturnValue( + of({ driver, exit$: throwError('Instant timeout has fired!'), metrics$: NEVER }) + ); + + await expect( + getScreenshots(driverFactory, logger, options).toPromise() + ).rejects.toMatchInlineSnapshot(`"Instant timeout has fired!"`); + }); + + it(`uses defaults for element positions and size when Kibana page is not ready`, async () => { + driver.evaluate.mockImplementation(async (_, { context }) => + context === CONTEXT_ELEMENTATTRIBUTES ? null : undefined + ); + + layout.getViewport = () => null; + const result = await getScreenshots(driverFactory, logger, options).toPromise(); + + expect(result).toHaveProperty('results'); + expect(result.results).toMatchInlineSnapshot(` + Array [ + Object { + "elementsPositionAndAttributes": Array [ + Object { + "attributes": Object {}, + "position": Object { + "boundingClientRect": Object { + "height": 1200, + "left": 0, + "top": 0, + "width": 1800, + }, + "scroll": Object { + "x": 0, + "y": 0, + }, + }, + }, + ], + "error": undefined, + "screenshots": Array [ + Object { + "data": Object { + "data": Array [ + 115, + 99, + 114, + 101, + 101, + 110, + 115, + 104, + 111, + 116, + ], + "type": "Buffer", + }, + "description": undefined, + "title": undefined, + }, + ], + "timeRange": undefined, + }, + ] + `); + }); + }); +}); diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.ts b/x-pack/plugins/screenshotting/server/screenshots/index.ts new file mode 100644 index 0000000000000..e264538d8be39 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/index.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import apm from 'elastic-apm-node'; +import { from, of, Observable } from 'rxjs'; +import { + catchError, + concatMap, + first, + map, + mergeMap, + take, + takeUntil, + toArray, +} from 'rxjs/operators'; +import type { Logger } from 'src/core/server'; +import { LayoutParams } from '../../common'; +import type { HeadlessChromiumDriverFactory, PerformanceMetrics } from '../browsers'; +import { createLayout } from '../layouts'; +import type { Layout } from '../layouts'; +import { ScreenshotObservableHandler } from './observable'; +import type { ScreenshotObservableOptions, ScreenshotObservableResult } from './observable'; + +export interface ScreenshotOptions extends ScreenshotObservableOptions { + layout: LayoutParams; +} + +export interface ScreenshotResult { + /** + * Used layout instance constructed from the given options. + */ + layout: Layout; + + /** + * Collected performance metrics during the screenshotting session. + */ + metrics$: Observable; + + /** + * Screenshotting results. + */ + results: ScreenshotObservableResult[]; +} + +const DEFAULT_SETUP_RESULT = { + elementsPositionAndAttributes: null, + timeRange: null, +}; + +export function getScreenshots( + browserDriverFactory: HeadlessChromiumDriverFactory, + logger: Logger, + options: ScreenshotOptions +): Observable { + const apmTrans = apm.startTransaction('screenshot-pipeline', 'screenshotting'); + const apmCreateLayout = apmTrans?.startSpan('create-layout', 'setup'); + const layout = createLayout(options.layout); + logger.debug(`Layout: width=${layout.width} height=${layout.height}`); + apmCreateLayout?.end(); + + const apmCreatePage = apmTrans?.startSpan('create-page', 'wait'); + const { + browserTimezone, + timeouts: { openUrl: openUrlTimeout }, + } = options; + + return browserDriverFactory.createPage({ browserTimezone, openUrlTimeout }, logger).pipe( + mergeMap(({ driver, exit$, metrics$ }) => { + apmCreatePage?.end(); + metrics$.subscribe(({ cpu, memory }) => { + apmTrans?.setLabel('cpu', cpu, false); + apmTrans?.setLabel('memory', memory, false); + }); + exit$.subscribe({ error: () => apmTrans?.end() }); + + const screen = new ScreenshotObservableHandler(driver, logger, layout, options); + + return from(options.urls).pipe( + concatMap((url, index) => + screen.setupPage(index, url, apmTrans).pipe( + catchError((error) => { + screen.checkPageIsOpen(); // this fails the job if the browser has closed + + logger.error(error); + return of({ ...DEFAULT_SETUP_RESULT, error }); // allow failover screenshot capture + }), + takeUntil(exit$), + screen.getScreenshots() + ) + ), + take(options.urls.length), + toArray(), + map((results) => ({ layout, metrics$, results })) + ); + }), + first() + ); +} diff --git a/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts b/x-pack/plugins/screenshotting/server/screenshots/inject_css.ts similarity index 78% rename from x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts rename to x-pack/plugins/screenshotting/server/screenshots/inject_css.ts index 607441e719c32..d4e38600db7de 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/inject_css.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/inject_css.ts @@ -8,8 +8,9 @@ import { i18n } from '@kbn/i18n'; import fs from 'fs'; import { promisify } from 'util'; -import { LevelLogger, startTrace } from '../'; -import { HeadlessChromiumDriver } from '../../browsers'; +import apm from 'elastic-apm-node'; +import type { Logger } from 'src/core/server'; +import type { HeadlessChromiumDriver } from '../browsers'; import { Layout } from '../layouts'; import { CONTEXT_INJECTCSS } from './constants'; @@ -17,12 +18,12 @@ const fsp = { readFile: promisify(fs.readFile) }; export const injectCustomCss = async ( browser: HeadlessChromiumDriver, - layout: Layout, - logger: LevelLogger + logger: Logger, + layout: Layout ): Promise => { - const endTrace = startTrace('inject_css', 'correction'); + const span = apm.startSpan('inject_css', 'correction'); logger.debug( - i18n.translate('xpack.reporting.screencapture.injectingCss', { + i18n.translate('xpack.screenshotting.screencapture.injectingCss', { defaultMessage: 'injecting custom css', }) ); @@ -49,12 +50,12 @@ export const injectCustomCss = async ( } catch (err) { logger.error(err); throw new Error( - i18n.translate('xpack.reporting.screencapture.injectCss', { + i18n.translate('xpack.screenshotting.screencapture.injectCss', { defaultMessage: `An error occurred when trying to update Kibana CSS for reporting. {error}`, values: { error: err }, }) ); } - endTrace(); + span?.end(); }; diff --git a/x-pack/plugins/screenshotting/server/screenshots/mock.ts b/x-pack/plugins/screenshotting/server/screenshots/mock.ts new file mode 100644 index 0000000000000..edef9c9044c9a --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/mock.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { of, NEVER } from 'rxjs'; +import { createMockLayout } from '../layouts/mock'; +import type { getScreenshots, ScreenshotResult } from '.'; + +export function createMockScreenshots(): jest.Mocked<{ getScreenshots: typeof getScreenshots }> { + return { + getScreenshots: jest.fn((driverFactory, logger, options) => + of({ + layout: createMockLayout(), + metrics$: NEVER, + results: options.urls.map(() => ({ + timeRange: null, + screenshots: [ + { + data: Buffer.from('screenshot'), + description: null, + title: null, + }, + ], + })), + } as ScreenshotResult) + ), + }; +} diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts new file mode 100644 index 0000000000000..5d5fbbde4e048 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/observable.test.ts @@ -0,0 +1,102 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { interval, throwError, of } from 'rxjs'; +import { map } from 'rxjs/operators'; +import type { Logger } from 'src/core/server'; +import type { ConditionalHeaders } from '../browsers'; +import { createMockBrowserDriver } from '../browsers/mock'; +import { createMockLayout } from '../layouts/mock'; +import { ScreenshotObservableHandler, ScreenshotObservableOptions } from './observable'; + +describe('ScreenshotObservableHandler', () => { + let browser: ReturnType; + let layout: ReturnType; + let logger: jest.Mocked; + let options: ScreenshotObservableOptions; + + beforeEach(async () => { + browser = createMockBrowserDriver(); + layout = createMockLayout(); + logger = { error: jest.fn() } as unknown as jest.Mocked; + options = { + conditionalHeaders: { + headers: { testHeader: 'testHeadValue' }, + conditions: {} as unknown as ConditionalHeaders['conditions'], + }, + timeouts: { + loadDelay: 5000, + openUrl: 30000, + waitForElements: 30000, + renderComplete: 30000, + }, + urls: [], + }; + + browser.isPageOpen.mockReturnValue(true); + }); + + describe('waitUntil', () => { + it('catches TimeoutError and references the timeout config in a custom message', async () => { + const screenshots = new ScreenshotObservableHandler(browser, logger, layout, options); + const test$ = interval(1000).pipe(screenshots.waitUntil(200, 'Test Config')); + + const testPipeline = () => test$.toPromise(); + await expect(testPipeline).rejects.toMatchInlineSnapshot( + `[Error: The "Test Config" phase took longer than 0.2 seconds.]` + ); + }); + + it('catches other Errors and explains where they were thrown', async () => { + const screenshots = new ScreenshotObservableHandler(browser, logger, layout, options); + const test$ = throwError(new Error(`Test Error to Throw`)).pipe( + screenshots.waitUntil(200, 'Test Config') + ); + + const testPipeline = () => test$.toPromise(); + await expect(testPipeline).rejects.toMatchInlineSnapshot( + `[Error: The "Test Config" phase encountered an error: Error: Test Error to Throw]` + ); + }); + + it('is a pass-through if there is no Error', async () => { + const screenshots = new ScreenshotObservableHandler(browser, logger, layout, options); + const test$ = of('nice to see you').pipe(screenshots.waitUntil(20, 'xxxxxxxxxxx')); + + await expect(test$.toPromise()).resolves.toBe(`nice to see you`); + }); + }); + + describe('checkPageIsOpen', () => { + it('throws a decorated Error when page is not open', async () => { + browser.isPageOpen.mockReturnValue(false); + const screenshots = new ScreenshotObservableHandler(browser, logger, layout, options); + const test$ = of(234455).pipe( + map((input) => { + screenshots.checkPageIsOpen(); + return input; + }) + ); + + await expect(test$.toPromise()).rejects.toMatchInlineSnapshot( + `[Error: Browser was closed unexpectedly! Check the server logs for more info.]` + ); + }); + + it('is a pass-through when the page is open', async () => { + const screenshots = new ScreenshotObservableHandler(browser, logger, layout, options); + const test$ = of(234455).pipe( + map((input) => { + screenshots.checkPageIsOpen(); + return input; + }) + ); + + await expect(test$.toPromise()).resolves.toBe(234455); + }); + }); +}); diff --git a/x-pack/plugins/screenshotting/server/screenshots/observable.ts b/x-pack/plugins/screenshotting/server/screenshots/observable.ts new file mode 100644 index 0000000000000..b77180a9399b1 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/screenshots/observable.ts @@ -0,0 +1,258 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Transaction } from 'elastic-apm-node'; +import { defer, forkJoin, throwError, Observable } from 'rxjs'; +import { catchError, mergeMap, switchMapTo, timeoutWith } from 'rxjs/operators'; +import type { Logger } from 'src/core/server'; +import type { Layout as ScreenshotModeLayout } from 'src/plugins/screenshot_mode/common'; +import type { ConditionalHeaders, HeadlessChromiumDriver } from '../browsers'; +import { getChromiumDisconnectedError } from '../browsers'; +import type { Layout } from '../layouts'; +import type { ElementsPositionAndAttribute } from './get_element_position_data'; +import { getElementPositionAndAttributes } from './get_element_position_data'; +import { getNumberOfItems } from './get_number_of_items'; +import { getRenderErrors } from './get_render_errors'; +import { getScreenshots } from './get_screenshots'; +import type { Screenshot } from './get_screenshots'; +import { getTimeRange } from './get_time_range'; +import { injectCustomCss } from './inject_css'; +import { openUrl } from './open_url'; +import type { UrlOrUrlWithContext } from './open_url'; +import { waitForRenderComplete } from './wait_for_render'; +import { waitForVisualizations } from './wait_for_visualizations'; + +export interface PhaseTimeouts { + /** + * Open URL phase timeout. + */ + openUrl: number; + + /** + * Timeout of the page readiness phase. + */ + waitForElements: number; + + /** + * Timeout of the page render phase. + */ + renderComplete: number; + + /** + * An additional delay to wait until the visualizations are ready. + */ + loadDelay: number; +} + +export interface ScreenshotObservableOptions { + /** + * The browser timezone that will be emulated in the browser instance. + * This option should be used to keep timezone on server and client in sync. + */ + browserTimezone?: string; + + /** + * Custom headers to be sent with each request. + */ + conditionalHeaders: ConditionalHeaders; + + /** + * Timeouts for each phase of the screenshot. + */ + timeouts: PhaseTimeouts; + + /** + * The list or URL to take screenshots of. + * Every item can either be a string or a tuple containing a URL and a context. + */ + urls: UrlOrUrlWithContext[]; +} + +export interface ScreenshotObservableResult { + /** + * Used time range filter. + */ + timeRange: string | null; + + /** + * Taken screenshots. + */ + screenshots: Screenshot[]; + + /** + * Error that occurred during the screenshotting. + */ + error?: Error; + + /** + * Individual visualizations might encounter errors at runtime. If there are any they are added to this + * field. Any text captured here is intended to be shown to the user for debugging purposes, reporting + * does no further sanitization on these strings. + */ + renderErrors?: string[]; + + /** + * @internal + */ + elementsPositionAndAttributes?: ElementsPositionAndAttribute[]; // NOTE: for testing +} + +interface PageSetupResults { + elementsPositionAndAttributes: ElementsPositionAndAttribute[] | null; + timeRange: string | null; + error?: Error; +} + +const DEFAULT_SCREENSHOT_CLIP_HEIGHT = 1200; +const DEFAULT_SCREENSHOT_CLIP_WIDTH = 1800; + +const getDefaultElementPosition = (dimensions: { height?: number; width?: number } | null) => { + const height = dimensions?.height || DEFAULT_SCREENSHOT_CLIP_HEIGHT; + const width = dimensions?.width || DEFAULT_SCREENSHOT_CLIP_WIDTH; + + return [ + { + position: { + boundingClientRect: { top: 0, left: 0, height, width }, + scroll: { x: 0, y: 0 }, + }, + attributes: {}, + }, + ]; +}; + +/* + * If Kibana is showing a non-HTML error message, the viewport might not be + * provided by the browser. + */ +const getDefaultViewPort = () => ({ + height: DEFAULT_SCREENSHOT_CLIP_HEIGHT, + width: DEFAULT_SCREENSHOT_CLIP_WIDTH, + zoom: 1, +}); + +export class ScreenshotObservableHandler { + constructor( + private readonly driver: HeadlessChromiumDriver, + private readonly logger: Logger, + private readonly layout: Layout, + private options: ScreenshotObservableOptions + ) {} + + /* + * Decorates a TimeoutError with context of the phase that has timed out. + */ + public waitUntil(timeoutValue: number, label: string) { + return (source: Observable) => + source.pipe( + catchError((error) => { + throw new Error(`The "${label}" phase encountered an error: ${error}`); + }), + timeoutWith( + timeoutValue, + throwError( + new Error(`The "${label}" phase took longer than ${timeoutValue / 1000} seconds.`) + ) + ) + ); + } + + private openUrl(index: number, url: UrlOrUrlWithContext) { + return defer(() => + openUrl( + this.driver, + this.logger, + this.options.timeouts.openUrl, + index, + url, + this.options.conditionalHeaders, + this.layout.id as ScreenshotModeLayout + ) + ).pipe(this.waitUntil(this.options.timeouts.openUrl, 'open URL')); + } + + private waitForElements() { + const driver = this.driver; + const waitTimeout = this.options.timeouts.waitForElements; + + return defer(() => getNumberOfItems(driver, this.logger, waitTimeout, this.layout)).pipe( + mergeMap((itemsCount) => { + // set the viewport to the dimentions from the job, to allow elements to flow into the expected layout + const viewport = this.layout.getViewport(itemsCount) || getDefaultViewPort(); + + return forkJoin([ + driver.setViewport(viewport, this.logger), + waitForVisualizations(driver, this.logger, waitTimeout, itemsCount, this.layout), + ]); + }), + this.waitUntil(waitTimeout, 'wait for elements') + ); + } + + private completeRender(apmTrans: Transaction | null) { + const driver = this.driver; + const layout = this.layout; + const logger = this.logger; + + return defer(async () => { + // Waiting till _after_ elements have rendered before injecting our CSS + // allows for them to be displayed properly in many cases + await injectCustomCss(driver, logger, layout); + + const apmPositionElements = apmTrans?.startSpan('position-elements', 'correction'); + // position panel elements for print layout + await layout.positionElements?.(driver, logger); + apmPositionElements?.end(); + + await waitForRenderComplete(driver, logger, this.options.timeouts.loadDelay, layout); + }).pipe( + mergeMap(() => + forkJoin({ + timeRange: getTimeRange(driver, logger, layout), + elementsPositionAndAttributes: getElementPositionAndAttributes(driver, logger, layout), + renderErrors: getRenderErrors(driver, logger, layout), + }) + ), + this.waitUntil(this.options.timeouts.renderComplete, 'render complete') + ); + } + + public setupPage(index: number, url: UrlOrUrlWithContext, apmTrans: Transaction | null) { + return this.openUrl(index, url).pipe( + switchMapTo(this.waitForElements()), + switchMapTo(this.completeRender(apmTrans)) + ); + } + + public getScreenshots() { + return (withRenderComplete: Observable) => + withRenderComplete.pipe( + mergeMap(async (data: PageSetupResults): Promise => { + this.checkPageIsOpen(); // fail the report job if the browser has closed + + const elements = + data.elementsPositionAndAttributes ?? + getDefaultElementPosition(this.layout.getViewport(1)); + const screenshots = await getScreenshots(this.driver, this.logger, elements); + const { timeRange, error: setupError } = data; + + return { + timeRange, + screenshots, + error: setupError, + elementsPositionAndAttributes: elements, + }; + }) + ); + } + + public checkPageIsOpen() { + if (!this.driver.isPageOpen()) { + throw getChromiumDisconnectedError(); + } + } +} diff --git a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts b/x-pack/plugins/screenshotting/server/screenshots/open_url.ts similarity index 54% rename from x-pack/plugins/reporting/server/lib/screenshots/open_url.ts rename to x-pack/plugins/screenshotting/server/screenshots/open_url.ts index b26037aa917b8..08639122a4c26 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/open_url.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/open_url.ts @@ -6,52 +6,57 @@ */ import { i18n } from '@kbn/i18n'; -import { LevelLogger, startTrace } from '../'; -import { LocatorParams, UrlOrUrlLocatorTuple } from '../../../common/types'; -import { HeadlessChromiumDriver } from '../../browsers'; -import { ConditionalHeaders } from '../../export_types/common'; -import { Layout } from '../layouts'; +import apm from 'elastic-apm-node'; +import type { Logger } from 'src/core/server'; +import type { Layout } from 'src/plugins/screenshot_mode/common'; +import { Context } from '../../common'; +import type { HeadlessChromiumDriver } from '../browsers'; +import type { ConditionalHeaders } from '../browsers'; import { DEFAULT_PAGELOAD_SELECTOR } from './constants'; +type Url = string; +type UrlWithContext = [url: Url, context: Context]; +export type UrlOrUrlWithContext = Url | UrlWithContext; + export const openUrl = async ( - timeout: number, browser: HeadlessChromiumDriver, + logger: Logger, + timeout: number, index: number, - urlOrUrlLocatorTuple: UrlOrUrlLocatorTuple, + urlOrUrlWithContext: UrlOrUrlWithContext, conditionalHeaders: ConditionalHeaders, - layout: undefined | Layout, - logger: LevelLogger + layout?: Layout ): Promise => { // If we're moving to another page in the app, we'll want to wait for the app to tell us // it's loaded the next page. const page = index + 1; const waitForSelector = page > 1 ? `[data-shared-page="${page}"]` : DEFAULT_PAGELOAD_SELECTOR; + const span = apm.startSpan('open_url', 'wait'); - const endTrace = startTrace('open_url', 'wait'); let url: string; - let locator: undefined | LocatorParams; + let context: Context | undefined; - if (typeof urlOrUrlLocatorTuple === 'string') { - url = urlOrUrlLocatorTuple; + if (typeof urlOrUrlWithContext === 'string') { + url = urlOrUrlWithContext; } else { - [url, locator] = urlOrUrlLocatorTuple; + [url, context] = urlOrUrlWithContext; } try { await browser.open( url, - { conditionalHeaders, waitForSelector, timeout, locator, layout }, + { conditionalHeaders, context, layout, waitForSelector, timeout }, logger ); } catch (err) { logger.error(err); throw new Error( - i18n.translate('xpack.reporting.screencapture.couldntLoadKibana', { + i18n.translate('xpack.screenshotting.screencapture.couldntLoadKibana', { defaultMessage: `An error occurred when trying to open the Kibana URL: {error}`, values: { error: err }, }) ); } - endTrace(); + span?.end(); }; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts b/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts similarity index 84% rename from x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts rename to x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts index 1ac4b58b61507..bdc75572e685e 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_render.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/wait_for_render.ts @@ -6,21 +6,22 @@ */ import { i18n } from '@kbn/i18n'; -import { LevelLogger, startTrace } from '../'; -import { HeadlessChromiumDriver } from '../../browsers'; -import { LayoutInstance } from '../layouts'; +import apm from 'elastic-apm-node'; +import type { Logger } from 'src/core/server'; +import type { HeadlessChromiumDriver } from '../browsers'; +import { Layout } from '../layouts'; import { CONTEXT_WAITFORRENDER } from './constants'; export const waitForRenderComplete = async ( - loadDelay: number, browser: HeadlessChromiumDriver, - layout: LayoutInstance, - logger: LevelLogger + logger: Logger, + loadDelay: number, + layout: Layout ) => { - const endTrace = startTrace('wait_for_render', 'wait'); + const span = apm.startSpan('wait_for_render', 'wait'); logger.debug( - i18n.translate('xpack.reporting.screencapture.waitingForRenderComplete', { + i18n.translate('xpack.screenshotting.screencapture.waitingForRenderComplete', { defaultMessage: 'waiting for rendering to complete', }) ); @@ -74,11 +75,11 @@ export const waitForRenderComplete = async ( ) .then(() => { logger.debug( - i18n.translate('xpack.reporting.screencapture.renderIsComplete', { + i18n.translate('xpack.screenshotting.screencapture.renderIsComplete', { defaultMessage: 'rendering is complete', }) ); - endTrace(); + span?.end(); }); }; diff --git a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts b/x-pack/plugins/screenshotting/server/screenshots/wait_for_visualizations.ts similarity index 80% rename from x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts rename to x-pack/plugins/screenshotting/server/screenshots/wait_for_visualizations.ts index 10a53b238d892..3102f444c2340 100644 --- a/x-pack/plugins/reporting/server/lib/screenshots/wait_for_visualizations.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/wait_for_visualizations.ts @@ -6,9 +6,10 @@ */ import { i18n } from '@kbn/i18n'; -import { LevelLogger, startTrace } from '../'; -import { HeadlessChromiumDriver } from '../../browsers'; -import { LayoutInstance } from '../layouts'; +import apm from 'elastic-apm-node'; +import type { Logger } from 'src/core/server'; +import type { HeadlessChromiumDriver } from '../browsers'; +import { Layout } from '../layouts'; import { CONTEXT_WAITFORELEMENTSTOBEINDOM } from './constants'; interface CompletedItemsCountParameters { @@ -36,17 +37,17 @@ const getCompletedItemsCount = ({ * 3. Wait for the render complete event to be fired once for each item */ export const waitForVisualizations = async ( - timeout: number, browser: HeadlessChromiumDriver, + logger: Logger, + timeout: number, toEqual: number, - layout: LayoutInstance, - logger: LevelLogger + layout: Layout ): Promise => { - const endTrace = startTrace('wait_for_visualizations', 'wait'); + const span = apm.startSpan('wait_for_visualizations', 'wait'); const { renderComplete: renderCompleteSelector } = layout.selectors; logger.debug( - i18n.translate('xpack.reporting.screencapture.waitingForRenderedElements', { + i18n.translate('xpack.screenshotting.screencapture.waitingForRenderedElements', { defaultMessage: `waiting for {itemsCount} rendered elements to be in the DOM`, values: { itemsCount: toEqual }, }) @@ -63,12 +64,12 @@ export const waitForVisualizations = async ( } catch (err) { logger.error(err); throw new Error( - i18n.translate('xpack.reporting.screencapture.couldntFinishRendering', { + i18n.translate('xpack.screenshotting.screencapture.couldntFinishRendering', { defaultMessage: `An error occurred when trying to wait for {count} visualizations to finish rendering. {error}`, values: { count: toEqual, error: err }, }) ); } - endTrace(); + span?.end(); }; diff --git a/x-pack/plugins/screenshotting/server/utils.ts b/x-pack/plugins/screenshotting/server/utils.ts new file mode 100644 index 0000000000000..eb6b18ef85906 --- /dev/null +++ b/x-pack/plugins/screenshotting/server/utils.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ChromiumArchivePaths, download as baseDownload, install as baseInstall } from './browsers'; + +const paths = new ChromiumArchivePaths(); + +export const download = baseDownload.bind(undefined, paths); +export const install = baseInstall.bind(undefined, paths); diff --git a/x-pack/plugins/screenshotting/tsconfig.json b/x-pack/plugins/screenshotting/tsconfig.json new file mode 100644 index 0000000000000..a1e81c4fb38d9 --- /dev/null +++ b/x-pack/plugins/screenshotting/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "outDir": "./target/types", + "emitDeclarationOnly": true, + "declaration": true, + "declarationMap": true + }, + "include": [ + "common/**/*", + "public/**/*", + "server/**/*", + "../../../typings/**/*" + ], + "references": [ + { "path": "../../../src/core/tsconfig.json" }, + { "path": "../../../src/plugins/screenshot_mode/tsconfig.json" }, + ] +} diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index 25c6b07fd03af..14e3c8cc95fe6 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -7,7 +7,6 @@ import type { TransformConfigSchema } from './transforms/types'; import { ENABLE_CASE_CONNECTOR } from '../../cases/common'; -import { METADATA_TRANSFORMS_PATTERN } from './endpoint/constants'; /** * as const @@ -252,8 +251,6 @@ export const DETECTION_ENGINE_PREPACKAGED_URL = export const DETECTION_ENGINE_PRIVILEGES_URL = `${DETECTION_ENGINE_URL}/privileges` as const; export const DETECTION_ENGINE_INDEX_URL = `${DETECTION_ENGINE_URL}/index` as const; export const DETECTION_ENGINE_TAGS_URL = `${DETECTION_ENGINE_URL}/tags` as const; -export const DETECTION_ENGINE_RULES_STATUS_URL = - `${DETECTION_ENGINE_RULES_URL}/_find_statuses` as const; export const DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL = `${DETECTION_ENGINE_RULES_URL}/prepackaged/_status` as const; export const DETECTION_ENGINE_RULES_BULK_ACTION = @@ -362,8 +359,6 @@ export const showAllOthersBucket: string[] = [ */ export const ELASTIC_NAME = 'estc' as const; -export const METADATA_TRANSFORM_STATS_URL = `/api/transform/transforms/${METADATA_TRANSFORMS_PATTERN}/_stats`; - export const RISKY_HOSTS_INDEX_PREFIX = 'ml_host_risk_score_latest_' as const; export const TRANSFORM_STATES = { diff --git a/x-pack/plugins/security_solution/common/cti/constants.ts b/x-pack/plugins/security_solution/common/cti/constants.ts index b33541c5057d8..9dbeffed6e8ab 100644 --- a/x-pack/plugins/security_solution/common/cti/constants.ts +++ b/x-pack/plugins/security_solution/common/cti/constants.ts @@ -23,6 +23,9 @@ export const FIRST_SEEN = 'indicator.first_seen'; export const LAST_SEEN = 'indicator.last_seen'; export const PROVIDER = 'indicator.provider'; export const REFERENCE = 'indicator.reference'; +export const FEED_NAME = 'feed.name'; + +export const FEED_NAME_PATH = `threat.${FEED_NAME}`; export const INDICATOR_FIRSTSEEN = `${ENRICHMENT_DESTINATION_PATH}.${FIRST_SEEN}`; export const INDICATOR_LASTSEEN = `${ENRICHMENT_DESTINATION_PATH}.${LAST_SEEN}`; @@ -58,14 +61,5 @@ export const EVENT_ENRICHMENT_INDICATOR_FIELD_MAP = { export const DEFAULT_EVENT_ENRICHMENT_FROM = 'now-30d'; export const DEFAULT_EVENT_ENRICHMENT_TO = 'now'; -export const CTI_DATASET_KEY_MAP: { [key: string]: string } = { - 'AbuseCH URL': 'ti_abusech.url', - 'AbuseCH Malware': 'ti_abusech.malware', - 'AbuseCH MalwareBazaar': 'ti_abusech.malwarebazaar', - 'AlienVault OTX': 'ti_otx.threat', - 'Anomali Limo': 'ti_anomali.limo', - 'Anomali Threatstream': 'ti_anomali.threatstream', - MISP: 'ti_misp.threat', - ThreatQuotient: 'ti_threatq.threat', - Cybersixgill: 'ti_cybersixgill.threat', -}; +export const TI_INTEGRATION_PREFIX = 'ti'; +export const OTHER_TI_DATASET_KEY = '_others_ti_'; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts index 3933d7e39275e..23c45c03b62a0 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/common/schemas.ts @@ -344,6 +344,15 @@ export type LastFailureAt = t.TypeOf; export const last_failure_message = t.string; export type LastFailureMessage = t.TypeOf; +export const last_gap = t.string; +export type LastGap = t.TypeOf; + +export const bulk_create_time_durations = t.array(t.string); +export type BulkCreateTimeDurations = t.TypeOf; + +export const search_after_time_durations = t.array(t.string); +export type SearchAfterTimeDurations = t.TypeOf; + export const status_date = IsoDateString; export type StatusDate = t.TypeOf; diff --git a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts index a9b9db09212dd..c5f4e5631e5c8 100644 --- a/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts +++ b/x-pack/plugins/security_solution/common/detection_engine/schemas/request/rule_schemas.ts @@ -71,6 +71,9 @@ import { last_failure_at, last_failure_message, namespace, + last_gap, + bulk_create_time_durations, + search_after_time_durations, } from '../common/schemas'; export const createSchema = < @@ -422,6 +425,9 @@ const responseOptionalFields = { last_success_message, last_failure_at, last_failure_message, + last_gap, + bulk_create_time_durations, + search_after_time_durations, }; export const fullResponseSchema = t.intersection([ diff --git a/x-pack/plugins/security_solution/common/endpoint/constants.ts b/x-pack/plugins/security_solution/common/endpoint/constants.ts index a7fe91345dd14..bcd3b9524bf60 100644 --- a/x-pack/plugins/security_solution/common/endpoint/constants.ts +++ b/x-pack/plugins/security_solution/common/endpoint/constants.ts @@ -44,6 +44,7 @@ export const LIMITED_CONCURRENCY_ENDPOINT_COUNT = 100; export const BASE_ENDPOINT_ROUTE = '/api/endpoint'; export const HOST_METADATA_LIST_ROUTE = `${BASE_ENDPOINT_ROUTE}/metadata`; export const HOST_METADATA_GET_ROUTE = `${BASE_ENDPOINT_ROUTE}/metadata/{id}`; +export const METADATA_TRANSFORMS_STATUS_ROUTE = `${BASE_ENDPOINT_ROUTE}/metadata/transforms`; export const TRUSTED_APPS_GET_API = `${BASE_ENDPOINT_ROUTE}/trusted_apps/{id}`; export const TRUSTED_APPS_LIST_API = `${BASE_ENDPOINT_ROUTE}/trusted_apps`; @@ -68,3 +69,6 @@ export const failedFleetActionErrorCode = '424'; export const ENDPOINT_DEFAULT_PAGE = 0; export const ENDPOINT_DEFAULT_PAGE_SIZE = 10; + +export const FORBIDDEN_MESSAGE = + 'You do not have permission to perform this action or license level does not allow for this action'; diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts index 26bf4ce6740a9..447817e52ee2c 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/cti/index.ts @@ -5,13 +5,16 @@ * 2.0. */ -import type { IEsSearchResponse } from 'src/plugins/data/public'; +import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { IEsSearchResponse, IEsSearchRequest } from 'src/plugins/data/public'; +import { FactoryQueryTypes } from '../..'; import { EVENT_ENRICHMENT_INDICATOR_FIELD_MAP } from '../../../cti/constants'; -import { Inspect } from '../../common'; +import { Inspect, Maybe, TimerangeInput } from '../../common'; import { RequestBasicOptions } from '..'; export enum CtiQueries { eventEnrichment = 'eventEnrichment', + dataSource = 'dataSource', } export interface CtiEventEnrichmentRequestOptions extends RequestBasicOptions { @@ -26,7 +29,7 @@ export interface CtiEnrichmentIdentifiers { field: string | undefined; value: string | undefined; type: string | undefined; - provider: string | undefined; + feedName: string | undefined; } export interface CtiEventEnrichmentStrategyResponse extends IEsSearchResponse { @@ -40,3 +43,33 @@ export const validEventFields = Object.keys(EVENT_ENRICHMENT_INDICATOR_FIELD_MAP export const isValidEventField = (field: string): field is EventField => validEventFields.includes(field as EventField); + +export interface CtiDataSourceRequestOptions extends IEsSearchRequest { + defaultIndex: string[]; + factoryQueryType?: FactoryQueryTypes; + timerange?: TimerangeInput; +} + +export interface BucketItem { + key: string; + doc_count: number; +} +export interface Bucket { + buckets: Array; +} + +export type DatasetBucket = { + name?: Bucket; + dashboard?: Bucket; +} & BucketItem; + +export interface CtiDataSourceStrategyResponse extends Omit { + inspect?: Maybe; + rawResponse: { + aggregations?: Record & { + dataset?: { + buckets: DatasetBucket[]; + }; + }; + }; +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/index.ts index d48172bebee4c..2acbce2c88653 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/index.ts @@ -9,6 +9,7 @@ export * from './authentications'; export * from './common'; export * from './hosts'; export * from './unique_ips'; +export * from './risky_hosts'; import { HostsKpiAuthenticationsStrategyResponse } from './authentications'; import { HostsKpiHostsStrategyResponse } from './hosts'; @@ -20,6 +21,7 @@ export enum HostsKpiQueries { kpiHosts = 'hostsKpiHosts', kpiHostsEntities = 'hostsKpiHostsEntities', kpiUniqueIps = 'hostsKpiUniqueIps', + kpiRiskyHosts = 'hostsKpiRiskyHosts', kpiUniqueIpsEntities = 'hostsKpiUniqueIpsEntities', } diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/risky_hosts/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/risky_hosts/index.ts new file mode 100644 index 0000000000000..5216052b1a6b1 --- /dev/null +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/kpi/risky_hosts/index.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import type { Inspect, Maybe } from '../../../../common'; +import type { RequestBasicOptions } from '../../..'; + +export type HostsKpiRiskyHostsRequestOptions = RequestBasicOptions; + +export interface HostsKpiRiskyHostsStrategyResponse extends IEsSearchResponse { + inspect?: Maybe; + riskyHosts: { + [key in HostRiskSeverity]: number; + }; +} + +export enum HostRiskSeverity { + unknown = 'Unknown', + low = 'Low', + moderate = 'Moderate', + high = 'High', + critical = 'Critical', +} diff --git a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts index f931694a4e229..23cda0b68f038 100644 --- a/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts +++ b/x-pack/plugins/security_solution/common/search_strategy/security_solution/hosts/risk_score/index.ts @@ -27,6 +27,14 @@ export interface HostsRiskScore { host: { name: string; }; - risk_score: number; risk: string; + risk_stats: { + rule_risks: RuleRisk[]; + risk_score: number; + }; +} + +export interface RuleRisk { + rule_name: string; + rule_risk: string; } 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 00cbdb941c11b..13a6eca3117c8 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 @@ -72,6 +72,8 @@ import { CtiEventEnrichmentRequestOptions, CtiEventEnrichmentStrategyResponse, CtiQueries, + CtiDataSourceRequestOptions, + CtiDataSourceStrategyResponse, } from './cti'; import { HostRulesRequestOptions, @@ -84,7 +86,12 @@ import { UserRulesRequestOptions, UserRulesStrategyResponse, } from './ueba'; +import { + HostsKpiRiskyHostsRequestOptions, + HostsKpiRiskyHostsStrategyResponse, +} from './hosts/kpi/risky_hosts'; +export * from './cti'; export * from './hosts'; export * from './matrix_histogram'; export * from './network'; @@ -146,6 +153,8 @@ export type StrategyResponseType = T extends HostsQ ? HostsKpiAuthenticationsStrategyResponse : T extends HostsKpiQueries.kpiHosts ? HostsKpiHostsStrategyResponse + : T extends HostsKpiQueries.kpiRiskyHosts + ? HostsKpiRiskyHostsStrategyResponse : T extends HostsKpiQueries.kpiUniqueIps ? HostsKpiUniqueIpsStrategyResponse : T extends NetworkQueries.details @@ -178,6 +187,8 @@ export type StrategyResponseType = T extends HostsQ ? MatrixHistogramStrategyResponse : T extends CtiQueries.eventEnrichment ? CtiEventEnrichmentStrategyResponse + : T extends CtiQueries.dataSource + ? CtiDataSourceStrategyResponse : never; export type StrategyRequestType = T extends HostsQueries.hosts @@ -200,6 +211,8 @@ export type StrategyRequestType = T extends HostsQu ? HostsKpiHostsRequestOptions : T extends HostsKpiQueries.kpiUniqueIps ? HostsKpiUniqueIpsRequestOptions + : T extends HostsKpiQueries.kpiRiskyHosts + ? HostsKpiRiskyHostsRequestOptions : T extends NetworkQueries.details ? NetworkDetailsRequestOptions : T extends NetworkQueries.dns @@ -238,6 +251,8 @@ export type StrategyRequestType = T extends HostsQu ? MatrixHistogramRequestOptions : T extends CtiQueries.eventEnrichment ? CtiEventEnrichmentRequestOptions + : T extends CtiQueries.dataSource + ? CtiDataSourceRequestOptions : never; export interface DocValueFieldsInput { diff --git a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts index c9c2ff2159333..0e87378f4ef96 100644 --- a/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/detection_alerts/cti_enrichments.spec.ts @@ -75,6 +75,7 @@ describe('CTI Enrichment', () => { it('Displays persisted enrichments on the JSON view', () => { const expectedEnrichment = [ { + feed: {}, indicator: { first_seen: '2021-03-10T08:02:14.000Z', file: { @@ -176,7 +177,7 @@ describe('CTI Enrichment', () => { const investigationTimeEnrichment = { field: 'source.ip', value: '192.168.1.1', - provider: 'another_provider', + feedName: 'feed_name', }; expandFirstAlert(); @@ -194,7 +195,7 @@ describe('CTI Enrichment', () => { .should('exist') .should( 'have.text', - `${investigationTimeEnrichment.field} ${investigationTimeEnrichment.value} from ${investigationTimeEnrichment.provider}` + `${investigationTimeEnrichment.field} ${investigationTimeEnrichment.value} from ${investigationTimeEnrichment.feedName}` ); }); }); diff --git a/x-pack/plugins/security_solution/cypress/integration/hosts/risky_hosts_kpi.spec.ts b/x-pack/plugins/security_solution/cypress/integration/hosts/risky_hosts_kpi.spec.ts new file mode 100644 index 0000000000000..4f282e1e69d5c --- /dev/null +++ b/x-pack/plugins/security_solution/cypress/integration/hosts/risky_hosts_kpi.spec.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { loginAndWaitForPage } from '../../tasks/login'; + +import { HOSTS_URL } from '../../urls/navigation'; +import { cleanKibana } from '../../tasks/common'; + +describe('RiskyHosts KPI', () => { + before(() => { + cleanKibana(); + }); + + it('it renders', () => { + loginAndWaitForPage(HOSTS_URL); + + cy.get('[data-test-subj="riskyHostsTotal"]').should('have.text', '0 Risky Hosts'); + cy.get('[data-test-subj="riskyHostsCriticalQuantity"]').should('have.text', '0 hosts'); + cy.get('[data-test-subj="riskyHostsHighQuantity"]').should('have.text', '0 hosts'); + }); +}); diff --git a/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts b/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts index 095401ff31422..75ff13b66b29c 100644 --- a/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts +++ b/x-pack/plugins/security_solution/cypress/integration/overview/cti_link_panel.spec.ts @@ -10,9 +10,8 @@ import { OVERVIEW_CTI_LINKS, OVERVIEW_CTI_LINKS_ERROR_INNER_PANEL, OVERVIEW_CTI_LINKS_INFO_INNER_PANEL, - OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL, OVERVIEW_CTI_TOTAL_EVENT_COUNT, - OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON, + OVERVIEW_CTI_ENABLE_INTEGRATIONS_BUTTON, } from '../../screens/overview'; import { loginAndWaitForPage } from '../../tasks/login'; @@ -28,12 +27,11 @@ describe('CTI Link Panel', () => { it('renders disabled threat intel module as expected', () => { loginAndWaitForPage(OVERVIEW_URL); cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_ERROR_INNER_PANEL}`).should('exist'); - cy.get(`${OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON}`).should('be.disabled'); cy.get(`${OVERVIEW_CTI_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 0 indicators'); cy.get(`${OVERVIEW_CTI_ENABLE_MODULE_BUTTON}`).should('exist'); cy.get(`${OVERVIEW_CTI_ENABLE_MODULE_BUTTON}`) .should('have.attr', 'href') - .and('match', /filebeat-module-threatintel.html/); + .and('match', /app\/integrations\/browse\?q=threat%20intelligence/); }); describe('enabled threat intel module', () => { @@ -49,17 +47,16 @@ describe('CTI Link Panel', () => { loginAndWaitForPage( `${OVERVIEW_URL}?sourcerer=(timerange:(from:%272021-07-08T04:00:00.000Z%27,kind:absolute,to:%272021-07-09T03:59:59.999Z%27))` ); - cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL}`).should('exist'); cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_INFO_INNER_PANEL}`).should('exist'); - cy.get(`${OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON}`).should('be.disabled'); cy.get(`${OVERVIEW_CTI_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 0 indicators'); }); it('renders dashboard module as expected when there are events in the selected time period', () => { loginAndWaitForPage(OVERVIEW_URL); - cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL}`).should('not.exist'); cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_LINKS_INFO_INNER_PANEL}`).should('exist'); - cy.get(`${OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON}`).should('be.disabled'); + cy.get(`${OVERVIEW_CTI_LINKS} ${OVERVIEW_CTI_ENABLE_INTEGRATIONS_BUTTON}`).should('exist'); + cy.get(OVERVIEW_CTI_LINKS).should('not.contain.text', 'Anomali'); + cy.get(OVERVIEW_CTI_LINKS).should('contain.text', 'AbuseCH malware'); cy.get(`${OVERVIEW_CTI_TOTAL_EVENT_COUNT}`).should('have.text', 'Showing: 1 indicator'); }); }); diff --git a/x-pack/plugins/security_solution/cypress/screens/inspect.ts b/x-pack/plugins/security_solution/cypress/screens/inspect.ts index ee9a3ad8dbbc6..f2b332b1772b2 100644 --- a/x-pack/plugins/security_solution/cypress/screens/inspect.ts +++ b/x-pack/plugins/security_solution/cypress/screens/inspect.ts @@ -20,10 +20,6 @@ export const INSPECT_HOSTS_BUTTONS_IN_SECURITY: InspectButtonMetadata[] = [ id: '[data-test-subj="stat-hosts"]', title: 'Hosts Stat', }, - { - id: '[data-test-subj="stat-authentication"]', - title: 'User Authentications Stat', - }, { id: '[data-test-subj="stat-uniqueIps"]', title: 'Unique IPs Stat', diff --git a/x-pack/plugins/security_solution/cypress/screens/overview.ts b/x-pack/plugins/security_solution/cypress/screens/overview.ts index 1945b7e3ce3e7..bc335ff6680ee 100644 --- a/x-pack/plugins/security_solution/cypress/screens/overview.ts +++ b/x-pack/plugins/security_solution/cypress/screens/overview.ts @@ -150,9 +150,9 @@ export const OVERVIEW_REVENT_TIMELINES = '[data-test-subj="overview-recent-timel export const OVERVIEW_CTI_LINKS = '[data-test-subj="cti-dashboard-links"]'; export const OVERVIEW_CTI_LINKS_ERROR_INNER_PANEL = '[data-test-subj="cti-inner-panel-danger"]'; -export const OVERVIEW_CTI_LINKS_WARNING_INNER_PANEL = '[data-test-subj="cti-inner-panel-warning"]'; export const OVERVIEW_CTI_LINKS_INFO_INNER_PANEL = '[data-test-subj="cti-inner-panel-info"]'; -export const OVERVIEW_CTI_VIEW_DASHBOARD_BUTTON = '[data-test-subj="cti-view-dashboard-button"]'; +export const OVERVIEW_CTI_ENABLE_INTEGRATIONS_BUTTON = + '[data-test-subj="cti-enable-integrations-button"]'; export const OVERVIEW_CTI_TOTAL_EVENT_COUNT = `${OVERVIEW_CTI_LINKS} [data-test-subj="header-panel-subtitle"]`; export const OVERVIEW_CTI_ENABLE_MODULE_BUTTON = '[data-test-subj="cti-enable-module-button"]'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_accordion_group.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_accordion_group.tsx index c17203de44c76..6531d9ae2823f 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_accordion_group.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_accordion_group.tsx @@ -87,7 +87,7 @@ const EnrichmentAccordion: React.FC<{ const { id = `threat-details-item`, field, - provider, + feedName, type, value, } = getEnrichmentIdentifiers(enrichment); @@ -98,7 +98,7 @@ const EnrichmentAccordion: React.FC<{ key={accordionId} initialIsOpen={true} arrowDisplay="right" - buttonContent={} + buttonContent={} extraAction={ isInvestigationTimeEnrichment(type) && ( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_button_content.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_button_content.test.tsx index d0793f1a3a124..2be7bdf76fcbf 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_button_content.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_button_content.test.tsx @@ -10,16 +10,16 @@ import { mount } from 'enzyme'; import { EnrichmentButtonContent } from './enrichment_button_content'; describe('EnrichmentButtonContent', () => { - it('renders string with provider if provider is present', () => { + it('renders string with feedName if feedName is present', () => { const wrapper = mount( - + ); expect(wrapper.find('[data-test-subj="enrichment-button-content"]').hostNodes().text()).toEqual( 'source.ip 127.0.0.1 from eceintel' ); }); - it('renders string without provider if provider is not present', () => { + it('renders string without feedName if feedName is not present', () => { const wrapper = mount(); expect(wrapper.find('[data-test-subj="enrichment-button-content"]').hostNodes().text()).toEqual( 'source.ip 127.0.0.1' diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_button_content.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_button_content.tsx index fb825b01a2f63..b03e2f4a2d21a 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_button_content.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_button_content.tsx @@ -22,10 +22,10 @@ const OverflowContainer = styled.div` export const EnrichmentButtonContent: React.FC<{ field?: string; - provider?: string; + feedName?: string; value?: string; -}> = ({ field = '', provider = '', value = '' }) => { - const title = `${field} ${value}${provider ? ` ${i18n.PROVIDER_PREPOSITION} ${provider}` : ''}`; +}> = ({ field = '', feedName = '', value = '' }) => { + const title = `${field} ${value}${feedName ? ` ${i18n.FEED_NAME_PREPOSITION} ${feedName}` : ''}`; return ( diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx index c2254c18f4364..0a16b9a612d43 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/enrichment_summary.tsx @@ -29,13 +29,13 @@ export interface ThreatSummaryDescription { data: FieldsData | undefined; eventId: string; index: number; - provider: string | undefined; + feedName: string | undefined; timelineId: string; value: string | undefined; isDraggable?: boolean; } -const EnrichmentFieldProvider = styled.span` +const EnrichmentFieldFeedName = styled.span` margin-left: ${({ theme }) => theme.eui.paddingSizes.xs}; white-space: nowrap; font-style: italic; @@ -46,13 +46,13 @@ const EnrichmentDescription: React.FC = ({ data, eventId, index, - provider, + feedName, timelineId, value, isDraggable, }) => { if (!data || !value) return null; - const key = `alert-details-value-formatted-field-value-${timelineId}-${eventId}-${data.field}-${value}-${index}-${provider}`; + const key = `alert-details-value-formatted-field-value-${timelineId}-${eventId}-${data.field}-${value}-${index}-${feedName}`; return ( @@ -67,10 +67,10 @@ const EnrichmentDescription: React.FC = ({ isObjectArray={data.isObjectArray} value={value} /> - {provider && ( - - {i18n.PROVIDER_PREPOSITION} {provider} - + {feedName && ( + + {i18n.FEED_NAME_PREPOSITION} {feedName} + )}
@@ -99,7 +99,7 @@ const EnrichmentSummaryComponent: React.FC<{ isDraggable?: boolean; }> = ({ browserFields, data, enrichments, timelineId, eventId, isDraggable }) => { const parsedEnrichments = enrichments.map((enrichment, index) => { - const { field, type, provider, value } = getEnrichmentIdentifiers(enrichment); + const { field, type, feedName, value } = getEnrichmentIdentifiers(enrichment); const eventData = data.find((item) => item.field === field); const category = eventData?.category ?? ''; const browserField = get([category, 'fields', field ?? ''], browserFields); @@ -114,7 +114,7 @@ const EnrichmentSummaryComponent: React.FC<{ return { fieldsData, type, - provider, + feedName, index, field, browserField, @@ -136,7 +136,7 @@ const EnrichmentSummaryComponent: React.FC<{ toolTipContent={i18n.INDICATOR_TOOLTIP_CONTENT} /> - {indicator.map(({ fieldsData, index, field, provider, browserField, value }) => ( + {indicator.map(({ fieldsData, index, field, feedName, browserField, value }) => ( - {investigation.map(({ fieldsData, index, field, provider, browserField, value }) => ( + {investigation.map(({ fieldsData, index, field, feedName, browserField, value }) => ( { @@ -356,3 +357,23 @@ describe('getEnrichmentFields', () => { }); }); }); + +describe('getEnrichmentIdentifiers', () => { + it(`return feed name as feedName if it's present in enrichment`, () => { + expect( + getEnrichmentIdentifiers({ + 'matched.id': [1], + 'matched.field': ['matched field'], + 'matched.atomic': ['matched atomic'], + 'matched.type': ['matched type'], + 'feed.name': ['feed name'], + }) + ).toEqual({ + id: 1, + field: 'matched field', + value: 'matched atomic', + type: 'matched type', + feedName: 'feed name', + }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx index 30409c48d07d7..812e4317e9e43 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/helpers.tsx @@ -14,7 +14,7 @@ import { MATCHED_FIELD, MATCHED_ID, MATCHED_TYPE, - PROVIDER, + FEED_NAME, } from '../../../../../common/cti/constants'; import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy'; import { @@ -81,7 +81,7 @@ export const getEnrichmentIdentifiers = (enrichment: CtiEnrichment): CtiEnrichme field: getEnrichmentValue(enrichment, MATCHED_FIELD), value: getEnrichmentValue(enrichment, MATCHED_ATOMIC), type: getEnrichmentValue(enrichment, MATCHED_TYPE), - provider: getShimmedIndicatorValue(enrichment, PROVIDER), + feedName: getShimmedIndicatorValue(enrichment, FEED_NAME), }); const buildEnrichmentId = (enrichment: CtiEnrichment): string => { diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.test.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.test.tsx index 21b86fc1740b7..9d60fbc496d8d 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.test.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.test.tsx @@ -23,8 +23,11 @@ describe('HostRiskSummary', () => { host: { name: 'test-host-name', }, - risk_score: 9999, risk: riskKeyword, + risk_stats: { + risk_score: 9999, + rule_risks: [], + }, }, ], }; @@ -63,8 +66,11 @@ describe('HostRiskSummary', () => { host: { name: 'test-host-name', }, - risk_score: 9999, risk: 'test-risk', + risk_stats: { + risk_score: 9999, + rule_risks: [], + }, }, ], }; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.tsx index dd7d10014022f..8b15ed4b250b8 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/host_risk_summary.tsx @@ -10,7 +10,7 @@ import { EuiLoadingSpinner, EuiPanel, EuiSpacer, EuiLink, EuiText } from '@elast import { FormattedMessage } from '@kbn/i18n-react'; import * as i18n from './translations'; import { RISKY_HOSTS_DOC_LINK } from '../../../../overview/components/overview_risky_host_links/risky_hosts_disabled_module'; -import { HostRisk } from '../../../../overview/containers/overview_risky_host_links/use_hosts_risk_score'; +import type { HostRisk } from '../../../containers/hosts_risk/use_hosts_risk_score'; import { EnrichedDataRow, ThreatSummaryPanelHeader } from './threat_summary_view'; const HostRiskSummaryComponent: React.FC<{ diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx index c4d7902e151b4..5382fb5a9bcc1 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/threat_summary_view.tsx @@ -28,7 +28,7 @@ import { BrowserFields, TimelineEventsDetailsItem, } from '../../../../../common/search_strategy'; -import { HostRisk } from '../../../../overview/containers/overview_risky_host_links/use_hosts_risk_score'; +import { HostRisk } from '../../../containers/hosts_risk/use_hosts_risk_score'; import { HostRiskSummary } from './host_risk_summary'; import { EnrichmentSummary } from './enrichment_summary'; diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts index 14a1fde29d15a..41273372489a7 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts +++ b/x-pack/plugins/security_solution/public/common/components/event_details/cti_details/translations.ts @@ -7,8 +7,8 @@ import { i18n } from '@kbn/i18n'; -export const PROVIDER_PREPOSITION = i18n.translate( - 'xpack.securitySolution.eventDetails.ctiSummary.providerPreposition', +export const FEED_NAME_PREPOSITION = i18n.translate( + 'xpack.securitySolution.eventDetails.ctiSummary.feedNamePreposition', { defaultMessage: 'from', } diff --git a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx index a8305a635f157..0fe48d5a998ea 100644 --- a/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx +++ b/x-pack/plugins/security_solution/public/common/components/event_details/event_details.tsx @@ -39,7 +39,7 @@ import { EnrichmentRangePicker } from './cti_details/enrichment_range_picker'; import { Reason } from './reason'; import { InvestigationGuideView } from './investigation_guide_view'; -import { HostRisk } from '../../../overview/containers/overview_risky_host_links/use_hosts_risk_score'; +import { HostRisk } from '../../containers/hosts_risk/use_hosts_risk_score'; type EventViewTab = EuiTabbedContentTab; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score.ts b/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score.ts similarity index 88% rename from x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score.ts rename to x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score.ts index eb363f4f77067..41fcd29191da2 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score.ts +++ b/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score.ts @@ -9,13 +9,13 @@ import { i18n } from '@kbn/i18n'; import { useCallback, useEffect, useState } from 'react'; import { useDispatch } from 'react-redux'; -import { useAppToasts } from '../../../common/hooks/use_app_toasts'; -import { useKibana } from '../../../common/lib/kibana'; -import { inputsActions } from '../../../common/store/actions'; -import { isIndexNotFoundError } from '../../../common/utils/exceptions'; +import { useAppToasts } from '../../hooks/use_app_toasts'; +import { useKibana } from '../../lib/kibana'; +import { inputsActions } from '../../store/actions'; +import { isIndexNotFoundError } from '../../utils/exceptions'; import { HostsRiskScore } from '../../../../common/search_strategy'; import { useHostsRiskScoreComplete } from './use_hosts_risk_score_complete'; -import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; +import { useIsExperimentalFeatureEnabled } from '../../hooks/use_experimental_features'; import { getHostRiskIndex } from '../../../helpers'; export const QUERY_ID = 'host_risk_score'; @@ -24,11 +24,11 @@ const noop = () => {}; const isRecord = (item: unknown): item is Record => typeof item === 'object' && !!item; -const isHostsRiskScoreHit = (item: unknown): item is HostsRiskScore => +const isHostsRiskScoreHit = (item: Partial): item is HostsRiskScore => isRecord(item) && isRecord(item.host) && typeof item.host.name === 'string' && - typeof item.risk_score === 'number' && + typeof item.risk_stats?.risk_score === 'number' && typeof item.risk === 'string'; export interface HostRisk { diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score_complete.ts b/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score_complete.ts similarity index 87% rename from x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score_complete.ts rename to x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score_complete.ts index 959fb94c5bbd7..934cb88ee0d86 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_risky_host_links/use_hosts_risk_score_complete.ts +++ b/x-pack/plugins/security_solution/public/common/containers/hosts_risk/use_hosts_risk_score_complete.ts @@ -4,14 +4,15 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Observable } from 'rxjs'; +import type { Observable } from 'rxjs'; import { filter } from 'rxjs/operators'; import { useObservable, withOptionalSignal } from '@kbn/securitysolution-hook-utils'; -import type { DataPublicPluginStart } from '../../../../../../../src/plugins/data/public'; - -import { isCompleteResponse, isErrorResponse } from '../../../../../../../src/plugins/data/common'; - +import { + DataPublicPluginStart, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/public'; import { HostsQueries, HostsRiskScoreRequestOptions, diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_error_toast.test.ts b/x-pack/plugins/security_solution/public/common/hooks/use_error_toast.test.ts new file mode 100644 index 0000000000000..993326d906a18 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_error_toast.test.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { renderHook } from '@testing-library/react-hooks'; +import { useErrorToast } from './use_error_toast'; + +jest.mock('./use_app_toasts'); + +import { useAppToasts } from './use_app_toasts'; + +describe('useErrorToast', () => { + let addErrorMock: jest.Mock; + + beforeEach(() => { + addErrorMock = jest.fn(); + (useAppToasts as jest.Mock).mockImplementation(() => ({ + addError: addErrorMock, + })); + }); + + it('calls useAppToasts error when an error param is provided', () => { + const title = 'testErrorTitle'; + const error = new Error(); + renderHook(() => useErrorToast(title, error)); + + expect(addErrorMock).toHaveBeenCalledWith(error, { title }); + }); + + it("doesn't call useAppToasts error when an error param is undefined", () => { + const title = 'testErrorTitle'; + const error = undefined; + renderHook(() => useErrorToast(title, error)); + + expect(addErrorMock).not.toHaveBeenCalled(); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_error_toast.ts b/x-pack/plugins/security_solution/public/common/hooks/use_error_toast.ts new file mode 100644 index 0000000000000..f459827f6cc9a --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_error_toast.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect } from 'react'; +import { useAppToasts } from './use_app_toasts'; + +/** + * Display App error toast when error is defined. + */ +export const useErrorToast = (title: string, error: unknown) => { + const { addError } = useAppToasts(); + + useEffect(() => { + if (error) { + addError(error, { title }); + } + }, [error, title, addError]); +}; diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_inspect_query.test.tsx b/x-pack/plugins/security_solution/public/common/hooks/use_inspect_query.test.tsx new file mode 100644 index 0000000000000..1bf2de3242ac7 --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_inspect_query.test.tsx @@ -0,0 +1,85 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { renderHook } from '@testing-library/react-hooks'; +import { useInspectQuery } from './use_inspect_query'; + +import { useGlobalTime } from '../containers/use_global_time'; + +jest.mock('../containers/use_global_time'); + +const QUERY_ID = 'tes_query_id'; + +const RESPONSE = { + inspect: { dsl: [], response: [] }, + isPartial: false, + isRunning: false, + total: 0, + loaded: 0, + rawResponse: { + took: 0, + timed_out: false, + _shards: { + total: 0, + successful: 0, + failed: 0, + skipped: 0, + }, + results: { + hits: { + total: 0, + }, + }, + hits: { + total: 0, + max_score: 0, + hits: [], + }, + }, + totalCount: 0, + enrichments: [], +}; + +describe('useInspectQuery', () => { + let deleteQuery: jest.Mock; + let setQuery: jest.Mock; + + beforeEach(() => { + deleteQuery = jest.fn(); + setQuery = jest.fn(); + (useGlobalTime as jest.Mock).mockImplementation(() => ({ + deleteQuery, + setQuery, + isInitializing: false, + })); + }); + + it('it calls setQuery', () => { + renderHook(() => useInspectQuery(QUERY_ID, false, RESPONSE)); + + expect(setQuery).toHaveBeenCalledTimes(1); + expect(setQuery.mock.calls[0][0].id).toBe(QUERY_ID); + }); + + it("doesn't call setQuery when response is undefined", () => { + renderHook(() => useInspectQuery(QUERY_ID, false, undefined)); + + expect(setQuery).not.toHaveBeenCalled(); + }); + + it("doesn't call setQuery when loading", () => { + renderHook(() => useInspectQuery(QUERY_ID, true)); + + expect(setQuery).not.toHaveBeenCalled(); + }); + + it('calls deleteQuery when unmouting', () => { + const result = renderHook(() => useInspectQuery(QUERY_ID, false, RESPONSE)); + result.unmount(); + + expect(deleteQuery).toHaveBeenCalledWith({ id: QUERY_ID }); + }); +}); diff --git a/x-pack/plugins/security_solution/public/common/hooks/use_inspect_query.ts b/x-pack/plugins/security_solution/public/common/hooks/use_inspect_query.ts new file mode 100644 index 0000000000000..4c0cb1c4fcdca --- /dev/null +++ b/x-pack/plugins/security_solution/public/common/hooks/use_inspect_query.ts @@ -0,0 +1,44 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { noop } from 'lodash'; +import { useEffect } from 'react'; +import type { FactoryQueryTypes, StrategyResponseType } from '../../../common/search_strategy'; +import { getInspectResponse } from '../../helpers'; +import { useGlobalTime } from '../containers/use_global_time'; +import type { Refetch, RefetchKql } from '../store/inputs/model'; + +/** + * Add and remove query response from global input store. + */ +export const useInspectQuery = ( + id: string, + loading: boolean, + response?: StrategyResponseType, + refetch: Refetch | RefetchKql = noop +) => { + const { deleteQuery, setQuery, isInitializing } = useGlobalTime(); + + useEffect(() => { + if (!loading && !isInitializing && response?.inspect) { + setQuery({ + id, + inspect: getInspectResponse(response, { + dsl: [], + response: [], + }), + loading, + refetch, + }); + } + + return () => { + if (deleteQuery) { + deleteQuery({ id }); + } + }; + }, [deleteQuery, setQuery, loading, response, isInitializing, id, refetch]); +}; diff --git a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts index 155588d1681cd..ea9b165d0d0f7 100644 --- a/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts +++ b/x-pack/plugins/security_solution/public/common/mock/mock_timeline_data.ts @@ -1106,6 +1106,9 @@ export const mockTimelineData: TimelineItem[] = [ field: ['source.ip'], type: ['ip'], }, + feed: { + name: ['feed_name'], + }, }, ], }, diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx index a1684d6564a0a..b8a2c767961d8 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.test.tsx @@ -86,7 +86,12 @@ describe('InvestigateInResolverAction', () => { }); test('it does NOT render AddToCase context menu item when timelineId is not in the allowed list', () => { - const wrapper = mount(, { + // In order to enable alert context menu without a timelineId, event needs to be event.kind === 'event' and agent.type === 'endpoint' + const customProps = { + ...props, + ecsRowData: { ...ecsRowData, agent: { type: ['endpoint'] }, event: { kind: ['event'] } }, + }; + const wrapper = mount(, { wrappingComponent: TestProviders, }); wrapper.find(actionMenuButton).simulate('click'); diff --git a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx index c95006866194b..3883373de97a3 100644 --- a/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/alerts_table/timeline_actions/alert_context_menu.tsx @@ -164,6 +164,7 @@ const AlertContextMenuComponent: React.FC diff --git a/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx b/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx index 1e1a5f1c35bc4..835ab73282f1a 100644 --- a/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/rules/severity_badge/index.tsx @@ -7,22 +7,25 @@ import { upperFirst } from 'lodash/fp'; import React from 'react'; - import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import type { Severity } from '@kbn/securitysolution-io-ts-alerting-types'; import { HealthTruncateText } from '../../../../common/components/health_truncate_text'; + +const { euiColorVis0, euiColorVis5, euiColorVis7, euiColorVis9 } = euiLightVars; +const severityToColorMap: Record = { + low: euiColorVis0, + medium: euiColorVis5, + high: euiColorVis7, + critical: euiColorVis9, +}; + interface Props { - value: string; + value: Severity; } const SeverityBadgeComponent: React.FC = ({ value }) => { const displayValue = upperFirst(value); - const color = 'low' - ? euiLightVars.euiColorVis0 - : value === 'medium' - ? euiLightVars.euiColorVis5 - : value === 'high' - ? euiLightVars.euiColorVis7 - : euiLightVars.euiColorVis9; + const color = severityToColorMap[value] ?? 'subdued'; return ( diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx index ab9d4a6344aec..bf990cb292cd7 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.test.tsx @@ -228,7 +228,7 @@ describe('take action dropdown', () => { }); }); - test('should disable the "Add Endpoint event filter" button if provided non-event or non-endpoint', async () => { + test('should hide the "Add Endpoint event filter" button if provided no event from endpoint', async () => { wrapper = mount( { ); wrapper.find('button[data-test-subj="take-action-dropdown-btn"]').simulate('click'); await waitFor(() => { - expect( - wrapper.find('[data-test-subj="add-event-filter-menu-item"]').first().getDOMNode() - ).toBeDisabled(); + expect(wrapper.exists('[data-test-subj="add-event-filter-menu-item"]')).toBeFalsy(); }); }); }); diff --git a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx index 3a388104d926e..d04f6c5d7d510 100644 --- a/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx +++ b/x-pack/plugins/security_solution/public/detections/components/take_action_dropdown/index.tsx @@ -91,8 +91,6 @@ export const TakeActionDropdown = React.memo( const isEndpointEvent = useMemo(() => isEvent && isAgentEndpoint, [isEvent, isAgentEndpoint]); - const disableEventFilterAction = useMemo(() => !isEndpointEvent, [isEndpointEvent]); - const togglePopoverHandler = useCallback(() => { setIsPopoverOpen(!isPopoverOpen); }, [isPopoverOpen]); @@ -141,7 +139,7 @@ export const TakeActionDropdown = React.memo( const { eventFilterActionItems } = useEventFilterAction({ onAddEventFilterClick: handleOnAddEventFilterClick, - disabled: disableEventFilterAction, + disabled: !isEndpointEvent, }); const afterCaseSelection = useCallback(() => { @@ -167,8 +165,17 @@ export const TakeActionDropdown = React.memo( () => !isEvent && actionsData.ruleId ? [...statusActionItems, ...exceptionActionItems] - : eventFilterActionItems, - [eventFilterActionItems, exceptionActionItems, statusActionItems, isEvent, actionsData.ruleId] + : isEndpointEvent + ? eventFilterActionItems + : [], + [ + eventFilterActionItems, + isEndpointEvent, + exceptionActionItems, + statusActionItems, + isEvent, + actionsData.ruleId, + ] ); const { addToCaseActionItems } = useAddToCaseActions({ 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 5f9ad7fdd2bf5..17207c2651dd6 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 @@ -14,7 +14,6 @@ import { HttpStart } from '../../../../../../../../src/core/public'; import { DETECTION_ENGINE_RULES_URL, DETECTION_ENGINE_PREPACKAGED_URL, - DETECTION_ENGINE_RULES_STATUS_URL, DETECTION_ENGINE_PREPACKAGED_RULES_STATUS_URL, DETECTION_ENGINE_TAGS_URL, DETECTION_ENGINE_RULES_BULK_ACTION, @@ -246,6 +245,9 @@ export const duplicateRules = async ({ rules }: DuplicateRulesProps): Promise => { - const res = await KibanaServices.get().http.fetch( - DETECTION_ENGINE_RULES_STATUS_URL, - { - method: 'POST', - body: JSON.stringify({ ids }), - signal, - } - ); - return res; -}; - /** * Fetch all unique Tags used by Rules * 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 fea28eba61d70..53adc93e39580 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 @@ -19,6 +19,7 @@ import { threats, type, severity_mapping, + severity, } from '@kbn/securitysolution-io-ts-alerting-types'; import { SortOrder, @@ -101,7 +102,7 @@ export const RuleSchema = t.intersection([ risk_score: t.number, risk_score_mapping, rule_id: t.string, - severity: t.string, + severity, severity_mapping, tags: t.array(t.string), type, @@ -125,6 +126,9 @@ export const RuleSchema = t.intersection([ last_failure_message: t.string, last_success_message: t.string, last_success_at: t.string, + last_gap: t.string, + bulk_create_time_durations: t.array(t.string), + search_after_time_durations: t.array(t.string), meta: MetaRule, machine_learning_job_id: t.array(t.string), output_index: t.string, diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx index a5809ea776322..73b05f94153e6 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.test.tsx @@ -6,70 +6,14 @@ */ import { renderHook, act, cleanup } from '@testing-library/react-hooks'; -import { - useRuleStatus, - ReturnRuleStatus, - useRulesStatuses, - ReturnRulesStatuses, -} from './use_rule_status'; +import { useRuleStatus, ReturnRuleStatus } from './use_rule_status'; import * as api from './api'; -import { Rule } from './types'; import { useAppToastsMock } from '../../../../common/hooks/use_app_toasts.mock'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; jest.mock('./api'); jest.mock('../../../../common/hooks/use_app_toasts'); -const testRule: Rule = { - actions: [ - { - group: 'fake group', - id: 'fake id', - action_type_id: 'fake action_type_id', - params: { - someKey: 'someVal', - }, - }, - ], - author: [], - created_at: 'mm/dd/yyyyTHH:MM:sssz', - created_by: 'mockUser', - description: 'some desc', - enabled: true, - false_positives: [], - filters: [], - from: 'now-360s', - id: '12345678987654321', - immutable: false, - index: [ - 'apm-*-transaction*', - 'traces-apm*', - 'auditbeat-*', - 'endgame-*', - 'filebeat-*', - 'packetbeat-*', - 'winlogbeat-*', - ], - interval: '5m', - language: 'kuery', - name: 'Test rule', - max_signals: 100, - query: "user.email: 'root@elastic.co'", - references: [], - risk_score: 75, - risk_score_mapping: [], - rule_id: 'bbd3106e-b4b5-4d7c-a1a2-47531d6a2baf', - severity: 'high', - severity_mapping: [], - tags: ['APM'], - threat: [], - throttle: null, - to: 'now', - type: 'query', - updated_at: 'mm/dd/yyyyTHH:MM:sssz', - updated_by: 'mockUser', -}; - describe('useRuleStatus', () => { (useAppToasts as jest.Mock).mockReturnValue(useAppToastsMock.create()); @@ -138,50 +82,4 @@ describe('useRuleStatus', () => { }); }); }); - - describe('useRulesStatuses', () => { - test('init rules statuses', async () => { - const payload = [testRule]; - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useRulesStatuses(payload) - ); - await waitForNextUpdate(); - expect(result.current).toEqual({ loading: false, rulesStatuses: [] }); - }); - }); - - test('fetch rules statuses', async () => { - const payload = [testRule]; - await act(async () => { - const { result, waitForNextUpdate } = renderHook(() => - useRulesStatuses(payload) - ); - await waitForNextUpdate(); - await waitForNextUpdate(); - expect(result.current).toEqual({ - loading: false, - rulesStatuses: [ - { - current_status: { - alert_id: 'alertId', - bulk_create_time_durations: ['2235.01'], - gap: null, - last_failure_at: null, - last_failure_message: null, - last_look_back_date: '2020-03-19T00:32:07.996Z', // NOTE: This is no longer used on the UI, but left here in case users are using it within the API - last_success_at: 'mm/dd/yyyyTHH:MM:sssz', - last_success_message: 'it is a success', - search_after_time_durations: ['616.97'], - status: 'succeeded', - status_date: 'mm/dd/yyyyTHH:MM:sssz', - }, - failures: [], - id: '12345678987654321', - }, - ], - }); - }); - }); - }); }); diff --git a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx index d0c75e08ae01b..815ed261c490f 100644 --- a/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx +++ b/x-pack/plugins/security_solution/public/detections/containers/detection_engine/rules/use_rule_status.tsx @@ -9,17 +9,12 @@ import { useEffect, useRef, useState } from 'react'; import { isNotFoundError } from '@kbn/securitysolution-t-grid'; import { useAppToasts } from '../../../../common/hooks/use_app_toasts'; -import { EnhancedRuleStatus } from '../../../pages/detection_engine/rules/all/columns'; -import { getRuleStatusById, getRulesStatusByIds } from './api'; +import { getRuleStatusById } from './api'; import * as i18n from './translations'; -import { RuleStatus, Rules } from './types'; +import { RuleStatus } from './types'; type Func = (ruleId: string) => void; export type ReturnRuleStatus = [boolean, RuleStatus | null, Func | null]; -export interface ReturnRulesStatuses { - loading: boolean; - rulesStatuses: EnhancedRuleStatus[]; -} /** * Hook for using to get a Rule from the Detection Engine API @@ -70,58 +65,3 @@ export const useRuleStatus = (id: string | undefined | null): ReturnRuleStatus = return [loading, ruleStatus, fetchRuleStatus.current]; }; - -/** - * Hook for using to get all the statuses for all given rule ids - * - * @param ids desired Rule ID's (not rule_id) - * - */ -export const useRulesStatuses = (rules: Rules): ReturnRulesStatuses => { - const [rulesStatuses, setRuleStatuses] = useState([]); - const [loading, setLoading] = useState(false); - const { addError } = useAppToasts(); - - useEffect(() => { - let isSubscribed = true; - const abortCtrl = new AbortController(); - - const fetchData = async (ids: string[]) => { - try { - setLoading(true); - const ruleStatusesResponse = await getRulesStatusByIds({ - ids, - signal: abortCtrl.signal, - }); - - if (isSubscribed) { - setRuleStatuses( - rules.map((rule) => ({ - id: rule.id, - ...ruleStatusesResponse[rule.id], - })) - ); - } - } catch (error) { - if (isSubscribed) { - setRuleStatuses([]); - addError(error, { title: i18n.RULE_AND_TIMELINE_FETCH_FAILURE }); - } - } - if (isSubscribed) { - setLoading(false); - } - }; - - if (rules.length > 0) { - fetchData(rules.map((r) => r.id)); - } - - return () => { - isSubscribed = false; - abortCtrl.abort(); - }; - }, [rules, addError]); - - return { loading, rulesStatuses }; -}; 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 d586f0e4856d4..f1771e6f44ddd 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 @@ -18,7 +18,7 @@ import { sum } from 'lodash'; import React, { Dispatch } from 'react'; import { isMlRule } from '../../../../../../common/machine_learning/helpers'; -import { Rule, RuleStatus } from '../../../../containers/detection_engine/rules'; +import { Rule } from '../../../../containers/detection_engine/rules'; import { getEmptyTagValue } from '../../../../../common/components/empty_value'; import { FormattedRelativePreferenceDate } from '../../../../../common/components/formatted_date'; import { getRuleDetailsUrl } from '../../../../../common/components/link_to/redirect_to_detection_engine'; @@ -54,10 +54,7 @@ type HasReadActionsPrivileges = [x: string]: boolean; }>; -export type TableItem = Rule & Partial; -export type TableColumn = EuiBasicTableColumn | EuiTableActionsColumnType; - -const extractRuleFromRow = ({ current_status: _, failures, ...rule }: TableItem): Rule => rule; +export type TableColumn = EuiBasicTableColumn | EuiTableActionsColumnType; export const getActions = ( dispatch: React.Dispatch, @@ -78,9 +75,8 @@ export const getActions = ( i18n.EDIT_RULE_SETTINGS ), icon: 'controlsHorizontal', - onClick: (rowItem: TableItem) => editRuleAction(rowItem.id, navigateToApp), - enabled: (rowItem: TableItem) => - canEditRuleWithActions(extractRuleFromRow(rowItem), actionsPrivileges), + onClick: (rowItem: Rule) => editRuleAction(rowItem.id, navigateToApp), + enabled: (rowItem: Rule) => canEditRuleWithActions(rowItem, actionsPrivileges), }, { 'data-test-subj': 'duplicateRuleAction', @@ -93,10 +89,10 @@ export const getActions = ( ) : ( i18n.DUPLICATE_RULE ), - enabled: (rowItem: TableItem) => canEditRuleWithActions(rowItem, actionsPrivileges), - onClick: async (rowItem: TableItem) => { + enabled: (rowItem: Rule) => canEditRuleWithActions(rowItem, actionsPrivileges), + onClick: async (rowItem: Rule) => { const createdRules = await duplicateRulesAction( - [extractRuleFromRow(rowItem)], + [rowItem], [rowItem.id], dispatch, dispatchToaster @@ -127,10 +123,6 @@ export const getActions = ( }, ]; -export type EnhancedRuleStatus = RuleStatus & { - id: string; -}; - interface GetColumnsProps { dispatch: React.Dispatch; formatUrl: FormatUrl; @@ -154,7 +146,7 @@ const getColumnEnabled = ({ }: GetColumnsProps): TableColumn => ({ field: 'enabled', name: i18n.COLUMN_ACTIVATE, - render: (_, rule: TableItem) => ( + render: (_, rule: Rule) => ( ({ field: 'name', name: i18n.COLUMN_RULE, - render: (value: Rule['name'], item: TableItem) => ( + render: (value: Rule['name'], item: Rule) => ( , + } as EuiTableActionsColumnType, ] : []; @@ -348,14 +340,14 @@ export const getMonitoringColumns = (columnsProps: GetColumnsProps): TableColumn { ...getColumnRuleName(columnsProps), width: '28%' }, getColumnTags(), { - field: 'current_status.bulk_create_time_durations', + field: 'bulk_create_time_durations', name: ( ), - render: (value: RuleStatus['current_status']['bulk_create_time_durations'] | undefined) => ( + render: (value: Rule['bulk_create_time_durations'] | undefined) => ( {value?.length ? sum(value.map(Number)).toFixed() : getEmptyTagValue()} @@ -364,14 +356,14 @@ export const getMonitoringColumns = (columnsProps: GetColumnsProps): TableColumn truncateText: true, }, { - field: 'current_status.search_after_time_durations', + field: 'search_after_time_durations', name: ( ), - render: (value: RuleStatus['current_status']['search_after_time_durations'] | undefined) => ( + render: (value: Rule['search_after_time_durations'] | undefined) => ( {value?.length ? sum(value.map(Number)).toFixed() : getEmptyTagValue()} @@ -380,7 +372,7 @@ export const getMonitoringColumns = (columnsProps: GetColumnsProps): TableColumn truncateText: true, }, { - field: 'current_status.gap', + field: 'last_gap', name: ( ), - render: (value: RuleStatus['current_status']['gap'] | undefined) => ( + render: (value: Rule['last_gap'] | undefined) => ( {value ?? getEmptyTagValue()} @@ -416,18 +408,16 @@ export const getMonitoringColumns = (columnsProps: GetColumnsProps): TableColumn truncateText: true, }, { - field: 'current_status.status', + field: 'status', name: i18n.COLUMN_LAST_RESPONSE, - render: (value: RuleStatus['current_status']['status'] | undefined) => ( - - ), + render: (value: Rule['status'] | undefined) => , width: '12%', truncateText: true, }, { - field: 'current_status.status_date', + field: 'status_date', name: i18n.COLUMN_LAST_COMPLETE_RUN, - render: (value: RuleStatus['current_status']['status_date'] | undefined) => { + render: (value: Rule['status_date'] | undefined) => { return value == null ? ( getEmptyTagValue() ) : ( diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx index 487d0862cf467..c63aa17902740 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/index.test.tsx @@ -22,11 +22,7 @@ import { DEFAULT_RULES_TABLE_REFRESH_SETTING, } from '../../../../../../common/constants'; -import { - useRulesTable, - useRulesStatuses, - RulesTableState, -} from '../../../../containers/detection_engine/rules'; +import { useRulesTable, RulesTableState } from '../../../../containers/detection_engine/rules'; import { AllRules } from './index'; @@ -147,29 +143,6 @@ describe('AllRules', () => { }; }); - (useRulesStatuses as jest.Mock).mockReturnValue({ - loading: false, - rulesStatuses: [ - { - current_status: { - alert_id: 'alertId', - bulk_create_time_durations: ['2235.01'], - gap: null, - last_failure_at: null, - last_failure_message: null, - last_look_back_date: new Date().toISOString(), // NOTE: This is no longer used on the UI, but left here in case users are using it within the API - last_success_at: new Date().toISOString(), - last_success_message: 'it is a success', - search_after_time_durations: ['616.97'], - status: 'succeeded', - status_date: new Date().toISOString(), - }, - failures: [], - id: '12345678987654321', - }, - ], - }); - useKibanaMock().services.application.capabilities = { navLinks: {}, management: {}, diff --git a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx index 54c2500d03b03..5668a4d489c53 100644 --- a/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx +++ b/x-pack/plugins/security_solution/public/detections/pages/detection_engine/rules/all/rules_tables.tsx @@ -21,10 +21,10 @@ import { History } from 'history'; import { useRulesTable, - useRulesStatuses, CreatePreBuiltRules, FilterOptions, RulesSortingFields, + Rule, } from '../../../../containers/detection_engine/rules'; import { FormatUrl } from '../../../../../common/components/link_to'; @@ -37,7 +37,7 @@ import { getPrePackagedRuleStatus } from '../helpers'; import * as i18n from '../translations'; import { EuiBasicTableOnChange } from '../types'; import { getBatchItems } from './batch_actions'; -import { getRulesColumns, getMonitoringColumns, TableItem } from './columns'; +import { getRulesColumns, getMonitoringColumns } from './columns'; import { showRulesTable } from './helpers'; import { RulesTableFilters } from './rules_table_filters/rules_table_filters'; import { useMlCapabilities } from '../../../../../common/components/ml/hooks/use_ml_capabilities'; @@ -144,7 +144,6 @@ export const RulesTables = React.memo( reFetchRules, } = rulesTable; - const { loading: isLoadingRulesStatuses, rulesStatuses } = useRulesStatuses(rules); const [, dispatchToaster] = useStateToaster(); const mlCapabilities = useMlCapabilities(); const { navigateToApp } = useKibana().services.application; @@ -305,10 +304,10 @@ export const RulesTables = React.memo( }, [reFetchRules, setRefreshRulesData]); useEffect(() => { - if (initLoading && !loading && !isLoadingRules && !isLoadingRulesStatuses) { + if (initLoading && !loading && !isLoadingRules) { setInitLoading(false); } - }, [initLoading, loading, isLoadingRules, isLoadingRulesStatuses]); + }, [initLoading, loading, isLoadingRules]); const handleCreatePrePackagedRules = useCallback(async () => { if (createPrePackagedRules != null) { @@ -329,8 +328,8 @@ export const RulesTables = React.memo( const euiBasicTableSelectionProps = useMemo( () => ({ - selectable: (item: TableItem) => !loadingRuleIds.includes(item.id), - onSelectionChange: (selected: TableItem[]) => { + selectable: (item: Rule) => !loadingRuleIds.includes(item.id), + onSelectionChange: (selected: Rule[]) => { /** * EuiBasicTable doesn't provide declarative API to control selected rows. * This limitation requires us to synchronize selection state manually using setSelection(). @@ -444,17 +443,6 @@ export const RulesTables = React.memo( [initLoading, prePackagedRuleStatus, rulesCustomInstalled] ); - const items = useMemo(() => { - const rulesStatusesMap = new Map(rulesStatuses.map((item) => [item.id, item])); - - return rules.map((rule) => { - return { - ...rule, - ...rulesStatusesMap.get(rule.id), - }; - }); - }, [rulesStatuses, rules]); - const tableProps = selectedTab === AllRulesTabs.rules ? { @@ -484,7 +472,7 @@ export const RulesTables = React.memo( growLeftSplit={false} title={i18n.ALL_RULES} subtitle={timelines.getLastUpdated({ - showUpdating: loading || isLoadingRules || isLoadingRulesStatuses, + showUpdating: loading || isLoadingRules, updatedAt: lastUpdated, })} > @@ -553,7 +541,7 @@ export const RulesTables = React.memo( /> ( ); if (loading) { - return ( - - - - - - ); + return ; } return ( @@ -80,3 +74,11 @@ export const HostsKpiBaseComponent = React.memo( HostsKpiBaseComponent.displayName = 'HostsKpiBaseComponent'; export const HostsKpiBaseComponentManage = manageQuery(HostsKpiBaseComponent); + +export const HostsKpiBaseComponentLoader: React.FC = () => ( + + + + + +); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx index 26d908cba4d0d..1f854b1328aad 100644 --- a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/index.tsx @@ -6,51 +6,96 @@ */ import React from 'react'; -import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem, EuiFlexGroup, EuiSpacer, EuiLink } from '@elastic/eui'; import { HostsKpiAuthentications } from './authentications'; import { HostsKpiHosts } from './hosts'; import { HostsKpiUniqueIps } from './unique_ips'; import { HostsKpiProps } from './types'; +import { RiskyHosts } from './risky_hosts'; +import { useIsExperimentalFeatureEnabled } from '../../../common/hooks/use_experimental_features'; +import { useRiskyHosts } from '../../containers/kpi_hosts/risky_hosts'; +import { CallOutSwitcher } from '../../../common/components/callouts'; +import { RISKY_HOSTS_DOC_LINK } from '../../../overview/components/overview_risky_host_links/risky_hosts_disabled_module'; +import * as i18n from './translations'; export const HostsKpiComponent = React.memo( - ({ filterQuery, from, indexNames, to, setQuery, skip, narrowDateRange }) => ( - - - - - - - - - - - - ) + ({ filterQuery, from, indexNames, to, setQuery, skip, narrowDateRange }) => { + const riskyHostsExperimentEnabled = useIsExperimentalFeatureEnabled('riskyHostsEnabled'); + const { + error, + response, + loading, + isModuleDisabled: isRiskHostsModuleDisabled, + } = useRiskyHosts({ + filterQuery, + from, + to, + skip: skip || !riskyHostsExperimentEnabled, + }); + + return ( + <> + {isRiskHostsModuleDisabled && ( + <> + + {i18n.LEARN_MORE}{' '} + + {i18n.HOST_RISK_DATA} + + + + ), + }} + /> + + + )} + + + + + + {riskyHostsExperimentEnabled && ( + + + + )} + + + + + + ); + } ); HostsKpiComponent.displayName = 'HostsKpiComponent'; diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.test.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.test.tsx new file mode 100644 index 0000000000000..f0e3dcfb69c6e --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.test.tsx @@ -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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; + +import { render } from '@testing-library/react'; + +import { RiskyHosts } from './'; +import { TestProviders } from '../../../../common/mock'; +import { HostsKpiRiskyHostsStrategyResponse } from '../../../../../common/search_strategy'; + +jest.mock('../../../containers/kpi_hosts/risky_hosts'); + +describe('RiskyHosts', () => { + const defaultProps = { + error: undefined, + loading: false, + }; + + test('it renders', () => { + const { queryByText } = render( + + + + ); + + expect(queryByText('Risky Hosts')).toBeInTheDocument(); + }); + + test('it displays loader while API is loading', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('hostsKpiLoader')).toBeInTheDocument(); + }); + + test('it displays 0 risky hosts when initializing', () => { + const { getByTestId } = render( + + + + ); + + expect(getByTestId('riskyHostsTotal').textContent).toEqual('0 Risky Hosts'); + expect(getByTestId('riskyHostsCriticalQuantity').textContent).toEqual('0 hosts'); + expect(getByTestId('riskyHostsHighQuantity').textContent).toEqual('0 hosts'); + }); + + test('it displays risky hosts quantity returned by the API', () => { + const data: HostsKpiRiskyHostsStrategyResponse = { + rawResponse: {} as HostsKpiRiskyHostsStrategyResponse['rawResponse'], + riskyHosts: { + Critical: 1, + High: 1, + Unknown: 0, + Low: 0, + Moderate: 0, + }, + }; + const { getByTestId } = render( + + + + ); + + expect(getByTestId('riskyHostsTotal').textContent).toEqual('2 Risky Hosts'); + expect(getByTestId('riskyHostsCriticalQuantity').textContent).toEqual('1 host'); + expect(getByTestId('riskyHostsHighQuantity').textContent).toEqual('1 host'); + }); +}); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx new file mode 100644 index 0000000000000..1030ea4c5e65b --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/index.tsx @@ -0,0 +1,154 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + EuiFlexGroup, + EuiFlexItem, + EuiHealth, + EuiHorizontalRule, + EuiIcon, + EuiPanel, + EuiTitle, + EuiText, + transparentize, +} from '@elastic/eui'; +import React from 'react'; +import styled, { css } from 'styled-components'; +import { euiLightVars } from '@kbn/ui-shared-deps-src/theme'; +import { InspectButtonContainer, InspectButton } from '../../../../common/components/inspect'; + +import { HostsKpiBaseComponentLoader } from '../common'; +import * as i18n from './translations'; + +import { + HostRiskSeverity, + HostsKpiRiskyHostsStrategyResponse, +} from '../../../../../common/search_strategy/security_solution/hosts/kpi/risky_hosts'; + +import { useInspectQuery } from '../../../../common/hooks/use_inspect_query'; +import { useErrorToast } from '../../../../common/hooks/use_error_toast'; + +const QUERY_ID = 'hostsKpiRiskyHostsQuery'; + +const HOST_RISK_SEVERITY_COLOUR = { + Unknown: euiLightVars.euiColorMediumShade, + Low: euiLightVars.euiColorVis0, + Moderate: euiLightVars.euiColorWarning, + High: euiLightVars.euiColorVis9_behindText, + Critical: euiLightVars.euiColorDanger, +}; + +const HostRiskBadge = styled.div<{ $severity: HostRiskSeverity }>` + ${({ theme, $severity }) => css` + width: fit-content; + padding-right: ${theme.eui.paddingSizes.s}; + padding-left: ${theme.eui.paddingSizes.xs}; + + ${($severity === 'Critical' || $severity === 'High') && + css` + background-color: ${transparentize(theme.eui.euiColorDanger, 0.2)}; + border-radius: 999px; // pill shaped + `}; + `} +`; + +const HostRisk: React.FC<{ severity: HostRiskSeverity }> = ({ severity }) => ( + + {severity} + +); + +const HostCount = styled(EuiText)` + font-weight: bold; +`; +HostCount.displayName = 'HostCount'; + +const StatusTitle = styled(EuiTitle)` + text-transform: lowercase; +`; + +const RiskScoreContainer = styled(EuiFlexItem)` + min-width: 80px; +`; + +const RiskyHostsComponent: React.FC<{ + error: unknown; + loading: boolean; + data?: HostsKpiRiskyHostsStrategyResponse; +}> = ({ error, loading, data }) => { + useInspectQuery(QUERY_ID, loading, data); + useErrorToast(i18n.ERROR_TITLE, error); + + if (loading) { + return ; + } + + const criticalRiskCount = data?.riskyHosts.Critical ?? 0; + const hightlRiskCount = data?.riskyHosts.High ?? 0; + + const totalCount = criticalRiskCount + hightlRiskCount; + + return ( + + + + + +
{i18n.RISKY_HOSTS_TITLE}
+
+
+ + {data?.inspect && } + +
+ + + + + + + + +

{i18n.RISKY_HOSTS_DESCRIPTION(totalCount, totalCount.toLocaleString())}

+
+
+
+
+
+ + + + + + + + + + {i18n.HOSTS_COUNT(criticalRiskCount)} + + + + + + + + + + + + {i18n.HOSTS_COUNT(hightlRiskCount)} + + + + + +
+
+ ); +}; + +export const RiskyHosts = React.memo(RiskyHostsComponent); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/translations.ts b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/translations.ts new file mode 100644 index 0000000000000..f97dc80fd9679 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/risky_hosts/translations.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const HOSTS_COUNT = (quantity: number) => + i18n.translate('xpack.securitySolution.kpiHosts.riskyHosts.hostsCount', { + defaultMessage: '{quantity} {quantity, plural, =1 {host} other {hosts}}', + values: { + quantity, + }, + }); + +export const RISKY_HOSTS_DESCRIPTION = (quantity: number, formattedQuantity: string) => + i18n.translate('xpack.securitySolution.kpiHosts.riskyHosts.description', { + defaultMessage: '{formattedQuantity} Risky {quantity, plural, =1 {Host} other {Hosts}}', + values: { + formattedQuantity, + quantity, + }, + }); + +export const RISKY_HOSTS_TITLE = i18n.translate( + 'xpack.securitySolution.kpiHosts.riskyHosts.title', + { + defaultMessage: 'Risky Hosts', + } +); + +export const INSPECT_RISKY_HOSTS = i18n.translate( + 'xpack.securitySolution.kpiHosts.riskyHosts.inspectTitle', + { + defaultMessage: 'KPI Risky Hosts', + } +); + +export const ERROR_TITLE = i18n.translate( + 'xpack.securitySolution.kpiHosts.riskyHosts.errorMessage', + { + defaultMessage: 'Error Fetching Risky Hosts API', + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/translations.ts b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/translations.ts new file mode 100644 index 0000000000000..cc706ed6e68e8 --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/components/kpi_hosts/translations.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { i18n } from '@kbn/i18n'; + +export const LEARN_MORE = i18n.translate('xpack.securitySolution.kpiHost.learnMore', { + defaultMessage: 'Learn more about', +}); + +export const HOST_RISK_DATA = i18n.translate('xpack.securitySolution.kpiHost.hostRiskData', { + defaultMessage: 'host risk data', +}); + +export const ENABLE_HOST_RISK_TEXT = i18n.translate( + 'xpack.securitySolution.kpiHost.enableHostRiskText', + { + defaultMessage: 'Enable host risk module to see more data', + } +); diff --git a/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/risky_hosts/index.tsx b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/risky_hosts/index.tsx new file mode 100644 index 0000000000000..cd9f01e2fd67c --- /dev/null +++ b/x-pack/plugins/security_solution/public/hosts/containers/kpi_hosts/risky_hosts/index.tsx @@ -0,0 +1,98 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { Observable } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { useEffect, useState } from 'react'; +import { useObservable, withOptionalSignal } from '@kbn/securitysolution-hook-utils'; +import { createFilter } from '../../../../common/containers/helpers'; + +import { HostsKpiQueries, RequestBasicOptions } from '../../../../../common/search_strategy'; + +import { + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../../src/plugins/data/common'; +import type { DataPublicPluginStart } from '../../../../../../../../src/plugins/data/public'; +import type { HostsKpiRiskyHostsStrategyResponse } from '../../../../../common/search_strategy/security_solution/hosts/kpi/risky_hosts'; +import { useKibana } from '../../../../common/lib/kibana'; +import { isIndexNotFoundError } from '../../../../common/utils/exceptions'; +import { getHostRiskIndex } from '../../../../helpers'; + +export type RiskyHostsScoreRequestOptions = RequestBasicOptions; + +type GetHostsRiskScoreProps = RiskyHostsScoreRequestOptions & { + data: DataPublicPluginStart; + signal: AbortSignal; +}; + +export const getRiskyHosts = ({ + data, + defaultIndex, + timerange, + signal, + filterQuery, +}: GetHostsRiskScoreProps): Observable => + data.search.search( + { + defaultIndex, + factoryQueryType: HostsKpiQueries.kpiRiskyHosts, + filterQuery: createFilter(filterQuery), + timerange, + }, + { + strategy: 'securitySolutionSearchStrategy', + abortSignal: signal, + } + ); + +export const getRiskyHostsComplete = ( + props: GetHostsRiskScoreProps +): Observable => { + return getRiskyHosts(props).pipe( + filter((response) => { + return isErrorResponse(response) || isCompleteResponse(response); + }) + ); +}; + +const getRiskyHostsWithOptionalSignal = withOptionalSignal(getRiskyHostsComplete); + +const useRiskyHostsComplete = () => useObservable(getRiskyHostsWithOptionalSignal); + +interface UseRiskyHostProps { + filterQuery?: string; + from: string; + to: string; + skip: boolean; +} + +export const useRiskyHosts = ({ filterQuery, from, to, skip }: UseRiskyHostProps) => { + const { error, result: response, start, loading } = useRiskyHostsComplete(); + const { data, spaces } = useKibana().services; + const isModuleDisabled = error && isIndexNotFoundError(error); + const [spaceId, setSpaceId] = useState(); + + useEffect(() => { + if (spaces) { + spaces.getActiveSpace().then((space) => setSpaceId(space.id)); + } + }, [spaces]); + + useEffect(() => { + if (!skip && spaceId) { + start({ + data, + timerange: { to, from, interval: '' }, + filterQuery, + defaultIndex: [getHostRiskIndex(spaceId)], + }); + } + }, [data, spaceId, start, filterQuery, to, from, skip]); + + return { error, response, loading, isModuleDisabled }; +}; diff --git a/x-pack/plugins/security_solution/public/management/components/effected_policy_select/utils.ts b/x-pack/plugins/security_solution/public/management/components/effected_policy_select/utils.ts index f273ecaa96c56..ba4f3ae38fffa 100644 --- a/x-pack/plugins/security_solution/public/management/components/effected_policy_select/utils.ts +++ b/x-pack/plugins/security_solution/public/management/components/effected_policy_select/utils.ts @@ -47,7 +47,7 @@ export function getEffectedPolicySelectionByTags( tags: string[], policies: PolicyData[] ): EffectedPolicySelection { - if (tags.length === 0 || tags.find((tag) => tag === GLOBAL_POLICY_TAG)) { + if (tags.find((tag) => tag === GLOBAL_POLICY_TAG)) { return { isGlobal: true, selected: [], diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts index 3aacd1db2f3dd..12685896d9535 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/mocks.ts @@ -24,12 +24,13 @@ import { ENDPOINT_ACTION_LOG_ROUTE, HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, + METADATA_TRANSFORMS_STATUS_ROUTE, } from '../../../../common/endpoint/constants'; import { pendingActionsHttpMock, PendingActionsHttpMockInterface, } from '../../../common/lib/endpoint_pending_actions/mocks'; -import { METADATA_TRANSFORM_STATS_URL, TRANSFORM_STATES } from '../../../../common/constants'; +import { TRANSFORM_STATES } from '../../../../common/constants'; import { TransformStatsResponse } from './types'; import { fleetGetAgentPolicyListHttpMock, @@ -162,7 +163,7 @@ export const failedTransformStateMock = { export const transformsHttpMocks = httpHandlerMockFactory([ { id: 'metadataTransformStats', - path: METADATA_TRANSFORM_STATS_URL, + path: METADATA_TRANSFORMS_STATUS_ROUTE, method: 'get', handler: () => failedTransformStateMock, }, 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 2759a35841524..9d839be623920 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 @@ -10,7 +10,6 @@ import { CoreStart, HttpStart } from 'kibana/public'; import { Dispatch } from 'redux'; import semverGte from 'semver/functions/gte'; import { AGENT_POLICY_SAVED_OBJECT_TYPE } from '../../../../../../fleet/common'; -import { METADATA_TRANSFORM_STATS_URL } from '../../../../../common/constants'; import { BASE_POLICY_RESPONSE_ROUTE, ENDPOINT_ACTION_LOG_ROUTE, @@ -18,6 +17,7 @@ import { HOST_METADATA_LIST_ROUTE, metadataCurrentIndexPattern, METADATA_UNITED_INDEX, + METADATA_TRANSFORMS_STATUS_ROUTE, } from '../../../../../common/endpoint/constants'; import { ActivityLog, @@ -783,7 +783,7 @@ export async function handleLoadMetadataTransformStats(http: HttpStart, store: E try { const transformStatsResponse: TransformStatsResponse = await http.get( - METADATA_TRANSFORM_STATS_URL + METADATA_TRANSFORMS_STATUS_ROUTE ); dispatch({ diff --git a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts index 61eb5ad3c541d..1e8b55ef977e8 100644 --- a/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts +++ b/x-pack/plugins/security_solution/public/management/pages/endpoint_hosts/store/mock_endpoint_result_list.ts @@ -32,8 +32,8 @@ import { pendingActionsResponseMock } from '../../../../common/lib/endpoint_pend import { ACTION_STATUS_ROUTE, HOST_METADATA_LIST_ROUTE, + METADATA_TRANSFORMS_STATUS_ROUTE, } from '../../../../../common/endpoint/constants'; -import { METADATA_TRANSFORM_STATS_URL } from '../../../../../common/constants'; import { TransformStats, TransformStatsResponse } from '../types'; const generator = new EndpointDocGenerator('seed'); @@ -162,7 +162,7 @@ const endpointListApiPathHandlerMocks = ({ return pendingActionsResponseMock(); }, - [METADATA_TRANSFORM_STATS_URL]: (): TransformStatsResponse => ({ + [METADATA_TRANSFORMS_STATUS_ROUTE]: (): TransformStatsResponse => ({ count: transforms.length, transforms, }), diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx index 10180d378457a..81e34e538468d 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/components/form.test.tsx @@ -233,5 +233,21 @@ describe('When on the host isolation exceptions entry form', () => { 'true' ); }); + + it('should show the policies selector when no policy is selected', () => { + existingException.tags = []; + + renderResult = render(existingException); + + expect(renderResult.queryByTestId('effectedPolicies-select-policiesSelectable')).toBeTruthy(); + }); + + it('should show the policies selector when no policy is selected and there are previous tags', () => { + existingException.tags = ['non-a-policy-tag']; + + renderResult = render(existingException); + + expect(renderResult.queryByTestId('effectedPolicies-select-policiesSelectable')).toBeTruthy(); + }); }); }); diff --git a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts index efdc481b2fe42..3cac7e7e466d6 100644 --- a/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts +++ b/x-pack/plugins/security_solution/public/management/pages/host_isolation_exceptions/view/hooks.ts @@ -57,7 +57,7 @@ export function useHostIsolationExceptionsNavigateCallback() { * Checks if the current user should be able to see the host isolation exceptions * menu item based on their current privileges */ -export function useCanSeeHostIsolationExceptionsMenu() { +export function useCanSeeHostIsolationExceptionsMenu(): boolean { const http = useHttp(); const privileges = useEndpointPrivileges(); @@ -77,6 +77,8 @@ export function useCanSeeHostIsolationExceptionsMenu() { } if (!privileges.canIsolateHost) { checkIfHasExceptions(); + } else { + setCanSeeMenu(true); } }, [http, privileges.canIsolateHost]); diff --git a/x-pack/plugins/security_solution/public/overview/components/link_panel/helpers.ts b/x-pack/plugins/security_solution/public/overview/components/link_panel/helpers.ts index 45d26d9269f6e..e2adaaae35547 100644 --- a/x-pack/plugins/security_solution/public/overview/components/link_panel/helpers.ts +++ b/x-pack/plugins/security_solution/public/overview/components/link_panel/helpers.ts @@ -5,13 +5,6 @@ * 2.0. */ -import { LinkPanelListItem } from '.'; - -export const isLinkPanelListItem = ( - item: LinkPanelListItem | Partial -): item is LinkPanelListItem => - typeof item.title === 'string' && typeof item.path === 'string' && typeof item.count === 'number'; - export interface EventCounts { [key: string]: number; } diff --git a/x-pack/plugins/security_solution/public/overview/components/link_panel/index.ts b/x-pack/plugins/security_solution/public/overview/components/link_panel/index.ts index 9d404abcf2223..9a827b137ae78 100644 --- a/x-pack/plugins/security_solution/public/overview/components/link_panel/index.ts +++ b/x-pack/plugins/security_solution/public/overview/components/link_panel/index.ts @@ -6,6 +6,5 @@ */ export { InnerLinkPanel } from './inner_link_panel'; -export { isLinkPanelListItem } from './helpers'; export { LinkPanel } from './link_panel'; export type { LinkPanelListItem } from './types'; diff --git a/x-pack/plugins/security_solution/public/overview/components/link_panel/link_panel.tsx b/x-pack/plugins/security_solution/public/overview/components/link_panel/link_panel.tsx index ed67fdb1c96f6..00a225635fb8b 100644 --- a/x-pack/plugins/security_solution/public/overview/components/link_panel/link_panel.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/link_panel/link_panel.tsx @@ -71,7 +71,7 @@ const LinkPanelComponent = ({ splitPanel, subtitle, }: { - button: React.ReactNode; + button?: React.ReactNode; columns: Array>; dataTestSubj: string; defaultSortField?: string; @@ -134,14 +134,16 @@ const LinkPanelComponent = ({ {splitPanel} {infoPanel} - + {chunkedItems.length > 0 && ( + + )} diff --git a/x-pack/plugins/security_solution/public/overview/components/link_panel/types.ts b/x-pack/plugins/security_solution/public/overview/components/link_panel/types.ts index f6c0fb6f3837f..1b8836fc2438d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/link_panel/types.ts +++ b/x-pack/plugins/security_solution/public/overview/components/link_panel/types.ts @@ -21,4 +21,5 @@ export interface LinkPanelViewProps { listItems: LinkPanelListItem[]; splitPanel?: JSX.Element; totalCount?: number; + allIntegrationsInstalled?: boolean; } diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx index 2697e4a571ad8..36f386e49c5c7 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_disabled_module.tsx @@ -6,24 +6,21 @@ */ import React from 'react'; -import { EMPTY_LIST_ITEMS } from '../../containers/overview_cti_links/helpers'; -import { useKibana } from '../../../common/lib/kibana'; import * as i18n from './translations'; import { DisabledLinkPanel } from '../link_panel/disabled_link_panel'; import { ThreatIntelPanelView } from './threat_intel_panel_view'; +import { useIntegrationsPageLink } from './use_integrations_page_link'; export const CtiDisabledModuleComponent = () => { - const threatIntelDocLink = `${ - useKibana().services.docLinks.links.filebeat.base - }/filebeat-module-threatintel.html`; + const integrationsLink = useIntegrationsPageLink(); return ( diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.test.tsx index db83d9e1bcfe5..fc36a0c4337cf 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.test.tsx @@ -19,20 +19,15 @@ import { mockGlobalState, SUB_PLUGINS_REDUCER, } from '../../../common/mock'; -import { mockTheme, mockProps, mockCtiEventCountsResponse, mockCtiLinksResponse } from './mock'; -import { useCtiEventCounts } from '../../containers/overview_cti_links/use_cti_event_counts'; +import { mockTheme, mockProps, mockTiDataSources, mockCtiLinksResponse } from './mock'; import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; -import { useRequestEventCounts } from '../../containers/overview_cti_links/use_request_event_counts'; +import { useTiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; jest.mock('../../../common/lib/kibana'); -jest.mock('../../containers/overview_cti_links/use_cti_event_counts'); -const useCTIEventCountsMock = useCtiEventCounts as jest.Mock; -useCTIEventCountsMock.mockReturnValue(mockCtiEventCountsResponse); - -jest.mock('../../containers/overview_cti_links/use_request_event_counts'); -const useRequestEventCountsMock = useRequestEventCounts as jest.Mock; -useRequestEventCountsMock.mockReturnValue([true, {}]); +jest.mock('../../containers/overview_cti_links/use_ti_data_sources'); +const useTiDataSourcesMock = useTiDataSources as jest.Mock; +useTiDataSourcesMock.mockReturnValue(mockTiDataSources); jest.mock('../../containers/overview_cti_links'); const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; @@ -54,42 +49,12 @@ describe('CtiEnabledModule', () => { - - - - - ); - - expect(screen.getByTestId('cti-with-events')).toBeInTheDocument(); - }); - - it('renders CtiWithNoEvents when there are no events', () => { - useCTIEventCountsMock.mockReturnValueOnce({ totalCount: 0 }); - render( - - - - - - - - ); - - expect(screen.getByTestId('cti-with-no-events')).toBeInTheDocument(); - }); - - it('renders null while event counts are loading', () => { - useCTIEventCountsMock.mockReturnValueOnce({ totalCount: -1 }); - const { container } = render( - - - - + ); - expect(container.firstChild).toBeNull(); + expect(screen.getByText('Showing: 5 indicators')).toBeInTheDocument(); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.tsx index 5a40c79d6e5ec..a339676ac361f 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_enabled_module.tsx @@ -7,37 +7,28 @@ import React from 'react'; import { ThreatIntelLinkPanelProps } from '.'; -import { useCtiEventCounts } from '../../containers/overview_cti_links/use_cti_event_counts'; -import { CtiNoEvents } from './cti_no_events'; -import { CtiWithEvents } from './cti_with_events'; +import { useTiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; +import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; +import { ThreatIntelPanelView } from './threat_intel_panel_view'; -export type CtiEnabledModuleProps = Omit; +export const CtiEnabledModuleComponent: React.FC = (props) => { + const { to, from, allIntegrationsInstalled, allTiDataSources, setQuery, deleteQuery } = props; + const { tiDataSources, totalCount } = useTiDataSources({ + to, + from, + allTiDataSources, + setQuery, + deleteQuery, + }); + const { listItems } = useCtiDashboardLinks({ to, from, tiDataSources }); -export const CtiEnabledModuleComponent: React.FC = (props) => { - const { eventCountsByDataset, totalCount } = useCtiEventCounts(props); - const { to, from } = props; - - switch (totalCount) { - case -1: - return null; - case 0: - return ( -
- -
- ); - default: - return ( -
- -
- ); - } + return ( + + ); }; export const CtiEnabledModule = React.memo(CtiEnabledModuleComponent); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx deleted file mode 100644 index 8f624dabd64d1..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { Provider } from 'react-redux'; -import { cloneDeep } from 'lodash/fp'; -import { render, screen } from '@testing-library/react'; -import { I18nProvider } from '@kbn/i18n-react'; -import { CtiNoEvents } from './cti_no_events'; -import { ThemeProvider } from 'styled-components'; -import { createStore, State } from '../../../common/store'; -import { - createSecuritySolutionStorageMock, - kibanaObservable, - mockGlobalState, - SUB_PLUGINS_REDUCER, -} from '../../../common/mock'; -import { mockEmptyCtiLinksResponse, mockTheme, mockProps } from './mock'; -import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; - -jest.mock('../../../common/lib/kibana'); - -jest.mock('../../containers/overview_cti_links'); -const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; -useCtiDashboardLinksMock.mockReturnValue(mockEmptyCtiLinksResponse); - -describe('CtiNoEvents', () => { - const state: State = mockGlobalState; - - const { storage } = createSecuritySolutionStorageMock(); - let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - - beforeEach(() => { - const myState = cloneDeep(state); - store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - }); - - it('renders warning inner panel', () => { - render( - - - - - - - - ); - - expect(screen.getByTestId('cti-dashboard-links')).toBeInTheDocument(); - expect(screen.getByTestId('cti-inner-panel-warning')).toBeInTheDocument(); - }); - - it('renders event counts as 0', () => { - render( - - - - - - - - ); - - expect(screen.getByText('Showing: 0 indicators')).toBeInTheDocument(); - }); -}); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.tsx deleted file mode 100644 index fa7ac50c08765..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_no_events.tsx +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; -import { ThreatIntelPanelView } from './threat_intel_panel_view'; -import { InnerLinkPanel } from '../link_panel'; -import * as i18n from './translations'; -import { emptyEventCountsByDataset } from '../../containers/overview_cti_links/helpers'; - -const warning = ( - -); - -export const CtiNoEventsComponent = ({ to, from }: { to: string; from: string }) => { - const { buttonHref, listItems, isPluginDisabled } = useCtiDashboardLinks( - emptyEventCountsByDataset, - to, - from - ); - - return ( - - ); -}; - -export const CtiNoEvents = React.memo(CtiNoEventsComponent); -CtiNoEvents.displayName = 'CtiNoEvents'; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx deleted file mode 100644 index a50e3e91ab9e5..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.test.tsx +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { Provider } from 'react-redux'; -import { cloneDeep } from 'lodash/fp'; -import { mount } from 'enzyme'; -import { I18nProvider } from '@kbn/i18n-react'; -import { CtiWithEvents } from './cti_with_events'; -import { ThemeProvider } from 'styled-components'; -import { createStore, State } from '../../../common/store'; -import { - createSecuritySolutionStorageMock, - kibanaObservable, - mockGlobalState, - SUB_PLUGINS_REDUCER, -} from '../../../common/mock'; -import { mockCtiLinksResponse, mockTheme, mockCtiWithEventsProps } from './mock'; -import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; - -jest.mock('../../../common/lib/kibana'); - -jest.mock('../../containers/overview_cti_links'); -const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; -useCtiDashboardLinksMock.mockReturnValue(mockCtiLinksResponse); - -describe('CtiWithEvents', () => { - const state: State = mockGlobalState; - - const { storage } = createSecuritySolutionStorageMock(); - let store = createStore(state, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - - beforeEach(() => { - const myState = cloneDeep(state); - store = createStore(myState, SUB_PLUGINS_REDUCER, kibanaObservable, storage); - }); - - it('renders total event count as expected', () => { - const wrapper = mount( - - - - - - - - ); - - expect(wrapper.find('[data-test-subj="cti-total-event-count"]').text()).toEqual( - `Showing: ${mockCtiWithEventsProps.totalCount} indicators` - ); - }); -}); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.tsx deleted file mode 100644 index f78451e205b1e..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/cti_with_events.tsx +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import React from 'react'; -import { isEqual } from 'lodash'; -import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; -import { ThreatIntelPanelView } from './threat_intel_panel_view'; - -export const CtiWithEventsComponent = ({ - eventCountsByDataset, - from, - to, - totalCount, -}: { - eventCountsByDataset: { [key: string]: number }; - from: string; - to: string; - totalCount: number; -}) => { - const { buttonHref, isPluginDisabled, listItems } = useCtiDashboardLinks( - eventCountsByDataset, - to, - from - ); - - return ( - - ); -}; - -CtiWithEventsComponent.displayName = 'CtiWithEvents'; - -export const CtiWithEvents = React.memo( - CtiWithEventsComponent, - (prevProps, nextProps) => - prevProps.to === nextProps.to && - prevProps.from === nextProps.from && - prevProps.totalCount === nextProps.totalCount && - isEqual(prevProps.eventCountsByDataset, nextProps.eventCountsByDataset) -); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.test.tsx index dfd9c6c9a7fcd..71d6d5eb0c583 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.test.tsx @@ -19,19 +19,19 @@ import { mockGlobalState, SUB_PLUGINS_REDUCER, } from '../../../common/mock'; -import { mockTheme, mockProps, mockCtiEventCountsResponse } from './mock'; -import { useRequestEventCounts } from '../../containers/overview_cti_links/use_request_event_counts'; -import { useCtiEventCounts } from '../../containers/overview_cti_links/use_cti_event_counts'; +import { mockTheme, mockProps, mockTiDataSources, mockCtiLinksResponse } from './mock'; +import { useTiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; +import { useCtiDashboardLinks } from '../../containers/overview_cti_links'; jest.mock('../../../common/lib/kibana'); -jest.mock('../../containers/overview_cti_links/use_request_event_counts'); -const useRequestEventCountsMock = useRequestEventCounts as jest.Mock; -useRequestEventCountsMock.mockReturnValue([true, {}]); +jest.mock('../../containers/overview_cti_links/use_ti_data_sources'); +const useTiDataSourcesMock = useTiDataSources as jest.Mock; +useTiDataSourcesMock.mockReturnValue(mockTiDataSources); -jest.mock('../../containers/overview_cti_links/use_cti_event_counts'); -const useCTIEventCountsMock = useCtiEventCounts as jest.Mock; -useCTIEventCountsMock.mockReturnValue(mockCtiEventCountsResponse); +jest.mock('../../containers/overview_cti_links'); +const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; +useCtiDashboardLinksMock.mockReturnValue(mockCtiLinksResponse); describe('ThreatIntelLinkPanel', () => { const state: State = mockGlobalState; @@ -49,40 +49,44 @@ describe('ThreatIntelLinkPanel', () => { - + ); expect(wrapper.find('[data-test-subj="cti-enabled-module"]').length).toEqual(1); + expect(wrapper.find('[data-test-subj="cti-enable-integrations-button"]').length).toEqual(0); }); - it('renders CtiDisabledModule when Threat Intel module is disabled', () => { + it('renders Enable source buttons when not all integrations installed', () => { const wrapper = mount( - + ); - - expect(wrapper.find('[data-test-subj="cti-disabled-module"]').length).toEqual(1); + expect(wrapper.find('[data-test-subj="cti-enable-integrations-button"]').length).not.toBe(0); }); - it('renders null while Threat Intel module state is loading', () => { + it('renders CtiDisabledModule when Threat Intel module is disabled', () => { const wrapper = mount( - + ); - expect(wrapper.html()).toEqual(''); + expect(wrapper.find('[data-test-subj="cti-disabled-module"]').length).toEqual(1); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.tsx index 5348c12fb6c8e..c89199c2cb0c5 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/index.tsx @@ -8,6 +8,7 @@ import React from 'react'; import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; +import { TiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; import { CtiEnabledModule } from './cti_enabled_module'; import { CtiDisabledModule } from './cti_disabled_module'; @@ -15,27 +16,26 @@ export type ThreatIntelLinkPanelProps = Pick< GlobalTimeArgs, 'from' | 'to' | 'deleteQuery' | 'setQuery' > & { - isThreatIntelModuleEnabled: boolean | undefined; + allIntegrationsInstalled: boolean | undefined; + allTiDataSources: TiDataSources[]; }; const ThreatIntelLinkPanelComponent: React.FC = (props) => { - switch (props.isThreatIntelModuleEnabled) { - case true: - return ( -
- -
- ); - case false: - return ( -
- -
- ); - case undefined: - default: - return null; - } + const { allIntegrationsInstalled, allTiDataSources } = props; + const isThreatIntelModuleEnabled = allTiDataSources.length > 0; + return isThreatIntelModuleEnabled ? ( +
+ +
+ ) : ( +
+ +
+ ); }; export const ThreatIntelLinkPanel = React.memo(ThreatIntelLinkPanelComponent); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/mock.ts b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/mock.ts index 1d02acaf65f48..c4cf876cbdc7d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/mock.ts +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/mock.ts @@ -15,6 +15,13 @@ export const mockTheme = getMockTheme({ }, }); +export const mockTiDataSources = { + totalCount: 5, + tiDataSources: [ + { dataset: 'ti_abusech', name: 'AbuseCH', count: 5, path: '/dashboard_path_abuseurl' }, + ], +}; + export const mockEventCountsByDataset = { abuseurl: 1, abusemalware: 1, @@ -31,8 +38,6 @@ export const mockCtiEventCountsResponse = { }; export const mockCtiLinksResponse = { - isPluginDisabled: false, - buttonHref: '/button', listItems: [ { title: 'abuseurl', count: 1, path: '/dashboard_path_abuseurl' }, { title: 'abusemalware', count: 2, path: '/dashboard_path_abusemalware' }, @@ -63,6 +68,10 @@ export const mockProps = { from: '2020-01-21T20:49:57.080Z', setQuery: jest.fn(), deleteQuery: jest.fn(), + allIntegrationsInstalled: true, + allTiDataSources: [ + { dataset: 'ti_abusech', name: 'AbuseCH', count: 5, path: '/dashboard_path_abuseurl' }, + ], }; export const mockCtiWithEventsProps = { diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx index 189f230c02c8d..3697d27015fdc 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/threat_intel_panel_view.tsx @@ -9,14 +9,14 @@ import React, { useMemo } from 'react'; import { EuiButton, EuiTableFieldDataColumnType } from '@elastic/eui'; import { FormattedMessage } from '@kbn/i18n-react'; -import { useKibana } from '../../../common/lib/kibana'; import * as i18n from './translations'; import { LinkPanel, InnerLinkPanel, LinkPanelListItem } from '../link_panel'; import { LinkPanelViewProps } from '../link_panel/types'; import { shortenCountIntoString } from '../../../common/utils/shorten_count_into_string'; import { Link } from '../link_panel/link'; -import { ID as CTIEventCountQueryId } from '../../containers/overview_cti_links/use_cti_event_counts'; +import { ID as CTIEventCountQueryId } from '../../containers/overview_cti_links/use_ti_data_sources'; import { LINK_COPY } from '../overview_risky_host_links/translations'; +import { useIntegrationsPageLink } from './use_integrations_page_link'; const columns: Array> = [ { name: 'Name', field: 'title', sortable: true, truncateText: true, width: '100%' }, @@ -39,51 +39,43 @@ const columns: Array> = [ ]; export const ThreatIntelPanelView: React.FC = ({ - buttonHref = '', - isPluginDisabled, isInspectEnabled = true, listItems, splitPanel, totalCount = 0, + allIntegrationsInstalled, }) => { - const threatIntelDashboardDocLink = `${ - useKibana().services.docLinks.links.filebeat.base - }/load-kibana-dashboards.html`; + const integrationsLink = useIntegrationsPageLink(); return ( ( - - {i18n.VIEW_DASHBOARD} - - ), - [buttonHref] - ), columns, dataTestSubj: 'cti-dashboard-links', infoPanel: useMemo( - () => - isPluginDisabled ? ( - - {i18n.INFO_BUTTON} - - } - /> - ) : null, - [isPluginDisabled, threatIntelDashboardDocLink] + () => ( + <> + {allIntegrationsInstalled === false ? ( + + {i18n.DANGER_BUTTON} + + } + /> + ) : null} + + ), + [allIntegrationsInstalled, integrationsLink] ), inspectQueryId: isInspectEnabled ? CTIEventCountQueryId : undefined, listItems, diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts index 4a64462b27ad5..e112942b09749 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/translations.ts @@ -53,15 +53,14 @@ export const DANGER_TITLE = i18n.translate( export const DANGER_BODY = i18n.translate( 'xpack.securitySolution.overview.ctiDashboardEnableThreatIntel', { - defaultMessage: - 'You need to enable the filebeat threatintel module in order to view data from different sources.', + defaultMessage: 'You need to enable threat intel sources in order to view data.', } ); export const DANGER_BUTTON = i18n.translate( - 'xpack.securitySolution.overview.ctiDashboardDangerPanelButton', + 'xpack.securitySolution.overview.ctiDashboardDangerButton', { - defaultMessage: 'Enable Module', + defaultMessage: 'Enable sources', } ); @@ -72,3 +71,17 @@ export const PANEL_TITLE = i18n.translate('xpack.securitySolution.overview.ctiDa export const VIEW_DASHBOARD = i18n.translate('xpack.securitySolution.overview.ctiViewDasboard', { defaultMessage: 'View dashboard', }); + +export const SOME_MODULES_DISABLE_TITLE = i18n.translate( + 'xpack.securitySolution.overview.ctiDashboardSomeModulesDisabledTItle', + { + defaultMessage: 'Some threat intel sources are disabled', + } +); + +export const OTHER_DATA_SOURCE_TITLE = i18n.translate( + 'xpack.securitySolution.overview.ctiDashboardOtherDatasourceTitle', + { + defaultMessage: 'Others', + } +); diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/request/index.ts b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/use_integrations_page_link.tsx similarity index 59% rename from x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/request/index.ts rename to x-pack/plugins/security_solution/public/overview/components/overview_cti_links/use_integrations_page_link.tsx index 47615a02668c6..de710c2f1b17c 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/request/index.ts +++ b/x-pack/plugins/security_solution/public/overview/components/overview_cti_links/use_integrations_page_link.tsx @@ -5,4 +5,7 @@ * 2.0. */ -export * from './download_artifact_schema'; +import { useBasePath } from '../../../common/lib/kibana'; + +export const useIntegrationsPageLink = () => + `${useBasePath()}/app/integrations/browse?q=threat%20intelligence`; diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.test.tsx index 2476b4d07c3c7..95ebe9be77019 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.test.tsx @@ -22,11 +22,11 @@ import { } from '../../../common/mock'; import { useRiskyHostsDashboardButtonHref } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href'; import { useRiskyHostsDashboardLinks } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_links'; -import { useHostsRiskScore } from '../../containers/overview_risky_host_links/use_hosts_risk_score'; +import { useHostsRiskScore } from '../../../common/containers/hosts_risk/use_hosts_risk_score'; jest.mock('../../../common/lib/kibana'); -jest.mock('../../containers/overview_risky_host_links/use_hosts_risk_score'); +jest.mock('../../../common/containers/hosts_risk/use_hosts_risk_score'); const useHostsRiskScoreMock = useHostsRiskScore as jest.Mock; jest.mock('../../containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href'); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.tsx index 57bcff45a6348..64829aab7776d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/index.tsx @@ -9,7 +9,7 @@ import React from 'react'; import { RiskyHostsEnabledModule } from './risky_hosts_enabled_module'; import { RiskyHostsDisabledModule } from './risky_hosts_disabled_module'; -import { useHostsRiskScore } from '../../containers/overview_risky_host_links/use_hosts_risk_score'; +import { useHostsRiskScore } from '../../../common/containers/hosts_risk/use_hosts_risk_score'; export interface RiskyHostLinksProps { timerange: { to: string; from: string }; } diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.test.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.test.tsx index 912945549be8c..364b608c6086d 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.test.tsx @@ -60,7 +60,10 @@ describe('RiskyHostsEnabledModule', () => { host: { name: 'a', }, - risk_score: 1, + risk_stats: { + risk_score: 1, + rule_risks: [], + }, risk: '', }, ], diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.tsx index 412c4a69ec2f5..875b7c206d793 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_enabled_module.tsx @@ -10,13 +10,13 @@ import { RiskyHostsPanelView } from './risky_hosts_panel_view'; import { LinkPanelListItem } from '../link_panel'; import { useRiskyHostsDashboardButtonHref } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_button_href'; import { useRiskyHostsDashboardLinks } from '../../containers/overview_risky_host_links/use_risky_hosts_dashboard_links'; -import { HostRisk } from '../../containers/overview_risky_host_links/use_hosts_risk_score'; +import { HostRisk } from '../../../common/containers/hosts_risk/use_hosts_risk_score'; import { HostsRiskScore } from '../../../../common/search_strategy'; const getListItemsFromHits = (items: HostsRiskScore[]): LinkPanelListItem[] => { - return items.map(({ host, risk_score: count, risk: copy }) => ({ + return items.map(({ host, risk_stats: riskStats, risk: copy }) => ({ title: host.name, - count, + count: riskStats.risk_score, copy, path: '', })); diff --git a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx index 87a5710ab0372..8a42cedc3be46 100644 --- a/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx +++ b/x-pack/plugins/security_solution/public/overview/components/overview_risky_host_links/risky_hosts_panel_view.tsx @@ -14,7 +14,7 @@ import { LinkPanelViewProps } from '../link_panel/types'; import { Link } from '../link_panel/link'; import * as i18n from './translations'; import { VIEW_DASHBOARD } from '../overview_cti_links/translations'; -import { QUERY_ID as RiskyHostsQueryId } from '../../containers/overview_risky_host_links/use_hosts_risk_score'; +import { QUERY_ID as RiskyHostsQueryId } from '../../../common/containers/hosts_risk/use_hosts_risk_score'; import { NavigateToHost } from './navigate_to_host'; const columns: Array> = [ diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/api.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/api.ts new file mode 100644 index 0000000000000..ad737ac410e3b --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/api.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { KibanaServices } from '../../../common/lib/kibana'; +import { EPM_API_ROUTES } from '../../../../../fleet/common'; + +export interface IntegrationResponse { + id: string; + status: string; + savedObject?: { + attributes?: { + installed_kibana: Array<{ + type: string; + id: string; + }>; + }; + }; +} + +export const fetchFleetIntegrations = () => + KibanaServices.get().http.fetch<{ + response: IntegrationResponse[]; + }>(EPM_API_ROUTES.LIST_PATTERN, { + method: 'GET', + }); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts deleted file mode 100644 index 9ac61cc9487ee..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/helpers.ts +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types'; -import { CTI_DATASET_KEY_MAP } from '../../../../common/cti/constants'; -import { LinkPanelListItem } from '../../components/link_panel'; -import { EventCounts } from '../../components/link_panel/helpers'; - -export const ctiTitles = Object.keys(CTI_DATASET_KEY_MAP) as string[]; - -export const EMPTY_LIST_ITEMS: LinkPanelListItem[] = ctiTitles.map((title) => ({ - title, - count: 0, - path: '', -})); - -const TAG_REQUEST_BODY_SEARCH = 'threat intel'; -export const TAG_REQUEST_BODY = { - type: 'tag', - search: TAG_REQUEST_BODY_SEARCH, - searchFields: ['name'], -}; - -export const DASHBOARD_SO_TITLE_PREFIX = '[Filebeat Threat Intel] '; -export const OVERVIEW_DASHBOARD_LINK_TITLE = 'Overview'; - -export const getCtiListItemsWithoutLinks = (eventCounts: EventCounts): LinkPanelListItem[] => { - return EMPTY_LIST_ITEMS.map((item) => ({ - ...item, - count: eventCounts[CTI_DATASET_KEY_MAP[item.title]] ?? 0, - })); -}; - -export const isOverviewItem = (item: { path?: string; title?: string }) => - item.title === OVERVIEW_DASHBOARD_LINK_TITLE; - -export const createLinkFromDashboardSO = ( - dashboardSO: { attributes?: SavedObjectAttributes }, - eventCountsByDataset: EventCounts, - path: string -) => { - const title = - typeof dashboardSO.attributes?.title === 'string' - ? dashboardSO.attributes.title.replace(DASHBOARD_SO_TITLE_PREFIX, '') - : undefined; - return { - title, - count: typeof title === 'string' ? eventCountsByDataset[CTI_DATASET_KEY_MAP[title]] : undefined, - path, - }; -}; - -export const emptyEventCountsByDataset = Object.values(CTI_DATASET_KEY_MAP).reduce((acc, id) => { - acc[id] = 0; - return acc; -}, {} as { [key: string]: number }); diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx index a546d20e49583..b1310e363eef0 100644 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/index.tsx @@ -6,34 +6,29 @@ */ import { useState, useEffect, useCallback } from 'react'; import { SavedObjectAttributes } from '@kbn/securitysolution-io-ts-alerting-types'; +import { TiDataSources } from '../../containers/overview_cti_links/use_ti_data_sources'; +import { LinkPanelListItem } from '../../components/link_panel'; import { useKibana } from '../../../common/lib/kibana'; -import { - TAG_REQUEST_BODY, - createLinkFromDashboardSO, - getCtiListItemsWithoutLinks, - isOverviewItem, - EMPTY_LIST_ITEMS, -} from './helpers'; -import { LinkPanelListItem, isLinkPanelListItem } from '../../components/link_panel'; -export const useCtiDashboardLinks = ( - eventCountsByDataset: { [key: string]: number }, - to: string, - from: string -) => { - const createDashboardUrl = useKibana().services.dashboard?.dashboardUrlGenerator?.createUrl; - const savedObjectsClient = useKibana().services.savedObjects.client; - - const [buttonHref, setButtonHref] = useState(); - const [listItems, setListItems] = useState(EMPTY_LIST_ITEMS); +const TAG_REQUEST_BODY_SEARCH = 'threat intel'; +export const TAG_REQUEST_BODY = { + type: 'tag', + search: TAG_REQUEST_BODY_SEARCH, + searchFields: ['name'], +}; - const [isPluginDisabled, setIsDashboardPluginDisabled] = useState(false); - const handleDisabledPlugin = useCallback(() => { - if (!isPluginDisabled) { - setIsDashboardPluginDisabled(true); - } - setListItems(getCtiListItemsWithoutLinks(eventCountsByDataset)); - }, [setIsDashboardPluginDisabled, setListItems, eventCountsByDataset, isPluginDisabled]); +export const useCtiDashboardLinks = ({ + to, + from, + tiDataSources = [], +}: { + to: string; + from: string; + tiDataSources?: TiDataSources[]; +}) => { + const [installedDashboardIds, setInstalledDashboardIds] = useState([]); + const dashboardLocator = useKibana().services.dashboard?.locator; + const savedObjectsClient = useKibana().services.savedObjects.client; const handleTagsReceived = useCallback( (TagsSO?) => { @@ -49,9 +44,7 @@ export const useCtiDashboardLinks = ( ); useEffect(() => { - if (!createDashboardUrl || !savedObjectsClient) { - handleDisabledPlugin(); - } else { + if (savedObjectsClient) { savedObjectsClient .find(TAG_REQUEST_BODY) .then(handleTagsReceived) @@ -63,53 +56,40 @@ export const useCtiDashboardLinks = ( }>; }) => { if (DashboardsSO?.savedObjects?.length) { - const dashboardUrls = await Promise.all( - DashboardsSO.savedObjects.map((SO) => - createDashboardUrl({ - dashboardId: SO.id, - timeRange: { - to, - from, - }, - }) - ) + setInstalledDashboardIds( + DashboardsSO.savedObjects.map((SO) => SO.id ?? '').filter(Boolean) ); - const items = DashboardsSO.savedObjects - ?.reduce((acc: LinkPanelListItem[], dashboardSO, i) => { - const item = createLinkFromDashboardSO( - dashboardSO, - eventCountsByDataset, - dashboardUrls[i] - ); - if (isOverviewItem(item)) { - setButtonHref(item.path); - } else if (isLinkPanelListItem(item)) { - acc.push(item); - } - return acc; - }, []) - .sort((a, b) => (a.title > b.title ? 1 : -1)); - setListItems(items); - } else { - handleDisabledPlugin(); } } ); } - }, [ - createDashboardUrl, - eventCountsByDataset, - from, - handleDisabledPlugin, - handleTagsReceived, - isPluginDisabled, - savedObjectsClient, - to, - ]); + }, [handleTagsReceived, savedObjectsClient]); + + const listItems = tiDataSources.map((tiDataSource) => { + const listItem: LinkPanelListItem = { + title: tiDataSource.name, + count: tiDataSource.count, + path: '', + }; + + if ( + tiDataSource.dashboardId && + installedDashboardIds.includes(tiDataSource.dashboardId) && + dashboardLocator + ) { + listItem.path = dashboardLocator.getRedirectUrl({ + dashboardId: tiDataSource.dashboardId, + timeRange: { + to, + from, + }, + }); + } + + return listItem; + }); return { - buttonHref, - isPluginDisabled, listItems, }; }; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_all_ti_data_sources.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_all_ti_data_sources.ts new file mode 100644 index 0000000000000..5686be269121a --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_all_ti_data_sources.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { useMemo } from 'react'; +import { useTiDataSources } from './use_ti_data_sources'; + +export const useAllTiDataSources = () => { + const { to, from } = useMemo( + () => ({ + to: new Date().toISOString(), + from: new Date(0).toISOString(), + }), + [] + ); + + const { tiDataSources, isInitiallyLoaded } = useTiDataSources({ to, from }); + + return { tiDataSources, isInitiallyLoaded }; +}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts deleted file mode 100644 index c8076ab6a4484..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_cti_event_counts.ts +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { useEffect, useState, useMemo } from 'react'; -import { useRequestEventCounts } from './use_request_event_counts'; -import { emptyEventCountsByDataset } from './helpers'; -import { CtiEnabledModuleProps } from '../../components/overview_cti_links/cti_enabled_module'; - -export const ID = 'ctiEventCountQuery'; - -export const useCtiEventCounts = ({ deleteQuery, from, setQuery, to }: CtiEnabledModuleProps) => { - const [isInitialLoading, setIsInitialLoading] = useState(true); - - const [loading, { data, inspect, totalCount, refetch }] = useRequestEventCounts(to, from); - - const eventCountsByDataset = useMemo( - () => - data.reduce( - (acc, item) => { - if (item.y && item.g) { - const id = item.g; - acc[id] += item.y; - } - return acc; - }, - { ...emptyEventCountsByDataset } as { [key: string]: number } - ), - [data] - ); - - useEffect(() => { - if (isInitialLoading && data) { - setIsInitialLoading(false); - } - }, [isInitialLoading, data]); - - useEffect(() => { - if (!loading && !isInitialLoading) { - setQuery({ id: ID, inspect, loading, refetch }); - } - }, [setQuery, inspect, loading, refetch, isInitialLoading, setIsInitialLoading]); - - useEffect(() => { - return () => { - if (deleteQuery) { - deleteQuery({ id: ID }); - } - }; - }, [deleteQuery]); - - useEffect(() => { - refetch(); - }, [to, from, refetch]); - - return { - eventCountsByDataset, - loading, - totalCount, - }; -}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_is_threat_intel_module_enabled.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_is_threat_intel_module_enabled.ts deleted file mode 100644 index 0dc0e8a3fe1f2..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_is_threat_intel_module_enabled.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { useState, useEffect, useMemo } from 'react'; -import { useRequestEventCounts } from './use_request_event_counts'; - -export const useIsThreatIntelModuleEnabled = () => { - const [isThreatIntelModuleEnabled, setIsThreatIntelModuleEnabled] = useState< - boolean | undefined - >(); - - const { to, from } = useMemo( - () => ({ - to: new Date().toISOString(), - from: new Date(0).toISOString(), - }), - [] - ); - - const [, { totalCount }] = useRequestEventCounts(to, from); - - useEffect(() => { - if (totalCount !== -1) { - setIsThreatIntelModuleEnabled(totalCount > 0); - } - }, [totalCount]); - - return isThreatIntelModuleEnabled; -}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_request_event_counts.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_request_event_counts.ts deleted file mode 100644 index a1bf4d9d35f65..0000000000000 --- a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_request_event_counts.ts +++ /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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ -import { useMemo } from 'react'; -import { i18n } from '@kbn/i18n'; -import { convertToBuildEsQuery } from '../../../common/lib/keury'; -import { getEsQueryConfig } from '../../../../../../../src/plugins/data/common'; -import { MatrixHistogramType } from '../../../../common/search_strategy'; -import { EVENT_DATASET } from '../../../../common/cti/constants'; -import { useMatrixHistogram } from '../../../common/containers/matrix_histogram'; -import { useKibana } from '../../../common/lib/kibana'; -import { DEFAULT_THREAT_INDEX_KEY } from '../../../../common/constants'; - -export const useRequestEventCounts = (to: string, from: string) => { - const { uiSettings } = useKibana().services; - const defaultThreatIndices = uiSettings.get(DEFAULT_THREAT_INDEX_KEY); - - const [filterQuery] = convertToBuildEsQuery({ - config: getEsQueryConfig(uiSettings), - indexPattern: { - fields: [ - { - name: 'event.kind', - type: 'string', - }, - ], - title: defaultThreatIndices.toString(), - }, - queries: [{ query: 'event.type:indicator', language: 'kuery' }], - filters: [], - }); - - const matrixHistogramRequest = useMemo(() => { - return { - endDate: to, - errorMessage: i18n.translate('xpack.securitySolution.overview.errorFetchingEvents', { - defaultMessage: 'Error fetching events', - }), - filterQuery, - histogramType: MatrixHistogramType.events, - indexNames: defaultThreatIndices, - stackByField: EVENT_DATASET, - startDate: from, - size: 0, - }; - }, [to, from, filterQuery, defaultThreatIndices]); - - const results = useMatrixHistogram(matrixHistogramRequest); - - return results; -}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_data_sources.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_data_sources.ts new file mode 100644 index 0000000000000..19ea075aee489 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_data_sources.ts @@ -0,0 +1,174 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { Observable } from 'rxjs'; +import { filter } from 'rxjs/operators'; +import { useEffect, useState } from 'react'; +import { useObservable, withOptionalSignal } from '@kbn/securitysolution-hook-utils'; +import { useKibana } from '../../../common/lib/kibana'; +import { + DataPublicPluginStart, + isCompleteResponse, + isErrorResponse, +} from '../../../../../../../src/plugins/data/public'; +import { + Bucket, + CtiQueries, + CtiDataSourceStrategyResponse, + CtiDataSourceRequestOptions, +} from '../../../../common/search_strategy/security_solution/cti'; +import { DEFAULT_THREAT_INDEX_KEY } from '../../../../common/constants'; +import { GlobalTimeArgs } from '../../../common/containers/use_global_time'; +import { OTHER_DATA_SOURCE_TITLE } from '../../components/overview_cti_links/translations'; +import { OTHER_TI_DATASET_KEY } from '../../../../common/cti/constants'; + +type GetThreatIntelSourcProps = CtiDataSourceRequestOptions & { + data: DataPublicPluginStart; + signal: AbortSignal; +}; +export const ID = 'ctiEventCountQuery'; + +export const getTiDataSources = ({ + data, + defaultIndex, + timerange, + signal, +}: GetThreatIntelSourcProps): Observable => + data.search.search( + { + defaultIndex, + factoryQueryType: CtiQueries.dataSource, + timerange, + }, + { + strategy: 'securitySolutionSearchStrategy', + abortSignal: signal, + } + ); + +export const getTiDataSourcesComplete = ( + props: GetThreatIntelSourcProps +): Observable => { + return getTiDataSources(props).pipe( + filter((response) => { + return isErrorResponse(response) || isCompleteResponse(response); + }) + ); +}; + +const getTiDataSourcesWithOptionalSignal = withOptionalSignal(getTiDataSourcesComplete); + +export const useTiDataSourcesComplete = () => useObservable(getTiDataSourcesWithOptionalSignal); + +export interface TiDataSources { + dataset: string; + name: string; + count: number; + dashboardId?: string; +} +interface TiDataSourcesProps extends Partial { + allTiDataSources?: TiDataSources[]; +} + +export const useTiDataSources = ({ + to, + from, + allTiDataSources, + setQuery, + deleteQuery, +}: TiDataSourcesProps) => { + const [tiDataSources, setTiDataSources] = useState([]); + const [isInitiallyLoaded, setIsInitiallyLoaded] = useState(false); + const { data, uiSettings } = useKibana().services; + const defaultThreatIndices = uiSettings.get(DEFAULT_THREAT_INDEX_KEY); + const { result, start, loading } = useTiDataSourcesComplete(); + + useEffect(() => { + start({ + data, + timerange: to && from ? { to, from, interval: '' } : undefined, + defaultIndex: defaultThreatIndices, + }); + }, [to, from, start, data, defaultThreatIndices]); + + useEffect(() => { + if (!loading && result?.rawResponse && result?.inspect && setQuery) { + setQuery({ + id: ID, + inspect: { + dsl: result?.inspect?.dsl ?? [], + response: [JSON.stringify(result.rawResponse, null, 2)], + }, + loading, + refetch: () => {}, + }); + } + }, [setQuery, loading, result]); + + useEffect(() => { + return () => { + if (deleteQuery) { + deleteQuery({ id: ID }); + } + }; + }, [deleteQuery]); + + useEffect(() => { + if (result && !isInitiallyLoaded) { + setIsInitiallyLoaded(true); + } + }, [isInitiallyLoaded, result]); + + useEffect(() => { + if (!loading && result) { + const datasets = result?.rawResponse?.aggregations?.dataset?.buckets ?? []; + const getChildAggregationValue = (aggregation?: Bucket) => aggregation?.buckets?.[0]?.key; + + const integrationMap = datasets.reduce((acc: Record, dataset) => { + const datasetName = getChildAggregationValue(dataset?.name); + if (datasetName) { + return { + ...acc, + [dataset.key]: { + dataset: dataset?.key, + name: datasetName, + dashboardId: getChildAggregationValue(dataset?.dashboard), + count: dataset?.doc_count, + }, + }; + } else { + const otherTiDatasetKey = OTHER_TI_DATASET_KEY; + const otherDatasetCount = acc[otherTiDatasetKey]?.count ?? 0; + return { + ...acc, + [otherTiDatasetKey]: { + dataset: otherTiDatasetKey, + name: OTHER_DATA_SOURCE_TITLE, + count: otherDatasetCount + (dataset?.doc_count ?? 0), + }, + }; + } + }, {}); + + if (Array.isArray(allTiDataSources)) { + allTiDataSources.forEach((integration) => { + if (!integrationMap[integration.dataset]) { + integrationMap[integration.dataset] = { + ...integration, + count: 0, + }; + } + }); + } + + setTiDataSources(Object.values(integrationMap)); + } + }, [result, loading, allTiDataSources]); + + const totalCount = tiDataSources.reduce((acc, val) => acc + val.count, 0); + + return { tiDataSources, totalCount, isInitiallyLoaded }; +}; diff --git a/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_integrations.ts b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_integrations.ts new file mode 100644 index 0000000000000..24bdc191b3d66 --- /dev/null +++ b/x-pack/plugins/security_solution/public/overview/containers/overview_cti_links/use_ti_integrations.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { useEffect, useState } from 'react'; + +import { installationStatuses } from '../../../../../fleet/common'; +import { TI_INTEGRATION_PREFIX } from '../../../../common/cti/constants'; +import { fetchFleetIntegrations, IntegrationResponse } from './api'; + +export interface Integration { + id: string; + dashboardIds: string[]; +} + +interface TiIntegrationStatus { + allIntegrationsInstalled: boolean; +} + +export const useTiIntegrations = () => { + const [tiIntegrationsStatus, setTiIntegrationsStatus] = useState( + null + ); + + useEffect(() => { + const getPackages = async () => { + try { + const { response: integrations } = await fetchFleetIntegrations(); + const tiIntegrations = integrations.filter((integration: IntegrationResponse) => + integration.id.startsWith(TI_INTEGRATION_PREFIX) + ); + + const allIntegrationsInstalled = tiIntegrations.every( + (integration: IntegrationResponse) => + integration.status === installationStatuses.Installed + ); + + setTiIntegrationsStatus({ + allIntegrationsInstalled, + }); + } catch (e) { + setTiIntegrationsStatus({ + allIntegrationsInstalled: false, + }); + } + }; + + getPackages(); + }, []); + + return tiIntegrationsStatus; +}; diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx index 33fd1918dad59..c98b612375f9b 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.test.tsx @@ -20,17 +20,14 @@ import { Overview } from './index'; import { useUserPrivileges } from '../../common/components/user_privileges'; import { useSourcererDataView } from '../../common/containers/sourcerer'; import { useFetchIndex } from '../../common/containers/source'; -import { useIsThreatIntelModuleEnabled } from '../containers/overview_cti_links/use_is_threat_intel_module_enabled'; -import { useCtiEventCounts } from '../containers/overview_cti_links/use_cti_event_counts'; -import { - mockCtiEventCountsResponse, - mockCtiLinksResponse, -} from '../components/overview_cti_links/mock'; +import { useAllTiDataSources } from '../containers/overview_cti_links/use_all_ti_data_sources'; +import { useTiIntegrations } from '../containers/overview_cti_links/use_ti_integrations'; +import { mockCtiLinksResponse, mockTiDataSources } from '../components/overview_cti_links/mock'; import { useCtiDashboardLinks } from '../containers/overview_cti_links'; import { useIsExperimentalFeatureEnabled } from '../../common/hooks/use_experimental_features'; -import { useHostsRiskScore } from '../containers/overview_risky_host_links/use_hosts_risk_score'; import { initialUserPrivilegesState } from '../../common/components/user_privileges/user_privileges_context'; import { EndpointPrivileges } from '../../../common/endpoint/types'; +import { useHostsRiskScore } from '../../common/containers/hosts_risk/use_hosts_risk_score'; jest.mock('../../common/lib/kibana'); jest.mock('../../common/containers/source'); @@ -71,20 +68,19 @@ jest.mock('../../common/components/user_privileges', () => { jest.mock('../../common/containers/local_storage/use_messages_storage'); jest.mock('../containers/overview_cti_links'); -jest.mock('../containers/overview_cti_links/use_cti_event_counts'); const useCtiDashboardLinksMock = useCtiDashboardLinks as jest.Mock; useCtiDashboardLinksMock.mockReturnValue(mockCtiLinksResponse); -jest.mock('../containers/overview_cti_links/use_cti_event_counts'); -const useCTIEventCountsMock = useCtiEventCounts as jest.Mock; -useCTIEventCountsMock.mockReturnValue(mockCtiEventCountsResponse); +jest.mock('../containers/overview_cti_links/use_all_ti_data_sources'); +const useAllTiDataSourcesMock = useAllTiDataSources as jest.Mock; +useAllTiDataSourcesMock.mockReturnValue(mockTiDataSources); -jest.mock('../containers/overview_cti_links/use_is_threat_intel_module_enabled'); -const useIsThreatIntelModuleEnabledMock = useIsThreatIntelModuleEnabled as jest.Mock; -useIsThreatIntelModuleEnabledMock.mockReturnValue(true); +jest.mock('../containers/overview_cti_links/use_ti_integrations'); +const useTiIntegrationsMock = useTiIntegrations as jest.Mock; +useTiIntegrationsMock.mockReturnValue({}); -jest.mock('../containers/overview_risky_host_links/use_hosts_risk_score'); +jest.mock('../../common/containers/hosts_risk/use_hosts_risk_score'); const useHostsRiskScoreMock = useHostsRiskScore as jest.Mock; useHostsRiskScoreMock.mockReturnValue({ loading: false, @@ -301,8 +297,8 @@ describe('Overview', () => { }); describe('Threat Intel Dashboard Links', () => { - it('invokes useIsThreatIntelModuleEnabled hook only once', () => { - useIsThreatIntelModuleEnabledMock.mockClear(); + it('invokes useAllTiDataSourcesMock hook only once', () => { + useAllTiDataSourcesMock.mockClear(); mount( @@ -310,7 +306,7 @@ describe('Overview', () => { ); - expect(useIsThreatIntelModuleEnabledMock).toHaveBeenCalledTimes(1); + expect(useAllTiDataSourcesMock).toHaveBeenCalledTimes(1); }); }); }); diff --git a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx index 67ee6c55ac06f..1df49fed07358 100644 --- a/x-pack/plugins/security_solution/public/overview/pages/overview.tsx +++ b/x-pack/plugins/security_solution/public/overview/pages/overview.tsx @@ -30,7 +30,8 @@ import { ENDPOINT_METADATA_INDEX } from '../../../common/constants'; import { useSourcererDataView } from '../../common/containers/sourcerer'; import { useDeepEqualSelector } from '../../common/hooks/use_selector'; import { ThreatIntelLinkPanel } from '../components/overview_cti_links'; -import { useIsThreatIntelModuleEnabled } from '../containers/overview_cti_links/use_is_threat_intel_module_enabled'; +import { useAllTiDataSources } from '../containers/overview_cti_links/use_all_ti_data_sources'; +import { useTiIntegrations } from '../containers/overview_cti_links/use_ti_integrations'; import { useUserPrivileges } from '../../common/components/user_privileges'; import { RiskyHostLinks } from '../components/overview_risky_host_links'; import { useAlertsPrivileges } from '../../detections/containers/detection_engine/alerts/use_alerts_privileges'; @@ -75,7 +76,10 @@ const OverviewComponent = () => { endpointPrivileges: { canAccessFleet }, } = useUserPrivileges(); const { hasIndexRead, hasKibanaREAD } = useAlertsPrivileges(); - const isThreatIntelModuleEnabled = useIsThreatIntelModuleEnabled(); + const { tiDataSources: allTiDataSources, isInitiallyLoaded: allTiDataSourcesLoaded } = + useAllTiDataSources(); + const tiIntegrationStatus = useTiIntegrations(); + const isTiLoaded = tiIntegrationStatus && allTiDataSourcesLoaded; const riskyHostsEnabled = useIsExperimentalFeatureEnabled('riskyHostsEnabled'); @@ -150,13 +154,16 @@ const OverviewComponent = () => { - + {isTiLoaded && ( + + )} {riskyHostsEnabled && ( diff --git a/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.tsx index 088c37d36c167..75e5b59df1ffa 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/create_field_button/index.tsx @@ -15,7 +15,7 @@ import { useKibana } from '../../../common/lib/kibana'; import * as i18n from './translations'; import { CreateFieldComponentType, TimelineId } from '../../../../../timelines/common'; -import { tGridActions } from '../../../../../timelines/public'; +import { upsertColumn } from '../../../../../timelines/public'; import { useDataView } from '../../../common/containers/source/use_data_view'; import { SourcererScopeName } from '../../../common/store/sourcerer/model'; import { sourcererSelectors } from '../../../common/store'; @@ -59,7 +59,7 @@ export const CreateFieldButton = React.memo( // Add the new field to the event table dispatch( - tGridActions.upsertColumn({ + upsertColumn({ column: { columnHeaderType: defaultColumnHeaderType, id: field.name, diff --git a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx index f5086fd1a83da..80fdd6dfc7dd4 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/formatted_ip/index.test.tsx @@ -44,6 +44,17 @@ jest.mock('../../../common/components/drag_and_drop/draggable_wrapper', () => { }; }); +jest.mock('../../store/timeline', () => { + const original = jest.requireActual('../../store/timeline'); + return { + ...original, + timelineActions: { + ...original.timelineActions, + toggleDetailPanel: jest.fn(), + }, + }; +}); + describe('FormattedIp', () => { const props = { value: '192.168.1.1', @@ -53,16 +64,13 @@ describe('FormattedIp', () => { fieldName: 'host.ip', }; - let toggleDetailPanel: jest.SpyInstance; let toggleExpandedDetail: jest.SpyInstance; beforeAll(() => { - toggleDetailPanel = jest.spyOn(timelineActions, 'toggleDetailPanel'); toggleExpandedDetail = jest.spyOn(activeTimeline, 'toggleExpandedDetail'); }); afterEach(() => { - toggleDetailPanel.mockClear(); toggleExpandedDetail.mockClear(); }); test('should render ip address', () => { @@ -98,7 +106,7 @@ describe('FormattedIp', () => { wrapper.find('[data-test-subj="network-details"]').first().simulate('click'); await waitFor(() => { - expect(toggleDetailPanel).not.toHaveBeenCalled(); + expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); expect(toggleExpandedDetail).not.toHaveBeenCalled(); }); }); @@ -120,7 +128,7 @@ describe('FormattedIp', () => { wrapper.find('[data-test-subj="network-details"]').first().simulate('click'); await waitFor(() => { - expect(toggleDetailPanel).toHaveBeenCalledWith({ + expect(timelineActions.toggleDetailPanel).toHaveBeenCalledWith({ panelView: 'networkDetail', params: { flowTarget: 'source', @@ -176,7 +184,7 @@ describe('FormattedIp', () => { wrapper.find('[data-test-subj="network-details"]').first().simulate('click'); await waitFor(() => { - expect(toggleDetailPanel).toHaveBeenCalledWith({ + expect(timelineActions.toggleDetailPanel).toHaveBeenCalledWith({ panelView: 'networkDetail', params: { flowTarget: 'source', diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx index 6a7f0602c3675..145edafe38318 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/expandable_event.tsx @@ -22,7 +22,7 @@ import { BrowserFields } from '../../../../common/containers/source'; import { EventDetails } from '../../../../common/components/event_details/event_details'; import { TimelineEventsDetailsItem } from '../../../../../common/search_strategy/timeline'; import * as i18n from './translations'; -import { HostRisk } from '../../../../overview/containers/overview_risky_host_links/use_hosts_risk_score'; +import { HostRisk } from '../../../../common/containers/hosts_risk/use_hosts_risk_score'; export type HandleOnEventClosed = () => void; interface Props { diff --git a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx index 224662f0fd6ab..1d68356fc0bb7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/side_panel/event_details/index.tsx @@ -35,7 +35,7 @@ import { TimelineNonEcsData } from '../../../../../common/search_strategy'; import { Ecs } from '../../../../../common/ecs'; import { EventDetailsFooter } from './footer'; import { EntityType } from '../../../../../../timelines/common'; -import { useHostsRiskScore } from '../../../../overview/containers/overview_risky_host_links/use_hosts_risk_score'; +import { useHostsRiskScore } from '../../../../common/containers/hosts_risk/use_hosts_risk_score'; const StyledEuiFlyoutBody = styled(EuiFlyoutBody)` .euiFlyoutBody__overflow { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx index 46566aa2e7f15..4a49d15cefa26 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.test.tsx @@ -169,7 +169,7 @@ describe('Actions', () => { wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') ).toBe(false); }); - test('it enables for event.kind: alert and agent.type: endpoint', () => { + test('it disables for event.kind: alert and agent.type: endpoint', () => { const ecsData = { ...mockTimelineData[0].ecs, event: { kind: ['alert'] }, @@ -183,7 +183,7 @@ describe('Actions', () => { expect( wrapper.find('[data-test-subj="timeline-context-menu-button"]').first().prop('isDisabled') - ).toBe(false); + ).toBe(true); }); test('it shows the analyze event button when the event is from an endpoint', () => { const ecsData = { diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx index 4ccec85c9a96c..50e93e678e9c7 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/actions/index.tsx @@ -107,10 +107,7 @@ const ActionsComponent: React.FC = ({ const isContextMenuDisabled = useMemo(() => { return ( eventType !== 'signal' && - !( - (ecsData.event?.kind?.includes('event') || ecsData.event?.kind?.includes('alert')) && - ecsData.agent?.type?.includes('endpoint') - ) + !(ecsData.event?.kind?.includes('event') && ecsData.agent?.type?.includes('endpoint')) ); }, [ecsData, eventType]); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap index 8bbd09f9a230d..de37169645352 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/__snapshots__/threat_match_row.test.tsx.snap @@ -23,7 +23,7 @@ exports[`ThreatMatchRowView matches the registered snapshot 1`] = ` diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx index a484499424b65..f9438de2dbba5 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/indicator_details.tsx @@ -11,7 +11,7 @@ import { FormattedMessage } from '@kbn/i18n-react'; import { INDICATOR_MATCHED_TYPE, - INDICATOR_PROVIDER, + FEED_NAME, INDICATOR_REFERENCE, } from '../../../../../../../common/cti/constants'; import { DraggableBadge } from '../../../../../../common/components/draggables'; @@ -21,7 +21,7 @@ import { HorizontalSpacer } from './helpers'; interface IndicatorDetailsProps { contextId: string; eventId: string; - indicatorProvider: string | undefined; + feedName: string | undefined; indicatorReference: string | undefined; indicatorType: string | undefined; isDraggable?: boolean; @@ -30,7 +30,7 @@ interface IndicatorDetailsProps { export const IndicatorDetails: React.FC = ({ contextId, eventId, - indicatorProvider, + feedName, indicatorReference, indicatorType, isDraggable, @@ -55,7 +55,7 @@ export const IndicatorDetails: React.FC = ({ /> )} - {indicatorProvider && ( + {feedName && ( <> @@ -68,11 +68,11 @@ export const IndicatorDetails: React.FC = ({ diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx index 68b5ab36c7185..e4600048bc1da 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.test.tsx @@ -22,7 +22,7 @@ describe('ThreatMatchRowView', () => { { { baseProps = { contextId: 'contextId', eventId: 'eventId', - indicatorProvider: 'provider', + feedName: 'feedName', indicatorReference: 'http://example.com', indicatorType: 'domain', sourceField: 'host.name', @@ -114,14 +114,14 @@ describe('ThreatMatchRowView', () => { expect(indicatorType.exists()).toBeFalsy(); }); - it('renders the indicator provider, if present', () => { + it('renders the feed name, if present', () => { const wrapper = render(baseProps); - const indicatorProvider = wrapper.find( - '[data-test-subj="threat-match-indicator-details-indicator-provider"]' + const feedName = wrapper.find( + '[data-test-subj="threat-match-indicator-details-indicator-feedName"]' ); - expect(indicatorProvider.props()).toEqual( + expect(feedName.props()).toEqual( expect.objectContaining({ - value: 'provider', + value: 'feedName', }) ); }); @@ -129,10 +129,10 @@ describe('ThreatMatchRowView', () => { it('does not render the indicator provider, if absent', () => { const wrapper = render({ ...baseProps, - indicatorProvider: undefined, + feedName: undefined, }); const indicatorProvider = wrapper.find( - '[data-test-subj="threat-match-indicator-details-indicator-provider"]' + '[data-test-subj="threat-match-indicator-details-indicator-feedName"]' ); expect(indicatorProvider.exists()).toBeFalsy(); }); diff --git a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx index 3b0ac277d6040..7b6ddd361ba8d 100644 --- a/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx +++ b/x-pack/plugins/security_solution/public/timelines/components/timeline/body/renderers/cti/threat_match_row.tsx @@ -14,8 +14,8 @@ import { MATCHED_ATOMIC, MATCHED_FIELD, MATCHED_TYPE, - PROVIDER, REFERENCE, + FEED_NAME, } from '../../../../../../../common/cti/constants'; import { MatchDetails } from './match_details'; import { IndicatorDetails } from './indicator_details'; @@ -23,7 +23,7 @@ import { IndicatorDetails } from './indicator_details'; export interface ThreatMatchRowProps { contextId: string; eventId: string; - indicatorProvider: string | undefined; + feedName: string | undefined; indicatorReference: string | undefined; indicatorType: string | undefined; isDraggable?: boolean; @@ -46,7 +46,7 @@ export const ThreatMatchRow = ({ contextId, eventId, indicatorReference: get(data, REFERENCE)[0] as string | undefined, - indicatorProvider: get(data, PROVIDER)[0] as string | undefined, + feedName: get(data, FEED_NAME)[0] as string | undefined, indicatorType: get(data, MATCHED_TYPE)[0] as string | undefined, isDraggable, sourceField: get(data, MATCHED_FIELD)[0] as string, @@ -59,7 +59,7 @@ export const ThreatMatchRow = ({ export const ThreatMatchRowView = ({ contextId, eventId, - indicatorProvider, + feedName, indicatorReference, indicatorType, isDraggable, @@ -86,7 +86,7 @@ export const ThreatMatchRowView = ({ ({ DefaultDraggable: () =>
, })); +jest.mock('../../../../store/timeline', () => { + const original = jest.requireActual('../../../../store/timeline'); + return { + ...original, + timelineActions: { + ...original.timelineActions, + toggleDetailPanel: jest.fn(), + }, + }; +}); + describe('HostName', () => { const props = { fieldName: 'host.name', @@ -49,16 +60,13 @@ describe('HostName', () => { value: 'Mock Host', }; - let toggleDetailPanel: jest.SpyInstance; let toggleExpandedDetail: jest.SpyInstance; beforeAll(() => { - toggleDetailPanel = jest.spyOn(timelineActions, 'toggleDetailPanel'); toggleExpandedDetail = jest.spyOn(activeTimeline, 'toggleExpandedDetail'); }); afterEach(() => { - toggleDetailPanel.mockClear(); toggleExpandedDetail.mockClear(); }); test('should render host name', () => { @@ -96,7 +104,7 @@ describe('HostName', () => { wrapper.find('[data-test-subj="host-details-button"]').first().simulate('click'); await waitFor(() => { - expect(toggleDetailPanel).not.toHaveBeenCalled(); + expect(timelineActions.toggleDetailPanel).not.toHaveBeenCalled(); expect(toggleExpandedDetail).not.toHaveBeenCalled(); }); }); @@ -118,7 +126,7 @@ describe('HostName', () => { wrapper.find('[data-test-subj="host-details-button"]').first().simulate('click'); await waitFor(() => { - expect(toggleDetailPanel).toHaveBeenCalledWith({ + expect(timelineActions.toggleDetailPanel).toHaveBeenCalledWith({ panelView: 'hostDetail', params: { hostName: props.value, @@ -172,7 +180,7 @@ describe('HostName', () => { wrapper.find('[data-test-subj="host-details-button"]').first().simulate('click'); await waitFor(() => { - expect(toggleDetailPanel).toHaveBeenCalledWith({ + expect(timelineActions.toggleDetailPanel).toHaveBeenCalledWith({ panelView: 'hostDetail', params: { hostName: props.value, diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts index 2264e62d6bed7..fc13d163c1883 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/actions.ts @@ -24,9 +24,7 @@ import type { TimelinePersistInput, SerializedFilterQuery, } from '../../../../common/types/timeline'; -import { tGridActions } from '../../../../../timelines/public'; -import { ResolveTimelineConfig } from '../../components/open_timeline/types'; -export const { +export { applyDeltaToColumnWidth, clearEventsDeleted, clearEventsLoading, @@ -46,7 +44,8 @@ export const { updateItemsPerPageOptions, updateSort, upsertColumn, -} = tGridActions; +} from '../../../../../timelines/public'; +import { ResolveTimelineConfig } from '../../components/open_timeline/types'; const actionCreator = actionCreatorFactory('x-pack/security_solution/local/timeline'); diff --git a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts index f46b55bcd3345..0dd898f1c4ab9 100644 --- a/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts +++ b/x-pack/plugins/security_solution/public/timelines/store/timeline/selectors.ts @@ -7,14 +7,12 @@ import { createSelector } from 'reselect'; -import { tGridSelectors } from '../../../../../timelines/public'; +export { getManageTimelineById } from '../../../../../timelines/public'; import { State } from '../../../common/store/types'; import { TimelineModel } from './model'; import { AutoSavedWarningMsg, InsertTimeline, TimelineById } from './types'; -export const { getManageTimelineById } = tGridSelectors; - const selectTimelineById = (state: State): TimelineById => state.timeline.timelineById; const selectAutoSaveMsg = (state: State): AutoSavedWarningMsg => state.timeline.autoSavedWarningMsg; diff --git a/x-pack/plugins/security_solution/server/endpoint/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/mocks.ts index dce08e2522beb..95a1f92ea94cd 100644 --- a/x-pack/plugins/security_solution/server/endpoint/mocks.ts +++ b/x-pack/plugins/security_solution/server/endpoint/mocks.ts @@ -40,6 +40,8 @@ import { createCasesClientMock } from '../../../cases/server/client/mocks'; import { requestContextFactoryMock } from '../request_context_factory.mock'; import { EndpointMetadataService } from './services/metadata'; import { createFleetAuthzMock } from '../../../fleet/common'; +import { createMockClients } from '../lib/detection_engine/routes/__mocks__/request_context'; +import type { EndpointAuthz } from '../../common/endpoint/types/authz'; /** * Creates a mocked EndpointAppContext. @@ -181,9 +183,13 @@ export const createMockMetadataRequestContext = (): jest.Mocked, - savedObjectsClient: jest.Mocked + savedObjectsClient: jest.Mocked, + overrides: { endpointAuthz?: Partial } = {} ) { - const context = requestContextMock.create() as jest.Mocked; + const context = requestContextMock.create( + createMockClients(), + overrides + ) as jest.Mocked; context.core.elasticsearch.client = dataClient; context.core.savedObjects.client = savedObjectsClient; return context; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts index 51f88730eb6fd..9c8decf6b90c2 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/actions/isolation.ts @@ -18,6 +18,7 @@ import { ISOLATE_HOST_ROUTE, UNISOLATE_HOST_ROUTE, failedFleetActionErrorCode, + FORBIDDEN_MESSAGE, } from '../../../../common/endpoint/constants'; import { AGENT_ACTIONS_INDEX } from '../../../../../fleet/common'; import { @@ -105,8 +106,7 @@ export const isolationRequestHandler = function ( if ((!canIsolateHost && isolate) || (!canUnIsolateHost && !isolate)) { return res.forbidden({ body: { - message: - 'You do not have permission to perform this action or license level does not allow for this action', + message: FORBIDDEN_MESSAGE, }, }); } diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts index 06e63c6b7ec59..b79b6985f68ea 100644 --- a/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts +++ b/x-pack/plugins/security_solution/server/endpoint/routes/metadata/handlers.ts @@ -41,6 +41,8 @@ import { GetMetadataListRequestQuery } from '../../../../common/endpoint/schema/ import { ENDPOINT_DEFAULT_PAGE, ENDPOINT_DEFAULT_PAGE_SIZE, + FORBIDDEN_MESSAGE, + METADATA_TRANSFORMS_PATTERN, } from '../../../../common/endpoint/constants'; import { EndpointFleetServicesInterface } from '../../services/endpoint_fleet_services'; @@ -185,6 +187,34 @@ export const getMetadataRequestHandler = function ( }; }; +export function getMetadataTransformStatsHandler( + logger: Logger +): RequestHandler { + return async (context, _, response) => { + const { canAccessEndpointManagement } = context.securitySolution.endpointAuthz; + if (!canAccessEndpointManagement) { + return response.forbidden({ + body: { + message: FORBIDDEN_MESSAGE, + }, + }); + } + + const esClient = context.core.elasticsearch.client.asInternalUser; + try { + const transformStats = await esClient.transform.getTransformStats({ + transform_id: METADATA_TRANSFORMS_PATTERN, + allow_no_match: true, + }); + return response.ok({ + body: transformStats.body, + }); + } catch (error) { + return errorHandler(logger, response, error); + } + }; +} + export async function mapToHostResultList( // eslint-disable-next-line @typescript-eslint/no-explicit-any queryParams: Record, 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 6cd1ae275d592..d5a428638702e 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,17 @@ import { schema } from '@kbn/config-schema'; import { HostStatus } from '../../../../common/endpoint/types'; import { EndpointAppContext } from '../../types'; -import { getLogger, getMetadataRequestHandler, getMetadataListRequestHandler } from './handlers'; +import { + getLogger, + getMetadataRequestHandler, + getMetadataListRequestHandler, + getMetadataTransformStatsHandler, +} from './handlers'; import type { SecuritySolutionPluginRouter } from '../../../types'; import { HOST_METADATA_GET_ROUTE, HOST_METADATA_LIST_ROUTE, + METADATA_TRANSFORMS_STATUS_ROUTE, } from '../../../../common/endpoint/constants'; import { GetMetadataListRequestSchema } from '../../../../common/endpoint/schema/metadata'; @@ -60,4 +66,13 @@ export function registerEndpointRoutes( }, getMetadataRequestHandler(endpointAppContext, logger) ); + + router.get( + { + path: METADATA_TRANSFORMS_STATUS_ROUTE, + validate: false, + options: { authRequired: true, tags: ['access:securitySolution'] }, + }, + getMetadataTransformStatsHandler(logger) + ); } 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 e324f66ad38f6..1050273a5ff75 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 @@ -44,8 +44,10 @@ import { HOST_METADATA_LIST_ROUTE, metadataCurrentIndexPattern, metadataTransformPrefix, + METADATA_TRANSFORMS_STATUS_ROUTE, METADATA_UNITED_INDEX, } from '../../../../common/endpoint/constants'; +import { TRANSFORM_STATES } from '../../../../common/constants'; import type { SecuritySolutionPluginRouter } from '../../../types'; import { AgentNotFoundError, PackagePolicyServiceInterface } from '../../../../../fleet/server'; import { @@ -56,6 +58,8 @@ import { import { EndpointHostNotFoundError } from '../../services/metadata'; import { FleetAgentGenerator } from '../../../../common/endpoint/data_generators/fleet_agent_generator'; import { createMockAgentClient } from '../../../../../fleet/server/mocks'; +import { TransformGetTransformStatsResponse } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { getEndpointAuthzInitialStateMock } from '../../../../common/endpoint/service/authz'; class IndexNotFoundException extends Error { meta: { body: { error: { type: string } } }; @@ -114,54 +118,52 @@ describe('test endpoint routes', () => { perPage: 1000, }); }); + + endpointAppContextService = new EndpointAppContextService(); + mockPackageService = createMockPackageService(); + mockPackageService.getInstallation.mockReturnValue( + Promise.resolve({ + installed_kibana: [], + package_assets: [], + es_index_patterns: {}, + name: '', + version: '', + install_status: 'installed', + install_version: '', + install_started_at: '', + install_source: 'registry', + installed_es: [ + { + id: 'logs-endpoint.events.security', + type: ElasticsearchAssetType.indexTemplate, + }, + { + id: `${metadataTransformPrefix}-0.16.0-dev.0`, + type: ElasticsearchAssetType.transform, + }, + ], + keep_policies_up_to_date: false, + }) + ); + endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); + endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); + mockAgentService = startContract.agentService!; + mockAgentClient = createMockAgentClient(); + mockAgentService.asScoped = () => mockAgentClient; + mockAgentPolicyService = startContract.agentPolicyService!; + + registerEndpointRoutes(routerMock, { + logFactory: loggingSystemMock.create(), + service: endpointAppContextService, + config: () => Promise.resolve(createMockConfig()), + experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), + }); }); + afterEach(() => endpointAppContextService.stop()); + describe('GET list endpoints route', () => { describe('with .metrics-endpoint.metadata_united_default index', () => { - beforeEach(() => { - endpointAppContextService = new EndpointAppContextService(); - mockPackageService = createMockPackageService(); - mockPackageService.getInstallation.mockReturnValue( - Promise.resolve({ - installed_kibana: [], - package_assets: [], - es_index_patterns: {}, - name: '', - version: '', - install_status: 'installed', - install_version: '', - install_started_at: '', - install_source: 'registry', - installed_es: [ - { - id: 'logs-endpoint.events.security', - type: ElasticsearchAssetType.indexTemplate, - }, - { - id: `${metadataTransformPrefix}-0.16.0-dev.0`, - type: ElasticsearchAssetType.transform, - }, - ], - keep_policies_up_to_date: false, - }) - ); - endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); - endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); - mockAgentService = startContract.agentService!; - mockAgentClient = createMockAgentClient(); - mockAgentService.asScoped = () => mockAgentClient; - mockAgentPolicyService = startContract.agentPolicyService!; - - registerEndpointRoutes(routerMock, { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }); - }); - - afterEach(() => endpointAppContextService.stop()); - it('should fallback to legacy index if index not found', async () => { const mockRequest = httpServerMock.createKibanaRequest({ query: { @@ -380,49 +382,6 @@ describe('test endpoint routes', () => { }); describe('with metrics-endpoint.metadata_current_default index', () => { - beforeEach(() => { - endpointAppContextService = new EndpointAppContextService(); - mockPackageService = createMockPackageService(); - mockPackageService.getInstallation.mockReturnValue( - Promise.resolve({ - installed_kibana: [], - package_assets: [], - es_index_patterns: {}, - name: '', - version: '', - install_status: 'installed', - install_version: '', - install_started_at: '', - install_source: 'registry', - installed_es: [ - { - id: 'logs-endpoint.events.security', - type: ElasticsearchAssetType.indexTemplate, - }, - { - id: `${metadataTransformPrefix}-0.16.0-dev.0`, - type: ElasticsearchAssetType.transform, - }, - ], - keep_policies_up_to_date: false, - }) - ); - endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); - endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); - mockAgentService = startContract.agentService!; - mockAgentClient = createMockAgentClient(); - mockAgentService.asScoped = () => mockAgentClient; - - registerEndpointRoutes(routerMock, { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }); - }); - - afterEach(() => endpointAppContextService.stop()); - it('test find the latest of all endpoints', async () => { const mockRequest = httpServerMock.createKibanaRequest({ query: { @@ -611,49 +570,6 @@ describe('test endpoint routes', () => { }); describe('GET endpoint details route', () => { - beforeEach(() => { - endpointAppContextService = new EndpointAppContextService(); - mockPackageService = createMockPackageService(); - mockPackageService.getInstallation.mockReturnValue( - Promise.resolve({ - installed_kibana: [], - package_assets: [], - es_index_patterns: {}, - name: '', - version: '', - install_status: 'installed', - install_version: '', - install_started_at: '', - install_source: 'registry', - installed_es: [ - { - id: 'logs-endpoint.events.security', - type: ElasticsearchAssetType.indexTemplate, - }, - { - id: `${metadataTransformPrefix}-0.16.0-dev.0`, - type: ElasticsearchAssetType.transform, - }, - ], - keep_policies_up_to_date: false, - }) - ); - endpointAppContextService.setup(createMockEndpointAppContextServiceSetupContract()); - endpointAppContextService.start({ ...startContract, packageService: mockPackageService }); - mockAgentService = startContract.agentService!; - mockAgentClient = createMockAgentClient(); - mockAgentService.asScoped = () => mockAgentClient; - - registerEndpointRoutes(routerMock, { - logFactory: loggingSystemMock.create(), - service: endpointAppContextService, - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }); - }); - - afterEach(() => endpointAppContextService.stop()); - it('should return 404 on no results', async () => { const mockRequest = httpServerMock.createKibanaRequest({ params: { id: 'BADID' } }); @@ -821,4 +737,60 @@ describe('test endpoint routes', () => { expect(mockResponse.badRequest).toBeCalled(); }); }); + + describe('GET metadata transform stats route', () => { + it('should get forbidden if no fleet access', async () => { + const mockRequest = httpServerMock.createKibanaRequest(); + + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith(METADATA_TRANSFORMS_STATUS_ROUTE) + )!; + + const contextOverrides = { + endpointAuthz: getEndpointAuthzInitialStateMock({ canAccessEndpointManagement: false }), + }; + await routeHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient, contextOverrides), + mockRequest, + mockResponse + ); + + expect(mockResponse.forbidden).toBeCalled(); + }); + + it('should correctly return metadata transform stats', async () => { + const mockRequest = httpServerMock.createKibanaRequest(); + const expectedResponse = { + count: 1, + transforms: [ + { + id: 'someid', + state: TRANSFORM_STATES.STARTED, + }, + ], + }; + const esClientMock = mockScopedClient.asInternalUser; + (esClientMock.transform.getTransformStats as jest.Mock).mockImplementationOnce(() => + Promise.resolve({ body: expectedResponse }) + ); + [routeConfig, routeHandler] = routerMock.get.mock.calls.find(([{ path }]) => + path.startsWith(METADATA_TRANSFORMS_STATUS_ROUTE) + )!; + await routeHandler( + createRouteHandlerContext(mockScopedClient, mockSavedObjectClient), + mockRequest, + mockResponse + ); + + expect(esClientMock.transform.getTransformStats).toHaveBeenCalledTimes(1); + expect(routeConfig.options).toEqual({ + authRequired: true, + tags: ['access:securitySolution'], + }); + expect(mockResponse.ok).toBeCalled(); + const response = mockResponse.ok.mock.calls[0][0]?.body as TransformGetTransformStatsResponse; + expect(response.count).toEqual(expectedResponse.count); + expect(response.transforms).toEqual(expectedResponse.transforms); + }); + }); }); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/errors.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/errors.ts deleted file mode 100644 index 277b19c46178b..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/errors.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -/* eslint-disable max-classes-per-file */ - -import { NotFoundError } from '../../errors'; - -export class TrustedAppNotFoundError extends NotFoundError { - constructor(id: string) { - super(`Trusted Application (${id}) not found`); - } -} -export class TrustedAppPolicyNotExistsError extends Error { - public readonly type = 'TrustedApps/PolicyNotFound'; - - constructor(name: string, policyIds: string[]) { - super( - `Trusted Application (${name}) is assigned with a policy that no longer exists: ${policyIds.join( - ', ' - )}` - ); - } -} -export class TrustedAppVersionConflictError extends Error { - constructor(id: string, public sourceError: Error) { - super(`Trusted Application (${id}) has been updated since last retrieved`); - } -} diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts deleted file mode 100644 index 614ad4fb548ea..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.test.ts +++ /dev/null @@ -1,673 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { KibanaResponseFactory } from 'kibana/server'; -import { Subject } from 'rxjs'; - -import { xpackMocks } from '../../../fixtures'; -import { loggingSystemMock, httpServerMock } from '../../../../../../../src/core/server/mocks'; -import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { listMock } from '../../../../../lists/server/mocks'; -import { ExceptionListClient } from '../../../../../lists/server'; -import { createMockConfig } from '../../../lib/detection_engine/routes/__mocks__'; - -import { - ConditionEntryField, - NewTrustedApp, - OperatingSystem, - TrustedApp, -} from '../../../../common/endpoint/types'; -import { LicenseService } from '../../../../common/license'; -import { ILicense } from '../../../../../licensing/common/types'; -import { licenseMock } from '../../../../../licensing/common/licensing.mock'; -import { parseExperimentalConfigValue } from '../../../../common/experimental_features'; -import { EndpointAppContextService } from '../../endpoint_app_context_services'; -import { createConditionEntry, createEntryMatch } from './mapping'; -import { - getTrustedAppsCreateRouteHandler, - getTrustedAppsDeleteRouteHandler, - getTrustedAppsGetOneHandler, - getTrustedAppsListRouteHandler, - getTrustedAppsSummaryRouteHandler, - getTrustedAppsUpdateRouteHandler, -} from './handlers'; -import type { SecuritySolutionRequestHandlerContext } from '../../../types'; -import { - TrustedAppNotFoundError, - TrustedAppVersionConflictError, - TrustedAppPolicyNotExistsError, -} from './errors'; -import { updateExceptionListItemImplementationMock } from './test_utils'; -import { Logger } from '@kbn/logging'; -import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; -import { createPackagePolicyServiceMock } from '../../../../../fleet/server/mocks'; -import { - getPackagePoliciesResponse, - getPutTrustedAppByPolicyMock, - getTrustedAppByPolicy, -} from './mocks'; -import { EndpointLicenseError } from '../../errors'; - -const EXCEPTION_LIST_ITEM: ExceptionListItemSchema = { - _version: 'abc123', - id: '123', - comments: [], - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - description: 'Linux trusted app 1', - entries: [ - createEntryMatch('process.executable.caseless', '/bin/malware'), - createEntryMatch('process.hash.md5', '1234234659af249ddf3e40864e9fb241'), - ], - item_id: '123', - list_id: 'endpoint_trusted_apps', - meta: undefined, - name: 'linux trusted app 1', - namespace_type: 'agnostic', - os_types: ['linux'], - tags: ['policy:all'], - type: 'simple', - tie_breaker_id: '123', - updated_at: '2021-01-04T13:55:00.561Z', - updated_by: 'me', -}; - -const NEW_TRUSTED_APP: NewTrustedApp = { - name: 'linux trusted app 1', - description: 'Linux trusted app 1', - os: OperatingSystem.LINUX, - effectScope: { type: 'global' }, - entries: [ - createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware'), - createConditionEntry(ConditionEntryField.HASH, 'match', '1234234659af249ddf3e40864e9fb241'), - ], -}; - -const TRUSTED_APP: TrustedApp = { - id: '123', - version: 'abc123', - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - updated_at: '2021-01-04T13:55:00.561Z', - updated_by: 'me', - name: 'linux trusted app 1', - description: 'Linux trusted app 1', - os: OperatingSystem.LINUX, - effectScope: { type: 'global' }, - entries: [ - createConditionEntry(ConditionEntryField.HASH, 'match', '1234234659af249ddf3e40864e9fb241'), - createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware'), - ], -}; - -const Platinum = licenseMock.createLicense({ license: { type: 'platinum', mode: 'platinum' } }); -const Gold = licenseMock.createLicense({ license: { type: 'gold', mode: 'gold' } }); - -const packagePolicyClient = - createPackagePolicyServiceMock() as jest.Mocked; - -describe('TrustedApps API Handlers', () => { - beforeEach(() => { - packagePolicyClient.getByIDs.mockReset(); - }); - const licenseEmitter: Subject = new Subject(); - const licenseService = new LicenseService(); - licenseService.start(licenseEmitter); - - const createAppContextMock = () => { - const context = { - logFactory: loggingSystemMock.create(), - service: new EndpointAppContextService(), - config: () => Promise.resolve(createMockConfig()), - experimentalFeatures: parseExperimentalConfigValue(createMockConfig().enableExperimental), - }; - - context.service.getPackagePolicyService = () => packagePolicyClient; - context.service.getLicenseService = () => licenseService; - - // Ensure that `logFactory.get()` always returns the same instance for the same given prefix - const instances = new Map>(); - const logFactoryGetMock = context.logFactory.get.getMockImplementation(); - context.logFactory.get.mockImplementation((prefix): Logger => { - if (!instances.has(prefix)) { - instances.set(prefix, logFactoryGetMock!(prefix)!); - } - return instances.get(prefix)!; - }); - - return context; - }; - - let appContextMock: ReturnType = createAppContextMock(); - let exceptionsListClient: jest.Mocked = - listMock.getExceptionListClient() as jest.Mocked; - - const createHandlerContextMock = () => - ({ - ...xpackMocks.createRequestHandlerContext(), - lists: { - getListClient: jest.fn(), - getExceptionListClient: jest.fn().mockReturnValue(exceptionsListClient), - }, - } as unknown as jest.Mocked); - - const assertResponse = ( - response: jest.Mocked, - expectedResponseType: keyof KibanaResponseFactory, - expectedResponseBody: T - ) => { - expect(response[expectedResponseType]).toBeCalled(); - expect(response[expectedResponseType].mock.calls[0][0]?.body).toEqual(expectedResponseBody); - }; - - beforeEach(() => { - appContextMock = createAppContextMock(); - exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked; - licenseEmitter.next(Platinum); - }); - - describe('getTrustedAppsDeleteRouteHandler', () => { - let deleteTrustedAppHandler: ReturnType; - - beforeEach(() => { - deleteTrustedAppHandler = getTrustedAppsDeleteRouteHandler(appContextMock); - }); - - it('should return ok when trusted app deleted', async () => { - const mockResponse = httpServerMock.createResponseFactory(); - - exceptionsListClient.deleteExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); - - await deleteTrustedAppHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ params: { id: '123' } }), - mockResponse - ); - - assertResponse(mockResponse, 'ok', undefined); - }); - - it('should return notFound when trusted app missing', async () => { - const mockResponse = httpServerMock.createResponseFactory(); - - exceptionsListClient.deleteExceptionListItem.mockResolvedValue(null); - exceptionsListClient.getExceptionListItem.mockResolvedValue(null); - - await deleteTrustedAppHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ params: { id: '123' } }), - mockResponse - ); - - assertResponse(mockResponse, 'notFound', new TrustedAppNotFoundError('123')); - }); - - it('should return internalError when errors happen', async () => { - const mockResponse = httpServerMock.createResponseFactory(); - const error = new Error('Something went wrong'); - - exceptionsListClient.deleteExceptionListItem.mockRejectedValue(error); - - await expect( - deleteTrustedAppHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ params: { id: '123' } }), - mockResponse - ) - ).rejects.toThrowError(error); - }); - }); - - describe('getTrustedAppsCreateRouteHandler', () => { - let createTrustedAppHandler: ReturnType; - - beforeEach(() => { - createTrustedAppHandler = getTrustedAppsCreateRouteHandler(appContextMock); - }); - - it('should return ok with body when trusted app created', async () => { - const mockResponse = httpServerMock.createResponseFactory(); - - exceptionsListClient.createExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); - - await createTrustedAppHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }), - mockResponse - ); - - assertResponse(mockResponse, 'ok', { data: TRUSTED_APP }); - }); - - it('should return internalError when errors happen', async () => { - const mockResponse = httpServerMock.createResponseFactory(); - const error = new Error('Something went wrong'); - - exceptionsListClient.createExceptionListItem.mockRejectedValue(error); - - await expect( - createTrustedAppHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }), - mockResponse - ) - ).rejects.toThrowError(error); - }); - - it("should return error when policy doesn't exists", async () => { - const mockResponse = httpServerMock.createResponseFactory(); - packagePolicyClient.getByIDs.mockReset(); - packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse()); - - const trustedAppByPolicy = getTrustedAppByPolicy(); - await createTrustedAppHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ body: trustedAppByPolicy }), - mockResponse - ); - - const error = new TrustedAppPolicyNotExistsError(trustedAppByPolicy.name, [ - '9da95be9-9bee-4761-a8c4-28d6d9bd8c71', - ]); - - expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error); - expect(mockResponse.badRequest).toHaveBeenCalledWith({ - body: { message: error.message, attributes: { type: error.type } }, - }); - }); - - it('should return error when license under platinum and by policy', async () => { - licenseEmitter.next(Gold); - const mockResponse = httpServerMock.createResponseFactory(); - packagePolicyClient.getByIDs.mockReset(); - packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse()); - - const trustedAppByPolicy = getTrustedAppByPolicy(); - await createTrustedAppHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ body: trustedAppByPolicy }), - mockResponse - ); - - const error = new EndpointLicenseError(); - - expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error); - expect(mockResponse.badRequest).toHaveBeenCalledWith({ - body: { message: error.message, attributes: { type: error.name } }, - }); - }); - }); - - describe('getTrustedAppsListRouteHandler', () => { - let getTrustedAppsListHandler: ReturnType; - - beforeEach(() => { - getTrustedAppsListHandler = getTrustedAppsListRouteHandler(appContextMock); - }); - - it('should return ok with list when no errors', async () => { - const mockResponse = httpServerMock.createResponseFactory(); - - exceptionsListClient.findExceptionListItem.mockResolvedValue({ - data: [EXCEPTION_LIST_ITEM], - page: 1, - per_page: 20, - total: 100, - }); - - await getTrustedAppsListHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ query: { page: 1, per_page: 20 } }), - mockResponse - ); - - assertResponse(mockResponse, 'ok', { - data: [TRUSTED_APP], - page: 1, - per_page: 20, - total: 100, - }); - }); - - it('should return internalError when errors happen', async () => { - const mockResponse = httpServerMock.createResponseFactory(); - const error = new Error('Something went wrong'); - - exceptionsListClient.findExceptionListItem.mockRejectedValue(error); - - await expect( - getTrustedAppsListHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ body: NEW_TRUSTED_APP }), - mockResponse - ) - ).rejects.toThrowError(error); - }); - - it('should pass all params to the service', async () => { - const mockResponse = httpServerMock.createResponseFactory(); - - exceptionsListClient.findExceptionListItem.mockResolvedValue({ - data: [EXCEPTION_LIST_ITEM], - page: 5, - per_page: 13, - total: 100, - }); - - const requestContext = createHandlerContextMock(); - - await getTrustedAppsListHandler( - requestContext, - httpServerMock.createKibanaRequest({ - query: { page: 5, per_page: 13, kuery: 'some-param.key: value' }, - }), - mockResponse - ); - - expect(exceptionsListClient.findExceptionListItem).toHaveBeenCalledWith( - expect.objectContaining({ filter: 'some-param.key: value', page: 5, perPage: 13 }) - ); - }); - }); - - describe('getTrustedAppsSummaryHandler', () => { - let getTrustedAppsSummaryHandler: ReturnType; - const getExceptionsListClientMokcResolvedValue = () => { - exceptionsListClient.findExceptionListItem.mockResolvedValue({ - data: [ - // Linux === 5 - ...Array.from({ length: 5 }, () => { - return { - ...EXCEPTION_LIST_ITEM, - }; - }), - // macos === 3 - ...Array.from({ length: 3 }, () => { - return { - ...EXCEPTION_LIST_ITEM, - os_types: ['macos'] as ExceptionListItemSchema['os_types'], - }; - }), - - // windows === 15 - ...Array.from({ length: 15 }, () => { - return { - ...EXCEPTION_LIST_ITEM, - os_types: ['windows'] as ExceptionListItemSchema['os_types'], - }; - }), - ], - page: 1, - per_page: 100, - total: 23, - }); - }; - - beforeEach(() => { - getTrustedAppsSummaryHandler = getTrustedAppsSummaryRouteHandler(appContextMock); - }); - - it('should return ok with list when no errors', async () => { - const mockResponse = httpServerMock.createResponseFactory(); - - getExceptionsListClientMokcResolvedValue(); - - await getTrustedAppsSummaryHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest(), - mockResponse - ); - - assertResponse(mockResponse, 'ok', { - linux: 5, - macos: 3, - windows: 15, - total: 23, - }); - }); - - it('should return ok with list when no errors filtering by policyId', async () => { - const mockResponse = httpServerMock.createResponseFactory(); - - const policyId = 'caf1a334-53f3-4be9-814d-a32245f43d34'; - - getExceptionsListClientMokcResolvedValue(); - - await getTrustedAppsSummaryHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ - query: { - kuery: `exception-list-agnostic.attributes.tags:"policy:${policyId}" OR exception-list-agnostic.attributes.tags:"policy:all"`, - }, - }), - mockResponse - ); - - assertResponse(mockResponse, 'ok', { - linux: 5, - macos: 3, - windows: 15, - total: 23, - }); - }); - - it('should return internalError when errors happen', async () => { - const mockResponse = httpServerMock.createResponseFactory(); - const error = new Error('Something went wrong'); - - exceptionsListClient.findExceptionListItem.mockRejectedValue(error); - - await expect( - getTrustedAppsSummaryHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest(), - mockResponse - ) - ).rejects.toThrowError(error); - }); - }); - - describe('getTrustedAppsGetOneHandler', () => { - let getOneHandler: ReturnType; - - beforeEach(() => { - getOneHandler = getTrustedAppsGetOneHandler(appContextMock); - }); - - it('should return single trusted app', async () => { - const mockResponse = httpServerMock.createResponseFactory(); - - exceptionsListClient.getExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); - - await getOneHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ query: { page: 1, per_page: 20 } }), - mockResponse - ); - - assertResponse(mockResponse, 'ok', { - data: TRUSTED_APP, - }); - }); - - it('should return 404 if trusted app does not exist', async () => { - const mockResponse = httpServerMock.createResponseFactory(); - - exceptionsListClient.getExceptionListItem.mockResolvedValue(null); - - await getOneHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ query: { page: 1, per_page: 20 } }), - mockResponse - ); - - assertResponse(mockResponse, 'notFound', expect.any(TrustedAppNotFoundError)); - }); - - it.each([ - [new TrustedAppNotFoundError('123')], - [new TrustedAppVersionConflictError('123', new Error('some conflict error'))], - ])('should log error: %s', async (error) => { - const mockResponse = httpServerMock.createResponseFactory(); - exceptionsListClient.getExceptionListItem.mockImplementation(async () => { - throw error; - }); - - await getOneHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ query: { page: 1, per_page: 20 } }), - mockResponse - ); - - expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith(error); - }); - }); - - describe('getTrustedAppsUpdateRouteHandler', () => { - let updateHandler: ReturnType; - let mockResponse: ReturnType; - - beforeEach(() => { - updateHandler = getTrustedAppsUpdateRouteHandler(appContextMock); - mockResponse = httpServerMock.createResponseFactory(); - }); - - it('should return success with updated trusted app', async () => { - exceptionsListClient.getExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); - exceptionsListClient.updateExceptionListItem.mockImplementationOnce( - updateExceptionListItemImplementationMock - ); - - await updateHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ params: { id: '123' }, body: NEW_TRUSTED_APP }), - mockResponse - ); - - expect(mockResponse.ok).toHaveBeenCalledWith({ - body: { - data: { - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - updated_at: '11/11/2011T11:11:11.111', - updated_by: 'admin', - description: 'Linux trusted app 1', - effectScope: { - type: 'global', - }, - entries: [ - { - field: 'process.hash.*', - operator: 'included', - type: 'match', - value: '1234234659af249ddf3e40864e9fb241', - }, - { - field: 'process.executable.caseless', - operator: 'included', - type: 'match', - value: '/bin/malware', - }, - ], - id: '123', - name: 'linux trusted app 1', - os: 'linux', - version: 'abc123', - }, - }, - }); - }); - - it('should return 404 if trusted app does not exist', async () => { - exceptionsListClient.getExceptionListItem.mockResolvedValue(null); - - await updateHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ params: { id: '123' }, body: NEW_TRUSTED_APP }), - mockResponse - ); - - expect(mockResponse.notFound).toHaveBeenCalledWith({ - body: expect.any(TrustedAppNotFoundError), - }); - }); - - it('should should return 409 if version conflict occurs', async () => { - exceptionsListClient.getExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); - exceptionsListClient.updateExceptionListItem.mockRejectedValue( - Object.assign(new Error(), { output: { statusCode: 409 } }) - ); - - await updateHandler( - createHandlerContextMock(), - httpServerMock.createKibanaRequest({ params: { id: '123' }, body: NEW_TRUSTED_APP }), - mockResponse - ); - - expect(mockResponse.conflict).toHaveBeenCalledWith({ - body: expect.any(TrustedAppVersionConflictError), - }); - }); - - it("should return error when policy doesn't exists", async () => { - packagePolicyClient.getByIDs.mockReset(); - packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse()); - - const exceptionByPolicy = getPutTrustedAppByPolicyMock(); - const customExceptionListClient = { - ...exceptionsListClient, - getExceptionListItem: () => exceptionByPolicy, - }; - const handlerContextMock = { - ...xpackMocks.createRequestHandlerContext(), - lists: { - getListClient: jest.fn(), - getExceptionListClient: jest.fn().mockReturnValue(customExceptionListClient), - }, - } as unknown as jest.Mocked; - await updateHandler( - handlerContextMock, - httpServerMock.createKibanaRequest({ body: getTrustedAppByPolicy() }), - mockResponse - ); - - expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith( - new TrustedAppPolicyNotExistsError(exceptionByPolicy.name, [ - '9da95be9-9bee-4761-a8c4-28d6d9bd8c71', - ]) - ); - }); - - it('should return error when license under platinum and by policy', async () => { - licenseEmitter.next(Gold); - packagePolicyClient.getByIDs.mockReset(); - packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse()); - - const exceptionByPolicy = getPutTrustedAppByPolicyMock(); - const customExceptionListClient = { - ...exceptionsListClient, - getExceptionListItem: () => exceptionByPolicy, - }; - const handlerContextMock = { - ...xpackMocks.createRequestHandlerContext(), - lists: { - getListClient: jest.fn(), - getExceptionListClient: jest.fn().mockReturnValue(customExceptionListClient), - }, - } as unknown as jest.Mocked; - await updateHandler( - handlerContextMock, - httpServerMock.createKibanaRequest({ body: getTrustedAppByPolicy() }), - mockResponse - ); - - expect(appContextMock.logFactory.get('trusted_apps').error).toHaveBeenCalledWith( - new EndpointLicenseError() - ); - }); - }); -}); 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 deleted file mode 100644 index b02b9d5430cad..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/handlers.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { KibanaResponseFactory, RequestHandler, IKibanaResponse, Logger } from 'kibana/server'; -import type { SecuritySolutionRequestHandlerContext } from '../../../types'; - -import { ExceptionListClient } from '../../../../../lists/server'; - -import { - DeleteTrustedAppsRequestParams, - GetOneTrustedAppRequestParams, - GetTrustedAppsListRequest, - PostTrustedAppCreateRequest, - PutTrustedAppsRequestParams, - PutTrustedAppUpdateRequest, - GetTrustedAppsSummaryRequest, -} from '../../../../common/endpoint/types'; -import { EndpointAppContext } from '../../types'; - -import { - createTrustedApp, - deleteTrustedApp, - getTrustedApp, - getTrustedAppsList, - getTrustedAppsSummary, - updateTrustedApp, -} from './service'; -import { - TrustedAppNotFoundError, - TrustedAppVersionConflictError, - TrustedAppPolicyNotExistsError, -} from './errors'; -import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; -import { EndpointLicenseError } from '../../errors'; - -const getBodyAfterFeatureFlagCheck = ( - body: PutTrustedAppUpdateRequest | PostTrustedAppCreateRequest, - endpointAppContext: EndpointAppContext -): PutTrustedAppUpdateRequest | PostTrustedAppCreateRequest => { - const isTrustedAppsByPolicyEnabled = - endpointAppContext.experimentalFeatures.trustedAppsByPolicyEnabled; - return { - ...body, - ...(isTrustedAppsByPolicyEnabled ? body.effectScope : { effectSctope: { type: 'policy:all' } }), - }; -}; - -const exceptionListClientFromContext = ( - context: SecuritySolutionRequestHandlerContext -): ExceptionListClient => { - const exceptionLists = context.lists?.getExceptionListClient(); - - if (!exceptionLists) { - throw new Error('Exception List client not found'); - } - - return exceptionLists; -}; - -const packagePolicyClientFromEndpointContext = ( - endpointAppContext: EndpointAppContext -): PackagePolicyServiceInterface => { - const packagePolicy = endpointAppContext.service.getPackagePolicyService(); - - if (!packagePolicy) { - throw new Error('Package policy service not found'); - } - - return packagePolicy; -}; - -const errorHandler = ( - logger: Logger, - res: KibanaResponseFactory, - error: E -): IKibanaResponse => { - if (error instanceof TrustedAppNotFoundError) { - logger.error(error); - return res.notFound({ body: error }); - } - - if (error instanceof TrustedAppPolicyNotExistsError) { - logger.error(error); - return res.badRequest({ body: { message: error.message, attributes: { type: error.type } } }); - } - - if (error instanceof EndpointLicenseError) { - logger.error(error); - return res.badRequest({ body: { message: error.message, attributes: { type: error.name } } }); - } - - if (error instanceof TrustedAppVersionConflictError) { - logger.error(error); - return res.conflict({ body: error }); - } - - // Kibana will take care of `500` errors when the handler `throw`'s, including logging the error - throw error; -}; - -export const getTrustedAppsDeleteRouteHandler = ( - endpointAppContext: EndpointAppContext -): RequestHandler< - DeleteTrustedAppsRequestParams, - unknown, - unknown, - SecuritySolutionRequestHandlerContext -> => { - const logger = endpointAppContext.logFactory.get('trusted_apps'); - - return async (context, req, res) => { - try { - await deleteTrustedApp(exceptionListClientFromContext(context), req.params); - - return res.ok(); - } catch (error) { - return errorHandler(logger, res, error); - } - }; -}; - -export const getTrustedAppsGetOneHandler = ( - endpointAppContext: EndpointAppContext -): RequestHandler< - GetOneTrustedAppRequestParams, - unknown, - unknown, - SecuritySolutionRequestHandlerContext -> => { - const logger = endpointAppContext.logFactory.get('trusted_apps'); - - return async (context, req, res) => { - try { - return res.ok({ - body: await getTrustedApp(exceptionListClientFromContext(context), req.params.id), - }); - } catch (error) { - return errorHandler(logger, res, error); - } - }; -}; - -export const getTrustedAppsListRouteHandler = ( - endpointAppContext: EndpointAppContext -): RequestHandler< - unknown, - GetTrustedAppsListRequest, - unknown, - SecuritySolutionRequestHandlerContext -> => { - const logger = endpointAppContext.logFactory.get('trusted_apps'); - - return async (context, req, res) => { - try { - return res.ok({ - body: await getTrustedAppsList(exceptionListClientFromContext(context), req.query), - }); - } catch (error) { - return errorHandler(logger, res, error); - } - }; -}; - -export const getTrustedAppsCreateRouteHandler = ( - endpointAppContext: EndpointAppContext -): RequestHandler< - unknown, - unknown, - PostTrustedAppCreateRequest, - SecuritySolutionRequestHandlerContext -> => { - const logger = endpointAppContext.logFactory.get('trusted_apps'); - return async (context, req, res) => { - try { - const body = getBodyAfterFeatureFlagCheck(req.body, endpointAppContext); - - return res.ok({ - body: await createTrustedApp( - exceptionListClientFromContext(context), - context.core.savedObjects.client, - packagePolicyClientFromEndpointContext(endpointAppContext), - body, - endpointAppContext.service.getLicenseService().isAtLeast('platinum') - ), - }); - } catch (error) { - return errorHandler(logger, res, error); - } - }; -}; - -export const getTrustedAppsUpdateRouteHandler = ( - endpointAppContext: EndpointAppContext -): RequestHandler< - PutTrustedAppsRequestParams, - unknown, - PutTrustedAppUpdateRequest, - SecuritySolutionRequestHandlerContext -> => { - const logger = endpointAppContext.logFactory.get('trusted_apps'); - - return async (context, req, res) => { - try { - const body = getBodyAfterFeatureFlagCheck(req.body, endpointAppContext); - - return res.ok({ - body: await updateTrustedApp( - exceptionListClientFromContext(context), - context.core.savedObjects.client, - packagePolicyClientFromEndpointContext(endpointAppContext), - req.params.id, - body, - endpointAppContext.service.getLicenseService().isAtLeast('platinum') - ), - }); - } catch (error) { - return errorHandler(logger, res, error); - } - }; -}; - -export const getTrustedAppsSummaryRouteHandler = ( - endpointAppContext: EndpointAppContext -): RequestHandler< - unknown, - GetTrustedAppsSummaryRequest, - unknown, - SecuritySolutionRequestHandlerContext -> => { - const logger = endpointAppContext.logFactory.get('trusted_apps'); - - return async (context, req, res) => { - try { - return res.ok({ - body: await getTrustedAppsSummary(exceptionListClientFromContext(context), req.query), - }); - } catch (error) { - return errorHandler(logger, res, error); - } - }; -}; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts deleted file mode 100644 index d00e1fc555b17..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/index.ts +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -// import { -// DeleteTrustedAppsRequestSchema, -// GetOneTrustedAppRequestSchema, -// GetTrustedAppsRequestSchema, -// PostTrustedAppCreateRequestSchema, -// PutTrustedAppUpdateRequestSchema, -// GetTrustedAppsSummaryRequestSchema, -// } from '../../../../common/endpoint/schema/trusted_apps'; -// import { -// TRUSTED_APPS_CREATE_API, -// TRUSTED_APPS_DELETE_API, -// TRUSTED_APPS_GET_API, -// TRUSTED_APPS_LIST_API, -// TRUSTED_APPS_UPDATE_API, -// TRUSTED_APPS_SUMMARY_API, -// } from '../../../../common/endpoint/constants'; -// -// import { -// getTrustedAppsCreateRouteHandler, -// getTrustedAppsDeleteRouteHandler, -// getTrustedAppsGetOneHandler, -// getTrustedAppsListRouteHandler, -// getTrustedAppsSummaryRouteHandler, -// getTrustedAppsUpdateRouteHandler, -// } from './handlers'; -import { SecuritySolutionPluginRouter } from '../../../types'; -import { EndpointAppContext } from '../../types'; - -export const registerTrustedAppsRoutes = ( - _router: SecuritySolutionPluginRouter, - _endpointAppContext: EndpointAppContext -) => { - // FIXME: DELETE all trusted apps api related modules (#2148) - // // DELETE one - // router.delete( - // { - // path: TRUSTED_APPS_DELETE_API, - // validate: DeleteTrustedAppsRequestSchema, - // options: { authRequired: true }, - // }, - // getTrustedAppsDeleteRouteHandler(endpointAppContext) - // ); - // - // // GET one - // router.get( - // { - // path: TRUSTED_APPS_GET_API, - // validate: GetOneTrustedAppRequestSchema, - // options: { authRequired: true }, - // }, - // getTrustedAppsGetOneHandler(endpointAppContext) - // ); - // - // // GET list - // router.get( - // { - // path: TRUSTED_APPS_LIST_API, - // validate: GetTrustedAppsRequestSchema, - // options: { authRequired: true }, - // }, - // getTrustedAppsListRouteHandler(endpointAppContext) - // ); - // - // // CREATE - // router.post( - // { - // path: TRUSTED_APPS_CREATE_API, - // validate: PostTrustedAppCreateRequestSchema, - // options: { authRequired: true }, - // }, - // getTrustedAppsCreateRouteHandler(endpointAppContext) - // ); - // - // // PUT - // router.put( - // { - // path: TRUSTED_APPS_UPDATE_API, - // validate: PutTrustedAppUpdateRequestSchema, - // options: { authRequired: true }, - // }, - // getTrustedAppsUpdateRouteHandler(endpointAppContext) - // ); - // - // // SUMMARY - // router.get( - // { - // path: TRUSTED_APPS_SUMMARY_API, - // validate: GetTrustedAppsSummaryRequestSchema, - // options: { authRequired: true }, - // }, - // getTrustedAppsSummaryRouteHandler(endpointAppContext) - // ); -}; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts deleted file mode 100644 index fe46277664408..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.test.ts +++ /dev/null @@ -1,587 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { CreateExceptionListItemOptions } from '../../../../../lists/server'; -import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; - -import { - ConditionEntryField, - NewTrustedApp, - OperatingSystem, - TrustedApp, - UpdateTrustedApp, -} from '../../../../common/endpoint/types'; - -import { - createConditionEntry, - createEntryMatch, - createEntryNested, - exceptionListItemToTrustedApp, - newTrustedAppToCreateExceptionListItemOptions, - updatedTrustedAppToUpdateExceptionListItemOptions, -} from './mapping'; - -const createExceptionListItemOptions = ( - options: Partial -): CreateExceptionListItemOptions => ({ - comments: [], - description: '', - entries: [], - itemId: expect.any(String), - listId: 'endpoint_trusted_apps', - meta: undefined, - name: '', - namespaceType: 'agnostic', - osTypes: [], - tags: ['policy:all'], - type: 'simple', - ...options, -}); - -const exceptionListItemSchema = ( - item: Partial -): ExceptionListItemSchema => ({ - _version: 'abc123', - id: '', - comments: [], - created_at: '', - created_by: '', - description: '', - entries: [], - item_id: '123', - list_id: 'endpoint_trusted_apps', - meta: undefined, - name: '', - namespace_type: 'agnostic', - os_types: [], - tags: ['policy:all'], - type: 'simple', - tie_breaker_id: '123', - updated_at: '11/11/2011T11:11:11.111', - updated_by: 'admin', - ...(item || {}), -}); - -describe('mapping', () => { - describe('newTrustedAppToCreateExceptionListItemOptions', () => { - const testMapping = (input: NewTrustedApp, expectedResult: CreateExceptionListItemOptions) => { - expect(newTrustedAppToCreateExceptionListItemOptions(input)).toEqual(expectedResult); - }; - - it('should map linux trusted app condition properly', function () { - testMapping( - { - name: 'linux trusted app', - description: 'Linux Trusted App', - effectScope: { type: 'global' }, - os: OperatingSystem.LINUX, - entries: [createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware')], - }, - createExceptionListItemOptions({ - name: 'linux trusted app', - description: 'Linux Trusted App', - osTypes: ['linux'], - entries: [ - createEntryMatch( - 'process.executable.caseless', - - '/bin/malware' - ), - ], - }) - ); - }); - - it('should map macos trusted app condition properly', function () { - testMapping( - { - name: 'macos trusted app', - description: 'MacOS Trusted App', - effectScope: { type: 'global' }, - os: OperatingSystem.MAC, - entries: [createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware')], - }, - createExceptionListItemOptions({ - name: 'macos trusted app', - description: 'MacOS Trusted App', - osTypes: ['macos'], - entries: [ - createEntryMatch( - 'process.executable.caseless', - - '/bin/malware' - ), - ], - }) - ); - }); - - it('should map windows trusted app condition properly', function () { - testMapping( - { - name: 'windows trusted app', - description: 'Windows Trusted App', - effectScope: { type: 'global' }, - os: OperatingSystem.WINDOWS, - entries: [ - createConditionEntry(ConditionEntryField.PATH, 'match', 'C:\\Program Files\\Malware'), - ], - }, - createExceptionListItemOptions({ - name: 'windows trusted app', - description: 'Windows Trusted App', - osTypes: ['windows'], - entries: [ - createEntryMatch( - 'process.executable.caseless', - - 'C:\\Program Files\\Malware' - ), - ], - }) - ); - }); - - it('should map signer condition properly', function () { - testMapping( - { - name: 'Signed trusted app', - description: 'Signed Trusted App', - effectScope: { type: 'global' }, - os: OperatingSystem.WINDOWS, - entries: [createConditionEntry(ConditionEntryField.SIGNER, 'match', 'Microsoft Windows')], - }, - createExceptionListItemOptions({ - name: 'Signed trusted app', - description: 'Signed Trusted App', - osTypes: ['windows'], - entries: [ - createEntryNested('process.Ext.code_signature', [ - createEntryMatch('trusted', 'true'), - createEntryMatch('subject_name', 'Microsoft Windows'), - ]), - ], - }) - ); - }); - - it('should map MD5 hash condition properly', function () { - testMapping( - { - name: 'MD5 trusted app', - description: 'MD5 Trusted App', - effectScope: { type: 'global' }, - os: OperatingSystem.LINUX, - entries: [ - createConditionEntry( - ConditionEntryField.HASH, - 'match', - '1234234659af249ddf3e40864e9fb241' - ), - ], - }, - createExceptionListItemOptions({ - name: 'MD5 trusted app', - description: 'MD5 Trusted App', - osTypes: ['linux'], - entries: [ - createEntryMatch( - 'process.hash.md5', - - '1234234659af249ddf3e40864e9fb241' - ), - ], - }) - ); - }); - - it('should map SHA1 hash condition properly', function () { - testMapping( - { - name: 'SHA1 trusted app', - description: 'SHA1 Trusted App', - effectScope: { type: 'global' }, - os: OperatingSystem.LINUX, - entries: [ - createConditionEntry( - ConditionEntryField.HASH, - 'match', - 'f635da961234234659af249ddf3e40864e9fb241' - ), - ], - }, - createExceptionListItemOptions({ - name: 'SHA1 trusted app', - description: 'SHA1 Trusted App', - osTypes: ['linux'], - entries: [ - createEntryMatch( - 'process.hash.sha1', - - 'f635da961234234659af249ddf3e40864e9fb241' - ), - ], - }) - ); - }); - - it('should map SHA256 hash condition properly', function () { - testMapping( - { - name: 'SHA256 trusted app', - description: 'SHA256 Trusted App', - effectScope: { type: 'global' }, - os: OperatingSystem.LINUX, - entries: [ - createConditionEntry( - ConditionEntryField.HASH, - 'match', - 'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241' - ), - ], - }, - createExceptionListItemOptions({ - name: 'SHA256 trusted app', - description: 'SHA256 Trusted App', - osTypes: ['linux'], - entries: [ - createEntryMatch( - 'process.hash.sha256', - - 'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241' - ), - ], - }) - ); - }); - - it('should lowercase hash condition value', function () { - testMapping( - { - name: 'MD5 trusted app', - description: 'MD5 Trusted App', - effectScope: { type: 'global' }, - os: OperatingSystem.LINUX, - entries: [ - createConditionEntry( - ConditionEntryField.HASH, - 'match', - '1234234659Af249ddf3e40864E9FB241' - ), - ], - }, - createExceptionListItemOptions({ - name: 'MD5 trusted app', - description: 'MD5 Trusted App', - osTypes: ['linux'], - entries: [ - createEntryMatch( - 'process.hash.md5', - - '1234234659af249ddf3e40864e9fb241' - ), - ], - }) - ); - }); - }); - - describe('exceptionListItemToTrustedApp', () => { - const testMapping = (input: ExceptionListItemSchema, expectedResult: TrustedApp) => { - expect(exceptionListItemToTrustedApp(input)).toEqual(expectedResult); - }; - - it('should map linux exception list item properly', function () { - testMapping( - exceptionListItemSchema({ - id: '123', - name: 'linux trusted app', - description: 'Linux Trusted App', - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - os_types: ['linux'], - entries: [ - createEntryMatch( - 'process.executable.caseless', - - '/bin/malware' - ), - ], - }), - { - id: '123', - version: 'abc123', - name: 'linux trusted app', - description: 'Linux Trusted App', - effectScope: { type: 'global' }, - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - updated_at: '11/11/2011T11:11:11.111', - updated_by: 'admin', - os: OperatingSystem.LINUX, - entries: [createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware')], - } - ); - }); - - it('should map macos exception list item properly', function () { - testMapping( - exceptionListItemSchema({ - id: '123', - name: 'macos trusted app', - description: 'MacOS Trusted App', - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - os_types: ['macos'], - entries: [ - createEntryMatch( - 'process.executable.caseless', - - '/bin/malware' - ), - ], - }), - { - id: '123', - version: 'abc123', - name: 'macos trusted app', - description: 'MacOS Trusted App', - effectScope: { type: 'global' }, - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - updated_at: '11/11/2011T11:11:11.111', - updated_by: 'admin', - os: OperatingSystem.MAC, - entries: [createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware')], - } - ); - }); - - it('should map windows exception list item properly', function () { - testMapping( - exceptionListItemSchema({ - id: '123', - name: 'windows trusted app', - description: 'Windows Trusted App', - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - os_types: ['windows'], - entries: [ - createEntryMatch( - 'process.executable.caseless', - - 'C:\\Program Files\\Malware' - ), - ], - }), - { - id: '123', - version: 'abc123', - name: 'windows trusted app', - description: 'Windows Trusted App', - effectScope: { type: 'global' }, - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - updated_at: '11/11/2011T11:11:11.111', - updated_by: 'admin', - os: OperatingSystem.WINDOWS, - entries: [ - createConditionEntry(ConditionEntryField.PATH, 'match', 'C:\\Program Files\\Malware'), - ], - } - ); - }); - - it('should map exception list item containing signer entry match properly', function () { - testMapping( - exceptionListItemSchema({ - id: '123', - name: 'signed trusted app', - description: 'Signed trusted app', - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - os_types: ['windows'], - entries: [ - createEntryNested('process.Ext.code_signature', [ - createEntryMatch('trusted', 'true'), - createEntryMatch('subject_name', 'Microsoft Windows'), - ]), - ], - }), - { - id: '123', - version: 'abc123', - name: 'signed trusted app', - description: 'Signed trusted app', - effectScope: { type: 'global' }, - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - updated_at: '11/11/2011T11:11:11.111', - updated_by: 'admin', - os: OperatingSystem.WINDOWS, - entries: [createConditionEntry(ConditionEntryField.SIGNER, 'match', 'Microsoft Windows')], - } - ); - }); - - it('should map exception list item containing MD5 hash entry match properly', function () { - testMapping( - exceptionListItemSchema({ - id: '123', - name: 'MD5 trusted app', - description: 'MD5 Trusted App', - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - os_types: ['linux'], - entries: [ - createEntryMatch( - 'process.hash.md5', - - '1234234659af249ddf3e40864e9fb241' - ), - ], - }), - { - id: '123', - version: 'abc123', - name: 'MD5 trusted app', - description: 'MD5 Trusted App', - effectScope: { type: 'global' }, - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - updated_at: '11/11/2011T11:11:11.111', - updated_by: 'admin', - os: OperatingSystem.LINUX, - entries: [ - createConditionEntry( - ConditionEntryField.HASH, - 'match', - '1234234659af249ddf3e40864e9fb241' - ), - ], - } - ); - }); - - it('should map exception list item containing SHA1 hash entry match properly', function () { - testMapping( - exceptionListItemSchema({ - id: '123', - name: 'SHA1 trusted app', - description: 'SHA1 Trusted App', - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - os_types: ['linux'], - entries: [ - createEntryMatch( - 'process.hash.sha1', - - 'f635da961234234659af249ddf3e40864e9fb241' - ), - ], - }), - { - id: '123', - version: 'abc123', - name: 'SHA1 trusted app', - description: 'SHA1 Trusted App', - effectScope: { type: 'global' }, - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - updated_at: '11/11/2011T11:11:11.111', - updated_by: 'admin', - os: OperatingSystem.LINUX, - entries: [ - createConditionEntry( - ConditionEntryField.HASH, - 'match', - 'f635da961234234659af249ddf3e40864e9fb241' - ), - ], - } - ); - }); - - it('should map exception list item containing SHA256 hash entry match properly', function () { - testMapping( - exceptionListItemSchema({ - id: '123', - name: 'SHA256 trusted app', - description: 'SHA256 Trusted App', - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - os_types: ['linux'], - entries: [ - createEntryMatch( - 'process.hash.sha256', - - 'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241' - ), - ], - }), - { - id: '123', - version: 'abc123', - name: 'SHA256 trusted app', - description: 'SHA256 Trusted App', - effectScope: { type: 'global' }, - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - updated_at: '11/11/2011T11:11:11.111', - updated_by: 'admin', - os: OperatingSystem.LINUX, - entries: [ - createConditionEntry( - ConditionEntryField.HASH, - 'match', - 'f635da96124659af249ddf3e40864e9fb234234659af249ddf3e40864e9fb241' - ), - ], - } - ); - }); - }); - - describe('updatedTrustedAppToUpdateExceptionListItemOptions', () => { - it('should map to UpdateExceptionListItemOptions', () => { - const updatedTrustedApp: UpdateTrustedApp = { - name: 'Linux trusted app', - description: 'Linux Trusted App', - effectScope: { type: 'global' }, - os: OperatingSystem.LINUX, - entries: [createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware')], - version: 'abc', - }; - - expect( - updatedTrustedAppToUpdateExceptionListItemOptions( - exceptionListItemSchema({ id: 'original-id-here', item_id: 'original-item-id-here' }), - updatedTrustedApp - ) - ).toEqual({ - _version: 'abc', - comments: [], - description: 'Linux Trusted App', - entries: [ - { - field: 'process.executable.caseless', - operator: 'included', - type: 'match', - value: '/bin/malware', - }, - ], - id: 'original-id-here', - itemId: 'original-item-id-here', - name: 'Linux trusted app', - namespaceType: 'agnostic', - osTypes: ['linux'], - tags: ['policy:all'], - type: 'simple', - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts deleted file mode 100644 index 08c1a3a809d4a..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mapping.ts +++ /dev/null @@ -1,274 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import uuid from 'uuid'; - -import type { - EntriesArray, - EntryMatch, - EntryMatchWildcard, - EntryNested, - ExceptionListItemSchema, - NestedEntriesArray, - OsType, -} from '@kbn/securitysolution-io-ts-list-types'; - -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; -import type { - CreateExceptionListItemOptions, - UpdateExceptionListItemOptions, -} from '../../../../../lists/server'; -import { - ConditionEntry, - ConditionEntryField, - EffectScope, - NewTrustedApp, - OperatingSystem, - TrustedApp, - TrustedAppEntryTypes, - UpdateTrustedApp, -} from '../../../../common/endpoint/types'; -import { - POLICY_REFERENCE_PREFIX, - tagsToEffectScope, -} from '../../../../common/endpoint/service/trusted_apps/mapping'; - -type ConditionEntriesMap = { [K in ConditionEntryField]?: ConditionEntry }; -type Mapping = { [K in T]: U }; - -const OS_TYPE_TO_OPERATING_SYSTEM: Mapping = { - linux: OperatingSystem.LINUX, - macos: OperatingSystem.MAC, - windows: OperatingSystem.WINDOWS, -}; - -const OPERATING_SYSTEM_TO_OS_TYPE: Mapping = { - [OperatingSystem.LINUX]: 'linux', - [OperatingSystem.MAC]: 'macos', - [OperatingSystem.WINDOWS]: 'windows', -}; - -const OPERATOR_VALUE = 'included'; - -const filterUndefined = (list: Array): T[] => { - return list.filter((item: T | undefined): item is T => item !== undefined); -}; - -export const createConditionEntry = ( - field: T, - type: TrustedAppEntryTypes, - value: string -): ConditionEntry => { - return { field, value, type, operator: OPERATOR_VALUE }; -}; - -export const entriesToConditionEntriesMap = (entries: EntriesArray): ConditionEntriesMap => { - return entries.reduce((result, entry) => { - if (entry.field.startsWith('process.hash') && entry.type === 'match') { - return { - ...result, - [ConditionEntryField.HASH]: createConditionEntry( - ConditionEntryField.HASH, - entry.type, - entry.value - ), - }; - } else if ( - entry.field === 'process.executable.caseless' && - (entry.type === 'match' || entry.type === 'wildcard') - ) { - return { - ...result, - [ConditionEntryField.PATH]: createConditionEntry( - ConditionEntryField.PATH, - entry.type, - entry.value - ), - }; - } else if (entry.field === 'process.Ext.code_signature' && entry.type === 'nested') { - const subjectNameCondition = entry.entries.find((subEntry): subEntry is EntryMatch => { - return subEntry.field === 'subject_name' && subEntry.type === 'match'; - }); - - if (subjectNameCondition) { - return { - ...result, - [ConditionEntryField.SIGNER]: createConditionEntry( - ConditionEntryField.SIGNER, - subjectNameCondition.type, - subjectNameCondition.value - ), - }; - } - } - - return result; - }, {} as ConditionEntriesMap); -}; - -/** - * Map an ExceptionListItem to a TrustedApp item - * @param exceptionListItem - */ -export const exceptionListItemToTrustedApp = ( - exceptionListItem: ExceptionListItemSchema -): TrustedApp => { - if (exceptionListItem.os_types[0]) { - const os = osFromExceptionItem(exceptionListItem); - const grouped = entriesToConditionEntriesMap(exceptionListItem.entries); - - return { - id: exceptionListItem.item_id, - version: exceptionListItem._version || '', - name: exceptionListItem.name, - description: exceptionListItem.description, - effectScope: tagsToEffectScope(exceptionListItem.tags), - created_at: exceptionListItem.created_at, - created_by: exceptionListItem.created_by, - updated_at: exceptionListItem.updated_at, - updated_by: exceptionListItem.updated_by, - ...(os === OperatingSystem.LINUX || os === OperatingSystem.MAC - ? { - os, - entries: filterUndefined([ - grouped[ConditionEntryField.HASH], - grouped[ConditionEntryField.PATH], - ]), - } - : { - os, - entries: filterUndefined([ - grouped[ConditionEntryField.HASH], - grouped[ConditionEntryField.PATH], - grouped[ConditionEntryField.SIGNER], - ]), - }), - }; - } else { - throw new Error('Unknown Operating System assigned to trusted application.'); - } -}; - -export const osFromExceptionItem = ( - exceptionListItem: ExceptionListItemSchema -): TrustedApp['os'] => { - return OS_TYPE_TO_OPERATING_SYSTEM[exceptionListItem.os_types[0]]; -}; - -const hashType = (hash: string): 'md5' | 'sha256' | 'sha1' | undefined => { - switch (hash.length) { - case 32: - return 'md5'; - case 40: - return 'sha1'; - case 64: - return 'sha256'; - } -}; - -export const createEntryMatch = (field: string, value: string): EntryMatch => { - return { field, value, type: 'match', operator: OPERATOR_VALUE }; -}; - -export const createEntryMatchWildcard = (field: string, value: string): EntryMatchWildcard => { - return { field, value, type: 'wildcard', operator: OPERATOR_VALUE }; -}; - -export const createEntryNested = (field: string, entries: NestedEntriesArray): EntryNested => { - return { field, entries, type: 'nested' }; -}; - -export const effectScopeToTags = (effectScope: EffectScope) => { - if (effectScope.type === 'policy') { - return effectScope.policies.map((policy) => `${POLICY_REFERENCE_PREFIX}${policy}`); - } else { - return [`${POLICY_REFERENCE_PREFIX}all`]; - } -}; - -export const conditionEntriesToEntries = (conditionEntries: ConditionEntry[]): EntriesArray => { - return conditionEntries.map((conditionEntry) => { - if (conditionEntry.field === ConditionEntryField.HASH) { - return createEntryMatch( - `process.hash.${hashType(conditionEntry.value)}`, - conditionEntry.value.toLowerCase() - ); - } else if (conditionEntry.field === ConditionEntryField.SIGNER) { - return createEntryNested(`process.Ext.code_signature`, [ - createEntryMatch('trusted', 'true'), - createEntryMatch('subject_name', conditionEntry.value), - ]); - } else if ( - conditionEntry.field === ConditionEntryField.PATH && - conditionEntry.type === 'wildcard' - ) { - return createEntryMatchWildcard(`process.executable.caseless`, conditionEntry.value); - } else { - return createEntryMatch(`process.executable.caseless`, conditionEntry.value); - } - }); -}; - -/** - * Map NewTrustedApp to CreateExceptionListItemOptions. - */ -export const newTrustedAppToCreateExceptionListItemOptions = ({ - os, - entries, - name, - description = '', - effectScope, -}: NewTrustedApp): CreateExceptionListItemOptions => { - return { - comments: [], - description, - entries: conditionEntriesToEntries(entries), - itemId: uuid.v4(), - listId: ENDPOINT_TRUSTED_APPS_LIST_ID, - meta: undefined, - name, - namespaceType: 'agnostic', - osTypes: [OPERATING_SYSTEM_TO_OS_TYPE[os]], - tags: effectScopeToTags(effectScope), - type: 'simple', - }; -}; - -/** - * Map UpdateTrustedApp to UpdateExceptionListItemOptions - * - * @param {ExceptionListItemSchema} currentTrustedAppExceptionItem - * @param {UpdateTrustedApp} updatedTrustedApp - */ -export const updatedTrustedAppToUpdateExceptionListItemOptions = ( - { - id, - item_id: itemId, - namespace_type: namespaceType, - type, - comments, - meta, - }: ExceptionListItemSchema, - { os, entries, name, description = '', effectScope, version }: UpdateTrustedApp -): UpdateExceptionListItemOptions => { - return { - _version: version, - name, - description, - entries: conditionEntriesToEntries(entries), - osTypes: [OPERATING_SYSTEM_TO_OS_TYPE[os]], - tags: effectScopeToTags(effectScope), - - // Copied from current trusted app exception item - id, - comments, - itemId, - meta, - namespaceType, - type, - }; -}; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mocks.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mocks.ts deleted file mode 100644 index dd871b7811066..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/mocks.ts +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { PackagePolicy } from '../../../../../fleet/common'; - -import { - ConditionEntryField, - OperatingSystem, - TrustedApp, -} from '../../../../common/endpoint/types'; -import { createConditionEntry } from './mapping'; - -export const getTrustedAppByPolicy = function (): TrustedApp { - return { - id: '123', - version: 'abc123', - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - updated_at: '11/11/2011T11:11:11.111', - updated_by: 'admin', - name: 'linux trusted app 1', - description: 'Linux trusted app 1', - os: OperatingSystem.LINUX, - effectScope: { - type: 'policy', - policies: ['e5cbb9cf-98aa-4303-a04b-6a1165915079', '9da95be9-9bee-4761-a8c4-28d6d9bd8c71'], - }, - entries: [ - createConditionEntry(ConditionEntryField.HASH, 'match', '1234234659af249ddf3e40864e9fb241'), - createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware'), - ], - }; -}; - -export const getPutTrustedAppByPolicyMock = function (): ExceptionListItemSchema { - return { - id: '123', - _version: '1', - comments: [], - namespace_type: 'agnostic', - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - updated_at: '11/11/2011T11:11:11.111', - updated_by: 'admin', - name: 'linux trusted app 1', - description: 'Linux trusted app 1', - os_types: [OperatingSystem.LINUX], - tags: ['policy:9da95be9-9bee-4761-a8c4-28d6d9bd8c71'], - entries: [ - createConditionEntry(ConditionEntryField.HASH, 'match', '1234234659af249ddf3e40864e9fb241'), - createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware'), - ], - item_id: '1', - list_id: '1', - meta: undefined, - tie_breaker_id: '1', - type: 'simple', - }; -}; - -export const getPackagePoliciesResponse = function (): PackagePolicy[] { - return [ - // Next line is ts-ignored as this is the response when the policy doesn't exists but the type is complaining about it. - // @ts-expect-error TS2740 - { id: '9da95be9-9bee-4761-a8c4-28d6d9bd8c71', version: undefined }, - { - id: 'e5cbb9cf-98aa-4303-a04b-6a1165915079', - version: 'Wzc0NDk5LDFd', - name: 'EI 2', - description: '', - namespace: 'default', - policy_id: '9fd2ac50-e86f-11eb-a87f-51e16104076a', - enabled: true, - output_id: '', - inputs: [], - package: { name: 'endpoint', title: 'Endpoint Security', version: '0.20.1' }, - revision: 3, - created_at: '2021-07-19T09:00:45.608Z', - created_by: 'elastic', - updated_at: '2021-07-19T09:02:47.193Z', - updated_by: 'system', - }, - ]; -}; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts deleted file mode 100644 index c57416ff1c974..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.test.ts +++ /dev/null @@ -1,513 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { listMock } from '../../../../../lists/server/mocks'; -import { createPackagePolicyServiceMock } from '../../../../../fleet/server/mocks'; -import { savedObjectsClientMock } from '../../../../../../../src/core/server/mocks'; -import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; -import type { SavedObjectsClientContract } from 'kibana/server'; -import { ExceptionListClient } from '../../../../../lists/server'; -import { - ConditionEntryField, - MaybeImmutable, - OperatingSystem, - TrustedApp, -} from '../../../../common/endpoint/types'; -import { createConditionEntry, createEntryMatch } from './mapping'; -import { - createTrustedApp, - deleteTrustedApp, - getTrustedApp, - getTrustedAppsList, - getTrustedAppsSummary, - updateTrustedApp, -} from './service'; -import { - TrustedAppNotFoundError, - TrustedAppVersionConflictError, - TrustedAppPolicyNotExistsError, -} from './errors'; -import { toUpdateTrustedApp } from '../../../../common/endpoint/service/trusted_apps/to_update_trusted_app'; -import { updateExceptionListItemImplementationMock } from './test_utils'; -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; -import { getPackagePoliciesResponse, getTrustedAppByPolicy } from './mocks'; -import { EndpointLicenseError } from '../../errors'; - -const exceptionsListClient = listMock.getExceptionListClient() as jest.Mocked; -const packagePolicyClient = - createPackagePolicyServiceMock() as jest.Mocked; -const savedObjectClient = - savedObjectsClientMock.create() as jest.Mocked; - -const EXCEPTION_LIST_ITEM: ExceptionListItemSchema = { - _version: 'abc123', - id: '123', - comments: [], - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - description: 'Linux trusted app 1', - entries: [ - createEntryMatch('process.executable.caseless', '/bin/malware'), - createEntryMatch('process.hash.md5', '1234234659af249ddf3e40864e9fb241'), - ], - item_id: '123', - list_id: 'endpoint_trusted_apps', - meta: undefined, - name: 'linux trusted app 1', - namespace_type: 'agnostic', - os_types: ['linux'], - tags: ['policy:all'], - type: 'simple', - tie_breaker_id: '123', - updated_at: '11/11/2011T11:11:11.111', - updated_by: 'admin', -}; - -const TRUSTED_APP: TrustedApp = { - id: '123', - version: 'abc123', - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - updated_at: '11/11/2011T11:11:11.111', - updated_by: 'admin', - name: 'linux trusted app 1', - description: 'Linux trusted app 1', - os: OperatingSystem.LINUX, - effectScope: { type: 'global' }, - entries: [ - createConditionEntry(ConditionEntryField.HASH, 'match', '1234234659af249ddf3e40864e9fb241'), - createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware'), - ], -}; - -describe('TrustedApps service', () => { - beforeEach(() => { - exceptionsListClient.deleteExceptionListItem.mockReset(); - exceptionsListClient.getExceptionListItem.mockReset(); - exceptionsListClient.createExceptionListItem.mockReset(); - exceptionsListClient.findExceptionListItem.mockReset(); - exceptionsListClient.createTrustedAppsList.mockReset(); - packagePolicyClient.getByIDs.mockReset(); - }); - - describe('deleteTrustedApp', () => { - it('should delete existing trusted app', async () => { - exceptionsListClient.getExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); - exceptionsListClient.deleteExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); - - expect(await deleteTrustedApp(exceptionsListClient, { id: '123' })).toBeUndefined(); - - expect(exceptionsListClient.deleteExceptionListItem).toHaveBeenCalledWith({ - id: '123', - namespaceType: 'agnostic', - }); - }); - - it('should throw for non existing trusted app', async () => { - exceptionsListClient.getExceptionListItem.mockResolvedValue(null); - exceptionsListClient.deleteExceptionListItem.mockResolvedValue(null); - - await expect(deleteTrustedApp(exceptionsListClient, { id: '123' })).rejects.toBeInstanceOf( - TrustedAppNotFoundError - ); - }); - }); - - describe('createTrustedApp', () => { - it('should create trusted app', async () => { - exceptionsListClient.createExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); - - const result = await createTrustedApp( - exceptionsListClient, - savedObjectClient, - packagePolicyClient, - { - name: 'linux trusted app 1', - description: 'Linux trusted app 1', - effectScope: { type: 'global' }, - os: OperatingSystem.LINUX, - entries: [ - createConditionEntry(ConditionEntryField.PATH, 'match', '/bin/malware'), - createConditionEntry( - ConditionEntryField.HASH, - 'match', - '1234234659af249ddf3e40864e9fb241' - ), - ], - }, - true - ); - - expect(result).toEqual({ data: TRUSTED_APP }); - - expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled(); - }); - - it('should create trusted app with correct wildcard type', async () => { - exceptionsListClient.createExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); - - const result = await createTrustedApp( - exceptionsListClient, - savedObjectClient, - packagePolicyClient, - { - name: 'linux trusted app 1', - description: 'Linux trusted app 1', - effectScope: { type: 'global' }, - os: OperatingSystem.LINUX, - entries: [ - createConditionEntry(ConditionEntryField.PATH, 'wildcard', '/bin/malware'), - createConditionEntry( - ConditionEntryField.HASH, - 'wildcard', - '1234234659af249ddf3e40864e9fb241' - ), - ], - }, - true - ); - - expect(result).toEqual({ data: TRUSTED_APP }); - - expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled(); - }); - - it("should throw wrong policy error if some policy doesn't exists", async () => { - packagePolicyClient.getByIDs.mockReset(); - packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse()); - await expect( - createTrustedApp( - exceptionsListClient, - savedObjectClient, - packagePolicyClient, - { - name: 'linux trusted app 1', - description: 'Linux trusted app 1', - effectScope: { - type: 'policy', - policies: [ - 'e5cbb9cf-98aa-4303-a04b-6a1165915079', - '9da95be9-9bee-4761-a8c4-28d6d9bd8c71', - ], - }, - os: OperatingSystem.LINUX, - entries: [ - createConditionEntry(ConditionEntryField.PATH, 'wildcard', '/bin/malware'), - createConditionEntry( - ConditionEntryField.HASH, - 'wildcard', - '1234234659af249ddf3e40864e9fb241' - ), - ], - }, - true - ) - ).rejects.toBeInstanceOf(TrustedAppPolicyNotExistsError); - }); - - it('should throw when license under platinum and by policy', async () => { - packagePolicyClient.getByIDs.mockReset(); - packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse()); - await expect( - createTrustedApp( - exceptionsListClient, - savedObjectClient, - packagePolicyClient, - { - name: 'linux trusted app 1', - description: 'Linux trusted app 1', - effectScope: { - type: 'policy', - policies: [ - 'e5cbb9cf-98aa-4303-a04b-6a1165915079', - '9da95be9-9bee-4761-a8c4-28d6d9bd8c71', - ], - }, - os: OperatingSystem.LINUX, - entries: [ - createConditionEntry(ConditionEntryField.PATH, 'wildcard', '/bin/malware'), - createConditionEntry( - ConditionEntryField.HASH, - 'wildcard', - '1234234659af249ddf3e40864e9fb241' - ), - ], - }, - false - ) - ).rejects.toBeInstanceOf(EndpointLicenseError); - }); - }); - - describe('getTrustedAppsList', () => { - beforeEach(() => { - exceptionsListClient.findExceptionListItem.mockResolvedValue({ - data: [EXCEPTION_LIST_ITEM], - page: 1, - per_page: 20, - total: 100, - }); - }); - - it('should get trusted apps', async () => { - const result = await getTrustedAppsList(exceptionsListClient, { page: 1, per_page: 20 }); - - expect(result).toEqual({ data: [TRUSTED_APP], page: 1, per_page: 20, total: 100 }); - - expect(exceptionsListClient.createTrustedAppsList).toHaveBeenCalled(); - }); - - it('should allow KQL to be defined', async () => { - const result = await getTrustedAppsList(exceptionsListClient, { - page: 1, - per_page: 20, - kuery: 'some-param.key: value', - }); - - expect(result).toEqual({ data: [TRUSTED_APP], page: 1, per_page: 20, total: 100 }); - expect(exceptionsListClient.findExceptionListItem).toHaveBeenCalledWith({ - listId: ENDPOINT_TRUSTED_APPS_LIST_ID, - page: 1, - perPage: 20, - filter: 'some-param.key: value', - namespaceType: 'agnostic', - sortField: 'name', - sortOrder: 'asc', - }); - }); - }); - - describe('getTrustedAppsSummary', () => { - beforeEach(() => { - exceptionsListClient.findExceptionListItem.mockImplementation(async ({ page }) => { - let data: ExceptionListItemSchema[] = []; - - // linux ++ windows entries - if (page === 1) { - data = [ - ...Array.from({ length: 45 }, () => ({ - ...EXCEPTION_LIST_ITEM, - os_types: ['linux'], - })), - ...Array.from({ length: 55 }, () => ({ - ...EXCEPTION_LIST_ITEM, - os_types: ['windows'] as ExceptionListItemSchema['os_types'], - })), - ]; - } - - // macos entries - if (page === 2) { - data = [ - ...Array.from({ length: 30 }, () => ({ - ...EXCEPTION_LIST_ITEM, - os_types: ['macos'] as ExceptionListItemSchema['os_types'], - })), - ]; - } - - return { - data, - page: page || 1, - total: 130, - per_page: 100, - }; - }); - }); - - it('should return summary of trusted app items', async () => { - expect(await getTrustedAppsSummary(exceptionsListClient, {})).toEqual({ - linux: 45, - windows: 55, - macos: 30, - total: 130, - }); - }); - - it('should return summary of trusted app items when filtering by policyId', async () => { - expect( - await getTrustedAppsSummary(exceptionsListClient, { - kuery: `exception-list-agnostic.attributes.tags:"policy:caf1a334-53f3-4be9-814d-a32245f43d34" OR exception-list-agnostic.attributes.tags:"policy:all"`, - }) - ).toEqual({ - linux: 45, - windows: 55, - macos: 30, - total: 130, - }); - }); - }); - - describe('updateTrustedApp', () => { - beforeEach(() => { - exceptionsListClient.getExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); - - exceptionsListClient.updateExceptionListItem.mockImplementationOnce( - updateExceptionListItemImplementationMock - ); - }); - - afterEach(() => jest.resetAllMocks()); - - it('should update exception item with trusted app data', async () => { - const trustedAppForUpdate = toUpdateTrustedApp(TRUSTED_APP); - trustedAppForUpdate.name = 'updated name'; - trustedAppForUpdate.description = 'updated description'; - trustedAppForUpdate.entries = [trustedAppForUpdate.entries[0]]; - - await expect( - updateTrustedApp( - exceptionsListClient, - savedObjectClient, - packagePolicyClient, - TRUSTED_APP.id, - trustedAppForUpdate, - true - ) - ).resolves.toEqual({ - data: { - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - updated_at: '11/11/2011T11:11:11.111', - updated_by: 'admin', - description: 'updated description', - effectScope: { - type: 'global', - }, - entries: [ - { - field: 'process.hash.*', - operator: 'included', - type: 'match', - value: '1234234659af249ddf3e40864e9fb241', - }, - ], - id: '123', - name: 'updated name', - os: 'linux', - version: 'abc123', - }, - }); - }); - - it('should throw a Not Found error if trusted app is not found prior to making update', async () => { - exceptionsListClient.getExceptionListItem.mockResolvedValue(null); - await expect( - updateTrustedApp( - exceptionsListClient, - savedObjectClient, - packagePolicyClient, - TRUSTED_APP.id, - toUpdateTrustedApp(TRUSTED_APP), - true - ) - ).rejects.toBeInstanceOf(TrustedAppNotFoundError); - }); - - it('should throw a Version Conflict error if update fails with 409', async () => { - exceptionsListClient.updateExceptionListItem.mockReset(); - exceptionsListClient.updateExceptionListItem.mockRejectedValueOnce( - Object.assign(new Error('conflict'), { output: { statusCode: 409 } }) - ); - - await expect( - updateTrustedApp( - exceptionsListClient, - savedObjectClient, - packagePolicyClient, - TRUSTED_APP.id, - toUpdateTrustedApp(TRUSTED_APP), - true - ) - ).rejects.toBeInstanceOf(TrustedAppVersionConflictError); - }); - - it('should throw Not Found if exception item is not found during update', async () => { - exceptionsListClient.updateExceptionListItem.mockReset(); - exceptionsListClient.updateExceptionListItem.mockResolvedValueOnce(null); - - exceptionsListClient.getExceptionListItem.mockReset(); - exceptionsListClient.getExceptionListItem.mockResolvedValueOnce(EXCEPTION_LIST_ITEM); - exceptionsListClient.getExceptionListItem.mockResolvedValueOnce(null); - - await expect( - updateTrustedApp( - exceptionsListClient, - savedObjectClient, - packagePolicyClient, - TRUSTED_APP.id, - toUpdateTrustedApp(TRUSTED_APP), - true - ) - ).rejects.toBeInstanceOf(TrustedAppNotFoundError); - }); - - it("should throw wrong policy error if some policy doesn't exists during update", async () => { - packagePolicyClient.getByIDs.mockReset(); - packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse()); - const trustedAppByPolicy = getTrustedAppByPolicy(); - await expect( - updateTrustedApp( - exceptionsListClient, - savedObjectClient, - packagePolicyClient, - trustedAppByPolicy.id, - toUpdateTrustedApp(trustedAppByPolicy as MaybeImmutable), - true - ) - ).rejects.toBeInstanceOf(TrustedAppPolicyNotExistsError); - }); - - it('should throw when license under platinum and by policy', async () => { - packagePolicyClient.getByIDs.mockReset(); - packagePolicyClient.getByIDs.mockResolvedValueOnce(getPackagePoliciesResponse()); - const trustedAppByPolicy = getTrustedAppByPolicy(); - await expect( - updateTrustedApp( - exceptionsListClient, - savedObjectClient, - packagePolicyClient, - trustedAppByPolicy.id, - toUpdateTrustedApp(trustedAppByPolicy as MaybeImmutable), - false - ) - ).rejects.toBeInstanceOf(EndpointLicenseError); - }); - }); - - describe('getTrustedApp', () => { - it('should return a single trusted app', async () => { - exceptionsListClient.getExceptionListItem.mockResolvedValue(EXCEPTION_LIST_ITEM); - expect(await getTrustedApp(exceptionsListClient, '123')).toEqual({ data: TRUSTED_APP }); - }); - - it('should return Trusted App Not Found Error if it does not exist', async () => { - exceptionsListClient.getExceptionListItem.mockResolvedValue(null); - await expect(getTrustedApp(exceptionsListClient, '123')).rejects.toBeInstanceOf( - TrustedAppNotFoundError - ); - }); - - it('should try to find trusted app by `itemId` and then by `id`', async () => { - exceptionsListClient.getExceptionListItem.mockResolvedValue(null); - await getTrustedApp(exceptionsListClient, '123').catch(() => Promise.resolve()); - - expect(exceptionsListClient.getExceptionListItem).toHaveBeenCalledTimes(2); - expect(exceptionsListClient.getExceptionListItem).toHaveBeenNthCalledWith(1, { - itemId: '123', - id: undefined, - namespaceType: 'agnostic', - }); - expect(exceptionsListClient.getExceptionListItem).toHaveBeenNthCalledWith(2, { - itemId: undefined, - id: '123', - namespaceType: 'agnostic', - }); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts deleted file mode 100644 index 7a4b2372ece8f..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/service.ts +++ /dev/null @@ -1,321 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import type { SavedObjectsClientContract } from 'kibana/server'; -import type { ExceptionListItemSchema } from '@kbn/securitysolution-io-ts-list-types'; -import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; -import { isEmpty, isEqual } from 'lodash/fp'; -import { ExceptionListClient } from '../../../../../lists/server'; - -import { - DeleteTrustedAppsRequestParams, - GetOneTrustedAppResponse, - GetTrustedAppsListRequest, - GetTrustedAppsListResponse, - GetTrustedAppsSummaryRequest, - GetTrustedAppsSummaryResponse, - PostTrustedAppCreateRequest, - PostTrustedAppCreateResponse, - PutTrustedAppUpdateRequest, - PutTrustedAppUpdateResponse, - TrustedApp, -} from '../../../../common/endpoint/types'; - -import { - exceptionListItemToTrustedApp, - newTrustedAppToCreateExceptionListItemOptions, - osFromExceptionItem, - updatedTrustedAppToUpdateExceptionListItemOptions, -} from './mapping'; -import { - TrustedAppNotFoundError, - TrustedAppPolicyNotExistsError, - TrustedAppVersionConflictError, -} from './errors'; -import { PackagePolicyServiceInterface } from '../../../../../fleet/server'; -import { PackagePolicy } from '../../../../../fleet/common'; -import { EndpointLicenseError } from '../../errors'; - -const getNonExistingPoliciesFromTrustedApp = async ( - savedObjectClient: SavedObjectsClientContract, - packagePolicyClient: PackagePolicyServiceInterface, - trustedApp: PutTrustedAppUpdateRequest | PostTrustedAppCreateRequest -): Promise => { - if ( - !trustedApp.effectScope || - trustedApp.effectScope.type === 'global' || - (trustedApp.effectScope.type === 'policy' && isEmpty(trustedApp.effectScope.policies)) - ) { - return []; - } - - const policies = await packagePolicyClient.getByIDs( - savedObjectClient, - trustedApp.effectScope.policies - ); - - if (!policies) { - return []; - } - - return policies.filter((policy) => policy.version === undefined); -}; - -const isUserTryingToModifyEffectScopeWithoutPermissions = ( - currentTrustedApp: TrustedApp, - updatedTrustedApp: PutTrustedAppUpdateRequest, - isAtLeastPlatinum: boolean -): boolean => { - if (updatedTrustedApp.effectScope.type === 'global') { - return false; - } else if (isAtLeastPlatinum) { - return false; - } else if ( - isEqual( - currentTrustedApp.effectScope.type === 'policy' && - currentTrustedApp.effectScope.policies.sort(), - updatedTrustedApp.effectScope.policies.sort() - ) - ) { - return false; - } else { - return true; - } -}; - -/** - * Attempts to first fine the ExceptionItem using `item_id` and if not found, then a second attempt wil be done - * against the Saved Object `id`. - * @param exceptionsListClient - * @param id - */ -export const findTrustedAppExceptionItemByIdOrItemId = async ( - exceptionsListClient: ExceptionListClient, - id: string -): Promise => { - const trustedAppExceptionItem = await exceptionsListClient.getExceptionListItem({ - itemId: id, - id: undefined, - namespaceType: 'agnostic', - }); - - if (trustedAppExceptionItem) { - return trustedAppExceptionItem; - } - - return exceptionsListClient.getExceptionListItem({ - itemId: undefined, - id, - namespaceType: 'agnostic', - }); -}; - -export const deleteTrustedApp = async ( - exceptionsListClient: ExceptionListClient, - { id }: DeleteTrustedAppsRequestParams -): Promise => { - const trustedAppExceptionItem = await findTrustedAppExceptionItemByIdOrItemId( - exceptionsListClient, - id - ); - - if (!trustedAppExceptionItem) { - throw new TrustedAppNotFoundError(id); - } - - await exceptionsListClient.deleteExceptionListItem({ - id: trustedAppExceptionItem.id, - itemId: undefined, - namespaceType: 'agnostic', - }); -}; - -export const getTrustedApp = async ( - exceptionsListClient: ExceptionListClient, - id: string -): Promise => { - const trustedAppExceptionItem = await findTrustedAppExceptionItemByIdOrItemId( - exceptionsListClient, - id - ); - - if (!trustedAppExceptionItem) { - throw new TrustedAppNotFoundError(id); - } - - return { - data: exceptionListItemToTrustedApp(trustedAppExceptionItem), - }; -}; - -export const getTrustedAppsList = async ( - exceptionsListClient: ExceptionListClient, - { page, per_page: perPage, kuery }: GetTrustedAppsListRequest -): Promise => { - // Ensure list is created if it does not exist - await exceptionsListClient.createTrustedAppsList(); - - const results = await exceptionsListClient.findExceptionListItem({ - listId: ENDPOINT_TRUSTED_APPS_LIST_ID, - page, - perPage, - filter: kuery, - namespaceType: 'agnostic', - sortField: 'name', - sortOrder: 'asc', - }); - - return { - data: results?.data.map(exceptionListItemToTrustedApp) ?? [], - total: results?.total ?? 0, - page: results?.page ?? 1, - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - per_page: results?.per_page ?? perPage!, - }; -}; - -export const createTrustedApp = async ( - exceptionsListClient: ExceptionListClient, - savedObjectClient: SavedObjectsClientContract, - packagePolicyClient: PackagePolicyServiceInterface, - newTrustedApp: PostTrustedAppCreateRequest, - isAtLeastPlatinum: boolean -): Promise => { - // Ensure list is created if it does not exist - await exceptionsListClient.createTrustedAppsList(); - - if (newTrustedApp.effectScope.type === 'policy' && !isAtLeastPlatinum) { - throw new EndpointLicenseError(); - } - - const unexistingPolicies = await getNonExistingPoliciesFromTrustedApp( - savedObjectClient, - packagePolicyClient, - newTrustedApp - ); - - if (!isEmpty(unexistingPolicies)) { - throw new TrustedAppPolicyNotExistsError( - newTrustedApp.name, - unexistingPolicies.map((policy) => policy.id) - ); - } - - const createdTrustedAppExceptionItem = await exceptionsListClient.createExceptionListItem( - newTrustedAppToCreateExceptionListItemOptions(newTrustedApp) - ); - - return { data: exceptionListItemToTrustedApp(createdTrustedAppExceptionItem) }; -}; - -export const updateTrustedApp = async ( - exceptionsListClient: ExceptionListClient, - savedObjectClient: SavedObjectsClientContract, - packagePolicyClient: PackagePolicyServiceInterface, - id: string, - updatedTrustedApp: PutTrustedAppUpdateRequest, - isAtLeastPlatinum: boolean -): Promise => { - const currentTrustedAppExceptionItem = await findTrustedAppExceptionItemByIdOrItemId( - exceptionsListClient, - id - ); - - if (!currentTrustedAppExceptionItem) { - throw new TrustedAppNotFoundError(id); - } - - if ( - isUserTryingToModifyEffectScopeWithoutPermissions( - exceptionListItemToTrustedApp(currentTrustedAppExceptionItem), - updatedTrustedApp, - isAtLeastPlatinum - ) - ) { - throw new EndpointLicenseError(); - } - - const unexistingPolicies = await getNonExistingPoliciesFromTrustedApp( - savedObjectClient, - packagePolicyClient, - updatedTrustedApp - ); - - if (!isEmpty(unexistingPolicies)) { - throw new TrustedAppPolicyNotExistsError( - updatedTrustedApp.name, - unexistingPolicies.map((policy) => policy.id) - ); - } - - let updatedTrustedAppExceptionItem: ExceptionListItemSchema | null; - - try { - updatedTrustedAppExceptionItem = await exceptionsListClient.updateExceptionListItem( - updatedTrustedAppToUpdateExceptionListItemOptions( - currentTrustedAppExceptionItem, - updatedTrustedApp - ) - ); - } catch (e) { - if (e?.output?.statusCode === 409) { - throw new TrustedAppVersionConflictError(id, e); - } - - throw e; - } - - // If `null` is returned, then that means the TA does not exist (could happen in race conditions) - if (!updatedTrustedAppExceptionItem) { - throw new TrustedAppNotFoundError(id); - } - - return { - data: exceptionListItemToTrustedApp(updatedTrustedAppExceptionItem), - }; -}; - -export const getTrustedAppsSummary = async ( - exceptionsListClient: ExceptionListClient, - { kuery }: GetTrustedAppsSummaryRequest -): Promise => { - // Ensure list is created if it does not exist - await exceptionsListClient.createTrustedAppsList(); - const summary = { - linux: 0, - windows: 0, - macos: 0, - total: 0, - }; - const perPage = 100; - let paging = true; - let page = 1; - - while (paging) { - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - const { data, total } = (await exceptionsListClient.findExceptionListItem({ - listId: ENDPOINT_TRUSTED_APPS_LIST_ID, - page, - perPage, - filter: kuery, - namespaceType: 'agnostic', - sortField: undefined, - sortOrder: undefined, - }))!; - - summary.total = total; - - for (const item of data) { - summary[osFromExceptionItem(item)]++; - } - - paging = (page - 1) * perPage + data.length < total; - page++; - } - - return summary; -}; diff --git a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/test_utils.ts b/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/test_utils.ts deleted file mode 100644 index b2def0f612d15..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/routes/trusted_apps/test_utils.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { ExceptionListClient } from '../../../../../lists/server'; - -export const updateExceptionListItemImplementationMock: ExceptionListClient['updateExceptionListItem'] = - async (listItem) => { - return { - _version: listItem._version || 'abc123', - id: listItem.id || '123', - comments: [], - created_at: '11/11/2011T11:11:11.111', - created_by: 'admin', - description: listItem.description || '', - entries: listItem.entries || [], - item_id: listItem.itemId || '', - list_id: 'endpoint_trusted_apps', - meta: undefined, - name: listItem.name || '', - namespace_type: listItem.namespaceType || '', - os_types: listItem.osTypes || '', - tags: listItem.tags || [], - type: 'simple', - tie_breaker_id: '123', - updated_at: '11/11/2011T11:11:11.111', - updated_by: 'admin', - }; - }; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/index.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/index.ts index b24acf02b6d72..4ffd636526351 100644 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/index.ts +++ b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/index.ts @@ -7,6 +7,4 @@ export * from './common'; export * from './lists'; -export * from './request'; -export * from './response'; export * from './saved_objects'; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/request/download_artifact_schema.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/request/download_artifact_schema.ts deleted file mode 100644 index b9f83f00af8e7..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/request/download_artifact_schema.ts +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { identifier, sha256 } from '../../../../../common/endpoint/schema/common'; - -export const downloadArtifactRequestParamsSchema = t.exact( - t.type({ - identifier, - sha256, - }) -); - -export type DownloadArtifactRequestParamsSchema = t.TypeOf< - typeof downloadArtifactRequestParamsSchema ->; diff --git a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/download_artifact_schema.ts b/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/download_artifact_schema.ts deleted file mode 100644 index 1bf5b44cff905..0000000000000 --- a/x-pack/plugins/security_solution/server/endpoint/schemas/artifacts/response/download_artifact_schema.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import * as t from 'io-ts'; -import { buffer, encoding } from '../common'; - -const headers = t.exact( - t.type({ - 'content-encoding': encoding, - 'content-disposition': t.string, - }) -); - -export const downloadArtifactResponseSchema = t.exact( - t.type({ - body: buffer, - headers, - }) -); - -export type DownloadArtifactResponseSchema = t.TypeOf; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts index 8abe054daeaf5..778efa05f692e 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_context.ts @@ -31,8 +31,9 @@ import type { SecuritySolutionRequestHandlerContext, } from '../../../../types'; import { getEndpointAuthzInitialStateMock } from '../../../../../common/endpoint/service/authz'; +import { EndpointAuthz } from '../../../../../common/endpoint/types/authz'; -const createMockClients = () => { +export const createMockClients = () => { const core = coreMock.createRequestHandlerContext(); const license = licensingMock.createLicenseMock(); @@ -67,11 +68,12 @@ type SecuritySolutionRequestHandlerContextMock = }; const createRequestContextMock = ( - clients: MockClients = createMockClients() + clients: MockClients = createMockClients(), + overrides: { endpointAuthz?: Partial } = {} ): SecuritySolutionRequestHandlerContextMock => { return { core: clients.core, - securitySolution: createSecuritySolutionRequestContextMock(clients), + securitySolution: createSecuritySolutionRequestContextMock(clients, overrides), actions: { getActionsClient: jest.fn(() => clients.actionsClient), } as unknown as jest.Mocked, @@ -87,14 +89,15 @@ const createRequestContextMock = ( }; const createSecuritySolutionRequestContextMock = ( - clients: MockClients + clients: MockClients, + overrides: { endpointAuthz?: Partial } = {} ): jest.Mocked => { const core = clients.core; const kibanaRequest = requestMock.create(); return { core, - endpointAuthz: getEndpointAuthzInitialStateMock(), + endpointAuthz: getEndpointAuthzInitialStateMock(overrides.endpointAuthz), getConfig: jest.fn(() => clients.config), getFrameworkRequest: jest.fn(() => { return { diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts index 3c1a49c640863..547bdb9105c21 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/__mocks__/request_responses.ts @@ -226,13 +226,6 @@ export const getFindResultWithMultiHits = ({ }; }; -export const ruleStatusRequest = () => - requestMock.create({ - method: 'post', - path: `${DETECTION_ENGINE_RULES_URL}/_find_statuses`, - body: { ids: ['04128c15-0d1b-4716-a4c5-46997ac7f3bd'] }, - }); - export const internalRuleStatusRequest = () => requestMock.create({ method: 'post', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts deleted file mode 100644 index 2286c010a0a5a..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.test.ts +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { - ruleStatusRequest, - getAlertMock, - getFindBulkResultStatus, -} from '../__mocks__/request_responses'; -import { serverMock, requestContextMock, requestMock } from '../__mocks__'; -import { findRulesStatusesRoute } from './find_rules_status_route'; -import { RuleStatusResponse } from '../../rules/types'; -import { AlertExecutionStatusErrorReasons } from '../../../../../../alerting/common'; -import { getQueryRuleParams } from '../../schemas/rule_schemas.mock'; - -describe.each([ - ['Legacy', false], - ['RAC', true], -])('find_statuses - %s', (_, isRuleRegistryEnabled) => { - let server: ReturnType; - let { clients, context } = requestContextMock.createTools(); - - beforeEach(async () => { - server = serverMock.create(); - ({ clients, context } = requestContextMock.createTools()); - clients.ruleExecutionLogClient.getCurrentStatusBulk.mockResolvedValue( - getFindBulkResultStatus() - ); // successful status search - clients.rulesClient.get.mockResolvedValue( - getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()) - ); - findRulesStatusesRoute(server.router); - }); - - describe('status codes with actionClient and alertClient', () => { - test('returns 200 when finding a single rule status with a valid rulesClient', async () => { - const response = await server.inject(ruleStatusRequest(), context); - expect(response.status).toEqual(200); - }); - - test('returns 404 if alertClient is not available on the route', async () => { - context.alerting.getRulesClient = jest.fn(); - const response = await server.inject(ruleStatusRequest(), context); - expect(response.status).toEqual(404); - expect(response.body).toEqual({ message: 'Not Found', status_code: 404 }); - }); - - test('catch error when status search throws error', async () => { - clients.ruleExecutionLogClient.getCurrentStatusBulk.mockImplementation(async () => { - throw new Error('Test error'); - }); - const response = await server.inject(ruleStatusRequest(), context); - expect(response.status).toEqual(500); - expect(response.body).toEqual({ - message: 'Test error', - status_code: 500, - }); - }); - - test('returns success if rule status client writes an error status', async () => { - // 0. task manager tried to run the rule but couldn't, so the alerting framework - // wrote an error to the executionStatus. - const failingExecutionRule = getAlertMock(isRuleRegistryEnabled, getQueryRuleParams()); - failingExecutionRule.executionStatus = { - status: 'error', - lastExecutionDate: failingExecutionRule.executionStatus.lastExecutionDate, - error: { - reason: AlertExecutionStatusErrorReasons.Read, - message: 'oops', - }, - }; - - // 1. getFailingRules api found a rule where the executionStatus was 'error' - clients.rulesClient.get.mockResolvedValue({ - ...failingExecutionRule, - }); - - const response = await server.inject(ruleStatusRequest(), context); - const body: RuleStatusResponse = response.body; - expect(response.status).toEqual(200); - expect(body[ruleStatusRequest().body.ids[0]].current_status?.status).toEqual('failed'); - expect(body[ruleStatusRequest().body.ids[0]].current_status?.last_failure_message).toEqual( - 'Reason: read Message: oops' - ); - }); - }); - - describe('request validation', () => { - test('disallows singular id query param', async () => { - const request = requestMock.create({ - method: 'post', - path: `${DETECTION_ENGINE_RULES_URL}/_find_statuses`, - body: { id: ['someId'] }, - }); - const result = server.validate(request); - - expect(result.badRequest).toHaveBeenCalledWith('Invalid value "undefined" supplied to "ids"'); - }); - }); -}); diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.ts deleted file mode 100644 index af4f8ddbb9ec8..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/routes/rules/find_rules_status_route.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { transformError } from '@kbn/securitysolution-es-utils'; -import { buildRouteValidation } from '../../../../utils/build_validation/route_validation'; -import type { SecuritySolutionPluginRouter } from '../../../../types'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../../common/constants'; -import { buildSiemResponse, mergeStatuses, getFailingRules } from '../utils'; -import { - findRulesStatusesSchema, - FindRulesStatusesSchemaDecoded, -} from '../../../../../common/detection_engine/schemas/request/find_rule_statuses_schema'; -import { mergeAlertWithSidecarStatus } from '../../schemas/rule_converters'; - -/** - * Returns the current execution status and metrics for N rules. - * Accepts an array of rule ids. - * - * NOTE: This endpoint is used on the Rule Management page and will be reworked. - * See the plan in https://github.com/elastic/kibana/pull/115574 - * - * @param router - * @returns RuleStatusResponse containing data for N requested rules. - * RuleStatusResponse[ruleId].failures is always an empty array, because - * we don't need failure history of every rule when we render tables with rules. - */ -export const findRulesStatusesRoute = (router: SecuritySolutionPluginRouter) => { - router.post( - { - path: `${DETECTION_ENGINE_RULES_URL}/_find_statuses`, - validate: { - body: buildRouteValidation( - findRulesStatusesSchema - ), - }, - options: { - tags: ['access:securitySolution'], - }, - }, - async (context, request, response) => { - const { body } = request; - const siemResponse = buildSiemResponse(response); - const rulesClient = context.alerting?.getRulesClient(); - - if (!rulesClient) { - return siemResponse.error({ statusCode: 404 }); - } - - const ids = body.ids; - try { - const ruleStatusClient = context.securitySolution.getExecutionLogClient(); - const [currentStatusesByRuleId, failingRules] = await Promise.all([ - ruleStatusClient.getCurrentStatusBulk({ - ruleIds: ids, - spaceId: context.securitySolution.getSpaceId(), - }), - getFailingRules(ids, rulesClient), - ]); - - const statuses = ids.reduce((acc, id) => { - const currentStatus = currentStatusesByRuleId[id]; - const failingRule = failingRules[id]; - - if (currentStatus == null) { - return acc; - } - - const finalCurrentStatus = - failingRule != null - ? mergeAlertWithSidecarStatus(failingRule, currentStatus) - : currentStatus; - - return mergeStatuses(id, [finalCurrentStatus], acc); - }, {}); - - return response.ok({ body: statuses }); - } catch (err) { - const error = transformError(err); - return siemResponse.error({ - body: error.message, - statusCode: error.statusCode, - }); - } - } - ); -}; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/rule_asset/rule_asset_saved_object_mappings.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/rule_asset/rule_asset_saved_object_mappings.ts index e2941b503664b..d42248823e733 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/rules/rule_asset/rule_asset_saved_object_mappings.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/rules/rule_asset/rule_asset_saved_object_mappings.ts @@ -27,6 +27,10 @@ export const ruleAssetSavedObjectMappings: SavedObjectsType['mappings'] = { export const ruleAssetType: SavedObjectsType = { name: ruleAssetSavedObjectType, hidden: false, + management: { + importableAndExportable: true, + visibleInManagement: false, + }, namespaceType: 'agnostic', mappings: ruleAssetSavedObjectMappings, }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts index c10aa0bd42ecd..4cc73f48202ab 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/schemas/rule_converters.ts @@ -318,6 +318,9 @@ export const internalRuleToAPIResponse = ( last_success_at: mergedStatus?.lastSuccessAt ?? undefined, last_failure_message: mergedStatus?.lastFailureMessage ?? undefined, last_success_message: mergedStatus?.lastSuccessMessage ?? undefined, + last_gap: mergedStatus?.gap ?? undefined, + bulk_create_time_durations: mergedStatus?.bulkCreateTimeDurations ?? undefined, + search_after_time_durations: mergedStatus?.searchAfterTimeDurations ?? undefined, }; }; diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_rules_statuses_by_ids.sh b/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_rules_statuses_by_ids.sh deleted file mode 100755 index 943530aa3e956..0000000000000 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/scripts/find_rules_statuses_by_ids.sh +++ /dev/null @@ -1,22 +0,0 @@ -#!/bin/sh - -# -# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one -# or more contributor license agreements. Licensed under the Elastic License -# 2.0; you may not use this file except in compliance with the Elastic License -# 2.0. -# - -set -e -./check_env_variables.sh - - -# Example: ./find_rules_statuses_by_ids.sh [\"12345\",\"6789abc\"] -curl -g -k \ - -s \ - -H 'Content-Type: application/json' \ - -H 'kbn-xsrf: 123' \ - -u ${ELASTICSEARCH_USERNAME}:${ELASTICSEARCH_PASSWORD} \ - -X POST "${KIBANA_URL}${SPACE_URL}/api/detection_engine/rules/_find_statuses" \ - -d "{\"ids\": $1}" \ - | jq . diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts index 8768fa7a08145..6826103d67152 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.test.ts @@ -132,7 +132,7 @@ describe('buildEnrichments', () => { threats, indicatorPath, }); - expect(Object.keys(enrichment)).toEqual(['indicator', 'matched']); + expect(Object.keys(enrichment)).toEqual(['indicator', 'feed', 'matched']); }); it('returns the _id of the matched indicator as matched.id', () => { @@ -227,6 +227,7 @@ describe('buildEnrichments', () => { expect(enrichments).toEqual([ { + feed: {}, indicator: { domain: 'domain_1', other: 'other_1', @@ -271,6 +272,7 @@ describe('buildEnrichments', () => { expect(enrichments).toEqual([ { + feed: {}, indicator: { indicator_field: 'indicator_field_1', reference: 'https://test3.com', @@ -303,6 +305,7 @@ describe('buildEnrichments', () => { expect(enrichments).toEqual([ { + feed: {}, indicator: {}, matched: { atomic: undefined, @@ -331,6 +334,7 @@ describe('buildEnrichments', () => { expect(enrichments).toEqual([ { + feed: {}, indicator: {}, matched: { atomic: undefined, @@ -366,6 +370,7 @@ describe('buildEnrichments', () => { expect(enrichments).toEqual([ { + feed: {}, indicator: { domain: 'foo', reference: 'https://test4.com', @@ -423,6 +428,53 @@ describe('buildEnrichments', () => { }) ).toThrowError('Expected indicator field to be an object, but found: not an object'); }); + + it('returns the feed data if it specified', () => { + threats = [ + getThreatListItemMock({ + _id: '123', + _source: { + event: { dataset: 'abuse.ch', reference: 'https://test.com' }, + threat: { + feed: { name: 'feed name' }, + indicator: { + domain: 'domain_1', + other: 'other_1', + reference: 'https://test.com', + type: 'type_1', + }, + }, + }, + }), + ]; + + const enrichments = buildEnrichments({ + queries, + threats, + indicatorPath, + }); + + expect(enrichments).toEqual([ + { + feed: { + name: 'feed name', + }, + indicator: { + domain: 'domain_1', + other: 'other_1', + reference: 'https://test.com', + type: 'type_1', + }, + matched: { + atomic: 'domain_1', + field: 'event.field', + id: '123', + index: 'threat-index', + type: 'indicator_match_rule', + }, + }, + ]); + }); }); describe('enrichSignalThreatMatches', () => { @@ -485,6 +537,7 @@ describe('enrichSignalThreatMatches', () => { expect(enrichments).toEqual([ { existing: 'indicator' }, { + feed: {}, indicator: { domain: 'domain_1', other: 'other_1', @@ -517,6 +570,7 @@ describe('enrichSignalThreatMatches', () => { expect(enrichments).toEqual([ { + feed: {}, indicator: {}, matched: { atomic: undefined, @@ -553,9 +607,14 @@ describe('enrichSignalThreatMatches', () => { const enrichments = get(enrichedHit._source, ENRICHMENT_DESTINATION_PATH); expect(enrichments).toEqual([ - { indicator: { existing: 'indicator' } }, - { indicator: { existing: 'indicator2' } }, { + indicator: { existing: 'indicator' }, + }, + { + indicator: { existing: 'indicator2' }, + }, + { + feed: {}, indicator: { domain: 'domain_1', other: 'other_1', @@ -620,6 +679,7 @@ describe('enrichSignalThreatMatches', () => { expect(enrichments).toEqual([ { + feed: {}, indicator: { domain: 'custom_domain', other: 'custom_other', @@ -684,6 +744,7 @@ describe('enrichSignalThreatMatches', () => { expect(enrichments).toEqual([ { + feed: {}, indicator: { domain: 'domain_1', other: 'other_1', @@ -698,6 +759,7 @@ describe('enrichSignalThreatMatches', () => { }, }, { + feed: {}, indicator: { domain: 'domain_2', other: 'other_2', diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts index 272c3f64fb105..297d52ecb634b 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/enrich_signal_threat_matches.ts @@ -6,7 +6,7 @@ */ import { get, isObject } from 'lodash'; -import { ENRICHMENT_TYPES } from '../../../../../common/cti/constants'; +import { ENRICHMENT_TYPES, FEED_NAME_PATH } from '../../../../../common/cti/constants'; import type { SignalSearchResponse, SignalSourceHit } from '../types'; import type { @@ -52,14 +52,19 @@ export const buildEnrichments = ({ queries.map((query) => { const matchedThreat = threats.find((threat) => threat._id === query.id); const indicatorValue = get(matchedThreat?._source, indicatorPath) as unknown; + const feedName = (get(matchedThreat?._source, FEED_NAME_PATH) ?? '') as string; const indicator = ([indicatorValue].flat()[0] ?? {}) as Record; if (!isObject(indicator)) { throw new Error(`Expected indicator field to be an object, but found: ${indicator}`); } const atomic = get(matchedThreat?._source, query.value) as unknown; - + const feed: { name?: string } = {}; + if (feedName) { + feed.name = feedName; + } return { indicator, + feed, matched: { atomic, field: query.field, diff --git a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts index be17682ea4d08..bf710cde93fe6 100644 --- a/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts +++ b/x-pack/plugins/security_solution/server/lib/detection_engine/signals/threat_mapping/types.ts @@ -173,6 +173,7 @@ export interface ThreatListDoc { export type ThreatListItem = estypes.SearchHit; export interface ThreatEnrichment { + feed: Record; indicator: Record; matched: Record; } diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts index 37f6debd50257..62b632d4a0803 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/helpers.ts @@ -16,7 +16,7 @@ import { LIST_ENDPOINT_EVENT_FILTER, LIST_TRUSTED_APPLICATION, } from './constants'; -import { TrustedApp } from '../../../common/endpoint/types'; +import { tagsToEffectScope } from '../../../common/endpoint/service/trusted_apps/mapping'; /** * Determines the when the last run was in order to execute to. @@ -97,18 +97,20 @@ export function isPackagePolicyList( /** * Maps trusted application to shared telemetry object * - * @param exceptionListItem + * @param trustedAppExceptionItem * @returns collection of trusted applications */ -export const trustedApplicationToTelemetryEntry = (trustedApplication: TrustedApp) => { +export const trustedApplicationToTelemetryEntry = ( + trustedAppExceptionItem: ExceptionListItemSchema +) => { return { - id: trustedApplication.id, - name: trustedApplication.name, - created_at: trustedApplication.created_at, - updated_at: trustedApplication.updated_at, - entries: trustedApplication.entries, - os_types: [trustedApplication.os], - scope: trustedApplication.effectScope, + id: trustedAppExceptionItem.id, + name: trustedAppExceptionItem.name, + created_at: trustedAppExceptionItem.created_at, + updated_at: trustedAppExceptionItem.updated_at, + entries: trustedAppExceptionItem.entries, + os_types: trustedAppExceptionItem.os_types, + scope: tagsToEffectScope(trustedAppExceptionItem.tags), } as ExceptionListItem; }; diff --git a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts index 8fcdbd7304656..b4240dac738ee 100644 --- a/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts +++ b/x-pack/plugins/security_solution/server/lib/telemetry/receiver.ts @@ -12,7 +12,7 @@ import { SavedObjectsClientContract, } from 'src/core/server'; import { SearchRequest } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { getTrustedAppsList } from '../../endpoint/routes/trusted_apps/service'; +import { ENDPOINT_TRUSTED_APPS_LIST_ID } from '@kbn/securitysolution-list-constants'; import { AgentClient, AgentPolicyServiceInterface } from '../../../../fleet/server'; import { ExceptionListClient } from '../../../../lists/server'; import { EndpointAppContextService } from '../../endpoint/endpoint_app_context_services'; @@ -221,10 +221,16 @@ export class TelemetryReceiver { throw Error('exception list client is unavailable: cannot retrieve trusted applications'); } - const results = await getTrustedAppsList(this.exceptionListClient, { + const results = await this.exceptionListClient.findExceptionListItem({ + listId: ENDPOINT_TRUSTED_APPS_LIST_ID, page: 1, - per_page: 10_000, + perPage: 10_000, + filter: undefined, + namespaceType: 'agnostic', + sortField: 'name', + sortOrder: 'asc', }); + return { data: results?.data.map(trustedApplicationToTelemetryEntry), total: results?.total ?? 0, @@ -331,8 +337,6 @@ export class TelemetryReceiver { path: '/_license', querystring: { local: true, - // For versions >= 7.6 and < 8.0, this flag is needed otherwise 'platinum' is returned for 'enterprise' license. - accept_enterprise: 'true', }, }) ).body as Promise<{ license: ESLicense }>; diff --git a/x-pack/plugins/security_solution/server/plugin.ts b/x-pack/plugins/security_solution/server/plugin.ts index dfff89a90c732..d317a633bf3d5 100644 --- a/x-pack/plugins/security_solution/server/plugin.ts +++ b/x-pack/plugins/security_solution/server/plugin.ts @@ -56,7 +56,6 @@ import { EndpointAppContextService } from './endpoint/endpoint_app_context_servi import { EndpointAppContext } from './endpoint/types'; import { initUsageCollectors } from './usage'; import type { SecuritySolutionRequestHandlerContext } from './types'; -import { registerTrustedAppsRoutes } from './endpoint/routes/trusted_apps'; import { securitySolutionSearchStrategyProvider } from './search_strategy/security_solution'; import { TelemetryEventsSender } from './lib/telemetry/sender'; import { TelemetryReceiver } from './lib/telemetry/receiver'; @@ -246,7 +245,6 @@ export class Plugin implements ISecuritySolutionPlugin { registerLimitedConcurrencyRoutes(core); registerResolverRoutes(router); registerPolicyRoutes(router, endpointContext); - registerTrustedAppsRoutes(router, endpointContext); registerActionRoutes(router, endpointContext); const racRuleTypes = [ diff --git a/x-pack/plugins/security_solution/server/routes/index.ts b/x-pack/plugins/security_solution/server/routes/index.ts index dd66a4333ad15..57af7153ff0a5 100644 --- a/x-pack/plugins/security_solution/server/routes/index.ts +++ b/x-pack/plugins/security_solution/server/routes/index.ts @@ -36,7 +36,6 @@ import { deleteRulesBulkRoute } from '../lib/detection_engine/routes/rules/delet import { performBulkActionRoute } from '../lib/detection_engine/routes/rules/perform_bulk_action_route'; import { importRulesRoute } from '../lib/detection_engine/routes/rules/import_rules_route'; import { exportRulesRoute } from '../lib/detection_engine/routes/rules/export_rules_route'; -import { findRulesStatusesRoute } from '../lib/detection_engine/routes/rules/find_rules_status_route'; import { findRuleStatusInternalRoute } from '../lib/detection_engine/routes/rules/find_rule_status_internal_route'; import { getPrepackagedRulesStatusRoute } from '../lib/detection_engine/routes/rules/get_prepackaged_rules_status_route'; import { @@ -95,8 +94,6 @@ export const initRoutes = ( // Once we no longer have the legacy notifications system/"side car actions" this should be removed. legacyCreateLegacyNotificationRoute(router, logger); - // TODO: pass isRuleRegistryEnabled to all relevant routes - addPrepackedRulesRoute(router); getPrepackagedRulesStatusRoute(router, config, security, isRuleRegistryEnabled); createRulesBulkRoute(router, ml, isRuleRegistryEnabled); @@ -125,7 +122,6 @@ export const initRoutes = ( persistNoteRoute(router, config, security); persistPinnedEventRoute(router, config, security); - findRulesStatusesRoute(router); findRuleStatusInternalRoute(router); // Detection Engine Signals routes that have the REST endpoints of /api/detection_engine/signals diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/index.ts index 5857a0417239c..e43af97e84af0 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/index.ts @@ -9,7 +9,9 @@ import type { FactoryQueryTypes } from '../../../../../common/search_strategy/se import { CtiQueries } from '../../../../../common/search_strategy/security_solution/cti'; import type { SecuritySolutionFactory } from '../types'; import { eventEnrichment } from './event_enrichment'; +import { dataSource } from './threat_intel_source'; export const ctiFactoryTypes: Record> = { [CtiQueries.eventEnrichment]: eventEnrichment, + [CtiQueries.dataSource]: dataSource, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/index.ts new file mode 100644 index 0000000000000..6f1c2cc7f60b0 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { SecuritySolutionFactory } from '../../types'; +import { + CtiDataSourceStrategyResponse, + CtiQueries, + CtiDataSourceRequestOptions, +} from '../../../../../../common/search_strategy/security_solution/cti'; +import { IEsSearchResponse } from '../../../../../../../../../src/plugins/data/common'; +import { inspectStringifyObject } from '../../../../../utils/build_query'; +import { buildTiDataSourceQuery } from './query.threat_intel_source.dsl'; + +export const dataSource: SecuritySolutionFactory = { + buildDsl: (options: CtiDataSourceRequestOptions) => buildTiDataSourceQuery(options), + parse: async ( + options: CtiDataSourceRequestOptions, + response: IEsSearchResponse + ): Promise => { + const inspect = { + dsl: [inspectStringifyObject(buildTiDataSourceQuery(options))], + }; + + return { + ...response, + inspect, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.test.ts new file mode 100644 index 0000000000000..a0df71e733871 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { buildTiDataSourceQuery } from './query.threat_intel_source.dsl'; +import { CtiQueries } from '../../../../../../common/search_strategy/security_solution/cti'; + +export const mockOptions = { + defaultIndex: ['logs-ti_*', 'filebeat-8*'], + docValueFields: [], + factoryQueryType: CtiQueries.dataSource, + filterQuery: '', + timerange: { + interval: '12h', + from: '2020-09-06T15:23:52.757Z', + to: '2020-09-07T15:23:52.757Z', + }, +}; + +export const expectedDsl = { + body: { + aggs: { + dataset: { + terms: { + field: 'event.dataset', + }, + aggs: { + name: { + terms: { + field: 'threat.feed.name', + }, + }, + dashboard: { + terms: { + field: 'threat.feed.dashboard_id', + }, + }, + }, + }, + }, + query: { + bool: { + filter: [ + { + range: { + '@timestamp': { + gte: '2020-09-06T15:23:52.757Z', + lte: '2020-09-07T15:23:52.757Z', + format: 'strict_date_optional_time', + }, + }, + }, + ], + }, + }, + }, + ignore_unavailable: true, + index: ['logs-ti_*', 'filebeat-8*'], + size: 0, + track_total_hits: true, + allow_no_indices: true, +}; + +describe('buildbuildTiDataSourceQueryQuery', () => { + test('build query from options correctly', () => { + expect(buildTiDataSourceQuery(mockOptions)).toEqual(expectedDsl); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.ts new file mode 100644 index 0000000000000..62ad39111b134 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/cti/threat_intel_source/query.threat_intel_source.dsl.ts @@ -0,0 +1,59 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { CtiDataSourceRequestOptions } from '../../../../../../common/search_strategy/security_solution/cti'; + +export const buildTiDataSourceQuery = ({ + timerange, + defaultIndex, +}: CtiDataSourceRequestOptions) => { + const filter = []; + + if (timerange) { + filter.push({ + range: { + '@timestamp': { + gte: timerange.from, + lte: timerange.to, + format: 'strict_date_optional_time', + }, + }, + }); + } + + const dslQuery = { + size: 0, + index: defaultIndex, + allow_no_indices: true, + ignore_unavailable: true, + track_total_hits: true, + body: { + aggs: { + dataset: { + terms: { field: 'event.dataset' }, + aggs: { + name: { + terms: { field: 'threat.feed.name' }, + }, + dashboard: { + terms: { + field: 'threat.feed.dashboard_id', + }, + }, + }, + }, + }, + query: { + bool: { + filter, + }, + }, + }, + }; + + return dslQuery; +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts index 9aef01d953c82..36add5af0ed25 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.test.ts @@ -18,6 +18,7 @@ import { authentications, authenticationsEntities } from './authentications'; import { hostsKpiAuthentications, hostsKpiAuthenticationsEntities } from './kpi/authentications'; import { hostsKpiHosts, hostsKpiHostsEntities } from './kpi/hosts'; import { hostsKpiUniqueIps, hostsKpiUniqueIpsEntities } from './kpi/unique_ips'; +import { hostsKpiRiskyHosts } from './kpi/risky_hosts'; jest.mock('./all'); jest.mock('./details'); @@ -45,6 +46,7 @@ describe('hostsFactory', () => { [HostsKpiQueries.kpiAuthenticationsEntities]: hostsKpiAuthenticationsEntities, [HostsKpiQueries.kpiHosts]: hostsKpiHosts, [HostsKpiQueries.kpiHostsEntities]: hostsKpiHostsEntities, + [HostsKpiQueries.kpiRiskyHosts]: hostsKpiRiskyHosts, [HostsKpiQueries.kpiUniqueIpsEntities]: hostsKpiUniqueIpsEntities, [HostsKpiQueries.kpiUniqueIps]: hostsKpiUniqueIps, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts index 5b501099a21ed..f182280667e13 100644 --- a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/index.ts @@ -22,6 +22,7 @@ import { hostsKpiAuthentications, hostsKpiAuthenticationsEntities } from './kpi/ import { hostsKpiHosts, hostsKpiHostsEntities } from './kpi/hosts'; import { hostsKpiUniqueIps, hostsKpiUniqueIpsEntities } from './kpi/unique_ips'; import { riskScore } from './risk_score'; +import { hostsKpiRiskyHosts } from './kpi/risky_hosts'; export const hostsFactory: Record< HostsQueries | HostsKpiQueries, @@ -40,6 +41,7 @@ export const hostsFactory: Record< [HostsKpiQueries.kpiAuthenticationsEntities]: hostsKpiAuthenticationsEntities, [HostsKpiQueries.kpiHosts]: hostsKpiHosts, [HostsKpiQueries.kpiHostsEntities]: hostsKpiHostsEntities, + [HostsKpiQueries.kpiRiskyHosts]: hostsKpiRiskyHosts, [HostsKpiQueries.kpiUniqueIps]: hostsKpiUniqueIps, [HostsKpiQueries.kpiUniqueIpsEntities]: hostsKpiUniqueIpsEntities, }; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/__mocks__/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/__mocks__/index.ts new file mode 100644 index 0000000000000..c0522d61e3804 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/__mocks__/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { + HostsKpiQueries, + HostsKpiRiskyHostsRequestOptions, +} from '../../../../../../../../common/search_strategy'; + +export const mockOptions: HostsKpiRiskyHostsRequestOptions = { + defaultIndex: [ + 'apm-*-transaction*', + 'traces-apm*', + 'auditbeat-*', + 'endgame-*', + 'filebeat-*', + 'logs-*', + 'packetbeat-*', + 'winlogbeat-*', + ], + factoryQueryType: HostsKpiQueries.kpiRiskyHosts, + filterQuery: + '{"bool":{"must":[],"filter":[{"match_all":{}},{"bool":{"filter":[{"bool":{"should":[{"exists":{"field":"host.name"}}],"minimum_should_match":1}}]}}],"should":[],"must_not":[]}}', + timerange: { interval: '12h', from: '2020-09-07T09:47:28.606Z', to: '2020-09-08T09:47:28.606Z' }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/index.test.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/index.test.ts new file mode 100644 index 0000000000000..cbfe63d86ea73 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/index.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { hostsKpiRiskyHosts } from '.'; +import * as buildQuery from './query.hosts_kpi_risky_hosts.dsl'; +import { mockOptions } from './__mocks__'; + +describe('buildHostsKpiRiskyHostsQuery search strategy', () => { + const buildHostsKpiRiskyHostsQuery = jest.spyOn(buildQuery, 'buildHostsKpiRiskyHostsQuery'); + + describe('buildDsl', () => { + test('should build dsl query', () => { + hostsKpiRiskyHosts.buildDsl(mockOptions); + expect(buildHostsKpiRiskyHostsQuery).toHaveBeenCalledWith(mockOptions); + }); + }); +}); diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/index.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/index.ts new file mode 100644 index 0000000000000..0e0f48f37efcf --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/index.ts @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { getOr } from 'lodash/fp'; + +import type { IEsSearchResponse } from '../../../../../../../../../../src/plugins/data/common'; +import type { HostsKpiQueries } from '../../../../../../../common/search_strategy'; + +import type { + HostsKpiRiskyHostsRequestOptions, + HostsKpiRiskyHostsStrategyResponse, + HostRiskSeverity, +} from '../../../../../../../common/search_strategy/security_solution/hosts/kpi/risky_hosts'; +import { inspectStringifyObject } from '../../../../../../utils/build_query'; +import type { SecuritySolutionFactory } from '../../../types'; +import { buildHostsKpiRiskyHostsQuery } from './query.hosts_kpi_risky_hosts.dsl'; + +interface AggBucket { + key: HostRiskSeverity; + doc_count: number; +} + +export const hostsKpiRiskyHosts: SecuritySolutionFactory = { + buildDsl: (options: HostsKpiRiskyHostsRequestOptions) => buildHostsKpiRiskyHostsQuery(options), + parse: async ( + options: HostsKpiRiskyHostsRequestOptions, + response: IEsSearchResponse + ): Promise => { + const inspect = { + dsl: [inspectStringifyObject(buildHostsKpiRiskyHostsQuery(options))], + }; + + const riskBuckets = getOr([], 'aggregations.risk.buckets', response.rawResponse); + + const riskyHosts: Record = riskBuckets.reduce( + (cummulative: Record, bucket: AggBucket) => ({ + ...cummulative, + [bucket.key]: bucket.doc_count, + }), + {} + ); + + return { + ...response, + riskyHosts, + inspect, + }; + }, +}; diff --git a/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/query.hosts_kpi_risky_hosts.dsl.ts b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/query.hosts_kpi_risky_hosts.dsl.ts new file mode 100644 index 0000000000000..201d73c4ebb18 --- /dev/null +++ b/x-pack/plugins/security_solution/server/search_strategy/security_solution/factory/hosts/kpi/risky_hosts/query.hosts_kpi_risky_hosts.dsl.ts @@ -0,0 +1,50 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { HostsKpiRiskyHostsRequestOptions } from '../../../../../../../common/search_strategy/security_solution/hosts/kpi/risky_hosts'; +import { createQueryFilterClauses } from '../../../../../../utils/build_query'; + +export const buildHostsKpiRiskyHostsQuery = ({ + filterQuery, + timerange: { from, to }, + defaultIndex, +}: HostsKpiRiskyHostsRequestOptions) => { + const filter = [ + ...createQueryFilterClauses(filterQuery), + { + range: { + '@timestamp': { + gte: from, + lte: to, + format: 'strict_date_optional_time', + }, + }, + }, + ]; + + const dslQuery = { + index: defaultIndex, + allow_no_indices: false, + ignore_unavailable: true, + track_total_hits: false, + body: { + aggs: { + risk: { + terms: { field: 'risk.keyword' }, + }, + }, + query: { + bool: { + filter, + }, + }, + size: 0, + }, + }; + + return dslQuery; +}; 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 55efcd4d15a33..7e05d4df7644a 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -5962,9 +5962,6 @@ "available": { "type": "boolean" }, - "browser_type": { - "type": "keyword" - }, "enabled": { "type": "boolean" }, diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.test.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.test.ts index bee6db57bee42..14e4b25673ad1 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.test.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.test.ts @@ -17,7 +17,7 @@ describe('getLicenseFromLocalOrMaster', () => { const license = await getLicenseFromLocalOrMaster(esClient); expect(license).toBeUndefined(); - expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true }); expect(esClient.license.get).toHaveBeenCalledTimes(1); }); @@ -30,7 +30,7 @@ describe('getLicenseFromLocalOrMaster', () => { const license = await getLicenseFromLocalOrMaster(esClient); expect(license).toStrictEqual({ type: 'basic' }); - expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true }); expect(esClient.license.get).toHaveBeenCalledTimes(1); }); @@ -42,8 +42,8 @@ describe('getLicenseFromLocalOrMaster', () => { await expect(getLicenseFromLocalOrMaster(esClient)).rejects.toStrictEqual(error); - expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); - expect(esClient.license.get).toHaveBeenCalledWith({ local: false, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: false }); expect(esClient.license.get).toHaveBeenCalledTimes(2); }); @@ -58,8 +58,8 @@ describe('getLicenseFromLocalOrMaster', () => { const license = await getLicenseFromLocalOrMaster(esClient); expect(license).toStrictEqual({ type: 'basic' }); - expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); - expect(esClient.license.get).toHaveBeenCalledWith({ local: false, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: false }); expect(esClient.license.get).toHaveBeenCalledTimes(2); }); @@ -72,14 +72,14 @@ describe('getLicenseFromLocalOrMaster', () => { const license = await getLicenseFromLocalOrMaster(esClient); expect(license).toBeUndefined(); - expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); - expect(esClient.license.get).toHaveBeenCalledWith({ local: false, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: false }); expect(esClient.license.get).toHaveBeenCalledTimes(2); // Now the cached license is cleared, next request only goes for local and gives up when failed esClient.license.get.mockClear(); await expect(getLicenseFromLocalOrMaster(esClient)).resolves.toBeUndefined(); - expect(esClient.license.get).toHaveBeenCalledWith({ local: true, accept_enterprise: true }); + expect(esClient.license.get).toHaveBeenCalledWith({ local: true }); expect(esClient.license.get).toHaveBeenCalledTimes(1); }); }); diff --git a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts index ff16d272b17b2..2dc63a2f72bd1 100644 --- a/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts +++ b/x-pack/plugins/telemetry_collection_xpack/server/telemetry_collection/get_license.ts @@ -14,15 +14,13 @@ let cachedLicense: ESLicense | undefined; async function fetchLicense(esClient: ElasticsearchClient, local: boolean) { const { body } = await esClient.license.get({ local, - // For versions >= 7.6 and < 8.0, this flag is needed otherwise 'platinum' is returned for 'enterprise' license. - accept_enterprise: true, }); return body; } /** * Get the cluster's license from the connected node. * - * This is the equivalent of GET /_license?local=true&accept_enterprise=true. + * This is the equivalent of GET /_license?local=true. * * Like any X-Pack related API, X-Pack must installed for this to work. * diff --git a/x-pack/plugins/timelines/common/ecs/threat/index.ts b/x-pack/plugins/timelines/common/ecs/threat/index.ts index 8136c5693d0e6..84a1098de69d8 100644 --- a/x-pack/plugins/timelines/common/ecs/threat/index.ts +++ b/x-pack/plugins/timelines/common/ecs/threat/index.ts @@ -31,9 +31,14 @@ export interface ThreatIndicatorEcs { type?: string[]; } +export interface ThreatFeedEcs { + name?: string[]; +} + export interface ThreatEnrichmentEcs { indicator?: ThreatIndicatorEcs; matched?: ThreatMatchEcs; + feed?: ThreatFeedEcs; } export interface ThreatEcs { diff --git a/x-pack/plugins/timelines/common/index.ts b/x-pack/plugins/timelines/common/index.ts index 004742c3ee3d4..0002dd6eb1432 100644 --- a/x-pack/plugins/timelines/common/index.ts +++ b/x-pack/plugins/timelines/common/index.ts @@ -5,14 +5,82 @@ * 2.0. */ -// TODO: https://github.com/elastic/kibana/issues/110904 -/* eslint-disable @kbn/eslint/no_export_all */ +// Careful of exporting anything from this file as any file(s) you export here will cause your page bundle size to increase. +// If you're using functions/types/etc... internally or within integration tests it's best to import directly from their paths +// than expose the functions/types/etc... here. You should _only_ expose functions/types/etc... that need to be shared with other plugins here. + +// When you do have to add things here you might want to consider creating a package to share with +// other plugins instead as packages are easier to break down and you do not have to carry the cost of extra plugin weight on +// first download since the other plugins/areas of your code can directly pull from the package in their async imports. +// See: https://docs.elastic.dev/kibana-dev-docs/key-concepts/platform-intro#public-plugin-api export { DELETED_SECURITY_SOLUTION_DATA_VIEW } from './constants'; -export * from './types'; -export * from './search_strategy'; -export * from './utils/accessibility'; +export type { + ActionProps, + AlertWorkflowStatus, + CellValueElementProps, + CreateFieldComponentType, + ColumnId, + ColumnRenderer, + ColumnHeaderType, + ColumnHeaderOptions, + ControlColumnProps, + DataProvidersAnd, + DataProvider, + GenericActionRowCellRenderProps, + HeaderActionProps, + HeaderCellRender, + QueryOperator, + QueryMatch, + RowCellRender, + RowRenderer, + SetEventsDeleted, + SetEventsLoading, +} from './types'; + +export { IS_OPERATOR, EXISTS_OPERATOR, DataProviderType, TimelineId } from './types'; + +export type { + BeatFields, + BrowserField, + BrowserFields, + CursorType, + DocValueFields, + EqlOptionsData, + EqlOptionsSelected, + FieldsEqlOptions, + FieldInfo, + IndexField, + IndexFieldsStrategyRequest, + IndexFieldsStrategyResponse, + LastTimeDetails, + TimelineNonEcsData, + Inspect, + SortField, + TimerangeInput, + TimelineEdges, + TimelineItem, + TimelineEventsAllStrategyResponse, + TimelineEventsAllRequestOptions, + TimelineEventsDetailsItem, + TimelineEventsDetailsStrategyResponse, + TimelineEventsDetailsRequestOptions, + TimelineEventsLastEventTimeStrategyResponse, + TimelineEventsLastEventTimeRequestOptions, + TimelineEqlRequestOptions, + TimelineEqlResponse, + TimelineKpiStrategyRequest, + TimelineKpiStrategyResponse, + TotalValue, + PaginationInputPaginated, +} from './search_strategy'; -export const PLUGIN_ID = 'timelines'; -export const PLUGIN_NAME = 'timelines'; +export { + Direction, + EntityType, + LastEventIndexKey, + EMPTY_BROWSER_FIELDS, + EMPTY_DOCVALUE_FIELD, + EMPTY_INDEX_FIELDS, +} from './search_strategy'; diff --git a/x-pack/plugins/timelines/common/types/timeline/cells/index.ts b/x-pack/plugins/timelines/common/types/timeline/cells/index.ts index c2f785d54c5d8..32c28dfb7adf7 100644 --- a/x-pack/plugins/timelines/common/types/timeline/cells/index.ts +++ b/x-pack/plugins/timelines/common/types/timeline/cells/index.ts @@ -7,7 +7,7 @@ import { EuiDataGridCellValueElementProps } from '@elastic/eui'; import type { Filter } from '@kbn/es-query'; -import { RowRenderer } from '../../..'; +import { RowRenderer } from '../../../types'; import { Ecs } from '../../../ecs'; import { BrowserFields, TimelineNonEcsData } from '../../../search_strategy'; import { ColumnHeaderOptions } from '../columns'; diff --git a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx index c734442d5a77b..f634e460844e8 100644 --- a/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx +++ b/x-pack/plugins/timelines/public/components/actions/timeline/cases/add_to_case_action.tsx @@ -8,11 +8,11 @@ import React, { memo, useMemo, useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { CaseStatuses, StatusAll, CasesContextValue } from '../../../../../../cases/common'; -import { TimelineItem } from '../../../../../common/'; +import { TimelineItem } from '../../../../../common/search_strategy'; import { useAddToCase, normalizedEventFields } from '../../../../hooks/use_add_to_case'; import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public'; import { TimelinesStartServices } from '../../../../types'; -import { tGridActions } from '../../../../'; +import { setOpenAddToExistingCase, setOpenAddToNewCase } from '../../../../store/t_grid/actions'; export interface AddToCaseActionProps { event?: TimelineItem; @@ -68,8 +68,7 @@ const AddToCaseActionComponent: React.FC = ({ updateCase: onCaseSuccess, userCanCrud: casePermissions?.crud ?? false, owner: [owner], - onClose: () => - dispatch(tGridActions.setOpenAddToExistingCase({ id: eventId, isOpen: false })), + onClose: () => dispatch(setOpenAddToExistingCase({ id: eventId, isOpen: false })), }; }, [ casePermissions?.crud, @@ -84,7 +83,7 @@ const AddToCaseActionComponent: React.FC = ({ ]); const closeCaseFlyoutOpen = useCallback(() => { - dispatch(tGridActions.setOpenAddToNewCase({ id: eventId, isOpen: false })); + dispatch(setOpenAddToNewCase({ id: eventId, isOpen: false })); }, [dispatch, eventId]); const createCaseFlyoutProps = useMemo(() => { diff --git a/x-pack/plugins/timelines/public/components/drag_and_drop/helpers.ts b/x-pack/plugins/timelines/public/components/drag_and_drop/helpers.ts index c32241cb876c4..237618594fedb 100644 --- a/x-pack/plugins/timelines/public/components/drag_and_drop/helpers.ts +++ b/x-pack/plugins/timelines/public/components/drag_and_drop/helpers.ts @@ -10,8 +10,10 @@ import { KEYBOARD_DRAG_OFFSET, getFieldIdFromDraggable } from '@kbn/securitysolu import { Dispatch } from 'redux'; import { isString, keyBy } from 'lodash/fp'; -import { stopPropagationAndPreventDefault, TimelineId } from '../../../common'; -import type { BrowserField, BrowserFields, ColumnHeaderOptions } from '../../../common'; +import { stopPropagationAndPreventDefault } from '../../../common/utils/accessibility'; +import { TimelineId } from '../../../common/types'; +import type { BrowserField, BrowserFields } from '../../../common/search_strategy'; +import type { ColumnHeaderOptions } from '../../../common/types'; import { tGridActions } from '../../store/t_grid'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../t_grid/body/constants'; diff --git a/x-pack/plugins/timelines/public/components/drag_and_drop/index.tsx b/x-pack/plugins/timelines/public/components/drag_and_drop/index.tsx index 65ec238ea4d40..33c568ed0231c 100644 --- a/x-pack/plugins/timelines/public/components/drag_and_drop/index.tsx +++ b/x-pack/plugins/timelines/public/components/drag_and_drop/index.tsx @@ -17,7 +17,8 @@ import React, { useCallback } from 'react'; import { DropResult, DragDropContext, BeforeCapture } from 'react-beautiful-dnd'; import { useDispatch } from 'react-redux'; -import type { ColumnHeaderOptions, BrowserFields } from '../../../common'; +import type { BrowserFields } from '../../../common/search_strategy'; +import type { ColumnHeaderOptions } from '../../../common/types'; import { useAddToTimelineSensor } from '../../hooks/use_add_to_timeline'; import { addFieldToTimelineColumns, getTimelineIdFromColumnDroppableId } from './helpers'; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx index b3ff8c9533cbe..0ab5cb0956551 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/add_to_timeline.tsx @@ -11,12 +11,13 @@ import { DraggableId } from 'react-beautiful-dnd'; import { useDispatch } from 'react-redux'; import { isEmpty } from 'lodash'; -import { DataProvider, stopPropagationAndPreventDefault, TimelineId } from '../../../../common'; +import { stopPropagationAndPreventDefault } from '../../../../common/utils/accessibility'; +import { DataProvider, TimelineId } from '../../../../common/types'; import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; import { getAdditionalScreenReaderOnlyContext } from '../utils'; import { useAddToTimeline } from '../../../hooks/use_add_to_timeline'; import { HoverActionComponentProps } from './types'; -import { tGridActions } from '../../..'; +import { addProviderToTimeline } from '../../../store/t_grid/actions'; import { useAppToasts } from '../../../hooks/use_app_toasts'; import * as i18n from './translations'; @@ -74,7 +75,7 @@ const AddToTimelineButton: React.FC = React.memo( addDataProvider.forEach((provider) => { if (provider) { dispatch( - tGridActions.addProviderToTimeline({ + addProviderToTimeline({ id: TimelineId.active, dataProvider: provider, }) diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx index c7936c6b17aaf..9a6c3b51e3f6c 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/column_toggle.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { EuiContextMenuItem, EuiButtonEmpty, EuiButtonIcon, EuiToolTip } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { stopPropagationAndPreventDefault } from '../../../../common'; +import { stopPropagationAndPreventDefault } from '../../../../common/utils/accessibility'; import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; import { getAdditionalScreenReaderOnlyContext } from '../utils'; import { defaultColumnHeaderType } from '../../t_grid/body/column_headers/default_headers'; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx index 0c1f8fbacd221..3c4b7379188b9 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/copy.tsx @@ -10,7 +10,7 @@ import copy from 'copy-to-clipboard'; import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import { i18n } from '@kbn/i18n'; -import { stopPropagationAndPreventDefault } from '../../../../common'; +import { stopPropagationAndPreventDefault } from '../../../../common/utils/accessibility'; import { WithCopyToClipboard } from '../../clipboard/with_copy_to_clipboard'; import { HoverActionComponentProps } from './types'; import { COPY_TO_CLIPBOARD_BUTTON_CLASS_NAME } from '../../clipboard'; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx index 7b8ecdb0f582f..8e46f99ab4543 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_for_value.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { stopPropagationAndPreventDefault } from '../../../../common'; +import { stopPropagationAndPreventDefault } from '../../../../common/utils/accessibility'; import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; import { createFilter, getAdditionalScreenReaderOnlyContext } from '../utils'; import { HoverActionComponentProps, FilterValueFnArgs } from './types'; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx index b4867936588ef..818ef37748c85 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/filter_out_value.tsx @@ -9,7 +9,7 @@ import React, { useCallback, useEffect, useMemo } from 'react'; import { i18n } from '@kbn/i18n'; import { EuiButtonIcon, EuiToolTip } from '@elastic/eui'; -import { stopPropagationAndPreventDefault } from '../../../../common'; +import { stopPropagationAndPreventDefault } from '../../../../common/utils/accessibility'; import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; import { createFilter, getAdditionalScreenReaderOnlyContext } from '../utils'; import { HoverActionComponentProps, FilterValueFnArgs } from './types'; diff --git a/x-pack/plugins/timelines/public/components/hover_actions/actions/overflow.tsx b/x-pack/plugins/timelines/public/components/hover_actions/actions/overflow.tsx index aa582504f4f71..ffd68a8c5efbf 100644 --- a/x-pack/plugins/timelines/public/components/hover_actions/actions/overflow.tsx +++ b/x-pack/plugins/timelines/public/components/hover_actions/actions/overflow.tsx @@ -17,7 +17,7 @@ import { } from '@elastic/eui'; import styled from 'styled-components'; -import { stopPropagationAndPreventDefault } from '../../../../common'; +import { stopPropagationAndPreventDefault } from '../../../../common/utils/accessibility'; import { TooltipWithKeyboardShortcut } from '../../tooltip_with_keyboard_shortcut'; import { getAdditionalScreenReaderOnlyContext } from '../utils'; import { HoverActionComponentProps } from './types'; diff --git a/x-pack/plugins/reporting/server/browsers/download/index.ts b/x-pack/plugins/timelines/public/components/stateful_event_context.ts similarity index 58% rename from x-pack/plugins/reporting/server/browsers/download/index.ts rename to x-pack/plugins/timelines/public/components/stateful_event_context.ts index d54a7a1f30cc7..830a28a3c34e2 100644 --- a/x-pack/plugins/reporting/server/browsers/download/index.ts +++ b/x-pack/plugins/timelines/public/components/stateful_event_context.ts @@ -5,4 +5,7 @@ * 2.0. */ -export { ensureBrowserDownloaded } from './ensure_downloaded'; +import { createContext } from 'react'; +import { StatefulEventContextType } from '../types'; + +export const StatefulEventContext = createContext(null); diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/helpers.ts b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/helpers.ts index b764c6d5eb96c..7a3cad47bdcba 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/helpers.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/header/helpers.ts @@ -5,8 +5,8 @@ * 2.0. */ -import { Direction } from '../../../../../../common'; -import type { ColumnHeaderOptions } from '../../../../../../common'; +import { Direction } from '../../../../../../common/search_strategy'; +import type { ColumnHeaderOptions } from '../../../../../../common/types'; import { assertUnreachable } from '../../../../../../common/utility_types'; import { Sort, SortDirection } from '../../sort'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx index b174eb8cc76c4..9de28b870aadb 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/column_headers/helpers.test.tsx @@ -23,7 +23,7 @@ import { DEFAULT_DATE_COLUMN_MIN_WIDTH, } from '../constants'; import { mockBrowserFields } from '../../../../mock/browser_fields'; -import { ColumnHeaderOptions } from '../../../../../common'; +import { ColumnHeaderOptions } from '../../../../../common/types'; window.matchMedia = jest.fn().mockImplementation((query) => { return { diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx index 56d2df3754fa8..aa46ba8976363 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.test.tsx @@ -6,7 +6,7 @@ */ import { render, fireEvent } from '@testing-library/react'; -import { ActionProps, HeaderActionProps, TimelineTabs } from '../../../../../common'; +import { ActionProps, HeaderActionProps, TimelineTabs } from '../../../../../common/types'; import { HeaderCheckBox, RowCheckBox } from './checkbox'; import React from 'react'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx index 6feb972b9a813..77a761edebd49 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/checkbox.tsx @@ -8,7 +8,7 @@ import { EuiCheckbox, EuiLoadingSpinner } from '@elastic/eui'; import React, { useCallback } from 'react'; import { ALERT_RULE_PRODUCER } from '@kbn/rule-data-utils'; -import { ActionProps, HeaderActionProps } from '../../../../../common'; +import type { ActionProps, HeaderActionProps } from '../../../../../common/types'; import * as i18n from './translations'; export const RowCheckBox = ({ diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/index.tsx index dbf7fc9b99cff..b11bdd2b54c22 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/control_columns/index.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { ControlColumnProps } from '../../../../../common'; +import type { ControlColumnProps } from '../../../../../common/types'; import { HeaderCheckBox, RowCheckBox } from './checkbox'; export const checkBoxControlColumn: ControlColumnProps = { diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx index 43a6284946b6a..31b1963a24edd 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/events/stateful_row_renderer/index.tsx @@ -13,7 +13,7 @@ import { ARIA_COLINDEX_ATTRIBUTE, ARIA_ROWINDEX_ATTRIBUTE, getRowRendererClassName, -} from '../../../../../../common'; +} from '../../../../../../common/utils/accessibility'; import { useStatefulEventFocus } from '../use_stateful_event_focus'; import * as i18n from '../translations'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/events/use_stateful_event_focus/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/events/use_stateful_event_focus/index.tsx index d39015995daa0..2710b8c463623 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/events/use_stateful_event_focus/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/events/use_stateful_event_focus/index.tsx @@ -6,8 +6,13 @@ */ import React, { useCallback, useState, useMemo } from 'react'; -import { focusColumn, isArrowDownOrArrowUp, isArrowUp, isEscape } from '../../../../../../common'; -import type { OnColumnFocused } from '../../../../../../common'; +import { + focusColumn, + isArrowDownOrArrowUp, + isArrowUp, + isEscape, +} from '../../../../../../common/utils/accessibility'; +import type { OnColumnFocused } from '../../../../../../common/utils/accessibility'; type FocusOwnership = 'not-owned' | 'owned'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx index 3ce066e02731e..56388b16b1b66 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/helpers.test.tsx @@ -7,7 +7,7 @@ import { omit } from 'lodash/fp'; -import { ColumnHeaderOptions } from '../../../../common'; +import { ColumnHeaderOptions } from '../../../../common/types'; import { Ecs } from '../../../../common/ecs'; import { allowSorting, diff --git a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx index 8ed848a44c25b..bed3290f4d310 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/body/index.tsx @@ -68,7 +68,8 @@ import type { BrowserFields } from '../../../../common/search_strategy/index_fie import type { OnRowSelected, OnSelectAll } from '../types'; import type { Refetch } from '../../../store/t_grid/inputs'; import { getPageRowIndex } from '../../../../common/utils/pagination'; -import { StatefulEventContext, StatefulFieldsBrowser } from '../../../'; +import { StatefulEventContext } from '../../../components/stateful_event_context'; +import { StatefulFieldsBrowser } from '../../../components/t_grid/toolbar/fields_browser'; import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../store/t_grid'; import { useDeepEqualSelector } from '../../../hooks/use_selector'; import { RowAction } from './row_action'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx index ea823630fe005..c2e686c92471b 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/event_rendered_view/index.tsx @@ -27,7 +27,8 @@ import styled from 'styled-components'; import { useUiSetting } from '../../../../../../../src/plugins/kibana_react/public'; -import type { BrowserFields, RowRenderer, TimelineItem } from '../../../../common'; +import type { BrowserFields, TimelineItem } from '../../../../common/search_strategy'; +import type { RowRenderer } from '../../../../common/types'; import { RuleName } from '../../rule_name'; import { isEventBuildingBlockType } from '../body/helpers'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx index caf97bdba7dae..6d738c49e5b6b 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/helpers.tsx @@ -15,7 +15,7 @@ import { getTableSkipFocus, handleSkipFocus, stopPropagationAndPreventDefault, -} from '../../../common'; +} from '../../../common/utils/accessibility'; import type { BrowserFields } from '../../../common/search_strategy/index_fields'; import { DataProviderType, EXISTS_OPERATOR } from '../../../common/types/timeline'; import type { DataProvider, DataProvidersAnd } from '../../../common/types/timeline'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx index 8a3662d7cbe55..0fde09f555e00 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/bulk_actions/alert_status_bulk_actions.tsx @@ -13,7 +13,7 @@ import type { SetEventsDeleted, OnUpdateAlertStatusSuccess, OnUpdateAlertStatusError, -} from '../../../../../common'; +} from '../../../../../common/types'; import type { Refetch } from '../../../../store/t_grid/inputs'; import { tGridActions, TGridModel, tGridSelectors, TimelineState } from '../../../../store/t_grid'; import { BulkActions } from './'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx index bac0a2eceda0c..ffb93aee11b55 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/categories_pane.tsx @@ -13,8 +13,8 @@ import { DATA_COLINDEX_ATTRIBUTE, DATA_ROWINDEX_ATTRIBUTE, onKeyDownFocusHandler, -} from '../../../../../common'; -import type { BrowserFields } from '../../../../../common'; +} from '../../../../../common/utils/accessibility'; +import type { BrowserFields } from '../../../../../common/search_strategy'; import { getCategoryColumns } from './category_columns'; import { CATEGORIES_PANE_CLASS_NAME, TABLE_HEIGHT } from './helpers'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx index 1b6194bff0f90..3130c46aa0684 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category.tsx @@ -14,8 +14,9 @@ import { DATA_COLINDEX_ATTRIBUTE, DATA_ROWINDEX_ATTRIBUTE, onKeyDownFocusHandler, -} from '../../../../../common'; -import type { BrowserFields, OnUpdateColumns } from '../../../../../common'; +} from '../../../../../common/utils/accessibility'; +import type { BrowserFields } from '../../../../../common/search_strategy'; +import type { OnUpdateColumns } from '../../../../../common/types'; import { CategoryTitle } from './category_title'; import { getFieldColumns } from './field_items'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx index 3c3d644edbd08..0fdf71ff5ffe1 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_columns.tsx @@ -27,7 +27,8 @@ import { import * as i18n from './translations'; import { tGridSelectors } from '../../../../store/t_grid'; import { getColumnsWithTimestamp } from '../../../utils/helpers'; -import type { OnUpdateColumns, BrowserFields } from '../../../../../common'; +import type { BrowserFields } from '../../../../../common/search_strategy'; +import type { OnUpdateColumns } from '../../../../../common/types'; const CategoryName = styled.span<{ bold: boolean }>` .euiText { diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx index 2296b4c855d42..0858f30a35246 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/category_title.tsx @@ -9,7 +9,8 @@ import { EuiFlexGroup, EuiFlexItem, EuiScreenReaderOnly, EuiTitle } from '@elast import React from 'react'; import { CountBadge, getFieldBrowserCategoryTitleClassName, getFieldCount } from './helpers'; -import type { BrowserFields, OnUpdateColumns } from '../../../../../common'; +import type { BrowserFields } from '../../../../../common/search_strategy'; +import type { OnUpdateColumns } from '../../../../../common/types'; import { ViewAllButton } from './category_columns'; import * as i18n from './translations'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx index b7f72e66b1a87..b16dc4b7171cc 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_browser.tsx @@ -21,12 +21,13 @@ import React, { useEffect, useCallback, useRef, useMemo } from 'react'; import styled from 'styled-components'; import { useDispatch } from 'react-redux'; -import type { - BrowserFields, - ColumnHeaderOptions, - CreateFieldComponentType, -} from '../../../../../common'; -import { isEscape, isTab, stopPropagationAndPreventDefault } from '../../../../../common'; +import type { BrowserFields } from '../../../../../common/search_strategy'; +import type { ColumnHeaderOptions, CreateFieldComponentType } from '../../../../../common/types'; +import { + isEscape, + isTab, + stopPropagationAndPreventDefault, +} from '../../../../../common/utils/accessibility'; import { CategoriesPane } from './categories_pane'; import { FieldsPane } from './fields_pane'; import { Search } from './search'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx index 789aeeeb187fd..a4c830c3d8808 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.test.tsx @@ -16,7 +16,7 @@ import { Category } from './category'; import { getFieldColumns, getFieldItems } from './field_items'; import { FIELDS_PANE_WIDTH } from './helpers'; import { useMountAppended } from '../../../utils/use_mount_appended'; -import { ColumnHeaderOptions } from '../../../../../common'; +import { ColumnHeaderOptions } from '../../../../../common/types'; const selectedCategoryId = 'base'; const selectedCategoryFields = mockBrowserFields[selectedCategoryId].fields; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx index 8015be2bcc857..a979e209bf64a 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/field_items.tsx @@ -19,7 +19,8 @@ import styled from 'styled-components'; import { getEmptyValue } from '../../../empty_value'; import { getExampleText, getIconFromType } from '../../../utils/helpers'; -import type { ColumnHeaderOptions, BrowserField } from '../../../../../common'; +import type { BrowserField } from '../../../../../common/search_strategy'; +import type { ColumnHeaderOptions } from '../../../../../common/types'; import { defaultColumnHeaderType } from '../../body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../../body/constants'; import { TruncatableText } from '../../../truncatable_text'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx index 11ad3b881b637..5345475a02501 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/fields_pane.tsx @@ -15,7 +15,8 @@ import { getFieldItems } from './field_items'; import { FIELDS_PANE_WIDTH, TABLE_HEIGHT } from './helpers'; import * as i18n from './translations'; -import type { BrowserFields, ColumnHeaderOptions, OnUpdateColumns } from '../../../../../common'; +import type { BrowserFields } from '../../../../../common/search_strategy'; +import type { ColumnHeaderOptions, OnUpdateColumns } from '../../../../../common/types'; import { tGridActions } from '../../../../store/t_grid'; const NoFieldsPanel = styled.div` diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx index ee7d0a334ed23..75166bba5b111 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.test.tsx @@ -16,7 +16,7 @@ import { getFieldCount, filterBrowserFieldsByFieldName, } from './helpers'; -import { BrowserFields } from '../../../../../common'; +import { BrowserFields } from '../../../../../common/search_strategy'; const timelineId = 'test'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx index 552c228363e49..5406940aab3e9 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/helpers.tsx @@ -13,9 +13,9 @@ import { elementOrChildrenHasFocus, skipFocusInContainerTo, stopPropagationAndPreventDefault, -} from '../../../../../public'; +} from '../../../../../common/utils/accessibility'; import { TimelineId } from '../../../../../public/types'; -import type { BrowserField, BrowserFields } from '../../../../../common'; +import type { BrowserField, BrowserFields } from '../../../../../common/search_strategy'; import { defaultHeaders } from '../../../../store/t_grid/defaults'; import { DEFAULT_CATEGORY_NAME } from '../../body/column_headers/default_headers'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx index f0f8c80fb0b85..935952fbf37e0 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/search.tsx @@ -8,7 +8,7 @@ import React from 'react'; import { EuiFieldSearch, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui'; import styled from 'styled-components'; -import type { BrowserFields } from '../../../../../common'; +import type { BrowserFields } from '../../../../../common/search_strategy'; import { getFieldBrowserSearchInputClassName, getFieldCount } from './helpers'; diff --git a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts index b4dd1ffb4b45a..bcf7287950624 100644 --- a/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts +++ b/x-pack/plugins/timelines/public/components/t_grid/toolbar/fields_browser/types.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { CreateFieldComponentType } from '../../../../../common'; +import { CreateFieldComponentType } from '../../../../../common/types'; import type { BrowserFields } from '../../../../../common/search_strategy/index_fields'; import type { ColumnHeaderOptions } from '../../../../../common/types/timeline/columns'; diff --git a/x-pack/plugins/timelines/public/components/utils/helpers.ts b/x-pack/plugins/timelines/public/components/utils/helpers.ts index 23f92c119d574..64bc328c97b05 100644 --- a/x-pack/plugins/timelines/public/components/utils/helpers.ts +++ b/x-pack/plugins/timelines/public/components/utils/helpers.ts @@ -6,7 +6,8 @@ */ import { getOr, isEmpty, uniqBy } from 'lodash/fp'; -import { BrowserField, BrowserFields, ColumnHeaderOptions } from '../../../common'; +import type { BrowserField, BrowserFields } from '../../../common/search_strategy'; +import { ColumnHeaderOptions } from '../../../common/types'; import { defaultHeaders } from '../t_grid/body/column_headers/default_headers'; import { DEFAULT_COLUMN_MIN_WIDTH } from '../t_grid/body/constants'; diff --git a/x-pack/plugins/timelines/public/container/index.tsx b/x-pack/plugins/timelines/public/container/index.tsx index 28e4a5ffda4af..0c748320bd0e4 100644 --- a/x-pack/plugins/timelines/public/container/index.tsx +++ b/x-pack/plugins/timelines/public/container/index.tsx @@ -12,7 +12,11 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { useDispatch } from 'react-redux'; import { Subscription } from 'rxjs'; import { MappingRuntimeFields } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; -import { tGridActions } from '..'; +import { + clearEventsLoading, + clearEventsDeleted, + setTimelineUpdatedAt, +} from '../store/t_grid/actions'; import type { DataPublicPluginStart } from '../../../../../src/plugins/data/public'; import { isCompleteResponse, isErrorResponse } from '../../../../../src/plugins/data/common'; @@ -143,8 +147,8 @@ export const useTimelineEvents = ({ const clearSignalsState = useCallback(() => { if (id != null && detectionsTimelineIds.some((timelineId) => timelineId === id)) { - dispatch(tGridActions.clearEventsLoading({ id })); - dispatch(tGridActions.clearEventsDeleted({ id })); + dispatch(clearEventsLoading({ id })); + dispatch(clearEventsDeleted({ id })); } }, [dispatch, id]); @@ -165,7 +169,7 @@ export const useTimelineEvents = ({ const setUpdated = useCallback( (updatedAt: number) => { - dispatch(tGridActions.setTimelineUpdatedAt({ id, updated: updatedAt })); + dispatch(setTimelineUpdatedAt({ id, updated: updatedAt })); }, [dispatch, id] ); diff --git a/x-pack/plugins/timelines/public/container/source/index.tsx b/x-pack/plugins/timelines/public/container/source/index.tsx index f13e48d82ac80..e5a84b3ee9d10 100644 --- a/x-pack/plugins/timelines/public/container/source/index.tsx +++ b/x-pack/plugins/timelines/public/container/source/index.tsx @@ -18,7 +18,7 @@ import { IndexField, IndexFieldsStrategyRequest, IndexFieldsStrategyResponse, -} from '../../../common'; +} from '../../../common/search_strategy'; import * as i18n from './translations'; import type { DataPublicPluginStart } from '../../../../../../src/plugins/data/public'; diff --git a/x-pack/plugins/timelines/public/container/use_update_alerts.ts b/x-pack/plugins/timelines/public/container/use_update_alerts.ts index b5d76f2fe324a..0638425564a70 100644 --- a/x-pack/plugins/timelines/public/container/use_update_alerts.ts +++ b/x-pack/plugins/timelines/public/container/use_update_alerts.ts @@ -9,7 +9,7 @@ import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { CoreStart } from '../../../../../src/core/public'; import { useKibana } from '../../../../../src/plugins/kibana_react/public'; -import { AlertStatus } from '../../../timelines/common'; +import type { AlertStatus } from '../../../timelines/common/types'; import { DETECTION_ENGINE_SIGNALS_STATUS_URL, RAC_ALERTS_BULK_UPDATE_URL, diff --git a/x-pack/plugins/timelines/public/hooks/use_add_to_case.ts b/x-pack/plugins/timelines/public/hooks/use_add_to_case.ts index 730243c4f1219..bd6ac89acbfb9 100644 --- a/x-pack/plugins/timelines/public/hooks/use_add_to_case.ts +++ b/x-pack/plugins/timelines/public/hooks/use_add_to_case.ts @@ -11,7 +11,7 @@ import { ALERT_RULE_NAME, ALERT_RULE_UUID } from '@kbn/rule-data-utils/technical import { useKibana } from '../../../../../src/plugins/kibana_react/public'; import { Case, SubCase } from '../../../cases/common'; import { TimelinesStartServices } from '../types'; -import { TimelineItem } from '../../common/'; +import { TimelineItem } from '../../common/search_strategy'; import { tGridActions } from '../store/t_grid'; import { useDeepEqualSelector } from './use_selector'; import { createUpdateSuccessToaster } from '../components/actions/timeline/cases/helpers'; diff --git a/x-pack/plugins/timelines/public/index.ts b/x-pack/plugins/timelines/public/index.ts index 2329d0a1bebf4..bfbd498eb5be0 100644 --- a/x-pack/plugins/timelines/public/index.ts +++ b/x-pack/plugins/timelines/public/index.ts @@ -5,29 +5,46 @@ * 2.0. */ -// TODO: https://github.com/elastic/kibana/issues/110904 -/* eslint-disable @kbn/eslint/no_export_all */ +// Careful of exporting anything from this file as any file(s) you export here will cause your page bundle size to increase. +// If you're using functions/types/etc... internally or within integration tests it's best to import directly from their paths +// than expose the functions/types/etc... here. You should _only_ expose functions/types/etc... that need to be shared with other plugins here. -import { createContext } from 'react'; +// When you do have to add things here you might want to consider creating a package to share with +// other plugins instead as packages are easier to break down and you do not have to carry the cost of extra plugin weight on +// first download since the other plugins/areas of your code can directly pull from the package in their async imports. +// See: https://docs.elastic.dev/kibana-dev-docs/key-concepts/platform-intro#public-plugin-api import { TimelinesPlugin } from './plugin'; -import type { StatefulEventContextType } from './types'; -export * as tGridActions from './store/t_grid/actions'; -export * as tGridSelectors from './store/t_grid/selectors'; -export type { - Inspect, - SortField, - TimerangeInput, - PaginationInputPaginated, - DocValueFields, - CursorType, - TotalValue, -} from '../common/search_strategy/common'; -export { Direction } from '../common/search_strategy/common'; + +export { + upsertColumn, + applyDeltaToColumnWidth, + updateColumnOrder, + updateColumnWidth, + toggleDetailPanel, + removeColumn, + updateIsLoading, + updateColumns, + updateItemsPerPage, + updateItemsPerPageOptions, + updateSort, + setSelected, + clearSelected, + setEventsLoading, + clearEventsLoading, + setEventsDeleted, + clearEventsDeleted, + initializeTGridSettings, + setTGridSelectAll, +} from './store/t_grid/actions'; + +export { getManageTimelineById } from './store/t_grid/selectors'; + export { tGridReducer } from './store/t_grid/reducer'; -export type { TGridModelForTimeline, TimelineState, TimelinesUIStart } from './types'; +export type { TimelinesUIStart, TGridModelForTimeline, TimelineState } from './types'; export type { TGridType, SortDirection, State as TGridState, TGridModel } from './types'; export type { OnColumnFocused } from '../common/utils/accessibility'; + export { ARIA_COLINDEX_ATTRIBUTE, ARIA_ROWINDEX_ATTRIBUTE, @@ -47,27 +64,25 @@ export { getRowRendererClassName, getTableSkipFocus, handleSkipFocus, - onFocusReFocusDraggable, onKeyDownFocusHandler, - skipFocusInContainerTo, stopPropagationAndPreventDefault, } from '../common/utils/accessibility'; -export { getPageRowIndex } from '../common/utils/pagination'; + export { addFieldToTimelineColumns, getTimelineIdFromColumnDroppableId, } from './components/drag_and_drop/helpers'; + export { getActionsColumnWidth } from './components/t_grid/body/column_headers/helpers'; export { DEFAULT_ACTION_BUTTON_WIDTH } from './components/t_grid/body/constants'; -export { StatefulFieldsBrowser } from './components/t_grid/toolbar/fields_browser'; export { useStatusBulkActionItems } from './hooks/use_status_bulk_action_items'; +export { getPageRowIndex } from '../common/utils/pagination'; + // This exports static code and TypeScript types, // as well as, Kibana Platform `plugin()` initializer. export function plugin() { return new TimelinesPlugin(); } -export const StatefulEventContext = createContext(null); +export { StatefulEventContext } from './components/stateful_event_context'; export { TimelineContext } from './components/t_grid/shared'; - -export type { CreateFieldComponentType } from '../common'; diff --git a/x-pack/plugins/timelines/public/mock/global_state.ts b/x-pack/plugins/timelines/public/mock/global_state.ts index 8bdd190cc8519..46457e5dec145 100644 --- a/x-pack/plugins/timelines/public/mock/global_state.ts +++ b/x-pack/plugins/timelines/public/mock/global_state.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { Direction } from '../../common'; +import { Direction } from '../../common/search_strategy'; import { TimelineState } from '../types'; import { defaultHeaders } from './header'; diff --git a/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts b/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts index e24d9d5c45768..464aa5df6c7c7 100644 --- a/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts +++ b/x-pack/plugins/timelines/public/mock/mock_timeline_data.ts @@ -1107,6 +1107,9 @@ export const mockTimelineData: TimelineItem[] = [ field: ['source.ip'], type: ['ip'], }, + feed: { + name: ['feed_name'], + }, }, ], }, diff --git a/x-pack/plugins/timelines/public/mock/t_grid.tsx b/x-pack/plugins/timelines/public/mock/t_grid.tsx index d1ec16d0be536..a9a48eccf19c5 100644 --- a/x-pack/plugins/timelines/public/mock/t_grid.tsx +++ b/x-pack/plugins/timelines/public/mock/t_grid.tsx @@ -11,7 +11,7 @@ import { TGridIntegratedProps } from '../components/t_grid/integrated'; import { mockBrowserFields, mockDocValueFields, mockRuntimeMappings } from './browser_fields'; import { mockDataProviders } from './mock_data_providers'; import { mockTimelineData } from './mock_timeline_data'; -import { ColumnHeaderOptions, TimelineId } from '../../common'; +import { ColumnHeaderOptions, TimelineId } from '../../common/types'; import { mockIndexNames, mockIndexPattern } from './index_pattern'; import { EventRenderedViewProps } from '../components/t_grid/event_rendered_view'; diff --git a/x-pack/plugins/timelines/public/store/t_grid/helpers.test.tsx b/x-pack/plugins/timelines/public/store/t_grid/helpers.test.tsx index 1e1fbe290a115..a29a117946392 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/helpers.test.tsx +++ b/x-pack/plugins/timelines/public/store/t_grid/helpers.test.tsx @@ -5,7 +5,7 @@ * 2.0. */ -import { SortColumnTimeline } from '../../../common'; +import { SortColumnTimeline } from '../../../common/types'; import { tGridDefaults } from './defaults'; import { setInitializeTgridSettings, diff --git a/x-pack/plugins/timelines/public/store/t_grid/types.ts b/x-pack/plugins/timelines/public/store/t_grid/types.ts index 0bc3f0d66f1e2..e7dbe5321bc80 100644 --- a/x-pack/plugins/timelines/public/store/t_grid/types.ts +++ b/x-pack/plugins/timelines/public/store/t_grid/types.ts @@ -6,7 +6,7 @@ */ import { Storage } from '../../../../../../src/plugins/kibana_utils/public'; -import type { ColumnHeaderOptions } from '../../../common'; +import type { ColumnHeaderOptions } from '../../../common/types'; import type { TGridModel, TGridModelSettings } from './model'; export type { TGridModel }; diff --git a/x-pack/plugins/timelines/public/types.ts b/x-pack/plugins/timelines/public/types.ts index 9c42f941ee03b..ddecac02be705 100644 --- a/x-pack/plugins/timelines/public/types.ts +++ b/x-pack/plugins/timelines/public/types.ts @@ -18,13 +18,13 @@ import type { UseDraggableKeyboardWrapper, UseDraggableKeyboardWrapperProps, } from './components'; -export type { SortDirection } from '../common'; +export type { SortDirection } from '../common/types'; import type { TGridIntegratedProps } from './components/t_grid/integrated'; import type { TGridStandaloneProps } from './components/t_grid/standalone'; import type { UseAddToTimelineProps, UseAddToTimeline } from './hooks/use_add_to_timeline'; import { HoverActionsConfig } from './components/hover_actions/index'; import type { AddToCaseActionProps } from './components/actions/timeline/cases/add_to_case_action'; -import { TimelineTabs } from '../common'; +import { TimelineTabs } from '../common/types'; export * from './store/t_grid'; export interface TimelinesUIStart { getHoverActions: () => HoverActionsConfig; diff --git a/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts b/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts index 7e2c03486d890..60caf1768192a 100644 --- a/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/index_fields/index.ts @@ -15,14 +15,13 @@ import { SearchStrategyDependencies, } from '../../../../../../src/plugins/data/server'; -// TODO cleanup path +import { DELETED_SECURITY_SOLUTION_DATA_VIEW } from '../../../common/constants'; import { IndexFieldsStrategyResponse, IndexField, IndexFieldsStrategyRequest, BeatFields, - DELETED_SECURITY_SOLUTION_DATA_VIEW, -} from '../../../common'; +} from '../../../common/search_strategy'; import { StartPlugins } from '../../types'; import type { FieldSpec } from '../../../../../../src/plugins/data_views/common'; diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/__mocks__/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/__mocks__/index.ts index ea212f07f442b..a4c8803a758d5 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/__mocks__/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/__mocks__/index.ts @@ -6,7 +6,7 @@ */ import type { EqlSearchStrategyResponse } from '../../../../../../../../src/plugins/data/common'; -import { EqlSearchResponse } from '../../../../../common'; +import { EqlSearchResponse } from '../../../../../common/search_strategy'; export const sequenceResponse = { rawResponse: { diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts index 50ef2ccd0293f..57ba55120bf4b 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/helpers.ts @@ -8,8 +8,12 @@ import { isEmpty } from 'lodash/fp'; import type { EqlSearchStrategyResponse } from '../../../../../../../src/plugins/data/common'; import { DEFAULT_MAX_TABLE_QUERY_SIZE } from '../../../../common/constants'; -import { EqlSearchResponse, EqlSequence, EventHit } from '../../../../common'; -import { TimelineEdges } from '../../../../common/search_strategy'; +import { + EqlSearchResponse, + EqlSequence, + EventHit, + TimelineEdges, +} from '../../../../common/search_strategy'; import { TimelineEqlRequestOptions, TimelineEqlResponse, diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/index.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/index.ts index b1ffab871fb8b..5ea8fefff7acb 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/eql/index.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/eql/index.ts @@ -15,7 +15,7 @@ import { EqlSearchStrategyResponse, EQL_SEARCH_STRATEGY, } from '../../../../../../../src/plugins/data/common'; -import { EqlSearchResponse } from '../../../../common'; +import { EqlSearchResponse } from '../../../../common/search_strategy'; import { TimelineEqlRequestOptions, TimelineEqlResponse, diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts index fc3ad0369c6c5..b79ff77aa96d0 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/constants.ts @@ -26,11 +26,13 @@ export const FIRST_SEEN = 'indicator.first_seen'; export const LAST_SEEN = 'indicator.last_seen'; export const PROVIDER = 'indicator.provider'; export const REFERENCE = 'indicator.reference'; +export const FEED_NAME = 'feed.name'; export const INDICATOR_FIRSTSEEN = `${ENRICHMENT_DESTINATION_PATH}.${FIRST_SEEN}`; export const INDICATOR_LASTSEEN = `${ENRICHMENT_DESTINATION_PATH}.${LAST_SEEN}`; export const INDICATOR_PROVIDER = `${ENRICHMENT_DESTINATION_PATH}.${PROVIDER}`; export const INDICATOR_REFERENCE = `${ENRICHMENT_DESTINATION_PATH}.${REFERENCE}`; +export const FEED_NAME_REFERENCE = `${ENRICHMENT_DESTINATION_PATH}.${FEED_NAME}`; export const CTI_ROW_RENDERER_FIELDS = [ INDICATOR_MATCHED_ATOMIC, @@ -38,6 +40,7 @@ export const CTI_ROW_RENDERER_FIELDS = [ INDICATOR_MATCHED_TYPE, INDICATOR_REFERENCE, INDICATOR_PROVIDER, + FEED_NAME_REFERENCE, ]; export const TIMELINE_EVENTS_FIELDS = [ diff --git a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/helpers.test.ts b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/helpers.test.ts index 4c8f339d25c51..13677c0151a88 100644 --- a/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/helpers.test.ts +++ b/x-pack/plugins/timelines/server/search_strategy/timeline/factory/events/all/helpers.test.ts @@ -97,6 +97,7 @@ describe('#formatTimelineData', () => { threat: { enrichments: [ { + feed: { name: [] }, indicator: { provider: ['yourself'], reference: [], @@ -108,6 +109,7 @@ describe('#formatTimelineData', () => { }, }, { + feed: { name: [] }, indicator: { provider: ['other_you'], reference: [], diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 4694b8c63d7e0..e1cbbe99a2e8b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -5936,7 +5936,6 @@ "xpack.apm.serviceIcons.serviceDetails.service.frameworkLabel": "フレームワーク名", "xpack.apm.serviceIcons.serviceDetails.service.runtimeLabel": "ランタイム名・バージョン", "xpack.apm.serviceIcons.serviceDetails.service.versionLabel": "サービスバージョン", - "xpack.apm.serviceInventory.mlNudgeMessageTitle": "異常検知を有効にして、正常性ステータスインジケーターをサービスに追加します", "xpack.apm.serviceInventory.toastText": "現在 Elastic Stack 7.0+ を実行中で、以前のバージョン 6.x からの互換性のないデータを検知しました。このデータを APM で表示するには、移行が必要です。詳細 ", "xpack.apm.serviceInventory.toastTitle": "選択された時間範囲内にレガシーデータが検知されました。", "xpack.apm.serviceInventory.upgradeAssistantLinkText": "アップグレードアシスタント", @@ -6021,9 +6020,6 @@ "xpack.apm.serviceOverview.latencyColumnP95Label": "レイテンシ(95 番目)", "xpack.apm.serviceOverview.latencyColumnP99Label": "レイテンシ(99 番目)", "xpack.apm.serviceOverview.loadingText": "読み込み中…", - "xpack.apm.serviceOverview.mlNudgeMessage.content": "APM の異常検知統合で、異常なトランザクションを特定し、アップストリームおよびダウンストリームサービスの正常性を確認します。わずか数分で開始できます。", - "xpack.apm.serviceOverview.mlNudgeMessage.dismissButton": "閉じる", - "xpack.apm.serviceOverview.mlNudgeMessage.learnMoreButton": "使ってみる", "xpack.apm.serviceOverview.noResultsText": "インスタンスが見つかりません", "xpack.apm.serviceOverview.throughtputChart.previousPeriodLabel": "前の期間", "xpack.apm.serviceOverview.throughtputChartTitle": "スループット", @@ -6053,9 +6049,7 @@ "xpack.apm.settings.agentConfig": "エージェントの編集", "xpack.apm.settings.agentConfig.createConfigButton.tooltip": "エージェント構成を作成する権限がありません", "xpack.apm.settings.agentConfig.descriptionText": "APMアプリ内からエージェント構成を微調整してください。変更はAPMエージェントに自動的に伝達されるので、再デプロイする必要はありません。", - "xpack.apm.settings.anomaly_detection.legacy_jobs.body": "以前の統合のレガシー機械学習ジョブが見つかりました。これは、APMアプリでは使用されていません。", "xpack.apm.settings.anomaly_detection.legacy_jobs.button": "ジョブの確認", - "xpack.apm.settings.anomaly_detection.legacy_jobs.title": "レガシーMLジョブはAPMアプリで使用されていません。", "xpack.apm.settings.anomalyDetection": "異常検知", "xpack.apm.settings.anomalyDetection.addEnvironments.cancelButtonText": "キャンセル", "xpack.apm.settings.anomalyDetection.addEnvironments.createJobsButtonText": "ジョブの作成", @@ -9464,8 +9458,6 @@ "xpack.enterpriseSearch.workplaceSearch.contentSource.saveConfig.button": "構成を保存", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveConfig.oauthStep1": "組織の{sourceName}アカウントでOAuthアプリを作成する", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveConfig.oauthStep2": "適切な構成情報を入力する", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.apiKeys.body": "このカスタムソースでドキュメントを同期するには、これらのキーが必要です。", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.apiKeys.title": "API キー", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.body1": "エンドポイントは要求を承認できます。", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.body2": "必ず次のAPIキーをコピーしてください。", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.displaySettings.text": "{link}を使用して、検索結果内でドキュメントが表示される方法をカスタマイズします。デフォルトでは、Workplace Searchは英字順でフィールドを使用します。", @@ -9790,7 +9782,6 @@ "xpack.enterpriseSearch.workplaceSearch.sourceRow.remoteLabel": "リモート", "xpack.enterpriseSearch.workplaceSearch.sourceRow.remoteTooltip": "リモートソースは直接ソースの検索サービスに依存しています。コンテンツはWorkplace Searchでインデックスされません。結果の速度と完全性はサードパーティサービスの正常性とパフォーマンスの機能です。", "xpack.enterpriseSearch.workplaceSearch.sourceRow.searchableToggleLabel": "ソース検索可能トグル", - "xpack.enterpriseSearch.workplaceSearch.sources.accessToken.label": "アクセストークン", "xpack.enterpriseSearch.workplaceSearch.sources.additionalConfig.heading": "追加の構成が必要", "xpack.enterpriseSearch.workplaceSearch.sources.applicationLinkTitles.github": "GitHub開発者ポータル", "xpack.enterpriseSearch.workplaceSearch.sources.baseUrlTitles.github": "GitHub Enterprise URL", @@ -19972,11 +19963,6 @@ "xpack.remoteClusters.updateRemoteCluster.unknownRemoteClusterErrorMessage": "ES からレスポンスが返らず、クラスターを編集できません。", "xpack.reporting.apiClient.unknownError": "レポートジョブ{job}が失敗しました。不明なエラーです。", "xpack.reporting.breadcrumb": "レポート", - "xpack.reporting.browsers.chromium.errorDetected": "レポートでエラーが発生しました:{err}", - "xpack.reporting.browsers.chromium.pageErrorDetected": "レポートのページで処理されていないエラーが発生し、無視されるます:{err}", - "xpack.reporting.chromiumDriver.disallowedOutgoingUrl": "許可されていない送信URLを受信しました:「{interceptedUrl}」。要求が失敗しています。ブラウザーを終了しています。", - "xpack.reporting.chromiumDriver.failedToCompleteRequest": "リクエストを完了できませんでした:{error}", - "xpack.reporting.chromiumDriver.failedToCompleteRequestUsingHeaders": "ヘッダーを使用してリクエストを完了できませんでした:{error}", "xpack.reporting.dashboard.csvDownloadStartedMessage": "間もなく CSV がダウンロードされます。", "xpack.reporting.dashboard.csvDownloadStartedTitle": "CSV のダウンロードが開始しました", "xpack.reporting.dashboard.downloadCsvPanelTitle": "CSV をダウンロード", @@ -20007,8 +19993,6 @@ "xpack.reporting.deprecations.reportingRoleUsers.manualStepTwo": "存在する場合は、kibana.ymlで「xpack.reporting.roles.allow」を削除します。", "xpack.reporting.deprecations.reportingRoleUsersMessage": "既存のユーザーには廃止予定の設定によって付与されたレポート権限があります。", "xpack.reporting.deprecations.reportingRoleUsersTitle": "\"{reportingUserRoleName}\"ロールは廃止予定です。ユーザーロールを確認してください", - "xpack.reporting.diagnostic.browserCrashed": "ブラウザーは起動中に異常終了しました", - "xpack.reporting.diagnostic.browserErrored": "ブラウザープロセスは起動中にエラーが発生しました", "xpack.reporting.diagnostic.browserMissingDependency": "システム依存関係が不足しているため、ブラウザーを正常に起動できませんでした。{url}を参照してください", "xpack.reporting.diagnostic.browserMissingFonts": "ブラウザーはデフォルトフォントを検索できませんでした。この問題を修正するには、{url}を参照してください。", "xpack.reporting.diagnostic.noUsableSandbox": "Chromiumサンドボックスを使用できません。これは「xpack.reporting.capture.browser.chromium.disableSandbox」で無効にすることができます。この作業はご自身の責任で行ってください。{url}を参照してください", @@ -20059,7 +20043,6 @@ "xpack.reporting.listing.ilmPolicyCallout.migrationNeededDescription": "レポートが一貫して管理されることを保証するために、すべてのレポートインデックスは{ilmPolicyName}ポリシーを使用します。", "xpack.reporting.listing.ilmPolicyCallout.migrationNeededTitle": "レポートの新しいライフサイクルポリシーを適用", "xpack.reporting.listing.infoPanel.attemptsInfo": "試行", - "xpack.reporting.listing.infoPanel.browserTypeInfo": "ブラウザータイプ", "xpack.reporting.listing.infoPanel.completedAtInfo": "完了日時", "xpack.reporting.listing.infoPanel.contentTypeInfo": "コンテンツタイプ", "xpack.reporting.listing.infoPanel.createdAtInfo": "作成日時:", @@ -20135,20 +20118,6 @@ "xpack.reporting.redirectApp.redirectConsoleErrorPrefixLabel": "リダイレクトページエラー:", "xpack.reporting.registerFeature.reportingDescription": "Discover、可視化、ダッシュボードから生成されたレポートを管理します。", "xpack.reporting.registerFeature.reportingTitle": "レポート", - "xpack.reporting.screencapture.browserWasClosed": "ブラウザーは予期せず終了しました。詳細については、サーバーログを確認してください。", - "xpack.reporting.screencapture.couldntFinishRendering": "{count} 件のビジュアライゼーションのレンダリングが完了するのを待つ間にエラーが発生しました。{error}", - "xpack.reporting.screencapture.couldntLoadKibana": "Kibana URLを開こうとするときにエラーが発生しました:{error}", - "xpack.reporting.screencapture.injectCss": "Kibana CSS をレポート用に更新しようとしたときにエラーが発生しました。{error}", - "xpack.reporting.screencapture.injectingCss": "カスタム css の投入中", - "xpack.reporting.screencapture.logWaitingForElements": "要素または項目のカウント属性を待ち、または見つからないため中断", - "xpack.reporting.screencapture.noElements": "ビジュアライゼーションパネルのページを読み取る間にエラーが発生しました:パネルが見つかりませんでした。", - "xpack.reporting.screencapture.readVisualizationsError": "ビジュアライゼーションパネル情報のページを読み取ろうとしたときにエラーが発生しました:{error}", - "xpack.reporting.screencapture.renderErrorsFound": "{count}件のエラーメッセージが見つかりました。詳細については、レポートオブジェクトを参照してください。", - "xpack.reporting.screencapture.renderIsComplete": "レンダリングが完了しました", - "xpack.reporting.screencapture.screenshotsTaken": "撮影したスクリーンショット:{numScreenhots}", - "xpack.reporting.screencapture.takingScreenshots": "スクリーンショットの撮影中", - "xpack.reporting.screencapture.waitingForRenderComplete": "レンダリングの完了を待っています", - "xpack.reporting.screencapture.waitingForRenderedElements": "レンダリングされた {itemsCount} 個の要素が DOM に入るのを待っています", "xpack.reporting.screenCapturePanelContent.canvasLayoutHelpText": "枠線とフッターロゴを削除", "xpack.reporting.screenCapturePanelContent.canvasLayoutLabel": "全ページレイアウト", "xpack.reporting.screenCapturePanelContent.optimizeForPrintingHelpText": "複数のページを使用します。ページごとに最大2のビジュアライゼーションが表示されます", @@ -20447,6 +20416,30 @@ "xpack.savedObjectsTagging.validation.description.errorTooLong": "タグ説明は {length} 文字以下で入力してください", "xpack.savedObjectsTagging.validation.name.errorTooLong": "タグ名は {length} 文字以下で入力してください", "xpack.savedObjectsTagging.validation.name.errorTooShort": "タグ名は {length} 文字以上で入力してください", + "xpack.screenshotting.browsers.chromium.errorDetected": "レポートでエラーが発生しました:{err}", + "xpack.screenshotting.browsers.chromium.pageErrorDetected": "レポートのページで処理されていないエラーが発生し、無視されるます:{err}", + "xpack.screenshotting.chromiumDriver.disallowedOutgoingUrl": "許可されていない送信URLを受信しました:「{interceptedUrl}」。要求が失敗しています。ブラウザーを終了しています。", + "xpack.screenshotting.chromiumDriver.failedToCompleteRequest": "リクエストを完了できませんでした:{error}", + "xpack.screenshotting.chromiumDriver.failedToCompleteRequestUsingHeaders": "ヘッダーを使用してリクエストを完了できませんでした:{error}", + "xpack.screenshotting.diagnostic.browserCrashed": "ブラウザーは起動中に異常終了しました", + "xpack.screenshotting.diagnostic.browserErrored": "ブラウザープロセスは起動中にエラーが発生しました", + "xpack.screenshotting.screencapture.browserWasClosed": "ブラウザーは予期せず終了しました。詳細については、サーバーログを確認してください。", + "xpack.screenshotting.screencapture.couldntFinishRendering": "{count} 件のビジュアライゼーションのレンダリングが完了するのを待つ間にエラーが発生しました。{error}", + "xpack.screenshotting.screencapture.couldntLoadKibana": "Kibana URLを開こうとするときにエラーが発生しました:{error}", + "xpack.screenshotting.screencapture.injectCss": "Kibana CSS をレポート用に更新しようとしたときにエラーが発生しました。{error}", + "xpack.screenshotting.screencapture.injectingCss": "カスタム css の投入中", + "xpack.screenshotting.screencapture.logWaitingForElements": "要素または項目のカウント属性を待ち、または見つからないため中断", + "xpack.screenshotting.screencapture.noElements": "ビジュアライゼーションパネルのページを読み取る間にエラーが発生しました:パネルが見つかりませんでした。", + "xpack.screenshotting.screencapture.readVisualizationsError": "ビジュアライゼーションパネル情報のページを読み取ろうとしたときにエラーが発生しました:{error}", + "xpack.screenshotting.screencapture.renderErrorsFound": "{count}件のエラーメッセージが見つかりました。詳細については、レポートオブジェクトを参照してください。", + "xpack.screenshotting.screencapture.renderIsComplete": "レンダリングが完了しました", + "xpack.screenshotting.screencapture.screenshotsTaken": "撮影したスクリーンショット:{numScreenhots}", + "xpack.screenshotting.screencapture.takingScreenshots": "スクリーンショットの撮影中", + "xpack.screenshotting.screencapture.waitingForRenderComplete": "レンダリングの完了を待っています", + "xpack.screenshotting.screencapture.waitingForRenderedElements": "レンダリングされた {itemsCount} 個の要素が DOM に入るのを待っています", + "xpack.screenshotting.serverConfig.autoSet.sandboxDisabled": "Chromiumサンドボックスは保護が強化されていますが、{osName} OSではサポートされていません。自動的に'{configKey}: true'を設定しています。", + "xpack.screenshotting.serverConfig.autoSet.sandboxEnabled": "Chromiumサンドボックスは保護が強化され、{osName} OSでサポートされています。自動的にChromiumサンドボックスを有効にしています。", + "xpack.screenshotting.serverConfig.osDetected": "OSは'{osName}'で実行しています", "xpack.searchProfiler.advanceTimeDescription": "イテレーターを次のドキュメントに進めるためにかかった時間。", "xpack.searchProfiler.aggregationProfileTabTitle": "集約プロフィール", "xpack.searchProfiler.basicLicenseTitle": "基本", @@ -22817,7 +22810,6 @@ "xpack.securitySolution.eventDetails.ctiSummary.informationAriaLabel": "情報", "xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTitle": "Threat Intelligenceで拡張", "xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent": "このフィールド値には脅威インテリジェンスソースの別の情報があります。", - "xpack.securitySolution.eventDetails.ctiSummary.providerPreposition": "開始", "xpack.securitySolution.eventDetails.description": "説明", "xpack.securitySolution.eventDetails.field": "フィールド", "xpack.securitySolution.eventDetails.filter.placeholder": "フィールド、値、または説明でフィルター...", @@ -23438,7 +23430,6 @@ "xpack.securitySolution.overview.auditBeatProcessTitle": "プロセス", "xpack.securitySolution.overview.auditBeatSocketTitle": "ソケット", "xpack.securitySolution.overview.auditBeatUserTitle": "ユーザー", - "xpack.securitySolution.overview.ctiDashboardDangerPanelButton": "モジュールを有効にする", "xpack.securitySolution.overview.ctiDashboardDangerPanelTitle": "表示する脅威インテリジェンスデータがありません", "xpack.securitySolution.overview.ctiDashboardEnableThreatIntel": "別のソースからデータを表示するには、filebeat脅威インテリジェンスモジュールを有効にする必要があります。", "xpack.securitySolution.overview.ctiDashboardInfoPanelBody": "このガイドに従い、ダッシュボードを有効にして、ビジュアライゼーションにソースを表示できるようにしてください。", @@ -23460,7 +23451,6 @@ "xpack.securitySolution.overview.endpointNotice.message": "脅威防御、検出、深いセキュリティデータの可視化を実現し、ホストを保護します。", "xpack.securitySolution.overview.endpointNotice.title": "Endpoint Security", "xpack.securitySolution.overview.endpointNotice.tryButton": "Endpoint Securityを試す", - "xpack.securitySolution.overview.errorFetchingEvents": "イベントの取得エラー", "xpack.securitySolution.overview.eventsTitle": "イベント数", "xpack.securitySolution.overview.filebeatCiscoTitle": "Cisco", "xpack.securitySolution.overview.filebeatNetflowTitle": "Netflow", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index ba523a4236b7d..6f49e98d1991d 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -5977,7 +5977,6 @@ "xpack.apm.serviceIcons.serviceDetails.service.frameworkLabel": "框架名称", "xpack.apm.serviceIcons.serviceDetails.service.runtimeLabel": "运行时名称和版本", "xpack.apm.serviceIcons.serviceDetails.service.versionLabel": "服务版本", - "xpack.apm.serviceInventory.mlNudgeMessageTitle": "启用异常检测以将运行状态指示器添加到您的服务中", "xpack.apm.serviceInventory.toastText": "您正在运行 Elastic Stack 7.0+,我们检测到来自以前 6.x 版本的数据不兼容。如果想在 APM 中查看此数据,您应迁移数据。在以下位置查看更多内容: ", "xpack.apm.serviceInventory.toastTitle": "在选定时间范围中检测到旧数据", "xpack.apm.serviceInventory.upgradeAssistantLinkText": "升级助手", @@ -6061,9 +6060,6 @@ "xpack.apm.serviceOverview.latencyColumnP95Label": "延迟(第 95 个)", "xpack.apm.serviceOverview.latencyColumnP99Label": "延迟(第 99 个)", "xpack.apm.serviceOverview.loadingText": "正在加载……", - "xpack.apm.serviceOverview.mlNudgeMessage.content": "通过 APM 的异常检测集成来查明异常事务,并了解上下游服务的运行状况。只需几分钟即可开始使用。", - "xpack.apm.serviceOverview.mlNudgeMessage.dismissButton": "关闭", - "xpack.apm.serviceOverview.mlNudgeMessage.learnMoreButton": "开始使用", "xpack.apm.serviceOverview.noResultsText": "未找到实例", "xpack.apm.serviceOverview.throughtputChart.previousPeriodLabel": "上一时段", "xpack.apm.serviceOverview.throughtputChartTitle": "吞吐量", @@ -6094,9 +6090,7 @@ "xpack.apm.settings.agentConfig": "代理配置", "xpack.apm.settings.agentConfig.createConfigButton.tooltip": "您无权创建代理配置", "xpack.apm.settings.agentConfig.descriptionText": "从 APM 应用中微调您的代理配置。更改将自动传播到 APM 代理,因此无需重新部署。", - "xpack.apm.settings.anomaly_detection.legacy_jobs.body": "我们在以前的集成中发现 APM 应用中不再使用的旧版 Machine Learning 作业", "xpack.apm.settings.anomaly_detection.legacy_jobs.button": "复查作业", - "xpack.apm.settings.anomaly_detection.legacy_jobs.title": "旧版 ML 作业不再用于 APM 应用", "xpack.apm.settings.anomalyDetection": "异常检测", "xpack.apm.settings.anomalyDetection.addEnvironments.cancelButtonText": "取消", "xpack.apm.settings.anomalyDetection.addEnvironments.createJobsButtonText": "创建作业", @@ -9551,8 +9545,6 @@ "xpack.enterpriseSearch.workplaceSearch.contentSource.saveConfig.button": "保存配置", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveConfig.oauthStep1": "在组织的 {sourceName} 帐户中创建 OAuth 应用", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveConfig.oauthStep2": "提供适当的配置信息", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.apiKeys.body": "您将需要这些密钥以便为此定制源同步文档。", - "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.apiKeys.title": "API 密钥", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.body1": "您的终端已准备好接受请求。", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.body2": "确保在下面复制您的 API 密钥。", "xpack.enterpriseSearch.workplaceSearch.contentSource.saveCustom.displaySettings.text": "请使用 {link} 定制您的文档在搜索结果内显示的方式。Workplace Search 默认按字母顺序使用字段。", @@ -9877,7 +9869,6 @@ "xpack.enterpriseSearch.workplaceSearch.sourceRow.remoteLabel": "远程", "xpack.enterpriseSearch.workplaceSearch.sourceRow.remoteTooltip": "远程源直接依赖于源的搜索服务,且没有内容使用 Workplace Search 进行索引。速度和结果完整性取决于第三方服务的运行状况和性能。", "xpack.enterpriseSearch.workplaceSearch.sourceRow.searchableToggleLabel": "源可搜索切换", - "xpack.enterpriseSearch.workplaceSearch.sources.accessToken.label": "访问令牌", "xpack.enterpriseSearch.workplaceSearch.sources.additionalConfig.heading": "需要其他配置", "xpack.enterpriseSearch.workplaceSearch.sources.applicationLinkTitles.github": "GitHub 开发者门户", "xpack.enterpriseSearch.workplaceSearch.sources.baseUrlTitles.github": "GitHub Enterprise URL", @@ -20264,11 +20255,6 @@ "xpack.remoteClusters.updateRemoteCluster.unknownRemoteClusterErrorMessage": "无法编辑集群,ES 未返回任何响应。", "xpack.reporting.apiClient.unknownError": "报告作业 {job} 失败。错误未知。", "xpack.reporting.breadcrumb": "Reporting", - "xpack.reporting.browsers.chromium.errorDetected": "报告时遇到错误:{err}", - "xpack.reporting.browsers.chromium.pageErrorDetected": "Reporting 在将忽略的页面上遇到未捕获的错误:{err}", - "xpack.reporting.chromiumDriver.disallowedOutgoingUrl": "收到禁止的传出 URL:“{interceptedUrl}”。请求失败,关闭浏览器。", - "xpack.reporting.chromiumDriver.failedToCompleteRequest": "无法完成请求:{error}", - "xpack.reporting.chromiumDriver.failedToCompleteRequestUsingHeaders": "无法完成使用 headers 的请求:{error}", "xpack.reporting.dashboard.csvDownloadStartedMessage": "您的 CSV 将很快下载。", "xpack.reporting.dashboard.csvDownloadStartedTitle": "CSV 下载已开始", "xpack.reporting.dashboard.downloadCsvPanelTitle": "下载 CSV", @@ -20299,8 +20285,6 @@ "xpack.reporting.deprecations.reportingRoleUsers.manualStepTwo": "移除 kibana.yml 中的“xpack.reporting.roles.allow”(如果存在)。", "xpack.reporting.deprecations.reportingRoleUsersMessage": "现有用户具有由过时设置授予的 Reporting 权限。", "xpack.reporting.deprecations.reportingRoleUsersTitle": "“{reportingUserRoleName}”角色已过时:检查用户角色", - "xpack.reporting.diagnostic.browserCrashed": "启动期间浏览器已异常退出", - "xpack.reporting.diagnostic.browserErrored": "启动时浏览器进程引发了错误", "xpack.reporting.diagnostic.browserMissingDependency": "由于缺少系统依赖项,浏览器无法正常启动。请参见 {url}", "xpack.reporting.diagnostic.browserMissingFonts": "浏览器找不到默认字体。请参见 {url} 以解决此问题。", "xpack.reporting.diagnostic.noUsableSandbox": "无法使用 Chromium 沙盒。您自行承担使用“xpack.reporting.capture.browser.chromium.disableSandbox”禁用此项的风险。请参见 {url}", @@ -20351,7 +20335,6 @@ "xpack.reporting.listing.ilmPolicyCallout.migrationNeededDescription": "为了确保得到一致的管理,所有报告索引应使用 {ilmPolicyName} 策略。", "xpack.reporting.listing.ilmPolicyCallout.migrationNeededTitle": "为报告应用新的生命周期策略", "xpack.reporting.listing.infoPanel.attemptsInfo": "尝试次数", - "xpack.reporting.listing.infoPanel.browserTypeInfo": "浏览器类型", "xpack.reporting.listing.infoPanel.completedAtInfo": "完成时间", "xpack.reporting.listing.infoPanel.contentTypeInfo": "内容类型", "xpack.reporting.listing.infoPanel.createdAtInfo": "创建于", @@ -20428,20 +20411,6 @@ "xpack.reporting.redirectApp.redirectConsoleErrorPrefixLabel": "重定向页面错误:", "xpack.reporting.registerFeature.reportingDescription": "管理您从 Discover、Visualize 和 Dashboard 生成的报告。", "xpack.reporting.registerFeature.reportingTitle": "Reporting", - "xpack.reporting.screencapture.browserWasClosed": "浏览器已意外关闭!有关更多信息,请查看服务器日志。", - "xpack.reporting.screencapture.couldntFinishRendering": "尝试等候 {count} 个可视化完成渲染时发生错误。{error}", - "xpack.reporting.screencapture.couldntLoadKibana": "尝试打开 Kibana URL 时发生错误:{error}", - "xpack.reporting.screencapture.injectCss": "尝试为 Reporting 更新 Kibana CSS 时发生错误。{error}", - "xpack.reporting.screencapture.injectingCss": "正在注入定制 css", - "xpack.reporting.screencapture.logWaitingForElements": "等候元素或项目计数属性;或未发现要中断", - "xpack.reporting.screencapture.noElements": "读取页面以获取可视化面板时发生了错误:未找到任何面板。", - "xpack.reporting.screencapture.readVisualizationsError": "尝试读取页面以获取可视化面板信息时发生错误:{error}", - "xpack.reporting.screencapture.renderErrorsFound": "找到 {count} 条错误消息。请参阅报告对象了解更多信息。", - "xpack.reporting.screencapture.renderIsComplete": "渲染已完成", - "xpack.reporting.screencapture.screenshotsTaken": "已捕获的屏幕截图:{numScreenhots}", - "xpack.reporting.screencapture.takingScreenshots": "正在捕获屏幕截图", - "xpack.reporting.screencapture.waitingForRenderComplete": "正在等候渲染完成", - "xpack.reporting.screencapture.waitingForRenderedElements": "正在等候 {itemsCount} 个已渲染元素进入 DOM", "xpack.reporting.screenCapturePanelContent.canvasLayoutHelpText": "删除边框和页脚徽标", "xpack.reporting.screenCapturePanelContent.canvasLayoutLabel": "全页面布局", "xpack.reporting.screenCapturePanelContent.optimizeForPrintingHelpText": "使用多页,每页最多显示 2 个可视化", @@ -20752,6 +20721,30 @@ "xpack.savedObjectsTagging.validation.description.errorTooLong": "标签描述不能超过 {length} 个字符", "xpack.savedObjectsTagging.validation.name.errorTooLong": "标签名称不能超过 {length} 个字符", "xpack.savedObjectsTagging.validation.name.errorTooShort": "标签名称必须至少有 {length} 个字符", + "xpack.screenshotting.browsers.chromium.errorDetected": "报告时遇到错误:{err}", + "xpack.screenshotting.browsers.chromium.pageErrorDetected": "Reporting 在将忽略的页面上遇到未捕获的错误:{err}", + "xpack.screenshotting.chromiumDriver.disallowedOutgoingUrl": "收到禁止的传出 URL:“{interceptedUrl}”。请求失败,关闭浏览器。", + "xpack.screenshotting.chromiumDriver.failedToCompleteRequest": "无法完成请求:{error}", + "xpack.screenshotting.chromiumDriver.failedToCompleteRequestUsingHeaders": "无法完成使用 headers 的请求:{error}", + "xpack.screenshotting.diagnostic.browserCrashed": "启动期间浏览器已异常退出", + "xpack.screenshotting.diagnostic.browserErrored": "启动时浏览器进程引发了错误", + "xpack.screenshotting.screencapture.browserWasClosed": "浏览器已意外关闭!有关更多信息,请查看服务器日志。", + "xpack.screenshotting.screencapture.couldntFinishRendering": "尝试等候 {count} 个可视化完成渲染时发生错误。{error}", + "xpack.screenshotting.screencapture.couldntLoadKibana": "尝试打开 Kibana URL 时发生错误:{error}", + "xpack.screenshotting.screencapture.injectCss": "尝试为 Reporting 更新 Kibana CSS 时发生错误。{error}", + "xpack.screenshotting.screencapture.injectingCss": "正在注入定制 css", + "xpack.screenshotting.screencapture.logWaitingForElements": "等候元素或项目计数属性;或未发现要中断", + "xpack.screenshotting.screencapture.noElements": "读取页面以获取可视化面板时发生了错误:未找到任何面板。", + "xpack.screenshotting.screencapture.readVisualizationsError": "尝试读取页面以获取可视化面板信息时发生错误:{error}", + "xpack.screenshotting.screencapture.renderErrorsFound": "找到 {count} 条错误消息。请参阅报告对象了解更多信息。", + "xpack.screenshotting.screencapture.renderIsComplete": "渲染已完成", + "xpack.screenshotting.screencapture.screenshotsTaken": "已捕获的屏幕截图:{numScreenhots}", + "xpack.screenshotting.screencapture.takingScreenshots": "正在捕获屏幕截图", + "xpack.screenshotting.screencapture.waitingForRenderComplete": "正在等候渲染完成", + "xpack.screenshotting.screencapture.waitingForRenderedElements": "正在等候 {itemsCount} 个已渲染元素进入 DOM", + "xpack.screenshotting.serverConfig.autoSet.sandboxDisabled": "Chromium 沙盒提供附加保护层,但不受 {osName} OS 支持。自动设置“{configKey}: true”。", + "xpack.screenshotting.serverConfig.autoSet.sandboxEnabled": "Chromium 沙盒提供附加保护层,受 {osName} OS 支持。自动启用 Chromium 沙盒。", + "xpack.screenshotting.serverConfig.osDetected": "正在以下 OS 上运行:“{osName}”", "xpack.searchProfiler.advanceTimeDescription": "将迭代器推进至下一文档所用时间。", "xpack.searchProfiler.aggregationProfileTabTitle": "聚合配置文件", "xpack.searchProfiler.basicLicenseTitle": "基本级", @@ -23180,7 +23173,6 @@ "xpack.securitySolution.eventDetails.ctiSummary.informationAriaLabel": "信息", "xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTitle": "已使用威胁情报扩充", "xpack.securitySolution.eventDetails.ctiSummary.investigationEnrichmentTooltipContent": "此字段值具有威胁情报源提供的其他信息。", - "xpack.securitySolution.eventDetails.ctiSummary.providerPreposition": "来自", "xpack.securitySolution.eventDetails.description": "描述", "xpack.securitySolution.eventDetails.field": "字段", "xpack.securitySolution.eventDetails.filter.placeholder": "按字段、值或描述筛选......", @@ -23828,7 +23820,6 @@ "xpack.securitySolution.overview.auditBeatProcessTitle": "进程", "xpack.securitySolution.overview.auditBeatSocketTitle": "套接字", "xpack.securitySolution.overview.auditBeatUserTitle": "用户", - "xpack.securitySolution.overview.ctiDashboardDangerPanelButton": "启用模块", "xpack.securitySolution.overview.ctiDashboardDangerPanelTitle": "没有可显示的威胁情报数据", "xpack.securitySolution.overview.ctiDashboardEnableThreatIntel": "您需要启用 filebeat threatintel 模块,以便查看不同源的数据。", "xpack.securitySolution.overview.ctiDashboardInfoPanelBody": "按照此指南启用您的仪表板,以便可以在可视化中查看您的源。", @@ -23851,7 +23842,6 @@ "xpack.securitySolution.overview.endpointNotice.message": "使用威胁防御、检测和深度安全数据可见性功能保护您的主机。", "xpack.securitySolution.overview.endpointNotice.title": "Endpoint Security", "xpack.securitySolution.overview.endpointNotice.tryButton": "试用 Endpoint Security", - "xpack.securitySolution.overview.errorFetchingEvents": "提取事件时出错", "xpack.securitySolution.overview.eventsTitle": "事件计数", "xpack.securitySolution.overview.filebeatCiscoTitle": "Cisco", "xpack.securitySolution.overview.filebeatNetflowTitle": "NetFlow", diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor_management/config_key.ts b/x-pack/plugins/uptime/common/runtime_types/monitor_management/config_key.ts index 13a7c1e1f2f2d..b7d66ba204e67 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor_management/config_key.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor_management/config_key.ts @@ -11,6 +11,7 @@ import { tEnum } from '../../utils/t_enum'; // values must match keys in the integration package export enum ConfigKey { APM_SERVICE_NAME = 'service.name', + ENABLED = 'enabled', HOSTS = 'hosts', IGNORE_HTTPS_ERRORS = 'ignore_https_errors', JOURNEY_FILTERS_MATCH = 'filter_journeys.match', diff --git a/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts b/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts index f54035b7f69ff..fd2ef24bacabd 100644 --- a/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts +++ b/x-pack/plugins/uptime/common/runtime_types/monitor_management/monitor_types.ts @@ -48,6 +48,7 @@ export type ZipUrlTLSFields = t.TypeOf; // CommonFields export const CommonFieldsCodec = t.interface({ [ConfigKey.MONITOR_TYPE]: DataStreamCodec, + [ConfigKey.ENABLED]: t.boolean, [ConfigKey.SCHEDULE]: Schedule, [ConfigKey.APM_SERVICE_NAME]: t.string, [ConfigKey.TIMEOUT]: t.string, diff --git a/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.ts b/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.ts index 59e15f2d9e9c3..25be1ac3dd0dc 100644 --- a/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.ts +++ b/x-pack/plugins/uptime/common/runtime_types/ping/synthetics.ts @@ -44,8 +44,12 @@ export const JourneyStepType = t.intersection([ text: t.string, }), step: t.type({ + status: t.string, index: t.number, name: t.string, + duration: t.type({ + us: t.number, + }), }), isFullScreenshot: t.boolean, isScreenshotRef: t.boolean, diff --git a/x-pack/plugins/uptime/e2e/fixtures/es_archiver/browser/data.json.gz b/x-pack/plugins/uptime/e2e/fixtures/es_archiver/browser/data.json.gz new file mode 100644 index 0000000000000..86b1b1cba1d29 Binary files /dev/null and b/x-pack/plugins/uptime/e2e/fixtures/es_archiver/browser/data.json.gz differ diff --git a/x-pack/plugins/uptime/e2e/fixtures/es_archiver/browser/mappings.json b/x-pack/plugins/uptime/e2e/fixtures/es_archiver/browser/mappings.json new file mode 100644 index 0000000000000..416b157b35a67 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/fixtures/es_archiver/browser/mappings.json @@ -0,0 +1,7762 @@ +{ + "type": "index", + "value": { + "aliases": { + "heartbeat-8.1.0": { + "is_write_index": true + } + }, + "index": "heartbeat-8.1.0-2021.11.21-000001", + "mappings": { + "_meta": { + "beat": "heartbeat", + "version": "8.1.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.*" + } + }, + { + "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.namespace.labels.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.namespace.labels.*" + } + }, + { + "kubernetes.namespace.annotations.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.namespace.annotations.*" + } + }, + { + "kubernetes.labels.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.labels.*" + } + }, + { + "kubernetes.annotations.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.annotations.*" + } + }, + { + "kubernetes.selectors.*": { + "mapping": { + "type": "keyword" + }, + "path_match": "kubernetes.selectors.*" + } + }, + { + "strings_as_keyword": { + "mapping": { + "ignore_above": 1024, + "type": "keyword" + }, + "match_mapping_type": "string" + } + } + ], + "properties": { + "@timestamp": { + "type": "date" + }, + "agent": { + "properties": { + "build": { + "properties": { + "original": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "client": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "type": "match_only_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_code": { + "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" + }, + "postal_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "timezone": { + "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" + }, + "subdomain": { + "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": { + "type": "match_only_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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "cloud": { + "properties": { + "account": { + "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": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "origin": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "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" + }, + "service": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "project": { + "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" + }, + "service": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "target": { + "properties": { + "account": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "availability_zone": { + "ignore_above": 1024, + "type": "keyword" + }, + "instance": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "machine": { + "properties": { + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "project": { + "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" + }, + "service": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "code_signature": { + "properties": { + "digest_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "exists": { + "type": "boolean" + }, + "signing_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "team_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "container": { + "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" + } + } + }, + "data_stream": { + "properties": { + "dataset": { + "type": "constant_keyword" + }, + "namespace": { + "type": "constant_keyword" + }, + "type": { + "type": "constant_keyword" + } + } + }, + "destination": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "type": "match_only_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_code": { + "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" + }, + "postal_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "timezone": { + "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" + }, + "subdomain": { + "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": { + "type": "match_only_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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "dll": { + "properties": { + "code_signature": { + "properties": { + "digest_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "exists": { + "type": "boolean" + }, + "signing_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "team_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "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" + }, + "ssdeep": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "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" + } + } + }, + "elf": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "byte_order": { + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "creation_date": { + "type": "date" + }, + "exports": { + "type": "flattened" + }, + "header": { + "properties": { + "abi_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "entrypoint": { + "type": "long" + }, + "object_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "os_abi": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "imports": { + "type": "flattened" + }, + "sections": { + "properties": { + "chi2": { + "type": "long" + }, + "entropy": { + "type": "long" + }, + "flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_offset": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "virtual_address": { + "type": "long" + }, + "virtual_size": { + "type": "long" + } + }, + "type": "nested" + }, + "segments": { + "properties": { + "sections": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "shared_libraries": { + "ignore_above": 1024, + "type": "keyword" + }, + "telfhash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "error": { + "properties": { + "code": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "message": { + "type": "match_only_text" + }, + "stack_trace": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "wildcard" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "event": { + "properties": { + "action": { + "ignore_above": 1024, + "type": "keyword" + }, + "agent_id_status": { + "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": { + "doc_values": false, + "ignore_above": 1024, + "index": false, + "type": "keyword" + }, + "outcome": { + "ignore_above": 1024, + "type": "keyword" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reason": { + "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" + } + } + }, + "faas": { + "properties": { + "coldstart": { + "type": "boolean" + }, + "execution": { + "ignore_above": 1024, + "type": "keyword" + }, + "trigger": { + "properties": { + "request_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + } + } + }, + "fields": { + "type": "object" + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "digest_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "exists": { + "type": "boolean" + }, + "signing_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "team_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "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" + }, + "elf": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "byte_order": { + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "creation_date": { + "type": "date" + }, + "exports": { + "type": "flattened" + }, + "header": { + "properties": { + "abi_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "entrypoint": { + "type": "long" + }, + "object_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "os_abi": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "imports": { + "type": "flattened" + }, + "sections": { + "properties": { + "chi2": { + "type": "long" + }, + "entropy": { + "type": "long" + }, + "flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_offset": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "virtual_address": { + "type": "long" + }, + "virtual_size": { + "type": "long" + } + }, + "type": "nested" + }, + "segments": { + "properties": { + "sections": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "shared_libraries": { + "ignore_above": 1024, + "type": "keyword" + }, + "telfhash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fork_name": { + "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" + }, + "ssdeep": { + "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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_code": { + "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" + }, + "postal_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "timezone": { + "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" + }, + "ssdeep": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "host": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "containerized": { + "type": "boolean" + }, + "cpu": { + "properties": { + "usage": { + "scaling_factor": 1000, + "type": "scaled_float" + } + } + }, + "disk": { + "properties": { + "read": { + "properties": { + "bytes": { + "type": "long" + } + } + }, + "write": { + "properties": { + "bytes": { + "type": "long" + } + } + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_code": { + "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" + }, + "postal_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "timezone": { + "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" + }, + "network": { + "properties": { + "egress": { + "properties": { + "bytes": { + "type": "long" + }, + "packets": { + "type": "long" + } + } + }, + "ingress": { + "properties": { + "bytes": { + "type": "long" + }, + "packets": { + "type": "long" + } + } + } + } + }, + "os": { + "properties": { + "build": { + "ignore_above": 1024, + "type": "keyword" + }, + "codename": { + "ignore_above": 1024, + "type": "keyword" + }, + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + } + } + }, + "http": { + "properties": { + "request": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "wildcard" + } + } + }, + "bytes": { + "type": "long" + }, + "has_post_data": { + "type": "boolean" + }, + "headers": { + "properties": { + "accept": { + "ignore_above": 1024, + "type": "keyword" + }, + "accept_encoding": { + "ignore_above": 1024, + "type": "keyword" + }, + "access_control_request_headers": { + "ignore_above": 1024, + "type": "keyword" + }, + "access_control_request_method": { + "ignore_above": 1024, + "type": "keyword" + }, + "authority": { + "ignore_above": 1024, + "type": "keyword" + }, + "authorization": { + "ignore_above": 1024, + "type": "keyword" + }, + "connection": { + "ignore_above": 1024, + "type": "keyword" + }, + "content_length": { + "ignore_above": 1024, + "type": "keyword" + }, + "content_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "cookie": { + "ignore_above": 1024, + "type": "keyword" + }, + "host": { + "ignore_above": 1024, + "type": "keyword" + }, + "if_modified_since": { + "ignore_above": 1024, + "type": "keyword" + }, + "if_none_match": { + "ignore_above": 1024, + "type": "keyword" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "purpose": { + "ignore_above": 1024, + "type": "keyword" + }, + "range": { + "ignore_above": 1024, + "type": "keyword" + }, + "referer": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "sec_ch_ua": { + "ignore_above": 1024, + "type": "keyword" + }, + "sec_ch_ua_mobile": { + "ignore_above": 1024, + "type": "keyword" + }, + "sec_ch_ua_platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "sec_fetch_dest": { + "ignore_above": 1024, + "type": "keyword" + }, + "sec_fetch_mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "sec_fetch_site": { + "ignore_above": 1024, + "type": "keyword" + }, + "sec_fetch_user": { + "ignore_above": 1024, + "type": "keyword" + }, + "traceparent": { + "ignore_above": 1024, + "type": "keyword" + }, + "upgrade_insecure_requests": { + "ignore_above": 1024, + "type": "keyword" + }, + "user_agent": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_swiftype_client": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_swiftype_client_version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "initial_priority": { + "ignore_above": 1024, + "type": "keyword" + }, + "is_link_preload": { + "type": "boolean" + }, + "is_same_site": { + "type": "boolean" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "mixed_content_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "post_data": { + "ignore_above": 1024, + "type": "keyword" + }, + "post_data_entries": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "referrer": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer_policy": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response": { + "properties": { + "body": { + "properties": { + "bytes": { + "type": "long" + }, + "content": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "wildcard" + }, + "hash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "bytes": { + "type": "long" + }, + "connection_id": { + "type": "long" + }, + "connection_reused": { + "type": "boolean" + }, + "encoded_data_length": { + "type": "long" + }, + "from_disk_cache": { + "type": "boolean" + }, + "from_prefetch_cache": { + "type": "boolean" + }, + "from_service_worker": { + "type": "boolean" + }, + "headers": { + "properties": { + "*": { + "enabled": false, + "type": "object" + }, + "accept_ranges": { + "ignore_above": 1024, + "type": "keyword" + }, + "access_control_allow_credentials": { + "ignore_above": 1024, + "type": "keyword" + }, + "access_control_allow_headers": { + "ignore_above": 1024, + "type": "keyword" + }, + "access_control_allow_methods": { + "ignore_above": 1024, + "type": "keyword" + }, + "access_control_allow_origin": { + "ignore_above": 1024, + "type": "keyword" + }, + "access_control_expose_headers": { + "ignore_above": 1024, + "type": "keyword" + }, + "access_control_max_age": { + "ignore_above": 1024, + "type": "keyword" + }, + "access_control_request_method": { + "ignore_above": 1024, + "type": "keyword" + }, + "age": { + "ignore_above": 1024, + "type": "keyword" + }, + "alt_svc": { + "ignore_above": 1024, + "type": "keyword" + }, + "bfcache_opt_in": { + "ignore_above": 1024, + "type": "keyword" + }, + "cache_control": { + "ignore_above": 1024, + "type": "keyword" + }, + "cached": { + "ignore_above": 1024, + "type": "keyword" + }, + "cf_cache_status": { + "ignore_above": 1024, + "type": "keyword" + }, + "cf_ray": { + "ignore_above": 1024, + "type": "keyword" + }, + "connection": { + "ignore_above": 1024, + "type": "keyword" + }, + "content_disposition": { + "ignore_above": 1024, + "type": "keyword" + }, + "content_encoding": { + "ignore_above": 1024, + "type": "keyword" + }, + "content_language": { + "ignore_above": 1024, + "type": "keyword" + }, + "content_length": { + "ignore_above": 1024, + "type": "keyword" + }, + "content_range": { + "ignore_above": 1024, + "type": "keyword" + }, + "content_security_policy": { + "ignore_above": 1024, + "type": "keyword" + }, + "content_security_policy_report_only": { + "ignore_above": 1024, + "type": "keyword" + }, + "content_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "cross_origin_opener_policy": { + "ignore_above": 1024, + "type": "keyword" + }, + "cross_origin_opener_policy_report_only": { + "ignore_above": 1024, + "type": "keyword" + }, + "cross_origin_resource_policy": { + "ignore_above": 1024, + "type": "keyword" + }, + "date": { + "ignore_above": 1024, + "type": "keyword" + }, + "e_tag": { + "ignore_above": 1024, + "type": "keyword" + }, + "elastic_vi": { + "ignore_above": 1024, + "type": "keyword" + }, + "etag": { + "ignore_above": 1024, + "type": "keyword" + }, + "expect_ct": { + "ignore_above": 1024, + "type": "keyword" + }, + "expires": { + "ignore_above": 1024, + "type": "keyword" + }, + "fastly_io_error": { + "ignore_above": 1024, + "type": "keyword" + }, + "fastly_io_info": { + "ignore_above": 1024, + "type": "keyword" + }, + "fastly_io_warning": { + "ignore_above": 1024, + "type": "keyword" + }, + "fastly_stats": { + "ignore_above": 1024, + "type": "keyword" + }, + "last_modified": { + "ignore_above": 1024, + "type": "keyword" + }, + "location": { + "ignore_above": 1024, + "type": "keyword" + }, + "p3p": { + "ignore_above": 1024, + "type": "keyword" + }, + "permissions_policy": { + "ignore_above": 1024, + "type": "keyword" + }, + "referrer_policy": { + "ignore_above": 1024, + "type": "keyword" + }, + "report_to": { + "ignore_above": 1024, + "type": "keyword" + }, + "request_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "server": { + "ignore_above": 1024, + "type": "keyword" + }, + "server_timing": { + "ignore_above": 1024, + "type": "keyword" + }, + "set_cookie": { + "ignore_above": 1024, + "type": "keyword" + }, + "source_age": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "strict_transport_security": { + "ignore_above": 1024, + "type": "keyword" + }, + "timing_allow_origin": { + "ignore_above": 1024, + "type": "keyword" + }, + "transfer_encoding": { + "ignore_above": 1024, + "type": "keyword" + }, + "vary": { + "ignore_above": 1024, + "type": "keyword" + }, + "via": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_amz_cf_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_amz_cf_pop": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_amz_id_2": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_amz_meta_pci_enabled": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_amz_meta_revision": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_amz_replication_status": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_amz_request_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_amz_server_side_encryption": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_amz_storage_class": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_amz_version_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_cache": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_cache_hits": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_cdn": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_change_language": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_china": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_cloud_request_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_content_type_options": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_contentstack_organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_cs_surrogate_key": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_dns_prefetch_control": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_download_options": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_fastly_request_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_found_handling_cluster": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_found_handling_instance": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_frame_options": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_github_request_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_influxdb_build": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_influxdb_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_permitted_cross_domain_policies": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_powered_by": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_ratelimit_limit": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_ratelimit_remaining": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_request_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_runtime": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_served_by": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_swiftype_backend_datacenter": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_swiftype_backend_node": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_swiftype_backend_region": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_swiftype_edge_datacenter": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_swiftype_edge_node": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_swiftype_frontend_datacenter": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_swiftype_frontend_node": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_timer": { + "ignore_above": 1024, + "type": "keyword" + }, + "x_xss_protection": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "mime_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "redirects": { + "ignore_above": 1024, + "type": "keyword" + }, + "remote_i_p_address": { + "ignore_above": 1024, + "type": "keyword" + }, + "remote_port": { + "type": "long" + }, + "request_headers": { + "properties": { + "accept": { + "ignore_above": 1024, + "type": "keyword" + }, + "accept_encoding": { + "ignore_above": 1024, + "type": "keyword" + }, + "authority": { + "ignore_above": 1024, + "type": "keyword" + }, + "content_length": { + "ignore_above": 1024, + "type": "keyword" + }, + "content_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "cookie": { + "ignore_above": 1024, + "type": "keyword" + }, + "if_none_match": { + "ignore_above": 1024, + "type": "keyword" + }, + "method": { + "ignore_above": 1024, + "type": "keyword" + }, + "origin": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "keyword" + }, + "referer": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "sec_ch_ua": { + "ignore_above": 1024, + "type": "keyword" + }, + "sec_ch_ua_mobile": { + "ignore_above": 1024, + "type": "keyword" + }, + "sec_ch_ua_platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "sec_fetch_dest": { + "ignore_above": 1024, + "type": "keyword" + }, + "sec_fetch_mode": { + "ignore_above": 1024, + "type": "keyword" + }, + "sec_fetch_site": { + "ignore_above": 1024, + "type": "keyword" + }, + "sec_fetch_user": { + "ignore_above": 1024, + "type": "keyword" + }, + "traceparent": { + "ignore_above": 1024, + "type": "keyword" + }, + "upgrade_insecure_requests": { + "ignore_above": 1024, + "type": "keyword" + }, + "user_agent": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "response_time": { + "type": "float" + }, + "security_details": { + "properties": { + "certificate_id": { + "type": "long" + }, + "certificate_transparency_compliance": { + "ignore_above": 1024, + "type": "keyword" + }, + "cipher": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "ignore_above": 1024, + "type": "keyword" + }, + "key_exchange": { + "ignore_above": 1024, + "type": "keyword" + }, + "key_exchange_group": { + "ignore_above": 1024, + "type": "keyword" + }, + "protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "san_list": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "valid_from": { + "type": "float" + }, + "valid_to": { + "type": "float" + } + } + }, + "security_state": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "type": "long" + }, + "status_code": { + "type": "long" + }, + "status_text": { + "ignore_above": 1024, + "type": "keyword" + }, + "timing": { + "properties": { + "connect_end": { + "type": "float" + }, + "connect_start": { + "type": "float" + }, + "dns_end": { + "type": "float" + }, + "dns_start": { + "type": "float" + }, + "proxy_end": { + "type": "long" + }, + "proxy_start": { + "type": "long" + }, + "push_end": { + "type": "long" + }, + "push_start": { + "type": "long" + }, + "receive_headers_end": { + "type": "float" + }, + "request_time": { + "type": "float" + }, + "send_end": { + "type": "float" + }, + "send_start": { + "type": "float" + }, + "ssl_end": { + "type": "float" + }, + "ssl_start": { + "type": "float" + }, + "worker_fetch_start": { + "type": "long" + }, + "worker_ready": { + "type": "long" + }, + "worker_respond_with_settled": { + "type": "long" + }, + "worker_start": { + "type": "long" + } + } + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "rtt": { + "properties": { + "content": { + "properties": { + "us": { + "type": "long" + } + } + }, + "response_header": { + "properties": { + "us": { + "type": "long" + } + } + }, + "total": { + "properties": { + "us": { + "type": "long" + } + } + }, + "validate": { + "properties": { + "us": { + "type": "long" + } + } + }, + "validate_body": { + "properties": { + "us": { + "type": "long" + } + } + }, + "write_request": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "icmp": { + "properties": { + "requests": { + "type": "long" + }, + "rtt": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "interface": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "jolokia": { + "properties": { + "agent": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "secured": { + "type": "boolean" + }, + "server": { + "properties": { + "product": { + "ignore_above": 1024, + "type": "keyword" + }, + "vendor": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "kubernetes": { + "properties": { + "annotations": { + "properties": { + "*": { + "type": "object" + } + } + }, + "container": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "deployment": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "labels": { + "properties": { + "*": { + "type": "object" + } + } + }, + "namespace": { + "properties": { + "annotations": { + "properties": { + "*": { + "type": "object" + } + } + }, + "labels": { + "properties": { + "*": { + "type": "object" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uuid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "node": { + "properties": { + "hostname": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pod": { + "properties": { + "ip": { + "type": "ip" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "replicaset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "selectors": { + "properties": { + "*": { + "type": "object" + } + } + }, + "statefulset": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "labels": { + "type": "object" + }, + "log": { + "properties": { + "file": { + "properties": { + "path": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "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" + } + } + }, + "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": { + "type": "match_only_text" + }, + "monitor": { + "properties": { + "check_group": { + "ignore_above": 1024, + "type": "keyword" + }, + "duration": { + "properties": { + "us": { + "type": "long" + } + } + }, + "id": { + "fields": { + "text": { + "analyzer": "simple", + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "name": { + "fields": { + "text": { + "analyzer": "simple", + "norms": false, + "type": "text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "timespan": { + "type": "date_range" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "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": { + "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_code": { + "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" + }, + "postal_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "timezone": { + "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" + }, + "mac": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "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" + } + } + }, + "orchestrator": { + "properties": { + "api_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "cluster": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "namespace": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "resource": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "organization": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "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" + } + } + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "ignore_above": 1024, + "type": "keyword" + }, + "original_file_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "product": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "process": { + "properties": { + "args": { + "ignore_above": 1024, + "type": "keyword" + }, + "args_count": { + "type": "long" + }, + "code_signature": { + "properties": { + "digest_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "exists": { + "type": "boolean" + }, + "signing_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "team_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "wildcard" + }, + "elf": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "byte_order": { + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "creation_date": { + "type": "date" + }, + "exports": { + "type": "flattened" + }, + "header": { + "properties": { + "abi_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "entrypoint": { + "type": "long" + }, + "object_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "os_abi": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "imports": { + "type": "flattened" + }, + "sections": { + "properties": { + "chi2": { + "type": "long" + }, + "entropy": { + "type": "long" + }, + "flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_offset": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "virtual_address": { + "type": "long" + }, + "virtual_size": { + "type": "long" + } + }, + "type": "nested" + }, + "segments": { + "properties": { + "sections": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "shared_libraries": { + "ignore_above": 1024, + "type": "keyword" + }, + "telfhash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "end": { + "type": "date" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "type": "match_only_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" + }, + "ssdeep": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "owner": { + "properties": { + "id": { + "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": { + "digest_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "exists": { + "type": "boolean" + }, + "signing_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "team_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "trusted": { + "type": "boolean" + }, + "valid": { + "type": "boolean" + } + } + }, + "command_line": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "wildcard" + }, + "elf": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "byte_order": { + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "creation_date": { + "type": "date" + }, + "exports": { + "type": "flattened" + }, + "header": { + "properties": { + "abi_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "entrypoint": { + "type": "long" + }, + "object_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "os_abi": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "imports": { + "type": "flattened" + }, + "sections": { + "properties": { + "chi2": { + "type": "long" + }, + "entropy": { + "type": "long" + }, + "flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_offset": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "virtual_address": { + "type": "long" + }, + "virtual_size": { + "type": "long" + } + }, + "type": "nested" + }, + "segments": { + "properties": { + "sections": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "shared_libraries": { + "ignore_above": 1024, + "type": "keyword" + }, + "telfhash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "end": { + "type": "date" + }, + "entity_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "executable": { + "fields": { + "text": { + "type": "match_only_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" + }, + "ssdeep": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "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" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "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" + }, + "start": { + "type": "date" + }, + "thread": { + "properties": { + "id": { + "type": "long" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "title": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "uptime": { + "type": "long" + }, + "working_directory": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "wildcard" + }, + "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" + }, + "hosts": { + "ignore_above": 1024, + "type": "keyword" + }, + "ip": { + "type": "ip" + }, + "user": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "resolve": { + "properties": { + "ip": { + "type": "ip" + }, + "rtt": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "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" + } + } + }, + "screenshot_ref": { + "properties": { + "blocks": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + }, + "height": { + "type": "long" + }, + "left": { + "type": "long" + }, + "top": { + "type": "long" + }, + "width": { + "type": "long" + } + } + }, + "height": { + "type": "long" + }, + "width": { + "type": "long" + } + } + }, + "server": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "type": "match_only_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_code": { + "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" + }, + "postal_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "timezone": { + "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" + }, + "subdomain": { + "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": { + "type": "match_only_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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "service": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "environment": { + "ignore_above": 1024, + "type": "keyword" + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "origin": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "environment": { + "ignore_above": 1024, + "type": "keyword" + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "target": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "environment": { + "ignore_above": 1024, + "type": "keyword" + }, + "ephemeral_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "node": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "state": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "socks5": { + "properties": { + "rtt": { + "properties": { + "connect": { + "properties": { + "us": { + "type": "long" + } + } + } + } + } + } + }, + "source": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + }, + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "type": "match_only_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_code": { + "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" + }, + "postal_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "timezone": { + "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" + }, + "subdomain": { + "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": { + "type": "match_only_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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "span": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "summary": { + "properties": { + "down": { + "type": "long" + }, + "up": { + "type": "long" + } + } + }, + "synthetics": { + "properties": { + "blob": { + "type": "binary" + }, + "blob_mime": { + "ignore_above": 1024, + "type": "keyword" + }, + "browser": { + "properties": { + "experience": { + "properties": { + "cls": { + "type": "long" + }, + "dcl": { + "properties": { + "us": { + "type": "long" + } + } + }, + "fcp": { + "properties": { + "us": { + "type": "long" + } + } + }, + "lcp": { + "properties": { + "us": { + "type": "long" + } + } + }, + "load": { + "properties": { + "duration": { + "type": "long" + } + } + } + } + }, + "relative_trace": { + "properties": { + "duration": { + "properties": { + "us": { + "type": "long" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "score": { + "type": "long" + }, + "start": { + "properties": { + "us": { + "type": "long" + } + } + }, + "type": { + "norms": false, + "type": "text" + } + } + } + } + }, + "error": { + "properties": { + "message": { + "norms": false, + "type": "text" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "stack": { + "norms": false, + "type": "text" + } + } + }, + "index": { + "type": "long" + }, + "journey": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "norms": false, + "type": "text" + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "package_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "payload": { + "enabled": false, + "type": "object" + }, + "screenshot_ref": { + "dynamic": "false", + "properties": { + "blocks": { + "properties": { + "hash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "height": { + "type": "long" + }, + "width": { + "type": "long" + } + } + }, + "step": { + "properties": { + "duration": { + "properties": { + "us": { + "type": "long" + } + } + }, + "index": { + "type": "long" + }, + "name": { + "fields": { + "keyword": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "norms": false, + "type": "text" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tags": { + "ignore_above": 1024, + "type": "keyword" + }, + "tcp": { + "properties": { + "rtt": { + "properties": { + "connect": { + "properties": { + "us": { + "type": "long" + } + } + }, + "validate": { + "properties": { + "us": { + "type": "long" + } + } + } + } + } + } + }, + "threat": { + "properties": { + "enrichments": { + "properties": { + "indicator": { + "properties": { + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "confidence": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "digest_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "exists": { + "type": "boolean" + }, + "signing_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "team_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "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" + }, + "elf": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "byte_order": { + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "creation_date": { + "type": "date" + }, + "exports": { + "type": "flattened" + }, + "header": { + "properties": { + "abi_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "entrypoint": { + "type": "long" + }, + "object_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "os_abi": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "imports": { + "type": "flattened" + }, + "sections": { + "properties": { + "chi2": { + "type": "long" + }, + "entropy": { + "type": "long" + }, + "flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_offset": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "virtual_address": { + "type": "long" + }, + "virtual_size": { + "type": "long" + } + }, + "type": "nested" + }, + "segments": { + "properties": { + "sections": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "shared_libraries": { + "ignore_above": 1024, + "type": "keyword" + }, + "telfhash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fork_name": { + "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" + }, + "ssdeep": { + "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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "first_seen": { + "type": "date" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_code": { + "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" + }, + "postal_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "last_seen": { + "type": "date" + }, + "marking": { + "properties": { + "tlp": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "modified_at": { + "type": "date" + }, + "port": { + "type": "long" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "wildcard" + }, + "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" + } + } + }, + "scanner_stats": { + "type": "long" + }, + "sightings": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "wildcard" + }, + "original": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "wildcard" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "wildcard" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "matched": { + "properties": { + "atomic": { + "ignore_above": 1024, + "type": "keyword" + }, + "field": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "index": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + }, + "type": "nested" + }, + "framework": { + "ignore_above": 1024, + "type": "keyword" + }, + "group": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "indicator": { + "properties": { + "as": { + "properties": { + "number": { + "type": "long" + }, + "organization": { + "properties": { + "name": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "confidence": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "properties": { + "address": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "file": { + "properties": { + "accessed": { + "type": "date" + }, + "attributes": { + "ignore_above": 1024, + "type": "keyword" + }, + "code_signature": { + "properties": { + "digest_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "exists": { + "type": "boolean" + }, + "signing_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "status": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "team_id": { + "ignore_above": 1024, + "type": "keyword" + }, + "timestamp": { + "type": "date" + }, + "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" + }, + "elf": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "byte_order": { + "ignore_above": 1024, + "type": "keyword" + }, + "cpu_type": { + "ignore_above": 1024, + "type": "keyword" + }, + "creation_date": { + "type": "date" + }, + "exports": { + "type": "flattened" + }, + "header": { + "properties": { + "abi_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "class": { + "ignore_above": 1024, + "type": "keyword" + }, + "data": { + "ignore_above": 1024, + "type": "keyword" + }, + "entrypoint": { + "type": "long" + }, + "object_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "os_abi": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "imports": { + "type": "flattened" + }, + "sections": { + "properties": { + "chi2": { + "type": "long" + }, + "entropy": { + "type": "long" + }, + "flags": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_offset": { + "ignore_above": 1024, + "type": "keyword" + }, + "physical_size": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "virtual_address": { + "type": "long" + }, + "virtual_size": { + "type": "long" + } + }, + "type": "nested" + }, + "segments": { + "properties": { + "sections": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + } + }, + "type": "nested" + }, + "shared_libraries": { + "ignore_above": 1024, + "type": "keyword" + }, + "telfhash": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fork_name": { + "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" + }, + "ssdeep": { + "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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "pe": { + "properties": { + "architecture": { + "ignore_above": 1024, + "type": "keyword" + }, + "company": { + "ignore_above": 1024, + "type": "keyword" + }, + "description": { + "ignore_above": 1024, + "type": "keyword" + }, + "file_version": { + "ignore_above": 1024, + "type": "keyword" + }, + "imphash": { + "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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "uid": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "first_seen": { + "type": "date" + }, + "geo": { + "properties": { + "city_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "continent_code": { + "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" + }, + "postal_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_iso_code": { + "ignore_above": 1024, + "type": "keyword" + }, + "region_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "timezone": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "ip": { + "type": "ip" + }, + "last_seen": { + "type": "date" + }, + "marking": { + "properties": { + "tlp": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "modified_at": { + "type": "date" + }, + "port": { + "type": "long" + }, + "provider": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "registry": { + "properties": { + "data": { + "properties": { + "bytes": { + "ignore_above": 1024, + "type": "keyword" + }, + "strings": { + "ignore_above": 1024, + "type": "wildcard" + }, + "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" + } + } + }, + "scanner_stats": { + "type": "long" + }, + "sightings": { + "type": "long" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "wildcard" + }, + "original": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "wildcard" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "wildcard" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "software": { + "properties": { + "alias": { + "ignore_above": 1024, + "type": "keyword" + }, + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "platforms": { + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + }, + "subtechnique": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "reference": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + } + } + }, + "timeseries": { + "properties": { + "instance": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "tls": { + "properties": { + "certificate_not_valid_after": { + "type": "date" + }, + "certificate_not_valid_before": { + "type": "date" + }, + "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" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "established": { + "type": "boolean" + }, + "next_protocol": { + "ignore_above": 1024, + "type": "keyword" + }, + "resumed": { + "type": "boolean" + }, + "rtt": { + "properties": { + "handshake": { + "properties": { + "us": { + "type": "long" + } + } + } + } + }, + "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_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + }, + "version_protocol": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "trace": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "transaction": { + "properties": { + "id": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "url": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "extension": { + "ignore_above": 1024, + "type": "keyword" + }, + "fragment": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "wildcard" + }, + "original": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "wildcard" + }, + "password": { + "ignore_above": 1024, + "type": "keyword" + }, + "path": { + "ignore_above": 1024, + "type": "wildcard" + }, + "port": { + "type": "long" + }, + "query": { + "ignore_above": 1024, + "type": "keyword" + }, + "registered_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "scheme": { + "ignore_above": 1024, + "type": "keyword" + }, + "subdomain": { + "ignore_above": 1024, + "type": "keyword" + }, + "top_level_domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "username": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "user": { + "properties": { + "changes": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "match_only_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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "effective": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "match_only_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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "match_only_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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + }, + "target": { + "properties": { + "domain": { + "ignore_above": 1024, + "type": "keyword" + }, + "email": { + "ignore_above": 1024, + "type": "keyword" + }, + "full_name": { + "fields": { + "text": { + "type": "match_only_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": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "roles": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "user_agent": { + "properties": { + "device": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "name": { + "ignore_above": 1024, + "type": "keyword" + }, + "original": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "os": { + "properties": { + "family": { + "ignore_above": 1024, + "type": "keyword" + }, + "full": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "kernel": { + "ignore_above": 1024, + "type": "keyword" + }, + "name": { + "fields": { + "text": { + "type": "match_only_text" + } + }, + "ignore_above": 1024, + "type": "keyword" + }, + "platform": { + "ignore_above": 1024, + "type": "keyword" + }, + "type": { + "ignore_above": 1024, + "type": "keyword" + }, + "version": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version": { + "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": { + "type": "match_only_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" + } + } + }, + "x509": { + "properties": { + "alternative_names": { + "ignore_above": 1024, + "type": "keyword" + }, + "issuer": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "not_after": { + "type": "date" + }, + "not_before": { + "type": "date" + }, + "public_key_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_curve": { + "ignore_above": 1024, + "type": "keyword" + }, + "public_key_exponent": { + "doc_values": false, + "index": false, + "type": "long" + }, + "public_key_size": { + "type": "long" + }, + "serial_number": { + "ignore_above": 1024, + "type": "keyword" + }, + "signature_algorithm": { + "ignore_above": 1024, + "type": "keyword" + }, + "subject": { + "properties": { + "common_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "country": { + "ignore_above": 1024, + "type": "keyword" + }, + "distinguished_name": { + "ignore_above": 1024, + "type": "keyword" + }, + "locality": { + "ignore_above": 1024, + "type": "keyword" + }, + "organization": { + "ignore_above": 1024, + "type": "keyword" + }, + "organizational_unit": { + "ignore_above": 1024, + "type": "keyword" + }, + "state_or_province": { + "ignore_above": 1024, + "type": "keyword" + } + } + }, + "version_number": { + "ignore_above": 1024, + "type": "keyword" + } + } + } + } + }, + "settings": { + "index": { + "codec": "best_compression", + "lifecycle": { + "name": "heartbeat", + "rollover_alias": "heartbeat-8.1.0" + }, + "mapping": { + "total_fields": { + "limit": "10000" + } + }, + "max_docvalue_fields_search": "200", + "number_of_replicas": "1", + "number_of_shards": "1", + "refresh_interval": "5s" + } + } + } +} \ No newline at end of file diff --git a/x-pack/plugins/uptime/e2e/journeys/index.ts b/x-pack/plugins/uptime/e2e/journeys/index.ts index e59ba1fa0c6e0..89abed5ce8f29 100644 --- a/x-pack/plugins/uptime/e2e/journeys/index.ts +++ b/x-pack/plugins/uptime/e2e/journeys/index.ts @@ -6,3 +6,4 @@ */ export * from './uptime.journey'; +export * from './step_duration.journey'; diff --git a/x-pack/plugins/uptime/e2e/journeys/step_duration.journey.ts b/x-pack/plugins/uptime/e2e/journeys/step_duration.journey.ts new file mode 100644 index 0000000000000..ffd235cf8106b --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/step_duration.journey.ts @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { journey, step, expect, before } from '@elastic/synthetics'; +import { loginToKibana, waitForLoadingToFinish } from './utils'; + +journey('StepsDuration', async ({ page, params }) => { + before(async () => { + await waitForLoadingToFinish({ page }); + }); + + const queryParams = new URLSearchParams({ + dateRangeStart: '2021-11-21T22:06:06.502Z', + dateRangeEnd: '2021-11-21T22:10:08.203Z', + }).toString(); + + const baseUrl = `${params.kibanaUrl}/app/uptime`; + + step('Go to uptime', async () => { + await page.goto(`${baseUrl}?${queryParams}`, { + waitUntil: 'networkidle', + }); + await loginToKibana({ page }); + }); + + step('Go to monitor details', async () => { + await page.click('text=Dismiss'); + await page.click('button:has-text("test-monitor - inline")'); + expect(page.url()).toBe(`${baseUrl}/monitor/dGVzdC1tb25pdG9yLWlubGluZQ==/?${queryParams}`); + }); + + step('Go to journey details', async () => { + await page.click('text=18 seconds'); + expect(page.url()).toBe(`${baseUrl}/journey/9f217c22-4b17-11ec-b976-aa665a54da40/steps`); + }); + + step('Check for monitor duration', async () => { + await page.hover('text=8.9 sec'); + await page.waitForSelector('text=Explore'); + expect(await page.$('text=Explore')).toBeTruthy(); + await page.waitForSelector('text=area chart'); + expect(await page.$('text=area chart')).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/uptime/e2e/journeys/uptime.journey.ts b/x-pack/plugins/uptime/e2e/journeys/uptime.journey.ts index 59a289ef21e7b..1c3f82c517b82 100644 --- a/x-pack/plugins/uptime/e2e/journeys/uptime.journey.ts +++ b/x-pack/plugins/uptime/e2e/journeys/uptime.journey.ts @@ -5,22 +5,17 @@ * 2.0. */ -import { journey, step } from '@elastic/synthetics'; +import { journey, step, before } from '@elastic/synthetics'; +import { waitForLoadingToFinish } from './utils'; export const byTestId = (testId: string) => { return `[data-test-subj=${testId}]`; }; journey('uptime', ({ page, params }) => { - async function waitForLoadingToFinish() { - let isLoadingVisible = true; - - while (isLoadingVisible) { - const loading = await page.$(byTestId('kbnLoadingMessage')); - isLoadingVisible = loading !== null; - await page.waitForTimeout(5 * 1000); - } - } + before(async () => { + await waitForLoadingToFinish({ page }); + }); step('Go to Kibana', async () => { await page.goto(`${params.kibanaUrl}/app/uptime?dateRangeStart=now-5y&dateRangeEnd=now`, { @@ -38,7 +33,6 @@ journey('uptime', ({ page, params }) => { }); step('dismiss synthetics notice', async () => { - await waitForLoadingToFinish(); await page.click('[data-test-subj=uptimeDismissSyntheticsCallout]', { timeout: 60 * 1000, }); diff --git a/x-pack/plugins/uptime/e2e/journeys/utils.ts b/x-pack/plugins/uptime/e2e/journeys/utils.ts new file mode 100644 index 0000000000000..3188c86f82049 --- /dev/null +++ b/x-pack/plugins/uptime/e2e/journeys/utils.ts @@ -0,0 +1,27 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Page } from '@elastic/synthetics'; +import { byTestId } from './uptime.journey'; + +export async function waitForLoadingToFinish({ page }: { page: Page }) { + while (true) { + if ((await page.$(byTestId('kbnLoadingMessage'))) === null) break; + await page.waitForTimeout(5 * 1000); + } +} + +export async function loginToKibana({ page }: { page: Page }) { + await page.fill('[data-test-subj=loginUsername]', 'elastic', { + timeout: 60 * 1000, + }); + await page.fill('[data-test-subj=loginPassword]', 'changeme'); + + await page.click('[data-test-subj=loginSubmit]'); + + await waitForLoadingToFinish({ page }); +} diff --git a/x-pack/plugins/uptime/e2e/playwright_run.ts b/x-pack/plugins/uptime/e2e/playwright_run.ts index 361a4bc6f7491..1b5cd959f8e87 100644 --- a/x-pack/plugins/uptime/e2e/playwright_run.ts +++ b/x-pack/plugins/uptime/e2e/playwright_run.ts @@ -5,13 +5,29 @@ * 2.0. */ import { FtrConfigProviderContext } from '@kbn/test'; +import yargs from 'yargs'; import { playwrightRunTests } from './playwright_start'; +const { argv } = yargs(process.argv.slice(2)) + .option('headless', { + default: true, + type: 'boolean', + description: 'Start in headless mode', + }) + .option('grep', { + default: undefined, + type: 'string', + description: 'run only journeys with a name or tags that matches the glob', + }) + .help(); + +const { headless, grep } = argv; + async function runE2ETests({ readConfigFile }: FtrConfigProviderContext) { const kibanaConfig = await readConfigFile(require.resolve('./config.ts')); return { ...kibanaConfig.getAll(), - testRunner: playwrightRunTests(), + testRunner: playwrightRunTests({ headless, match: grep }), }; } diff --git a/x-pack/plugins/uptime/e2e/playwright_start.ts b/x-pack/plugins/uptime/e2e/playwright_start.ts index 5949339c1ba25..fe4d3ff804bf9 100644 --- a/x-pack/plugins/uptime/e2e/playwright_start.ts +++ b/x-pack/plugins/uptime/e2e/playwright_start.ts @@ -13,22 +13,32 @@ import { esArchiverLoad, esArchiverUnload } from './tasks/es_archiver'; import './journeys'; -export function playwrightRunTests() { +export function playwrightRunTests({ headless, match }: { headless: boolean; match?: string }) { return async ({ getService }: any) => { - const result = await playwrightStart(getService); - - if (result && result.uptime.status !== 'succeeded') { + const result = await playwrightStart(getService, headless, match); + + if ( + result?.uptime && + result.uptime.status !== 'succeeded' && + result.StepsDuration && + result.StepsDuration.status !== 'succeeded' + ) { throw new Error('Tests failed'); } }; } -async function playwrightStart(getService: any) { +async function playwrightStart(getService: any, headless = true, match?: string) { console.log('Loading esArchiver...'); - await esArchiverLoad('full_heartbeat'); + const esArchiver = getService('esArchiver'); + + esArchiverLoad('full_heartbeat'); + esArchiverLoad('browser'); const config = getService('config'); + await esArchiver.loadIfNeeded('x-pack/test/functional/es_archives/ml/farequote'); + const kibanaUrl = Url.format({ protocol: config.get('servers.kibana.protocol'), hostname: config.get('servers.kibana.hostname'), @@ -37,11 +47,13 @@ async function playwrightStart(getService: any) { const res = await playwrightRun({ params: { kibanaUrl }, - playwrightOptions: { headless: true, chromiumSandbox: false, timeout: 60 * 1000 }, + playwrightOptions: { headless, chromiumSandbox: false, timeout: 60 * 1000 }, + match: match === 'undefined' ? '' : match, }); console.log('Removing esArchiver...'); - await esArchiverUnload('full_heartbeat'); + esArchiverUnload('full_heartbeat'); + esArchiverUnload('browser'); return res; } diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx index b0f3b29599242..621e269521ad9 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/simple_fields.tsx @@ -13,7 +13,7 @@ import { ConfigKey } from '../types'; import { useBrowserSimpleFieldsContext } from '../contexts'; import { ScheduleField } from '../schedule_field'; import { SourceField } from './source_field'; -import { CommonFields } from '../common/common_fields'; +import { SimpleFieldsWrapper } from '../common/simple_fields_wrapper'; interface Props { validate: Validation; @@ -58,7 +58,7 @@ export const BrowserSimpleFields = memo(({ validate }) => { ); return ( - <> + (({ validate }) => { )} /> - - + ); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.test.tsx index 6cf37aa2238f3..402bd175a09ea 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/browser/source_field.test.tsx @@ -17,6 +17,12 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => `id-${Math.random()}`, })); +// ensures that fields appropriately match to their label +jest.mock('@elastic/eui/lib/services/accessibility', () => ({ + ...jest.requireActual('@elastic/eui/lib/services/accessibility'), + useGeneratedHtmlId: () => `id-${Math.random()}`, +})); + jest.mock('../../../../../../../src/plugins/kibana_react/public', () => { const original = jest.requireActual('../../../../../../../src/plugins/kibana_react/public'); return { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/default_values.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/default_values.ts index 50c540266724a..c997d42a22c00 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/common/default_values.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/default_values.ts @@ -9,6 +9,7 @@ import { CommonFields, ConfigKey, ScheduleUnit, DataStream } from '../types'; export const defaultValues: CommonFields = { [ConfigKey.MONITOR_TYPE]: DataStream.HTTP, + [ConfigKey.ENABLED]: true, [ConfigKey.SCHEDULE]: { number: '3', unit: ScheduleUnit.MINUTES, diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/enabled.tsx b/x-pack/plugins/uptime/public/components/fleet_package/common/enabled.tsx new file mode 100644 index 0000000000000..09a8e9aec3719 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/enabled.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { EuiFormRow, EuiSwitch } from '@elastic/eui'; +import { ConfigKey, CommonFields } from '../types'; + +interface Props { + fields: CommonFields; + onChange: ({ value, configKey }: { value: boolean; configKey: ConfigKey }) => void; +} + +export function Enabled({ fields, onChange }: Props) { + return ( + <> + + } + > + + } + data-test-subj="syntheticsEnabled" + checked={fields[ConfigKey.ENABLED]} + onChange={(event) => + onChange({ + value: event.target.checked, + configKey: ConfigKey.ENABLED, + }) + } + /> + + + ); +} diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts index 077e0fb0becda..4934882430cb4 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/formatters.ts @@ -14,6 +14,7 @@ export type CommonFormatMap = Record JSON.stringify( `@every ${fields[ConfigKey.SCHEDULE]?.number}${fields[ConfigKey.SCHEDULE]?.unit}` diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts index 5710a99a65660..bcc521abf468a 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/normalizers.ts @@ -57,6 +57,7 @@ export const getCommonCronToSecondsNormalizer = (key: ConfigKey) => { export const commonNormalizers: CommonNormalizerMap = { [ConfigKey.NAME]: (fields) => fields?.[ConfigKey.NAME]?.value ?? '', + [ConfigKey.ENABLED]: getCommonNormalizer(ConfigKey.ENABLED), [ConfigKey.MONITOR_TYPE]: getCommonNormalizer(ConfigKey.MONITOR_TYPE), [ConfigKey.SCHEDULE]: (fields) => { const value = fields?.[ConfigKey.SCHEDULE]?.value; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/common/simple_fields_wrapper.tsx b/x-pack/plugins/uptime/public/components/fleet_package/common/simple_fields_wrapper.tsx new file mode 100644 index 0000000000000..989763ab92275 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/fleet_package/common/simple_fields_wrapper.tsx @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { ConfigKey, Validation, CommonFields as CommonFieldsType } from '../types'; +import { CommonFields } from '../common/common_fields'; +import { Enabled } from '../common/enabled'; + +interface Props { + validate: Validation; + onInputChange: ({ value, configKey }: { value: unknown; configKey: ConfigKey }) => void; + children: React.ReactNode; + fields: CommonFieldsType; +} + +export const SimpleFieldsWrapper = ({ validate, onInputChange, children, fields }: Props) => { + return ( + <> + + {children} + + + ); +}; diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx index 2aed5db789f44..6597953279e81 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.test.tsx @@ -28,6 +28,12 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => `id-${Math.random()}`, })); +// ensures that fields appropriately match to their label +jest.mock('@elastic/eui/lib/services/accessibility', () => ({ + ...jest.requireActual('@elastic/eui/lib/services/accessibility'), + useGeneratedHtmlId: () => `id-${Math.random()}`, +})); + jest.mock('../../../../../../src/plugins/kibana_react/public', () => { const original = jest.requireActual('../../../../../../src/plugins/kibana_react/public'); return { @@ -323,4 +329,19 @@ describe('', () => { expect(queryByText('Browser (Beta)')).not.toBeInTheDocument(); }); }); + + it('allows monitors to be disabled', async () => { + const { queryByLabelText } = render( + + ); + + const enabled = queryByLabelText('Enabled') as HTMLInputElement; + expect(enabled).toBeChecked(); + + fireEvent.click(enabled); + + await waitFor(() => { + expect(enabled).not.toBeChecked(); + }); + }); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx index 50f8d93ca8f4a..603c6b0e72560 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/custom_fields.tsx @@ -176,9 +176,10 @@ export const CustomFields = memo(({ validate, dataStreams = [], children defaultMessage="Configure TLS options, including verification mode, certificate authorities, and client certificates." /> } + id="uptimeFleetIsTLSEnabled" > (({ validate }) => { }; return ( - <> + (({ validate }) => { } /> - - + ); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx index 20bd9b422ea0b..2fa1d471008f9 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/icmp/simple_fields.tsx @@ -12,7 +12,7 @@ import { ConfigKey, Validation } from '../types'; import { useICMPSimpleFieldsContext } from '../contexts'; import { OptionalLabel } from '../optional_label'; import { ScheduleField } from '../schedule_field'; -import { CommonFields } from '../common/common_fields'; +import { SimpleFieldsWrapper } from '../common/simple_fields_wrapper'; interface Props { validate: Validation; @@ -25,7 +25,7 @@ export const ICMPSimpleFields = memo(({ validate }) => { }; return ( - <> + (({ validate }) => { step={'any'} /> - - + ); }); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.test.tsx index 313fc460c5c5e..cbb3c0d3e477d 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_create_extension_wrapper.test.tsx @@ -21,6 +21,12 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => `id-${Math.random()}`, })); +// ensures that fields appropriately match to their label +jest.mock('@elastic/eui/lib/services/accessibility', () => ({ + ...jest.requireActual('@elastic/eui/lib/services/accessibility'), + useGeneratedHtmlId: () => `id-${Math.random()}`, +})); + jest.mock('../../../../../../src/plugins/kibana_react/public', () => { const original = jest.requireActual('../../../../../../src/plugins/kibana_react/public'); return { diff --git a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx index 5ad02ef2b9a50..d22f72b762e49 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/synthetics_policy_edit_extension_wrapper.test.tsx @@ -21,6 +21,12 @@ jest.mock('@elastic/eui/lib/services/accessibility/html_id_generator', () => ({ htmlIdGenerator: () => () => `id-${Math.random()}`, })); +// ensures that fields appropriately match to their label +jest.mock('@elastic/eui/lib/services/accessibility', () => ({ + ...jest.requireActual('@elastic/eui/lib/services/accessibility'), + useGeneratedHtmlId: () => `id-${Math.random()}`, +})); + jest.mock('../../../../../../src/plugins/kibana_react/public', () => { const original = jest.requireActual('../../../../../../src/plugins/kibana_react/public'); return { @@ -1174,6 +1180,7 @@ describe('', () => { }} /> ); + const verificationMode = queryByLabelText('Verification mode'); const enableTLSConfig = getByLabelText('Enable TLS configuration') as HTMLInputElement; expect(enableTLSConfig.getAttribute('aria-checked')).toEqual('false'); diff --git a/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx b/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx index cb6ebf6430eb8..4055b92dcf70f 100644 --- a/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx +++ b/x-pack/plugins/uptime/public/components/fleet_package/tcp/simple_fields.tsx @@ -11,7 +11,7 @@ import { EuiFormRow, EuiFieldText } from '@elastic/eui'; import { ConfigKey, Validation } from '../types'; import { useTCPSimpleFieldsContext } from '../contexts'; import { ScheduleField } from '../schedule_field'; -import { CommonFields } from '../common/common_fields'; +import { SimpleFieldsWrapper } from '../common/simple_fields_wrapper'; interface Props { validate: Validation; @@ -24,7 +24,7 @@ export const TCPSimpleFields = memo(({ validate }) => { }; return ( - <> + (({ validate }) => { unit={fields[ConfigKey.SCHEDULE].unit} /> - - + ); }); diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx index 7934d9878b435..7558a82e45df4 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_icon.test.tsx @@ -27,7 +27,11 @@ describe('', () => { type: 'step/end', step: { index: 0, + status: 'succeeded', name: 'test-name', + duration: { + us: 9999, + }, }, }, monitor: { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.test.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.test.tsx index f77457aa6df7b..39033103820e5 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.test.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.test.tsx @@ -17,16 +17,22 @@ describe('', () => { jest.spyOn(moment.prototype, 'diff').mockImplementation(mockDiff); + const timestamp = '2021-12-03T14:35:41.072Z'; + let activeStep: JourneyStep | undefined; beforeEach(() => { activeStep = { - '@timestamp': '123', + '@timestamp': timestamp, _id: 'id', synthetics: { type: 'step/end', step: { index: 0, + status: 'succeeded', name: 'test-name', + duration: { + us: 9999, + }, }, }, monitor: { @@ -38,7 +44,8 @@ describe('', () => { }, }, }; - mockDiff.mockReturnValue(0); + // value diff in milliseconds + mockDiff.mockReturnValue(10 * 1000); }); const BASE_PATH = 'xyz'; @@ -63,31 +70,20 @@ describe('', () => { expect(heading.innerHTML).toEqual('test title'); expect(getByLabelText('append title').innerHTML.indexOf(BASE_PATH)).not.toBe(-1); expect(getByText('kpi-over-time')); - expect(getByLabelText('attributes').innerHTML.indexOf('0s')).not.toBe(-1); - expect(getByLabelText('attributes').innerHTML.indexOf('0h')).toBe(-1); - expect(getByLabelText('attributes').innerHTML.indexOf('0m')).toBe(-1); - expect(getByLabelText('attributes').innerHTML.indexOf('0d')).toBe(-1); - }); - - it('handles days', () => { - mockDiff.mockReturnValue(10); - const { getByLabelText } = render( - - - - ); - const attributesText = getByLabelText('attributes').innerHTML; - expect(attributesText.indexOf('480s')).toBe(-1); - expect(attributesText.indexOf('480h')).toBe(-1); - expect(attributesText.indexOf('480m')).toBe(-1); - expect(attributesText.indexOf('480d')).not.toBe(-1); + expect(attributesText.includes('"2021-12-03T14:35:41.072Z"')).toBeTruthy(); + const attributes = JSON.parse(attributesText); + expect( + moment(attributes[0].time.from) + .add(10 * 1000 * 48, 'millisecond') + .toISOString() + ).toBe(timestamp); }); - it('handles hours', () => { - mockDiff.mockReturnValueOnce(0); - mockDiff.mockReturnValue(10); + it('handles timespan difference', () => { + const oneMinDiff = 60 * 1000; + mockDiff.mockReturnValue(oneMinDiff); const { getByLabelText } = render( @@ -96,28 +92,29 @@ describe('', () => { const attributesText = getByLabelText('attributes').innerHTML; - expect(attributesText.indexOf('480s')).toBe(-1); - expect(attributesText.indexOf('480h')).not.toBe(-1); - expect(attributesText.indexOf('480m')).toBe(-1); - expect(attributesText.indexOf('480d')).toBe(-1); - }); - - it('handles minutes', () => { - mockDiff.mockReturnValueOnce(0); - mockDiff.mockReturnValueOnce(0); - mockDiff.mockReturnValue(10); - const { getByLabelText } = render( - - - + expect(attributesText).toBe( + JSON.stringify([ + { + name: 'test title(test-name)', + selectedMetricField: 'field', + time: { to: '2021-12-03T14:35:41.072Z', from: '2021-12-03T13:47:41.072Z' }, + seriesType: 'area', + dataType: 'synthetics', + reportDefinitions: { + 'monitor.name': [null], + 'synthetics.step.name.keyword': ['test-name'], + }, + operationType: 'last_value', + }, + ]) ); - const attributesText = getByLabelText('attributes').innerHTML; - - expect(attributesText.indexOf('480s')).toBe(-1); - expect(attributesText.indexOf('480h')).toBe(-1); - expect(attributesText.indexOf('480m')).not.toBe(-1); - expect(attributesText.indexOf('480d')).toBe(-1); + const attributes = JSON.parse(attributesText); + expect( + moment(attributes[0].time.from) + .add(oneMinDiff * 48, 'millisecond') + .toISOString() + ).toBe(timestamp); }); it('returns null for missing active step', () => { diff --git a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.tsx b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.tsx index 1639e8a0d872c..cf533c6df63cc 100644 --- a/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.tsx +++ b/x-pack/plugins/uptime/public/components/monitor/synthetics/waterfall/components/waterfall_marker_trend.tsx @@ -6,101 +6,15 @@ */ import React from 'react'; -import { EuiButton } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import moment from 'moment'; -import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public'; -import { useUptimeStartPlugins } from '../../../../../contexts/uptime_startup_plugins_context'; -import { AllSeries, createExploratoryViewUrl } from '../../../../../../../observability/public'; -import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common'; import { useWaterfallContext } from '../context/waterfall_chart'; -import { JourneyStep } from '../../../../../../common/runtime_types'; - -const getLast48Intervals = (activeStep: JourneyStep) => { - const { lt, gte } = activeStep.monitor.timespan!; - const inDays = moment(lt).diff(moment(gte), 'days'); - if (inDays > 0) { - return { to: 'now', from: `now-${inDays * 48}d` }; - } - - const inHours = moment(lt).diff(moment(gte), 'hours'); - if (inHours > 0) { - return { to: 'now', from: `now-${inHours * 48}h` }; - } - - const inMinutes = moment(lt).diff(moment(gte), 'minutes'); - if (inMinutes > 0) { - return { to: 'now', from: `now-${inMinutes * 48}m` }; - } - - const inSeconds = moment(lt).diff(moment(gte), 'seconds'); - return { to: 'now', from: `now-${inSeconds * 48}s` }; -}; +import { StepFieldTrend } from '../../../../synthetics/check_steps/step_field_trend'; export function WaterfallMarkerTrend({ title, field }: { title: string; field: string }) { - const { observability } = useUptimeStartPlugins(); - - const EmbeddableExpView = observability!.ExploratoryViewEmbeddable; - - const basePath = useKibana().services.http?.basePath?.get(); - const { activeStep } = useWaterfallContext(); if (!activeStep) { return null; } - const allSeries: AllSeries = [ - { - name: `${title}(${activeStep.synthetics.step?.name!})`, - selectedMetricField: field, - time: getLast48Intervals(activeStep), - seriesType: 'area', - dataType: 'synthetics', - reportDefinitions: { - 'monitor.name': [activeStep.monitor.name!], - 'synthetics.step.name.keyword': [activeStep.synthetics.step?.name!], - }, - operationType: 'last_value', - }, - ]; - - const href = createExploratoryViewUrl( - { - reportType: 'kpi-over-time', - allSeries, - }, - basePath - ); - - return ( - - - {EXPLORE_LABEL} - - } - reportType={'kpi-over-time'} - attributes={allSeries} - axisTitlesVisibility={{ x: false, yLeft: false, yRight: false }} - legendIsVisible={false} - /> - - ); + return ; } - -export const EXPLORE_LABEL = i18n.translate('xpack.uptime.synthetics.markers.explore', { - defaultMessage: 'Explore', -}); - -const Wrapper = euiStyled.div` - height: 200px; - width: 400px; - &&& { - .expExpressionRenderer__expression { - padding-bottom: 0 !important; - } - } -`; diff --git a/x-pack/plugins/uptime/public/components/monitor_management/formatters/common.ts b/x-pack/plugins/uptime/public/components/monitor_management/formatters/common.ts index da50dfd4b0860..c8defe16b488e 100644 --- a/x-pack/plugins/uptime/public/components/monitor_management/formatters/common.ts +++ b/x-pack/plugins/uptime/public/components/monitor_management/formatters/common.ts @@ -15,6 +15,7 @@ export type CommonFormatMap = Record `@every ${fields[ConfigKey.SCHEDULE]?.number}${fields[ConfigKey.SCHEDULE]?.unit}`, diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_duration.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_duration.tsx new file mode 100644 index 0000000000000..182018d0881e2 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_duration.tsx @@ -0,0 +1,71 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import * as React from 'react'; +import { EuiButtonEmpty, EuiPopover } from '@elastic/eui'; +import { useMemo } from 'react'; +import { i18n } from '@kbn/i18n'; +import { JourneyStep } from '../../../../common/runtime_types'; +import { StepFieldTrend } from './step_field_trend'; +import { microToSec } from '../../../lib/formatting'; + +interface Props { + step: JourneyStep; + durationPopoverOpenIndex: number | null; + setDurationPopoverOpenIndex: (val: number | null) => void; +} + +export const StepDuration = ({ + step, + durationPopoverOpenIndex, + setDurationPopoverOpenIndex, +}: Props) => { + const component = useMemo( + () => ( + + ), + [step] + ); + + if (step.synthetics.step?.status === 'skipped') { + return --; + } + + const button = ( + setDurationPopoverOpenIndex(step.synthetics.step?.index ?? null)} + iconType="visArea" + > + {i18n.translate('xpack.uptime.synthetics.step.duration', { + defaultMessage: '{value} seconds', + values: { + value: microToSec(step.synthetics.step?.duration.us!, 1), + }, + })} + + ); + + return ( + setDurationPopoverOpenIndex(null)} + zIndex={100} + ownFocus={false} + > + {component} + + ); +}; + +const STEP_DURATION_TREND = i18n.translate('xpack.uptime.synthetics.step.durationTrend', { + defaultMessage: 'Step duration trend', +}); diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_field_trend.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_field_trend.test.tsx new file mode 100644 index 0000000000000..165208c032705 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_field_trend.test.tsx @@ -0,0 +1,89 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React from 'react'; +import { getLast48Intervals, StepFieldTrend } from './step_field_trend'; +import { render } from '../../../lib/helper/rtl_helpers'; +import { JourneyStep } from '../../../../common/runtime_types'; + +const step: JourneyStep = { + _id: 'docID', + monitor: { + check_group: 'check_group', + duration: { + us: 123, + }, + id: 'id', + status: 'up', + type: 'browser', + timespan: { + gte: '2021-12-01T12:54:28.098Z', + lt: '2021-12-01T12:55:28.098Z', + }, + }, + synthetics: { + step: { + index: 4, + status: 'succeeded', + name: 'STEP_NAME', + duration: { + us: 9999, + }, + }, + type: 'step/end', + }, + '@timestamp': '2021-12-03T15:23:41.072Z', +}; + +describe('StepFieldTrend', () => { + it('it renders embeddable', async () => { + const { findByText } = render( + + ); + + expect(await findByText('Embeddable exploratory view')).toBeInTheDocument(); + }); +}); + +describe('getLast48Intervals', () => { + it('it returns expected values', () => { + // 48 minutes difference + expect(getLast48Intervals(step)).toEqual({ + from: '2021-12-03T14:35:41.072Z', + to: '2021-12-03T15:23:41.072Z', + }); + step.monitor.timespan = { + gte: '2021-12-01T12:55:38.098Z', + lt: '2021-12-01T12:55:48.098Z', + }; + // 8 minutes difference + expect(getLast48Intervals(step)).toEqual({ + from: '2021-12-03T15:15:41.072Z', + to: '2021-12-03T15:23:41.072Z', + }); + step.monitor.timespan = { + gte: '2021-12-01T12:54:28.098Z', + lt: '2021-12-01T13:55:28.098Z', + }; + + // 48h difference + expect(getLast48Intervals(step)).toEqual({ + from: '2021-12-01T14:35:41.072Z', + to: '2021-12-03T15:23:41.072Z', + }); + step.monitor.timespan = { + gte: '2021-12-01T12:54:28.098Z', + lt: '2021-12-02T12:55:28.098Z', + }; + + // 48d difference + expect(getLast48Intervals(step)).toEqual({ + from: '2021-10-16T14:35:41.072Z', + to: '2021-12-03T15:23:41.072Z', + }); + }); +}); diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_field_trend.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_field_trend.tsx new file mode 100644 index 0000000000000..8c270f4bc2199 --- /dev/null +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/step_field_trend.tsx @@ -0,0 +1,108 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import React from 'react'; + +import { EuiButton } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import moment from 'moment'; +import { useSelector } from 'react-redux'; +import { useUptimeStartPlugins } from '../../../contexts/uptime_startup_plugins_context'; +import { JourneyStep } from '../../../../common/runtime_types'; +import { AllSeries, createExploratoryViewUrl } from '../../../../../observability/public'; +import { euiStyled } from '../../../../../../../src/plugins/kibana_react/common'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; +import { selectDynamicSettings } from '../../../state/selectors'; + +export const getLast48Intervals = (activeStep: JourneyStep) => { + const timestamp = activeStep['@timestamp']; + const { lt, gte } = activeStep.monitor.timespan!; + const difference = moment(lt).diff(moment(gte), 'millisecond') * 48; + + return { + to: timestamp, + from: moment(timestamp).subtract(difference, 'millisecond').toISOString(), + }; +}; + +export function StepFieldTrend({ + title, + field, + step: activeStep, +}: { + title: string; + field: string; + step: JourneyStep; +}) { + const { observability } = useUptimeStartPlugins(); + + const indexSettings = useSelector(selectDynamicSettings); + + const EmbeddableExpView = observability!.ExploratoryViewEmbeddable; + + const basePath = useKibana().services.http?.basePath?.get(); + + const allSeries: AllSeries = [ + { + name: `${title}(${activeStep.synthetics.step?.name!})`, + selectedMetricField: field, + time: getLast48Intervals(activeStep), + seriesType: 'area', + dataType: 'synthetics', + reportDefinitions: { + 'monitor.name': [activeStep.monitor.name!], + 'synthetics.step.name.keyword': [activeStep.synthetics.step?.name!], + }, + operationType: 'last_value', + }, + ]; + + const href = createExploratoryViewUrl( + { + reportType: 'kpi-over-time', + allSeries, + }, + basePath + ); + + return ( + + + {EXPLORE_LABEL} + + } + reportType={'kpi-over-time'} + attributes={allSeries} + axisTitlesVisibility={{ x: false, yLeft: false, yRight: false }} + legendIsVisible={false} + dataTypesIndexPatterns={ + indexSettings.settings?.heartbeatIndices + ? { + synthetics: indexSettings.settings?.heartbeatIndices, + } + : undefined + } + /> + + ); +} + +export const EXPLORE_LABEL = i18n.translate('xpack.uptime.synthetics.markers.explore', { + defaultMessage: 'Explore', +}); + +const Wrapper = euiStyled.div` + height: 200px; + width: 400px; + &&& { + .expExpressionRenderer__expression { + padding-bottom: 0 !important; + } + } +`; diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.test.tsx index 738960eb2af3e..3843ee013fe56 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.test.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.test.tsx @@ -35,6 +35,10 @@ describe('StepList component', () => { step: { name: 'load page', index: 1, + status: 'succeeded', + duration: { + us: 9999, + }, }, }, }, @@ -58,6 +62,10 @@ describe('StepList component', () => { step: { name: 'go to login', index: 2, + status: 'succeeded', + duration: { + us: 9999, + }, }, }, }, diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx index 9f2e5d609e867..2e2f863bdf7b5 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/steps_list.tsx @@ -7,7 +7,7 @@ import { EuiBasicTable, EuiBasicTableColumn, EuiButtonIcon, EuiTitle } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import React, { MouseEvent } from 'react'; +import React, { MouseEvent, useState } from 'react'; import styled from 'styled-components'; import { JourneyStep } from '../../../../common/runtime_types'; import { STATUS_LABEL } from '../../monitor/ping_list/translations'; @@ -17,6 +17,7 @@ import { StepDetailLink } from '../../common/step_detail_link'; import { VIEW_PERFORMANCE } from '../../monitor/synthetics/translations'; import { StepImage } from './step_image'; import { useExpandedRow } from './use_expanded_row'; +import { StepDuration } from './step_duration'; export const SpanWithMargin = styled.span` margin-right: 16px; @@ -79,6 +80,8 @@ export const StepsList = ({ data, error, loading }: Props) => { const { expandedRows, toggleExpand } = useExpandedRow({ steps, allSteps: data, loading }); + const [durationPopoverOpenIndex, setDurationPopoverOpenIndex] = useState(null); + const columns: Array> = [ { field: 'synthetics.payload.status', @@ -93,6 +96,18 @@ export const StepsList = ({ data, error, loading }: Props) => { name: STEP_NAME_LABEL, render: (_timestamp: string, item) => , }, + { + name: 'Step duration', + render: (item: JourneyStep) => { + return ( + + ); + }, + }, { align: 'left', field: 'timestamp', @@ -106,6 +121,7 @@ export const StepsList = ({ data, error, loading }: Props) => { ), }, + { align: 'right', width: '24px', @@ -133,7 +149,13 @@ export const StepsList = ({ data, error, loading }: Props) => { const targetElem = evt.target as HTMLElement; // we dont want to capture image click event - if (targetElem.tagName !== 'IMG' && targetElem.tagName !== 'BUTTON') { + if ( + targetElem.tagName !== 'IMG' && + targetElem.tagName !== 'BUTTON' && + targetElem.tagName !== 'CANVAS' && + !targetElem.classList.contains('euiButtonEmpty__text') && + !targetElem.classList.contains('euiIcon') + ) { toggleExpand({ journeyStep: item }); } }, diff --git a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx index e1f43cfebdbb2..c16522804eed6 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/check_steps/use_expanded_row.test.tsx @@ -51,6 +51,10 @@ describe('useExpandedROw', () => { step: { name: 'load page', index: 1, + status: 'succeeded', + duration: { + us: 9999, + }, }, }, }, @@ -73,6 +77,10 @@ describe('useExpandedROw', () => { step: { name: 'go to login', index: 2, + status: 'succeeded', + duration: { + us: 9999, + }, }, }, }, diff --git a/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx b/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx index e14b32fc8da9f..81d506bd05413 100644 --- a/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx +++ b/x-pack/plugins/uptime/public/components/synthetics/executed_step.test.tsx @@ -28,7 +28,11 @@ describe('ExecutedStep', () => { synthetics: { step: { index: 4, + status: 'succeeded', name: 'STEP_NAME', + duration: { + us: 9999, + }, }, type: 'step/end', }, @@ -43,7 +47,11 @@ describe('ExecutedStep', () => { }, step: { index: 3, + status: 'succeeded', name: 'STEP_NAME', + duration: { + us: 9999, + }, }, type: 'step/end', }; diff --git a/x-pack/plugins/uptime/public/lib/formatting.ts b/x-pack/plugins/uptime/public/lib/formatting.ts index 9bf0cac82dc09..ac04a9074d887 100644 --- a/x-pack/plugins/uptime/public/lib/formatting.ts +++ b/x-pack/plugins/uptime/public/lib/formatting.ts @@ -14,7 +14,10 @@ export function milliToSec(ms: number) { return ms / ONE_SECOND_AS_MILLI; } -export function microToSec(micro: number) { +export function microToSec(micro: number, fixedNumber?: number) { + if (fixedNumber) { + return (micro / ONE_SECOND_AS_MICROS).toFixed(fixedNumber); + } return (micro / ONE_SECOND_AS_MICROS).toFixed(0); } diff --git a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx index 84d2566304336..2c356c315a6dc 100644 --- a/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx +++ b/x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx @@ -124,6 +124,7 @@ const mockCore: () => Partial = () => { // @ts-ignore PageTemplate: EuiPageTemplate, }, + ExploratoryViewEmbeddable: () =>
Embeddable exploratory view
, }, }; @@ -143,7 +144,10 @@ export function MockKibanaProvider({ return ( - + {children} diff --git a/x-pack/plugins/uptime/scripts/e2e.js b/x-pack/plugins/uptime/scripts/e2e.js index e7c0cb612646d..d7405f2b8cab6 100644 --- a/x-pack/plugins/uptime/scripts/e2e.js +++ b/x-pack/plugins/uptime/scripts/e2e.js @@ -33,9 +33,19 @@ const { argv } = yargs(process.argv.slice(2)) type: 'string', description: 'Path to the Kibana install directory', }) + .option('headless', { + default: true, + type: 'boolean', + description: 'Start in headless mode', + }) + .option('grep', { + default: undefined, + type: 'string', + description: 'run only journeys with a name or tags that matches the glob', + }) .help(); -const { server, runner, open, kibanaInstallDir } = argv; +const { server, runner, open, kibanaInstallDir, headless, grep } = argv; const e2eDir = path.join(__dirname, '../e2e'); @@ -49,12 +59,22 @@ if (server) { const config = './playwright_run.ts'; function executeRunner() { - childProcess.execSync( - `node ../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}'`, - { - cwd: e2eDir, - stdio: 'inherit', - } - ); + if (runner) { + childProcess.execSync( + `node ../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}' --headless ${headless} --grep ${grep}`, + { + cwd: e2eDir, + stdio: 'inherit', + } + ); + } else { + childProcess.execSync( + `node ../../../scripts/${ftrScript} --config ${config} --kibana-install-dir '${kibanaInstallDir}' `, + { + cwd: e2eDir, + stdio: 'inherit', + } + ); + } } executeRunner(); diff --git a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts index c5779a56bfe82..988cb3ddb9447 100644 --- a/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts +++ b/x-pack/plugins/uptime/server/lib/adapters/framework/adapter_types.ts @@ -46,7 +46,7 @@ export interface UptimeServerSetup { cloud?: CloudSetup; fleet: FleetStartContract; security: SecurityPluginStart; - savedObjectsClient: SavedObjectsClientContract; + savedObjectsClient?: SavedObjectsClientContract; encryptedSavedObjects: EncryptedSavedObjectsPluginStart; syntheticsService: SyntheticsService; } diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts index 9ca854598e786..fbee9bc28fd8d 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/get_api_key.ts @@ -47,7 +47,7 @@ export const generateAndSaveAPIKey = async ({ }: { request?: KibanaRequest; security: SecurityPluginStart; - savedObjectsClient: SavedObjectsClientContract; + savedObjectsClient?: SavedObjectsClientContract; }) => { const isApiKeysEnabled = await security.authc.apiKeys?.areAPIKeysEnabled(); @@ -81,8 +81,10 @@ export const generateAndSaveAPIKey = async ({ if (apiKeyResult) { const { id, name, api_key: apiKey } = apiKeyResult; const apiKeyObject = { id, name, apiKey }; - // discard decoded key and rest of the keys - await setSyntheticsServiceApiKey(savedObjectsClient, apiKeyObject); + if (savedObjectsClient) { + // discard decoded key and rest of the keys + await setSyntheticsServiceApiKey(savedObjectsClient, apiKeyObject); + } return apiKeyObject; } }; diff --git a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts index cd5a72c2a1414..268d478822b71 100644 --- a/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts +++ b/x-pack/plugins/uptime/server/lib/synthetics_service/synthetics_service.ts @@ -190,13 +190,13 @@ export class SyntheticsService { async getMonitorConfigs() { const savedObjectsClient = this.server.savedObjectsClient; - const monitorsSavedObjects = await savedObjectsClient.find< + const monitorsSavedObjects = await savedObjectsClient?.find< SyntheticsMonitorSavedObject['attributes'] >({ type: syntheticsMonitorType, }); - const savedObjectsList = monitorsSavedObjects.saved_objects; + const savedObjectsList = monitorsSavedObjects?.saved_objects ?? []; return savedObjectsList.map>(({ attributes, id }) => ({ ...attributes, id, diff --git a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts index cd25e0e742625..faefb71e34f66 100644 --- a/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts +++ b/x-pack/plugins/uptime/server/rest_api/uptime_route_wrapper.ts @@ -5,12 +5,14 @@ * 2.0. */ +import { SavedObjectsClientContract } from 'kibana/server'; import { UMKibanaRouteWrapper } from './types'; import { createUptimeESClient, inspectableEsQueriesMap } from '../lib/lib'; // eslint-disable-next-line @kbn/eslint/no-restricted-paths import { KibanaResponse } from '../../../../../src/core/server/http/router'; import { enableInspectEsQueries } from '../../../observability/common'; +import { syntheticsServiceApiKey } from '../lib/saved_objects/service_api_key'; export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server) => ({ ...uptimeRoute, @@ -19,7 +21,17 @@ export const uptimeRouteWrapper: UMKibanaRouteWrapper = (uptimeRoute, server) => }, handler: async (context, request, response) => { const { client: esClient } = context.core.elasticsearch; - const { client: savedObjectsClient } = context.core.savedObjects; + let savedObjectsClient: SavedObjectsClientContract; + if (server.config?.unsafe?.service?.enabled) { + savedObjectsClient = context.core.savedObjects.getClient({ + includedHiddenTypes: [syntheticsServiceApiKey.name], + }); + } else { + savedObjectsClient = context.core.savedObjects.client; + } + + // specifically needed for the synthetics service api key generation + server.savedObjectsClient = savedObjectsClient; const isInspectorEnabled = await context.core.uiSettings.client.get( enableInspectEsQueries diff --git a/x-pack/scripts/functional_tests.js b/x-pack/scripts/functional_tests.js index f95237a76a861..a6fc2e0d0ba92 100644 --- a/x-pack/scripts/functional_tests.js +++ b/x-pack/scripts/functional_tests.js @@ -45,6 +45,7 @@ const onlyNotInCoverageTests = [ require.resolve('../test/plugin_api_integration/config.ts'), require.resolve('../test/rule_registry/security_and_spaces/config_basic.ts'), require.resolve('../test/rule_registry/security_and_spaces/config_trial.ts'), + require.resolve('../test/rule_registry/spaces_only/config_basic.ts'), require.resolve('../test/rule_registry/spaces_only/config_trial.ts'), require.resolve('../test/security_api_integration/saml.config.ts'), require.resolve('../test/security_api_integration/session_idle.config.ts'), diff --git a/x-pack/tasks/download_chromium.ts b/x-pack/tasks/download_chromium.ts index 6e1efc60f3185..51394bfb00349 100644 --- a/x-pack/tasks/download_chromium.ts +++ b/x-pack/tasks/download_chromium.ts @@ -5,14 +5,13 @@ * 2.0. */ -import { LevelLogger } from '../plugins/reporting/server/lib'; -import { ensureBrowserDownloaded } from '../plugins/reporting/server/browsers/download'; +import { download } from '../plugins/screenshotting/server/utils'; export const downloadChromium = async () => { // eslint-disable-next-line no-console const consoleLogger = (tag: string) => (message: unknown) => console.log(tag, message); - const innerLogger = { - get: () => innerLogger, + const logger = { + get: () => logger, debug: consoleLogger('debug'), info: consoleLogger('info'), warn: consoleLogger('warn'), @@ -22,6 +21,5 @@ export const downloadChromium = async () => { log: consoleLogger('log'), }; - const levelLogger = new LevelLogger(innerLogger); - await ensureBrowserDownloaded(levelLogger); + await download(logger); }; diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts index f739d9d956cb5..c364b1f054a9d 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/index.ts @@ -36,6 +36,7 @@ export default function alertingTests({ loadTestFile, getService }: FtrProviderC loadTestFile(require.resolve('./alerts_default_space')); loadTestFile(require.resolve('./builtin_alert_types')); loadTestFile(require.resolve('./transform_rule_types')); + loadTestFile(require.resolve('./ml_rule_types')); loadTestFile(require.resolve('./mustache_templates.ts')); loadTestFile(require.resolve('./notify_when')); loadTestFile(require.resolve('./ephemeral')); diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts index f1a9ef5de2bb7..3a2d8b8397127 100644 --- a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/migrations.ts @@ -346,5 +346,19 @@ export default function createGetTests({ getService }: FtrProviderContext) { FILEBEAT_7X_INDICATOR_PATH ); }); + + it('8.0 migrates incorrect action group spellings on the Metrics Inventory Threshold rule type', async () => { + const response = await es.get<{ alert: RawAlert }>( + { + index: '.kibana', + id: 'alert:92237b30-4e03-11ec-9ab9-d980518a2d28', + }, + { meta: true } + ); + expect(response.statusCode).to.eql(200); + expect(response.body._source?.alert?.actions?.[0].group).to.be( + 'metrics.inventory_threshold.fired' + ); + }); }); } diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/anomaly_detection/alert.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/anomaly_detection/alert.ts new file mode 100644 index 0000000000000..0364bd251a4c7 --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/anomaly_detection/alert.ts @@ -0,0 +1,290 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { sample } from 'lodash'; +import { duration } from 'moment'; +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; +import { + ES_TEST_INDEX_NAME, + ESTestIndexTool, + getUrlPrefix, + ObjectRemover, +} from '../../../../../common/lib'; +import { Spaces } from '../../../../scenarios'; +import { Datafeed, Job } from '../../../../../../../plugins/ml/common/types/anomaly_detection_jobs'; +import { MlAnomalyDetectionAlertParams } from '../../../../../../../plugins/ml/common/types/alerts'; +import { ANOMALY_SCORE_MATCH_GROUP_ID } from '../../../../../../../plugins/ml/server/lib/alerts/register_anomaly_detection_alert_type'; +import { ML_ALERT_TYPES } from '../../../../../../../plugins/ml/common/constants/alerts'; + +const ACTION_TYPE_ID = '.index'; +const ALERT_TYPE_ID = ML_ALERT_TYPES.ANOMALY_DETECTION; +const ES_TEST_INDEX_SOURCE = 'ml-alert:anomaly-detection'; +const ES_TEST_INDEX_REFERENCE = '-na-'; +const ES_TEST_OUTPUT_INDEX_NAME = `${ES_TEST_INDEX_NAME}-ad-alert-output`; + +const ALERT_INTERVAL_SECONDS = 3; + +const AD_JOB_ID = 'rt-anomaly-mean-value'; +const DATAFEED_ID = `datafeed-${AD_JOB_ID}`; +const BASIC_TEST_DATA_INDEX = `rt-ad-basic-data-anomalies`; +const DOC_KEYS = ['first-key', 'second-key', 'third-key']; + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +export function getAnomalyDetectionConfig(): Job { + return { + job_id: AD_JOB_ID, + description: '', + groups: ['real-time', 'anomaly-alerting'], + analysis_config: { + bucket_span: '1m', + detectors: [{ function: 'mean', field_name: 'value', partition_field_name: 'key' }], + influencers: ['key'], + }, + data_description: { time_field: '@timestamp' }, + analysis_limits: { model_memory_limit: '11MB' }, + model_plot_config: { enabled: true, annotations_enabled: true }, + } as Job; +} + +export function getDatafeedConfig(): Datafeed { + return { + indices: [BASIC_TEST_DATA_INDEX], + query: { bool: { must: [{ match_all: {} }] } }, + runtime_mappings: {}, + query_delay: '5s', + frequency: '10s', + job_id: AD_JOB_ID, + datafeed_id: DATAFEED_ID, + } as Datafeed; +} + +// eslint-disable-next-line import/no-default-export +export default function alertTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const retry = getService('retry'); + const es = getService('es'); + const log = getService('log'); + const ml = getService('ml'); + + const esTestIndexTool = new ESTestIndexTool(es, retry); + const esTestIndexToolOutput = new ESTestIndexTool(es, retry, ES_TEST_OUTPUT_INDEX_NAME); + + describe('alert', async () => { + const objectRemover = new ObjectRemover(supertest); + let actionId: string; + + beforeEach(async () => { + await esTestIndexTool.destroy(); + await esTestIndexTool.setup(); + + await esTestIndexToolOutput.destroy(); + await esTestIndexToolOutput.setup(); + + await ml.testResources.setKibanaTimeZoneToUTC(); + + actionId = await createAction(); + + // Create source index + await createSourceIndex(); + + // Ingest normal docs + await ingestNormalDocs(BASIC_TEST_DATA_INDEX); + + await ml.api.createAnomalyDetectionJob(getAnomalyDetectionConfig(), Spaces.space1.id); + await ml.api.createDatafeed(getDatafeedConfig(), Spaces.space1.id); + await ml.api.openAnomalyDetectionJob(AD_JOB_ID); + await ml.api.startDatafeed(DATAFEED_ID); + }); + + afterEach(async () => { + await objectRemover.removeAll(); + await esTestIndexTool.destroy(); + await esTestIndexToolOutput.destroy(); + await ml.api.deleteAnomalyDetectionJobES(AD_JOB_ID); + await ml.api.cleanMlIndices(); + await ml.api.deleteIndices(BASIC_TEST_DATA_INDEX); + }); + + it('runs correctly', async () => { + await createAlert({ + name: 'Test AD job', + // To make sure the alert is triggered ASAP + includeInterim: true, + jobSelection: { + jobIds: [AD_JOB_ID], + }, + severity: 0, + lookbackInterval: undefined, + resultType: 'bucket', + topNBuckets: 3, + }); + + // Ingest anomalous records + await ingestAnomalousDoc(BASIC_TEST_DATA_INDEX); + + log.debug('Wait for bucket to finalize...'); + await sleep(60000); + + log.debug('Checking created alert instances...'); + + const docs = await waitForDocs(1); + for (const doc of docs) { + const { name, message } = doc._source.params; + + expect(name).to.be('Test AD job'); + expect(message).to.be( + 'Alerts are raised based on real-time scores. Remember that scores may be adjusted over time as data continues to be analyzed.' + ); + } + }); + + async function waitForDocs(count: number): Promise { + return await esTestIndexToolOutput.waitForDocs( + ES_TEST_INDEX_SOURCE, + ES_TEST_INDEX_REFERENCE, + count + ); + } + + async function createAlert({ + name, + ...params + }: MlAnomalyDetectionAlertParams & { name: string }): Promise { + log.debug(`Creating an alerting rule "${name}"...`); + const action = { + id: actionId, + group: ANOMALY_SCORE_MATCH_GROUP_ID, + params: { + documents: [ + { + source: ES_TEST_INDEX_SOURCE, + reference: ES_TEST_INDEX_REFERENCE, + params: { + name: '{{{alertName}}}', + message: '{{{context.message}}}', + }, + }, + ], + }, + }; + + const { status, body: createdAlert } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/alerting/rule`) + .set('kbn-xsrf', 'foo') + .send({ + name, + consumer: 'alerts', + enabled: true, + rule_type_id: ALERT_TYPE_ID, + schedule: { interval: `${ALERT_INTERVAL_SECONDS}s` }, + actions: [action], + notify_when: 'onActiveAlert', + params, + }); + + expect(status).to.be(200); + + const alertId = createdAlert.id; + objectRemover.add(Spaces.space1.id, alertId, 'rule', 'alerting'); + + return alertId; + } + + async function createAction(): Promise { + log.debug('Creating an action...'); + // @ts-ignore + const { statusCode, body: createdAction } = await supertest + .post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/connector`) + .set('kbn-xsrf', 'foo') + .send({ + name: 'index action for anomaly detection FT', + connector_type_id: ACTION_TYPE_ID, + config: { + index: ES_TEST_OUTPUT_INDEX_NAME, + }, + secrets: {}, + }); + + expect(statusCode).to.be(200); + + log.debug(`Action with id "${createdAction.id}" has been created.`); + + const resultId = createdAction.id; + objectRemover.add(Spaces.space1.id, resultId, 'connector', 'actions'); + + return resultId; + } + + async function createSourceIndex() { + log.debug('Creating the source index...'); + await ml.api.createIndex(BASIC_TEST_DATA_INDEX, { + properties: { + '@timestamp': { type: 'date' }, + value: { type: 'integer' }, + key: { type: 'keyword' }, + }, + }); + } + + async function ingestNormalDocs( + indexName: string, + hoursAgo: number = 24, + hoursFromNow: number = 4, + secondsBetweenDocs: number = 30 + ) { + log.debug(`Ingesting baseline documents into ${indexName}...`); + const timestamp = Date.now(); + const start = timestamp - duration(hoursAgo, 'h').asMilliseconds(); + const end = timestamp - duration(hoursFromNow, 'h').asMilliseconds(); + + log.debug( + `> from ${start} until ${end} with one document every ${secondsBetweenDocs} seconds` + ); + + const step = duration(secondsBetweenDocs, 's').asMilliseconds(); + + let docTime = start; + const docs: Array<{ _index: string; '@timestamp': number; value: number; key: string }> = []; + while (docTime + step < end) { + for (const key of DOC_KEYS) { + docs.push({ + _index: indexName, + '@timestamp': docTime, + value: Math.floor(Math.random() * 10 + 1), + key, + }); + } + docTime += step; + } + + const body = docs.flatMap(({ _index, ...doc }) => { + return [{ index: { _index } }, doc]; + }); + + await es.bulk({ + refresh: 'wait_for', + body, + }); + + log.debug('> docs ingested.'); + } + + async function ingestAnomalousDoc(indexName: string) { + log.debug('Ingesting anomalous doc...'); + await es.index({ + refresh: 'wait_for', + index: indexName, + body: { '@timestamp': Date.now(), value: 10 * 1000, key: sample(DOC_KEYS) }, + }); + log.debug('Anomalous doc indexed successfully...'); + } + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/anomaly_detection/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/anomaly_detection/index.ts new file mode 100644 index 0000000000000..f2875c62c67cd --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/anomaly_detection/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile }: FtrProviderContext) { + describe('Anomaly detection', function () { + loadTestFile(require.resolve('./alert')); + }); +} diff --git a/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/index.ts b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/index.ts new file mode 100644 index 0000000000000..1b7a2ea1842ee --- /dev/null +++ b/x-pack/test/alerting_api_integration/spaces_only/tests/alerting/ml_rule_types/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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; + +// eslint-disable-next-line import/no-default-export +export default function alertingTests({ loadTestFile }: FtrProviderContext) { + describe('machine learning alert rule types', function () { + loadTestFile(require.resolve('./anomaly_detection')); + }); +} diff --git a/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts index 4467afb7585ad..ecefef2fe930c 100644 --- a/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts +++ b/x-pack/test/api_integration/apis/metrics_ui/metric_threshold_alert.ts @@ -10,9 +10,9 @@ import { convertToKibanaClient } from '@kbn/test'; import { InfraSource } from '../../../../plugins/infra/common/source_configuration/source_configuration'; import { FtrProviderContext } from '../../ftr_provider_context'; import { - evaluateAlert, - EvaluatedAlertParams, -} from '../../../../plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_alert'; + evaluateRule, + EvaluatedRuleParams, +} from '../../../../plugins/infra/server/lib/alerting/metric_threshold/lib/evaluate_rule'; import { Aggregators, CountMetricExpressionParams, @@ -27,7 +27,7 @@ export default function ({ getService }: FtrProviderContext) { const esArchiver = getService('esArchiver'); const esClient = getService('es'); - const baseParams: EvaluatedAlertParams = { + const baseParams: EvaluatedRuleParams = { groupBy: void 0, filterQuery: void 0, criteria: [ @@ -100,7 +100,7 @@ export default function ({ getService }: FtrProviderContext) { }; const timeFrame = { end: DATES.ten_thousand_plus.max }; const kbnClient = convertToKibanaClient(esClient); - const results = await evaluateAlert(kbnClient, params, config, [], timeFrame); + const results = await evaluateRule(kbnClient, params, config, [], timeFrame); expect(results).to.eql([ { '*': { @@ -142,7 +142,7 @@ export default function ({ getService }: FtrProviderContext) { }; const timeFrame = { end: DATES.ten_thousand_plus.max }; const kbnClient = convertToKibanaClient(esClient); - const results = await evaluateAlert(kbnClient, params, config, [], timeFrame); + const results = await evaluateRule(kbnClient, params, config, [], timeFrame); expect(results).to.eql([ { web: { @@ -184,7 +184,7 @@ export default function ({ getService }: FtrProviderContext) { }; const timeFrame = { end: gauge.max }; const kbnClient = convertToKibanaClient(esClient); - const results = await evaluateAlert(kbnClient, params, configuration, [], timeFrame); + const results = await evaluateRule(kbnClient, params, configuration, [], timeFrame); expect(results).to.eql([ { '*': { @@ -208,7 +208,7 @@ export default function ({ getService }: FtrProviderContext) { const params = { ...baseParams }; const timeFrame = { end: gauge.max }; const kbnClient = convertToKibanaClient(esClient); - const results = await evaluateAlert(kbnClient, params, configuration, [], timeFrame); + const results = await evaluateRule(kbnClient, params, configuration, [], timeFrame); expect(results).to.eql([ { '*': { @@ -246,7 +246,7 @@ export default function ({ getService }: FtrProviderContext) { }; const timeFrame = { end: gauge.max }; const kbnClient = convertToKibanaClient(esClient); - const results = await evaluateAlert(kbnClient, params, configuration, [], timeFrame); + const results = await evaluateRule(kbnClient, params, configuration, [], timeFrame); expect(results).to.eql([ { dev: { @@ -287,7 +287,7 @@ export default function ({ getService }: FtrProviderContext) { }; const timeFrame = { end: gauge.max }; const kbnClient = convertToKibanaClient(esClient); - const results = await evaluateAlert(kbnClient, params, configuration, [], timeFrame); + const results = await evaluateRule(kbnClient, params, configuration, [], timeFrame); expect(results).to.eql([ { dev: { @@ -329,7 +329,7 @@ export default function ({ getService }: FtrProviderContext) { }; const timeFrame = { end: gauge.midpoint }; const kbnClient = convertToKibanaClient(esClient); - const results = await evaluateAlert( + const results = await evaluateRule( kbnClient, params, configuration, @@ -392,7 +392,7 @@ export default function ({ getService }: FtrProviderContext) { }; const timeFrame = { end: rate.max }; const kbnClient = convertToKibanaClient(esClient); - const results = await evaluateAlert(kbnClient, params, configuration, [], timeFrame); + const results = await evaluateRule(kbnClient, params, configuration, [], timeFrame); expect(results).to.eql([ { '*': { @@ -433,7 +433,7 @@ export default function ({ getService }: FtrProviderContext) { }; const timeFrame = { end: rate.max }; const kbnClient = convertToKibanaClient(esClient); - const results = await evaluateAlert(kbnClient, params, configuration, [], timeFrame); + const results = await evaluateRule(kbnClient, params, configuration, [], timeFrame); expect(results).to.eql([ { dev: { diff --git a/x-pack/test/api_integration/apis/observability/index.ts b/x-pack/test/api_integration/apis/observability/index.ts deleted file mode 100644 index 5136ff9b3d468..0000000000000 --- a/x-pack/test/api_integration/apis/observability/index.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { FtrProviderContext } from '../../ftr_provider_context'; - -export default function observabilityApiIntegrationTests({ loadTestFile }: FtrProviderContext) { - describe('Observability specs', () => { - loadTestFile(require.resolve('./annotations')); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.spec.ts b/x-pack/test/apm_api_integration/tests/alerts/rule_registry.spec.ts deleted file mode 100644 index efa8aa3ace9dc..0000000000000 --- a/x-pack/test/apm_api_integration/tests/alerts/rule_registry.spec.ts +++ /dev/null @@ -1,576 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; -import { - ALERT_DURATION, - ALERT_END, - ALERT_RULE_UUID, - ALERT_START, - ALERT_STATUS, - ALERT_UUID, - EVENT_KIND, - VERSION, -} from '@kbn/rule-data-utils'; -import { merge, omit } from 'lodash'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; - -interface Alert { - schedule: { - interval: string; - }; - updatedAt: string; - executionStatus: { - lastExecutionDate: string; - status: string; - }; - updatedBy: string; - id: string; - params: Record; - scheduledTaskId: string; -} - -export default function ApiTest({ getService }: FtrProviderContext) { - const registry = getService('registry'); - const supertest = getService('legacySupertestAsApmWriteUser'); - const es = getService('es'); - - const MAX_POLLS = 10; - const BULK_INDEX_DELAY = 1000; - const INDEXING_DELAY = 5000; - - const getAlertsTargetIndicesUrl = - '/api/observability/rules/alerts/dynamic_index_pattern?namespace=default®istrationContexts=observability.apm®istrationContexts='; - - const getAlertsTargetIndices = async () => - supertest.get(getAlertsTargetIndicesUrl).send().set('kbn-xsrf', 'foo'); - const APM_METRIC_INDEX_NAME = 'apm-8.0.0-transaction'; - - const createTransactionMetric = (override: Record) => { - const now = Date.now(); - - const time = now - INDEXING_DELAY; - - return merge( - { - '@timestamp': new Date(time).toISOString(), - service: { - name: 'opbeans-go', - }, - event: { - outcome: 'success', - }, - transaction: { - duration: { - histogram: { - values: [1000000], - counts: [1], - }, - }, - type: 'request', - }, - processor: { - event: 'metric', - }, - observer: { - version_major: 7, - }, - }, - override - ); - }; - - async function waitUntilNextExecution( - alert: Alert, - intervalInSeconds: number = 1, - count: number = 0 - ): Promise { - await new Promise((resolve) => { - setTimeout(resolve, intervalInSeconds * 1000); - }); - - const { body, status } = await supertest - .get(`/api/alerts/alert/${alert.id}`) - .set('kbn-xsrf', 'foo'); - - const { body: targetIndices, status: targetIndicesStatus } = await getAlertsTargetIndices(); - if (targetIndices.length === 0) { - const error = new Error('Error getting alert'); - Object.assign(error, { response: { body: targetIndices, status: targetIndicesStatus } }); - throw error; - } - - if (status >= 300) { - const error = new Error('Error getting alert'); - Object.assign(error, { response: { body, status } }); - throw error; - } - - const nextAlert = body as Alert; - - if (nextAlert.executionStatus.lastExecutionDate !== alert.executionStatus.lastExecutionDate) { - await new Promise((resolve) => { - setTimeout(resolve, BULK_INDEX_DELAY); - }); - - /** - * When calling refresh on an index pattern .alerts-observability.apm.alerts* (as was originally the hard-coded string in this test) - * The response from Elasticsearch is a 200, even if no indices which match that index pattern have been created. - * When calling refresh on a concrete index alias .alerts-observability.apm.alerts-default for instance, - * we receive a 404 error index_not_found_exception when no indices have been created which match that alias (obviously). - * Since we are receiving a concrete index alias from the observability api instead of a kibana index pattern - * and we understand / expect that this index does not exist at certain points of the test, we can try-catch at certain points without caring if the call fails. - * There are points in the code where we do want to ensure we get the appropriate error message back - */ - try { - await es.indices.refresh({ - index: targetIndices[0], - }); - // eslint-disable-next-line no-empty - } catch (exc) {} - return nextAlert; - } - - if (count >= MAX_POLLS) { - throw new Error('Maximum number of polls exceeded'); - } - - return waitUntilNextExecution(alert, intervalInSeconds, count + 1); - } - - registry.when('Rule registry with write enabled', { config: 'rules', archives: [] }, () => { - it('does not bootstrap indices on plugin startup', async () => { - const { body: targetIndices } = await getAlertsTargetIndices(); - try { - const res = await es.indices.get({ - index: targetIndices[0], - expand_wildcards: 'open', - allow_no_indices: true, - }); - expect(res).to.be.empty(); - } catch (exc) { - expect(exc.statusCode).to.eql(404); - } - }); - - describe('when creating a rule', () => { - let createResponse: { - alert: Alert; - status: number; - }; - - before(async () => { - await es.indices.create({ - index: APM_METRIC_INDEX_NAME, - body: { - mappings: { - dynamic: 'strict', - properties: { - event: { - properties: { - outcome: { - type: 'keyword', - }, - }, - }, - processor: { - properties: { - event: { - type: 'keyword', - }, - }, - }, - observer: { - properties: { - version_major: { - type: 'byte', - }, - }, - }, - service: { - properties: { - name: { - type: 'keyword', - }, - environment: { - type: 'keyword', - }, - }, - }, - transaction: { - properties: { - type: { - type: 'keyword', - }, - duration: { - properties: { - histogram: { - type: 'histogram', - }, - }, - }, - }, - }, - '@timestamp': { - type: 'date', - }, - }, - }, - }, - }); - - const body = { - params: { - threshold: 30, - windowSize: 5, - windowUnit: 'm', - transactionType: 'request', - environment: 'ENVIRONMENT_ALL', - serviceName: 'opbeans-go', - }, - consumer: 'apm', - alertTypeId: 'apm.transaction_error_rate', - schedule: { interval: '5s' }, - actions: [], - tags: ['apm', 'service.name:opbeans-go'], - notifyWhen: 'onActionGroupChange', - name: 'Failed transaction rate threshold | opbeans-go', - }; - - const { body: response, status } = await supertest - .post('/api/alerts/alert') - .send(body) - .set('kbn-xsrf', 'foo'); - - createResponse = { - alert: response, - status, - }; - }); - - after(async () => { - const { body: targetIndices } = await getAlertsTargetIndices(); - if (createResponse.alert) { - const { body, status } = await supertest - .delete(`/api/alerts/alert/${createResponse.alert.id}`) - .set('kbn-xsrf', 'foo'); - - if (status >= 300) { - const error = new Error('Error deleting alert'); - Object.assign(error, { response: { body, status } }); - throw error; - } - } - - await es.deleteByQuery({ - index: targetIndices[0], - body: { - query: { - match_all: {}, - }, - }, - refresh: true, - }); - - await es.indices.delete({ - index: APM_METRIC_INDEX_NAME, - }); - }); - - it('writes alerts data to the alert indices', async () => { - expect(createResponse.status).to.be.below(299); - - expect(createResponse.alert).not.to.be(undefined); - let alert = await waitUntilNextExecution(createResponse.alert); - - const { body: targetIndices } = await getAlertsTargetIndices(); - - try { - const res = await es.search({ - index: targetIndices[0], - body: { - query: { - term: { - [EVENT_KIND]: 'signal', - }, - }, - size: 1, - sort: { - '@timestamp': 'desc', - }, - }, - }); - expect(res).to.be.empty(); - } catch (exc) { - expect(exc.message).contain('index_not_found_exception'); - } - - await es.index({ - index: APM_METRIC_INDEX_NAME, - body: createTransactionMetric({ - event: { - outcome: 'success', - }, - }), - refresh: true, - }); - - alert = await waitUntilNextExecution(alert); - - try { - const res = await es.search({ - index: targetIndices[0], - body: { - query: { - term: { - [EVENT_KIND]: 'signal', - }, - }, - size: 1, - sort: { - '@timestamp': 'desc', - }, - }, - }); - expect(res).to.be.empty(); - } catch (exc) { - expect(exc.message).contain('index_not_found_exception'); - } - - await es.index({ - index: APM_METRIC_INDEX_NAME, - body: createTransactionMetric({ - event: { - outcome: 'failure', - }, - }), - refresh: true, - }); - - alert = await waitUntilNextExecution(alert); - - const afterViolatingDataResponse = await es.search({ - index: targetIndices[0], - body: { - query: { - term: { - [EVENT_KIND]: 'signal', - }, - }, - size: 1, - sort: { - '@timestamp': 'desc', - }, - _source: false, - fields: [{ field: '*', include_unmapped: true }], - }, - }); - - expect(afterViolatingDataResponse.hits.hits.length).to.be(1); - - const alertEvent = afterViolatingDataResponse.hits.hits[0].fields as Record; - - const exclude = ['@timestamp', ALERT_START, ALERT_UUID, ALERT_RULE_UUID, VERSION]; - - const toCompare = omit(alertEvent, exclude); - - expectSnapshot(toCompare).toMatchInline(` - Object { - "event.action": Array [ - "open", - ], - "event.kind": Array [ - "signal", - ], - "kibana.alert.duration.us": Array [ - 0, - ], - "kibana.alert.evaluation.threshold": Array [ - 30, - ], - "kibana.alert.evaluation.value": Array [ - 50, - ], - "kibana.alert.instance.id": Array [ - "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", - ], - "kibana.alert.reason": Array [ - "Failed transactions rate is greater than 30% (current value is 50%) for opbeans-go", - ], - "kibana.alert.rule.category": Array [ - "Failed transaction rate threshold", - ], - "kibana.alert.rule.consumer": Array [ - "apm", - ], - "kibana.alert.rule.name": Array [ - "Failed transaction rate threshold | opbeans-go", - ], - "kibana.alert.rule.producer": Array [ - "apm", - ], - "kibana.alert.rule.rule_type_id": Array [ - "apm.transaction_error_rate", - ], - "kibana.alert.status": Array [ - "active", - ], - "kibana.alert.workflow_status": Array [ - "open", - ], - "kibana.space_ids": Array [ - "default", - ], - "processor.event": Array [ - "transaction", - ], - "service.name": Array [ - "opbeans-go", - ], - "tags": Array [ - "apm", - "service.name:opbeans-go", - ], - "transaction.type": Array [ - "request", - ], - } - `); - - await es.bulk({ - index: APM_METRIC_INDEX_NAME, - body: [ - { index: {} }, - createTransactionMetric({ - event: { - outcome: 'success', - }, - }), - { index: {} }, - createTransactionMetric({ - event: { - outcome: 'success', - }, - }), - ], - refresh: true, - }); - - alert = await waitUntilNextExecution(alert); - - const afterRecoveryResponse = await es.search({ - index: targetIndices[0], - body: { - query: { - term: { - [EVENT_KIND]: 'signal', - }, - }, - size: 1, - sort: { - '@timestamp': 'desc', - }, - _source: false, - fields: [{ field: '*', include_unmapped: true }], - }, - }); - - expect(afterRecoveryResponse.hits.hits.length).to.be(1); - - const recoveredAlertEvent = afterRecoveryResponse.hits.hits[0].fields as Record< - string, - any - >; - - expect(recoveredAlertEvent[ALERT_STATUS]?.[0]).to.eql('recovered'); - expect(recoveredAlertEvent[ALERT_DURATION]?.[0]).to.be.greaterThan(0); - expect(new Date(recoveredAlertEvent[ALERT_END]?.[0]).getTime()).to.be.greaterThan(0); - - expectSnapshot(omit(recoveredAlertEvent, exclude.concat([ALERT_DURATION, ALERT_END]))) - .toMatchInline(` - Object { - "event.action": Array [ - "close", - ], - "event.kind": Array [ - "signal", - ], - "kibana.alert.evaluation.threshold": Array [ - 30, - ], - "kibana.alert.evaluation.value": Array [ - 50, - ], - "kibana.alert.instance.id": Array [ - "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", - ], - "kibana.alert.reason": Array [ - "Failed transactions rate is greater than 30% (current value is 50%) for opbeans-go", - ], - "kibana.alert.rule.category": Array [ - "Failed transaction rate threshold", - ], - "kibana.alert.rule.consumer": Array [ - "apm", - ], - "kibana.alert.rule.name": Array [ - "Failed transaction rate threshold | opbeans-go", - ], - "kibana.alert.rule.producer": Array [ - "apm", - ], - "kibana.alert.rule.rule_type_id": Array [ - "apm.transaction_error_rate", - ], - "kibana.alert.status": Array [ - "recovered", - ], - "kibana.alert.workflow_status": Array [ - "open", - ], - "kibana.space_ids": Array [ - "default", - ], - "processor.event": Array [ - "transaction", - ], - "service.name": Array [ - "opbeans-go", - ], - "tags": Array [ - "apm", - "service.name:opbeans-go", - ], - "transaction.type": Array [ - "request", - ], - } - `); - }); - }); - }); - - registry.when('Rule registry with write not enabled', { config: 'basic', archives: [] }, () => { - it('does not bootstrap the apm rule indices', async () => { - const { body: targetIndices } = await getAlertsTargetIndices(); - const errorOrUndefined = await es.indices - .get({ - index: targetIndices[0], - expand_wildcards: 'open', - allow_no_indices: false, - }) - .then(() => {}) - .catch((error) => { - return error.toString(); - }); - - expect(errorOrUndefined).not.to.be(undefined); - - expect(errorOrUndefined).to.contain('index_not_found_exception'); - }); - }); -} diff --git a/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/update_to_v3.spec.ts b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/update_to_v3.spec.ts new file mode 100644 index 0000000000000..0a2ea7fd6efe0 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/settings/anomaly_detection/update_to_v3.spec.ts @@ -0,0 +1,119 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { FtrProviderContext } from '../../../common/ftr_provider_context'; + +export default function apiTest({ getService }: FtrProviderContext) { + const registry = getService('registry'); + const apmApiClient = getService('apmApiClient'); + const ml = getService('ml'); + + const es = getService('es'); + + function getJobs() { + return apmApiClient.writeUser({ + endpoint: `GET /internal/apm/settings/anomaly-detection/jobs`, + }); + } + + function createJobs(environments: string[]) { + return apmApiClient.writeUser({ + endpoint: `POST /internal/apm/settings/anomaly-detection/jobs`, + params: { + body: { environments }, + }, + }); + } + + async function createV2Jobs(environments: string[]) { + await createJobs(environments); + + const { body } = await getJobs(); + + for (const mlJob of body.jobs) { + await es.ml.updateJob({ + job_id: mlJob.jobId, + custom_settings: { + job_tags: { + apm_ml_version: '2', + environment: mlJob.environment, + }, + }, + }); + } + } + + async function createV3Jobs(environments: string[]) { + await createJobs(environments); + } + + function callUpdateEndpoint() { + return apmApiClient.writeUser({ + endpoint: 'POST /internal/apm/settings/anomaly-detection/update_to_v3', + }); + } + + registry.when('Updating ML jobs to v3', { config: 'trial', archives: [] }, () => { + describe('when there are no v2 jobs', () => { + it('returns a 200/true', async () => { + const { status, body } = await callUpdateEndpoint(); + expect(status).to.eql(200); + expect(body.update).to.eql(true); + }); + }); + + describe('when there are only v2 jobs', () => { + before(async () => { + await createV2Jobs(['production', 'development']); + }); + it('creates a new job for each environment that has a v2 job', async () => { + await callUpdateEndpoint(); + + const { + body: { jobs }, + } = await getJobs(); + + expect( + jobs + .filter((job) => job.version === 3) + .map((job) => job.environment) + .sort() + ).to.eql(['development', 'production']); + }); + + after(() => ml.cleanMlIndices()); + }); + + describe('when there are both v2 and v3 jobs', () => { + before(async () => { + await createV2Jobs(['production', 'development']); + }); + + before(async () => { + await createV3Jobs(['production']); + }); + + after(() => ml.cleanMlIndices()); + + it('only creates new jobs for environments that did not have a v3 job', async () => { + await callUpdateEndpoint(); + + const { + body: { jobs }, + } = await getJobs(); + + expect( + jobs + .filter((job) => job.version === 3) + .map((job) => job.environment) + .sort() + ).to.eql(['development', 'production']); + }); + }); + }); +} diff --git a/x-pack/test/cases_api_integration/common/fixtures/plugins/cases_client_user/server/plugin.ts b/x-pack/test/cases_api_integration/common/fixtures/plugins/cases_client_user/server/plugin.ts index d26da34579dff..8bb361fba4440 100644 --- a/x-pack/test/cases_api_integration/common/fixtures/plugins/cases_client_user/server/plugin.ts +++ b/x-pack/test/cases_api_integration/common/fixtures/plugins/cases_client_user/server/plugin.ts @@ -12,7 +12,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../.. import { SpacesPluginStart } from '../../../../../../../plugins/spaces/server'; import { SecurityPluginStart } from '../../../../../../../plugins/security/server'; import { PluginStartContract as CasesPluginStart } from '../../../../../../../plugins/cases/server'; -import { CasesPatchRequest } from '../../../../../../../plugins/cases/common'; +import { CasesPatchRequest } from '../../../../../../../plugins/cases/common/api'; export interface FixtureSetupDeps { features: FeaturesPluginSetup; diff --git a/x-pack/test/cases_api_integration/common/lib/validation.ts b/x-pack/test/cases_api_integration/common/lib/validation.ts index 8b1c8ca124149..3dcb15c14645d 100644 --- a/x-pack/test/cases_api_integration/common/lib/validation.ts +++ b/x-pack/test/cases_api_integration/common/lib/validation.ts @@ -7,7 +7,7 @@ import expect from '@kbn/expect'; -import { CaseResponse, CasesByAlertId } from '../../../../plugins/cases/common'; +import { CaseResponse, CasesByAlertId } from '../../../../plugins/cases/common/api'; /** * Ensure that the result of the alerts API request matches with the cases created for the test. diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts index 136e52d08f46a..f1c16e9b6dc56 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/alerts/get_cases.ts @@ -17,7 +17,7 @@ import { deleteAllCaseItems, } from '../../../../common/lib/utils'; import { validateCasesFromAlertIDResponse } from '../../../../common/lib/validation'; -import { CaseResponse } from '../../../../../../plugins/cases/common'; +import { CaseResponse } from '../../../../../../plugins/cases/common/api'; import { globalRead, noKibanaPrivileges, diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts index dd1c2e810f150..1377bbdabf2e0 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/import_export.ts @@ -19,19 +19,21 @@ import { } from '../../../../common/lib/utils'; import { getPostCaseRequest, postCommentUserReq } from '../../../../common/lib/mock'; import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + CASES_URL, + CASE_SAVED_OBJECT, + CASE_USER_ACTION_SAVED_OBJECT, + CASE_COMMENT_SAVED_OBJECT, +} from '../../../../../../plugins/cases/common/constants'; import { AttributesTypeUser, CommentsResponse, - CASES_URL, CaseType, - CASE_SAVED_OBJECT, CaseAttributes, - CASE_USER_ACTION_SAVED_OBJECT, CaseUserActionAttributes, - CASE_COMMENT_SAVED_OBJECT, CasePostRequest, CaseUserActionResponse, -} from '../../../../../../plugins/cases/common'; +} from '../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts index a489b403354b7..4d42609f57d49 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/cases/migrations.ts @@ -13,7 +13,7 @@ import { } from '../../../../../../plugins/cases/common/constants'; import { getCase, getCaseSavedObjectsFromES, resolveCase } from '../../../../common/lib/utils'; import { superUser } from '../../../../common/lib/authentication/users'; -import { AttributesTypeUser } from '../../../../../../plugins/cases/common'; +import { AttributesTypeUser } from '../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/client/update_alert_status.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/client/update_alert_status.ts index d2949c9728989..006deaad27f56 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/client/update_alert_status.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/client/update_alert_status.ts @@ -15,7 +15,11 @@ import { deleteAllCaseItems, getSignalsWithES, } from '../../../../common/lib/utils'; -import { CasesResponse, CaseStatuses, CommentType } from '../../../../../../plugins/cases/common'; +import { + CasesResponse, + CaseStatuses, + CommentType, +} from '../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default ({ getService }: FtrProviderContext): void => { diff --git a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts index f9e66880c5230..2dc4f740a6819 100644 --- a/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts +++ b/x-pack/test/cases_api_integration/security_and_spaces/tests/common/user_actions/migrations.ts @@ -15,7 +15,7 @@ import { getCaseUserActions } from '../../../../common/lib/utils'; import { CaseUserActionResponse, CaseUserActionsResponse, -} from '../../../../../../plugins/cases/common'; +} from '../../../../../../plugins/cases/common/api'; // eslint-disable-next-line import/no-default-export export default function createGetTests({ getService }: FtrProviderContext) { diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts b/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts deleted file mode 100644 index 82a793619483e..0000000000000 --- a/x-pack/test/detection_engine_api_integration/basic/tests/find_statuses.ts +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; - -import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - createSignalsIndex, - deleteAllAlerts, - deleteSignalsIndex, - deleteAllRulesStatuses, - getSimpleRule, - createRule, - waitForRuleSuccessOrStatus, -} from '../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const esArchiver = getService('esArchiver'); - const es = getService('es'); - const log = getService('log'); - - describe('find_statuses', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - beforeEach(async () => { - await createSignalsIndex(supertest, log); - }); - - afterEach(async () => { - await deleteSignalsIndex(supertest, log); - await deleteAllAlerts(supertest, log); - await deleteAllRulesStatuses(es, log); - }); - - it('should return an empty find statuses body correctly if no statuses are loaded', async () => { - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) - .set('kbn-xsrf', 'true') - .send({ ids: [] }) - .expect(200); - - expect(body).to.eql({}); - }); - - it('should return a single rule status when a single rule is loaded from a find status with defaults added', async () => { - const resBody = await createRule(supertest, log, getSimpleRule('rule-1', true)); - - await waitForRuleSuccessOrStatus(supertest, log, resBody.id); - - // query the single rule from _find - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) - .set('kbn-xsrf', 'true') - .send({ ids: [resBody.id] }) - .expect(200); - - // expected result for status should be 'going to run' or 'succeeded - expect(['succeeded', 'going to run']).to.contain(body[resBody.id].current_status.status); - }); - }); -}; diff --git a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts index 5fa4540bbe854..1a5ea8de935b4 100644 --- a/x-pack/test/detection_engine_api_integration/basic/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/basic/tests/index.ts @@ -19,7 +19,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./delete_rules_bulk')); loadTestFile(require.resolve('./export_rules')); loadTestFile(require.resolve('./find_rules')); - loadTestFile(require.resolve('./find_statuses')); loadTestFile(require.resolve('./get_prepackaged_rules_status')); loadTestFile(require.resolve('./import_rules')); loadTestFile(require.resolve('./read_rules')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts index cf29875839060..5506febb781f6 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/add_actions.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, @@ -76,14 +75,6 @@ export default ({ getService }: FtrProviderContext) => { getRuleWithWebHookAction(hookAction.id, true) ); await waitForRuleSuccessOrStatus(supertest, log, rule.id); - - // expected result for status should be 'succeeded' - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) - .set('kbn-xsrf', 'true') - .send({ ids: [rule.id] }) - .expect(200); - expect(body[rule.id].current_status.status).to.eql('succeeded'); }); it('should be able to create a new webhook action and attach it to a rule with a meta field and run it correctly', async () => { @@ -102,14 +93,6 @@ export default ({ getService }: FtrProviderContext) => { const rule = await createRule(supertest, log, ruleWithAction); await waitForRuleSuccessOrStatus(supertest, log, rule.id); - - // expected result for status should be 'succeeded' - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) - .set('kbn-xsrf', 'true') - .send({ ids: [rule.id] }) - .expect(200); - expect(body[rule.id].current_status.status).to.eql('succeeded'); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/check_privileges.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/check_privileges.ts index 4e7cccd85d828..6d2610dfce186 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/check_privileges.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/check_privileges.ts @@ -66,11 +66,11 @@ export default ({ getService }: FtrProviderContext) => { }); await waitForRuleSuccessOrStatus(supertest, log, id, 'partial failure'); const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .get(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send({ ids: [id] }) + .query({ id }) .expect(200); - expect(body[id]?.current_status?.last_success_message).to.eql( + expect(body?.last_success_message).to.eql( `This rule may not have the required read privileges to the following indices/index patterns: ["${index[0]}"]` ); @@ -92,11 +92,11 @@ export default ({ getService }: FtrProviderContext) => { }); await waitForRuleSuccessOrStatus(supertest, log, id, 'partial failure'); const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .get(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send({ ids: [id] }) + .query({ id }) .expect(200); - expect(body[id]?.current_status?.last_success_message).to.eql( + expect(body?.last_success_message).to.eql( `This rule may not have the required read privileges to the following indices/index patterns: ["${index[0]}"]` ); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts index d7bba5fa5dbe5..4a572f94b959d 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules.ts @@ -8,10 +8,7 @@ import expect from '@kbn/expect'; import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; -import { - DETECTION_ENGINE_RULES_URL, - DETECTION_ENGINE_RULES_STATUS_URL, -} from '../../../../plugins/security_solution/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, @@ -33,7 +30,6 @@ import { } from '../../utils'; import { ROLES } from '../../../../plugins/security_solution/common/test'; import { createUserAndRole, deleteUserAndRole } from '../../../common/services/security_solution'; -import { RuleStatusResponse } from '../../../../plugins/security_solution/server/lib/detection_engine/rules/types'; function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); @@ -105,14 +101,6 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); await waitForRuleSuccessOrStatus(supertest, log, body.id); - - const { body: statusBody } = await supertest - .post(DETECTION_ENGINE_RULES_STATUS_URL) - .set('kbn-xsrf', 'true') - .send({ ids: [body.id] }) - .expect(200); - - expect(statusBody[body.id].current_status.status).to.eql('succeeded'); }); // TODO: does the below test work? @@ -126,14 +114,14 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccessOrStatus(supertest, log, body.id, 'partial failure'); - const { body: statusBody } = await supertest - .post(DETECTION_ENGINE_RULES_STATUS_URL) + const { body: rule } = await supertest + .get(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send({ ids: [body.id] }) + .query({ id: body.id }) .expect(200); - expect(statusBody[body.id].current_status.status).to.eql('partial failure'); - expect(statusBody[body.id].current_status.last_success_message).to.eql( + expect(rule.status).to.eql('partial failure'); + expect(rule.last_success_message).to.eql( 'This rule is attempting to query data from Elasticsearch indices listed in the "Index pattern" section of the rule definition, however no index matching: ["does-not-exist-*"] was found. This warning will continue to appear until a matching index is created or this rule is de-activated.' ); }); @@ -147,14 +135,6 @@ export default ({ getService }: FtrProviderContext) => { .expect(200); await waitForRuleSuccessOrStatus(supertest, log, body.id, 'succeeded'); - - const { body: statusBody } = await supertest - .post(DETECTION_ENGINE_RULES_STATUS_URL) - .set('kbn-xsrf', 'true') - .send({ ids: [body.id] }) - .expect(200); - - expect(statusBody[body.id].current_status.status).to.eql('succeeded'); }); it('should create a single rule without an input index', async () => { @@ -321,18 +301,14 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccessOrStatus(supertest, log, bodyId, 'partial failure'); await sleep(5000); - const { body: statusBody } = await supertest - .post(DETECTION_ENGINE_RULES_STATUS_URL) + const { body: rule } = await supertest + .get(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send({ ids: [bodyId] }) + .query({ id: bodyId }) .expect(200); - expect((statusBody as RuleStatusResponse)[bodyId].current_status?.status).to.eql( - 'partial failure' - ); - expect( - (statusBody as RuleStatusResponse)[bodyId].current_status?.last_success_message - ).to.eql( + expect(rule?.status).to.eql('partial failure'); + expect(rule?.last_success_message).to.eql( 'The following indices are missing the timestamp override field "event.ingested": ["myfakeindex-1"]' ); }); @@ -353,13 +329,13 @@ export default ({ getService }: FtrProviderContext) => { await sleep(5000); await waitForSignalsToBePresent(supertest, log, 2, [bodyId]); - const { body: statusBody } = await supertest - .post(DETECTION_ENGINE_RULES_STATUS_URL) + const { body: rule } = await supertest + .get(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send({ ids: [bodyId] }) + .query({ id: bodyId }) .expect(200); - expect(statusBody[bodyId].current_status.status).to.eql('partial failure'); + expect(rule.status).to.eql('partial failure'); }); }); }); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts index e12f9b7fe7825..06bc7139d5ac2 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_rules_bulk.ts @@ -7,10 +7,7 @@ import expect from '@kbn/expect'; -import { - DETECTION_ENGINE_RULES_URL, - DETECTION_ENGINE_RULES_STATUS_URL, -} from '../../../../plugins/security_solution/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, @@ -90,14 +87,6 @@ export default ({ getService }: FtrProviderContext): void => { .expect(200); await waitForRuleSuccessOrStatus(supertest, log, body[0].id); - - const { body: statusBody } = await supertest - .post(DETECTION_ENGINE_RULES_STATUS_URL) - .set('kbn-xsrf', 'true') - .send({ ids: [body[0].id] }) - .expect(200); - - expect(statusBody[body[0].id].current_status.status).to.eql('succeeded'); }); it('should create a single rule without a rule_id', async () => { diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts index 3d09599278809..c306c207fc829 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/create_threat_matching.ts @@ -22,10 +22,7 @@ import { import { flattenWithPrefix } from '@kbn/securitysolution-rules'; import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; -import { - DETECTION_ENGINE_RULES_STATUS_URL, - DETECTION_ENGINE_RULES_URL, -} from '../../../../plugins/security_solution/common/constants'; +import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createRule, @@ -110,15 +107,15 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccessOrStatus(supertest, log, ruleResponse.id, 'succeeded'); - const { body: statusBody } = await supertest - .post(DETECTION_ENGINE_RULES_STATUS_URL) + const { body: rule } = await supertest + .get(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send({ ids: [ruleResponse.id] }) + .query({ id: ruleResponse.id }) .expect(200); const bodyToCompare = removeServerGeneratedProperties(ruleResponse); expect(bodyToCompare).to.eql(getThreatMatchingSchemaPartialMock(true)); - expect(statusBody[ruleResponse.id].current_status.status).to.eql('succeeded'); + expect(rule.status).to.eql('succeeded'); }); }); @@ -495,11 +492,11 @@ export default ({ getService }: FtrProviderContext) => { await waitForRuleSuccessOrStatus(supertest, log, id, 'failed'); const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .post(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send({ ids: [id] }) + .query({ id }) .expect(200); - expect(body[id].current_status.last_failure_message).to.contain( + expect(body.last_failure_message).to.contain( 'execution has exceeded its allotted interval' ); }); @@ -555,6 +552,7 @@ export default ({ getService }: FtrProviderContext) => { { enrichments: [ { + feed: {}, indicator: { description: "domain should match the auditbeat hosts' data's source.ip", domain: '159.89.119.67', @@ -579,6 +577,7 @@ export default ({ getService }: FtrProviderContext) => { { enrichments: [ { + feed: {}, indicator: { description: "domain should match the auditbeat hosts' data's source.ip", domain: '159.89.119.67', @@ -645,6 +644,7 @@ export default ({ getService }: FtrProviderContext) => { assertContains(threat.enrichments, [ { + feed: {}, indicator: { description: 'this should match auditbeat/hosts on both port and ip', first_seen: '2021-01-26T11:06:03.000Z', @@ -662,6 +662,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, { + feed: {}, indicator: { description: 'this should match auditbeat/hosts on ip', first_seen: '2021-01-26T11:06:03.000Z', @@ -732,6 +733,7 @@ export default ({ getService }: FtrProviderContext) => { assertContains(threat.enrichments, [ { + feed: {}, indicator: { description: 'this should match auditbeat/hosts on both port and ip', first_seen: '2021-01-26T11:06:03.000Z', @@ -754,6 +756,7 @@ export default ({ getService }: FtrProviderContext) => { // threat.indicator.matched data). That's the case with the // first and third indicators matched, here. { + feed: {}, indicator: { description: 'this should match auditbeat/hosts on both port and ip', first_seen: '2021-01-26T11:06:03.000Z', @@ -772,6 +775,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, { + feed: {}, indicator: { description: 'this should match auditbeat/hosts on ip', first_seen: '2021-01-26T11:06:03.000Z', @@ -846,6 +850,7 @@ export default ({ getService }: FtrProviderContext) => { assertContains(threats[0].enrichments, [ { + feed: {}, indicator: { description: "domain should match the auditbeat hosts' data's source.ip", domain: '159.89.119.67', @@ -866,6 +871,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, { + feed: {}, indicator: { description: 'this should match auditbeat/hosts on both port and ip', first_seen: '2021-01-26T11:06:03.000Z', @@ -883,6 +889,7 @@ export default ({ getService }: FtrProviderContext) => { }, }, { + feed: {}, indicator: { description: 'this should match auditbeat/hosts on both port and ip', first_seen: '2021-01-26T11:06:03.000Z', @@ -903,6 +910,7 @@ export default ({ getService }: FtrProviderContext) => { assertContains(threats[1].enrichments, [ { + feed: {}, indicator: { description: "domain should match the auditbeat hosts' data's source.ip", domain: '159.89.119.67', diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.ts deleted file mode 100644 index 0b430d6ae2d92..0000000000000 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/find_statuses.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 - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import expect from '@kbn/expect'; - -import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; -import { FtrProviderContext } from '../../common/ftr_provider_context'; -import { - createSignalsIndex, - deleteAllAlerts, - deleteSignalsIndex, - deleteAllRulesStatuses, - getSimpleRule, - waitForRuleSuccessOrStatus, - createRule, -} from '../../utils'; - -// eslint-disable-next-line import/no-default-export -export default ({ getService }: FtrProviderContext): void => { - const supertest = getService('supertest'); - const es = getService('es'); - const esArchiver = getService('esArchiver'); - const log = getService('log'); - - describe('find_statuses', () => { - before(async () => { - await esArchiver.load('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - after(async () => { - await esArchiver.unload('x-pack/test/functional/es_archives/auditbeat/hosts'); - }); - - beforeEach(async () => { - await createSignalsIndex(supertest, log); - }); - - afterEach(async () => { - await deleteSignalsIndex(supertest, log); - await deleteAllAlerts(supertest, log); - await deleteAllRulesStatuses(es, log); - }); - - it('should return an empty find statuses body correctly if no statuses are loaded', async () => { - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) - .set('kbn-xsrf', 'true') - .send({ ids: [] }) - .expect(200); - - expect(body).to.eql({}); - }); - - /* - This test is to ensure no future regressions introduced by the following scenario - a call to updateApiKey was invalidating the api key used by the - rule while the rule was executing, or even before it executed, - on the first rule run. - this pr https://github.com/elastic/kibana/pull/68184 - fixed this by finding the true source of a bug that required the manual - api key update, and removed the call to that function. - - When the api key is updated before / while the rule is executing, the alert - executor no longer has access to a service to update the rule status - saved object in Elasticsearch. Because of this, we cannot set the rule into - a 'failure' state, so the user ends up seeing 'going to run' as that is the - last status set for the rule before it erupts in an error that cannot be - recorded inside of the executor. - - This adds an e2e test for the backend to catch that in case - this pops up again elsewhere. - */ - it('should return a single rule status when a single rule is loaded from a find status with defaults added', async () => { - const resBody = await createRule(supertest, log, getSimpleRule('rule-1', true)); - - await waitForRuleSuccessOrStatus(supertest, log, resBody.id); - - // query the single rule from _find - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) - .set('kbn-xsrf', 'true') - .send({ ids: [resBody.id] }) - .expect(200); - - // expected result for status should be 'going to run' or 'succeeded - expect(body[resBody.id].current_status.status).to.eql('succeeded'); - }); - }); -}; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts index 073c5a9971f4b..eaf30f18c1ca3 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/generating_signals.ts @@ -1189,12 +1189,10 @@ export default ({ getService }: FtrProviderContext) => { expect(signals.hits.hits.length).to.eql(1); const statusResponse = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .get(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send({ ids: [ruleResponse.id] }); - const initialStatusDate = new Date( - statusResponse.body[ruleResponse.id].current_status.status_date - ); + .query({ id: ruleResponse.id }); + const initialStatusDate = new Date(statusResponse.body.status_date); const initialSignal = signals.hits.hits[0]; diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts index db616baa0678a..05f5b7973e9ea 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/index.ts @@ -29,7 +29,6 @@ export default ({ loadTestFile }: FtrProviderContext): void => { loadTestFile(require.resolve('./delete_rules_bulk')); loadTestFile(require.resolve('./export_rules')); loadTestFile(require.resolve('./find_rules')); - loadTestFile(require.resolve('./find_statuses')); loadTestFile(require.resolve('./generating_signals')); loadTestFile(require.resolve('./get_prepackaged_rules_status')); loadTestFile(require.resolve('./import_rules')); diff --git a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_actions.ts b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_actions.ts index 43b982ff537d7..a46df0711c45d 100644 --- a/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_actions.ts +++ b/x-pack/test/detection_engine_api_integration/security_and_spaces/tests/update_actions.ts @@ -8,7 +8,6 @@ import expect from '@kbn/expect'; import { CreateRulesSchema } from '../../../../plugins/security_solution/common/detection_engine/schemas/request'; -import { DETECTION_ENGINE_RULES_URL } from '../../../../plugins/security_solution/common/constants'; import { FtrProviderContext } from '../../common/ftr_provider_context'; import { createSignalsIndex, @@ -91,14 +90,6 @@ export default ({ getService }: FtrProviderContext) => { const ruleToUpdate = getRuleWithWebHookAction(hookAction.id, true, rule); const updatedRule = await updateRule(supertest, log, ruleToUpdate); await waitForRuleSuccessOrStatus(supertest, log, updatedRule.id); - - // expected result for status should be 'succeeded' - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) - .set('kbn-xsrf', 'true') - .send({ ids: [updatedRule.id] }) - .expect(200); - expect(body[updatedRule.id].current_status.status).to.eql('succeeded'); }); it('should be able to create a new webhook action and attach it to a rule with a meta field and run it correctly', async () => { @@ -111,14 +102,6 @@ export default ({ getService }: FtrProviderContext) => { }; const updatedRule = await updateRule(supertest, log, ruleToUpdate); await waitForRuleSuccessOrStatus(supertest, log, updatedRule.id); - - // expected result for status should be 'succeeded' - const { body } = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) - .set('kbn-xsrf', 'true') - .send({ ids: [updatedRule.id] }) - .expect(200); - expect(body[updatedRule.id].current_status.status).to.eql('succeeded'); }); it('should be able to create a new webhook action and attach it to an immutable rule', async () => { diff --git a/x-pack/test/detection_engine_api_integration/utils.ts b/x-pack/test/detection_engine_api_integration/utils.ts index bb4f564edaf1e..b36a94e76ca60 100644 --- a/x-pack/test/detection_engine_api_integration/utils.ts +++ b/x-pack/test/detection_engine_api_integration/utils.ts @@ -69,6 +69,9 @@ export const removeServerGeneratedProperties = ( last_failure_message, last_success_at, last_success_message, + last_gap, + search_after_time_durations, + bulk_create_time_durations, status, status_date, /* eslint-enable @typescript-eslint/naming-convention */ @@ -1315,9 +1318,9 @@ export const waitForRuleSuccessOrStatus = async ( async () => { try { const response = await supertest - .post(`${DETECTION_ENGINE_RULES_URL}/_find_statuses`) + .get(DETECTION_ENGINE_RULES_URL) .set('kbn-xsrf', 'true') - .send({ ids: [id] }); + .query({ id }); if (response.status !== 200) { log.error( `Did not get an expected 200 "ok" when waiting for a rule success or status (waitForRuleSuccessOrStatus). CI issues could happen. Suspect this line if you are seeing CI issues. body: ${JSON.stringify( @@ -1325,9 +1328,9 @@ export const waitForRuleSuccessOrStatus = async ( )}, status: ${JSON.stringify(response.status)}` ); } - const currentStatus = response.body[id]?.current_status; + const rule = response.body; - if (currentStatus?.status !== status) { + if (rule?.status !== status) { log.debug( `Did not get an expected status of ${status} while waiting for a rule success or status for rule id ${id} (waitForRuleSuccessOrStatus). Will continue retrying until status is found. body: ${JSON.stringify( response.body @@ -1335,9 +1338,9 @@ export const waitForRuleSuccessOrStatus = async ( ); } return ( - currentStatus != null && - currentStatus.status === status && - (afterDate ? new Date(currentStatus.status_date) > afterDate : true) + rule != null && + rule.status === status && + (afterDate ? new Date(rule.status_date) > afterDate : true) ); } catch (e) { if ((e as Error).message.includes('got 503 "Service Unavailable"')) { diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts index 0915af7e25f0c..e553ee35a6eb6 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_assets.ts @@ -255,48 +255,6 @@ export default function (providerContext: FtrProviderContext) { } expect(resIndexPattern.response.data.statusCode).equal(404); }); - it('should have removed the fields from the index patterns', async () => { - // The reason there is an expect inside the try and inside the catch in this test case is to guard against two - // different scenarios. - // - // If a test case in another file calls /setup then the system and endpoint packages will be installed and - // will be present for the remainder of the tests (because they cannot be removed). If that is the case the - // expect in the try will work because the logs-* and metrics-* index patterns will still be present even - // after this test uninstalls its package. - // - // If /setup was never called prior to this test, when the test package is uninstalled the index pattern code - // checks to see if there are no packages installed and completely removes the logs-* and metrics-* index - // patterns. If that happens this code will throw an error and indicate that the index pattern being searched - // for was completely removed. In this case the catch's expect will test to make sure the error thrown was - // a 404 because all of the packages have been removed. - try { - const resIndexPatternLogs = await kibanaServer.savedObjects.get({ - type: 'index-pattern', - id: 'logs-*', - }); - const fields = JSON.parse(resIndexPatternLogs.attributes.fields); - const exists = fields.find((field: { name: string }) => field.name === 'logs_test_name'); - expect(exists).to.be(undefined); - } catch (err) { - // if all packages are uninstalled there won't be a logs-* index pattern - expect(err.response.data.statusCode).equal(404); - } - - try { - const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ - type: 'index-pattern', - id: 'metrics-*', - }); - const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); - const existsMetrics = fieldsMetrics.find( - (field: { name: string }) => field.name === 'metrics_test_name' - ); - expect(existsMetrics).to.be(undefined); - } catch (err) { - // if all packages are uninstalled there won't be a metrics-* index pattern - expect(err.response.data.statusCode).equal(404); - } - }); it('should have removed the saved object', async function () { let res; try { @@ -512,23 +470,19 @@ const expectAssetsInstalled = ({ } expect(resInvalidTypeIndexPattern.response.data.statusCode).equal(404); }); - it('should create an index pattern with the package fields', async () => { + it('should not add fields to the index patterns', async () => { const resIndexPatternLogs = await kibanaServer.savedObjects.get({ type: 'index-pattern', id: 'logs-*', }); - const fields = JSON.parse(resIndexPatternLogs.attributes.fields); - const exists = fields.find((field: { name: string }) => field.name === 'logs_test_name'); - expect(exists).not.to.be(undefined); + const logsAttributes = resIndexPatternLogs.attributes; + expect(logsAttributes.fields).to.be(undefined); const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ type: 'index-pattern', id: 'metrics-*', }); - const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); - const metricsExists = fieldsMetrics.find( - (field: { name: string }) => field.name === 'metrics_test_name' - ); - expect(metricsExists).not.to.be(undefined); + const metricsAttributes = resIndexPatternMetrics.attributes; + expect(metricsAttributes.fields).to.be(undefined); }); it('should have created the correct saved object', async function () { const res = await kibanaServer.savedObjects.get({ diff --git a/x-pack/test/fleet_api_integration/apis/epm/install_remove_multiple.ts b/x-pack/test/fleet_api_integration/apis/epm/install_remove_multiple.ts index 5e9aa415d6c90..b3c70539579c5 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/install_remove_multiple.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/install_remove_multiple.ts @@ -61,14 +61,7 @@ export default function (providerContext: FtrProviderContext) { const uninstallingPackagesPromise = pkgs.map((pkg) => uninstallPackage(pkg)); return Promise.all(uninstallingPackagesPromise); }; - const expectPkgFieldToExist = (fields: any[], fieldName: string, exists: boolean = true) => { - const fieldExists = fields.find((field: { name: string }) => field.name === fieldName); - if (exists) { - expect(fieldExists).not.to.be(undefined); - } else { - expect(fieldExists).to.be(undefined); - } - }; + describe('installs and uninstalls multiple packages side effects', async () => { skipIfNoDockerRegistry(providerContext); setupFleetAndAgents(providerContext); @@ -81,69 +74,17 @@ export default function (providerContext: FtrProviderContext) { if (!server.enabled) return; await uninstallPackages([pkgKey, experimentalPkgKey]); }); - it('should create index patterns from all installed packages: uploaded, experimental, beta', async () => { - const resIndexPatternLogs = await kibanaServer.savedObjects.get({ - type: 'index-pattern', - id: 'logs-*', - }); - - const fieldsLogs = JSON.parse(resIndexPatternLogs.attributes.fields); - - expectPkgFieldToExist(fieldsLogs, 'logs_test_name'); - expectPkgFieldToExist(fieldsLogs, 'logs_experimental_name'); - expectPkgFieldToExist(fieldsLogs, 'logs_experimental2_name'); - expectPkgFieldToExist(fieldsLogs, 'apache.error.uploadtest'); - const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ - type: 'index-pattern', - id: 'metrics-*', - }); - const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); - expectPkgFieldToExist(fieldsMetrics, 'metrics_test_name'); - expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental_name'); - expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental2_name'); - expectPkgFieldToExist(fieldsMetrics, 'apache.status.uploadtest'); - }); - it('should correctly recreate index patterns when a package is uninstalled', async () => { - await uninstallPackage(experimental2PkgKey); - const resIndexPatternLogs = await kibanaServer.savedObjects.get({ - type: 'index-pattern', - id: 'logs-*', - }); - const fieldsLogs = JSON.parse(resIndexPatternLogs.attributes.fields); - expectPkgFieldToExist(fieldsLogs, 'logs_test_name'); - expectPkgFieldToExist(fieldsLogs, 'logs_experimental_name'); - expectPkgFieldToExist(fieldsLogs, 'logs_experimental2_name', false); - expectPkgFieldToExist(fieldsLogs, 'apache.error.uploadtest'); - const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ - type: 'index-pattern', - id: 'metrics-*', - }); - const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); - - expectPkgFieldToExist(fieldsMetrics, 'metrics_test_name'); - expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental_name'); - expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental2_name', false); - expectPkgFieldToExist(fieldsMetrics, 'apache.status.uploadtest'); - }); - it('should correctly recreate index patterns when an uploaded package is uninstalled', async () => { - await uninstallPackage(uploadPkgKey); + it('should create index patterns (without fields)', async () => { const resIndexPatternLogs = await kibanaServer.savedObjects.get({ type: 'index-pattern', id: 'logs-*', }); - const fieldsLogs = JSON.parse(resIndexPatternLogs.attributes.fields); - expectPkgFieldToExist(fieldsLogs, 'logs_test_name'); - expectPkgFieldToExist(fieldsLogs, 'logs_experimental_name'); - expectPkgFieldToExist(fieldsLogs, 'apache.error.uploadtest', false); + expect(resIndexPatternLogs.attributes.fields).to.be(undefined); const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ type: 'index-pattern', id: 'metrics-*', }); - const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); - - expectPkgFieldToExist(fieldsMetrics, 'metrics_test_name'); - expectPkgFieldToExist(fieldsMetrics, 'metrics_experimental_name'); - expectPkgFieldToExist(fieldsMetrics, 'apache.status.uploadtest', false); + expect(resIndexPatternMetrics.attributes.fields).to.be(undefined); }); }); } diff --git a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts index 032abac4be4de..b46c932373cdf 100644 --- a/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts +++ b/x-pack/test/fleet_api_integration/apis/epm/update_assets.ts @@ -287,24 +287,6 @@ export default function (providerContext: FtrProviderContext) { ], }); }); - it('should have updated the index patterns', async function () { - const resIndexPatternLogs = await kibanaServer.savedObjects.get({ - type: 'index-pattern', - id: 'logs-*', - }); - const fields = JSON.parse(resIndexPatternLogs.attributes.fields); - const updated = fields.filter((field: { name: string }) => field.name === 'new_field_name'); - expect(!!updated.length).equal(true); - const resIndexPatternMetrics = await kibanaServer.savedObjects.get({ - type: 'index-pattern', - id: 'metrics-*', - }); - const fieldsMetrics = JSON.parse(resIndexPatternMetrics.attributes.fields); - const updatedMetrics = fieldsMetrics.filter( - (field: { name: string }) => field.name === 'metrics_test_name2' - ); - expect(!!updatedMetrics.length).equal(true); - }); it('should have updated the kibana assets', async function () { const resDashboard = await kibanaServer.savedObjects.get({ type: 'dashboard', diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/search/all_logs.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/search/all_logs.json new file mode 100644 index 0000000000000..52fb2fd62957d --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/search/all_logs.json @@ -0,0 +1,36 @@ +{ + "attributes": { + "columns": [ + "log.level", + "kafka.log.component", + "message" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"$state\":{\"store\":\"appState\"},\"meta\":{\"alias\":null,\"disabled\":false,\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\",\"key\":\"dataset.name\",\"negate\":false,\"params\":{\"query\":\"kafka.log\",\"type\":\"phrase\"},\"type\":\"phrase\",\"value\":\"log\"},\"query\":{\"match\":{\"dataset.name\":{\"query\":\"kafka.log\",\"type\":\"phrase\"}}}}],\"highlightAll\":true,\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"version\":true}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "All logs [Logs Kafka] ECS", + "version": 1 + }, + "id": "Kafka stacktraces-ecs", + "references": [ + { + "id": "logs-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "logs-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "search" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/search/ecs_logs.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/search/ecs_logs.json new file mode 100644 index 0000000000000..1b34746cec89e --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/search/ecs_logs.json @@ -0,0 +1,36 @@ +{ + "attributes": { + "columns": [ + "log.level", + "kafka.log.component", + "message" + ], + "description": "", + "hits": 0, + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[{\"$state\":{\"store\":\"appState\"},\"meta\":{\"alias\":null,\"disabled\":false,\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index\",\"key\":\"dataset.name\",\"negate\":false,\"params\":{\"query\":\"kafka.log\",\"type\":\"phrase\"},\"type\":\"phrase\",\"value\":\"log\"},\"query\":{\"match\":{\"dataset.name\":{\"query\":\"kafka.log\",\"type\":\"phrase\"}}}}],\"highlightAll\":true,\"indexRefName\":\"kibanaSavedObjectMeta.searchSourceJSON.index\",\"query\":{\"language\":\"kuery\",\"query\":\"\"},\"version\":true}" + }, + "sort": [ + [ + "@timestamp", + "desc" + ] + ], + "title": "All logs [Logs Kafka] ECS", + "version": 1 + }, + "id": "All Kafka logs-ecs", + "references": [ + { + "id": "logs-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.index", + "type": "index-pattern" + }, + { + "id": "logs-*", + "name": "kibanaSavedObjectMeta.searchSourceJSON.filter[0].meta.index", + "type": "index-pattern" + } + ], + "type": "search" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/search/sample_search.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/search/sample_search.json index 1b34746cec89e..2698337d0b6fd 100644 --- a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/search/sample_search.json +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/search/sample_search.json @@ -19,7 +19,7 @@ "title": "All logs [Logs Kafka] ECS", "version": 1 }, - "id": "All Kafka logs-ecs", + "id": "sample_search", "references": [ { "id": "logs-*", diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/visualization/panel_0.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/visualization/panel_0.json new file mode 100644 index 0000000000000..095bf2ee94b59 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/visualization/panel_0.json @@ -0,0 +1,22 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Log levels over time [Logs Kafka] ECS", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":\"segment\",\"type\":\"date_histogram\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"customLabel\":\"Log Level\",\"field\":\"log.level\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"group\",\"type\":\"terms\"}],\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"labels\":{\"show\":true,\"truncate\":100},\"position\":\"bottom\",\"scale\":{\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"@timestamp per day\"},\"type\":\"category\"}],\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"legendPosition\":\"right\",\"seriesParams\":[{\"data\":{\"id\":\"1\",\"label\":\"Count\"},\"drawLinesBetweenPoints\":true,\"mode\":\"stacked\",\"show\":\"true\",\"showCircles\":true,\"type\":\"histogram\",\"valueAxis\":\"ValueAxis-1\"}],\"times\":[],\"type\":\"histogram\",\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"labels\":{\"filter\":false,\"rotate\":0,\"show\":true,\"truncate\":100},\"name\":\"LeftAxis-1\",\"position\":\"left\",\"scale\":{\"mode\":\"normal\",\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"Count\"},\"type\":\"value\"}]},\"title\":\"Log levels over time [Logs Kafka] ECS\",\"type\":\"histogram\"}" + }, + "id": "panel_0", + "references": [ + { + "id": "All Kafka logs-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/visualization/panel_1.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/visualization/panel_1.json new file mode 100644 index 0000000000000..9f98c35881c04 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/visualization/panel_1.json @@ -0,0 +1,22 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Log levels over time [Logs Kafka] ECS", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":\"segment\",\"type\":\"date_histogram\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"customLabel\":\"Log Level\",\"field\":\"log.level\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"group\",\"type\":\"terms\"}],\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"labels\":{\"show\":true,\"truncate\":100},\"position\":\"bottom\",\"scale\":{\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"@timestamp per day\"},\"type\":\"category\"}],\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"legendPosition\":\"right\",\"seriesParams\":[{\"data\":{\"id\":\"1\",\"label\":\"Count\"},\"drawLinesBetweenPoints\":true,\"mode\":\"stacked\",\"show\":\"true\",\"showCircles\":true,\"type\":\"histogram\",\"valueAxis\":\"ValueAxis-1\"}],\"times\":[],\"type\":\"histogram\",\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"labels\":{\"filter\":false,\"rotate\":0,\"show\":true,\"truncate\":100},\"name\":\"LeftAxis-1\",\"position\":\"left\",\"scale\":{\"mode\":\"normal\",\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"Count\"},\"type\":\"value\"}]},\"title\":\"Log levels over time [Logs Kafka] ECS\",\"type\":\"histogram\"}" + }, + "id": "panel_1", + "references": [ + { + "id": "All Kafka logs-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/visualization/panel_2.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/visualization/panel_2.json new file mode 100644 index 0000000000000..25a175ab040dc --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/visualization/panel_2.json @@ -0,0 +1,22 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Log levels over time [Logs Kafka] ECS", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":\"segment\",\"type\":\"date_histogram\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"customLabel\":\"Log Level\",\"field\":\"log.level\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"group\",\"type\":\"terms\"}],\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"labels\":{\"show\":true,\"truncate\":100},\"position\":\"bottom\",\"scale\":{\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"@timestamp per day\"},\"type\":\"category\"}],\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"legendPosition\":\"right\",\"seriesParams\":[{\"data\":{\"id\":\"1\",\"label\":\"Count\"},\"drawLinesBetweenPoints\":true,\"mode\":\"stacked\",\"show\":\"true\",\"showCircles\":true,\"type\":\"histogram\",\"valueAxis\":\"ValueAxis-1\"}],\"times\":[],\"type\":\"histogram\",\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"labels\":{\"filter\":false,\"rotate\":0,\"show\":true,\"truncate\":100},\"name\":\"LeftAxis-1\",\"position\":\"left\",\"scale\":{\"mode\":\"normal\",\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"Count\"},\"type\":\"value\"}]},\"title\":\"Log levels over time [Logs Kafka] ECS\",\"type\":\"histogram\"}" + }, + "id": "panel_2", + "references": [ + { + "id": "All Kafka logs-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/visualization/panel_3.json b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/visualization/panel_3.json new file mode 100644 index 0000000000000..4987ac23b8029 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/filetest/0.1.0/kibana/visualization/panel_3.json @@ -0,0 +1,22 @@ +{ + "attributes": { + "description": "", + "kibanaSavedObjectMeta": { + "searchSourceJSON": "{\"filter\":[]}" + }, + "savedSearchRefName": "search_0", + "title": "Log levels over time [Logs Kafka] ECS", + "uiStateJSON": "{}", + "version": 1, + "visState": "{\"aggs\":[{\"enabled\":true,\"id\":\"1\",\"params\":{},\"schema\":\"metric\",\"type\":\"count\"},{\"enabled\":true,\"id\":\"2\",\"params\":{\"extended_bounds\":{},\"field\":\"@timestamp\",\"interval\":\"auto\",\"min_doc_count\":1},\"schema\":\"segment\",\"type\":\"date_histogram\"},{\"enabled\":true,\"id\":\"3\",\"params\":{\"customLabel\":\"Log Level\",\"field\":\"log.level\",\"order\":\"desc\",\"orderBy\":\"1\",\"size\":5},\"schema\":\"group\",\"type\":\"terms\"}],\"params\":{\"addLegend\":true,\"addTimeMarker\":false,\"addTooltip\":true,\"categoryAxes\":[{\"id\":\"CategoryAxis-1\",\"labels\":{\"show\":true,\"truncate\":100},\"position\":\"bottom\",\"scale\":{\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"@timestamp per day\"},\"type\":\"category\"}],\"grid\":{\"categoryLines\":false,\"style\":{\"color\":\"#eee\"}},\"legendPosition\":\"right\",\"seriesParams\":[{\"data\":{\"id\":\"1\",\"label\":\"Count\"},\"drawLinesBetweenPoints\":true,\"mode\":\"stacked\",\"show\":\"true\",\"showCircles\":true,\"type\":\"histogram\",\"valueAxis\":\"ValueAxis-1\"}],\"times\":[],\"type\":\"histogram\",\"valueAxes\":[{\"id\":\"ValueAxis-1\",\"labels\":{\"filter\":false,\"rotate\":0,\"show\":true,\"truncate\":100},\"name\":\"LeftAxis-1\",\"position\":\"left\",\"scale\":{\"mode\":\"normal\",\"type\":\"linear\"},\"show\":true,\"style\":{},\"title\":{\"text\":\"Count\"},\"type\":\"value\"}]},\"title\":\"Log levels over time [Logs Kafka] ECS\",\"type\":\"histogram\"}" + }, + "id": "panel_3", + "references": [ + { + "id": "All Kafka logs-ecs", + "name": "search_0", + "type": "search" + } + ], + "type": "visualization" +} \ No newline at end of file diff --git a/x-pack/test/functional/apps/dashboard/index.ts b/x-pack/test/functional/apps/dashboard/index.ts index 59211ecf37f2d..be817faeaa6c4 100644 --- a/x-pack/test/functional/apps/dashboard/index.ts +++ b/x-pack/test/functional/apps/dashboard/index.ts @@ -22,6 +22,7 @@ export default function ({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./dashboard_maps_by_value')); loadTestFile(require.resolve('./migration_smoke_tests/lens_migration_smoke_test')); + loadTestFile(require.resolve('./migration_smoke_tests/controls_migration_smoke_test')); loadTestFile(require.resolve('./migration_smoke_tests/visualize_migration_smoke_test')); loadTestFile(require.resolve('./migration_smoke_tests/tsvb_migration_smoke_test')); }); diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/controls_migration_smoke_test.ts b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/controls_migration_smoke_test.ts new file mode 100644 index 0000000000000..72de77c2e2c2b --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/controls_migration_smoke_test.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +/* + * This test imports a dashboard saved with controls from 8.0.0, because that is the earliest version + * with the dashboard controls integration in place. + */ + +import expect from '@kbn/expect'; +import path from 'path'; +import { FtrProviderContext } from '../../../ftr_provider_context'; + +export default function ({ getService, getPageObjects }: FtrProviderContext) { + const retry = getService('retry'); + const pieChart = getService('pieChart'); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + const testSubjects = getService('testSubjects'); + + const { common, settings, savedObjects, dashboard, dashboardControls } = getPageObjects([ + 'common', + 'settings', + 'dashboard', + 'savedObjects', + 'dashboardControls', + ]); + + describe('Export import saved objects between versions', () => { + before(async () => { + await esArchiver.loadIfNeeded( + 'x-pack/test/functional/es_archives/getting_started/shakespeare' + ); + await kibanaServer.uiSettings.replace({}); + await settings.navigateTo(); + await settings.clickKibanaSavedObjects(); + await savedObjects.importFile( + path.join(__dirname, 'exports', 'controls_dashboard_migration_test_8_0_0.ndjson') + ); + }); + + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/getting_started/shakespeare'); + await esArchiver.load('x-pack/test/functional/es_archives/empty_kibana'); + }); + + it('should be able to import dashboard with controls from 8.0.0', async () => { + // this will catch cases where there is an error in the migrations. + await savedObjects.checkImportSucceeded(); + await savedObjects.clickImportDone(); + }); + + it('should render all panels on the dashboard', async () => { + await dashboardControls.enableControlsLab(); + await common.navigateToApp('dashboard'); + await dashboard.loadSavedDashboard('[8.0.0] Controls Dashboard'); + + // dashboard should load properly + await dashboard.expectOnDashboard('[8.0.0] Controls Dashboard'); + await dashboard.waitForRenderComplete(); + + // There should be 0 error embeddables on the dashboard + const errorEmbeddables = await testSubjects.findAll('embeddableStackError'); + expect(errorEmbeddables.length).to.be(0); + }); + + it('loads all controls from the saved dashboard', async () => { + expect(await dashboardControls.getControlsCount()).to.be(2); + expect(await dashboardControls.getAllControlTitles()).to.eql(['Speaker Name', 'Play Name']); + + const ids = await dashboardControls.getAllControlIds(); + for (const id of ids) { + await dashboardControls.optionsListOpenPopover(id); + await retry.try(async () => { + // Value counts should be 10, because there are more than 10 speakers and plays in the data set + expect(await dashboardControls.optionsListPopoverGetAvailableOptionsCount()).to.be(10); + }); + await dashboardControls.optionsListEnsurePopoverIsClosed(id); + } + }); + + it('applies default selected options list options to control', async () => { + const controlIds = await dashboardControls.getAllControlIds(); + const selectionString = await dashboardControls.optionsListGetSelectionsString(controlIds[0]); + expect(selectionString).to.be('HAMLET, ROMEO, JULIET, BRUTUS'); + }); + + it('applies default selected options list options to dashboard', async () => { + // because 4 selections are made on the control, the pie chart should only show 4 slices. + expect(await pieChart.getPieSliceCount()).to.be(4); + }); + }); +} diff --git a/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/controls_dashboard_migration_test_8_0_0.ndjson b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/controls_dashboard_migration_test_8_0_0.ndjson new file mode 100644 index 0000000000000..8996a93476621 --- /dev/null +++ b/x-pack/test/functional/apps/dashboard/migration_smoke_tests/exports/controls_dashboard_migration_test_8_0_0.ndjson @@ -0,0 +1,3 @@ +{"attributes":{"fieldAttrs":"{}","fields":"[]","runtimeFieldMap":"{}","title":"shakespeare","typeMeta":"{}"},"coreMigrationVersion":"8.1.0","id":"f60ebe00-4e0f-11ec-afa1-59364130c0f2","migrationVersion":{"index-pattern":"8.0.0"},"references":[],"type":"index-pattern","updated_at":"2021-11-25T17:46:44.515Z","version":"WzEwLDJd"} +{"attributes":{"controlGroupInput":{"controlStyle":"oneLine","panelsJSON":"{\"ec64cb73-f891-4f48-bfa5-e3ca0df9920e\":{\"order\":1,\"width\":\"auto\",\"type\":\"optionsListControl\",\"explicitInput\":{\"title\":\"Speaker Name\",\"fieldName\":\"speaker\",\"id\":\"ec64cb73-f891-4f48-bfa5-e3ca0df9920e\",\"enhancements\":{},\"selectedOptions\":[\"HAMLET\",\"ROMEO\",\"JULIET\",\"BRUTUS\"]}},\"3b343fbe-c0a0-4bc5-825e-7c4fff42ad8f\":{\"order\":2,\"width\":\"auto\",\"type\":\"optionsListControl\",\"explicitInput\":{\"title\":\"Play Name\",\"fieldName\":\"play_name\",\"id\":\"3b343fbe-c0a0-4bc5-825e-7c4fff42ad8f\",\"enhancements\":{},\"selectedOptions\":[]}}}"},"description":"","hits":0,"kibanaSavedObjectMeta":{"searchSourceJSON":"{\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}"},"optionsJSON":"{\"useMargins\":true,\"syncColors\":false,\"hidePanelTitles\":false}","panelsJSON":"[{\"version\":\"8.1.0\",\"type\":\"lens\",\"gridData\":{\"x\":0,\"y\":0,\"w\":23,\"h\":15,\"i\":\"bee1d6e0-389a-4992-a802-d51a658e3faa\"},\"panelIndex\":\"bee1d6e0-389a-4992-a802-d51a658e3faa\",\"embeddableConfig\":{\"attributes\":{\"title\":\"\",\"visualizationType\":\"lnsXY\",\"type\":\"lens\",\"references\":[{\"id\":\"f60ebe00-4e0f-11ec-afa1-59364130c0f2\",\"name\":\"indexpattern-datasource-current-indexpattern\",\"type\":\"index-pattern\"},{\"id\":\"f60ebe00-4e0f-11ec-afa1-59364130c0f2\",\"name\":\"indexpattern-datasource-layer-623f6f95-c70a-4e42-9505-b2b428d3c86b\",\"type\":\"index-pattern\"}],\"state\":{\"visualization\":{\"legend\":{\"isVisible\":true,\"position\":\"right\"},\"valueLabels\":\"hide\",\"fittingFunction\":\"None\",\"yLeftExtent\":{\"mode\":\"full\"},\"yRightExtent\":{\"mode\":\"full\"},\"axisTitlesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"tickLabelsVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"labelsOrientation\":{\"x\":0,\"yLeft\":0,\"yRight\":0},\"gridlinesVisibilitySettings\":{\"x\":true,\"yLeft\":true,\"yRight\":true},\"preferredSeriesType\":\"bar_stacked\",\"layers\":[{\"layerId\":\"623f6f95-c70a-4e42-9505-b2b428d3c86b\",\"accessors\":[\"9f9547b8-c132-4634-80f6-ed70ba4ec689\"],\"position\":\"top\",\"seriesType\":\"bar_stacked\",\"showGridlines\":false,\"layerType\":\"data\",\"xAccessor\":\"44c77394-0cbc-43ec-b97a-7c446631c77e\"}]},\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filters\":[],\"datasourceStates\":{\"indexpattern\":{\"layers\":{\"623f6f95-c70a-4e42-9505-b2b428d3c86b\":{\"columns\":{\"44c77394-0cbc-43ec-b97a-7c446631c77e\":{\"label\":\"Top values of play_name\",\"dataType\":\"string\",\"operationType\":\"terms\",\"scale\":\"ordinal\",\"sourceField\":\"play_name\",\"isBucketed\":true,\"params\":{\"size\":5,\"orderBy\":{\"type\":\"column\",\"columnId\":\"9f9547b8-c132-4634-80f6-ed70ba4ec689\"},\"orderDirection\":\"desc\",\"otherBucket\":false,\"missingBucket\":false}},\"9f9547b8-c132-4634-80f6-ed70ba4ec689\":{\"label\":\"Count of records\",\"dataType\":\"number\",\"operationType\":\"count\",\"isBucketed\":false,\"scale\":\"ratio\",\"sourceField\":\"Records\"}},\"columnOrder\":[\"44c77394-0cbc-43ec-b97a-7c446631c77e\",\"9f9547b8-c132-4634-80f6-ed70ba4ec689\"],\"incompleteColumns\":{}}}}}}},\"enhancements\":{}}},{\"version\":\"8.1.0\",\"type\":\"visualization\",\"gridData\":{\"x\":23,\"y\":0,\"w\":24,\"h\":15,\"i\":\"63eee8ef-0220-4089-9be9-13a9839cae17\"},\"panelIndex\":\"63eee8ef-0220-4089-9be9-13a9839cae17\",\"embeddableConfig\":{\"savedVis\":{\"id\":\"\",\"title\":\"\",\"description\":\"\",\"type\":\"pie\",\"params\":{\"type\":\"pie\",\"addTooltip\":true,\"addLegend\":true,\"legendPosition\":\"right\",\"nestedLegend\":false,\"truncateLegend\":true,\"maxLegendLines\":1,\"distinctColors\":false,\"isDonut\":true,\"palette\":{\"type\":\"palette\",\"name\":\"default\"},\"labels\":{\"show\":true,\"last_level\":true,\"values\":true,\"valuesFormat\":\"percent\",\"percentDecimals\":2,\"truncate\":100,\"position\":\"default\"}},\"uiState\":{},\"data\":{\"aggs\":[{\"id\":\"1\",\"enabled\":true,\"type\":\"count\",\"params\":{},\"schema\":\"metric\"},{\"id\":\"2\",\"enabled\":true,\"type\":\"terms\",\"params\":{\"field\":\"speaker\",\"orderBy\":\"1\",\"order\":\"desc\",\"size\":5,\"otherBucket\":false,\"otherBucketLabel\":\"Other\",\"missingBucket\":false,\"missingBucketLabel\":\"Missing\"},\"schema\":\"segment\"}],\"searchSource\":{\"index\":\"f60ebe00-4e0f-11ec-afa1-59364130c0f2\",\"query\":{\"query\":\"\",\"language\":\"kuery\"},\"filter\":[]}}},\"enhancements\":{}}}]","timeRestore":false,"title":"[8.0.0] Controls Dashboard","version":1},"coreMigrationVersion":"8.1.0","id":"5bcfc8b0-4e10-11ec-afa1-59364130c0f2","migrationVersion":{"dashboard":"8.0.0"},"references":[{"id":"f60ebe00-4e0f-11ec-afa1-59364130c0f2","name":"bee1d6e0-389a-4992-a802-d51a658e3faa:indexpattern-datasource-current-indexpattern","type":"index-pattern"},{"id":"f60ebe00-4e0f-11ec-afa1-59364130c0f2","name":"bee1d6e0-389a-4992-a802-d51a658e3faa:indexpattern-datasource-layer-623f6f95-c70a-4e42-9505-b2b428d3c86b","type":"index-pattern"},{"id":"f60ebe00-4e0f-11ec-afa1-59364130c0f2","name":"63eee8ef-0220-4089-9be9-13a9839cae17:kibanaSavedObjectMeta.searchSourceJSON.index","type":"index-pattern"},{"id":"f60ebe00-4e0f-11ec-afa1-59364130c0f2","name":"controlGroup_ec64cb73-f891-4f48-bfa5-e3ca0df9920e:optionsListDataView","type":"index-pattern"},{"id":"f60ebe00-4e0f-11ec-afa1-59364130c0f2","name":"controlGroup_3b343fbe-c0a0-4bc5-825e-7c4fff42ad8f:optionsListDataView","type":"index-pattern"}],"type":"dashboard","updated_at":"2021-11-25T17:48:02.981Z","version":"WzM5LDJd"} +{"excludedObjects":[],"excludedObjectsCount":0,"exportedCount":2,"missingRefCount":0,"missingReferences":[]} \ No newline at end of file diff --git a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts index 67f7d80deba80..b2ec3278df315 100644 --- a/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts +++ b/x-pack/test/functional/apps/dashboard/reporting/download_csv.ts @@ -79,7 +79,8 @@ export default function ({ getService, getPageObjects }: FtrProviderContext) { } }); - describe('E-Commerce Data', () => { + // FLAKY: https://github.com/elastic/kibana/issues/120433 + describe.skip('E-Commerce Data', () => { before(async () => { await esArchiver.load(ecommerceDataPath); await kibanaServer.importExport.load(ecommerceSOPath); diff --git a/x-pack/test/functional/apps/lens/index.ts b/x-pack/test/functional/apps/lens/index.ts index 72442be7645fa..24bb1440af622 100644 --- a/x-pack/test/functional/apps/lens/index.ts +++ b/x-pack/test/functional/apps/lens/index.ts @@ -47,6 +47,16 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid loadTestFile(require.resolve('./persistent_context')); }); + describe('', function () { + this.tags(['ciGroup16', 'skipFirefox']); + + loadTestFile(require.resolve('./add_to_dashboard')); + loadTestFile(require.resolve('./table_dashboard')); + loadTestFile(require.resolve('./table')); + loadTestFile(require.resolve('./runtime_fields')); + loadTestFile(require.resolve('./dashboard')); + }); + describe('', function () { this.tags(['ciGroup4', 'skipFirefox']); @@ -66,15 +76,5 @@ export default function ({ getService, loadTestFile, getPageObjects }: FtrProvid // has to be last one in the suite because it overrides saved objects loadTestFile(require.resolve('./rollup')); }); - - describe('', function () { - this.tags(['ciGroup16', 'skipFirefox']); - - loadTestFile(require.resolve('./add_to_dashboard')); - loadTestFile(require.resolve('./table_dashboard')); - loadTestFile(require.resolve('./table')); - loadTestFile(require.resolve('./runtime_fields')); - loadTestFile(require.resolve('./dashboard')); - }); }); } diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts index 384d6fa43cac8..7ce3fcdc51e1b 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/outlier_detection_creation.ts @@ -63,13 +63,13 @@ export default function ({ getService }: FtrProviderContext) { // markers { color: '#52B398', percentage: 15 }, // grey boilerplate - { color: '#6A717D', percentage: 33 }, + { color: '#6A717D', percentage: 13 }, ], scatterplotMatrixColorStatsResults: [ // red markers { color: '#D98071', percentage: 1 }, // tick/grid/axis, grey markers - { color: '#6A717D', percentage: 33 }, + { color: '#6A717D', percentage: 12 }, { color: '#D3DAE6', percentage: 8 }, { color: '#98A1B3', percentage: 12 }, // anti-aliasing diff --git a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts index 66d0ba0ec6e36..d2a9554f3c6d6 100644 --- a/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts +++ b/x-pack/test/functional/apps/ml/data_frame_analytics/regression_creation.ts @@ -53,8 +53,8 @@ export default function ({ getService }: FtrProviderContext) { { color: '#61AFA3', percentage: 2 }, { color: '#D1E5E0', percentage: 2 }, // tick/grid/axis - { color: '#6A717D', percentage: 10 }, - { color: '#F5F7FA', percentage: 10 }, + { color: '#6A717D', percentage: 5 }, + { color: '#F5F7FA', percentage: 5 }, { color: '#D3DAE6', percentage: 3 }, ], runtimeFieldsEditorContent: ['{', ' "uppercase_stab": {', ' "type": "keyword",'], diff --git a/x-pack/test/functional/apps/spaces/enter_space.ts b/x-pack/test/functional/apps/spaces/enter_space.ts index 229da6aa708f5..1055aa43eac39 100644 --- a/x-pack/test/functional/apps/spaces/enter_space.ts +++ b/x-pack/test/functional/apps/spaces/enter_space.ts @@ -14,7 +14,8 @@ export default function enterSpaceFunctonalTests({ const esArchiver = getService('esArchiver'); const PageObjects = getPageObjects(['security', 'spaceSelector']); - describe('Enter Space', function () { + // FLAKY: https://github.com/elastic/kibana/issues/99879 + describe.skip('Enter Space', function () { // FLAKY: https://github.com/elastic/kibana/issues/100570 // These tests fail very intermittently in Firefox. Skip Firefox testing until resolved. // this.tags('includeFirefox'); 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 49211deaa2c42..735efec4a3c66 100644 --- a/x-pack/test/functional/apps/transform/creation_index_pattern.ts +++ b/x-pack/test/functional/apps/transform/creation_index_pattern.ts @@ -181,7 +181,7 @@ export default function ({ getService }: FtrProviderContext) { legend: 'top 20 of 46 categories', colorStats: [ { color: '#000000', percentage: 60 }, - { color: '#54B399', percentage: 35 }, + { color: '#54B399', percentage: 37 }, ], }, { @@ -190,7 +190,7 @@ export default function ({ getService }: FtrProviderContext) { legend: 'top 20 of 3321 categories', colorStats: [ { color: '#000000', percentage: 25 }, - { color: '#54B399', percentage: 67 }, + { color: '#54B399', percentage: 75 }, ], }, { @@ -207,7 +207,7 @@ export default function ({ getService }: FtrProviderContext) { id: 'customer_id', legend: 'top 20 of 46 categories', colorStats: [ - { color: '#54B399', percentage: 35 }, + { color: '#54B399', percentage: 37 }, { color: '#000000', percentage: 60 }, ], }, @@ -216,8 +216,8 @@ export default function ({ getService }: FtrProviderContext) { id: 'customer_last_name', legend: 'top 20 of 183 categories', colorStats: [ - { color: '#000000', percentage: 25 }, - { color: '#54B399', percentage: 70 }, + { color: '#000000', percentage: 23 }, + { color: '#54B399', percentage: 77 }, ], }, { diff --git a/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts b/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts index 30baeb9022833..bbcfe91b54a7d 100644 --- a/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts +++ b/x-pack/test/functional/apps/transform/creation_runtime_mappings.ts @@ -69,7 +69,7 @@ export default function ({ getService }: FtrProviderContext) { legend: '19 categories', colorStats: [ { color: '#000000', percentage: 49 }, - { color: '#54B399', percentage: 41 }, + { color: '#54B399', percentage: 50 }, ], }, { @@ -87,7 +87,7 @@ export default function ({ getService }: FtrProviderContext) { legend: '19 categories', colorStats: [ { color: '#000000', percentage: 49 }, - { color: '#54B399', percentage: 41 }, + { color: '#54B399', percentage: 50 }, ], }, { diff --git a/x-pack/test/functional/apps/uptime/synthetics_integration.ts b/x-pack/test/functional/apps/uptime/synthetics_integration.ts index 49f5895bd98f8..ae7edf3524d7d 100644 --- a/x-pack/test/functional/apps/uptime/synthetics_integration.ts +++ b/x-pack/test/functional/apps/uptime/synthetics_integration.ts @@ -53,6 +53,7 @@ export default function ({ getPageObjects, getService }: FtrProviderContext) { id: `${getSyntheticsPolicy(agentFullPolicy)?.streams?.[0]?.id}`, name, type: monitorType, + enabled: true, processors: [ { add_observer_metadata: { diff --git a/x-pack/test/functional/es_archives/alerts/data.json b/x-pack/test/functional/es_archives/alerts/data.json index c40137fa4b5a6..249f14caf28f7 100644 --- a/x-pack/test/functional/es_archives/alerts/data.json +++ b/x-pack/test/functional/es_archives/alerts/data.json @@ -613,3 +613,52 @@ } } } + +{ + "type": "doc", + "value": { + "id": "alert:92237b30-4e03-11ec-9ab9-d980518a2d28", + "index": ".kibana_1", + "source": { + "alert" : { + "alertTypeId" : "metrics.alert.inventory.threshold", + "consumer" : "infrastructure", + "params" : { + }, + "schedule" : { + "interval" : "1m" + }, + "enabled" : true, + "actions" : [ + { + "actionTypeId" : ".server-log", + "params" : { + "level" : "info", + "message" : "Alert message" + }, + "actionRef" : "action_0", + "group" : "metrics.invenotry_threshold.fired" + } + ], + "throttle" : null, + "apiKeyOwner" : null, + "apiKey" : null, + "createdBy" : "elastic", + "updatedBy" : "elastic", + "createdAt" : "2021-07-27T20:42:55.896Z", + "muteAll" : false, + "mutedInstanceIds" : [ ], + "scheduledTaskId" : null, + "tags": [] + }, + "type" : "alert", + "migrationVersion" : { + "alert" : "7.8.0" + }, + "updated_at" : "2021-08-13T23:00:11.985Z", + "references": [ + ] + } + } +} + diff --git a/x-pack/test/functional/services/canvas_element.ts b/x-pack/test/functional/services/canvas_element.ts index 6167c8fab45f8..61bfe23950ee2 100644 --- a/x-pack/test/functional/services/canvas_element.ts +++ b/x-pack/test/functional/services/canvas_element.ts @@ -7,6 +7,8 @@ import { rgb, nest } from 'd3'; +import { FtrProviderContext } from '../ftr_provider_context'; + interface ColorStat { color: string; percentage: number; @@ -16,7 +18,9 @@ interface ColorStat { export type CanvasElementColorStats = ColorStat[]; -import { FtrProviderContext } from '../ftr_provider_context'; +function getRoundedChannel(value: number, tolerance: number): number { + return Math.round(value / tolerance) * tolerance; +} export async function CanvasElementProvider({ getService }: FtrProviderContext) { const { driver } = await getService('__webdriver__').init(); @@ -87,9 +91,9 @@ export async function CanvasElementProvider({ getService }: FtrProviderContext) const colors: string[] = []; for (let i = 0; i < imageData.length; i += 4) { // uses d3's `rgb` method create a color object, `toString()` returns the hex value - const r = imageData[i]; - const g = imageData[i + 1]; - const b = imageData[i + 2]; + const r = getRoundedChannel(imageData[i], channelTolerance); + const g = getRoundedChannel(imageData[i + 1], channelTolerance); + const b = getRoundedChannel(imageData[i + 2], channelTolerance); const color = rgb(r, g, b).toString().toUpperCase(); if (exclude === undefined || !exclude.includes(color)) colors.push(color); } @@ -196,20 +200,13 @@ export async function CanvasElementProvider({ getService }: FtrProviderContext) const actualRGB = rgb(actualColor); const expectedRGB = rgb(expectedColor); - const lowerR = expectedRGB.r - toleranceRange / 2; - const upperR = expectedRGB.r + toleranceRange / 2; - const lowerG = expectedRGB.g - toleranceRange / 2; - const upperG = expectedRGB.g + toleranceRange / 2; - const lowerB = expectedRGB.b - toleranceRange / 2; - const upperB = expectedRGB.b + toleranceRange / 2; - return ( - lowerR <= actualRGB.r && - upperR >= actualRGB.r && - lowerG <= actualRGB.g && - upperG >= actualRGB.g && - lowerB <= actualRGB.b && - upperB >= actualRGB.b + getRoundedChannel(expectedRGB.r, toleranceRange) === + getRoundedChannel(actualRGB.r, toleranceRange) && + getRoundedChannel(expectedRGB.g, toleranceRange) === + getRoundedChannel(actualRGB.g, toleranceRange) && + getRoundedChannel(expectedRGB.b, toleranceRange) === + getRoundedChannel(actualRGB.b, toleranceRange) ); } diff --git a/x-pack/test/reporting_api_integration/reporting_and_security.config.ts b/x-pack/test/reporting_api_integration/reporting_and_security.config.ts index b5f75ed7d501c..3574ee5b8b2b1 100644 --- a/x-pack/test/reporting_api_integration/reporting_and_security.config.ts +++ b/x-pack/test/reporting_api_integration/reporting_and_security.config.ts @@ -33,7 +33,7 @@ export default async function ({ readConfigFile }: FtrConfigProviderContext) { ...apiConfig.get('kbnTestServer'), serverArgs: [ ...apiConfig.get('kbnTestServer.serverArgs'), - `--xpack.reporting.capture.networkPolicy.rules=${JSON.stringify(testPolicyRules)}`, + `--xpack.screenshotting.networkPolicy.rules=${JSON.stringify(testPolicyRules)}`, `--xpack.reporting.capture.maxAttempts=1`, `--xpack.reporting.csv.maxSizeBytes=6000`, '--xpack.reporting.roles.enabled=false', // Reporting access control is implemented by sub-feature application privileges diff --git a/x-pack/test/rule_registry/common/constants.ts b/x-pack/test/rule_registry/common/constants.ts new file mode 100644 index 0000000000000..55b632352ffc4 --- /dev/null +++ b/x-pack/test/rule_registry/common/constants.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export const APM_METRIC_INDEX_NAME = 'apm-8.0.0-transaction'; +export const MAX_POLLS = 10; +export const BULK_INDEX_DELAY = 1000; +export const INDEXING_DELAY = 5000; +export const ALERTS_TARGET_INDICES_URL = + '/api/observability/rules/alerts/dynamic_index_pattern?namespace=default®istrationContexts=observability.apm®istrationContexts='; diff --git a/x-pack/test/rule_registry/common/lib/helpers/cleanup_target_indices.ts b/x-pack/test/rule_registry/common/lib/helpers/cleanup_target_indices.ts new file mode 100644 index 0000000000000..a249c57d8c3dc --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/helpers/cleanup_target_indices.ts @@ -0,0 +1,30 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { GetService } from '../../types'; +import { User } from '../authentication/types'; +import { getAlertsTargetIndices } from './get_alerts_target_indices'; + +export const cleanupTargetIndices = async (getService: GetService, user: User, spaceId: string) => { + const es = getService('es'); + try { + const { body: targetIndices } = await getAlertsTargetIndices(getService, user, spaceId); + const aliasMap = await es.indices.getAlias({ + name: targetIndices, + allow_no_indices: true, + expand_wildcards: 'open', + }); + const indices = Object.keys(aliasMap); + expect(indices.length > 0).to.be(true); + return es.indices.delete({ index: indices }, { ignore: [404] }); + } catch (error) { + if (error.meta.statusCode !== 404) { + throw error; + } + } +}; diff --git a/x-pack/test/rule_registry/common/lib/helpers/create_alert.ts b/x-pack/test/rule_registry/common/lib/helpers/create_alert.ts new file mode 100644 index 0000000000000..40d43ac80d210 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/helpers/create_alert.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { User } from '../authentication/types'; +import { GetService, AlertDef } from '../../types'; +import { getSpaceUrlPrefix } from '../authentication/spaces'; + +export const createAlert = async ( + getService: GetService, + user: User, + spaceId: string, + alertDef: AlertDef +) => { + const supertest = getService('supertestWithoutAuth'); + const { body: response, status } = await supertest + .post(`${getSpaceUrlPrefix(spaceId)}/api/alerts/alert`) + .auth(user.username, user.password) + .send(alertDef) + .set('kbn-xsrf', 'foo'); + return { alert: response, status }; +}; diff --git a/x-pack/test/rule_registry/common/lib/helpers/create_apm_metric_index.ts b/x-pack/test/rule_registry/common/lib/helpers/create_apm_metric_index.ts new file mode 100644 index 0000000000000..fab0f79d5a15f --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/helpers/create_apm_metric_index.ts @@ -0,0 +1,70 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { APM_METRIC_INDEX_NAME } from '../../constants'; +import { GetService } from '../../types'; + +export const createApmMetricIndex = async (getService: GetService) => { + const es = getService('es'); + await es.indices.create({ + index: APM_METRIC_INDEX_NAME, + body: { + mappings: { + dynamic: 'strict', + properties: { + event: { + properties: { + outcome: { + type: 'keyword', + }, + }, + }, + processor: { + properties: { + event: { + type: 'keyword', + }, + }, + }, + observer: { + properties: { + version_major: { + type: 'byte', + }, + }, + }, + service: { + properties: { + name: { + type: 'keyword', + }, + environment: { + type: 'keyword', + }, + }, + }, + transaction: { + properties: { + type: { + type: 'keyword', + }, + duration: { + properties: { + histogram: { + type: 'histogram', + }, + }, + }, + }, + }, + '@timestamp': { + type: 'date', + }, + }, + }, + }, + }); +}; diff --git a/x-pack/test/rule_registry/common/lib/helpers/create_transaction_metric.ts b/x-pack/test/rule_registry/common/lib/helpers/create_transaction_metric.ts new file mode 100644 index 0000000000000..0675f919fbc4e --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/helpers/create_transaction_metric.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { merge } from 'lodash'; +import { INDEXING_DELAY } from '../../constants'; + +export const createTransactionMetric = (override: Record) => { + const now = Date.now(); + const time = now - INDEXING_DELAY; + + return merge( + { + '@timestamp': new Date(time).toISOString(), + service: { + name: 'opbeans-go', + }, + event: { + outcome: 'success', + }, + transaction: { + duration: { + histogram: { + values: [1000000], + counts: [1], + }, + }, + type: 'request', + }, + processor: { + event: 'metric', + }, + observer: { + version_major: 7, + }, + }, + override + ); +}; diff --git a/x-pack/test/rule_registry/common/lib/helpers/delete_alert.ts b/x-pack/test/rule_registry/common/lib/helpers/delete_alert.ts new file mode 100644 index 0000000000000..209b182a958c5 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/helpers/delete_alert.ts @@ -0,0 +1,49 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { APM_METRIC_INDEX_NAME } from '../../constants'; +import { GetService } from '../../types'; +import { getSpaceUrlPrefix } from '../authentication/spaces'; +import { User } from '../authentication/types'; +import { getAlertsTargetIndices } from './get_alerts_target_indices'; + +export const deleteAlert = async ( + getService: GetService, + user: User, + spaceId: string, + id: string | undefined +) => { + const es = getService('es'); + const supertest = getService('supertestWithoutAuth'); + const { body: targetIndices } = await getAlertsTargetIndices(getService, user, spaceId); + if (id) { + const { body, status } = await supertest + .delete(`${getSpaceUrlPrefix(spaceId)}/api/alerts/alert/${id}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'foo'); + + if (status >= 300) { + const error = new Error('Error deleting alert'); + Object.assign(error, { response: { body, status } }); + throw error; + } + } + + await es.deleteByQuery({ + index: targetIndices[0], + body: { + query: { + match_all: {}, + }, + }, + refresh: true, + }); + + await es.indices.delete({ + index: APM_METRIC_INDEX_NAME, + }); +}; diff --git a/x-pack/test/rule_registry/common/lib/helpers/get_alerts_target_indices.ts b/x-pack/test/rule_registry/common/lib/helpers/get_alerts_target_indices.ts new file mode 100644 index 0000000000000..c78f97f30de74 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/helpers/get_alerts_target_indices.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { ALERTS_TARGET_INDICES_URL } from '../../constants'; +import { GetService } from '../../types'; +import { User } from '../authentication/types'; +import { getSpaceUrlPrefix } from '../authentication/spaces'; + +export const getAlertsTargetIndices = async ( + getService: GetService, + user: User, + spaceId: string +) => { + const supertest = getService('supertestWithoutAuth'); + return supertest + .get(`${getSpaceUrlPrefix(spaceId)}${ALERTS_TARGET_INDICES_URL}`) + .auth(user.username, user.password) + .send() + .set('kbn-xsrf', 'foo'); +}; diff --git a/x-pack/test/rule_registry/common/lib/helpers/index.ts b/x-pack/test/rule_registry/common/lib/helpers/index.ts new file mode 100644 index 0000000000000..695f71021d5ab --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/helpers/index.ts @@ -0,0 +1,14 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +export * from './create_alert'; +export * from './create_apm_metric_index'; +export * from './create_transaction_metric'; +export * from './get_alerts_target_indices'; +export * from './wait_until_next_execution'; +export * from './cleanup_target_indices'; +export * from './delete_alert'; diff --git a/x-pack/test/rule_registry/common/lib/helpers/wait_until_next_execution.ts b/x-pack/test/rule_registry/common/lib/helpers/wait_until_next_execution.ts new file mode 100644 index 0000000000000..8bc325c4a6bb7 --- /dev/null +++ b/x-pack/test/rule_registry/common/lib/helpers/wait_until_next_execution.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { GetService } from '../../types'; +import { getAlertsTargetIndices } from './get_alerts_target_indices'; +import { BULK_INDEX_DELAY, MAX_POLLS } from '../../constants'; +import { Alert } from '../../../../../plugins/alerting/common'; +import { getSpaceUrlPrefix } from '../authentication/spaces'; +import { User } from '../authentication/types'; + +export async function waitUntilNextExecution( + getService: GetService, + user: User, + alert: Alert, + spaceId: string, + intervalInSeconds: number = 1, + count: number = 0 +): Promise { + const supertest = getService('supertestWithoutAuth'); + const es = getService('es'); + + await new Promise((resolve) => { + setTimeout(resolve, intervalInSeconds * 1000); + }); + + const { body, status } = await supertest + .get(`${getSpaceUrlPrefix(spaceId)}/api/alerts/alert/${alert.id}`) + .auth(user.username, user.password) + .set('kbn-xsrf', 'foo'); + + const { body: targetIndices, status: targetIndicesStatus } = await getAlertsTargetIndices( + getService, + user, + spaceId + ); + if (targetIndices.length === 0) { + const error = new Error('Error getting target indices'); + Object.assign(error, { response: { body: targetIndices, status: targetIndicesStatus } }); + throw error; + } + + if (status >= 300) { + const error = new Error('Error getting alert'); + Object.assign(error, { response: { body, status } }); + throw error; + } + + const nextAlert = body as Alert; + + if (nextAlert.executionStatus.lastExecutionDate !== alert.executionStatus.lastExecutionDate) { + await new Promise((resolve) => { + setTimeout(resolve, BULK_INDEX_DELAY); + }); + + /** + * When calling refresh on an index pattern .alerts-observability.apm.alerts* (as was originally the hard-coded string in this test) + * The response from Elasticsearch is a 200, even if no indices which match that index pattern have been created. + * When calling refresh on a concrete index alias .alerts-observability.apm.alerts-default for instance, + * we receive a 404 error index_not_found_exception when no indices have been created which match that alias (obviously). + * Since we are receiving a concrete index alias from the observability api instead of a kibana index pattern + * and we understand / expect that this index does not exist at certain points of the test, we can try-catch at certain points without caring if the call fails. + * There are points in the code where we do want to ensure we get the appropriate error message back + */ + try { + await es.indices.refresh({ + index: targetIndices[0], + }); + // eslint-disable-next-line no-empty + } catch (exc) {} + return nextAlert; + } + + if (count >= MAX_POLLS) { + throw new Error('Maximum number of polls exceeded'); + } + + return waitUntilNextExecution(getService, user, alert, spaceId, intervalInSeconds, count + 1); +} diff --git a/x-pack/test/rule_registry/common/types.ts b/x-pack/test/rule_registry/common/types.ts new file mode 100644 index 0000000000000..63d45b0d850d9 --- /dev/null +++ b/x-pack/test/rule_registry/common/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ +import { GenericFtrProviderContext } from '@kbn/test'; +import { Alert, AlertTypeParams } from '../../../plugins/alerting/common'; +import { services } from './services'; + +export type GetService = GenericFtrProviderContext['getService']; + +export interface AlertParams extends AlertTypeParams { + windowSize?: number; + windowUnit?: string; + threshold?: number; + serviceName?: string; + transactionType?: string; + environment?: string; +} + +export type AlertDef = Partial>; diff --git a/x-pack/test/rule_registry/spaces_only/config_basic.ts b/x-pack/test/rule_registry/spaces_only/config_basic.ts new file mode 100644 index 0000000000000..5a2ee4c1c1178 --- /dev/null +++ b/x-pack/test/rule_registry/spaces_only/config_basic.ts @@ -0,0 +1,16 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { createTestConfig } from '../common/config'; + +// eslint-disable-next-line import/no-default-export +export default createTestConfig('spaces_only', { + license: 'basic', + disabledPlugins: ['security'], + ssl: false, + testFiles: [require.resolve('./tests/basic')], +}); diff --git a/x-pack/test/rule_registry/spaces_only/tests/basic/bootstrap.ts b/x-pack/test/rule_registry/spaces_only/tests/basic/bootstrap.ts new file mode 100644 index 0000000000000..ccae5189f6d30 --- /dev/null +++ b/x-pack/test/rule_registry/spaces_only/tests/basic/bootstrap.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import type { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { obsOnlyRead } from '../../../common/lib/authentication/users'; +import { getAlertsTargetIndices } from '../../../common/lib/helpers'; + +// eslint-disable-next-line import/no-default-export +export default function registryRulesApiTest({ getService }: FtrProviderContext) { + const es = getService('es'); + + describe('Rule Registry API', () => { + describe('with read permissions', () => { + it('does not bootstrap the apm rule indices', async () => { + const { body: targetIndices } = await getAlertsTargetIndices( + getService, + obsOnlyRead, + 'space1' + ); + const errorOrUndefined = await es.indices + .get({ + index: targetIndices[0], + expand_wildcards: 'open', + allow_no_indices: false, + }) + .then(() => {}) + .catch((error) => { + return error.toString(); + }); + + expect(errorOrUndefined).not.to.be(undefined); + + expect(errorOrUndefined).to.contain('index_not_found_exception'); + }); + }); + }); +} diff --git a/x-pack/test/rule_registry/spaces_only/tests/basic/index.ts b/x-pack/test/rule_registry/spaces_only/tests/basic/index.ts new file mode 100644 index 0000000000000..aeb2b085ad379 --- /dev/null +++ b/x-pack/test/rule_registry/spaces_only/tests/basic/index.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { createSpaces, deleteSpaces } from '../../../common/lib/authentication'; + +// eslint-disable-next-line import/no-default-export +export default ({ loadTestFile, getService }: FtrProviderContext): void => { + describe('rule registry spaces only: trial', function () { + // Fastest ciGroup for the moment. + this.tags('ciGroup5'); + + before(async () => { + await createSpaces(getService); + }); + + after(async () => { + await deleteSpaces(getService); + }); + + // Basic + loadTestFile(require.resolve('./bootstrap')); + }); +}; diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/__snapshots__/create_rule.snap b/x-pack/test/rule_registry/spaces_only/tests/trial/__snapshots__/create_rule.snap new file mode 100644 index 0000000000000..cf0b942faa5f8 --- /dev/null +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/__snapshots__/create_rule.snap @@ -0,0 +1,124 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`rule registry spaces only: trial Rule Registry API with write permissions when creating a rule writes alerts data to the alert indices 1`] = ` +Object { + "event.action": Array [ + "open", + ], + "event.kind": Array [ + "signal", + ], + "kibana.alert.duration.us": Array [ + 0, + ], + "kibana.alert.evaluation.threshold": Array [ + 30, + ], + "kibana.alert.evaluation.value": Array [ + 50, + ], + "kibana.alert.instance.id": Array [ + "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", + ], + "kibana.alert.reason": Array [ + "Failed transactions rate is greater than 30% (current value is 50%) for opbeans-go", + ], + "kibana.alert.rule.category": Array [ + "Failed transaction rate threshold", + ], + "kibana.alert.rule.consumer": Array [ + "apm", + ], + "kibana.alert.rule.name": Array [ + "Failed transaction rate threshold | opbeans-go", + ], + "kibana.alert.rule.producer": Array [ + "apm", + ], + "kibana.alert.rule.rule_type_id": Array [ + "apm.transaction_error_rate", + ], + "kibana.alert.status": Array [ + "active", + ], + "kibana.alert.workflow_status": Array [ + "open", + ], + "kibana.space_ids": Array [ + "space1", + ], + "processor.event": Array [ + "transaction", + ], + "service.name": Array [ + "opbeans-go", + ], + "tags": Array [ + "apm", + "service.name:opbeans-go", + ], + "transaction.type": Array [ + "request", + ], +} +`; + +exports[`rule registry spaces only: trial Rule Registry API with write permissions when creating a rule writes alerts data to the alert indices 2`] = ` +Object { + "event.action": Array [ + "close", + ], + "event.kind": Array [ + "signal", + ], + "kibana.alert.evaluation.threshold": Array [ + 30, + ], + "kibana.alert.evaluation.value": Array [ + 50, + ], + "kibana.alert.instance.id": Array [ + "apm.transaction_error_rate_opbeans-go_request_ENVIRONMENT_NOT_DEFINED", + ], + "kibana.alert.reason": Array [ + "Failed transactions rate is greater than 30% (current value is 50%) for opbeans-go", + ], + "kibana.alert.rule.category": Array [ + "Failed transaction rate threshold", + ], + "kibana.alert.rule.consumer": Array [ + "apm", + ], + "kibana.alert.rule.name": Array [ + "Failed transaction rate threshold | opbeans-go", + ], + "kibana.alert.rule.producer": Array [ + "apm", + ], + "kibana.alert.rule.rule_type_id": Array [ + "apm.transaction_error_rate", + ], + "kibana.alert.status": Array [ + "recovered", + ], + "kibana.alert.workflow_status": Array [ + "open", + ], + "kibana.space_ids": Array [ + "space1", + ], + "processor.event": Array [ + "transaction", + ], + "service.name": Array [ + "opbeans-go", + ], + "tags": Array [ + "apm", + "service.name:opbeans-go", + ], + "transaction.type": Array [ + "request", + ], +} +`; diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/create_rule.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/create_rule.ts new file mode 100644 index 0000000000000..ac36bad1f595b --- /dev/null +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/create_rule.ts @@ -0,0 +1,252 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import expect from '@kbn/expect'; +import { + ALERT_DURATION, + ALERT_END, + ALERT_RULE_UUID, + ALERT_START, + ALERT_STATUS, + ALERT_UUID, + EVENT_KIND, + VERSION, +} from '@kbn/rule-data-utils'; +import { omit } from 'lodash'; +import type { FtrProviderContext } from '../../../common/ftr_provider_context'; +import { + getAlertsTargetIndices, + createApmMetricIndex, + createAlert, + waitUntilNextExecution, + createTransactionMetric, + cleanupTargetIndices, + deleteAlert, +} from '../../../common/lib/helpers'; +import { AlertDef, AlertParams } from '../../../common/types'; +import { Alert } from '../../../../../plugins/alerting/common'; +import { APM_METRIC_INDEX_NAME } from '../../../common/constants'; +import { obsOnly } from '../../../common/lib/authentication/users'; + +const SPACE_ID = 'space1'; + +// eslint-disable-next-line import/no-default-export +export default function registryRulesApiTest({ getService }: FtrProviderContext) { + const es = getService('es'); + + describe('Rule Registry API', () => { + describe('with write permissions', () => { + it('does not bootstrap indices on plugin startup', async () => { + const { body: targetIndices } = await getAlertsTargetIndices(getService, obsOnly, SPACE_ID); + try { + const res = await es.indices.get({ + index: targetIndices[0], + expand_wildcards: 'open', + allow_no_indices: true, + }); + expect(res).to.be.empty(); + } catch (exc) { + expect(exc.statusCode).to.eql(404); + } + }); + + describe('when creating a rule', () => { + let createResponse: { + alert: Alert; + status: number; + }; + before(async () => { + await createApmMetricIndex(getService); + const alertDef: AlertDef = { + params: { + threshold: 30, + windowSize: 5, + windowUnit: 'm', + transactionType: 'request', + environment: 'ENVIRONMENT_ALL', + serviceName: 'opbeans-go', + }, + consumer: 'apm', + alertTypeId: 'apm.transaction_error_rate', + schedule: { interval: '5s' }, + actions: [], + tags: ['apm', 'service.name:opbeans-go'], + notifyWhen: 'onActionGroupChange', + name: 'Failed transaction rate threshold | opbeans-go', + }; + createResponse = await createAlert(getService, obsOnly, SPACE_ID, alertDef); + }); + after(async () => { + await deleteAlert(getService, obsOnly, SPACE_ID, createResponse.alert.id); + await cleanupTargetIndices(getService, obsOnly, SPACE_ID); + }); + + it('writes alerts data to the alert indices', async () => { + expect(createResponse.status).to.be.below(299); + + expect(createResponse.alert).not.to.be(undefined); + let alert = await waitUntilNextExecution( + getService, + obsOnly, + createResponse.alert, + SPACE_ID + ); + + const { body: targetIndices } = await getAlertsTargetIndices( + getService, + obsOnly, + SPACE_ID + ); + + try { + const res = await es.search({ + index: targetIndices[0], + body: { + query: { + term: { + [EVENT_KIND]: 'signal', + }, + }, + size: 1, + sort: { + '@timestamp': 'desc', + }, + }, + }); + expect(res).to.be.empty(); + } catch (exc) { + expect(exc.message).contain('index_not_found_exception'); + } + + await es.index({ + index: APM_METRIC_INDEX_NAME, + body: createTransactionMetric({ + event: { + outcome: 'success', + }, + }), + refresh: true, + }); + + alert = await waitUntilNextExecution(getService, obsOnly, alert, SPACE_ID); + + try { + const res = await es.search({ + index: targetIndices[0], + body: { + query: { + term: { + [EVENT_KIND]: 'signal', + }, + }, + size: 1, + sort: { + '@timestamp': 'desc', + }, + }, + }); + expect(res).to.be.empty(); + } catch (exc) { + expect(exc.message).contain('index_not_found_exception'); + } + + await es.index({ + index: APM_METRIC_INDEX_NAME, + body: createTransactionMetric({ + event: { + outcome: 'failure', + }, + }), + refresh: true, + }); + + alert = await waitUntilNextExecution(getService, obsOnly, alert, SPACE_ID); + + const afterViolatingDataResponse = await es.search({ + index: targetIndices[0], + body: { + query: { + term: { + [EVENT_KIND]: 'signal', + }, + }, + size: 1, + sort: { + '@timestamp': 'desc', + }, + _source: false, + fields: [{ field: '*', include_unmapped: true }], + }, + }); + + expect(afterViolatingDataResponse.hits.hits.length).to.be(1); + + const alertEvent = afterViolatingDataResponse.hits.hits[0].fields as Record; + + const exclude = ['@timestamp', ALERT_START, ALERT_UUID, ALERT_RULE_UUID, VERSION]; + + const toCompare = omit(alertEvent, exclude); + + expectSnapshot(toCompare).toMatch(); + + await es.bulk({ + index: APM_METRIC_INDEX_NAME, + body: [ + { index: {} }, + createTransactionMetric({ + event: { + outcome: 'success', + }, + }), + { index: {} }, + createTransactionMetric({ + event: { + outcome: 'success', + }, + }), + ], + refresh: true, + }); + + alert = await waitUntilNextExecution(getService, obsOnly, alert, SPACE_ID); + + const afterRecoveryResponse = await es.search({ + index: targetIndices[0], + body: { + query: { + term: { + [EVENT_KIND]: 'signal', + }, + }, + size: 1, + sort: { + '@timestamp': 'desc', + }, + _source: false, + fields: [{ field: '*', include_unmapped: true }], + }, + }); + + expect(afterRecoveryResponse.hits.hits.length).to.be(1); + + const recoveredAlertEvent = afterRecoveryResponse.hits.hits[0].fields as Record< + string, + any + >; + + expect(recoveredAlertEvent[ALERT_STATUS]?.[0]).to.eql('recovered'); + expect(recoveredAlertEvent[ALERT_DURATION]?.[0]).to.be.greaterThan(0); + expect(new Date(recoveredAlertEvent[ALERT_END]?.[0]).getTime()).to.be.greaterThan(0); + + expectSnapshot( + omit(recoveredAlertEvent, exclude.concat([ALERT_DURATION, ALERT_END])) + ).toMatch(); + }); + }); + }); + }); +} diff --git a/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts b/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts index 6deba4c68d0e2..c8fc677eb0670 100644 --- a/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts +++ b/x-pack/test/rule_registry/spaces_only/tests/trial/index.ts @@ -22,8 +22,9 @@ export default ({ loadTestFile, getService }: FtrProviderContext): void => { await deleteSpaces(getService); }); - // Basic + // Trial loadTestFile(require.resolve('./get_alert_by_id')); loadTestFile(require.resolve('./update_alert')); + loadTestFile(require.resolve('./create_rule')); }); }; diff --git a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts index 76e3cb2248815..d6f47b06f4612 100644 --- a/x-pack/test/security_api_integration/tests/pki/pki_auth.ts +++ b/x-pack/test/security_api_integration/tests/pki/pki_auth.ts @@ -150,7 +150,7 @@ export default function ({ getService }: FtrProviderContext) { enabled: true, metadata: { pki_delegated_by_realm: 'reserved', - pki_delegated_by_user: 'kibana', + pki_delegated_by_user: 'kibana_system', pki_dn: 'CN=first_client', }, authentication_realm: { name: 'pki1', type: 'pki' }, @@ -187,7 +187,7 @@ export default function ({ getService }: FtrProviderContext) { enabled: true, metadata: { pki_delegated_by_realm: 'reserved', - pki_delegated_by_user: 'kibana', + pki_delegated_by_user: 'kibana_system', pki_dn: 'CN=second_client', }, authentication_realm: { name: 'pki1', type: 'pki' }, diff --git a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json index e42a13ab8d8a8..e6187d1f7e0a6 100644 --- a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/data.json @@ -5,17 +5,18 @@ "index":"ml_host_risk_score_latest_default", "source":{ "@timestamp":"2021-03-10T14:51:05.766Z", - "risk_score":21, + "risk_stats": { + "risk_score": 21, + "rule_risks": [ + { + "rule_name": "Unusual Linux Username", + "rule_risk": 42 + } + ] + }, "host":{ "name":"ip-10-10-10-121" }, - "rules":{ - "Unusual Linux Username":{ - "average_risk":21, - "rule_count":2, - "rule_risk":42 - } - }, "ingest_timestamp":"2021-03-09T18:02:08.319296053Z", "risk":"Low" } diff --git a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/mappings.json b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/mappings.json index f71c9cf8ed4c2..2738d85d8b3af 100644 --- a/x-pack/test/security_solution_cypress/es_archives/risky_hosts/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/risky_hosts/mappings.json @@ -32,8 +32,12 @@ } } }, - "risk_score": { - "type": "long" + "risk_stats": { + "properties": { + "risk_score": { + "type": "long" + } + } } } }, diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json index a2e0c2d2921dc..ec5e2aae6e2e2 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/data.json @@ -31,6 +31,9 @@ } }, "type": "file" + }, + "feed": { + "name": "AbuseCH malware" } }, "abusemalware": { @@ -72,4 +75,4 @@ } } } -} +} \ No newline at end of file diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json index 8840cd4bee0dd..bc5f6e3db9169 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator/mappings.json @@ -796,6 +796,14 @@ "type": "keyword" } } + }, + "feed":{ + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } } } } diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json index 1a8d3ff5a309a..8efc704a63ea8 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/data.json @@ -29,6 +29,9 @@ "link": "https://www.virustotal.com/gui/file/a04ac6d98ad989312783d4fe3456c53730b212c79a426fb215708b6c6daa3de3/detection/f-a04ac6d", "percent": "62.30" } + }, + "feed": { + "name": "feed_name" } }, "tags": ["threatintel-abusemalware", "forwarded"], diff --git a/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json index cba4263f32b69..412cefe061372 100644 --- a/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json +++ b/x-pack/test/security_solution_cypress/es_archives/threat_indicator2/mappings.json @@ -796,6 +796,14 @@ "type": "keyword" } } + }, + "feed": { + "properties": { + "name": { + "ignore_above": 1024, + "type": "keyword" + } + } } } } 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 dc4b4113c6b11..63e9430679f80 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 @@ -119,10 +119,12 @@ export async function startTransform( const transformsResponse = await client.transform.getTransform({ transform_id: `${transformId}*`, }); - return transformsResponse.transforms.map((transform) => { - const t = transform as unknown as { id: string }; - return client.transform.startTransform({ transform_id: t.id }); - }); + return Promise.all( + transformsResponse.transforms.map((transform) => { + const t = transform as unknown as { id: string }; + return client.transform.startTransform({ transform_id: t.id }); + }) + ); } export function bulkIndex( 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 93f3756fc111c..b0aaf71ef3257 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 @@ -7,6 +7,7 @@ import uuid from 'uuid'; import expect from '@kbn/expect'; +import { TransformGetTransformStatsTransformStats } from '@elastic/elasticsearch/lib/api/types'; import { FtrProviderContext } from '../ftr_provider_context'; import { deleteAllDocsFromMetadataCurrentIndex, @@ -24,10 +25,13 @@ import { HOST_METADATA_LIST_ROUTE, METADATA_UNITED_INDEX, METADATA_UNITED_TRANSFORM, + METADATA_TRANSFORMS_STATUS_ROUTE, + metadataTransformPrefix, } from '../../../plugins/security_solution/common/endpoint/constants'; import { AGENTS_INDEX } from '../../../plugins/fleet/common'; import { generateAgentDocs, generateMetadataDocs } from './metadata.fixtures'; import { indexFleetEndpointPolicy } from '../../../plugins/security_solution/common/endpoint/data_loaders/index_fleet_endpoint_policy'; +import { TRANSFORM_STATES } from '../../../plugins/security_solution/common/constants'; export default function ({ getService }: FtrProviderContext) { const supertest = getService('supertest'); @@ -71,7 +75,6 @@ export default function ({ getService }: FtrProviderContext) { await deleteAllDocsFromFleetAgents(getService); await deleteAllDocsFromMetadataDatastream(getService); await deleteAllDocsFromMetadataCurrentIndex(getService); - await stopTransform(getService, `${METADATA_UNITED_TRANSFORM}*`); await deleteAllDocsFromIndex(getService, METADATA_UNITED_INDEX); }); @@ -504,5 +507,62 @@ export default function ({ getService }: FtrProviderContext) { }); }); }); + + describe('get metadata transforms', () => { + it('should respond forbidden if no fleet access', async () => { + await getService('supertestWithoutAuth') + .get(METADATA_TRANSFORMS_STATUS_ROUTE) + .set('kbn-xsrf', 'xxx') + .expect(401); + }); + + it('correctly returns stopped transform stats', async () => { + await stopTransform(getService, `${metadataTransformPrefix}*`); + await stopTransform(getService, `${METADATA_UNITED_TRANSFORM}*`); + + const { body } = await supertest + .get(METADATA_TRANSFORMS_STATUS_ROUTE) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body.count).to.eql(2); + + const transforms: TransformGetTransformStatsTransformStats[] = body.transforms.sort( + ( + a: TransformGetTransformStatsTransformStats, + b: TransformGetTransformStatsTransformStats + ) => a.id > b.id + ); + + expect(transforms[0].id).to.contain(metadataTransformPrefix); + expect(transforms[0].state).to.eql(TRANSFORM_STATES.STOPPED); + expect(transforms[1].id).to.contain(METADATA_UNITED_TRANSFORM); + expect(transforms[1].state).to.eql(TRANSFORM_STATES.STOPPED); + + await startTransform(getService, metadataTransformPrefix); + await startTransform(getService, METADATA_UNITED_TRANSFORM); + }); + + it('correctly returns started transform stats', async () => { + const { body } = await supertest + .get(METADATA_TRANSFORMS_STATUS_ROUTE) + .set('kbn-xsrf', 'xxx') + .expect(200); + + expect(body.count).to.eql(2); + + const transforms: TransformGetTransformStatsTransformStats[] = body.transforms.sort( + ( + a: TransformGetTransformStatsTransformStats, + b: TransformGetTransformStatsTransformStats + ) => a.id > b.id + ); + + expect(transforms[0].id).to.contain(metadataTransformPrefix); + expect(transforms[0].state).to.eql(TRANSFORM_STATES.STARTED); + expect(transforms[1].id).to.contain(METADATA_UNITED_TRANSFORM); + expect(transforms[1].state).to.eql(TRANSFORM_STATES.STARTED); + }); + }); }); } diff --git a/yarn.lock b/yarn.lock index a7b3a6bed4f57..68eb7893fb00f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -137,7 +137,7 @@ jsesc "^2.5.1" source-map "^0.5.0" -"@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.15.4", "@babel/helper-annotate-as-pure@^7.16.0": +"@babel/helper-annotate-as-pure@^7.0.0", "@babel/helper-annotate-as-pure@^7.16.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.16.0.tgz#9a1f0ebcda53d9a2d00108c4ceace6a5d5f1f08d" integrity sha512-ItmYF9vR4zA8cByDocY05o0LGUkp1zhbTQOH1NFyl5xXEqlTJQCEJjieriw+aFpxo16swMxUnUiKS7a/r4vtHg== @@ -247,7 +247,7 @@ dependencies: "@babel/types" "^7.16.0" -"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.15.4", "@babel/helper-module-imports@^7.16.0", "@babel/helper-module-imports@^7.7.0": +"@babel/helper-module-imports@^7.0.0", "@babel/helper-module-imports@^7.12.13", "@babel/helper-module-imports@^7.16.0", "@babel/helper-module-imports@^7.7.0": version "7.16.0" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.0.tgz#90538e60b672ecf1b448f5f4f5433d37e79a3ec3" integrity sha512-kkH7sWzKPq0xt3H1n+ghb4xEMP8k0U7XV3kkB+ZGy69kDk2ySFW1qPi06sjKzFY3t1j6XbJSqr4mF9L7CYVyhg== @@ -1527,10 +1527,10 @@ dependencies: object-hash "^1.3.0" -"@elastic/charts@40.0.0": - version "40.0.0" - resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-40.0.0.tgz#aa79a34c160086bff3a4ade5f6c48bfdf4c7eab5" - integrity sha512-81gq7/loJO5znr3jUxKKUiXPvOFewsdFAGI/yIsbJJCMJ+1MR63A8V1h4ZzqVYoVne4S4btl/beIx0s/JFN+aw== +"@elastic/charts@40.1.0": + version "40.1.0" + resolved "https://registry.yarnpkg.com/@elastic/charts/-/charts-40.1.0.tgz#2cb19709ad6fb70ac5f96556bbccee2205ec0600" + integrity sha512-t3BCeSdILVKM+iXboTXD4y8mqrNnLUzlX/t6+NDjx2eBcAcfJ4Rr4FtAJBr16YtHlQZNxoFEMHAiRnBpwhxv8A== dependencies: "@popperjs/core" "^2.4.0" chroma-js "^2.1.0" @@ -5502,11 +5502,6 @@ resolved "https://registry.yarnpkg.com/@types/geojson/-/geojson-7946.0.7.tgz#c8fa532b60a0042219cdf173ca21a975ef0666ad" integrity sha512-wE2v81i4C4Ol09RtsWFAqg3BUitWbHSpSlIo+bNdsCJijO9sjme+zm+73ZMCa/qMC8UEERxzGbvmr1cffo2SiQ== -"@types/getopts@^2.0.1": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/getopts/-/getopts-2.0.1.tgz#b7e5478fe7571838b45aff736a59ab69b8bcda18" - integrity sha512-JsQJHtzLYKunMz7acYOX6x5IJ/42CsjjHFfLCmis1Hn/qFoD/y0kJFUIAg8HoDigQpKUcWj55d8rxZQBnvz1Pw== - "@types/getos@^3.0.0": version "3.0.0" resolved "https://registry.yarnpkg.com/@types/getos/-/getos-3.0.0.tgz#582c758e99e9d634f31f471faf7ce59cf1c39a71" @@ -5851,6 +5846,10 @@ version "0.0.0" uid "" +"@types/kbn__docs-utils@link:bazel-bin/packages/kbn-docs-utils/npm_module_types": + version "0.0.0" + uid "" + "@types/kbn__i18n-react@link:bazel-bin/packages/kbn-i18n-react/npm_module_types": version "0.0.0" uid "" @@ -8335,13 +8334,13 @@ babel-plugin-require-context-hook@^1.0.0: babel-plugin-syntax-jsx "^6.18.0" lodash "^4.17.11" -babel-plugin-styled-components@^1.13.3: - version "1.13.3" - resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-1.13.3.tgz#1f1cb3927d4afa1e324695c78f690900e3d075bc" - integrity sha512-meGStRGv+VuKA/q0/jXxrPNWEm4LPfYIqxooDTdmh8kFsP/Ph7jJG5rUPwUPX3QHUvggwdbgdGpo88P/rRYsVw== +babel-plugin-styled-components@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/babel-plugin-styled-components/-/babel-plugin-styled-components-2.0.2.tgz#0fac11402dc9db73698b55847ab1dc73f5197c54" + integrity sha512-7eG5NE8rChnNTDxa6LQfynwgHTVOYYaHJbUYSlOhk8QBXIQiMBKq4gyfHBBKPrxUcVBXVJL61ihduCpCQbuNbw== dependencies: - "@babel/helper-annotate-as-pure" "^7.15.4" - "@babel/helper-module-imports" "^7.15.4" + "@babel/helper-annotate-as-pure" "^7.16.0" + "@babel/helper-module-imports" "^7.16.0" babel-plugin-syntax-jsx "^6.18.0" lodash "^4.17.11" @@ -8724,16 +8723,17 @@ broadcast-channel@^3.4.1: rimraf "3.0.2" unload "2.2.0" -broadcast-channel@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-4.5.0.tgz#d4717c493e219908fcb7f2f9078fe0baf95b77c1" - integrity sha512-jp+VPlQ1HyR0CM3uIYUrdpXupBvhTMFRkjR6mEmt5W4HaGDPFEzrO2Jqvi2PZ6zCC4zwLeco7CC5EUJPrVH8Tw== +broadcast-channel@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/broadcast-channel/-/broadcast-channel-4.7.0.tgz#4f5c31982f627eae4ffe463623ba36a9e7da1992" + integrity sha512-1C7wDPqeiKkwpScqFP044MsPAtxxDNKZzOnJmkHaTuOlUdaMLo11op56NrCOMiRh8dzktstcNsiHELGeTMKnNQ== dependencies: "@babel/runtime" "^7.16.0" detect-node "^2.1.0" microseconds "0.2.0" nano-time "1.0.0" oblivious-set "1.0.0" + p-queue "6.6.2" rimraf "3.0.2" unload "2.3.1" @@ -13334,7 +13334,7 @@ eventemitter2@^6.4.3: resolved "https://registry.yarnpkg.com/eventemitter2/-/eventemitter2-6.4.3.tgz#35c563619b13f3681e7eb05cbdaf50f56ba58820" integrity sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ== -eventemitter3@^4.0.0: +eventemitter3@^4.0.0, eventemitter3@^4.0.4: version "4.0.7" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== @@ -21121,6 +21121,14 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" +p-queue@6.6.2: + version "6.6.2" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + p-retry@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/p-retry/-/p-retry-3.0.1.tgz#316b4c8893e2c8dc1cfa891f406c4b422bebf328" @@ -21143,6 +21151,13 @@ p-timeout@^2.0.1: dependencies: p-finally "^1.0.0" +p-timeout@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" + integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== + dependencies: + p-finally "^1.0.0" + p-try@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3"